diff --git a/.env.example b/.env.example index 35ea12a885..df63e08e0e 100644 --- a/.env.example +++ b/.env.example @@ -19,4 +19,13 @@ FORWARDED_ALLOW_IPS='*' # DO NOT TRACK SCARF_NO_ANALYTICS=true DO_NOT_TRACK=true -ANONYMIZED_TELEMETRY=false \ No newline at end of file +ANONYMIZED_TELEMETRY=false + +# Valkey Vector Store (requires VECTOR_DB=valkey) +# VALKEY_URL='valkey://localhost:6379' +# VALKEY_COLLECTION_PREFIX='open_webui' +# VALKEY_INDEX_TYPE='HNSW' +# VALKEY_DISTANCE_METRIC='COSINE' +# VALKEY_HNSW_M='16' +# VALKEY_HNSW_EF_CONSTRUCTION='200' +# VALKEY_HNSW_EF_RUNTIME='10' diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 05dc6cfa94..d5f3a86132 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,82 +1,76 @@ name: Feature Request -description: Suggest an idea for this project +description: Suggest a new feature or improvement title: 'feat: ' labels: ['triage'] body: - type: markdown attributes: value: | - ## Important Notes - ### Before submitting - - Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted. - It's likely we're already tracking it! If you’re unsure, start a discussion post first. + ## Before Submitting - #### Scope + Please check **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) for similar requests. If you find one, add your input there instead. - If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead. - **We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.** - This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers. - Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions). - - If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it. - - This will help us efficiently focus on improving the project. - - ### Collaborate respectfully - We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**. + ### Scope Guidelines - Remember: - - Open WebUI is a **volunteer-driven project** - - It's managed by a **single maintainer** - - It's supported by contributors who also have **full-time jobs** + Feature requests that require significant implementation effort should be posted in the **Ideas** section of [Discussions](https://github.com/open-webui/open-webui/discussions) instead. We will move oversized feature requests to Discussions to keep the Issues tab focused on actionable items. - We appreciate your time and ask that you **respect ours**. + If your request might impact the broader community, please open a Discussion first so others can weigh in on the design. + + ### Be Respectful + + Open WebUI is a volunteer-driven project maintained by a small team. We value constructive, positive communication. Please be mindful of maintainers' time and energy. ### Contributing - If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI. - ### Bug reproducibility - If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help! + If you encounter an issue, we encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout and maintain project quality. + + ### Reproducibility + + If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may be moved to the "Issues" section in Discussions for community assistance. - type: checkboxes id: existing-issue attributes: label: Check Existing Issues - description: Please confirm that you've checked for existing similar requests + description: Confirm you have searched for similar requests. options: - - label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request. + - label: I have searched all existing **open AND closed** issues and discussions and found none comparable to my request. required: true + - type: checkboxes id: feature-scope attributes: label: Verify Feature Scope - description: Please confirm the feature's scope is within the described scope + description: Confirm this request belongs in Issues rather than Discussions. options: - - label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions. + - label: I believe this feature request is appropriately scoped for the Issues section as described above. required: true + - type: textarea id: problem-description attributes: label: Problem Description - description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is. - placeholder: "Ex. I'm always frustrated when... / Not related to a problem" + description: Is this related to a problem? Describe the pain point clearly. + placeholder: "e.g., I'm frustrated when..." validations: required: true + - type: textarea id: solution-description attributes: - label: Desired Solution you'd like - description: Clearly describe what you want to happen. + label: Proposed Solution + description: Describe what you would like to happen. validations: required: true + - type: textarea id: alternatives-considered attributes: label: Alternatives Considered - description: A clear and concise description of any alternative solutions or features you've considered. + description: Describe any alternative solutions or workarounds you have considered. + - type: textarea id: additional-context attributes: label: Additional Context - description: Add any other context or screenshots about the feature request here. + description: Add any other context, mockups, or screenshots about the feature request. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1c83fd305b..5998b3dc96 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,17 +4,22 @@ updates: directory: '/' schedule: interval: monthly - target-branch: 'dev' + target-branch: dev - package-ecosystem: pip directory: '/backend' schedule: interval: monthly - target-branch: 'dev' + target-branch: dev - - package-ecosystem: 'github-actions' + - package-ecosystem: github-actions directory: '/' schedule: - # Check for updates to GitHub Actions every week interval: monthly - target-branch: 'dev' + target-branch: dev + + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + target-branch: dev diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 75b9359168..957f5a3768 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,82 +1,104 @@ + + # Pull Request Checklist ### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request. This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR. + + **Before submitting, make sure you've checked the following:** -- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **Not targeting the `dev` branch will lead to immediate closure of the PR.** -- [ ] **Description:** Provide a concise description of the changes made in this pull request down below. -- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. -- [ ] **Documentation:** If necessary, update relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs) like environment variables, the tutorials, or other documentation sources. -- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation? -- [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Take this as an opportunity to **make screenshots of the feature/fix and include it in the PR description**. -- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR. -- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? -- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following: - - **BREAKING CHANGE**: Significant changes that may affect compatibility - - **build**: Changes that affect the build system or external dependencies - - **ci**: Changes to our continuous integration processes or workflows - - **chore**: Refactor, cleanup, or other non-functional code changes - - **docs**: Documentation update or addition - - **feat**: Introduces a new feature or enhancement to the codebase - - **fix**: Bug fix or error correction +- [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review. +- [ ] **Target branch:** The pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.** +- [ ] **Description:** A concise description of the changes is provided below. +- [ ] **Changelog:** A changelog entry following [Keep a Changelog](https://keepachangelog.com/) format is included at the bottom. +- [ ] **Documentation:** Relevant documentation has been added or updated in the [Open WebUI Docs Repository](https://github.com/open-webui/docs). +- [ ] **Dependencies:** Any new or updated dependencies are explained, tested, and documented. +- [ ] **Testing:** Manual tests have been performed to verify the fix/feature works correctly and does not introduce regressions. Screenshots or recordings are included where applicable. +- [ ] **No Unchecked AI Code:** This PR is either human-written or has undergone thorough human review AND manual testing. Unreviewed AI-generated PRs may be closed immediately. +- [ ] **Self-Review:** A self-review of the code has been performed, ensuring adherence to project coding standards. +- [ ] **Architecture:** Smart defaults are preferred over new settings. Local state is used for ephemeral UI logic. Major architectural or UX changes have been discussed first. +- [ ] **Git Hygiene:** The PR is atomic (one logical change), rebased on `dev`, and contains no unrelated commits. +- [ ] **Title Prefix:** The PR title uses one of the following prefixes: + - **BREAKING CHANGE**: Changes affecting backward compatibility + - **build**: Build system or dependency changes + - **ci**: CI/CD workflow changes + - **chore**: Refactoring, cleanup, or non-functional changes + - **docs**: Documentation additions or updates + - **feat**: New features or enhancements + - **fix**: Bug fixes or corrections - **i18n**: Internationalization or localization changes - - **perf**: Performance improvement - - **refactor**: Code restructuring for better maintainability, readability, or scalability - - **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.) - - **test**: Adding missing tests or correcting existing tests - - **WIP**: Work in progress, a temporary label for incomplete or ongoing work + - **perf**: Performance improvements + - **refactor**: Code restructuring + - **style**: Formatting changes (whitespace, semicolons, etc.) + - **test**: Test additions or corrections + - **WIP**: Work in progress # Changelog Entry ### Description -- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)] +- [Describe the changes, including motivation and impact] ### Added -- [List any new features, functionalities, or additions] +- [New features, functionalities, or additions] ### Changed -- [List any changes, updates, refactorings, or optimizations] +- [Changes, updates, refactorings, or optimizations] ### Deprecated -- [List any deprecated functionality or features that have been removed] +- [Deprecated functionality or features] ### Removed -- [List any removed features, files, or functionalities] +- [Removed features, files, or functionalities] ### Fixed -- [List any fixes, corrections, or bug fixes] +- [Bug fixes or corrections] ### Security -- [List any new or updated security-related changes, including vulnerability fixes] +- [Security-related changes or vulnerability fixes] ### Breaking Changes -- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality] +- **BREAKING CHANGE**: [Changes affecting compatibility or functionality] --- ### Additional Information -- [Insert any additional context, notes, or explanations for the changes] - - [Reference any related issues, commits, or other relevant information] +- [Any additional context, notes, or references to related issues/commits] ### Screenshots or Videos -- [Attach any relevant screenshots or videos demonstrating the changes] +- [Attach relevant screenshots or videos demonstrating the changes] ### Contributor License Agreement -By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms. + + +- [ ] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms. > [!NOTE] > Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in. diff --git a/.github/workflows/backend.yaml b/.github/workflows/backend.yaml new file mode 100644 index 0000000000..877a6b0ecc --- /dev/null +++ b/.github/workflows/backend.yaml @@ -0,0 +1,40 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Backend CI — Python formatting checks via Ruff +# Runs on pushes and PRs to main/dev when backend files change +# ───────────────────────────────────────────────────────────────────────────── +name: Python CI + +on: + push: + branches: [main, dev] + paths: ['backend/**', 'pyproject.toml', 'uv.lock'] + pull_request: + branches: [main, dev] + paths: ['backend/**', 'pyproject.toml', 'uv.lock'] + +concurrency: + group: backend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Ruff format check across supported Python versions ─────────────────── + format-check: + name: Ruff Format (${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install formatter + run: pip install "ruff>=0.15.5" + + - name: Verify formatting + run: ruff format --check . --exclude .venv --exclude venv diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index 019fbb6bae..0000000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Release - -on: - push: - branches: - - main # or whatever branch you want to use - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Check for changes in package.json - run: | - git diff --cached --diff-filter=d package.json || { - echo "No changes to package.json" - exit 1 - } - - - name: Get version number from package.json - id: get_version - run: | - VERSION=$(jq -r '.version' package.json) - echo "::set-output name=version::$VERSION" - - - name: Extract latest CHANGELOG entry - id: changelog - run: | - CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md) - CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g') - echo "Extracted latest release notes from CHANGELOG.md:" - echo -e "$CHANGELOG_CONTENT" - echo "::set-output name=content::$CHANGELOG_ESCAPED" - - - name: Create GitHub release - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const changelog = `${{ steps.changelog.outputs.content }}`; - const release = await github.rest.repos.createRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - tag_name: `v${{ steps.get_version.outputs.version }}`, - name: `v${{ steps.get_version.outputs.version }}`, - body: changelog, - }) - console.log(`Created release ${release.data.html_url}`) - - - name: Upload package to GitHub release - uses: actions/upload-artifact@v4 - with: - name: package - path: | - . - !.git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Trigger Docker build workflow - uses: actions/github-script@v8 - with: - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'docker-build.yaml', - ref: 'v${{ steps.get_version.outputs.version }}', - }) diff --git a/.github/workflows/deploy-to-hf-spaces.yml b/.github/workflows/deploy-to-hf-spaces.yml deleted file mode 100644 index cda9f1eb4b..0000000000 --- a/.github/workflows/deploy-to-hf-spaces.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Deploy to HuggingFace Spaces - -on: - push: - branches: - - dev - - main - workflow_dispatch: - -jobs: - check-secret: - runs-on: ubuntu-latest - outputs: - token-set: ${{ steps.check-key.outputs.defined }} - steps: - - id: check-key - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} - if: "${{ env.HF_TOKEN != '' }}" - run: echo "defined=true" >> $GITHUB_OUTPUT - - deploy: - runs-on: ubuntu-latest - needs: [check-secret] - if: needs.check-secret.outputs.token-set == 'true' - env: - HF_TOKEN: ${{ secrets.HF_TOKEN }} - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - lfs: true - - - name: Remove git history - run: rm -rf .git - - - name: Prepend YAML front matter to README.md - run: | - echo "---" > temp_readme.md - echo "title: Open WebUI" >> temp_readme.md - echo "emoji: 🐳" >> temp_readme.md - echo "colorFrom: purple" >> temp_readme.md - echo "colorTo: gray" >> temp_readme.md - echo "sdk: docker" >> temp_readme.md - echo "app_port: 8080" >> temp_readme.md - echo "---" >> temp_readme.md - cat README.md >> temp_readme.md - mv temp_readme.md README.md - - - name: Configure git - run: | - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - - name: Set up Git and push to Space - run: | - git init --initial-branch=main - git lfs install - git lfs track "*.ttf" - git lfs track "*.jpg" - rm demo.png - rm banner.png - git add . - git commit -m "GitHub deploy: ${{ github.sha }}" - git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml deleted file mode 100644 index 7a5dc651c4..0000000000 --- a/.github/workflows/docker-build.yaml +++ /dev/null @@ -1,806 +0,0 @@ -name: Create and publish Docker images with specific build args - -on: - workflow_dispatch: - push: - branches: - - main - - dev - tags: - - v* - -env: - REGISTRY: ghcr.io - -jobs: - build-main-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (latest) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: | - BUILD_HASH=${{ github.sha }} - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-main-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-cuda-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (cuda tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-cuda-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (cuda) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: | - BUILD_HASH=${{ github.sha }} - USE_CUDA=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-cuda-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-cuda126-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (cuda126 tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126 - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda126,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-cuda126-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (cuda126) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: | - BUILD_HASH=${{ github.sha }} - USE_CUDA=true - USE_CUDA_VER=cu126 - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-cuda126-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-ollama-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (ollama tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-ollama,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-ollama-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (ollama) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: | - BUILD_HASH=${{ github.sha }} - USE_OLLAMA=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-ollama-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-slim-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (slim tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-slim,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-slim-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (slim) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - build-args: | - BUILD_HASH=${{ github.sha }} - USE_SLIM=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-slim-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge-main-images: - runs-on: ubuntu-latest - needs: [build-main-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-main-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-cuda-images: - runs-on: ubuntu-latest - needs: [build-cuda-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-cuda-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-cuda126-images: - runs-on: ubuntu-latest - needs: [build-cuda126-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-cuda126-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126 - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda126,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-ollama-images: - runs-on: ubuntu-latest - needs: [build-ollama-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-ollama-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default ollama tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-ollama,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-slim-images: - runs-on: ubuntu-latest - needs: [build-slim-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-slim-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default slim tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-slim,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000000..bd46f65d54 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,311 @@ +name: Create and publish Docker images with specific build args + +on: + workflow_dispatch: + push: + branches: + - main + - dev + tags: + - v* + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ${{ matrix.platform.runner }} + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - arch: linux/amd64 + runner: ubuntu-latest + - arch: linux/arm64 + runner: ubuntu-24.04-arm + variant: + - name: main + suffix: "" + build_args: "" + free_disk: false + - name: cuda + suffix: "-cuda" + build_args: "USE_CUDA=true" + free_disk: true + - name: cuda126 + suffix: "-cuda126" + build_args: | + USE_CUDA=true + USE_CUDA_VER=cu126 + free_disk: true + - name: ollama + suffix: "-ollama" + build_args: "USE_OLLAMA=true" + free_disk: false + - name: slim + suffix: "-slim" + build_args: "USE_SLIM=true" + free_disk: false + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + platform=${{ matrix.platform.arch }} + echo "PLATFORM_PAIR=${platform//\//-}" >> ${GITHUB_ENV} + + - name: Free disk space + if: matrix.variant.free_disk + run: rm -rf /opt/hostedtoolcache + + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + ${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + ${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }} + + - name: Extract metadata for Docker cache + id: cache-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} + flavor: | + prefix=cache-${{ matrix.variant.name }}-${{ matrix.platform.arch }}- + latest=false + + - name: Build Docker image + uses: docker/build-push-action@v5 + id: build + with: + context: . + push: true + platforms: ${{ matrix.platform.arch }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} + cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true + build-args: | + BUILD_HASH=${{ github.sha }} + ${{ matrix.variant.build_args }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.variant.name }}-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: [build] + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + variant: + - name: main + suffix: "" + - name: cuda + suffix: "-cuda" + - name: cuda126 + suffix: "-cuda126" + - name: ollama + suffix: "-ollama" + - name: slim + suffix: "-slim" + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + + - name: Download digests + uses: actions/download-artifact@v5 + with: + pattern: digests-${{ matrix.variant.name }}-* + path: /tmp/digests + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + ${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + ${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + copy-to-dockerhub: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') + needs: [merge] + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - variant: main + suffix: "" + - variant: cuda + suffix: "-cuda" + - variant: cuda126 + suffix: "-cuda126" + - variant: ollama + suffix: "-ollama" + - variant: slim + suffix: "-slim" + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine source and destination tags + id: tags + run: | + DOCKERHUB_IMAGE="openwebui/open-webui" + SUFFIX="${{ matrix.suffix }}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + MAJOR_MINOR="${VERSION%.*}" + + echo "tags<> $GITHUB_OUTPUT + echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT + echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + if [ -z "$SUFFIX" ]; then + echo "tags=latest" >> $GITHUB_OUTPUT + else + VARIANT_NAME="${SUFFIX#-}" + echo "tags<> $GITHUB_OUTPUT + echo "latest${SUFFIX}" >> $GITHUB_OUTPUT + echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + fi + + echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT + + - name: Copy images from GHCR to Docker Hub + run: | + DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}" + SUFFIX="${{ matrix.suffix }}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + SOURCE_TAG="${VERSION}${SUFFIX}" + else + if [ -z "$SUFFIX" ]; then + SOURCE_TAG="latest" + else + SOURCE_TAG="latest${SUFFIX}" + fi + fi + + SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}" + + echo "Copying from ${SOURCE} to Docker Hub..." + + while IFS= read -r TAG; do + [ -z "$TAG" ] && continue + DEST="${DOCKERHUB_IMAGE}:${TAG}" + echo " -> ${DEST}" + docker buildx imagetools create -t "${DEST}" "${SOURCE}" + done <<< "${{ steps.tags.outputs.tags }}" diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml deleted file mode 100644 index 562e6aa1c1..0000000000 --- a/.github/workflows/format-backend.yaml +++ /dev/null @@ -1,49 +0,0 @@ -name: Python CI - -on: - push: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - pull_request: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - -jobs: - build: - name: 'Format Backend' - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: - - 3.11.x - - 3.12.x - - steps: - - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '${{ matrix.python-version }}' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black - - - name: Format backend - run: npm run format:backend - - - name: Check for changes after format - run: git diff --exit-code diff --git a/.github/workflows/format-build-frontend.yaml b/.github/workflows/format-build-frontend.yaml deleted file mode 100644 index eaa1072fbc..0000000000 --- a/.github/workflows/format-build-frontend.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Frontend Build - -on: - push: - branches: - - main - - dev - paths-ignore: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - pull_request: - branches: - - main - - dev - paths-ignore: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - -jobs: - build: - name: 'Format & Build Frontend' - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Dependencies - run: npm install --force - - - name: Format Frontend - run: npm run format - - - name: Run i18next - run: npm run i18n:parse - - - name: Check for Changes After Format - run: git diff --exit-code - - - name: Build Frontend - run: npm run build - - test-frontend: - name: 'Frontend Unit Tests' - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Dependencies - run: npm ci --force - - - name: Run vitest - run: npm run test:frontend diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml new file mode 100644 index 0000000000..9b5f2a099f --- /dev/null +++ b/.github/workflows/frontend.yaml @@ -0,0 +1,63 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Frontend CI — Lint, format check, build, and unit tests +# Runs on pushes and PRs to main/dev, skipping backend-only changes +# ───────────────────────────────────────────────────────────────────────────── +name: Frontend Build + +on: + push: + branches: [main, dev] + paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock'] + pull_request: + branches: [main, dev] + paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock'] + +concurrency: + group: frontend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Format, i18n, and production build ──────────────────────────────────── + format-and-build: + name: Format & Build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + run: npm install --force + + - name: Verify code formatting + run: npm run format + + - name: Verify i18n strings + run: npm run i18n:parse + + - name: Ensure working tree is clean + run: git diff --exit-code + + - name: Production build + run: npm run build + + # ── Vitest unit tests ──────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies (frozen lockfile) + run: npm ci --force + + - name: Execute test suite + run: npm run test:frontend diff --git a/.github/workflows/integration-test.disabled b/.github/workflows/integration-test.disabled deleted file mode 100644 index b248df4b5d..0000000000 --- a/.github/workflows/integration-test.disabled +++ /dev/null @@ -1,255 +0,0 @@ -name: Integration Test - -on: - push: - branches: - - main - - dev - pull_request: - branches: - - main - - dev - -jobs: - cypress-run: - name: Run Cypress Integration Tests - runs-on: ubuntu-latest - steps: - - name: Maximize build space - uses: AdityaGarg8/remove-unwanted-software@v4.1 - with: - remove-android: 'true' - remove-haskell: 'true' - remove-codeql: 'true' - - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Build and run Compose Stack - run: | - docker compose \ - --file docker-compose.yaml \ - --file docker-compose.api.yaml \ - --file docker-compose.a1111-test.yaml \ - up --detach --build - - - name: Delete Docker build cache - run: | - docker builder prune --all --force - - - name: Wait for Ollama to be up - timeout-minutes: 5 - run: | - until curl --output /dev/null --silent --fail http://localhost:11434; do - printf '.' - sleep 1 - done - echo "Service is up!" - - - name: Preload Ollama model - run: | - docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K - - - name: Cypress run - uses: cypress-io/github-action@v6 - env: - LIBGL_ALWAYS_SOFTWARE: 1 - with: - browser: chrome - wait-on: 'http://localhost:3000' - config: baseUrl=http://localhost:3000 - - - uses: actions/upload-artifact@v4 - if: always() - name: Upload Cypress videos - with: - name: cypress-videos - path: cypress/videos - if-no-files-found: ignore - - - name: Extract Compose logs - if: always() - run: | - docker compose logs > compose-logs.txt - - - uses: actions/upload-artifact@v4 - if: always() - name: Upload Compose logs - with: - name: compose-logs - path: compose-logs.txt - if-no-files-found: ignore - - # pytest: - # name: Run Backend Tests - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - - # - name: Set up Python - # uses: actions/setup-python@v5 - # with: - # python-version: ${{ matrix.python-version }} - - # - name: Install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r backend/requirements.txt - - # - name: pytest run - # run: | - # ls -al - # cd backend - # PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO - - migration_test: - name: Run Migration Tests - runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - # mysql: - # image: mysql - # env: - # MYSQL_ROOT_PASSWORD: mysql - # MYSQL_DATABASE: mysql - # options: >- - # --health-cmd "mysqladmin ping -h localhost" - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # ports: - # - 3306:3306 - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Set up uv - uses: yezz123/setup-uv@v4 - with: - uv-venv: venv - - - name: Activate virtualenv - run: | - . venv/bin/activate - echo PATH=$PATH >> $GITHUB_ENV - - - name: Install dependencies - run: | - uv pip install -r backend/requirements.txt - - - name: Test backend with SQLite - id: sqlite - env: - WEBUI_SECRET_KEY: secret-key - GLOBAL_LOG_LEVEL: debug - run: | - cd backend - uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' & - UVICORN_PID=$! - # Wait up to 40 seconds for the server to start - for i in {1..40}; do - curl -s http://localhost:8080/api/config > /dev/null && break - sleep 1 - if [ $i -eq 40 ]; then - echo "Server failed to start" - kill -9 $UVICORN_PID - exit 1 - fi - done - # Check that the server is still running after 5 seconds - sleep 5 - if ! kill -0 $UVICORN_PID; then - echo "Server has stopped" - exit 1 - fi - - - name: Test backend with Postgres - if: success() || steps.sqlite.conclusion == 'failure' - env: - WEBUI_SECRET_KEY: secret-key - GLOBAL_LOG_LEVEL: debug - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres - DATABASE_POOL_SIZE: 10 - DATABASE_POOL_MAX_OVERFLOW: 10 - DATABASE_POOL_TIMEOUT: 30 - run: | - cd backend - uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' & - UVICORN_PID=$! - # Wait up to 20 seconds for the server to start - for i in {1..20}; do - curl -s http://localhost:8081/api/config > /dev/null && break - sleep 1 - if [ $i -eq 20 ]; then - echo "Server failed to start" - kill -9 $UVICORN_PID - exit 1 - fi - done - # Check that the server is still running after 5 seconds - sleep 5 - if ! kill -0 $UVICORN_PID; then - echo "Server has stopped" - exit 1 - fi - - # Check that service will reconnect to postgres when connection will be closed - status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db) - if [[ "$status_code" -ne 200 ]] ; then - echo "Server has failed before postgres reconnect check" - exit 1 - fi - - echo "Terminating all connections to postgres..." - python -c "import os, psycopg2 as pg2; \ - conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \ - cur = conn.cursor(); \ - cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')" - - status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db) - if [[ "$status_code" -ne 200 ]] ; then - echo "Server has not reconnected to postgres after connection was closed: returned status $status_code" - exit 1 - fi - -# - name: Test backend with MySQL -# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure' -# env: -# WEBUI_SECRET_KEY: secret-key -# GLOBAL_LOG_LEVEL: debug -# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql -# run: | -# cd backend -# uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' & -# UVICORN_PID=$! -# # Wait up to 20 seconds for the server to start -# for i in {1..20}; do -# curl -s http://localhost:8083/api/config > /dev/null && break -# sleep 1 -# if [ $i -eq 20 ]; then -# echo "Server failed to start" -# kill -9 $UVICORN_PID -# exit 1 -# fi -# done -# # Check that the server is still running after 5 seconds -# sleep 5 -# if ! kill -0 $UVICORN_PID; then -# echo "Server has stopped" -# exit 1 -# fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..8654c2f7db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Release — Create GitHub release from CHANGELOG, trigger Docker builds +# Runs on pushes to main when package.json version changes +# ───────────────────────────────────────────────────────────────────────────── +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + # ── Create release and trigger downstream workflows ────────────────────── + publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - name: Abort if package.json unchanged + run: | + git diff --cached --diff-filter=d package.json || { + echo "package.json not modified — skipping release" + exit 1 + } + + - name: Read version + id: pkg + run: echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + + - name: Extract release notes from CHANGELOG + run: | + VER="${{ steps.pkg.outputs.version }}" + awk "/^## \[${VER}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" \ + CHANGELOG.md > /tmp/release-notes.md + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ steps.pkg.outputs.version }}" \ + --title "v${{ steps.pkg.outputs.version }}" \ + --notes-file /tmp/release-notes.md + + - name: Archive source + uses: actions/upload-artifact@v4 + with: + name: release-archive + path: | + . + !.git + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Dispatch Docker build + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'docker.yaml', + ref: 'v${{ steps.pkg.outputs.version }}', + }) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..052336f8e7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.5 + hooks: + - id: ruff + args: [--fix, backend] + - id: ruff-format + args: [backend] diff --git a/CHANGELOG.md b/CHANGELOG.md index 572ac40257..2d23ed077f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,1206 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.6] - 2026-06-01 + +### Added + +- 📦 **Official knowledge base sync tool.** A new companion tool from Open WebUI, oikb, keeps a knowledge base in sync with a local directory, GitHub repo, S3 bucket, Confluence space, or any of more than 40 other sources, uploading only new and changed files using the incremental sync support added in this release. [oikb](https://github.com/open-webui/oikb) +- 📂 **Smart directory sync for knowledge bases.** Local directories can now be synced into a knowledge base in one action: file checksums are compared against what's already stored, and only added or modified files are uploaded while removed files and orphaned subdirectories are cleaned up, with the directory structure mirrored automatically and per-file progress shown throughout. [#19190](https://github.com/open-webui/open-webui/issues/19190), [#19394](https://github.com/open-webui/open-webui/issues/19394), [Commit](https://github.com/open-webui/open-webui/commit/60c9db1cb81d021589cb49bee8744a799b51211f), [Commit](https://github.com/open-webui/open-webui/commit/73bdf86766d3f44467cdec436786fe089481baf3), [Commit](https://github.com/open-webui/open-webui/commit/9835b3f1dd7aa9c92ff08a634a0f97cc2a046e42), [Commit](https://github.com/open-webui/open-webui/commit/97252fa609440573251d8e75090f347ed1e51e1d), [Commit](https://github.com/open-webui/open-webui/commit/8f2d346e10c47b57bf6b5a6aa02d453488a88b89), [Commit](https://github.com/open-webui/open-webui/commit/1527eb6e01d441225c979d12d78b7625edf1086f) +- 🗂️ **Knowledge base folders.** Files inside a knowledge base can now be organized into nested folders, with breadcrumb navigation that makes it much easier to manage and find content in large collections. [Commit](https://github.com/open-webui/open-webui/commit/c2cbc47ca76ebfa21e1d36279dbd859283cbbfb1), [Commit](https://github.com/open-webui/open-webui/commit/ab0ee858b73738c5b77d1d455e66e66bc61df1c4), [Commit](https://github.com/open-webui/open-webui/commit/2ad327a4dce64a8eedf5eb41c73b8520344fdd82), [Commit](https://github.com/open-webui/open-webui/commit/171150c1e12b5713f77a5ea0dd19c6581c7bef2e), [Commit](https://github.com/open-webui/open-webui/commit/e7d2ddbb1d14c03a52797751d24a98132aac0cd8), [Commit](https://github.com/open-webui/open-webui/commit/32a417bbf66ed28a5ca8a759ad3a87b2a2aa358a) +- 🧰 **Filesystem tool for knowledge bases.** A new built-in tool, enabled via the "ENABLE_KB_EXEC" environment variable, lets AI models browse and search knowledge base contents using familiar filesystem commands such as 'ls', 'cat', 'grep', 'find', 'head', 'tail', and 'sed', including pipes between them. [Commit](https://github.com/open-webui/open-webui/commit/5b125c24d4eae925d3287efa626595bab29d5c33), [Commit](https://github.com/open-webui/open-webui/commit/ecec86dd32aa396dd5a383e9be5b0f16c4346c48), [Commit](https://github.com/open-webui/open-webui/commit/9ef579ce4be5b82f9281b085a621fed5b04e0d26), [Commit](https://github.com/open-webui/open-webui/commit/2f642754ac6b8c430228a7fc289357a6c6652235), [Commit](https://github.com/open-webui/open-webui/commit/3b00e5721a60518ea317c4fd61a6d0d182961a2f), [Commit](https://github.com/open-webui/open-webui/commit/4e78b355efe0611f27fddef591c69f7c1259a9f6), [Commit](https://github.com/open-webui/open-webui/commit/74f95a9b0d6b89b241701422170d54e21e701baa), [Commit](https://github.com/open-webui/open-webui/commit/cc16e06c32de48bdf464db5814483a49f3527b63), [Commit](https://github.com/open-webui/open-webui/commit/1ea54c3217f487990b6a4c0f0d8ea675a479b6e1) +- ✏️ **File renaming in knowledge bases.** Files inside a knowledge base can now be renamed directly from the workspace, with the new name reflected wherever the file is referenced. [Commit](https://github.com/open-webui/open-webui/commit/3127f1b46255626ea92eb0b598e45078287303f1) +- 😀 **Emoji picker in message input.** A new emoji button in the rich text formatting toolbar lets you browse and insert emojis directly into your messages. [#24704](https://github.com/open-webui/open-webui/pull/24704) +- 🔎 **Access preview for users and groups.** Administrators can now preview exactly which models, knowledge bases, and tools a given user or group can access, making it easier to audit and verify permission setups. [Commit](https://github.com/open-webui/open-webui/commit/9c14740ffb009d550dcbd5d6c599dac57053f112) +- 📄 **Configurable knowledge base file page size.** Administrators can now request a larger page size when listing a knowledge base's files through the API, reducing the number of requests needed to retrieve large collections instead of paging through fixed increments of 30. [#25148](https://github.com/open-webui/open-webui/issues/25148), [Commit](https://github.com/open-webui/open-webui/commit/a4d1b3e9378a61a11dd9822a8dbb525f39753081) +- 📑 **MinerU file type configuration.** Administrators can now configure which file types are processed by the MinerU document loader, via the new "MINERU_FILE_EXTENSIONS" setting, extending it beyond PDF to formats like DOCX, PPTX, and XLSX. [Commit](https://github.com/open-webui/open-webui/commit/d4030a8aa5d48c2a1cb06c461566844aca2530ab) +- 📁 **Create subfolders from the folder header.** Chat folders can now have subfolders created directly from the folder header in the chat view, not just from the sidebar. [Commit](https://github.com/open-webui/open-webui/commit/1f0948bcbef2af73b155535ea27762c522260afc) +- ⚡ **Faster initial page loads.** The configuration endpoint that loads on every page visit no longer runs an unnecessary user-count query, making the initial application load lighter on the database, especially on instances with many users. [Commit](https://github.com/open-webui/open-webui/commit/0adc090dcbe636c9c9645d8ec7f6b89ea514870b) +- 🚀 **Faster tool-enabled chat completions.** Chat completions that use multiple tools now start faster because the tools they reference are fetched from the database in a single batch query instead of one query per tool. [#24808](https://github.com/open-webui/open-webui/pull/24808), [Commit](https://github.com/open-webui/open-webui/commit/cc94a90b4d4d690bc7cb9f7124f2d6e552973970) +- 🏎️ **More responsive web search under load.** Web search through SearXNG, Google PSE, Brave, Serper, and Serpstack now uses non-blocking network calls, so the server stays responsive to other users while a search is in flight, and concurrent multi-query searches complete faster. [Commit](https://github.com/open-webui/open-webui/commit/b94245d2ee191e8ef118bf9de1ff7539503bfec9) +- 🐎 **Lighter Ollama backend connections.** Requests to Ollama backends now reuse a shared connection pool instead of opening a fresh session each time, reducing TCP and TLS handshake overhead for installs that poll Ollama frequently or have multiple backends configured. [Commit](https://github.com/open-webui/open-webui/commit/5d9a09a88a9094ebfcd249340be3aaee544b34d0) +- 📝 **Frontmatter auto-fill for tools, functions, and skills.** Opening a tool, function, or skill editor now auto-fills the name, id, and description fields from the file's frontmatter, saving you from re-entering metadata already declared in the source. [#24649](https://github.com/open-webui/open-webui/pull/24649), [Commit](https://github.com/open-webui/open-webui/commit/ef975649b26d3e7cd589c49be2fc77cee80fad8f) +- 🪪 **More user placeholders in custom headers.** Custom-header templates for direct connections and tool servers now support "{{USER_EMAIL}}" and "{{USER_ROLE}}" alongside the existing user and session placeholders. [Commit](https://github.com/open-webui/open-webui/commit/ed73ef3d8df988b0e9646b82df5b1a453202ef8d) +- 🌎 **Linkup web search provider.** Administrators can now select Linkup as the web search provider from the admin settings, with options to configure the API key and search depth. [#24752](https://github.com/open-webui/open-webui/pull/24752), [Commit](https://github.com/open-webui/open-webui/commit/56c0d00e13c74d665124ec9f1cae78e83ca60b6a) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Spanish (Spain), Swedish, German, Korean, Catalan, Russian, Irish, Simplified Chinese, Traditional Chinese, Finnish, Polish, Turkish, and Malay were enhanced and expanded. + +### Fixed + +- 🛡️ **Security Advisory**: This release includes security and access-control fixes. We recommend updating production deployments at your earliest convenience. Not all security fixes in this version may be enumerated in the fixed section — some may be withheld for a short time to give administrators time to upgrade. [Advisories](https://github.com/open-webui/open-webui/security) +- 🛡️ **Tool server permission enforcement.** The per-user permission for inline tool servers is now enforced on chat-completion requests, so users without that permission can no longer bypass the admin setting by supplying tool servers directly in their requests. [Commit](https://github.com/open-webui/open-webui/commit/5cc1eb517094e3507915664817285f1e6e37a16d) +- 🔒 **Knowledge base access check in search tool.** The built-in knowledge search tool now verifies that the caller can access a knowledge base before searching it by id, preventing users from reading the contents of knowledge bases they have not been granted access to. [#25113](https://github.com/open-webui/open-webui/pull/25113) +- 🧱 **API key endpoint restriction bypass.** The endpoint allow-list that limits which paths an API key may reach is now matched against the routed request path directly, preventing a crafted request from slipping past the restriction. [#25123](https://github.com/open-webui/open-webui/pull/25123) +- 🚧 **System prompt bypass via request parameter.** The flag that skips a model's configured system prompt can no longer be set by external clients through a request parameter, so admin-configured system prompts can't be bypassed from the API. [#25156](https://github.com/open-webui/open-webui/pull/25156) +- 🚪 **Terminal proxy path traversal.** The terminal proxy now fully decodes request paths before validating them, blocking multi-encoded payloads that could otherwise escape the intended path. [#25157](https://github.com/open-webui/open-webui/pull/25157) +- 🔓 **Cross-user file exfiltration via image URLs.** When a chat message references a file by id in an "image_url" field, the server now resolves that file only for its owner, an administrator, or a user with an explicit read grant, preventing other authenticated users from extracting a file's contents by routing it through the model. [#24625](https://github.com/open-webui/open-webui/pull/24625), [Commit](https://github.com/open-webui/open-webui/commit/c75fe8e74b72617c51282cc3ea0a2e8d9cdd9140) +- 📌 **Chat file attachment access checks.** Attaching files to a chat now links only files the caller can read, preventing a user from associating another user's file with their chat to access its contents. [#25054](https://github.com/open-webui/open-webui/pull/25054) +- 📅 **Calendar event move authorization.** Updating a calendar event to move it into a different calendar now requires write access on the destination calendar, preventing users from injecting events into calendars they cannot write to. [#24764](https://github.com/open-webui/open-webui/pull/24764) +- 🕸️ **Web loader SSRF gating with Playwright.** When the Playwright-based web loader is in use, page navigations and redirects are now validated the same way as the default loader, closing a gap where the Playwright path could reach internal or otherwise blocked URLs. [#24756](https://github.com/open-webui/open-webui/pull/24756) +- 🛂 **DNS rebinding protection for URL fetches.** The IP address validated for an outbound URL fetch is now the same one used for the actual connection, closing a DNS rebinding window where an attacker-controlled hostname could resolve to a public IP during the safety check and then to a private IP when the connection was opened. [#24759](https://github.com/open-webui/open-webui/pull/24759) +- 🪞 **OAuth profile picture redirect handling.** The OAuth profile picture fetch now follows redirects only when administrators have explicitly allowed it, closing a window where a redirect from an externally validated URL could be used to reach internal addresses. [#24809](https://github.com/open-webui/open-webui/pull/24809) +- 🧼 **Model profile image script injection.** Model profile images are now validated on save and only served inline when they are a known-safe image type, preventing a crafted SVG profile image from running scripts in other users' browsers, while existing legacy images that fail validation are cleared gracefully instead of breaking the model list. [#25060](https://github.com/open-webui/open-webui/pull/25060), [#25173](https://github.com/open-webui/open-webui/pull/25173) +- 🧯 **Diagram rendering script injection.** Mermaid diagrams rendered in chat are now sanitized before display, preventing a crafted diagram from running scripts in the viewer's browser. [#25219](https://github.com/open-webui/open-webui/pull/25219) +- 🔐 **Shared-chat file write protection.** Access to a file through a shared chat now only grants read access, so users who can read a shared chat can no longer modify or delete files attached to it. [#24755](https://github.com/open-webui/open-webui/pull/24755) +- 🔏 **Cross-origin embed prompt control.** When Open WebUI is embedded in an iframe on a different origin, the embedding page can now only drive the chat input or submit prompts if the user has explicitly opted in via the "iframe Sandbox Allow Same Origin" setting, preventing untrusted host pages from triggering confirmation dialogs or controlling the chat. [#24767](https://github.com/open-webui/open-webui/pull/24767), [Commit](https://github.com/open-webui/open-webui/commit/eb3076c1b02d90c2ce6e6d3beb08a37987c740ec) +- 🗂️ **Chat folder ownership checks.** Creating a chat or updating a chat's folder now verifies the referenced folder belongs to the current user, preventing chats from being associated with folders owned by other people. [#24588](https://github.com/open-webui/open-webui/pull/24588) +- 🧩 **Chat recovery from corrupted history.** Chats whose internal message graph was left in a malformed state by a failed regeneration now open and load correctly, with missing roles, parent references, and current-message pointers reconstructed automatically instead of breaking the chat. [#24424](https://github.com/open-webui/open-webui/issues/24424), [#24157](https://github.com/open-webui/open-webui/issues/24157), [#20474](https://github.com/open-webui/open-webui/issues/20474), [#24799](https://github.com/open-webui/open-webui/pull/24799), [Commit](https://github.com/open-webui/open-webui/commit/d310a0777c4c48ec772bdc9a510005d5e91b09c7) +- 🔍 **Web search reliability.** Web search again fetches page content reliably with the default web loader engine, a new "USER_AGENT" environment variable lets administrators set a real browser user-agent so fetches aren't blocked by Cloudflare, Wikipedia, and other bot-detection systems, and the startup script no longer fails to launch when these new environment variables are unset. [#24560](https://github.com/open-webui/open-webui/issues/24560), [#24793](https://github.com/open-webui/open-webui/issues/24793), [#24683](https://github.com/open-webui/open-webui/pull/24683), [Commit](https://github.com/open-webui/open-webui/commit/f60733758272c4532cd032e518c9cd73f648043a) +- 🔌 **API chat completions reliability.** Direct calls to the chat completions API no longer fail with an internal error when no chat session identifier is supplied. [#24553](https://github.com/open-webui/open-webui/issues/24553), [#25235](https://github.com/open-webui/open-webui/issues/25235), [Commit](https://github.com/open-webui/open-webui/commit/bc244fdc90504824b76654880898bf3f6649c299), [Commit](https://github.com/open-webui/open-webui/commit/f16b5c446027eae1bd767617bac2fdf54b24d6fc) +- 🖼️ **ComfyUI image generation and editing.** Generating and editing images via a ComfyUI backend now works again, including when ComfyUI is hosted on a private or internal network where URL validation was previously blocking the admin-configured endpoint. [#24565](https://github.com/open-webui/open-webui/issues/24565), [Commit](https://github.com/open-webui/open-webui/commit/7dcd932ad7cb5feab007737c6e0281def5cd47fc), [Commit](https://github.com/open-webui/open-webui/commit/8aa2a42dc7887512e697ff85d044220945ced40f) +- 🐘 **Knowledge search on large documents.** Searching knowledge bases on PostgreSQL no longer fails when scanning across documents with very large extracted text content. [#24670](https://github.com/open-webui/open-webui/issues/24670), [Commit](https://github.com/open-webui/open-webui/commit/d74ee34d9128295faa116c919bd1dcca77744975) +- 💬 **Chat title generation.** Automatically generated chat titles now use the model currently selected in the dropdown for the active chat and fall back to the model from the active message branch otherwise, and a clear message is shown if no model is available instead of an unhelpful error. [#24604](https://github.com/open-webui/open-webui/issues/24604), [#24745](https://github.com/open-webui/open-webui/issues/24745), [Commit](https://github.com/open-webui/open-webui/commit/e5c8f8110a88739e011d8ab23e5cb11b4768469f), [Commit](https://github.com/open-webui/open-webui/commit/3c5e7968f0b5130890e17f1f2a2a52297cb145a6) +- 🧮 **Message search and analytics consistency.** Edits, deletions, and branch changes made in a chat are now reflected in message search results and analytics counts instead of leaving stale entries behind. [#25205](https://github.com/open-webui/open-webui/pull/25205), [Commit](https://github.com/open-webui/open-webui/commit/aa06200f789a4c3fc54ea9a141cd347d76cda4c5) +- ⌨️ **Responsive knowledge file search.** Searching for knowledge files in the chat picker and model knowledge selector now matches on file names by default instead of scanning the full extracted text of every document on each keystroke, keeping the search responsive on large deployments, with content search available as an explicit opt-in. [#25082](https://github.com/open-webui/open-webui/issues/25082), [#25119](https://github.com/open-webui/open-webui/pull/25119), [Commit](https://github.com/open-webui/open-webui/commit/591e0aafa1d5e21dcd9fa1279dada2076968b9cb) +- 🧷 **Tool call continuity with strict providers.** Chats that contain incomplete tool calls or orphaned tool results no longer fail to continue when sent to providers that strictly validate tool pairings, such as Anthropic and AWS Bedrock Converse. [#24758](https://github.com/open-webui/open-webui/issues/24758), [#24798](https://github.com/open-webui/open-webui/pull/24798), [Commit](https://github.com/open-webui/open-webui/commit/cfa6908d579e1f7f202a321289029996130b8411) +- 🛑 **Stream termination for pipe functions.** Streamed responses from pipe functions now always send the standard end-of-stream marker, so chat clients and external integrations reliably detect when a response is complete instead of waiting on streams that already finished. [#24763](https://github.com/open-webui/open-webui/pull/24763) +- 🔊 **Non-blocking text-to-speech transcoding.** Converting text-to-speech audio to MP3 no longer blocks the server's event loop, so other requests stay responsive even while a TTS response is being transcoded. [#24876](https://github.com/open-webui/open-webui/pull/24876) +- 🎚️ **Default text-to-speech voice.** Text-to-speech requests that don't specify a voice now fall back to the configured default voice instead of failing or using an unintended one. [#25035](https://github.com/open-webui/open-webui/issues/25035), [Commit](https://github.com/open-webui/open-webui/commit/f16b5c446027eae1bd767617bac2fdf54b24d6fc) +- 🪝 **Reliable knowledge base file linking.** Files uploaded to a knowledge collection are now linked on the server as part of the upload itself, so they remain attached to the collection even if you navigate away or close the page before processing finishes. [#24807](https://github.com/open-webui/open-webui/issues/24807), [Commit](https://github.com/open-webui/open-webui/commit/d0b17f056911ec73df2b686a0d92bb789391db27) +- ☁️ **Azure connections on custom hostnames.** Connections marked as the Azure provider now use the Azure code path even when the endpoint does not contain "azure" in its hostname, fixing custom Azure deployments served from non-standard domains. [#24882](https://github.com/open-webui/open-webui/pull/24882), [Commit](https://github.com/open-webui/open-webui/commit/c8f851bd2de3d127f1e818fc116c27174f96d3f1) +- 🗓️ **Clearing calendar event fields.** Removing the description or location from a calendar event now saves correctly instead of silently keeping the previous value. [#25026](https://github.com/open-webui/open-webui/issues/25026), [Commit](https://github.com/open-webui/open-webui/commit/91810f1c4e93d4f559bdf61cd085cadc288a732d), [Commit](https://github.com/open-webui/open-webui/commit/78b1637a035d71099262412e5dee3e4d65c7fb2f) +- 💭 **Advanced parameter settings.** Custom reasoning tags and custom model parameters are now saved correctly instead of being dropped, and the presence penalty and repeat penalty no longer save the frequency penalty's value instead of their own. [#25183](https://github.com/open-webui/open-webui/pull/25183), [#25200](https://github.com/open-webui/open-webui/pull/25200), [#25204](https://github.com/open-webui/open-webui/pull/25204) +- 📏 **Long username display.** Long usernames no longer overflow their containers in the admin user list, user modals, and sidebar. [#25185](https://github.com/open-webui/open-webui/pull/25185) +- 🎙️ **Voice mode mute shortcut listing.** The keyboard shortcut for muting voice mode now appears in the keyboard shortcuts help modal. [#25193](https://github.com/open-webui/open-webui/pull/25193) +- 📎 **Document attachments in channel model replies.** Tagging a model in a channel thread now forwards uploaded non-image documents such as PDFs and DOCX files into the model's context, so document summarization and comparison workflows that already worked in direct chat now work in channels too. [#24896](https://github.com/open-webui/open-webui/issues/24896), [#24898](https://github.com/open-webui/open-webui/pull/24898), [Commit](https://github.com/open-webui/open-webui/commit/7e9d41d664d7065a92ac93606d18e88c45eb36b6) +- 🙈 **Hidden models in channel mentions.** Models marked as hidden no longer appear in the channel message-input model mention selector, matching how hidden models are excluded elsewhere in the interface. [#24892](https://github.com/open-webui/open-webui/pull/24892) +- 🧵 **Channel thread and pinned message stability.** Opening a channel thread or the pinned messages view no longer fails to render when a message or its data is missing. [#25209](https://github.com/open-webui/open-webui/pull/25209) +- 📺 **YouTube short link transcripts.** Pasting a "youtu.be" short link into a chat now loads the video transcript correctly instead of failing with an empty-content error. [#24856](https://github.com/open-webui/open-webui/issues/24856), [Commit](https://github.com/open-webui/open-webui/commit/1e36a206008c3abce5ccdc98d5750e30a7345b98) +- 🙉 **Hidden models in default-model pickers.** The admin pickers for default models and default pinned models now filter out hidden models, consistent with how hidden models are treated elsewhere. [Commit](https://github.com/open-webui/open-webui/commit/1fa3050f069a72de1daaaa29233b545c4deaea51) +- 🔊 **Speech-to-text SSL setting honored.** Speech-to-text requests now respect the "AIOHTTP_CLIENT_SESSION_SSL" setting, so administrators using self-signed certificates or custom SSL configurations can use STT engines that were previously failing TLS verification. [#24568](https://github.com/open-webui/open-webui/issues/24568), [#24857](https://github.com/open-webui/open-webui/pull/24857), [Commit](https://github.com/open-webui/open-webui/commit/2ca91ceeeca6a6a21e5a6ad68ea3ab8c9d9f6deb), [Commit](https://github.com/open-webui/open-webui/commit/94b66b17972e0ad77954df4b81a6bb86a7a6a04b) +- 🔗 **Placeholders in MCP connection headers.** Custom header templates configured on MCP server connections now have their "{{USER_ID}}", "{{USER_NAME}}", "{{USER_EMAIL}}", "{{USER_ROLE}}", "{{CHAT_ID}}", and "{{MESSAGE_ID}}" placeholders interpolated at request time, matching how custom headers already work for direct connections and tool servers. [#24822](https://github.com/open-webui/open-webui/pull/24822) +- 🪟 **Bing search CLI smoke test.** Running the Bing web-search module from the command line for a quick connectivity check no longer raises an error about missing arguments. [#24765](https://github.com/open-webui/open-webui/issues/24765), [#24768](https://github.com/open-webui/open-webui/pull/24768) +- 🩺 **Database health check recovery.** After a transient database connection error, the health check endpoint now recovers automatically instead of staying permanently broken on the affected worker. [Commit](https://github.com/open-webui/open-webui/commit/0b81520e072bb4ed15532b8e153b43dd3243feaf) +- 🪵 **JSON exception logging.** With JSON log formatting enabled, exceptions are now recorded correctly with a structured type, message, and stacktrace instead of being dropped, and a logging failure can no longer crash the application. [#25135](https://github.com/open-webui/open-webui/issues/25135), [Commit](https://github.com/open-webui/open-webui/commit/79bf3d28d88e78b0136ef3c3c9f8e7bb85d3cea9) +- 🧭 **Workspace skills permission.** Users granted only the "workspace.skills" permission can now see the workspace entry in the sidebar and are correctly routed to the skills page from the workspace index. [#24729](https://github.com/open-webui/open-webui/pull/24729) +- 🔁 **Resilient database migrations.** Database migrations now skip tables, indexes, and columns that already exist and add missing primary keys to legacy tables, so upgrades succeed even when parts of the schema were manually or partially created beforehand. [Commit](https://github.com/open-webui/open-webui/commit/81f611fb73c726cfcfc20ee57a926a543f07e95f), [Commit](https://github.com/open-webui/open-webui/commit/f0e88dadc8502ea05b1a00f3155bee7d1cf32249), [Commit](https://github.com/open-webui/open-webui/commit/bd9f82d5a681ee94bde44325033447500a0c76bf), [Commit](https://github.com/open-webui/open-webui/commit/459b1c3fda2ec3579fbd2ab408d0fdeb07c96b99), [Commit](https://github.com/open-webui/open-webui/commit/6df09a4039d181f4e2d41324e93cd36c6fb27dfd), [Commit](https://github.com/open-webui/open-webui/commit/95840e307a66429168775de389b329487a4311c4), [Commit](https://github.com/open-webui/open-webui/commit/6b1df94bf933af92f5c7d093a2d92e2e50d6fdd0), [Commit](https://github.com/open-webui/open-webui/commit/98d3b2308564e2112290773ea240b90419ebb48d), [Commit](https://github.com/open-webui/open-webui/commit/1b9d22e324181b96511a15cbacc40fdbb439ad79), [Commit](https://github.com/open-webui/open-webui/commit/dc0f8ae6f2372b6e5dd42f3520813b09c54146c0), [Commit](https://github.com/open-webui/open-webui/commit/ee3b14233a642ff1bc38aef03a2418afcf5d6c1d), [Commit](https://github.com/open-webui/open-webui/commit/1004dad2749bcc369d511315a808d4bb5277854c), [Commit](https://github.com/open-webui/open-webui/commit/db2b3d7fd86cc7d76424671ead3c62a3f6302fe7), [Commit](https://github.com/open-webui/open-webui/commit/2e1b671e8db49f47d69fd59ece2b58e109ec8b84), [Commit](https://github.com/open-webui/open-webui/commit/9a8969ca93f8c9109d956518a085be3e2afacc07), [Commit](https://github.com/open-webui/open-webui/commit/9717ada92fdb3761a217d309f322ee9fbe418d0d), [Commit](https://github.com/open-webui/open-webui/commit/d7cfc1e46a8f3e5c8f6758c12ef641f41fb51a39), [Commit](https://github.com/open-webui/open-webui/commit/9263b7568eaa83f43e4a118c98490c4f7aaba570), [Commit](https://github.com/open-webui/open-webui/commit/73d2065227e651cf90a2cfade721516815743ab4), [#24722](https://github.com/open-webui/open-webui/pull/24722) + +### Changed + +- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility. +- ⚙️ **Tool-call iteration cap renamed and raised.** The environment variable that limits how many tool calls a single chat response may make is now "CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS", with its default raised from 30 to 256 and a new "-1" value for unlimited; the previous "CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES" name continues to work as a fallback, and chats that hit the cap now show a clear error in-chat instead of stopping silently. [#24918](https://github.com/open-webui/open-webui/pull/24918), [Commit](https://github.com/open-webui/open-webui/commit/2b99945d2726bdf5aed7b1712f9e3b7b622671df) +- 🔐 **Reduced public "/api/config" exposure.** The "/api/config" response no longer includes several feature flags ("enable_api_keys", "enable_password_change_form", "enable_version_update_check", "enable_public_active_users_count", "enable_easter_eggs") for unauthenticated callers, reducing information disclosure to anonymous visitors. [Commit](https://github.com/open-webui/open-webui/commit/245e0ee029e9e10617f62953a9e6f67dd00ecf81), [Commit](https://github.com/open-webui/open-webui/commit/ae06e199d5d3f65296a978cc35079bdacba596d2) +- 🔑 **"WEBUI_SECRET_KEY" is now a hard requirement even for unsupported deployments.** Deployments that start the backend in an explicitly unsupported way (such as invoking uvicorn directly) without setting "WEBUI_SECRET_KEY" will now refuse to start instead of falling back to an empty key; the supported start methods (start.sh, start_windows.bat, and "open-webui serve") still set or auto-generate it automatically, so standard deployments are unaffected. Direct Uvicorn startup is not supported. [#25218](https://github.com/open-webui/open-webui/pull/25218) + +## [0.9.5] - 2026-05-09 + +### Added + +- 🛡️ **Redirect-based SSRF protection.** All outbound HTTP requests now block 3xx redirects by default via a new `AIOHTTP_CLIENT_ALLOW_REDIRECTS` environment variable, preventing redirect-based SSRF where a public URL silently redirects to internal addresses (RFC 1918, loopback, cloud-metadata endpoints). Affected call sites include web fetch, image loading, OAuth discovery, tool server execution, and code interpreter login. [#24491](https://github.com/open-webui/open-webui/pull/24491) +- 🛡️ **Iframe content security policy.** Administrators can now configure a Content-Security-Policy for all srcdoc iframes (Artifacts, tool embeds, file previews, citation modals) via the `IFRAME_CSP` environment variable, restricting what LLM-generated or user-uploaded HTML can load and execute inside previews. [Commit](https://github.com/open-webui/open-webui/commit/3bba1c227059a44c7eeefa97b8c69a63bf4f3454) +- 🎛️ **Granular markdown rendering controls.** Users can now independently disable Markdown rendering for user messages and assistant responses from Interface settings, preventing unintended formatting when pasting text that contains Markdown-sensitive characters. [Commit](https://github.com/open-webui/open-webui/commit/4a1064cefd6f48a8b3b02cd31f77838c8802b635) +- 🔧 **Terminal proxy response headers.** Administrators can now inject custom response headers into terminal proxy responses via the `TERMINAL_PROXY_HEADERS` environment variable (JSON object), enabling deployment-specific security headers like sandbox policies for proxied content. [Commit](https://github.com/open-webui/open-webui/commit/8d3133fe2835122bffaa4f2ce584730bc9c78981) +- 🔌 **Channel streaming and tool support.** Mentioning a model in a Channel now streams responses in real time and supports the full chat completion pipeline, including native and default function calling, built-in tools (web search, image generation), user tools, MCP tools, filters, and RAG knowledge injection — the same capabilities available in standard chats. + +### Fixed + +- 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486) +- 🔐 **Skill public sharing permission enforcement.** Creating or updating skills now filters access grants through the `sharing.public_skills` permission, preventing non-admin users from making skills publicly accessible without the required permission. [#24494](https://github.com/open-webui/open-webui/pull/24494) +- 🔐 **Calendar public sharing permission enforcement.** Creating or updating calendars now filters access grants through a new `sharing.public_calendars` permission, preventing users from making calendars publicly readable or writable without explicit admin-granted sharing permission. [#24493](https://github.com/open-webui/open-webui/pull/24493) +- 🔐 **Feedback user attribution spoofing.** Submitting evaluation feedback can no longer forge the `user_id` field through mass-assignment, preventing authenticated users from attributing ratings to other users and corrupting Elo leaderboard rankings and admin feedback exports. [#24508](https://github.com/open-webui/open-webui/pull/24508) +- 🛡️ **Image URL redirect-based SSRF.** Chat messages containing image URLs no longer follow 3xx redirects to internal addresses during base64 conversion, closing the most reachable redirect-based SSRF variant that required no special permissions or feature flags. [#24524](https://github.com/open-webui/open-webui/pull/24524) +- 🛡️ **Collection write access on file processing.** The `process_file` and `process_files_batch` retrieval endpoints now enforce collection write-access checks before embedding content, preventing authenticated users from injecting file content into another user's knowledge-base collection. [#24524](https://github.com/open-webui/open-webui/pull/24524) +- 🔐 **Tool source code update authorization.** Updating a tool's Python source code now requires `workspace.tools` or `workspace.tools_import` permission, preventing users with only a write-access grant from overwriting executable tool code while still allowing metadata edits. [#24513](https://github.com/open-webui/open-webui/pull/24513) +- 🔐 **Channel message ownership enforcement.** Updating or deleting messages in group and DM channels now requires message ownership, preventing channel members from tampering with or silently removing other members' messages. [#24506](https://github.com/open-webui/open-webui/pull/24506) +- 🔐 **Channel pin write permission.** Pinning and unpinning messages on standard channels now requires write permission instead of read permission, preventing read-only users from modifying pinned content. [#24521](https://github.com/open-webui/open-webui/pull/24521) +- 🛡️ **Image generation URL validation.** Generated image URLs are now validated through `validate_url()` before fetching, aligning the defense-in-depth posture with sibling image-loading paths. [#24518](https://github.com/open-webui/open-webui/pull/24518) +- 🔐 **Model params exposure for read-only users.** The per-model API endpoint now strips the `params` dict (including system prompts) from responses to callers without write access, preventing read-only users from viewing admin-curated model configuration. [#24525](https://github.com/open-webui/open-webui/pull/24525) +- 🛡️ **URL parser SSRF bypass.** URL validation now rejects backslash, tab, CR, and LF characters that cause urllib and requests/aiohttp to disagree on the target host, closing a parser-confusion SSRF bypass. [#24534](https://github.com/open-webui/open-webui/pull/24534) +- 🛡️ **Profile image MIME-type allowlist.** Serving profile images from data URIs now enforces a strict MIME-type allowlist (PNG, JPEG, GIF, WEBP by default, configurable via `PROFILE_IMAGE_ALLOWED_MIME_TYPES`) and sets `X-Content-Type-Options: nosniff`, preventing stored-XSS through SVG or other executable content types. [Commit](https://github.com/open-webui/open-webui/commit/15e696691cad98692c329de62ed8a5bdb3a26d4e) +- 🔐 **File ownership in folder and knowledge attachments.** Attaching files to folders or knowledge bases now verifies per-file read access, and folder file lists in chat middleware are filtered to entries the caller can read, preventing unauthorized file content from being injected into RAG context. [Commit](https://github.com/open-webui/open-webui/commit/2dbf7b6764a7922458d3b0139687ad6dcd7596d9) +- 🔐 **Shared chat access for owners and admins.** Chat owners can now view and clone their own shared chats without requiring an explicit access grant, and administrators can manage shared chat access controls on any chat. [Commit](https://github.com/open-webui/open-webui/commit/3a21b334cce30226750c5c537345dc51bb8bef17), [Commit](https://github.com/open-webui/open-webui/commit/315566064aedeff071854b023d09e5f1ea0eb950) +- 🧵 **Legacy chat history self-healing.** Loading legacy conversations now automatically detects broken parent-link graphs in migrated message records, merges missing messages from the embedded JSON history, and backfills them to the normalized table so future loads use the fast path without data loss. [Commit](https://github.com/open-webui/open-webui/commit/1388f4568b8f508c26542673dd01f1fa049e798a) +- 🎛️ **Filter selector reactivity.** Model filter checkboxes now derive state reactively from the current filter list and selected IDs instead of capturing a one-time snapshot at mount, so checkboxes update correctly when model contexts or filter configurations change at runtime. [Commit](https://github.com/open-webui/open-webui/commit/d1ef5382377f590f97a6dbaee88f369e6d7c5f6f) +- 🌐 **Portuguese (Brazil) translation updates.** Translations for newly added UI items were added along with a consistency pass across existing entries. [#24503](https://github.com/open-webui/open-webui/pull/24503) + +### Changed + +- 🧹 **Removed unauthenticated retrieval status endpoint.** The unauthenticated `GET /api/v1/retrieval/` status endpoint has been removed as dead code — retrieval configuration is already available through authenticated admin endpoints. [#24497](https://github.com/open-webui/open-webui/pull/24497) +- 📋 **PR template issue requirement.** Pull requests now require a linked Issue or Discussion reference, ensuring better traceability for all contributions. PRs without a linked issue or discussion may be closed without review. + +## [0.9.4] - 2026-05-09 + +### Fixed + +- 📜 **Chat scroll position on load.** Opening a chat conversation now reliably scrolls to the bottom of the message history, fixing a regression caused by `content-visibility: auto` where estimated element sizes prevented the initial scroll from reaching the true bottom. + +## [0.9.3] - 2026-05-09 + +### Added + +- 🔇 **Voice Mode mute control.** Voice Mode now includes a dedicated mute toggle with an "M" shortcut and auto-unmute after assistant playback, so you can prevent accidental interruptions from background noise without leaving the call overlay. [Commit](https://github.com/open-webui/open-webui/commit/072d2000f35a9f7b96342fa9bb28f925a92e7b4c), [#23832](https://github.com/open-webui/open-webui/issues/23832) +- 🚀 **Faster prompt list loading.** Prompt and prompt-tag pages now load much faster for non-admin users, even with large prompt libraries, because accessible prompts are filtered efficiently in a single database query. [#24288](https://github.com/open-webui/open-webui/pull/24288), [#24258](https://github.com/open-webui/open-webui/discussions/24258) +- ⚡ **Faster chat history loading.** Chat history maps now load from normalized message records when available, reducing overhead for large conversations while preserving fallback behavior for legacy chats. [Commit](https://github.com/open-webui/open-webui/commit/485d689cfd1ef8b9e7f77cd7b535b8b8747dff1f), [#23159](https://github.com/open-webui/open-webui/pull/23159) +- 🗑️ **Delete from conversation menu.** You can now delete the current conversation directly from the chat menu with a confirmation step, so cleanup is faster without searching through the full chat list. [Commit](https://github.com/open-webui/open-webui/commit/ef6d4f2d6c4b79c7e12e864a4fcb6a57ee84e5d4), [#24329](https://github.com/open-webui/open-webui/issues/24329) +- ⬆️ **Scroll to Top shortcut.** Long conversations now include a Scroll to Top action in the chat menu when you are away from the top, making it much faster to jump back to the beginning of a chat. [Commit](https://github.com/open-webui/open-webui/commit/cdfcbc4af6e9aec835b88dc1806a2a46711e6947), [#24133](https://github.com/open-webui/open-webui/issues/24133) +- 📅 **Calendar creation flow.** Users can now create calendars from a dedicated modal and a quick-add action in the calendar sidebar, making calendar setup faster from the calendar workspace. [Commit](https://github.com/open-webui/open-webui/commit/34146ab60f5dc1a2f8bdda8e61ce02797233a25d), [Commit](https://github.com/open-webui/open-webui/commit/1baf73bdd56f4e5ded12a4bd3c168f4d2a70b840) +- 🧭 **Unified model unload controls.** Administrators can now unload running models from the model selector across supported providers, with loaded-state indicators shown for Ollama and llama.cpp models. [Commit](https://github.com/open-webui/open-webui/commit/4fe2de78643c2213652190d2820f4e8d9f4f89cc) +- ⚡ **Health check responsiveness.** Health and readiness probes now avoid blocking database calls and skip sync session commit handling on probe paths, improving responsiveness and reducing false unready transitions during database pressure. [#24380](https://github.com/open-webui/open-webui/pull/24380), [#24384](https://github.com/open-webui/open-webui/pull/24384) +- 🎛️ **Playground controls panel.** The Playground now includes a dedicated Controls toggle so you can adjust parameters like temperature and related settings per chat run without changing model-level defaults. [Commit](https://github.com/open-webui/open-webui/commit/c6763521c00f042a28829e33fb6f1b7355054046), [#24103](https://github.com/open-webui/open-webui/issues/24103) +- 🎙️ **STT file extension controls.** Administrators can now configure which audio file extensions are accepted for speech-to-text uploads, helping enforce safer and more predictable upload policies. [Commit](https://github.com/open-webui/open-webui/commit/4754ece4a2de5bba85a1d53af2dc8d24fdfb58be) +- 📷 **Remembered call camera selection.** Voice call overlay now remembers your last selected camera and restores it automatically when available, so you do not need to reselect it every time you start voice mode. [Commit](https://github.com/open-webui/open-webui/commit/5c3edc2539ac4d92c4cc2d37079549995203238a), [#24416](https://github.com/open-webui/open-webui/issues/24416) +- 👥 **User group prompt variable.** System and template prompts now support the "{{USER_GROUPS}}" variable, which expands to the user’s group memberships so prompts can adapt to role- or access-based context automatically. [Commit](https://github.com/open-webui/open-webui/commit/c1202a23277abb8e7080271a929dcc9d29b67e66), [#24462](https://github.com/open-webui/open-webui/issues/24462) +- 🔐 **Public chat sharing permission control.** Administrators can now control whether users are allowed to create publicly shareable chats through a dedicated permission setting. [Commit](https://github.com/open-webui/open-webui/commit/ef6d4f2d6c4b79c7e12e864a4fcb6a57ee84e5d4) +- 🔐 **Profile image forwarding control.** Administrators can now disable external profile image URL forwarding with the "ENABLE_PROFILE_IMAGE_URL_FORWARDING" setting to prevent browser metadata leaks to third-party servers. [#24420](https://github.com/open-webui/open-webui/pull/24420) +- 🏷️ **Dynamic header template variables.** Administrators can now use chat, message, and user template variables in custom connection and tool server headers so each request can carry per-conversation context automatically. [Commit](https://github.com/open-webui/open-webui/commit/9907c0a25ae830d134af70022238715f834d20c6), [#24164](https://github.com/open-webui/open-webui/pull/24164) +- 🛂 **MCP OAuth server URL setting.** Static OAuth tool server setups can now define a separate OAuth server URL, making discovery and client registration work when authentication endpoints are hosted separately from the tool server URL. [Commit](https://github.com/open-webui/open-webui/commit/9907c0a25ae830d134af70022238715f834d20c6), [#24164](https://github.com/open-webui/open-webui/pull/24164), [#24216](https://github.com/open-webui/open-webui/issues/24216) +- ⚡ **Faster memory query performance.** Per-user memory lookups and deletions now run much faster at scale because the memory user filter is indexed for existing and new installations. [Commit](https://github.com/open-webui/open-webui/commit/38a382ef888685650135d61dcc8ec0e29eb65573), [#23836](https://github.com/open-webui/open-webui/pull/23836) +- 🚀 **Smarter function dependency installs.** Function dependencies are now skipped when they were already preinstalled and unchanged, reducing first-load delays and repeated package installation churn after startup. [Commit](https://github.com/open-webui/open-webui/commit/ae43562b869b24699408e5ab107261a0a8bdb4bc), [#24166](https://github.com/open-webui/open-webui/pull/24166) +- 🔎 **Brave LLM Context web search.** Administrators can now choose Brave LLM Context as a web search provider to retrieve richer grounded passages with a configurable context token budget. [Commit](https://github.com/open-webui/open-webui/commit/6700f7bb72d14a3f8dbb72dfa064cae3b3dc29ac), [#24120](https://github.com/open-webui/open-webui/issues/24120) +- 🗂️ **Open Terminal date sorting.** Open Terminal now includes sort controls for name and date, with directory-first ordering and modified-time visibility to make file browsing faster. [Commit](https://github.com/open-webui/open-webui/commit/6bdc2ffa79d72daf78981209c9c5292c697cbfe5), [#24425](https://github.com/open-webui/open-webui/issues/24425) +- 🎤 **Voice mode prompt toggle.** Administrators can now explicitly enable or disable the Voice Mode custom prompt behavior from Interface settings, giving finer control over how voice replies are guided. [Commit](https://github.com/open-webui/open-webui/commit/17893038869e3a763a8b34457f723b9666804e27) +- 🧮 **LaTeX copy shortcut.** You can now click rendered LaTeX expressions to copy the raw formula to your clipboard, making it easier to reuse equations outside chat. [Commit](https://github.com/open-webui/open-webui/commit/064fdecb675c176a04b024c16ce179f4dda45236), [#24244](https://github.com/open-webui/open-webui/pull/24244) +- ✨ **Smoother rich text editing.** The message composer now defers formatting toolbar refresh work to the next animation frame, reducing typing jank while formatting controls stay accurate. [Commit](https://github.com/open-webui/open-webui/commit/794b97025d4c56f91d49c9d1ec4775d2ea07b53a), [#24013](https://github.com/open-webui/open-webui/pull/24013) +- 🖼️ **Arena model profile images.** Arena models can now reliably display configured profile images instead of falling back to the default icon. [Commit](https://github.com/open-webui/open-webui/commit/1dee67b64d0b34e70bac949682b216c0aaec8152), [#24412](https://github.com/open-webui/open-webui/issues/24412) +- 🔄 **Replaceable tool embed updates.** Pipes and Tools can now overwrite previously emitted rich-UI embeds in-place by passing a `replace` flag on the `embeds` event, enabling live dashboards and progress panels that update without stacking duplicate entries. +- ✏️ **Assistant response editing and continuation.** You can now edit and restructure assistant output items — including reasoning blocks, tool calls, and text content — from a dedicated editor view, and continue generating from the edited state so the model receives full prior context. +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Chinese, Catalan, Filipino, and Korean were enhanced and expanded. + +### Fixed + +- 🧵 **Background code execution reliability.** Code execution no longer hangs indefinitely when you switch conversations or browser tabs during a run, and disconnected or inactive sessions now fail with a clear timeout error instead of endless processing. [Commit](https://github.com/open-webui/open-webui/commit/552bbcecfae5ae273ab98e2ce3e540d0771aa964), [#24089](https://github.com/open-webui/open-webui/issues/24089) +- 🎙️ **Voice recording MIME fallback support.** Voice recording now tries a broader set of browser-supported audio formats and resets halted audio playback cleanly, improving microphone capture reliability in browsers where recording previously failed to start. [Commit](https://github.com/open-webui/open-webui/commit/8ffc3d746f20007e9eb4e3ae4f152f383bc371e1), [#24162](https://github.com/open-webui/open-webui/issues/24162) +- 🧠 **Direct-connection task generation reliability.** Title, tags, follow-up, emoji, query, and related task-generation endpoints now work correctly when chats use direct-connection models instead of failing with model-not-found errors. [Commit](https://github.com/open-webui/open-webui/commit/1b4cd705d0b9a51a5e3a7851ec012fb3141eb0a9), [Commit](https://github.com/open-webui/open-webui/commit/005df577fec16733a64edbec8a1b46f42f4e9a43), [#24092](https://github.com/open-webui/open-webui/issues/24092) +- 🔧 **Parameterized URL tool readiness.** New chats now wait for model defaults to finish applying before auto-submit, preventing early requests that can miss configured external tools. [Commit](https://github.com/open-webui/open-webui/commit/212bb68a66435dc1803a6d67cb4ea584d3455fb7), [#24176](https://github.com/open-webui/open-webui/issues/24176) +- 🚦 **MCP cleanup response reliability.** Successful native MCP tool calls no longer get replaced by a 500 "No response returned" error during cleanup, so valid chat responses are now returned consistently. [#24105](https://github.com/open-webui/open-webui/pull/24105) +- 🧵 **Active task state recovery.** Chat input no longer stays blocked by unrelated background tasks after a response is already complete, and interrupted assistant replies are now marked done more reliably. [Commit](https://github.com/open-webui/open-webui/commit/04bd0425ead28185bcd124e77892e31209a6e15b), [#23264](https://github.com/open-webui/open-webui/pull/23264) +- 📌 **Per-user note pinning behavior.** Pinned notes are now tracked per user instead of with a shared note-level flag, so one person’s pin changes no longer affect everyone else. [Commit](https://github.com/open-webui/open-webui/commit/33e588cf09b294f0abe08b9566efa8545a7dbf92) +- 🧱 **Custom header value coercion.** Custom header values are now converted to text before requests are sent, preventing request failures when non-text values are configured. [Commit](https://github.com/open-webui/open-webui/commit/9907c0a25ae830d134af70022238715f834d20c6), [#24164](https://github.com/open-webui/open-webui/pull/24164) +- 🔗 **HTTP share link copy fallback.** Copy Link now works reliably on HTTP deployments by using a selection-based fallback when secure clipboard APIs are unavailable. [Commit](https://github.com/open-webui/open-webui/commit/f70b0da1563ffa0a8daecbe71cbc30fd8cf834c4), [#24135](https://github.com/open-webui/open-webui/issues/24135) +- 🧵 **Regeneration loading lock recovery.** Chats no longer get stuck in a permanent loading state after failed regenerations because invalid message-tree references are repaired before rendering. [Commit](https://github.com/open-webui/open-webui/commit/ee3b82926b37843f2771c6a8d432781a557ea96a), [#24424](https://github.com/open-webui/open-webui/issues/24424) +- 📸 **Complete chat image capture.** Downloaded chat snapshots now include all messages more reliably through visibility overrides and layout timing improvements during capture. [Commit](https://github.com/open-webui/open-webui/commit/34146ab60f5dc1a2f8bdda8e61ce02797233a25d), [Commit](https://github.com/open-webui/open-webui/commit/1baf73bdd56f4e5ded12a4bd3c168f4d2a70b840), [#24088](https://github.com/open-webui/open-webui/issues/24088) +- 🗓️ **Calendar deletion lock handling.** Calendar deletion now avoids SQLite write-lock contention by revoking calendar access grants in a separate transaction after calendar and event removal. [Commit](https://github.com/open-webui/open-webui/commit/1d892ce2c513c4d933c902de5e5d76c317a06dd2) +- 🧩 **Filter and internal tool coexistence.** Internal tools now remain available when filters add provider-native tools, so filter-added tools no longer replace the built-in tool set during request processing. [Commit](https://github.com/open-webui/open-webui/commit/02f9fe78907c2ecf6f1d93646cbfa2173409bbe8), [#24237](https://github.com/open-webui/open-webui/issues/24237) +- 🛠️ **OpenAPI tool spec compatibility.** OpenAPI tool integrations now handle null or non-operation path entries more safely and parse path-level parameters consistently, preventing crashes and improving tool execution reliability across imperfect OpenAPI specs. [Commit](https://github.com/open-webui/open-webui/commit/2ba6b423aa0c9c800bd96cb638c6ade867cac0f6), [Commit](https://github.com/open-webui/open-webui/commit/5b80932e5951786bb348b91589e8d87753f18905), [#24376](https://github.com/open-webui/open-webui/pull/24376) +- 🧰 **OpenAPI tool schema parsing.** OpenAPI tool imports now ignore non-method path item fields and correctly resolve nested composition schemas, preventing invalid tool parsing for compatible specs. [Commit](https://github.com/open-webui/open-webui/commit/85c7373f68ac3e39a9cd37e63b6926b13fb8b8cc), [#23254](https://github.com/open-webui/open-webui/pull/23254) +- 🌍 **Web search proxy compatibility.** DuckDuckGo search now respects configured proxy environments more reliably, and trust-env behavior defaults to enabled so proxied web loading does not fail unexpectedly. [Commit](https://github.com/open-webui/open-webui/commit/bb0e6cb1085aa3c3da66a5f5ea1cecff7e9b5297), [#23810](https://github.com/open-webui/open-webui/pull/23810) +- 🧾 **Final markdown render flush.** Streaming markdown now forces an immediate final parse when generation completes, preventing stale or partially rendered final output. [Commit](https://github.com/open-webui/open-webui/commit/29f6c72e879d67f23021938e24a21914cc9fb120), [#24088](https://github.com/open-webui/open-webui/issues/24088) +- 🛡️ **Webhook avatar URL validation.** Channel webhook profile image URLs are now validated before saving, preventing invalid or unsafe avatar URLs from being accepted. [#24370](https://github.com/open-webui/open-webui/pull/24370) +- 📝 **System prompt editor scroll stability.** Editing large system prompts no longer jumps the page back to the top, so you can continue editing long model prompts without losing your place. [Commit](https://github.com/open-webui/open-webui/commit/c978a788c8315e37357c93c1b605a2831fc77485), [#23999](https://github.com/open-webui/open-webui/issues/23999) +- 🔎 **Knowledge content search matching.** Knowledge file search now matches both file titles and file content, so relevant files are easier to find even when the keyword is not in the filename. [Commit](https://github.com/open-webui/open-webui/commit/11e076817ae5db34621ce03136353248f7377d97), [#24297](https://github.com/open-webui/open-webui/pull/24297) +- ⚡ **Faster prompt tag loading.** Prompt tag filters now load much faster for non-admin users by fetching only accessible tags directly, avoiding per-prompt permission checks and unnecessary prompt data loading. [#24287](https://github.com/open-webui/open-webui/pull/24287), [#24258](https://github.com/open-webui/open-webui/discussions/24258) +- 🧾 **Citation overflow badge readability.** Citation overflow badges now keep multi-digit counts readable in a single compact bubble, preventing wrapped or cramped display when many sources are attached. [Commit](https://github.com/open-webui/open-webui/commit/23ff9943a9fc8c314100fa074157853fbece1a55), [#24391](https://github.com/open-webui/open-webui/pull/24391) +- 🌐 **Yandex result parsing guard.** Yandex web search no longer fails when some XML fields are missing in individual results, so valid search responses continue to return usable sources instead of dropping to no results. [Commit](https://github.com/open-webui/open-webui/commit/9386fc83a3eff3e55cc157ac8c15c337e3d822c1), [#24243](https://github.com/open-webui/open-webui/issues/24243) +- 🎧 **Safer voice transcription uploads.** Empty or failed voice conversions are now rejected with a clear error instead of continuing as malformed audio, reducing failed transcription attempts from corrupted or near-empty recordings. [Commit](https://github.com/open-webui/open-webui/commit/072d2000f35a9f7b96342fa9bb28f925a92e7b4c) +- 🎚️ **Safer chunked STT processing.** Chunked transcription now limits worker concurrency when no external STT engine is configured, reducing failed transcription behavior caused by overly parallel local processing. [Commit](https://github.com/open-webui/open-webui/commit/55e7c7854bba5182803239c903a0ac2d14426a4c) +- 📈 **Imported chat analytics coverage.** Imported ChatGPT conversations now carry proper model and timestamp metadata and reliably write imported messages into analytics-backed storage, so imported chats are reflected correctly in Admin Analytics totals and model usage views. [Commit](https://github.com/open-webui/open-webui/commit/4d766a3edfa116abcefe7168f1d1284683b860b2), [#24263](https://github.com/open-webui/open-webui/issues/24263) +- 📎 **Knowledge collection persistence.** Knowledge collections selected with the chat input selector now remain attached after reloads and chat switches, so attached context no longer disappears between sessions. [Commit](https://github.com/open-webui/open-webui/commit/7c398a625a8d51f79d80217f6d329fc30c72b782), [#24142](https://github.com/open-webui/open-webui/issues/24142) +- 🧹 **Embedding model name trimming.** Embedding model names entered in Documents settings now automatically trim surrounding whitespace, preventing silent embedding failures caused by accidental trailing spaces. [Commit](https://github.com/open-webui/open-webui/commit/6082e1adaebc8aa3e7f55265c8dc2dbe130c0446), [#24090](https://github.com/open-webui/open-webui/issues/24090) +- 🔊 **PCM TTS playback compatibility.** Text-to-speech audio returned as PCM is now converted to MP3 before delivery, so speech playback works correctly with providers that return raw PCM audio. [Commit](https://github.com/open-webui/open-webui/commit/ff791b4814fc1453df2235ea78016d7015aa6806), [#24143](https://github.com/open-webui/open-webui/issues/24143) +- 🪟 **Windows PostgreSQL startup compatibility.** Windows pip installs using PostgreSQL now start reliably with psycopg async by using a compatible event loop policy instead of the default Proactor loop. [Commit](https://github.com/open-webui/open-webui/commit/7eaecbad5a0913ed04ca3bc10c930bb051dd2bd9), [#24152](https://github.com/open-webui/open-webui/issues/24152) +- ⏱️ **MCP OAuth timeout control.** OAuth token exchanges for MCP tool server connections now respect the configurable client timeout setting, reducing callback failures with slower providers. [Commit](https://github.com/open-webui/open-webui/commit/cde72dab71671645e119564ca9747ce25dd590ad), [#24138](https://github.com/open-webui/open-webui/issues/24138) +- 📄 **PDF text search restoration.** PDF previews now include a proper text layer so browser text selection and find-in-page search work again instead of rendering only image-like pages. [Commit](https://github.com/open-webui/open-webui/commit/bc4d6eef33dcb92719b07483cdb1d63ebf250721), [#24149](https://github.com/open-webui/open-webui/issues/24149) +- 🔑 **Android password autofill support.** Password inputs now expose the expected field name metadata, improving password manager autofill reliability on Android login pages. [Commit](https://github.com/open-webui/open-webui/commit/60ea4214aa42f1ad22142f1a43535007a2293d16), [#24137](https://github.com/open-webui/open-webui/issues/24137) +- 🎤 **Non-blocking STT processing.** Speech-to-text transcription no longer blocks the server event loop during both live transcription and uploaded audio file processing, so other users can continue using chats and live connections under concurrent load. [#24338](https://github.com/open-webui/open-webui/pull/24338), [#24379](https://github.com/open-webui/open-webui/pull/24379), [#24169](https://github.com/open-webui/open-webui/issues/24169) +- 🌐 **SearXNG language parameter handling.** Web searches now send clean multi-language values without trailing separators, so SearXNG requests no longer fail when multiple languages are selected. [Commit](https://github.com/open-webui/open-webui/commit/6dff85b9d205cfc4bc2845dac40909b8d859910c), [#24198](https://github.com/open-webui/open-webui/issues/24198) +- 📂 **File modal open-link behavior.** Clicking a file name in the file details modal now opens the correct file content in a new tab for uploaded file items instead of failing to open. [#24125](https://github.com/open-webui/open-webui/pull/24125) +- 📎 **Chat attachment display recovery.** Files attached by chat tools now appear reliably in assistant responses, including non-image file attachments that were previously hidden. [Commit](https://github.com/open-webui/open-webui/commit/7eeff2fdf945024585a01b72071a61971afc844d), [#24332](https://github.com/open-webui/open-webui/pull/24332) +- 🧱 **Channel embed rendering guard.** Channel message embeds now appear only for model-generated messages and are suppressed in reply previews, preventing unintended embed expansion in regular user posts. [Commit](https://github.com/open-webui/open-webui/commit/e1dce9914745de9b4d2c67b1deddde3472ce4dfa) +- 🛡️ **Safer image URL handling.** Untrusted external image URLs are now blocked in profile and rich-text image rendering paths, preventing unintended client-side requests to attacker-controlled domains. [#24420](https://github.com/open-webui/open-webui/pull/24420) +- 🛡️ **Sanitized spreadsheet HTML previews.** Spreadsheet previews now sanitize generated HTML before rendering, reducing the risk of unsafe content being executed when opening office files in chat and file modals. [#24468](https://github.com/open-webui/open-webui/pull/24468) +- 🧰 **Multi-worker tool update consistency.** Updated tool code now refreshes correctly across workers without requiring a full service restart, so chats no longer run stale tool versions after edits. [Commit](https://github.com/open-webui/open-webui/commit/3309f5d9f11f521c0ee97b64c59a83e3cf390bde), [#24400](https://github.com/open-webui/open-webui/issues/24400), [#24433](https://github.com/open-webui/open-webui/pull/24433) +- 🧩 **Default model metadata env parsing.** The "DEFAULT_MODEL_METADATA" environment setting is now parsed and applied correctly, including when persistent config is disabled, so configured model capability defaults are no longer ignored at startup. [Commit](https://github.com/open-webui/open-webui/commit/0103d7e82cccbd5c4b1c8daabcb3e5160fa74a97), [#24319](https://github.com/open-webui/open-webui/issues/24319) +- 🔄 **Config import and Redis consistency.** Imported settings now remain effective after import because configuration values are immediately synchronized to Redis, preventing stale cached values from overriding imported permissions and settings. [Commit](https://github.com/open-webui/open-webui/commit/55a572cd398c9b4e6118728f8f129941437aa225), [Commit](https://github.com/open-webui/open-webui/commit/1c1c8b18e5cc90ca3c6961a4c193a4363febbc83), [#24346](https://github.com/open-webui/open-webui/issues/24346) +- 🔔 **LDAP signup webhook parity.** New accounts created through LDAP now trigger the same signup webhook notifications as password and OAuth signups, so downstream provisioning and audit automations receive consistent events. [Commit](https://github.com/open-webui/open-webui/commit/fd3368c0bff168417e3c49ffd73491c344702339), [#24377](https://github.com/open-webui/open-webui/issues/24377) +- 🦆 **DDGS auto-backend compatibility.** Web search now handles DDGS automatic backend selection correctly and safely falls back on empty or rate-limited responses, preventing search failures in newer DDGS versions. [Commit](https://github.com/open-webui/open-webui/commit/9adc0c442a57eaa88a5f30c2b2cb393623154e20), [#24188](https://github.com/open-webui/open-webui/issues/24188) +- 🤖 **Automation update tool reliability.** Updating existing automations in chat now works correctly instead of failing with a missing method error. [Commit](https://github.com/open-webui/open-webui/commit/f39f4a86aedc2769d8268670a020b1f3c16776dd), [#24405](https://github.com/open-webui/open-webui/issues/24405#issuecomment-4408011166) +- 📅 **Calendar event permission checks.** Calendar event update and delete actions now handle ownership and access checks more reliably, returning clean access-denied results when appropriate. [Commit](https://github.com/open-webui/open-webui/commit/2977910ffd9d2369dfa504aa6ab12745b3dbd19a) +- 🛡️ **Safer cached file delivery.** Cached files that are not recognized as image, audio, or video now download as attachments instead of rendering inline, reducing the risk of unsafe browser content handling. [Commit](https://github.com/open-webui/open-webui/commit/4754ece4a2de5bba85a1d53af2dc8d24fdfb58be) +- 📊 **Streaming token analytics accuracy.** Admin Analytics now records and aggregates token usage correctly for streaming chats across Responses API and OpenAI-compatible providers, including fallback handling for provider usage formats that use prompt and completion token keys. [Commit](https://github.com/open-webui/open-webui/commit/989d5fd4e2ce285edf4475a1e13f0981a78d3821), [Commit](https://github.com/open-webui/open-webui/commit/a32d26e61d24d9f63650faed5cb8909ed90af661), [#24217](https://github.com/open-webui/open-webui/issues/24217), [#24294](https://github.com/open-webui/open-webui/issues/24294), [#24241](https://github.com/open-webui/open-webui/issues/24241) +- 🔗 **Admin shared chat links.** Admin users can now open and clone shared chat links reliably without 401 errors because shared links are now resolved by share ID first, with safe fallback behavior for direct chat ID access. [Commit](https://github.com/open-webui/open-webui/commit/cde21b9f6dc11575a668484f42440824ec5a4fae), [#24311](https://github.com/open-webui/open-webui/issues/24311), [#24096](https://github.com/open-webui/open-webui/issues/24096) +- 💾 **Chat settings persistence.** System prompts and other chat-level settings now persist correctly after creating a new chat and reloading, preventing prompt loss in affected conversations. [Commit](https://github.com/open-webui/open-webui/commit/86df8bf27e1b84abbe2eeedcc8650df59c7d23d6), [#24193](https://github.com/open-webui/open-webui/issues/24193), [#24270](https://github.com/open-webui/open-webui/issues/24270) +- 💾 **Chat control autosave persistence.** Changes to chat controls like system prompt, parameters, and attached files are now autosaved on existing chats, so edits are no longer lost when you refresh or navigate away before sending a message. [Commit](https://github.com/open-webui/open-webui/commit/a938c8ae2e45a00d2f06151fdaeaee94e54a8095), [#23897](https://github.com/open-webui/open-webui/pull/23897) +- ☁️ **OneDrive option visibility.** OneDrive personal and business upload options now appear only when their respective client IDs are configured, preventing unavailable options from showing in attachment menus. [Commit](https://github.com/open-webui/open-webui/commit/b72019db393a658ca0ceecdcc59b70f6cc5dcd40), [#24411](https://github.com/open-webui/open-webui/issues/24411) +- 🧠 **Reasoning content leakage prevention.** Tool-call round-trip messages no longer wrap reasoning text in `` tags inside the content field, preventing raw markup from leaking into chat output for models whose templates don't strip think tags (e.g. Gemma 4). [#23844](https://github.com/open-webui/open-webui/issues/23844) +- 🖥️ **Terminal sidebar auto-open guard.** The terminal sidebar no longer auto-opens on chat load when OpenTerminal is disabled, because stale terminal IDs saved on models or in localStorage are now validated against available terminal servers before use. +- 🔁 **Single-confirmation connection deletion.** Deleting OpenAI, Ollama, tool server, and terminal server connections now shows exactly one confirmation dialog instead of two, because redundant outer confirmation wrappers were removed from all connection components. +- 🧵 **Reliable background task cleanup.** The chat task lifecycle now deregisters completed tasks before checking for remaining siblings, eliminating the off-by-one timing issue that could leave the stop button stuck or dismiss the sidebar activity spinner too early. + +### Changed + +- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility. +- 🚪 **Signout request method.** The signout endpoint now requires POST instead of GET, so custom clients and integrations must update logout calls accordingly. [#24420](https://github.com/open-webui/open-webui/pull/24420) + +## [0.9.2] - 2026-04-24 + +### Added + +- 🧠 **PaddleOCR-vl document extraction.** Administrators can now use PaddleOCR-vl as a content extraction engine for document processing, with configurable API URL and token settings in document retrieval configuration. [#23945](https://github.com/open-webui/open-webui/pull/23945) +- 🔥 **Firecrawl v2 API.** Firecrawl web loading now uses the v2 API directly with proper retry logic, exponential backoff on rate limits, and configurable timeout handling, improving reliability for both cloud and self-hosted Firecrawl setups. [#23934](https://github.com/open-webui/open-webui/pull/23934) +- ⏰ **Calendar event reminder customization.** Calendar events now support a configurable `reminder_minutes` parameter, allowing models to set custom reminder durations instead of the default 10-minute notification. +- 🔑 **Custom API key header.** Administrators can now configure a custom header name for API key authentication via the `CUSTOM_API_KEY_HEADER` environment variable, enabling compatibility with reverse proxies that use the `Authorization` header for their own authentication. +- 🔌 **OAuth session disconnection.** Users can now disconnect OAuth sessions for specific providers (e.g., MCP connections) through a new API endpoint, enabling cleaner re-authentication workflows. +- 📚 **Source overflow indicator.** The Sources button now shows a +N badge when more than three sources are available, so hidden sources are clearly indicated in chat responses. [#23918](https://github.com/open-webui/open-webui/pull/23918) +- ⚡ **Model list performance.** Model list API responses now strip base64 profile image data from paginated results, and model tags are fetched via a dedicated efficient query instead of loading all models. This significantly reduces payload sizes and improves workspace Models page responsiveness. +- ⚡ **Model avatar cache reuse.** Default model profile images now redirect to a shared static path instead of reading files from disk per-request, reducing repeated I/O and improving loading efficiency when multiple models use the fallback icon. [#24015](https://github.com/open-webui/open-webui/pull/24015) +- 🚀 **Faster splash image loading.** Splash screen images are now prioritized earlier during page load with preload links, improving first-load LCP behavior and reducing delayed image discovery. [#24011](https://github.com/open-webui/open-webui/pull/24011) +- 🧵 **Streaming markdown performance stability.** Streaming responses now stay more memory-efficient by preventing repeated cleanup callback registration during markdown updates. [#24048](https://github.com/open-webui/open-webui/pull/24048) +- 📊 **Telemetry gauge reliability.** OpenTelemetry user gauge callbacks now use synchronous database queries directly, eliminating cross-thread async bridging issues that could cause silent failures in metric collection. +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Finnish, Korean, Portuguese (Brazil), and Dutch were enhanced and expanded. + +### Fixed + +- 🔧 **MCP task cancellation stability.** Interrupted MCP tool calls no longer cause CPU spikes or runaway cleanup behavior. MCP client disconnection now runs in the same asyncio task as connection, respecting cancel scope constraints, and chat-active events are properly shielded during cancellation. +- 🧠 **Persistent chat skill injection.** Skills mentioned in persisted chats now inject into the system prompt reliably. Skill ID extraction from `<$skillId|label>` message tags is now handled server-side, and tags are stripped before messages reach the model. +- 🗄️ **Async database driver migration.** The async database backend now uses psycopg (v3) instead of asyncpg, eliminating brittle SSL parameter translation and supporting native libpq connection strings including `sslmode`, `options`, and `target_session_attrs` without any stripping or conversion. +- 🐳 **Docker ARM64 reliability.** Docker images built for arm64 via QEMU cross-compilation no longer produce 0-byte corrupted Python dependencies. `UV_LINK_MODE=copy` is now set in the Dockerfile to force reliable file installation. +- 🛠️ **Throttle request handling.** Request handling no longer fails when user activity status updates are throttled with a non-zero interval. [#23979](https://github.com/open-webui/open-webui/pull/23979) +- ✍️ **Rich text extension conflicts.** Rich text editing no longer triggers duplicate extension conflicts for lists and code blocks, improving editor stability. [#24009](https://github.com/open-webui/open-webui/pull/24009) +- 🔇 **Fetch URL null content guard.** The `fetch_url` built-in tool now safely handles `None` content returned by web loaders instead of crashing with a `TypeError`. +- 🌐 **OAuth discovery fallback.** OAuth protected resource discovery now falls back to well-known RFC 9728 URIs when the `WWW-Authenticate` header doesn't contain a `resource_metadata` link, improving compatibility with more MCP server implementations. +- 🔐 **Session token resolution.** Session user endpoints now gracefully handle missing `Authorization` headers by falling back to cookie and request state tokens, preventing errors when used behind forward-auth proxies. +- 🚫 **Direct API error responses.** Chat completion requests without a WebSocket channel (direct API calls) now return proper HTTP error responses instead of silently returning null on failure. +- 📡 **Cancelled response stream cleanup.** Cancelled chat generation now explicitly closes the upstream response body iterator, preventing orphaned async generators from spinning in anyio internals. +- 🔒 **Model profile image path safety.** Model profile image endpoints now validate and sanitize static asset redirect paths, preventing path traversal through encoded dots or malicious URL patterns. +- 📊 **RAG template validation UI.** The Documents settings page now displays a warning when RAG templates contain multiple `[context]` or `{{CONTEXT}}` placeholders, helping administrators avoid accidental redundant context injection. +- 🧩 **Automation model detection.** The `create_automation` tool now correctly detects the current model ID even when `model_id` is not yet set in metadata, falling back to the model dict. +- 🔄 **MCP resource content handling.** MCP tool results with the `resource` content type are now correctly detected and their `resource.text` payload is extracted, instead of being silently ignored. +- 🔄 **Ollama and OpenAI metadata forwarding.** Ollama and OpenAI proxy routes now forward request metadata to downstream handlers, ensuring consistent context propagation. +- 🧹 **Browser-native message virtualization.** The custom JavaScript-based message culling system (spacers, height caching, scroll listeners) was replaced with CSS `content-visibility: auto`, letting the browser natively skip rendering of off-screen messages without destroying component trees. This eliminates scroll jump artifacts and mount/destroy thrashing while preserving memory efficiency in long conversations. +- 📻 **Redis notification compatibility.** Redis pub/sub now handles missing or incompatible `client_name` support more gracefully, preventing connection errors with certain Redis configurations. + +### Changed + +- ⚙️ **psycopg v3 async driver.** The async database driver has been migrated from `asyncpg` to `psycopg` (v3). This is a transparent change for most deployments, but custom connection strings with `asyncpg`-specific parameters may need adjustment. +- 🔑 **Brotli dependency update.** Brotli has been updated to address CVE-2025-6176. +- 🖥️ **Windows startup script.** The Windows startup batch script has been updated for improved compatibility. + +## [0.9.1] - 2026-04-21 + +### Fixed + +- 🐛 **Missing `aiosqlite` dependency.** Fixed a startup crash (`ModuleNotFoundError: No module named 'aiosqlite'`) when installing Open WebUI via `pip` or `uv` by adding the missing `aiosqlite` package to `pyproject.toml`. The dependency was listed in `requirements.txt` but not in the published package metadata, so it was not installed automatically. [#23916](https://github.com/open-webui/open-webui/issues/23916) +- 🐛 **Missing `asyncpg` dependency.** Added the missing `asyncpg` package to `pyproject.toml` to prevent the same startup crash for PostgreSQL users. Like `aiosqlite`, it was present in `requirements.txt` but absent from the published package dependencies. + +## [0.9.0] - 2026-04-20 + +### Added + +- 🖥️ **Official Open WebUI Desktop App.** Open WebUI is now available as a native desktop app for Mac, Windows, and Linux. No Docker, no terminal, no setup. Runs Open WebUI locally without any server setup, or connects to your existing remote Open WebUI instances. Switch between multiple servers instantly from the sidebar. Comes with a system-wide floating chat bar (Shift+Cmd+I on macOS, Shift+Ctrl+I on Windows/Linux), system-wide push-to-talk, offline support after first launch, automatic updates, and zero telemetry. [#8262](https://github.com/open-webui/open-webui/issues/8262), [Desktop](https://github.com/open-webui/desktop) +- 🤖 **Scheduled chat automations.** You can now schedule the AI to run tasks automatically on a recurring basis: daily digests, periodic reports, anything you'd otherwise need to remember to ask for. Create and manage automations from the Automations page or directly in chat, with full run history and manual trigger controls. [#23303](https://github.com/open-webui/open-webui/pull/23303), [Commit](https://github.com/open-webui/open-webui/commit/5a2ff8b2e5b6f55a20f7ed491f818490eb535ea7), [Commit](https://github.com/open-webui/open-webui/commit/d30a0531d4add045c21a2368d6321a9b1906865f), [Commit](https://github.com/open-webui/open-webui/commit/bae5ff938ac88a3a647cc31ca8db1101015ae18b), [Commit](https://github.com/open-webui/open-webui/commit/588b81eedaacbfd7394b707ae1600d9fb729b809..674695918e5e3e1811314ce2a082c5bbb42d76b2) +- 🧰 **Automation tools in chat.** Built-in chat tools can now create, update, list, pause, and delete scheduled automations directly in conversation when automation access is enabled. [Commit](https://github.com/open-webui/open-webui/commit/588b81eedaacbfd7394b707ae1600d9fb729b809..674695918e5e3e1811314ce2a082c5bbb42d76b2) +- ⏱️ **Automation scheduling limits.** Administrators can now set "AUTOMATION_MAX_COUNT" and "AUTOMATION_MIN_INTERVAL" to limit how many automations each non-admin user can create and prevent overly frequent schedules that could overload the system. [Commit](https://github.com/open-webui/open-webui/commit/406251c2f358ffabce4d631c98c6f2c879feae5c) +- 📋 **Task management tool.** AI models can now create, update, and track tasks within a chat conversation, breaking down complex requests into manageable steps with real-time status updates. [Commit](https://github.com/open-webui/open-webui/commit/bcb71bb5206ac01d97a39fde8ecf0e0541dde636) +- 🗓️ **Calendar workspace and event management.** Open WebUI now has a full Calendar workspace. Create and manage events, set up recurring schedules, get reminders via in-app toasts or browser notifications, and see your scheduled automations alongside your calendar. [#23880](https://github.com/open-webui/open-webui/pull/23880) +- 🔔 **Calendar reminders and alerts.** Calendar events now support reminder options from no alert up to one hour before start time, with upcoming alerts delivered through in-app toasts, browser notifications, and optional webhooks while avoiding duplicate sends. [Commit](https://github.com/open-webui/open-webui/commit/e5b5a174265d6710e986f6534ee7e3b2923233be) +- ⚙️ **Scheduler reminder configuration.** Administrators can now configure calendar reminder processing with "SCHEDULER_POLL_INTERVAL" and "CALENDAR_ALERT_LOOKAHEAD_MINUTES", while existing "AUTOMATION_POLL_INTERVAL" setups continue to work as a legacy fallback. [Commit](https://github.com/open-webui/open-webui/commit/e5b5a174265d6710e986f6534ee7e3b2923233be) +- ☁️ **Azure responses support.** Azure OpenAI connections now support the newer "/openai/v1" format, enabling chat, responses, and proxy calls to work correctly with that endpoint style. [#23484](https://github.com/open-webui/open-webui/pull/23484) +- 🤖 **Ollama responses support.** The Ollama proxy now supports the Responses API, letting clients use "/v1/responses" directly with Ollama-hosted models through Open WebUI. [#23483](https://github.com/open-webui/open-webui/pull/23483) +- 🧩 **Responses tool output rendering.** Built-in tool outputs in Responses API flows now render more consistently so downstream chat output is easier to interpret. [Commit](https://github.com/open-webui/open-webui/commit/e695d854f2d11fada84d5fbec8d3edea4e468e19), [#23482](https://github.com/open-webui/open-webui/pull/23482) +- 🔎 **Responses citation visibility.** Responses API flows now emit citation sources more consistently, making linked references easier to preserve and display in chat output. [Commit](https://github.com/open-webui/open-webui/commit/e695d854f2d11fada84d5fbec8d3edea4e468e19), [#23774](https://github.com/open-webui/open-webui/issues/23774) +- 📎 **Attach previously uploaded files.** The chat input menu now includes a Files tab for browsing and attaching previously uploaded files, eliminating the need to re-upload files you have already shared. [Commit](https://github.com/open-webui/open-webui/commit/edb8971c7dbd974322c3207c4655ff66479c3ee2) +- 🧷 **Default model terminal selection.** Workspace model editors can now preselect an Open Terminal connection, so new chats automatically start with the model’s configured terminal ready to use. [Commit](https://github.com/open-webui/open-webui/commit/47d413ce7b2a006a8126f4a9055b13e5fcb33a1d), [#23605](https://github.com/open-webui/open-webui/issues/23605) +- 🎙️ **Mistral TTS support.** Mistral can now be used as a text-to-speech provider, with admin settings for the API key, base URL, voices, and model selection. [Commit](https://github.com/open-webui/open-webui/commit/4cee67e2be0c80a0b501073ea49a80d13efd1c41) +- 🎧 **STT preprocessing bypass option.** Administrators can now enable "AUDIO_STT_SKIP_PREPROCESSING" to send audio files directly to the speech-to-text backend, reducing memory and CPU consumption during large uploads for better transcription performance and stability on constrained deployments. [#23661](https://github.com/open-webui/open-webui/pull/23661) +- 🗑️ **Admin model deletion.** Administrators can now delete Ollama models directly from the model selector menu, making it easier to clean up unused or unwanted models. [Commit](https://github.com/open-webui/open-webui/commit/2388dd7dc3530b5dd5419c5d0bb1bcdcb7544099) +- 🔌 **Backend outlet filters for local and persisted chats.** Pipeline and function outlet filters now run reliably in backend completion flows for persisted chats and temporary local chats. [#3237](https://github.com/open-webui/open-webui/issues/3237), [Commit](https://github.com/open-webui/open-webui/commit/cf4218e688def6f11d195aeda6665ae5b5376b67) +- 🎨 **Emoji shortcode support.** Typing a colon in the chat input now opens an emoji suggestion menu, making it easier to insert emojis using shortcodes like :wave:. [Commit](https://github.com/open-webui/open-webui/commit/2040095050056d01c61aa597c5010445449a42c7) +- 📌 **Recently used emojis.** The emoji picker now shows your most recently used emojis at the top, making it faster to find emojis you use often. [Commit](https://github.com/open-webui/open-webui/commit/64da99a32218171d41b3af5acc14783de8dbdf49) +- 👆 **Swipe to reply on mobile.** Swiping right on a message now triggers a reply, making it easier to respond on touch devices with a natural gesture. [Commit](https://github.com/open-webui/open-webui/commit/012ce95f27d57bea8911bd63bfb923443c5797ae) +- 📱 **Screen-awake voice recording.** Voice recording now keeps the screen awake during active dictation and safely re-acquires wake lock after visibility changes, helping prevent long transcriptions from being cut off on mobile devices. [#23145](https://github.com/open-webui/open-webui/issues/23145) +- 🔔 **Unread chat indicators.** Sidebar chats now show unread status and are marked as read when opened, making it easier to spot conversations with new activity. [Commit](https://github.com/open-webui/open-webui/commit/0638b9f56ce1ba8a496d0e84da2e7fa178b01a3f) +- 🔌 **WebSocket reconnect status feedback.** Open WebUI now warns when the real-time connection drops and confirms when it reconnects, while avoiding a reconnect message on the initial page load. [Commit](https://github.com/open-webui/open-webui/commit/1824e69a70e756cfcf543a9fbe4b0780d9b57292) +- 📍 **Pinned notes in sidebar.** Notes can now be pinned to the sidebar for quick access, and you can also create a new note directly from the pinned notes section. [Commit](https://github.com/open-webui/open-webui/commit/ecd74f220c7dd671d5705189a3f4493a3868c8bf), [Commit](https://github.com/open-webui/open-webui/commit/f1be85d997439b49fc143d2bcd2dc710f44446c8) +- 🗂️ **Model selector focus.** The model selector now resets its search only when it opens, making the popup feel more predictable while still focusing the search field automatically. [Commit](https://github.com/open-webui/open-webui/commit/b89019a8e1f96e01dc8e19a81ef8fb4f4eae3eef) +- 🗂️ **Model selector layout.** The model selector now behaves more predictably as a custom popup, and the completions playground uses a simpler model picker for easier selection. [Commit](https://github.com/open-webui/open-webui/commit/c40ea7f29d34fa9535cdf9ffe599f4429ff3f455) +- 🎚️ **Active filter valve shortcut.** Active filter badges now expose valve configuration directly in the chat input area, so filter tuning is faster during conversations. [Commit](https://github.com/open-webui/open-webui/commit/3c22afc5a67404047797921185aca984b10b45cd), [#23811](https://github.com/open-webui/open-webui/issues/23811), [#23813](https://github.com/open-webui/open-webui/pull/23813) +- 🎨 **Theme updates.** Other windows can now update the app theme directly, keeping the interface in sync when theme changes are triggered externally. [Commit](https://github.com/open-webui/open-webui/commit/9f1b279e88bd22dfff4d2531209536dea6a2f65e) +- 🚀 **Async performance and responsiveness improvements.** The core backend database and request paths now run asynchronously across the application, massively improving responsiveness and performance under concurrent load and reducing request blocking during heavy activity. [Commit](https://github.com/open-webui/open-webui/commit/27169124f220e5cea21c88601c731c3749496ab0), [Commit](https://github.com/open-webui/open-webui/commit/8936721414a17832852a90f3ee592af5a8b7232d) +- ⚡ **Drawer performance and memory optimization.** Drawer interactions now stay smoother over long sessions by removing stale keyboard listeners on teardown, which reduces memory growth and avoids accumulated event handling overhead. [#23724](https://github.com/open-webui/open-webui/pull/23724#issuecomment-4245840810) +- 🚀 **Chat history memory culling.** Long conversations now stay responsive no matter how many messages they contain. Off-screen messages are unloaded automatically and reloaded as you scroll, keeping memory usage low and the UI smooth on both desktop and mobile. [#23067](https://github.com/open-webui/open-webui/issues/23067), [Commit](https://github.com/open-webui/open-webui/commit/026903399be73ac4b6c226647110e5662d043a50), [Commit](https://github.com/open-webui/open-webui/commit/9dccd29c94875e6f0ac373c5802cb183296e47ff) +- 🧵 **Async file and knowledge processing performance.** File processing, knowledge reindexing, and channel message helper paths now consistently await async operations, preventing skipped processing steps and improving reliability and performance of indexing and tool responses. [Commit](https://github.com/open-webui/open-webui/commit/de27a121511a31606f250ba4033490797216a0eb) +- 🚀 **Persistent chat payload efficiency.** Persisted chats now use server-side history loading instead of repeatedly resending full message payloads, improving multimodal performance and reducing stale-history overwrite risk across devices. [#19064](https://github.com/open-webui/open-webui/issues/19064), [Commit](https://github.com/open-webui/open-webui/commit/18fe17127a7175579506e7456d3e5aba201371e6), [Commit](https://github.com/open-webui/open-webui/commit/cf4218e688def6f11d195aeda6665ae5b5376b67) +- 🧵 **Non-blocking file storage operations.** Uploading, reading, transcribing, and deleting files now offloads storage I/O to background threads, keeping the application responsive during file-heavy workflows. [Commit](https://github.com/open-webui/open-webui/commit/4866bec0f238198a721c952fe18dd04ba643be33) +- 🏎️ **Streaming response performance.** Streaming responses now process each output line in a single step instead of two separate yields, reducing async overhead and improving responsiveness during long-running generations. [#23266](https://github.com/open-webui/open-webui/pull/23266) +- 🔎 **Faster mention parsing.** Chat text with HTML-like content, file paths, or tool output now parses mentions more efficiently, which helps keep typing and rendering responsive in messages that contain many '<' characters. [#23551](https://github.com/open-webui/open-webui/pull/23551) +- 🧪 **Code block rendering performance.** Code blocks now reuse a shared HTML unescape helper, reducing extra browser work when displaying encoded output in chat. [#23553](https://github.com/open-webui/open-webui/pull/23553) +- 🚀 **Inline code rendering performance.** Inline code tokens in streaming responses now fade in with a lightweight CSS animation, making chat output feel smoother while reducing interface overhead during rapid token updates. [#23258](https://github.com/open-webui/open-webui/pull/23258) +- 🎞️ **Streaming text token animation performance.** Streaming text tokens now use a lightweight CSS intro animation, making output feel smoother while reducing transition overhead and preventing tokens from fading out when generation completes. [#23257](https://github.com/open-webui/open-webui/pull/23257) +- 🎯 **Template token scan optimization.** Streaming responses now skip unnecessary token-replacement processing when no template markers are present, reducing per-update overhead and keeping chat output smoother during rapid generation. [#23161](https://github.com/open-webui/open-webui/pull/23161) +- 🔬 **Chinese text processing guard performance.** Streaming responses without Chinese characters now skip unnecessary Chinese-format processing checks, reducing per-update overhead and keeping output smoother during rapid generation. [#23162](https://github.com/open-webui/open-webui/pull/23162) +- 🧠 **HTML entity decode performance.** Streaming text decoding now avoids repeated document parsing for HTML entity handling, reducing memory churn and improving responsiveness in token-heavy chat output. [#23165](https://github.com/open-webui/open-webui/pull/23165) +- 🏷️ **Chat title update performance.** Chat title updates now run in a single database operation instead of multiple round trips, improving responsiveness and reducing overhead when titles are generated or renamed. [#23214](https://github.com/open-webui/open-webui/pull/23214) +- 📂 **Faster chat list queries performance.** Chat and folder lists now load more efficiently by fetching only the fields needed for sidebar views, improving responsiveness when browsing large conversation histories. [Commit](https://github.com/open-webui/open-webui/commit/0e5696de74cc0ba55b24cfc3d02efa83f08d7d3f) +- 📈 **Sidebar memory optimization.** Sidebar chat items now use shared drag-preview resources and safer listener cleanup, reducing memory growth and keeping large chat lists more responsive during long sessions. [#23209](https://github.com/open-webui/open-webui/pull/23209) +- 🧠 **Image viewer memory optimization.** Viewing images and SVGs now uses significantly less memory and performs faster, keeping the application snappy and responsive even when browsing through many media files during extended sessions. [#23236](https://github.com/open-webui/open-webui/pull/23236) +- 📡 **Optimized user activity tracking performance.** User activity updates now use a single database query instead of multiple operations, improving response times across all authenticated requests. [#23215](https://github.com/open-webui/open-webui/pull/23215) +- 👥 **Faster channel thread author loading.** Channel thread responses now load author details in a single batch query, reducing database overhead and improving responsiveness in threads with many participants. [#23795](https://github.com/open-webui/open-webui/pull/23795) +- 💨 **Optimized shared chat deletion.** Deleting shared chats by user is now faster and more memory-efficient by only loading necessary data. [#23216](https://github.com/open-webui/open-webui/pull/23216) +- 🗃️ **Faster chat tag loading.** Chat tag lookups now load only the metadata needed instead of full chat payloads, improving responsiveness for chats with large histories. [#23798](https://github.com/open-webui/open-webui/pull/23798) +- 📎 **Faster chat file deduplication.** Attaching files to chat messages now checks duplicates more efficiently, reducing overhead when handling larger file lists. [#23800](https://github.com/open-webui/open-webui/pull/23800) +- 📈 **Faster message diff checks.** Chat message and status updates now compare content more efficiently during streaming, making active conversations feel smoother and more responsive. [#23370](https://github.com/open-webui/open-webui/pull/23370) +- ⚖️ **Faster deep equality checks.** Chat message updates, model selection, note editing, code block refreshes, and rich text state comparisons now use deep equality checks that reduce unnecessary UI work and improve responsiveness in active sessions. [#23845](https://github.com/open-webui/open-webui/pull/23845) +- 🏃 **Faster knowledge access updates.** Updating access grants for knowledge items now completes with less backend overhead, making permission changes apply more quickly. [#23799](https://github.com/open-webui/open-webui/pull/23799) +- 🧹 **Mermaid render cleanup performance.** Mermaid diagrams now always clean up temporary render elements after failures, reducing DOM buildup and keeping repeated rendering more stable over time. [#23727](https://github.com/open-webui/open-webui/pull/23727) +- 🖼️ **Model image lookup efficiency.** Model profile image requests now reuse the current request database session, reducing per-request overhead and improving response efficiency. [#23796](https://github.com/open-webui/open-webui/pull/23796) +- 👤 **User endpoint query reduction.** Session-based user settings and status endpoints now avoid redundant user re-fetches, reducing unnecessary database load while preserving behavior. [#23794](https://github.com/open-webui/open-webui/pull/23794) +- 🚦 **Faster startup performance.** Open WebUI now checks for Torch MPS support only on macOS, avoiding unnecessary startup work on other platforms. [#23438](https://github.com/open-webui/open-webui/pull/23438) +- 🛡️ **Redis timeout consistency.** Redis connections now honor the "REDIS_SOCKET_CONNECT_TIMEOUT" setting across standard and cluster setups, helping workers fail faster when Redis is unreachable. [#23572](https://github.com/open-webui/open-webui/pull/23572) +- 🧰 **AIOHTTP pool controls.** Administrators can now tune shared outbound HTTP connection behavior with "AIOHTTP_POOL_CONNECTIONS", "AIOHTTP_POOL_CONNECTIONS_PER_HOST", and "AIOHTTP_POOL_DNS_TTL" for better control under high concurrency. [Commit](https://github.com/open-webui/open-webui/commit/c47dd7b7717c4186e0f0549ca3c8cb4d9bb38135) +- ⏱️ **MCP tool server timeout configuration.** Administrators can now configure request timeouts for MCP tool server connections via the AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER environment variable. [Commit](https://github.com/open-webui/open-webui/commit/10b4b86ada93cd62d994c3179ff14dfd1a6e56f0) +- 🎫 **Static OAuth tool authentication.** Tool server authentication now works reliably for both "oauth_2.1" and "oauth_2.1_static" connection types, so OAuth-backed tool access is correctly detected and forwarded during chat requests. [Commit](https://github.com/open-webui/open-webui/commit/60676bfdcfbce1a69b3e97f2013f0cfd63371737) +- 🗄️ **Configurable storage local cache.** Administrators can now disable persistent local caching for cloud-backed uploads with the "STORAGE_LOCAL_CACHE" setting, reducing local disk usage by cleaning temporary upload copies after processing. [Commit](https://github.com/open-webui/open-webui/commit/8172c7e3d56918d1372be06b9369b58a3a88f6b1) +- 🚪 **Back-channel logout.** OpenID Connect providers can now trigger centralized logout through the "ENABLE_OAUTH_BACKCHANNEL_LOGOUT" setting, helping administrators invalidate user sessions more reliably across connected devices. [Commit](https://github.com/open-webui/open-webui/commit/0dd9f462ffb2f160bc4aebad182047f41874d250) +- 🛡️ **Expanded security header controls.** Administrators can now configure additional browser security headers, including "CONTENT_SECURITY_POLICY_REPORT_ONLY", "CROSS_ORIGIN_EMBEDDER_POLICY", "CROSS_ORIGIN_OPENER_POLICY", and "CROSS_ORIGIN_RESOURCE_POLICY", for stricter and more flexible deployment hardening. [Commit](https://github.com/open-webui/open-webui/commit/f246a66810fa4995d9494da3599c0fb297fb0213) +- 🖼️ **Image MIME fallback option.** Administrators can now enable "ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK" so image-to-base64 conversion can still detect common image types by file extension when MIME metadata is missing, improving compatibility on minimal container images and older file records. [Commit](https://github.com/open-webui/open-webui/commit/5127354b3eb4eaa71bc4ad68da69729e2196e7a4) +- 🛡️ **Public sharing permissions.** Public channels, models, notes, prompts, and tools now respect allowed access grants more consistently, helping administrators control who can share content more safely. [Commit](https://github.com/open-webui/open-webui/commit/9d3e0637c86292b8b92e7607097a83f1075d7cd8) +- 🆔 **Skill lookup by ID.** Skill instructions now include each skill’s ID, and the skill viewer now finds skills by ID in a case-insensitive way so attached skills are identified more reliably in chats. [Commit](https://github.com/open-webui/open-webui/commit/65ee771fd0d62d785ecbcf189e3f5b63858c11e6) +- 🏷️ **Source context metadata.** Retrieval source context now includes each source’s resource type and resource ID metadata, helping downstream model workflows preserve richer source identity during processing. [Commit](https://github.com/open-webui/open-webui/commit/c3c8c605d76a3b0ee067307f9cef6d081658e287) +- 🗂️ **Feedback filtering.** Administrators can now filter feedback history by model and export only the feedback they need. [Commit](https://github.com/open-webui/open-webui/commit/60e4d7517463690b3a87de38babc9ac561897c61) +- 📤 **CSV feedback export.** Feedback history can now be exported as either JSON or CSV, making it easier to analyze feedback in spreadsheet tools. [Commit](https://github.com/open-webui/open-webui/commit/342582676a5212bf196a69d11825cb407992f257) +- 📝 **Optional GET audit logging.** Administrators can now enable auditing for GET requests with the "ENABLE_AUDIT_GET_REQUESTS" setting when they need fuller request visibility. [Commit](https://github.com/open-webui/open-webui/commit/5ee791d5d28f236755243cb7d16d8737bb69ce36) +- 🕒 **Model access updates.** Changing a model’s access grants now updates its timestamp, so recently modified models stay easier to find and sort correctly. [Commit](https://github.com/open-webui/open-webui/commit/53eadb7df7281f5661cbe22c8b26b5aedaba3083) +- 💬 **Queued message handling.** Queued chat messages now send more reliably without advancing the queue too early, keeping follow-up prompts in the intended order. [Commit](https://github.com/open-webui/open-webui/commit/730e52a431d157dc62d72260668087437f1d52f4) +- 🔒 **Rendered content safety.** Placeholder descriptions and the pending account notice now render markdown with safer sanitization ordering, reducing the risk of unsafe HTML appearing in these views. [Commit](https://github.com/open-webui/open-webui/commit/253f416de3f2d3a939a6feef2a56413fd61cc70b) +- 🛡️ **Safer placeholder rendering.** Chat placeholder descriptions and the pending account notice now sanitize rendered markdown more consistently, reducing the risk of unsafe content being shown in these views. [Commit](https://github.com/open-webui/open-webui/commit/ae0316a30e01a2e5ff3f9d2f9f759c1cd6410f34) +- 🧮 **Usage analytics accuracy.** Token usage is now normalized before chat messages are saved, so model and user usage reports stay accurate across OpenAI-compatible providers. [Commit](https://github.com/open-webui/open-webui/commit/4dea4fdf54e00ebaba8e3178128bf8709453d2a2) +- 🧩 **Richer Anthropic tool results.** Anthropic-compatible tool calls now preserve more tool result content types, including images and structured search or document outputs, so models can use fuller tool context instead of receiving only plain text fragments. [#23188](https://github.com/open-webui/open-webui/issues/23188), [Commit](https://github.com/open-webui/open-webui/commit/40f5b3d135190dc9a2d8e94dbb1b2cbcbd829132) +- 🖼️ **ComfyUI request reliability.** ComfyUI image generation and editing now use shared async connections with consistent SSL handling, making image uploads and workflow runs more reliable under concurrent load. [Commit](https://github.com/open-webui/open-webui/commit/5944eda0ff25a284f7157252683bccede741cbe7) +- 🎛️ **Reranking batch size control.** Administrators can now set "RAG_RERANKING_BATCH_SIZE" in Documents settings to control reranking workload size, helping balance retrieval speed and resource usage for their deployment. [Commit](https://github.com/open-webui/open-webui/commit/4d2f18981051205016bd24d39521e25a33581225) +- 🔗 **Shared chat access controls.** You can now control who has access to a shared chat by granting access to specific users or groups, instead of sharing with anyone who has the link. +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Irish, Catalan, German, Simplified Chinese, Hindi, and Portuguese (Brazil) were enhanced and expanded. + +### Fixed + +- 🛡️ **Model description XSS protection.** Model descriptions shown in chat placeholders are now sanitized before rendering, preventing malicious links from executing scripts and helping protect user sessions from takeover. [#23621](https://github.com/open-webui/open-webui/pull/23621) +- 🧠 **Memory search filtering.** Memory search now correctly filters by the query text instead of returning unrelated results. [Commit](https://github.com/open-webui/open-webui/commit/43e5905c133049036353978704b0abd179716749), [#23826](https://github.com/open-webui/open-webui/issues/23826) +- 📊 **Shared chat analytics consistency.** Usage and message-count analytics now count assistant activity consistently across regular and shared chats, improving accuracy in model, user, chat, and time-based reporting views. [Commit](https://github.com/open-webui/open-webui/commit/e29d145a1cff23122de16123a4cfda1b84abffbb) +- 🧭 **Safer in-flight chat navigation.** Sending a message no longer overwrites your active chat or causes duplicate background notifications when you switch conversations before a response finishes. [Commit](https://github.com/open-webui/open-webui/commit/dc6df52a917b49fa1264ac81a8cc74603f6155b3) +- 🗣️ **Pipeline error detail visibility.** Pipeline inlet and outlet failures now preserve and surface provider error details more reliably in chat error messages, making troubleshooting failed requests much clearer. [Commit](https://github.com/open-webui/open-webui/commit/d5e69f182cd7a6371ab25248f6432b277f83ef23) +- 📨 **Shared chat event routing.** Message update and send events now target the chat owner’s event channel, so shared chats receive the correct real-time updates instead of routing events to the acting user. [Commit](https://github.com/open-webui/open-webui/commit/47329b5032ba29716a7e7e973b07c6d9894968e0) +- 🔐 **Consistent outbound SSL handling.** External requests for tools, functions, terminals, webhooks, retrieval loaders, audio provider discovery, and OpenAI-compatible embedding calls now consistently apply the configured SSL client setting, improving reliability for deployments that require custom certificate or verification behavior. [Commit](https://github.com/open-webui/open-webui/commit/fd25152076ea7c310e42c9bacc5cd2b544eeae48), [Commit](https://github.com/open-webui/open-webui/commit/56c5bc1d3487020ab886d3332aacc1644c1d6123) +- 🖼️ **Image SSL setting support.** Image generation now respects the configured SSL session setting, preventing avoidable connection failures in strict certificate environments. [Commit](https://github.com/open-webui/open-webui/commit/128cf41fcedf2638fc8a6acd850d8b0409be1c4e), [#23777](https://github.com/open-webui/open-webui/issues/23777) +- 🗂️ **Folder ownership assignment hardening.** Folder create and update inputs now reject unexpected extra fields, preventing clients from overriding protected values like ownership through mass-assignment payloads. [#23648](https://github.com/open-webui/open-webui/pull/23648) +- 🔐 **Knowledge file deletion ownership checks.** Collaborators with knowledge base write access can no longer permanently delete files they do not own, preventing unintended file removal across other linked chats and knowledge bases. [Commit](https://github.com/open-webui/open-webui/commit/914ccf07ef158afe5588b97ed42778c93c439938), [#23636](https://github.com/open-webui/open-webui/pull/23636#issuecomment-4232439454) +- 🗑️ **Knowledge deletion reliability.** Deleting a knowledge base by ID now completes reliably without unexpected failures. [Commit](https://github.com/open-webui/open-webui/commit/7e453de4f7794ff386e285aa5951b94e926ec273), [#23776](https://github.com/open-webui/open-webui/issues/23776), [#23814](https://github.com/open-webui/open-webui/pull/23814) +- 🔐 **OAuth 2.1 PKCE enforcement.** OAuth 2.1 providers now default to S256 PKCE even when discovery metadata omits supported challenge methods, preventing login failures with providers that require PKCE by default. [#23667](https://github.com/open-webui/open-webui/issues/23667), [Commit](https://github.com/open-webui/open-webui/commit/050c4b97a95addc5eaeef86ba00631673a90dec4) +- 🔐 **Static OAuth scope handling.** Static OAuth credential flows now prioritize administrator-defined scopes and handle OAuth 2.1 static flow behavior more reliably. [Commit](https://github.com/open-webui/open-webui/commit/349ea4ea9e577f2cbfb4917ef5f52e5ac53c5b70), [#23668](https://github.com/open-webui/open-webui/issues/23668), [#23696](https://github.com/open-webui/open-webui/pull/23696), [#23783](https://github.com/open-webui/open-webui/pull/23783) +- 🔐 **Static OAuth tool registration reliability.** Static OAuth tool server registration now resolves and uses saved admin credentials more reliably, preventing registration failures when valid client credentials are provided. [#23670](https://github.com/open-webui/open-webui/issues/23670), [Commit](https://github.com/open-webui/open-webui/commit/2943955c529138c0e530fd07b6333a0052e3684e), [Commit](https://github.com/open-webui/open-webui/commit/c767bcaa739f76b1a4337dfd9d6be47adb504825) +- ⏳ **OAuth token expiry fallback.** OAuth sessions now always store a safe expiry value even when providers omit "expires_in" or "expires_at", so token refresh checks continue working and tool calls are less likely to fail later with unexpected authorization errors. [#23669](https://github.com/open-webui/open-webui/issues/23669), [Commit](https://github.com/open-webui/open-webui/commit/31406caa795173a59d5843d3601b891bf617cbaa) +- 🔑 **Anthropic x-api-key model access.** Anthropic-compatible clients can now authenticate with the "x-api-key" header across all relevant API routes, so model listing requests like GET "/api/v1/models" no longer fail with unauthorized errors. [#23319](https://github.com/open-webui/open-webui/issues/23319), [Commit](https://github.com/open-webui/open-webui/commit/611fe0c8a938539b73b559e84964f40c30bf436d) +- 🔑 **SSO password option visibility.** Account settings now hide password change controls when password-change access is disabled, avoiding misleading password options for SSO-focused setups. [#15292](https://github.com/open-webui/open-webui/issues/15292), [Commit](https://github.com/open-webui/open-webui/commit/cced77b584d6ea46c58fecddb2b3dd5e955c8417) +- 🔑 **Open Terminal MCP authentication.** Open Terminal MCP tool calls now include the configured API key when calling internal routes, preventing unauthorized errors for commands like file reads and command execution. [#106](https://github.com/open-webui/open-terminal/pull/106) +- 🧯 **Provider error freeze recovery.** Task-based chat requests now surface provider HTTP errors through normal failure handling, so content-filter and other upstream 4xx responses no longer leave chats stuck in a perpetual loading state. [#23663](https://github.com/open-webui/open-webui/issues/23663), [Commit](https://github.com/open-webui/open-webui/commit/96265cf042c8ab97dbec5d0efcce8010d0cd76e5) +- 🔄 **Immediate outlet filter updates.** Assistant messages modified by outlet filters now appear correctly as soon as streaming completes, without requiring a page refresh. [#23829](https://github.com/open-webui/open-webui/pull/23829) +- 🌊 **Middleware cancellation reliability.** Long-running requests now complete more reliably by preventing middleware-level cancellations from interrupting in-flight database and embedding work, reducing unexpected failures and noisy error logs when connections close early. [#23709](https://github.com/open-webui/open-webui/pull/23709) +- 🚦 **Async vector search responsiveness.** File processing, memory updates, and knowledge retrieval no longer block the server event loop during vector database operations, so other chats and requests stay responsive while indexing or search is running. [#23706](https://github.com/open-webui/open-webui/pull/23706) +- 🗒️ **Notes chat llama.cpp compatibility.** Notes AI chat no longer sends empty assistant prefill messages that can conflict with reasoning-enabled llama.cpp responses, preventing immediate 400 errors in Notes conversations. [Commit](https://github.com/open-webui/open-webui/commit/fd93bd3414a1725219e14561bc5640b62f9fd4a1), [#23703](https://github.com/open-webui/open-webui/issues/23703#issuecomment-4243907629) +- 🧩 **Ollama thinking field preservation.** Messages modified by filters now keep the Ollama "thinking" field when sent to the model, so reasoning-aware workflows and custom filter-based passthrough setups work reliably. [Commit](https://github.com/open-webui/open-webui/commit/8bd23b91459914eb7df5b5a66567d3544e0da168), [#22508](https://github.com/open-webui/open-webui/issues/22508) +- 🧾 **Reasoning content preservation.** Assistant tool-call messages now retain reasoning content across turns, improving reliability for reasoning-heavy model workflows. [Commit](https://github.com/open-webui/open-webui/commit/3dd8255816898467246c81cba3c9bc48bc18d86d), [#23175](https://github.com/open-webui/open-webui/issues/23175), [#23742](https://github.com/open-webui/open-webui/pull/23742) +- 🧭 **Background task scoping for new chats.** Chat title and auto-tag generation now run only for the first message of a new conversation and only once in multi-model responses, preventing duplicate or incorrectly triggered background tasks in follow-up flows. [Commit](https://github.com/open-webui/open-webui/commit/f102060a6d85db4acd3d0bf5c25e976f36cd5533..a4ed16999eec9a654a37c2bb4c15ba5ecd1fa3b7) +- 📚 **Channel document context retention.** Channel conversations now preserve and load the correct stored message history so model responses can use uploaded and retrieved document context more reliably. [#23686](https://github.com/open-webui/open-webui/issues/23686), [Commit](https://github.com/open-webui/open-webui/commit/cf4218e688def6f11d195aeda6665ae5b5376b67), [Commit](https://github.com/open-webui/open-webui/commit/18fe17127a7175579506e7456d3e5aba201371e6) +- ⏳ **Interrupted response recovery.** Assistant placeholder messages now start as incomplete and recover more safely after interrupted generations, preventing silent empty replies after refreshes or dropped requests. [#23176](https://github.com/open-webui/open-webui/issues/23176), [Commit](https://github.com/open-webui/open-webui/commit/c8ef7b028931263e8773cb60a7111d80d9572d26), [Commit](https://github.com/open-webui/open-webui/commit/cf4218e688def6f11d195aeda6665ae5b5376b67) +- 🧰 **Large tool result rendering.** Tool call details now display large result payloads reliably in chat instead of intermittently showing empty output for bigger tool responses. [#18743](https://github.com/open-webui/open-webui/issues/18743), [Commit](https://github.com/open-webui/open-webui/commit/45e49d33e51f7720c00b564215484aff9b48b20c) +- 🧼 **Null-byte document sanitization.** PDF and other document ingests now sanitize null bytes and invalid surrogate characters before pgvector writes, preventing PostgreSQL upload failures and allowing affected files to index successfully. [#22992](https://github.com/open-webui/open-webui/issues/22992), [Commit](https://github.com/open-webui/open-webui/commit/8dba798cce9fb1efc5f6acc5f37b152662db78d7) +- 📝 **Knowledge text editor stability.** The Knowledge "Add Text Content" modal now uses a plain text editor, avoiding current rich text editor issues and keeping drafting behavior consistent with existing knowledge editing flows. [Commit](https://github.com/open-webui/open-webui/commit/cd55c3e21237e000c13c6f396bb95b261f3bda82) +- 🎤 **STT SSL setting consistency.** Speech and related outbound media requests now consistently use shared async HTTP sessions and honor the configured SSL verification setting, improving compatibility with self-signed deployments. [#23672](https://github.com/open-webui/open-webui/issues/23672), [Commit](https://github.com/open-webui/open-webui/commit/2ddcb30b9a519885422ba1f36cc3485a7d897bf8) +- 🎙️ **Mistral speech input format.** Mistral speech-to-text requests now use the correct chat-completions audio input format for better compatibility. [Commit](https://github.com/open-webui/open-webui/commit/34d569d564a8ef2702c647dbad83eac840b76b2e), [#23822](https://github.com/open-webui/open-webui/issues/23822) +- 🖼️ **Optional image size parameter.** Image generation no longer sends the "size" field when no size is configured, improving compatibility with providers that reject unsupported size arguments. [#23611](https://github.com/open-webui/open-webui/issues/23611), [Commit](https://github.com/open-webui/open-webui/commit/869cf9e848b741705dc058550fa1b3f70db47fe8) +- 🔎 **FireCrawl timeout reliability.** FireCrawl web loading now uses direct scrape requests and improved timeout handling for single-URL fetches, reducing empty results and premature timeout failures with local FireCrawl setups. [#23411](https://github.com/open-webui/open-webui/issues/23411), [Commit](https://github.com/open-webui/open-webui/commit/9c64d84ad90804bf7d891e4a5097c03c4d7044c3) +- 🖱️ **Custom action icon drag prevention.** Custom user-added action icons in chat responses are no longer accidentally draggable, so clicks and hover interactions behave consistently with built-in action icons. [#23412](https://github.com/open-webui/open-webui/pull/23412) +- 🖼️ **Image URL conversion reliability.** Sending image URLs to AI models no longer fails with "cannot pickle 'coroutine' object" errors, so image inputs now convert to base64 reliably during request processing. [#23685](https://github.com/open-webui/open-webui/pull/23685#issuecomment-4240424635) +- 📂 **Channel input menu dismissal.** In Workspace Channels, the message input dropdown now closes immediately after selecting "Upload Files" or "Capture", matching normal chat input behavior and preventing the menu from staying open unnecessarily. [#23684](https://github.com/open-webui/open-webui/pull/23684) +- 📋 **Clipboard copy scroll stability.** Copying content with the fallback clipboard method no longer triggers unwanted page scrolling during focus, keeping your current reading position stable. [Commit](https://github.com/open-webui/open-webui/commit/fc98000aa8d439bbff21a70370f5e962bf23f4bc) +- 🖼️ **Profile image URL validation.** Profile saves now accept valid Open WebUI profile-image paths, trusted external HTTP(S) avatar URLs, and safe raster data-image formats while rejecting unsafe URL patterns that could be abused. [#23389](https://github.com/open-webui/open-webui/pull/23389) +- 👤 **Partial user profile updates.** User update API requests can now modify only the fields you provide, so administrators no longer need to resubmit unchanged name, email, and profile image values when changing a single setting like role. [#23424](https://github.com/open-webui/open-webui/issues/23424), [Commit](https://github.com/open-webui/open-webui/commit/3c2c611ba91d794a1e73134ec41b0de2b3927677) +- 🚨 **Provider SSE error visibility.** Provider failures returned with streaming content types are now surfaced as proper API errors and logged clearly, so issues like context-window limits no longer fail silently during chat generation. [#23379](https://github.com/open-webui/open-webui/pull/23379) +- 🧵 **Queued prompt race prevention.** Chat request queues now prevent overlapping processing for the same chat, avoiding duplicate queue handling when multiple queue-processing triggers fire close together. [#23181](https://github.com/open-webui/open-webui/issues/23181), [Commit](https://github.com/open-webui/open-webui/commit/e10a00132eed54a0108fb6ac120e8229deef3656) +- 🛑 **Cancellation event delivery reliability.** Cancelled chat processing now safely emits task-cancel and error events only when an event emitter is available, while provider HTTP errors now also route through task-cancel handling so chats recover from blocked-loading states more reliably. [#23663](https://github.com/open-webui/open-webui/issues/23663), [Commit](https://github.com/open-webui/open-webui/commit/51765b619c8584b042af68c3a5c87525a105ccd8), [Commit](https://github.com/open-webui/open-webui/commit/96265cf042c8ab97dbec5d0efcce8010d0cd76e5) +- 🔑 **OIDC key-rotation recovery.** OIDC login now retries token authorization with refreshed provider signing keys after a bad-signature failure, so logins recover automatically after identity-provider key rotation without requiring a service restart. [#23582](https://github.com/open-webui/open-webui/issues/23582), [Commit](https://github.com/open-webui/open-webui/commit/facb194a07486e847f0725a0a839e99b5864d37b) +- 🌍 **Non-ASCII tag filtering.** Prompt and model tag filters now handle non-Latin tags more reliably across SQLite and PostgreSQL, so tags like Cyrillic values return the expected items in Workspace lists. [#23381](https://github.com/open-webui/open-webui/issues/23381), [#23427](https://github.com/open-webui/open-webui/pull/23427), [Commit](https://github.com/open-webui/open-webui/commit/57784706e4fee75dec67e20b0d89a97351ac6256) +- 🏷️ **Prompt tag query accuracy.** Prompt tag filtering now uses JSON-element-aware queries so tag-based lookups return the correct prompts. [Commit](https://github.com/open-webui/open-webui/commit/e7e752f8e74e7b01fe2e6cb56f06e99312e1afe7), [#23386](https://github.com/open-webui/open-webui/pull/23386) +- 🗃️ **SQLite async pool compatibility.** SQLite async database setup no longer forces an explicit queue pool class, avoiding pool configuration conflicts in SQLite deployments. [Commit](https://github.com/open-webui/open-webui/commit/26b8ca5b5eeb144fae3fe6eaeae826150d8af826) +- 🧠 **Knowledge embedding deadlock prevention.** Knowledge file processing now runs blocking vector-save work in a worker thread while keeping async status updates reliable, preventing file processing from stalling during long embedding operations. [Commit](https://github.com/open-webui/open-webui/commit/d4b90f93bda2413ec8f040e61959acdb7b242061), [Commit](https://github.com/open-webui/open-webui/commit/22cfb3c673cbfa4a6bce26fde8e2e2754ce4963b) +- 🤖 **Automation worker async DB handling.** Automation claiming and run recording now use async database sessions consistently, improving worker stability for scheduled automations. [Commit](https://github.com/open-webui/open-webui/commit/cb6e77be3ec6ce00dd1f5b9ce3a655e6f65bc5da) +- 🕒 **Automation timezone scheduling.** Scheduled automations now calculate each user’s next run time using that user’s saved timezone, preventing run drift caused by server-time fallback. [Commit](https://github.com/open-webui/open-webui/commit/a4d62253df55c6307112eb76a6bfa29a7f538e21) +- 🔎 **Notes search matching.** Notes search now handles multi-word and hyphenated queries more reliably, so relevant notes and snippets are easier to find from partial phrase searches. [Commit](https://github.com/open-webui/open-webui/commit/a35926261646f8897ba71da1572ed5dff802e3be) +- 📐 **Display math rendering.** Chat markdown now correctly recognizes and renders "$$...$$" expressions as display math, improving reliability for multiline and escaped KaTeX content while keeping malformed delimiters from disrupting message rendering. [#23526](https://github.com/open-webui/open-webui/issues/23526), [Commit](https://github.com/open-webui/open-webui/commit/15b89b9218b7d2c7239c579aa3d23c2892227ac6) +- 🚫 **LDAP empty-password rejection.** LDAP login now rejects empty or whitespace-only passwords before bind attempts, preventing unauthenticated simple-bind behavior from granting access on permissive LDAP server configurations. [#23633](https://github.com/open-webui/open-webui/pull/23633) +- 🌐 **IPv6 SSRF address blocking.** URL validation now uses standard IP address checks for both IPv4 and IPv6, preventing private, loopback, link-local, reserved, and mapped-address SSRF bypasses through IPv6 hostname resolution. [#23453](https://github.com/open-webui/open-webui/pull/23453) +- 🔒 **API key endpoint restriction bypass.** API key endpoint restrictions are now enforced regardless of whether the key is sent through Authorization headers, cookies, or "x-api-key", preventing bypass through alternate key transport paths. [#23637](https://github.com/open-webui/open-webui/pull/23637) +- 🔐 **Channel sharing permission enforcement.** Channel creation and updates now enforce allowed access grant rules for public sharing, preventing unauthorized wildcard sharing on group channels. [#23638](https://github.com/open-webui/open-webui/pull/23638) +- 🛑 **Socket role invalidation.** Socket sessions now disconnect automatically when a user is demoted or deleted, preventing stale admin privileges from persisting until reconnect. [#23642](https://github.com/open-webui/open-webui/pull/23642) +- 🛂 **Tool server access checks.** Tool listing now correctly awaits server access checks, preventing users from seeing server-backed tools they do not have permission to use. [Commit](https://github.com/open-webui/open-webui/commit/d40f31982be3eed37e55e3f67b1eea9a5dc8c525) +- 🛑 **Task endpoint access control.** Global task listing and direct task stop endpoints are now restricted to administrators, while regular users can stop only their own chat tasks through a scoped chat endpoint. [#23454](https://github.com/open-webui/open-webui/pull/23454) +- 🧱 **Redis cache key isolation.** Tool server and terminal server cache entries now include the Redis key prefix, preventing multiple Open WebUI instances that share one Redis database from overwriting each other’s cached connection data. [#23649](https://github.com/open-webui/open-webui/pull/23649) +- 🧠 **Client session leak prevention.** Outbound provider requests now use a shared session pool with safer response cleanup and shutdown handling, preventing aiohttp session buildup and reducing memory growth during heavy concurrent API traffic. [#23540](https://github.com/open-webui/open-webui/issues/23540), [Commit](https://github.com/open-webui/open-webui/commit/c47dd7b7717c4186e0f0549ca3c8cb4d9bb38135) +- 🧩 **Tool enum value handling.** Tool schema generation now safely handles enum values as strings, preventing failures when OpenAPI parameters include non-string enum entries. [#23597](https://github.com/open-webui/open-webui/issues/23597), [Commit](https://github.com/open-webui/open-webui/commit/4498e6faf2b1bdd1caa0e2c1c15d90a2790cd721) +- 🧷 **Responses model access control.** The OpenAI-compatible Responses endpoint now enforces per-model permissions, preventing non-admin users from accessing models they are not allowed to use. [#23481](https://github.com/open-webui/open-webui/pull/23481) +- 🛡️ **Collection process endpoint permissions.** Collection processing endpoints now enforce collection ownership checks for web and text processing requests. [Commit](https://github.com/open-webui/open-webui/commit/ba83613ff297bc82db660b5273f04672d744902f), [#23634](https://github.com/open-webui/open-webui/pull/23634) +- 📚 **Knowledge query access enforcement.** Knowledge-base collection queries now block unauthorized enumeration and require read access before returning results. [Commit](https://github.com/open-webui/open-webui/commit/860b90fd17d14ba00674621edd294dee150491d2), [#23635](https://github.com/open-webui/open-webui/pull/23635), [#23452](https://github.com/open-webui/open-webui/pull/23452) +- 🔍 **RAG collection query permissions.** Vector search collection queries now enforce access checks before retrieval results are returned. [Commit](https://github.com/open-webui/open-webui/commit/f44b7a01f5b854f47c1594a1ab5f72096f736262), [#23627](https://github.com/open-webui/open-webui/pull/23627) +- 🔗 **Chained base model access checks.** Chained base model execution now enforces per-model access rules to prevent unauthorized model usage. [Commit](https://github.com/open-webui/open-webui/commit/8acce144f99992b75c25f0e5038b16881ce9f066), [Commit](https://github.com/open-webui/open-webui/commit/50363ba66b19613a2fc0cab6a3f7f724a825135e), [#23647](https://github.com/open-webui/open-webui/pull/23647) +- ✍️ **Collaborative document write checks.** Collaborative document updates now require proper write permission before changes are accepted. [Commit](https://github.com/open-webui/open-webui/commit/638c7ab80216452910bdc59a19eb90e6b7244c6c), [Commit](https://github.com/open-webui/open-webui/commit/3271b013a8b30a882364679dcb40ffc9a89f037e), [#23624](https://github.com/open-webui/open-webui/pull/23624) +- 📥 **Model import ownership validation.** Model import now enforces ownership and access grant checks to prevent unauthorized imports. [Commit](https://github.com/open-webui/open-webui/commit/499129625bf96b2c03a6d057a2f91fdf07fd1c49), [#23628](https://github.com/open-webui/open-webui/pull/23628) +- 🚫 **Inactive member channel access.** Deactivated group members can no longer read or write channel content through direct API calls, so channel permissions now match active membership status. [#23623](https://github.com/open-webui/open-webui/pull/23623) +- 🎛️ **Ollama endpoint model permissions.** Restricted models are now protected on Ollama show, generate, embed, and embeddings endpoints, preventing authenticated users from using private models without read access. [#23631](https://github.com/open-webui/open-webui/pull/23631) +- 🧭 **Azure deployment path validation.** Azure model names are now validated and safely encoded before request URL construction, preventing path traversal attempts from reaching unintended Azure endpoints. [#23629](https://github.com/open-webui/open-webui/pull/23629) +- 👥 **Private channel member list access.** Standard channel member lists now require proper read permission, preventing unauthorized users from enumerating members of private channels by direct API calls. [#23625](https://github.com/open-webui/open-webui/pull/23625) +- 🌀 **Tool server schema recursion safety.** Tool server OpenAPI conversion now handles circular request schema references safely, preventing conversion crashes and ensuring one bad tool server spec does not break the full tool server list. [#23588](https://github.com/open-webui/open-webui/pull/23588), [Commit](https://github.com/open-webui/open-webui/commit/d3df8f1f372411314be9121fbf61d107939fa258) +- 🧱 **Safer file path handling.** File upload, transcription cache, and model download paths now use safer path construction helpers to reduce path parsing risks and improve cross-platform path safety. [Commit](https://github.com/open-webui/open-webui/commit/15f9a8f3f13f112c96cb1b16f88859f65de58346) +- 🧾 **Prompt save error feedback.** Saving prompt edits now shows a clear error toast if the save fails, so failed updates are visible instead of silently failing in the editor flow. [Commit](https://github.com/open-webui/open-webui/commit/36a81ad43b7c0d450079f818a7546eaa517e3d95) +- 🧾 **Tool call JSON rendering.** Tool call arguments and structured results now render as plain formatted JSON blocks instead of markdown code fences, preventing formatting quirks and making tool output easier to read consistently. [Commit](https://github.com/open-webui/open-webui/commit/a7d4c53f3adb80768b67e4a410b486b04a581521) +- 👥 **First-user admin race protection.** Concurrent first-time LDAP or OAuth registrations can no longer create multiple admin accounts, so only the true first account is promoted during initial setup. [#23626](https://github.com/open-webui/open-webui/pull/23626) +- 🔒 **SCIM token checks.** SCIM authentication now compares tokens in a safer way, helping prevent timing-based token guessing attacks. [#23577](https://github.com/open-webui/open-webui/pull/23577) +- 🔒 **Safer file access checks.** HTML file previews now treat missing or non-admin owners as inaccessible, preventing accidental access to files that should not be shown. [Commit](https://github.com/open-webui/open-webui/commit/6acaaea59a50ec26da03e6144017a2fd86241ce9) +- 🖼️ **ComfyUI request hangs.** Concurrent image generation and editing requests to ComfyUI now complete reliably instead of getting stuck when the same user starts multiple requests at once. [#23592](https://github.com/open-webui/open-webui/pull/23592), [#23591](https://github.com/open-webui/open-webui/issues/23591) +- 🧭 **Permission-aware built-in tools.** Built-in tools now consistently respect user feature permissions for memories, web search, image generation, code interpreter, notes, channels, and automations, preventing tools from being exposed to users without access. [Commit](https://github.com/open-webui/open-webui/commit/588b81eedaacbfd7394b707ae1600d9fb729b809..674695918e5e3e1811314ce2a082c5bbb42d76b2) +- 🛑 **Interrupted MCP cleanup stability.** Interrupted MCP tool calls no longer leave runaway cleanup behavior that can drive container CPU usage to 100%, keeping instances stable after cancellations or dropped connections. [#23143](https://github.com/open-webui/open-webui/issues/23143) +- 🚪 **OAuth redirect URI reliability.** OAuth login redirects now use provider client metadata more consistently, preventing incorrect HTTP callback URLs behind reverse proxies and improving sign-in reliability for providers such as Feishu. [#23203](https://github.com/open-webui/open-webui/pull/23203), [#23128](https://github.com/open-webui/open-webui/issues/23128) +- 🌐 **OAuth redirect handling.** OAuth provider token exchange now follows redirects automatically, improving sign-in reliability with identity providers that redirect token endpoint requests. [#23409](https://github.com/open-webui/open-webui/issues/23409), [Commit](https://github.com/open-webui/open-webui/commit/498ff8cdc3dd47000cdc60e5adcf36f4adfbe07d) +- ☁️ **OneDrive picker redirect handling.** OneDrive file picker authentication now uses the current app origin as the redirect URI, improving sign-in reliability when launching the picker from deployed environments. [#23450](https://github.com/open-webui/open-webui/issues/23450), [Commit](https://github.com/open-webui/open-webui/commit/21cc8281323d505d7d084cc496bd433063315c86) +- 🍪 **OAuth session cookie persistence.** OIDC sign-in now correctly sets the "oauth_session_id" cookie, so "system_oauth" connections can forward user OAuth tokens to upstream providers as expected. [#23251](https://github.com/open-webui/open-webui/pull/23251), [#23250](https://github.com/open-webui/open-webui/issues/23250) +- 🔑 **OAuth session cookie handling.** OAuth callback processing no longer fails on undefined cookie expiry data, so OAuth session cookies are stored correctly after sign-in. [#23207](https://github.com/open-webui/open-webui/pull/23207), [#23197](https://github.com/open-webui/open-webui/issues/23197) +- 🔏 **Ollama SSL handling.** Ollama model management and file uploads now respect the configured SSL verification setting, so self-signed certificates work when SSL verification is disabled. [#23503](https://github.com/open-webui/open-webui/issues/23503), [Commit](https://github.com/open-webui/open-webui/commit/e51b661af0e71a24f041428f328fcc6e97a15262) +- 🛡️ **OAuth avatar URL validation.** OAuth sign-in now validates profile picture URLs before fetching them, preventing invalid image links from causing login-time errors. [#23356](https://github.com/open-webui/open-webui/pull/23356) +- 🔑 **User invite token expiry.** New user invite logins now respect the configured "JWT_EXPIRES_IN" setting, so signup tokens expire as expected instead of using the default lifetime. [#23576](https://github.com/open-webui/open-webui/pull/23576) +- 🚪 **Channel access checks.** Channel actions now verify the current user when checking access, improving permission enforcement across channel views and message actions. [Commit](https://github.com/open-webui/open-webui/commit/4632f200a9ac98c915aee412b34e86c3d3c58bb1) +- 📣 **Channel message lookups.** Channel message details and pinning now work more reliably when the sender account is missing, avoiding failures in those views. [Commit](https://github.com/open-webui/open-webui/commit/6acaaea59a50ec26da03e6144017a2fd86241ce9) +- 📌 **Pinned webhook message handling.** Viewing pinned webhook messages now works reliably even when webhook profile data is missing, preventing server errors and frontend crashes in channel pinned message dialogs. [#23414](https://github.com/open-webui/open-webui/pull/23414) +- 🛡️ **Note edit permission enforcement.** Note saving now requires write access instead of read access, preventing unauthorized users from modifying notes while preserving expected collaboration permissions. [Commit](https://github.com/open-webui/open-webui/commit/584a9a0920d8c8c72fc89ccbac83c970b5a4bd4a) +- 🗂️ **Archived chats menu visibility.** The 'Archived Chats' option in the user menu is now shown reliably for all users, so non-admin accounts can consistently access archived conversations. [Commit](https://github.com/open-webui/open-webui/commit/07262fa62c2323fc7948389e5b5b8a5d1b72fade) +- 💾 **Error message persistence.** LLM errors that occur during streaming are now saved to the database even if the connection drops, so users can see what went wrong when they reconnect. [#23231](https://github.com/open-webui/open-webui/pull/23231) +- 🚫 **Missing message completion guard.** Chat completion finalization now skips invalid requests without a message identifier, preventing unnecessary error toasts caused by rare frontend concurrency timing. [#23184](https://github.com/open-webui/open-webui/pull/23184) +- 🧠 **Active message completion accuracy.** Switching chats or refreshing during generation no longer marks the currently streaming assistant message as finished too early, so thinking blocks and action buttons appear at the correct time. [#23171](https://github.com/open-webui/open-webui/issues/23171) +- 📞 **Call overlay visibility.** Incoming call events now open the call overlay and controls reliably, preventing cases where the call interface briefly appeared and then disappeared. [Commit](https://github.com/open-webui/open-webui/commit/ee9db91df02120e1e3651e8881734966b710ad52) +- 💬 **Prompt submission handling.** Chat messages now preserve attached files more reliably when prompts are sent, including queued messages and shared prompt actions. [Commit](https://github.com/open-webui/open-webui/commit/6d6dfbf02c893d72d85d4490cb41f1665b1f9f95) +- 🧾 **Prompt variable form saving.** Prompt variable forms now save reliably without runtime errors or an unresponsive save action, so input values and placeholders work correctly when applying prompt templates with variables. [#23225](https://github.com/open-webui/open-webui/issues/23225), [#23480](https://github.com/open-webui/open-webui/issues/23480) +- 🛟 **Task model fallback safety.** Task routing now handles missing default model entries safely, preventing task execution failures when the previously selected model is no longer available. [#23169](https://github.com/open-webui/open-webui/pull/23169) +- 📊 **Usage statistic preservation.** Follow-up generation no longer overwrites existing token usage fields, so stored usage statistics remain accurate for the main response. [#23152](https://github.com/open-webui/open-webui/issues/23152) +- 📝 **Writing block parsing reliability.** ":::writing" blocks now parse more reliably when headers or extra inline text are present, preventing malformed rendering and duplicate output artifacts. [#23174](https://github.com/open-webui/open-webui/issues/23174) +- 🧾 **Code block line break reliability.** Blank lines in submitted code blocks are now preserved more reliably instead of being collapsed. [Commit](https://github.com/open-webui/open-webui/commit/1be9627dd27ffe75957729a4a0d1682a98684f01), [#20302](https://github.com/open-webui/open-webui/issues/20302), [#23451](https://github.com/open-webui/open-webui/pull/23451) +- ✂️ **Citation spacing cleanup.** When citations are disabled for a model, citation markers and their leftover spacing are now removed together so punctuation and copied text remain cleanly formatted. [#23141](https://github.com/open-webui/open-webui/issues/23141) +- 🧰 **Pipe tool access.** Pipe functions now receive built-in and MCP tools in **tools**, so tools like Web Search and code execution are available when enabled. [#23365](https://github.com/open-webui/open-webui/issues/23365) +- 📚 **Batch file processing database handling.** Batch knowledge file processing now consistently uses the active database session, preventing failures caused by missing database context during file ownership checks and update writes. [#23137](https://github.com/open-webui/open-webui/issues/23137) +- ⚙️ **Default model parameter loading.** The "DEFAULT_MODEL_PARAMS" environment variable is now parsed and applied correctly, so default generation settings are honored reliably without being ignored at startup. [#23223](https://github.com/open-webui/open-webui/pull/23223) +- 🔧 **Web search settings save reliability.** Saving web search configuration now works without server errors, so administrators can update "WEB_FETCH_MAX_CONTENT_LENGTH" and related retrieval settings successfully from the admin interface. [Commit](https://github.com/open-webui/open-webui/commit/36d02aa1477aa1b4e7fb59d022f99693ebfa8667), [#23127](https://github.com/open-webui/open-webui/issues/23127) +- 🔍 **Web search result count.** The built-in search_web tool now respects the admin-configured "Search Result Count" setting instead of always returning 5 results when using Native Function Calling mode. [#23488](https://github.com/open-webui/open-webui/pull/23488), [#23485](https://github.com/open-webui/open-webui/issues/23485) +- 🖼️ **Open Terminal file response handling.** Open Terminal tool responses now preserve binary content types in user-side connections, so image and non-text file reads work consistently instead of being forced into plain text. [#23125](https://github.com/open-webui/open-webui/issues/23125) +- 🖥️ **Terminal label casing.** Terminal names in the chat input now display exactly as stored instead of being automatically capitalized, so domain-style server names appear correctly. [#23518](https://github.com/open-webui/open-webui/pull/23518) +- 🖼️ **Gravatar profile photo saving.** Gravatar profile images can now be saved successfully from account settings, with clearer validation and error handling instead of failing with generic object errors. [#23156](https://github.com/open-webui/open-webui/issues/23156) +- 🪟 **Details expansion preference.** Tool call detail groups now honor the 'Always Expand Details' chat setting, so they open expanded by default when that preference is enabled. [#23262](https://github.com/open-webui/open-webui/pull/23262), [#23255](https://github.com/open-webui/open-webui/issues/23255) +- 🖱️ **Rapid sidebar action protection.** Archive and delete actions in the chat sidebar now ignore repeated clicks while a request is in progress, preventing duplicate requests and stacked error toasts. [#23172](https://github.com/open-webui/open-webui/issues/23172) +- 📲 **Mobile model selector positioning.** The mobile model selector dropdown now applies a constrained viewport width and left offset, preventing overflow and making model selection easier on small screens. [#23310](https://github.com/open-webui/open-webui/pull/23310) +- 🔽 **Task list toggle icons.** The task list collapse button now shows the correct arrow direction, making task sections easier to expand and collapse at a glance. [Commit](https://github.com/open-webui/open-webui/commit/f66b67c8b86b6f9d896a23c7bb53907c2e6b15d3), [#23354](https://github.com/open-webui/open-webui/issues/23354) +- ➕ **Attachment menu auto-close.** The chat attachment menu now closes immediately after selecting upload actions like file upload, camera capture, web attach, Google Drive, or OneDrive, preventing the menu from lingering on screen. [Commit](https://github.com/open-webui/open-webui/commit/4764dd5d3765c22384ed38cbc97a8170daa7a75f), [#23320](https://github.com/open-webui/open-webui/issues/23320) +- 🧹 **Per-chat draft clearing.** Sent message drafts are now cleared using the active chat key, so sent text no longer reappears in the input after a refresh. [Commit](https://github.com/open-webui/open-webui/commit/124b7e9154d7f3ca8a16f2b90621209ac8d6b8c1), [#23296](https://github.com/open-webui/open-webui/issues/23296) +- ✉️ **Context-aware input action button.** The input now shows the send action when text or files are present during generation, while keeping stop controls for truly empty input states to avoid action confusion. [Commit](https://github.com/open-webui/open-webui/commit/86472bb4453af7ea4e5ddc8d127b14d8e67733bc), [#23306](https://github.com/open-webui/open-webui/issues/23306) +- 📉 **Pyodide prompt cache stability.** Pyodide code interpreter context is now appended to the system prompt instead of user messages, preserving stable prefix caching across turns and reducing repeated token costs in long native tool-calling chats. [#23269](https://github.com/open-webui/open-webui/issues/23269) +- 🧪 **Temp chat outlet filtering.** Outlet filters now process temporary chats more reliably, preserving assistant output and usage data so local chat responses stay consistent when filter pipelines are enabled. [Commit](https://github.com/open-webui/open-webui/commit/70a6a24f143b221c787bc50b72582ee1e0c2dac0) + +### Changed + +- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility. +- 🧨 **Plugin async migration required.** Custom plugins for Tools, Functions, and Pipelines may require migration to the new async backend signatures after upgrading, so plugin maintainers should update handlers and database call patterns for compatibility and follow the 0.9.0 plugin migration guide. [Migration Guide](https://docs.openwebui.com/features/extensibility/plugin/migration/to-0.9.0) +- 🔄 **Automation terminal source.** Automations now use the terminal configured on the selected model instead of a separate per-automation terminal picker, keeping terminal behavior consistent between chat and scheduled runs. [Commit](https://github.com/open-webui/open-webui/commit/47d413ce7b2a006a8126f4a9055b13e5fcb33a1d) +- 🚧 **OpenAI passthrough now opt-in.** Direct OpenAI catch-all proxy requests are now disabled by default and require enabling "ENABLE_OPENAI_API_PASSTHROUGH", so deployments relying on passthrough must explicitly turn it on after upgrading. [#23640](https://github.com/open-webui/open-webui/pull/23640) +- 🗄️ **SQLite WAL default enabled.** SQLite deployments now default to enabling write-ahead logging, improving concurrent read and write behavior without requiring manual configuration. [Commit](https://github.com/open-webui/open-webui/commit/2f9e326dba3b1087932cb6b8075ed1881bd1c6d6) + +## [0.8.12] - 2026-03-26 + +### Added + +- 🌐 **Translation updates.** Translations for Simplified Chinese, Catalan, Portuguese (Brazil), Finnish, and Lithuanian were enhanced and expanded. + +### Fixed + +- 🔒 **Terminal server connection security.** Terminal server verification and policy saving now proxy through the backend, preventing API key exposure and CORS errors when connecting to in-cluster services. [Commit](https://github.com/open-webui/open-webui/commit/a6413257079a52fa4487eda36543f3955d0fbd53), [Commit](https://github.com/open-webui/open-webui/commit/4567cdc0d9cb7b42b6eba7b676c0ced3f4850d31) +- 🛠️ **Terminal tools exception handling.** Exceptions in middleware.py due to invalid return values from get_terminal_tools() have been resolved. [Commit](https://github.com/open-webui/open-webui/commit/52a06bd48aff34fb2211aac2879f0cd028129267) +- 📦 **Missing beautifulsoup4 dependency.** Users can now start Open WebUI using uvx without encountering the "bs4 module missing" error. [Commit](https://github.com/open-webui/open-webui/commit/1994d65306bbcc7406584e1bfef82f5d353fc91c) +- 🔌 **API files list error.** The /api/v1/files/ endpoint no longer returns a 500 error, fixing a regression that prevented file listing via the API. [Commit](https://github.com/open-webui/open-webui/commit/11f52921dc21c2dc61c03f12bcdf6f19140a350c) +- 📜 **License data loading.** License data now loads correctly, displaying the expected color and logo in the interface. [Commit](https://github.com/open-webui/open-webui/commit/16335f866ea4cedf00c4971963622fcc1fe02d82) +- 👑 **Admin model visibility.** Administrators can now see models even when no access control is configured yet, allowing them to manage all available models. [Commit](https://github.com/open-webui/open-webui/commit/f3f8f9874f55282603c2650b91801640cb3f69cb) +- 📊 **Tool call embed visibility.** Rich UI embeds from tool calls (like visualizations) are now rendered outside collapsed groups and remain visible without requiring manual expansion. [Commit](https://github.com/open-webui/open-webui/commit/4c872a8d128757d4a6f311fb86bc382af2ba5d0d), [Commit](https://github.com/open-webui/open-webui/commit/308fa924a5b2b7e08cd1e8f15b9c8c96e1de8f02) + +## [0.8.11] - 2026-03-25 + +### Added + +- 🔀 **Responses API streaming improvements.** The OpenAI proxy now properly handles tool call streaming and re-invocations in the Responses API, preventing duplicate tool calls and preserving output during model re-invocations. [Commit](https://github.com/open-webui/open-webui/commit/93415a48e8893139db13d02d0a6d24e8604a2ac5), [Commit](https://github.com/open-webui/open-webui/commit/f8b3a32caf00dad76687fd8fe698b86f304f3997), [Commit](https://github.com/open-webui/open-webui/commit/2ae47cf20057e92a83fd618b938f3ee9bb124e5b), [Commit](https://github.com/open-webui/open-webui/commit/adcbba34f8bbfbab3e4041269a084f2b71c076d9) +- 🔀 **Responses API stateful sessions.** Administrators can now enable experimental stateful session support via the ENABLE_RESPONSES_API_STATEFUL environment variable, allowing compatible backends to store responses server-side with previous_response_id anchoring for improved multi-turn conversations. [Commit](https://github.com/open-webui/open-webui/commit/dfc2dc2c0bd298cb4bfcf212ef11223586aa54f1) +- 📄 **File viewing pagination.** The view_file and view_knowledge_file tools now support pagination with offset and max_chars parameters, allowing models to read large files in chunks. [Commit](https://github.com/open-webui/open-webui/commit/5d7766e1b6f7ca7749c5a5a780d7b1bb2da28a2f) +- 🗺️ **Knowledge search scoping.** The search_knowledge_files tool now respects model-attached knowledge, searching only within attached knowledge bases and files when available. [Commit](https://github.com/open-webui/open-webui/commit/0f0ba7dadd043460d205477fd3b57556aa970847) +- 🛠️ **Tool HTML embed context.** Tools can now return custom context alongside HTML embeds by using a tuple format, providing the LLM with actionable information instead of a generic message. [#22691](https://github.com/open-webui/open-webui/pull/22691) +- 🔒 **Trusted role header configuration.** Administrators can now configure the WEBUI_AUTH_TRUSTED_ROLE_HEADER environment variable to set user roles (admin, user, or pending) via a trusted header from their identity provider or reverse proxy. [#22523](https://github.com/open-webui/open-webui/pull/22523) +- 🔑 **OIDC authorization parameter injection.** Administrators can now inject extra parameters into the OIDC authorization redirect URL via the OAUTH_AUTHORIZE_PARAMS environment variable, enabling IdP pre-selection for brokers like CILogon and Keycloak. [#22863](https://github.com/open-webui/open-webui/issues/22863), [Commit](https://github.com/open-webui/open-webui/commit/69171a4c8bb7f995461b4a2feef194f112b32004) +- 🔑 **Google OAuth session persistence.** Administrators can now configure Google OAuth to issue refresh tokens via the GOOGLE_OAUTH_AUTHORIZE_PARAMS environment variable, preventing OAuth sessions from expiring after one hour and ensuring tools and integrations that rely on OAuth tokens remain functional. [#22652](https://github.com/open-webui/open-webui/pull/22652) +- 🔌 **Embed prompt confirmation.** Interactive tool embeds can now submit prompts to the chat without requiring same-origin access, showing a confirmation dialog for cross-origin requests to prevent abuse. [#22908](https://github.com/open-webui/open-webui/pull/22908) +- 🏮 **Tool binary response handling.** Tool servers can now return binary data such as images, which are properly processed and displayed in chat for both multimodal and non-multimodal models. [Commit](https://github.com/open-webui/open-webui/commit/1c25b06dca83ad491b4dc3d373b1c215a7a8fd3e), [Commit](https://github.com/open-webui/open-webui/commit/108a019cb8e63a533250abe84f2b6f2b7c2131c4) +- ⚡ **Svelte upgrade performance.** Page and markdown rendering are now approximately 25% faster across the board, with significantly less memory usage for smoother UI interactions. [#22611](https://github.com/open-webui/open-webui/issues/22611) +- 🧩 **Model and filter lookup optimization.** Model and filter membership lookups are now faster thanks to optimized data structure operations during model list loading. [Commit](https://github.com/open-webui/open-webui/commit/7eae377c01f8d2de94a694b72279f769c82658cd) +- 💨 **Chat render throttling.** Chat message rendering now uses requestAnimationFrame batching to stay smooth during rapid model responses, preventing dropped frames when fast models send many events per second. [#22947](https://github.com/open-webui/open-webui/pull/22947) +- 🚀 **Function list API optimization.** The functions list API now returns only essential metadata without function source code, reducing payload sizes by over 99% and making the Functions admin page load significantly faster. [#22788](https://github.com/open-webui/open-webui/pull/22788) +- ✨ **Smoother loading animation.** The loading shimmer animation now looks smoother and more natural, with softer highlight colors. [#22516](https://github.com/open-webui/open-webui/pull/22516) +- 🧪 **Terminal connection verification.** Users can now verify their terminal server connection is working before saving the configuration, making setup more reliable. [#22567](https://github.com/open-webui/open-webui/pull/22567) +- 📁 **Chat folder emoji reset.** Users can now reset chat folder emojis back to the default icon using a "Reset to Default" button in the emoji picker, making it easier to revert custom icons. [#22554](https://github.com/open-webui/open-webui/pull/22554) +- 📊 **Metrics export interval configuration.** Administrators can now control OpenTelemetry metrics export frequency via the OTEL_METRICS_EXPORT_INTERVAL_MILLIS environment variable, enabling cost optimization for metrics services like Grafana Cloud. [#22529](https://github.com/open-webui/open-webui/pull/22529) +- 🏥 **Readiness probe endpoint.** A new /ready endpoint is now available for Kubernetes deployments, returning 200 only after startup completes and database/Redis are reachable, enabling more reliable container orchestration. [#22507](https://github.com/open-webui/open-webui/pull/22507) +- 🔩 **Tool server timeout configuration.** Administrators can now configure a separate HTTP timeout for tool server requests via the AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER environment variable, enabling fine-tuned control over how long tool calls are allowed to take. [Commit](https://github.com/open-webui/open-webui/commit/a3238aa79f344765f5b62cb64eba71ffd001abaf) +- 📌 **Knowledge file previews.** Knowledge base files can now be opened in a new tab directly from the file list, making it easier to view content without downloading. [#22629](https://github.com/open-webui/open-webui/pull/22629) +- 🎯 **Knowledge tool hybrid search support.** The built-in query_knowledge_files tool now respects hybrid search and reranking settings, matching the behavior of the middleware RAG pipeline. [Commit](https://github.com/open-webui/open-webui/commit/9a2c60d5954ecbc172d09e9955d52a07d135dcbc) +- 🗣️ **Temporary chat folder support.** Temporary chats can now use folder-level system prompts and knowledge files, making them more powerful for quick explorations. [Commit](https://github.com/open-webui/open-webui/commit/adcc50d3370301afd5561e0f58ff6f3ab3750818) +- 📡 **Terminal port previews.** Detected ports in the File Navigator can now be previewed inline with a browser-style view, navigation controls, and an address bar, instead of only opening in a new tab. [Commit](https://github.com/open-webui/open-webui/commit/689061822173e561a153290b2bb816f4cb6f4959), [Commit](https://github.com/open-webui/open-webui/commit/1dc647f43b1929f5c4d1af393a90a47f56cb745e) +- ✏️ **File renaming.** Files and folders in the File Navigator can now be renamed by double-clicking or using the context menu, with Enter to confirm and Escape to cancel. [Commit](https://github.com/open-webui/open-webui/commit/637cd136c2271baf4787815bc8bc25241626a943) +- 🧭 **File Navigator navigation history.** The File Navigator toolbar now includes Back and Forward buttons for navigating through folder and file history, similar to a web browser. [Commit](https://github.com/open-webui/open-webui/commit/3a4b862e818c69fff6f3a3c67b50c51aa00c03e9) +- 🗑️ **Delete connection confirmations.** Users are now prompted with a confirmation dialog before deleting connections, preventing accidental deletions. [Commit](https://github.com/open-webui/open-webui/commit/157ff57c40bc40c53bc608828dac3779e95c2ffa) +- 📦 **Document loader fallbacks.** Excel and PowerPoint files can now be processed even when the unstructured package is not installed, using pandas and python-pptx as fallback loaders. [Commit](https://github.com/open-webui/open-webui/commit/6862d618ee17f95d3cae78819ed993e7fbc7e632) +- 🧠 **Memory management search and sort.** Users can now search and sort their personal memories in the Memory management modal, making it easier to find specific memories. [Commit](https://github.com/open-webui/open-webui/commit/47ab4c71d50fd631b04c95f2febb085dd0a13083) +- 📦 **SBOM generation script.** A new script for generating CycloneDX Software Bill of Materials is now available in the scripts directory. [Commit](https://github.com/open-webui/open-webui/commit/39100eca4915e4fe86a6912aa97dde86ed72e015) +- ⚙️ **Ruff linter and formatter.** Added Ruff as the Python linter and formatter, replacing the black-based workflow for better code quality with near-instant execution. [#22576](https://github.com/open-webui/open-webui/pull/22576), [#22462](https://github.com/open-webui/open-webui/discussions/22462) +- 🖥️ **Offline code formatting support.** The black formatter for Python code editing is now bundled locally in the Docker image, enabling code formatting to work in air-gapped deployments where client browsers cannot reach PyPI. Formatting failures no longer block saves, allowing code to be preserved even when offline. [#22509](https://github.com/open-webui/open-webui/issues/22509), [Commit](https://github.com/open-webui/open-webui/commit/8507e5eb0d18896f1bbf990a00a4361aec171a30) +- ✏️ **Markdown file editing.** Users can now edit and save Markdown files directly in the file navigator, with empty files automatically switching to editor mode for immediate editing. [Commit](https://github.com/open-webui/open-webui/commit/47e47e42af682e7f75c8359999f7cdf969bf903e) +- 🍔 **Model bulk actions menu.** Users can now quickly enable, disable, show, or hide multiple models at once using a new hamburger menu on the workspace Models page filter bar, with actions respecting the current search and filter settings. [#22484](https://github.com/open-webui/open-webui/pull/22484) +- 📂 **Files list pagination.** The files list API now supports pagination, returning paginated results with a total count for easier navigation through large file collections. [Commit](https://github.com/open-webui/open-webui/commit/f9756de693a93e918c037d757afddb7defc847e4) +- 🖇 **Web fetch content length config.** Administrators can now configure the maximum characters to return from fetched URLs via WEB_FETCH_MAX_CONTENT_LENGTH environment variable or the admin settings page, instead of the previous hardcoded 50K limit. [Commit](https://github.com/open-webui/open-webui/commit/b171b0216b916745420c7caf513093a315ed9560), [#22774](https://github.com/open-webui/open-webui/issues/22774) +- 🤖 **Ollama Anthropic endpoint support.** The Ollama proxy now supports the Anthropic-compatible /v1/messages endpoint, allowing clients using the Anthropic API format to work through Open WebUI with proper authentication and model access controls. [Commit](https://github.com/open-webui/open-webui/commit/f23296b22d3304e5bfcd19151e5802eec55bd98f), [#22861](https://github.com/open-webui/open-webui/issues/22861) +- 📝 **Writing block rendering.** Responses from OpenAI models that include :::writing blocks are now rendered as formatted content in a styled container with a copy button, instead of displaying raw marker text. [#22672](https://github.com/open-webui/open-webui/issues/22672), [Commit](https://github.com/open-webui/open-webui/commit/53b8a1f71bd0cb0a0122175ad5210da492018728) +- 💡 **Memory deletion confirmation.** Users are now asked to confirm before deleting individual memory entries, with the memory content displayed for review. [#22888](https://github.com/open-webui/open-webui/pull/22888) +- 📓 **Multi-artifact HTML rendering.** Code blocks with multiple HTML sections now render as separate artifacts instead of merging into one, allowing models to display distinct interactive components. [Commit](https://github.com/open-webui/open-webui/commit/9a6bf78e14a13864e72db87426da4f5996abe716) +- 🚩 **Drag chats as references.** Users can now drag chats from the sidebar and drop them into the message input to add them as Reference Chats. [Commit](https://github.com/open-webui/open-webui/commit/ebb7ce2092efc8d78da4974623647dbd18b6e372) +- ⌨️ **Terminal system prompts.** Terminal servers can now provide custom system prompts that are automatically included when their tools are used. [Commit](https://github.com/open-webui/open-webui/commit/6a9d67b5bb4c93fd343b334bee3e37703dff59f6) +- 💾 **Terminal state persistence.** The selected terminal server and its enabled state now persist across page loads, making terminal usage more seamless. [Commit](https://github.com/open-webui/open-webui/commit/d577ff1e4af750dda09e558dac7edb8dd2470850) +- 💾 **Terminal folder downloads.** Users can now download folders as ZIP archives and bulk-download multiple selected files as a single ZIP directly from the File Navigator toolbar, making file exports faster and more convenient. [Commit](https://github.com/open-webui/open-webui/commit/3841e85abb3ea3e8d8b364dff0102f0124844d22), [Commit](https://github.com/open-webui/open-webui/commit/cf60b1882f1929200649b59f867289dea54e4210) +- 🔐 **MCP OAuth 2.1 static credentials.** MCP servers that require static client_id and client_secret can now be connected using a new OAuth 2.1 Static auth type, enabling integration with MCP servers that don't support dynamic client registration. [#22266](https://github.com/open-webui/open-webui/pull/22266), [Commit](https://github.com/open-webui/open-webui/commit/601bb783587a3e965cf88c148e4856b988655b13) +- 🎪 **Collapsible tool and thinking groups.** Consecutive tool calls and reasoning blocks are now grouped into a single collapsible summary (e.g., "Explored tool1, tool2"), keeping chat responses clean and readable while preserving full detail on expand. [#21604](https://github.com/open-webui/open-webui/issues/21604), [Commit](https://github.com/open-webui/open-webui/commit/261aec8c864646eb7215be0d5c14a79cad3cb93f) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for Finnish, Portuguese (Portugal), Catalan, Turkish, Japanese, Simplified Chinese, Traditional Chinese, Estonian, Spanish, Azerbaijani, and German were enhanced and expanded. + +### Fixed + +- 🔒 **Model access control bypass.** Fixed a security vulnerability where external clients could bypass model access controls by setting a URL parameter, preventing unauthorized access to restricted models. [Commit](https://github.com/open-webui/open-webui/commit/c0385f60ba049da48d2d5452068586d375303c37) +- 🛡️ **Terminal proxy path sanitization.** The terminal server proxy now properly sanitizes paths to prevent directory traversal and SSRF attacks, protecting against security vulnerabilities. [Commit](https://github.com/open-webui/open-webui/commit/f9d38a073fae32032ed44073cf2817cba20210bb) +- 🛡️ **Tool configuration access control.** Tool configuration endpoints now properly verify user permissions, preventing unauthorized access to tool settings. [Commit](https://github.com/open-webui/open-webui/commit/bc5b3ec6b8ec0fef894eb8046c636ee33688b8c4) +- 🗝️ **Tool valves access control.** The tool user valves endpoints now properly verify ownership and access grants before returning or updating configuration, with appropriate 404 responses for missing tools and 401 for unauthorized access. [Commit](https://github.com/open-webui/open-webui/commit/f949d17db1e62e0b79aecbbcbcabe3d57d8d4af6) +- 🔐 **Collaborative document authorization.** Fixed a security vulnerability in collaborative documents where authorization could be bypassed using alternative document ID formats, preventing unauthorized access to notes. [Commit](https://github.com/open-webui/open-webui/commit/3107a5363d13c899a995c930cbb1121a80f754f9) +- 🔏 **OAuth session persistence.** Users logging in via OAuth or OIDC providers now stay logged in for the configured JWT expiry duration instead of being logged out when closing the browser. [#22809](https://github.com/open-webui/open-webui/pull/22809) +- 🚪 **OAuth sub claim configuration crash.** Using the OAUTH_SUB_CLAIM environment variable no longer causes crashes during token exchange requests, fixing a missing configuration registration. [#22865](https://github.com/open-webui/open-webui/pull/22865) +- 🔍 **OAuth discovery header parsing.** The OAuth protected resource discovery now correctly handles both quoted and unquoted values in the WWW-Authenticate header, fixing compatibility with MCP servers that return unquoted metadata. [#22646](https://github.com/open-webui/open-webui/discussions/22646), [Commit](https://github.com/open-webui/open-webui/commit/fe7e002fea7283abcf901e22de5c8a7d86e336ea) +- 👤 **Admin OAuth group sync.** Admin user group memberships from OAuth and LDAP providers are now properly synced to Open WebUI, fixing a limitation where admin role excluded users from group updates. [#22537](https://github.com/open-webui/open-webui/pull/22537), [Commit](https://github.com/open-webui/open-webui/commit/a1aceb5f879abd130ef83085d98a0d51316a8fc3) +- 🎫 **Password change complexity validation.** Password complexity rules are now properly enforced when users change their password, closing a security gap where new passwords could bypass configured complexity requirements. [Commit](https://github.com/open-webui/open-webui/commit/bd8aa3b6a0b6a2320f41b20a51b9842f39aadb7f) +- 🔏 **OAuth role enforcement.** OAuth role management now properly denies access when a user's roles don't match any configured OAUTH_ALLOWED_ROLES or OAUTH_ADMIN_ROLES, instead of silently bypassing the restriction. [#13676](https://github.com/open-webui/open-webui/issues/13676), [#15551](https://github.com/open-webui/open-webui/issues/15551), [Commit](https://github.com/open-webui/open-webui/commit/6d7744c21903ec5a9ad951770dea76e9ba19cbcc) +- 🔑 **Microsoft Entra ID role claim preservation.** Role claims from Microsoft Entra ID tokens are now preserved during OAuth login, fixing ENABLE_OAUTH_ROLE_MANAGEMENT for Microsoft OAuth which was previously ignored because the userinfo endpoint stripped the roles claim. [#20518](https://github.com/open-webui/open-webui/issues/20518), [Commit](https://github.com/open-webui/open-webui/commit/aa2f7fbe5229c3985ce427602069cdeababda481) +- 🔍 **SCIM group filtering.** The SCIM endpoint now properly handles displayName and externalId filters when provisioning groups from identity providers like Microsoft Entra ID, preventing all groups from being returned instead of the filtered subset. [#21543](https://github.com/open-webui/open-webui/pull/21543) +- 🔐 **Forwarded allow IPs configuration.** The FORWARDED_ALLOW_IPS environment variable is now properly respected by the startup scripts instead of being hardcoded to '\*', allowing administrators to restrict which proxies are trusted for request forwarding. [#22539](https://github.com/open-webui/open-webui/issues/22539), [Commit](https://github.com/open-webui/open-webui/commit/0aebdd5f83cd1d811009edcbb2bec432a34e7c81) +- 🍪 **Model list auth cookie forwarding.** Model list requests to backends that require cookie-based authentication now properly forward auth headers and cookies, preventing "Unauthorized" errors when loading models. [Commit](https://github.com/open-webui/open-webui/commit/76ece4049e96bd6890593f17a946a9af6b082fab) +- 🔱 **Model lookup race condition.** Fixed a race condition in Redis model storage that caused intermittent "model not found" errors in multi-replica deployments under heavy load, by eliminating the window between hash deletion and updates. [Commit](https://github.com/open-webui/open-webui/commit/ee901fcd2ca82d7a7dad48170c64df782d3e040a) +- 🎚️ **Bulk model action reliability.** Bulk enable, disable, show, and hide operations in the admin Models settings now properly refresh the model list after completion, ensuring changes are reflected immediately and correct toast notifications are shown. [#22962](https://github.com/open-webui/open-webui/pull/22962), [Commit](https://github.com/open-webui/open-webui/commit/75932be880f3b86f78f00b4352b9f1350b8f53fa), [Commit](https://github.com/open-webui/open-webui/commit/15ae3f588b1aa4ddb686ae68afebd6064036a201) +- 🔄 **Paginated list duplicates.** Fixed duplicate items appearing in paginated lists when loading more items in chats, knowledge, notes, and search across the UI. [Commit](https://github.com/open-webui/open-webui/commit/58e78e8946fb3644107489fe8e01b17709302b2f) +- 🧽 **Duplicate chat list refresh.** Sending messages no longer triggers duplicate sidebar chat list refreshes, eliminating an unnecessary database query that was already handled by the save and completion handlers. [#22982](https://github.com/open-webui/open-webui/pull/22982) +- 🧹 **Chat history save optimization.** The chat list is no longer refreshed on every chat history save, branch navigation, or edit — only on meaningful state changes like new chat creation, title generation, and response completion. [#22983](https://github.com/open-webui/open-webui/pull/22983) +- 💬 **Message queue responsiveness.** The message queue no longer waits for background tasks like title generation and follow-up suggestions to complete, allowing users to send new messages immediately after a response finishes without unnecessary delays. [Commit](https://github.com/open-webui/open-webui/commit/486c004cbb43f15d5c3e31561f51f22effff1f6c), [#22565](https://github.com/open-webui/open-webui/issues/22565) +- 🗄️ **Migration reliability.** Database migrations no longer fail when chat data has unexpected format, making upgrades more reliable. [#22588](https://github.com/open-webui/open-webui/pull/22588), [#22568](https://github.com/open-webui/open-webui/issues/22568) +- 🫧 **Memory modal event bubbling.** Fixed an issue where clicking the Delete button in the Memory management modal would also open the Edit Memory modal due to event bubbling. [#22783](https://github.com/open-webui/open-webui/issues/22783) +- 🧩 **Memory tool registration.** Models with capabilities.memory: true now correctly have memory tools available for execution, fixing a retry loop where add_memory appeared in the tool schema but was not registered for backend execution. [#22666](https://github.com/open-webui/open-webui/issues/22666), [#22675](https://github.com/open-webui/open-webui/pull/22675), [Commit](https://github.com/open-webui/open-webui/commit/d9339919046c3e977f313f603782d220aab4257f) +- 📝 **Input variables modal crash.** Fixed a crash that occurred when selecting custom prompts with prompt variables, causing the Input Variables modal to display an infinite loading spinner instead of the variable input fields. [#22748](https://github.com/open-webui/open-webui/issues/22748), [Commit](https://github.com/open-webui/open-webui/commit/0dcd6ac983bede06b8477179192154467f5b24a2) +- 🪛 **Function list API crash fix.** Fixed a 500 error on the functions list API endpoint that was introduced by the recent optimization, by adding proper model configuration for SQLAlchemy ORM objects. [#22924](https://github.com/open-webui/open-webui/pull/22924) +- 🗂️ **Sidebar chat menu closure.** Sidebar chat dropdown menus now close properly after clicking "Clone", "Share", "Download", "Rename", "Pin", "Move", "Archive", or "Delete", instead of remaining visible. [#22884](https://github.com/open-webui/open-webui/pull/22884), [#22784](https://github.com/open-webui/open-webui/issues/22784) +- 🧭 **Chat deletion and archive redirection.** Users are now redirected to the chat list when deleting or archiving the currently active chat, instead of being left on a stale chat page. [#22755](https://github.com/open-webui/open-webui/pull/22755) +- 🚩 **User menu navigation fix.** Clicking Playground or Admin Panel from the user menu now uses client-side routing instead of causing full page reloads, restoring smooth SPA navigation. [Commit](https://github.com/open-webui/open-webui/commit/7ffcd3908ee90f88a4c4684d6cd6e75efd117461) +- 🔧 **Tool server connection persistence.** Fixed a bug where tool server connection updates were not being saved to persistent storage, ensuring OAuth client information is now properly preserved. [Commit](https://github.com/open-webui/open-webui/commit/b8ea267f8ec3931de55db7801156b9c07d3ad5f6) +- 🔩 **Tool server index bounds checking.** Tool servers with invalid indices no longer crash the application with IndexError after upgrades, preventing tool server configuration loss. [#22490](https://github.com/open-webui/open-webui/issues/22490), [Commit](https://github.com/open-webui/open-webui/commit/8da29566a1f81c38e80009bdea3ce4d9be860605) +- 🔌 **Tool server frontend timeout.** Fetch requests to external tool servers now time out after 10 seconds, preventing the UI from hanging indefinitely when a configured tool server is unreachable. [#22543](https://github.com/open-webui/open-webui/issues/22543), [Commit](https://github.com/open-webui/open-webui/commit/adf7af34ff934319a35470c572237d2d08f1de0b) +- 🔌 **MCP OAuth tool auto-selection.** MCP tools requiring OAuth authentication are now automatically re-selected after completing the auth flow, instead of leaving users to manually re-enable the tool on return to the chat. [#22994](https://github.com/open-webui/open-webui/issues/22994), [#22995](https://github.com/open-webui/open-webui/pull/22995), [Commit](https://github.com/open-webui/open-webui/commit/4d50001c4192c609b1010626ebb6496692823873) +- 🏷️ **Channel @mentions.** Direct connection models no longer appear in channel @mention suggestions, preventing confusion since they don't work in channels. [#22553](https://github.com/open-webui/open-webui/issues/22553), [Commit](https://github.com/open-webui/open-webui/commit/0a87c1ecd078320a08c4cc62d41fe8727fb3b5f7) +- 📎 **Channel message attachments.** Users can now press Enter to send messages with only file or image attachments in channels, direct messages, and threads, aligning with the behavior of the Send button. [#22752](https://github.com/open-webui/open-webui/pull/22752) +- 🗣️ **Image-only message handling.** Models like Gemini and Claude no longer fail when receiving messages with only file or image attachments and no text, by stripping empty text content blocks before sending to the API. [Commit](https://github.com/open-webui/open-webui/commit/ea515fa26e11faac146c48a5e3a2a284e1792bb3), [#22880](https://github.com/open-webui/open-webui/issues/22880) +- 🧹 **Channel thread sidebar cleanup.** The thread sidebar in channels and direct messages now automatically closes when the parent message is deleted, preventing orphaned threads. [#22890](https://github.com/open-webui/open-webui/pull/22890) +- 💡 **Chat input suggestion modal.** The suggestion modal for tags, mentions, and commands now correctly reappears when backspacing into a trigger character after it was dismissed. [#22899](https://github.com/open-webui/open-webui/pull/22899) +- ⏱️ **Chat action button timing.** Action buttons under assistant messages no longer appear prematurely when switching chats while a response is still streaming. [Commit](https://github.com/open-webui/open-webui/commit/ecba37070d6eb3cb033195a070b6c4ab5f396415), [#22891](https://github.com/open-webui/open-webui/issues/22891) +- 💬 **Skill and model mention persistence.** Skills selected via $ and models selected via @ in the chat input are now properly restored after a page refresh, instead of reverting to plain text while losing their interactive state. [#22913](https://github.com/open-webui/open-webui/issues/22913), [Commit](https://github.com/open-webui/open-webui/commit/be21db706993c0db95ac09509dfdb023de64daff) +- 🧹 **Webhook profile image errors.** Fixed 404 errors appearing in the browser console when scrolling through channel messages sent by webhooks, by skipping the user profile preview for webhook senders. [#22893](https://github.com/open-webui/open-webui/pull/22893) +- 🧮 **Logit bias parameter handling.** Using logit_bias parameters no longer causes errors when the input is already in dictionary format. [#22597](https://github.com/open-webui/open-webui/issues/22597), [Commit](https://github.com/open-webui/open-webui/commit/e34ed72e1e958505e940b74bf1c6a4808640bd17) +- 🪛 **Temp chat tool calling.** Temporary chats now properly preserve tool call information, fixing native tool calling with JSON schema that was previously broken. [#22475](https://github.com/open-webui/open-webui/pull/22475), [Commit](https://github.com/open-webui/open-webui/commit/bcd313c363ca50d71aa80bcb2f29c81fad3dff37) +- 🔗 **Multi-system message merging.** Models with strict chat templates like Qwen no longer fail when multiple pipeline stages inject separate system messages, as all system messages are now merged into one at the start. [#22505](https://github.com/open-webui/open-webui/issues/22505), [Commit](https://github.com/open-webui/open-webui/commit/631bd20c3537ce85bbaec02f9e0049c88fa8fdd4) +- 📜 **Public note access.** Opening public notes via direct share link no longer returns a 500 error caused by a missing function import. [#22680](https://github.com/open-webui/open-webui/issues/22680), [Commit](https://github.com/open-webui/open-webui/commit/566e25569e5e7d9c1e42db840ba4ba578887d208) +- 👤 **Terminal access user visibility.** The terminal connection access dialog now shows the currently logged-in user when searching for users to grant access, fixing an issue where users with identical display names were filtered incorrectly. [#22491](https://github.com/open-webui/open-webui/issues/22491), [Commit](https://github.com/open-webui/open-webui/commit/4a8f995c3fd4602ec2aaccc07efc4e8504dda84d) +- 👥 **User groups display.** User groups in the admin panel profile preview now wrap properly instead of overflowing horizontally, with a scrollbar when the list is long. [#22547](https://github.com/open-webui/open-webui/pull/22547) +- 🔧 **Model list drag-and-drop.** Fixed drag-and-drop reordering of models in admin settings, preventing UI glitches and state synchronization issues. [Commit](https://github.com/open-webui/open-webui/commit/753589e51ccbbe5c4f78a7d13e19c67e6c0000d7) +- 🖼️ **Model profile image fallbacks.** Model profile images now display a fallback icon when they fail to load, and model icons no longer disappear on paginated Models pages in admin and workspace settings. [#22485](https://github.com/open-webui/open-webui/pull/22485) +- 🖼️ **Profile image fallbacks.** Added fallback handlers for model and user profile images throughout the chat interface, preventing broken image icons when avatars fail to load. [#22486](https://github.com/open-webui/open-webui/pull/22486) +- 🧲 **RAG thinking model support.** Knowledge base queries now correctly parse JSON responses from thinking models like GLM-5 and DeepSeek-R1 by stripping their reasoning blocks before JSON extraction. [#22400](https://github.com/open-webui/open-webui/pull/22400) +- 🔍 **RAG query generation robustness.** The RAG query generation, web search, and image generation handlers now correctly extract JSON from model responses containing thinking tags by finding the last JSON block instead of the first, preventing "No sources found" errors with thinking models. [#21888](https://github.com/open-webui/open-webui/issues/21888), [Commit](https://github.com/open-webui/open-webui/commit/c0fcbc5b4cb29012e2913983c632edc5d24b9aea) +- 🔍 **Ollama embedding robustness.** Ollama embedding requests now include the truncate parameter to handle inputs exceeding the context window, preventing 500 errors when processing long documents. Error messages from failed embedding requests are also now properly surfaced instead of being silently swallowed. [#22671](https://github.com/open-webui/open-webui/issues/22671), [Commit](https://github.com/open-webui/open-webui/commit/d738044f47c70c755bec9bf244aa11878fe98d9c) +- 🔄 **Ollama embedding retry logic.** Embedding requests to Ollama now retry with exponential backoff when encountering 503 errors (such as when the model reloads mid-processing), preventing files from being silently dropped from knowledge bases. [#22571](https://github.com/open-webui/open-webui/issues/22571), [Commit](https://github.com/open-webui/open-webui/commit/8b6fa1f4ab6099a305de08706621075c205f65c4) +- 🗄️ **Oracle 23AI hybrid search.** Fixed an UnboundLocalError that occurred when using hybrid search with Oracle 23AI as the vector store, preventing knowledge base queries from failing. [Commit](https://github.com/open-webui/open-webui/commit/fcf720835285a4cea10fc1ebed0b454971463b20), [#22616](https://github.com/open-webui/open-webui/issues/22616) +- 🌐 **Dynamic HTML language attribute.** The HTML lang attribute now dynamically updates when users change their interface language, preventing browsers from triggering unwanted translation popups. [Commit](https://github.com/open-webui/open-webui/commit/de5e0fbc00e7abcd84e1272c301b0707f8ea5ac6) +- 📐 **File upload deduplication.** Attaching files that are already in the chat no longer triggers duplicate uploads. [Commit](https://github.com/open-webui/open-webui/commit/10f06a64fed474e9958b96295a953e0eebf9e4be) +- 🕵️ **Serper.dev search results.** Fixed web search results not displaying properly when using the Serper.dev provider by using the correct API response field. [#22869](https://github.com/open-webui/open-webui/pull/22869) +- 🔲 **Markdown task list checkbox styling.** Fixed task list checkboxes in markdown rendering to display consistently without shrinking in narrow layouts. [#22886](https://github.com/open-webui/open-webui/pull/22886) +- 🎨 **Artifacts sidebar tab background fix.** The Artifacts sidebar now correctly updates and displays when switching back to a browser tab that was in the background, ensuring artifacts are visible without requiring a manual refresh. [#22889](https://github.com/open-webui/open-webui/issues/22889) +- 🔃 **Chat input URL indexing fix.** Fixed an issue where URLs could be indexed twice when using multiple triggers followed by backspace and re-entering a URL. [#22749](https://github.com/open-webui/open-webui/issues/22749) +- 🔎 **Search modal chat preview avatars.** Fixed assistant profile images not displaying in the chat preview pane of the Search Modal. [#22782](https://github.com/open-webui/open-webui/pull/22782) +- 📋 **Prompts search pagination fix.** Fixed a bug where searching prompts from a paginated page would incorrectly use the current page number, resulting in "No prompts found" even when matching results existed. [#22912](https://github.com/open-webui/open-webui/pull/22912) +- 🗂️ **Reasoning block copy cleanup.** Copied chat responses no longer include reasoning block content or excess whitespace, ensuring only the intended message text is captured. [#22786](https://github.com/open-webui/open-webui/issues/22786), [Commit](https://github.com/open-webui/open-webui/commit/4f0e57420154800946394bc986b2c691462b2782) +- 🔤 **Emoji removal for text normalization.** Fixed the emoji removal function used in search and title generation to correctly handle all emoji types, including those with variation selectors (❤️, ☀️, ✅), keycap sequences (1️⃣), and ZWJ family sequences (👨‍👩‍👧‍👦). [#22915](https://github.com/open-webui/open-webui/pull/22915) +- ⏹️ **Task cancellation status tracking.** Cancelled tasks now correctly mark only the affected messages as done instead of clearing all task statuses for the chat, ensuring proper status tracking when multiple messages have pending tasks. [#22743](https://github.com/open-webui/open-webui/pull/22743) +- 🎨 **Filter icon display fix.** Fixed filter icons showing the wrong icon after removing one of multiple active filters below the chat input. [#22862](https://github.com/open-webui/open-webui/pull/22862) +- 📊 **Channel message data loading.** Fixed redundant 404 API calls that occurred when rendering channel messages, preventing unnecessary requests and console errors. [#22894](https://github.com/open-webui/open-webui/pull/22894) +- 👻 **Response message skeleton display.** Fixed an issue where the skeleton loader would incorrectly show or hide based on complex status history conditions, by extracting the visibility logic into a cleaner reactive variable. [Commit](https://github.com/open-webui/open-webui/commit/5df4277216fbb9de603fdf4289f8366292568234) +- 🐛 **Shared chat viewing crash.** Shared chats can now be viewed by unauthenticated users without crashing, with proper fallback handling for missing user profile information. [#22751](https://github.com/open-webui/open-webui/pull/22751), [#22742](https://github.com/open-webui/open-webui/issues/22742) +- 🛠️ **Plugin ID sanitization.** Creating Functions or Tools with emojis or special characters in their names now generates valid IDs that pass backend validation, instead of failing with an error. [#22695](https://github.com/open-webui/open-webui/pull/22695) +- 📋 **Chat title preservation.** Regenerating responses or using branches no longer overwrites user-specified chat titles when auto-naming is disabled, by checking the full chat message count instead of just the current branch. [#22754](https://github.com/open-webui/open-webui/pull/22754) +- 🎧 **Read Aloud in chat preview.** The Read Aloud button in the Search Chats modal preview no longer causes crashes, and TTS functionality is now properly hidden in read-only chat contexts. [Commit](https://github.com/open-webui/open-webui/commit/d8fa0f426a88f5c27b3216b7db35e1db47bbba28) +- 📡 **Heartbeat event loop blocking.** The WebSocket heartbeat handler no longer blocks the event loop when updating user activity, improving responsiveness under heavy load with many concurrent connections. [#22980](https://github.com/open-webui/open-webui/pull/22980) +- 🗝️ **Message upsert API reliability.** The message upsert API endpoint no longer crashes when called, fixing an error where a database session was incorrectly passed to a function that doesn't accept it. [#22959](https://github.com/open-webui/open-webui/issues/22959), [Commit](https://github.com/open-webui/open-webui/commit/70285fb6cad26b50d783583b68be5227ace16055) +- 🔓 **Forward auth proxy compatibility.** Fixed error pages that could appear when using authenticating reverse-proxies by properly handling 401 responses from background API requests, allowing the browser to re-authenticate with the identity provider. [#22942](https://github.com/open-webui/open-webui/pull/22942) +- 🔃 **Tool call streaming display.** Sequential tool calls are now properly accumulated during streaming, fixing an issue where completed tool calls could disappear from the display before the next tool call finished streaming. [Commit](https://github.com/open-webui/open-webui/commit/a9c5c787b9f6b10491924d38645042064b3c941e) +- 🧠 **Reasoning spinner content preservation.** Prior assistant content and tool call blocks no longer disappear during the reasoning spinner when responding after tool execution. [#23001](https://github.com/open-webui/open-webui/pull/23001) +- 🖥️ **Pyodide file list refresh.** Files created or modified during manual code execution now appear immediately in the pyodide files list without requiring a browser tab refresh. [Commit](https://github.com/open-webui/open-webui/commit/5c4062c64841974bf193ff321d92d10f28a09746) +- 🖱️ **Dropdown submenu hover stability.** Secondary hover menus like Download and Move now remain open while navigating into them, fixing an issue where an 8px gap between the trigger and submenu would cause the menu to disappear before a selection could be made. [#22744](https://github.com/open-webui/open-webui/issues/22744), [Commit](https://github.com/open-webui/open-webui/commit/cffbc3558e911abd6c4780cd028794b2f7282cd7) +- 📊 **Model tag normalization.** Model tags from backends that return them as string arrays are now properly normalized to object format, preventing crashes when filtering models by tag in the admin and workspace models pages. [#20819](https://github.com/open-webui/open-webui/issues/20819), [Commit](https://github.com/open-webui/open-webui/commit/90ca2e9b0f15cc9be7cf298fbefacaa45074cae9) +- 🎯 **Arena model sub-model settings.** Arena models now properly use the selected sub-model's settings — including RAG knowledge bases, web access, code interpreter, and tool capabilities — instead of the arena wrapper's empty defaults. [#16950](https://github.com/open-webui/open-webui/issues/16950), [Commit](https://github.com/open-webui/open-webui/commit/857d7e6f373d26a7a8989417c3a7fe99cdc03f20) +- 🧩 **Model editor default metadata.** The Model Editor now loads admin-configured default model metadata instead of hardcoded values, preventing admin defaults from being silently overwritten when users save models without realizing they were overriding system-wide settings. [#22996](https://github.com/open-webui/open-webui/issues/22996), [Commit](https://github.com/open-webui/open-webui/commit/cdc2b3bf850044051aafcd46f22fb25a1899788c) +- ✏️ **Rich text paste sanitization.** Copying and pasting text with HTML characters (like `<` or `>`) no longer corrupts the editor content, as the paste handler now properly escapes HTML entities before processing mentions and special syntax. [Commit](https://github.com/open-webui/open-webui/commit/94f877ff328d410339308ad2c566c9afcdf43014) + +### Changed + +- 🪝 **User webhooks disabled by default.** User webhook notifications are now disabled by default and properly gated by the ENABLE_USER_WEBHOOKS configuration, ensuring webhooks only fire when explicitly enabled. [Commit](https://github.com/open-webui/open-webui/commit/c24a4da17dbaddf47e2e0f865c1d602d0ff36ee6) +- 🧩 **MCP integration visibility.** MCP (Streamable HTTP) integrations are now hidden from user-level settings, matching the intended behavior where only administrators can configure MCP connections through the admin panel. User-level connections now show the connection type as read-only. [#22615](https://github.com/open-webui/open-webui/issues/22615), [Commit](https://github.com/open-webui/open-webui/commit/1eef5b4f6a718c0fcf3605f1ed62669aca07b454) +- 🧲 **Web search result limit.** The configured web search result count now acts as a maximum limit, preventing models from requesting more results than administrators allow. [#22577](https://github.com/open-webui/open-webui/pull/22577) + +## [0.8.10] - 2026-03-08 + +### Added + +- 🔐 **Custom OIDC logout endpoint.** Administrators can now configure a custom OpenID Connect logout URL via OPENID_END_SESSION_ENDPOINT, enabling logout functionality for OIDC providers that require custom endpoints like AWS Cognito. [Commit](https://github.com/open-webui/open-webui/commit/3f350f865920daf2844769a758b2d2e6a7ee3efa) +- 🗄️ **MariaDB Vector community support.** Added MariaDB Vector as a new vector database backend, enabling deployments with VECTOR_DB=mariadb-vector; supports cosine and euclidean distance strategies with configurable HNSW indexing. [#21931](https://github.com/open-webui/open-webui/pull/21931) +- 📝 **Task message truncation.** Chat messages sent to task models for title and tag generation can now be truncated using a filter in the prompt template, reducing token usage and processing time for long conversations. [#21499](https://github.com/open-webui/open-webui/issues/21499) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for Portuguese (Brazil), Spanish, and Malay were enhanced and expanded. + +### Fixed + +- 🔗 **Pipeline filter HTTP errors.** Fixed a bug where HTTP errors in pipeline inlet/outlet filters would silently corrupt the user's chat payload; errors are now properly raised before parsing the response. [#22445](https://github.com/open-webui/open-webui/pull/22445) +- 📚 **Knowledge file embedding updates.** Fixed a bug where updating knowledge files left old embeddings in the database, causing search results to include duplicate and stale data. [#20558](https://github.com/open-webui/open-webui/issues/20558) +- 📁 **Files list stability.** Fixed the files list ordering to use created_at with id as secondary sort, ensuring consistent ordering and preventing page crashes when managing many files. [#21879](https://github.com/open-webui/open-webui/issues/21879) +- 📨 **Teams webhook crash.** Fixed a TypeError crash in the Teams webhook handler when user data is missing from the event payload. [#22444](https://github.com/open-webui/open-webui/pull/22444) +- 🛠️ **Process shutdown handling.** Fixed bare except clauses in the main process that prevented clean shutdown; replaced with proper exception handling. [#22423](https://github.com/open-webui/open-webui/pull/22423) +- 🐳 **Docker deployment startup.** Docker deployments now start correctly; the missing OpenTelemetry system metrics dependency was added. [#22447](https://github.com/open-webui/open-webui/pull/22447), [#22401](https://github.com/open-webui/open-webui/issues/22401) +- 🛠️ **Tool access for non-admin users.** Fixed a NameError that prevented non-admin users from viewing tools; the missing has_access function is now properly imported. [#22393](https://github.com/open-webui/open-webui/issues/22393) +- 🔐 **OAuth error handling.** Fixed a bug where bare except clauses silently caught SystemExit and KeyboardInterrupt, preventing clean process shutdown during OAuth authentication. [#22420](https://github.com/open-webui/open-webui/pull/22420) +- 🛠️ **Exception error messages.** Fixed three locations where incorrect exception raising caused confusing TypeError messages instead of proper error descriptions, making debugging much easier. [#22446](https://github.com/open-webui/open-webui/pull/22446) +- 📄 **YAML file processing.** Fixed an error when uploading YAML files with Docling enabled; YAML and YML files are now properly recognized as text files and processed correctly. [#22399](https://github.com/open-webui/open-webui/pull/22399), [#22263](https://github.com/open-webui/open-webui/issues/22263) +- 📅 **Time range month names.** Fixed month names in time range labels appearing in the wrong language when OS regional settings differ from browser language; month names now consistently display in English. [#22454](https://github.com/open-webui/open-webui/pull/22454) +- 🔐 **OAuth error URL encoding.** Fixed OAuth error messages with special characters causing malformed redirect URLs; error messages are now properly URL-encoded. [#22415](https://github.com/open-webui/open-webui/pull/22415) +- 🛠️ **Internal tool method filtering.** Tools no longer expose internal methods starting with underscore to the LLM, reducing clutter and improving accuracy. [#22408](https://github.com/open-webui/open-webui/pull/22408) +- 🔊 **Azure TTS locale extraction.** Fixed Azure text-to-speech using incomplete locale codes in SSML; now correctly uses full locale like "en-US" instead of just "en". [#22443](https://github.com/open-webui/open-webui/pull/22443) +- 🎤 **Azure speech transcription errors.** Improved Azure AI Speech error handling to display user-friendly messages instead of generic connection errors; empty transcripts, no language identified, and other Azure-specific errors now show clear descriptions. [#20485](https://github.com/open-webui/open-webui/issues/20485) +- 📊 **Analytics group filtering.** Fixed token usage analytics not being filtered by user group; the query now properly respects group filters like other analytics metrics. [#22167](https://github.com/open-webui/open-webui/pull/22167) +- 🔍 **Web search favicon fallback.** Fixed web search sources showing broken image icons when favicons couldn't be loaded from external sources; now falls back to the default Open WebUI favicon. [#21897](https://github.com/open-webui/open-webui/pull/21897) +- 🔄 **Custom model fallback.** Fixed custom model fallback not working when the base model is unavailable; the base model ID is now correctly retrieved from model info instead of empty params. [#22456](https://github.com/open-webui/open-webui/issues/22456) +- 🖼️ **Pending message image display.** Fixed images in queued messages appearing blank; image thumbnails are now properly displayed in the pending message queue. [#22256](https://github.com/open-webui/open-webui/issues/22256) +- 🛠️ **File metadata sanitization.** Fixed file uploads failing with JSON serialization errors when metadata contained non-serializable objects like callable functions; metadata is now sanitized before database insertion. [#20561](https://github.com/open-webui/open-webui/issues/20561) + +## [0.8.9] - 2026-03-07 + +### Added + +- ▶️ **Open Terminal notebook cell execution.** Users can now run Jupyter Notebook code cells directly in the Open Terminal file navigator, execute entire notebooks with a single click, edit and modify cells before running, and control the kernel - bringing full interactive notebook execution to the browser. [Commit](https://github.com/open-webui/open-webui/commit/4b3ed3e802d6f2ec8ee7caf358af810b7d09f789) +- 🗃️ **Open Terminal SQLite browser.** Users can now browse SQLite database files directly in the Open Terminal file navigator, viewing tables and running queries without downloading them first. [Commit](https://github.com/open-webui/open-webui/commit/a181b4a731a9ec7856be08d0b045a454d1341cf4) +- 📉 **Open Terminal Mermaid diagram rendering.** Markdown files with Mermaid code blocks are now rendered as diagrams directly in the Open Terminal file navigator, making it easier to visualize flowcharts and other diagrams. [Commit](https://github.com/open-webui/open-webui/commit/aaa49bdd6d6e5c10e8be554039d3cac673008fc2) +- 📓 **Open Terminal Jupyter Notebook previews.** Users can now preview Jupyter Notebook files directly in the Open Terminal file navigator, making it easier to view notebook content without downloading them first. [Commit](https://github.com/open-webui/open-webui/commit/b081e33c0a37585a1ee60b6e0e1ea03457f1e5f4) +- 🔃 **Open Terminal auto-refresh.** The Open Terminal file navigator now automatically refreshes when the model writes or modifies files, keeping the view in sync without manual refresh. [Commit](https://github.com/open-webui/open-webui/commit/828656b35f04bf486609183799cf8aa2e9850a76) +- 📎 **Open Terminal file copy button.** Users can now copy file contents directly to clipboard in the Open Terminal file navigator with a single click, making it easier to quickly grab file content without downloading. [Commit](https://github.com/open-webui/open-webui/commit/f5ea1ce250cb02fbc583c6cb3f52a923912d0178) +- 💻 **Code syntax highlighting and XLSX improvements in Open Terminal.** Code files now display with syntax highlighting in the Open Terminal file navigator, and XLSX spreadsheets now show column headers and row numbers for easier navigation. [Commit](https://github.com/open-webui/open-webui/commit/f962bae98306ea9264967b78b803397f4821f9b0) +- 🌳 **Open Terminal JSON tree view.** JSON, JSONC, JSONL, and JSON5 files now display as interactive collapsible tree views in the Open Terminal file navigator, and SVG files render as preview images with syntax highlighting support. [Commit](https://github.com/open-webui/open-webui/commit/f4c38e6001dd9d4853ed923e0bc5e790c4fd9941) +- 🛜 **Open Terminal port viewing.** Users can now view listening ports in the Open Terminal file navigator and open proxy connections to them directly from the UI. [Commit](https://github.com/open-webui/open-webui/commit/e08341dab3bb10e26a64eb44cbebd2d507087b03) +- 🎬 **Open Terminal video previews.** Users can now preview video and audio files directly in the Open Terminal file navigator, making it easier to view media without downloading them first. [Commit](https://github.com/open-webui/open-webui/commit/c40f26946f2eaeb1587a1f8b0c643b4a5121fc06) +- ✏️ **Open Terminal HTML editing.** Users can now edit HTML source files in Open Terminal with CodeMirror editor, and the save button is properly hidden in preview mode. [Commit](https://github.com/open-webui/open-webui/commit/7806cd5aef9fb0505b2c642ef70599a403cf14ba) +- 📄 **Open Terminal DOCX preview.** Word documents generated or modified by the AI can now be viewed directly in the file navigator with formatted text, tables, and images rendered inline — no need to download and open in a separate application. [Commit](https://github.com/open-webui/open-webui/commit/890949abe6b01d201355a86c50317e20da07dd34) +- 📊 **Open Terminal XLSX preview.** Excel spreadsheets in the file navigator now render as interactive tables with column headers and row numbers, making it easy to verify data the AI has generated or processed. [Commit](https://github.com/open-webui/open-webui/commit/890949abe6b01d201355a86c50317e20da07dd34) +- 📽️ **Open Terminal PPTX preview.** PowerPoint presentations created by the AI can now be viewed slide-by-slide directly in the file navigator, enabling quick review and iteration without leaving the browser. [Commit](https://github.com/open-webui/open-webui/commit/890949abe6b01d201355a86c50317e20da07dd34) +- 📁 **Pyodide file system support.** Users can now upload files for Python code execution in the code interpreter. Uploaded files are available in the `/mnt/uploads/` directory, and code can write output files there for download. The file system persists across code executions within the same session. The code interpreter now also informs models that pip install is not available in the Pyodide environment, guiding them to use alternative approaches with available modules. [#3583](https://github.com/open-webui/open-webui/issues/3583), [Commit](https://github.com/open-webui/open-webui/commit/ce0ca894fea8a2904bc6f832ff186d5fe53dd0b9), [Commit](https://github.com/open-webui/open-webui/commit/989938856fdb4b4afa584ae2d18c88d5be614ae2) +- 🧰 **Tool files access.** Tools can now access the files from the current chat context via the files property in their metadata, enabling more powerful tool integrations. [Commit](https://github.com/open-webui/open-webui/commit/35bc8310772c222fd8a466f7d00113a84e0402d0) +- ⚡ **Chat performance.** Chat messages now load and display significantly faster thanks to optimized markdown rendering, eliminating delays when viewing messages with mathematical expressions. [#22196](https://github.com/open-webui/open-webui/pull/22196), [#20878](https://github.com/open-webui/open-webui/discussions/20878) +- 📜 **Message list performance.** Improved message list rendering performance by optimizing array operations, reducing complexity from O(n²) to O(n). [#22280](https://github.com/open-webui/open-webui/pull/22280) +- 🧵 **Streaming markdown performance.** Improved chat responsiveness during streaming by skipping unnecessary markdown re-parsing when the content hasn't changed, eliminating wasted processing during model pauses. [#22183](https://github.com/open-webui/open-webui/pull/22183) +- 🏃 **Chat streaming performance.** Chat streaming is now faster for users not using the voice call feature by skipping unnecessary text parsing that was running on every token. [#22195](https://github.com/open-webui/open-webui/pull/22195) +- 🔖 **Source list performance.** Source lists in chat now render faster thanks to optimized computation that avoids unnecessary recalculations, including moving sourceIds computation to a reactive variable. [#22279](https://github.com/open-webui/open-webui/pull/22279), [Commit](https://github.com/open-webui/open-webui/commit/88af78c), [Commit](https://github.com/open-webui/open-webui/commit/339ed1d72e100c89d8eb26de761dfefe842ef90c) +- 💨 **Chat message tree operations.** Chat message tree operations are now significantly faster, improving overall chat responsiveness. [#22194](https://github.com/open-webui/open-webui/pull/22194) +- 🚀 **Initial page load speed.** Page load is now significantly faster thanks to deferred loading of the syntax highlighting library, reducing the initial JavaScript bundle by several megabytes. [#22304](https://github.com/open-webui/open-webui/pull/22304) +- 🗓️ **Action priority query optimization.** Improved performance of action priority resolution by fixing an N+1 query pattern, reducing database round-trips when loading model actions. [#22301](https://github.com/open-webui/open-webui/pull/22301) +- 🔑 **API key middleware optimization.** The API key restriction middleware was converted to a pure ASGI middleware for improved streaming performance, removing per-chunk call overhead. [#22188](https://github.com/open-webui/open-webui/pull/22188) +- 🏎️ **Model list loading performance.** Model lists now load significantly faster thanks to optimized custom model matching that uses dictionary lookups instead of nested loops. [#22299](https://github.com/open-webui/open-webui/pull/22299), [Commit](https://github.com/open-webui/open-webui/commit/29160741a3defa8768a43100cb6e63c56400279c), [Commit](https://github.com/open-webui/open-webui/commit/03c6caac1fc8625f85cf1164f5a977be8005c1bc) +- ⏱️ **Event call timeout configuration.** Administrators can now configure the WebSocket event call timeout via the WEBSOCKET_EVENT_CALLER_TIMEOUT environment variable, giving users more time to respond to event_call forms instead of timing out after 60 seconds. [#22222](https://github.com/open-webui/open-webui/pull/22222), [#22220](https://github.com/open-webui/open-webui/issues/22220) +- 🔁 **File refresh button visibility.** The refresh button in the chat file navigator now appears when viewing files as well as directories, allowing users to refresh the file view at any time. [Commit](https://github.com/open-webui/open-webui/commit/49a2e5bf573415dae6d4c7e5bd635e499c8de77a) +- 📂 **Nested folders support.** Users can now create subfolders within parent folders, improving organization of chats. A new "Create Subfolder" option is available in the folder context menu. [#22073](https://github.com/open-webui/open-webui/pull/22073), [Commit](https://github.com/open-webui/open-webui/commit/8913f37c3d8fde7dea6d54a550357f1d495b3941) +- 🔔 **Banner loading on navigation.** Admin-configured banners now load when navigating to the homepage, not just on page refresh, ensuring users see new banners immediately. [#22340](https://github.com/open-webui/open-webui/pull/22340), [#22180](https://github.com/open-webui/open-webui/issues/22180) +- 📡 **System metrics via OpenTelemetry.** Administrators can now monitor Python runtime and system metrics including CPU, memory, garbage collection, and thread counts through the existing OpenTelemetry pipeline. [#22265](https://github.com/open-webui/open-webui/pull/22265) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for French, Finnish, Turkish, German, Simplified Chinese, and Traditional Chinese were enhanced and expanded. +- 🔍 **Web search tool guidance.** The web search tool description was updated to encourage direct usage without first checking knowledge bases, making it clearer for users who want to search the web immediately. [#22264](https://github.com/open-webui/open-webui/pull/22264) + +### Fixed + +- 🗄️ **Migration memory usage.** Database migration on large deployments now processes messages in batches instead of loading everything into memory, preventing out-of-memory errors during upgrades. [#21542](https://github.com/open-webui/open-webui/pull/21542), [#21539](https://github.com/open-webui/open-webui/discussions/21539) +- 🔒 **SQLCipher connection stability.** Fixed a crash that occurred when using database encryption with SQLCipher by changing the default connection pool behavior, ensuring stable operation during multi-threaded operations like user signup. [#22273](https://github.com/open-webui/open-webui/pull/22273), [#22258](https://github.com/open-webui/open-webui/issues/22258) +- 🛑 **Stop sequence error.** Fixed a bug where setting stop sequences on a model caused the chat to fail with a split error, preventing any responses from being returned. The fix handles both string and array formats for stop tokens. [#22251](https://github.com/open-webui/open-webui/issues/22251), [Commit](https://github.com/open-webui/open-webui/commit/c7d1d1e390a79c6c86d4bfe439fd7de6f5fb060f) +- 🔐 **Microsoft OAuth refresh token fix.** Fixed a bug where Microsoft OAuth refresh token requests failed with error AADSTS90009 by adding support for the required scope parameter. Users can now stay logged in reliably with Microsoft OAuth. [#22359](https://github.com/open-webui/open-webui/pull/22359) +- 🛠️ **Parameterless tool calls.** Fixed parameterless tool calls failing during streaming by correcting the default arguments initialization, eliminating unnecessary model retries. [#22189](https://github.com/open-webui/open-webui/pull/22189) +- 🔧 **Tool call streaming fixes.** Fixed two bugs where streaming tool calls failed silently for models like GPT-5: function names were incorrectly duplicated when sent in multiple delta chunks, and arguments containing multiple JSON objects were not properly split. Tools now execute correctly instead of failing without explanation. [#22177](https://github.com/open-webui/open-webui/issues/22177), [Commit](https://github.com/open-webui/open-webui/commit/d7efdcce2b1cdbe1637a469294bf9d52dbacab53), [Commit](https://github.com/open-webui/open-webui/commit/459a60a24240eab33441ed50f4f68cc27e65a037) +- 🔗 **Tool server URL trailing slash.** Fixed tool server connection failures when URLs have trailing slashes by stripping them before path concatenation. Previously, URLs like "http://host:8080/v1/" + "/openapi.json" produced double-slash URLs that some servers rejected. [#22116](https://github.com/open-webui/open-webui/pull/22116), [#21917](https://github.com/open-webui/open-webui/issues/21917) +- 🛡️ **Citation parser error handling.** Fixed crashes when tools return error strings instead of expected data structures by adding type guards to the citation parser. The system now returns an empty source list instead of crashing with AttributeError. [#22118](https://github.com/open-webui/open-webui/pull/22118) +- 🧠 **Artifacts memory leak.** Fixed a memory leak where Svelte store subscriptions in the Artifacts component were not properly cleaned up when the component unmounted, causing memory to accumulate over time. [#22303](https://github.com/open-webui/open-webui/pull/22303) +- ♾️ **Artifacts reactive loop fix.** Fixed an infinite reactive loop in chat when artifacts are present by moving the animation frame logic outside the reactive block, preventing continuous re-rendering and CPU usage. [#22238](https://github.com/open-webui/open-webui/pull/22238), [Commit](https://github.com/open-webui/open-webui/commit/626fcff417afba642f4f71e0498267a21435c524) +- 🔀 **Artifact navigation.** Artifact navigation via arrow buttons now works correctly; the selected artifact is no longer reset when content updates. [#22239](https://github.com/open-webui/open-webui/pull/22239) +- 🧩 **Artifact thinking block fix.** Fixed a bug where HTML preview rendered code blocks inside thinking blocks for certain models like Mistral and Z.ai, causing stray code with ">" symbols to appear before the actual artifact. The fix strips thinking blocks before extracting code for artifact rendering. [#22267](https://github.com/open-webui/open-webui/issues/22267), [Commit](https://github.com/open-webui/open-webui/commit/35bc8310772c222fd8a466f7d00113a84e0402d0) +- 💬 **Floating Quick Actions availability.** Fixed an issue where the "Ask" and "Explain" Floating Quick Actions were missing when selecting text in chats that used a model that is no longer available. [#22149](https://github.com/open-webui/open-webui/pull/22149), [#22139](https://github.com/open-webui/open-webui/issues/22139) +- 💡 **Follow-up suggestions.** Fixed follow-up suggestions not appearing by correcting contradictory format instructions in the prompt template, ensuring the LLM returns the correct JSON object format. [#22212](https://github.com/open-webui/open-webui/pull/22212) +- 🔊 **TTS thinking content.** Fixed TTS playback reading think tags instead of skipping them by handling edge cases where code blocks inside thinking content prevented proper tag removal. [#22237](https://github.com/open-webui/open-webui/pull/22237), [#22197](https://github.com/open-webui/open-webui/issues/22197) +- 🎨 **Button spinner alignment.** Button spinners across multiple modals now align correctly and stay on the same line as the button text, fixing layout issues when loading states are displayed. [#22227](https://github.com/open-webui/open-webui/pull/22227) +- 📶 **Terminal keepalive.** Terminal connections now stay active without being closed by idle timeouts from proxies or load balancers, and spurious disconnection messages no longer appear. [Commit](https://github.com/open-webui/open-webui/commit/ca2aaf0321c219d041e92e2c0c842a4e424732ef) +- 📥 **Chat archive handler.** The archive button in the chat navbar now actually archives the chat and refreshes the chat list, instead of doing nothing. [#22229](https://github.com/open-webui/open-webui/pull/22229) +- 🐍 **BeautifulSoup4 dependency.** Added the missing BeautifulSoup4 package to backend requirements, fixing failures when using features that depend on HTML parsing. [#22231](https://github.com/open-webui/open-webui/pull/22231) +- 👥 **Group users default sort.** Group members in the admin panel now sort by last active time by default instead of creation date, making it easier to find active users. [#22211](https://github.com/open-webui/open-webui/pull/22211) +- 🔓 **Tool access permissions.** Users can now change tool and skill access permissions from private to public without errors. [#22325](https://github.com/open-webui/open-webui/pull/22325), [#22324](https://github.com/open-webui/open-webui/issues/22324) +- 🖥️ **Open Terminal permission fix.** Open Terminal is now visible without requiring "Allow Speech to Text" permission, fixing an issue where users without microphone access couldn't access the terminal feature. [#22374](https://github.com/open-webui/open-webui/issues/22374), [Commit](https://github.com/open-webui/open-webui/commit/70a31a9a57bdd0690ac270f31ebd1b46e8fdfa98) +- 📌 **Stale pinned models cleanup.** Pinned models that are deleted or hidden are now automatically unpinned, keeping your pinned models list up to date. [Commit](https://github.com/open-webui/open-webui/commit/af4500e5040c8343d339cd88dd1d2fb6138c7a72) +- 📏 **OpenTelemetry metric descriptions.** Fixed conflicting metric instrument descriptions that caused warnings in the OpenTelemetry collector, resulting in cleaner telemetry logs for administrators. [#22293](https://github.com/open-webui/open-webui/pull/22293) +- 🔢 **Non-streaming token tracking.** Token usage from non-streaming chat responses is now correctly saved to the database, fixing missing token counts in the Admin Panel analytics. Previously, non-streaming responses saved NULL usage data, causing messages to be excluded from token aggregation queries. [#22166](https://github.com/open-webui/open-webui/pull/22166) +- ⌨️ **Inline code typing.** Fixed a bug where typing inline code with backticks incorrectly deleted the character immediately before the opening backtick, so text formatted as inline code now correctly produces the full word instead of missing the last character. [#20417](https://github.com/open-webui/open-webui/issues/20417), [Commit](https://github.com/open-webui/open-webui/commit/e303c3da3b174da9e92a79b174f85ba574ca06ef) +- 📝 **Variable input newlines.** Fixed a bug where variables containing newlines were not displayed correctly in chat messages, and input values from Windows systems are now properly normalized to use standard line endings. [#21447](https://github.com/open-webui/open-webui/issues/21447), [Commit](https://github.com/open-webui/open-webui/commit/7b2f597b30c77ef300d1966e1c6a3edfdb0c465d) +- 📷 **Android photo capture.** Fixed an issue where the first photo taken in chat appeared completely black on some Android devices by using an alternative canvas export method. [#22317](https://github.com/open-webui/open-webui/pull/22317) +- 🪟 **Open Terminal Windows path fix.** Fixed a bug where navigating back to parent directories on Windows added an incorrect leading slash, causing directory loads to fail. Paths are now properly normalized for Windows drive letters. [#22352](https://github.com/open-webui/open-webui/issues/22352), [Commit](https://github.com/open-webui/open-webui/commit/044fd1bd15cae06a5c56a321ca79d8362942f66a) +- 🖼️ **Chat overview profile image sizing.** Fixed a bug where profile images in the chat overview could shrink incorrectly in tight spaces. The images now maintain their proper size with the flex-shrink-0 property. [#22261](https://github.com/open-webui/open-webui/pull/22261) +- 📨 **Queued messages display.** Fixed an issue where queued messages could be cut off or hidden. The queued messages area now scrolls properly when content exceeds the visible area, showing up to 25% of the viewport height. [#22176](https://github.com/open-webui/open-webui/pull/22176) +- 🖌️ **Image generation in temporary chats.** Generated images now display correctly in temporary chat mode when using builtin image generation tools. Previously, images were not shown because the code was overwriting the image list with a null database response. [#22330](https://github.com/open-webui/open-webui/pull/22330), [#22309](https://github.com/open-webui/open-webui/issues/22309) +- 🤖 **Ollama model unload fix.** Fixed a bug where unloading a model from Ollama via the Open WebUI proxy failed with a "Field required" error for the prompt field. The proxy now correctly allows omitting the prompt when using keep_alive: 0 to unload models. [#22260](https://github.com/open-webui/open-webui/issues/22260), [Commit](https://github.com/open-webui/open-webui/commit/95b65ff751f91131b633cb128ff2decdd87c4a85) +- 🏷️ **Banner type dropdown fix.** Fixed a bug where selecting a banner type required two clicks to register, as the first selection was being swallowed due to DOM structure changes. The dropdown now works correctly on the first click. [#22378](https://github.com/open-webui/open-webui/pull/22378) +- 📈 **Analytics URL encoding fix.** Fixed a bug where the Analytics page failed to load data for models with slashes in their ID, such as "anthropic/claude-opus-4.6". The frontend now properly URL-encodes forward slashes, allowing model analytics to load correctly. [#22380](https://github.com/open-webui/open-webui/issues/22380), [#22382](https://github.com/open-webui/open-webui/pull/22382) +- 📋 **Analytics chat list duplicate fix.** Fixed a bug where the Analytics page chat list threw an "each_key_duplicate" Svelte error when chat IDs were duplicated during pagination. The fix adds deterministic ordering to prevent duplicate entries. [#22383](https://github.com/open-webui/open-webui/pull/22383) +- 📂 **Folder knowledge base native tool call fix.** Fixed a bug where folders with attached knowledge bases were querying the knowledge base twice when using native tool call mode. The fix now correctly separates knowledge files from regular attachments, letting the builtin query_knowledge_files tool handle knowledge searches instead of duplicating RAG queries. [#22236](https://github.com/open-webui/open-webui/issues/22236), [Commit](https://github.com/open-webui/open-webui/commit/967b1137dcb7a52615f17d086ee89095bb9b60f3), [Commit](https://github.com/open-webui/open-webui/commit/80b5896b70d07ea868e2010b187430d43c9808f0) + +## [0.8.8] - 2026-03-02 + +### Added + +- 📁 **Open Terminal file moving.** Users can now move files and folders between directories in the Open Terminal file browser by dragging and dropping them. [Commit](https://github.com/open-webui/open-webui/commit/0c42cd2c012f9f49816adac897e2b46573b3cb6c), [Commit](https://github.com/open-webui/open-webui/commit/72951324dfeef64e09f4776898d675bc1c44f040), [Commit](https://github.com/open-webui/open-webui/commit/395098c6f1b7499d37ad55145a5931431d3e72e9), [Commit](https://github.com/open-webui/open-webui/commit/11487d66fc1a2dfafbdaa2b7ef939a86caaf3872) +- 📄 **Open Terminal HTML file preview.** Users can now preview HTML files directly in the Open Terminal file browser, with a rendered iframe view and source toggle, enabling iterative AI editing of HTML files. [Commit](https://github.com/open-webui/open-webui/commit/3909b62ffcf49839fa57346ed8487ae759811503), [Commit](https://github.com/open-webui/open-webui/commit/933a3bbbd3f4fc3eeb0ec52c7965e9ac1c4cea39) +- 🌐 **Open Terminal WebSocket proxy.** Added a new WebSocket proxy endpoint for interactive terminal sessions, enabling real-time bidirectional terminal communication with the terminal server. [Commit](https://github.com/open-webui/open-webui/commit/4f6cb771f1afded09aad6199cdb244dd8a6c77a6) +- ⚙️ **Open Terminal feature toggle.** Administrators can now enable or disable the Interactive Terminal feature for Open Terminal via configuration on the terminal server, controlling access to terminal routes. [Commit](https://github.com/open-webui/open-webui/commit/b5c3395f79bcc7ff5bc1d82bb86a60583bb3b5bd) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for Simplified Chinese, Traditional Chinese, Irish, and Catalan were enhanced and expanded. + +### Fixed + +- 🔧 **Middleware variable shadowing.** Fixed a variable shadowing issue in the middleware that could cause incorrect tool output processing during chat. [#22145](https://github.com/open-webui/open-webui/pull/22145) +- ⚡ **ChatControls reactivity fix.** Fixed a Svelte reactivity issue where the active tab state in the ChatControls panel was not properly saved when switching between chats. [#22127](https://github.com/open-webui/open-webui/pull/22127) +- 🔧 **ChatControls TypeScript fix.** Fixed a TypeScript syntax error in ChatControls.svelte where the module script block was missing lang="ts", causing esbuild to fail during vite dev. [#22131](https://github.com/open-webui/open-webui/pull/22131) +- 🔌 **Open Terminal tools for direct connections.** Fixed an issue where Open Terminal tools were not available to the model when the terminal was configured via direct connection settings, ensuring users can now interact with terminal files and operations through the AI. [#22137](https://github.com/open-webui/open-webui/issues/22137) +- 📜 **Chat history pagination.** Fixed an issue where older messages in long chats were not loaded when scrolling to the top. [Commit](https://github.com/open-webui/open-webui/commit/d7147d6cddfd314f0f1be77b15cec406a609ef36), [Commit](https://github.com/open-webui/open-webui/commit/c701ebe07bd152eecb42b0bf6de26071358a5c76) +- 🔧 **Terminal tool null parameter handling.** Fixed a bug where null parameters in terminal tool calls were sent as the string "None" instead of being omitted, causing 422 validation errors from the open-terminal server. [#22124](https://github.com/open-webui/open-webui/issues/22124), [#22144](https://github.com/open-webui/open-webui/pull/22144) + +### Changed + +## [0.8.7] - 2026-03-01 + +### Fixed + +- 🔒 **Connection access control privacy.** Tool server and terminal connections without explicit access grants are now private (admin-only) by default, fixing a bug where connections configured with no access grants were visible to all users instead of being restricted. [Commit](https://github.com/open-webui/open-webui/commit/2751a0f0b) +- 🧠 **ChatControls memory leak.** The ChatControls panel no longer leaks event listeners, ResizeObserver instances, and media query handlers when navigating between chats, fixing memory accumulation that could degrade performance during extended use. [#22112](https://github.com/open-webui/open-webui/pull/22112) +- 💾 **Temporary chat params preservation.** Model parameters are now correctly saved when creating a temporary chat, ensuring custom settings like temperature and top_p persist across the session. [Commit](https://github.com/open-webui/open-webui/commit/fe837d80e) +- ⚡ **Faster artifact content updates.** Artifact content extraction during streaming is now debounced via requestAnimationFrame, reducing redundant DOM reads and improving CPU efficiency when tokens arrive faster than the browser can paint. [Commit](https://github.com/open-webui/open-webui/commit/6863ca482) + +## [0.8.6] - 2026-03-01 + +### Added + +- 🖥️ **Open Terminal integration.** Users can now connect to [Open Terminal](https://github.com/open-webui/open-terminal) instances to browse, read, and upload files directly in chat, with the terminal acting as an always-on tool. File navigation includes folder browsing, image and PDF previews, drag-and-drop uploads, directory creation, and file deletion. The current working directory is automatically injected into tool descriptions for context-aware commands. [Commit](https://github.com/open-webui/open-webui/commit/636ab99ad8e5b71b32dd37ba7c62c32368585b2a), [Commit](https://github.com/open-webui/open-webui/commit/64ff15a5365e2c4122fccab582782669f06ec58d), [Commit](https://github.com/open-webui/open-webui/commit/4737e1f11847d057859ec78892fa89e24cbcd83b) +- 📄 **Terminal file creation.** Users can now create new empty files directly in the Open Terminal file browser, in addition to the existing folder creation functionality. [Commit](https://github.com/open-webui/open-webui/commit/234306ff57c9e24314ff805a60de919632465319) +- ✏️ **Terminal file editing.** Users can now edit text files directly in the Open Terminal file browser, with the ability to save changes back to the terminal. [Commit](https://github.com/open-webui/open-webui/commit/3d535db304bfc6fa09e655f737de8a36c0482868) +- 🛠️ **Terminal file preview toolbar.** The Open Terminal file browser now displays contextual toolbar buttons based on file type, including preview/source toggle for Markdown and CSV files, reset view for images, and improved editing controls for text files. [Commit](https://github.com/open-webui/open-webui/commit/d2b38127d0572006577b85c770607b04782de4f9) +- 🔄 **Terminal file write refresh.** The file browser now automatically refreshes when files are written or modified via the write_file or replace_file_content tools, eliminating the need to manually refresh. [Commit](https://github.com/open-webui/open-webui/commit/18865a9fef1bb154603b7b8af0116a10560e03ac) +- 🛡️ **Docker image SBOM attestation.** Docker images now include a Software Bill of Materials (SBOM) for vulnerability scanning and supply chain security compliance. [#21779](https://github.com/open-webui/open-webui/issues/21779), [Commit](https://github.com/open-webui/open-webui/commit/febc66ef2bb05606b59719e737ac5ad839002977) +- 📡 **Reporting-Endpoints security header.** Administrators can now configure a Reporting-Endpoints header via the REPORTING_ENDPOINTS environment variable to receive CSP violation reports directly, aiding in security policy debugging and hardening. [#21830](https://github.com/open-webui/open-webui/issues/21830) +- 🎯 **Action button priority sorting.** Action buttons under assistant messages now appear in a consistent order based on the priority field from function Valves, allowing developers to control button placement. [#21790](https://github.com/open-webui/open-webui/pull/21790) +- 🏷️ **Public/Private model filtering.** The Admin Settings Model listing now displays Public/Private badges and includes filter options to easily view public or private models. [#21732](https://github.com/open-webui/open-webui/issues/21732), [#21797](https://github.com/open-webui/open-webui/pull/21797) +- 👁️ **Show/Hide all models bulk action.** Administrators can now show or hide all models at once from the Admin Settings Models page Actions menu, making it faster to manage model visibility. Bulk actions now display a single toast notification on success for better user feedback. [#21838](https://github.com/open-webui/open-webui/pull/21838), [#21958](https://github.com/open-webui/open-webui/pull/21958) +- 🔐 **Individual user sharing control.** Administrators can now disable individual user sharing via the USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS environment variable, allowing only group-based sharing when set to false. [#21793](https://github.com/open-webui/open-webui/issues/21793), [Commit](https://github.com/open-webui/open-webui/commit/3d99de67716774af2f95f2e3c8e7cc4879464c71), [Commit](https://github.com/open-webui/open-webui/commit/176f9a781619d836be003d28d53904639cad4128) +- 🔄 **OAuth profile sync on login.** Administrators can now enable automatic synchronization of user profile name and email from OAuth providers on login via the OAUTH_UPDATE_NAME_ON_LOGIN and OAUTH_UPDATE_EMAIL_ON_LOGIN environment variables. [#21787](https://github.com/open-webui/open-webui/pull/21787), [Commit](https://github.com/open-webui/open-webui/commit/9478c5e7ac8254b5f522c006da0c1c49bb282727) +- 👥 **Default group share permission.** Administrators can now configure the default sharing permission for new groups via the DEFAULT_GROUP_SHARE_PERMISSION environment variable, controlling whether anyone, no one, or only members can share to new groups. [Commit](https://github.com/open-webui/open-webui/commit/538501c88da034434bcd1969f15341dbbaf154e4) +- 💨 **Streaming performance.** Chat responses now render more efficiently during streaming, reducing CPU usage and improving responsiveness. [Commit](https://github.com/open-webui/open-webui/commit/484ba91b0777042eb848134f206ef3921f968dea) +- 🧮 **Streaming message comparison.** Chat message updates during streaming are now faster thanks to an optimization that skips expensive comparisons when content changes. [#21884](https://github.com/open-webui/open-webui/pull/21884) +- 🚀 **Streaming scroll optimization.** Chat auto-scroll during streaming is now more efficient by batching scroll operations via requestAnimationFrame, reducing unnecessary layout reflows when tokens arrive faster than the browser can paint. [#21946](https://github.com/open-webui/open-webui/pull/21946) +- 📋 **Message cloning performance.** Chat message cloning during streaming is now more efficient thanks to the use of structuredClone() instead of JSON.parse(JSON.stringify(...)). [#21948](https://github.com/open-webui/open-webui/pull/21948) +- 🎯 **Faster code block rendering.** Chat message updates during streaming are now faster. [#22101](https://github.com/open-webui/open-webui/pull/22101) +- 📊 **Faster status history display.** Chat message updates during streaming are now faster. [#22103](https://github.com/open-webui/open-webui/pull/22103) +- 🛠️ **Faster tool result handling.** Tool execution results are now handled more efficiently, improving streaming performance. [#22104](https://github.com/open-webui/open-webui/pull/22104) +- 💾 **Faster model and file operations.** Model selection, file preparation, and history saving are now faster. [#22102](https://github.com/open-webui/open-webui/pull/22102) +- 🛠️ **Tool server advanced options toggle.** Advanced OpenAPI configuration options in the tool server modal are now hidden by default behind a toggle, simplifying the interface for basic setups. The admin settings tab was also renamed from "Tools" to "Integrations" for clearer organization. [Commit](https://github.com/open-webui/open-webui/commit/f0c71e5a6d971af7322d4245313e5e04620253f0), [Commit](https://github.com/open-webui/open-webui/commit/4731ccb73c4b4bab78fd86fec7b2c231af8cca8b) +- 🔧 **Faster tool loading.** Tool access control now skips an unnecessary database query when no tools are attached to the request, slightly improving performance. [#21873](https://github.com/open-webui/open-webui/pull/21873) +- ➗ **Faster math rendering.** Mathematical notation now renders more efficiently, improving responsiveness when displaying equations in chat. [#21880](https://github.com/open-webui/open-webui/pull/21880) +- 🏎️ **Faster message list updates.** The chat message list now rebuilds at most once per animation frame during streaming, reducing CPU overhead. [#21885](https://github.com/open-webui/open-webui/pull/21885) +- 📋 **Faster message rendering.** Chat message rendering is now more efficient during streaming. [#22086](https://github.com/open-webui/open-webui/pull/22086) +- 🗄️ **Faster real-time chat updates.** Chat responses now process faster with improved handling for concurrent users. [#22087](https://github.com/open-webui/open-webui/pull/22087) +- 📝 **Faster status persistence.** Only final status updates are now saved to the database during streaming, reducing unnecessary writes. [#22085](https://github.com/open-webui/open-webui/pull/22085) +- 🔄 **Faster event matching.** Event handling in the socket handler is now more efficient. [Commit](https://github.com/open-webui/open-webui/commit/ff86283be0479ccb86b639926b2b67ccbbe78746) +- 🔀 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for German, Portuguese (Brazil), Simplified Chinese, Traditional Chinese, Catalan, and Spanish were enhanced and expanded. + +### Fixed + +- 🗄️ **Database migration execution.** Database migrations now run correctly on startup, fixing a circular import issue that caused schema updates to fail silently. [#21848](https://github.com/open-webui/open-webui/pull/21848), [Commit](https://github.com/open-webui/open-webui/commit/87d33f6e18196876603eee7d1bf8e4977c7fa9c1) +- 🔔 **Notification HTML escaping.** Notification messages now properly escape HTML content, matching the behavior in chat messages and ensuring consistent rendering across the interface. [#21860](https://github.com/open-webui/open-webui/issues/21860), [Commit](https://github.com/open-webui/open-webui/commit/e83f668107723fa90ba0efa76c340c8338f45431) +- 🛠️ **Tool call JSON error handling.** Chat no longer crashes when models generate malformed JSON in tool call arguments; instead, a descriptive error message is returned to the model for retry. [#21984](https://github.com/open-webui/open-webui/pull/21984), [Commit](https://github.com/open-webui/open-webui/commit/668bd44485bdf88e9083c6f09c3c47ab97a128a4) +- 🧠 **Reasoning model KV cache preservation.** Reasoning model thinking tags are no longer stored as HTML in the database, preserving KV cache efficiency for backends like llama.cpp and ensuring faster subsequent conversation turns. [#21815](https://github.com/open-webui/open-webui/issues/21815), [Commit](https://github.com/open-webui/open-webui/commit/81781e6495dcc788c863bbf6b4aa4cf0ddd9fdcc) +- ⚡ **Duplicate model execution prevention.** Models are no longer called twice when no tools are configured, eliminating unnecessary API requests and reducing latency. [#21802](https://github.com/open-webui/open-webui/issues/21802), [Commit](https://github.com/open-webui/open-webui/commit/3c8d658160809f6d651837cf93d89dddc1d17caf) +- 🔐 **OAuth session database error.** OAuth login no longer fails with a database error when creating sessions, fixing the "'NoneType' object has no attribute 'id'" and "can't adapt type 'dict'" errors that occurred during OAuth group creation. [#21788](https://github.com/open-webui/open-webui/issues/21788) +- 👤 **User sharing permission enforcement.** The user sharing option now correctly respects the USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS setting, fixing an issue where sharing to individual users was incorrectly allowed even when disabled. [#21856](https://github.com/open-webui/open-webui/pull/21856), [Commit](https://github.com/open-webui/open-webui/commit/acb21470241ed6fd3eb3f659f196f697c418d9e8), [Commit](https://github.com/open-webui/open-webui/commit/ace69bba7512dc0a653695f9e6311712dfabb640) +- 🔑 **Password manager autofill.** Password manager autofill (like iCloud Passwords, 1Password, Bitwarden) now correctly captures filled-in passwords, fixing login failures where the password appeared filled but was sent as empty. [#21869](https://github.com/open-webui/open-webui/pull/21869), [Commit](https://github.com/open-webui/open-webui/commit/9dff497abf821dfba6eb8ea65e48a657ee91fd71) +- 📝 **RAG template duplication.** RAG templates are no longer duplicated in chat messages when models make multiple tool calls, preventing hallucinations and incorrect tool usage. [#21780](https://github.com/open-webui/open-webui/issues/21780), [Commit](https://github.com/open-webui/open-webui/commit/8f49725aa5f2d9b87e559e7d3f02f037335b7914) +- 📋 **Audit log stdout.** Audit logs now correctly appear on stdout when the ENABLE_AUDIT_STDOUT environment variable is set to true, aligning runtime behavior with the intended configuration. [#21777](https://github.com/open-webui/open-webui/pull/21777) +- 🎯 **Function valve priority resolution.** Function priorities defined in code are now correctly applied when no custom value has been saved in the database, ensuring consistent action button and filter ordering. [#21841](https://github.com/open-webui/open-webui/pull/21841) +- 📄 **Web content knowledge base append.** Processing web URLs with overwrite=false now correctly appends content to existing knowledge bases instead of silently doing nothing, fixing a regression where no content was being added. [#21786](https://github.com/open-webui/open-webui/pull/21786), [Commit](https://github.com/open-webui/open-webui/commit/5ee509325970f01524348b0f91081110340f2e7e) +- 🔍 **Web search domain filter config.** The WEB_SEARCH_DOMAIN_FILTER_LIST environment variable is now correctly read and applied, fixing an issue where domain filtering for web searches always used an empty default value. [#21964](https://github.com/open-webui/open-webui/pull/21964), [#20186](https://github.com/open-webui/open-webui/issues/20186) +- 🧹 **Tooltip memory leak.** Tooltip instances are now properly destroyed when elements change, fixing a memory leak that could cause performance issues over time. [#21969](https://github.com/open-webui/open-webui/pull/21969) +- ⌨️ **MessageInput memory leak.** Event listeners in the message input component are now properly cleaned up, preventing a memory leak that could cause page crashes during extended use. [#21968](https://github.com/open-webui/open-webui/pull/21968) +- 📝 **Notes memory leak.** Event listeners in the Notes component are now properly cleaned up, fixing a memory leak that could cause page crashes during extended use. [#21963](https://github.com/open-webui/open-webui/pull/21963) +- 🏗️ **Model create memory leak.** Event listeners in the model creation page are now properly cleaned up, fixing a memory leak that could cause page crashes during extended use. [#21966](https://github.com/open-webui/open-webui/pull/21966) +- 💬 **MentionList memory leak.** Event listeners in the MentionList component are now properly cleaned up, fixing a memory leak that could cause page crashes during extended use. [#21965](https://github.com/open-webui/open-webui/pull/21965) +- 📐 **Sidebar memory leak.** Event listeners in the Sidebar component are now properly cleaned up, fixing a memory leak that could cause page crashes during extended use. [#22082](https://github.com/open-webui/open-webui/pull/22082) +- 🎨 **Sidebar user menu positioning.** The sidebar user menu no longer drifts rightward when the sidebar is resized, keeping the menu properly aligned with its trigger. [#21853](https://github.com/open-webui/open-webui/pull/21853) +- 💻 **Code block UI.** Code block headers are now sticky and properly positioned, with language labels now showing tooltips for truncated text. [Commit](https://github.com/open-webui/open-webui/commit/6b462ff121d28cd2d335db7763052622d374e3a5) +- 📊 **Multi-model responses horizontal scroll.** The model list in multi-model responses tabs now has horizontal scroll support, making all models accessible on desktop screens. [#21800](https://github.com/open-webui/open-webui/issues/21800), [Commit](https://github.com/open-webui/open-webui/commit/a3de0bcc586ddd14dde6ae915067f082d628eaeb) +- 🎭 **TailwindCSS gray color theme.** Custom gray color palette is now correctly applied to the CSS root theme layer, fixing an issue where --color-gray-x variables were missing. [#21900](https://github.com/open-webui/open-webui/pull/21900), [#21899](https://github.com/open-webui/open-webui/issues/21899) +- 📎 **Broken documentation links.** Fixed broken links in the backend config and admin settings that pointed to outdated documentation locations. [#21904](https://github.com/open-webui/open-webui/pull/21904) +- 🔓 **OAuth session token decryption.** OAuth sessions are now properly detached from the database context before token decryption, preventing potential database session conflicts when reading encrypted tokens. [#21794](https://github.com/open-webui/open-webui/pull/21794) +- 🕐 **Chat timestamp i18n fix.** Chat timestamps in the sidebar now display correctly, fixing an issue where the time ago format (e.g., "5m", "2h", "3d") was not being localized properly due to incorrect variable casing in the translation function. [Commit](https://github.com/open-webui/open-webui/commit/ae28e7d24530eb9f7909b293bcd0f33048a022a9) +- 🍞 **Model toast notification fix.** Hiding or showing a single model now displays only one toast notification instead of two, removing the redundant generic "model updated" message when a specific action toast is shown. [#22079](https://github.com/open-webui/open-webui/pull/22079) +- 📡 **Offline mode embedding model fix.** Open WebUI no longer attempts to download embedding models when in offline mode, fixing error logs that occurred when trying to fetch models that weren't cached locally. [#22106](https://github.com/open-webui/open-webui/pull/22106), [#21405](https://github.com/open-webui/open-webui/issues/21405) + +## [0.8.5] - 2026-02-23 + +### Added + +- ⌨️ **Voice dictation shortcut.** Users can now toggle voice dictation using Cmd+Shift+L (or Ctrl+Shift+L on Windows/Linux), making it faster to start and stop dictation without clicking the microphone button. + +### Fixed + +- 🚫 **Model access KeyError fix.** The /api/models endpoint no longer crashes with a 500 error when models have incomplete info metadata missing the user_id field (e.g. models using global default metadata). +- 🔄 **Frontend initialization resilience.** The app layout now gracefully handles individual API failures during initialization (getModels, getBanners, getTools, getUserSettings, setToolServers) instead of blocking the entire page load when any single call fails. +- 🛡️ **Backend config null safety.** Language detection during app initialization no longer crashes when the backend config fetch fails, preventing a secondary cause of infinite loading. + +## [0.8.4] - 2026-02-23 + +### Added + +- 🛜 **Provider URL suggestions.** The connection form now displays a dropdown with suggested URLs for popular AI providers, making it easier to configure connections. [Commit](https://github.com/open-webui/open-webui/commit/49c36238d01aaff5466344ecd316a6dd3edd74a3) +- ☁️ **Anthropic model fetching.** The system now properly fetches available models from the Anthropic API, ensuring all Anthropic models are accessible. [Commit](https://github.com/open-webui/open-webui/commit/e9d852545cc17f0eeb8bdcfa77575a80fed8706d) +- 💡 **No models prompt.** When no models are available, a helpful prompt now guides users to manage their provider connections. [Commit](https://github.com/open-webui/open-webui/commit/a0195cd5ae9b9915295839cd0a5fbac5a1b0bfa2) +- ⚙️ **Connection enable/disable toggles.** Individual provider connections can now be enabled or disabled from both admin and user settings. [Commit](https://github.com/open-webui/open-webui/commit/990c638f6cf91507b61898f454c26f9516114c36) +- ⏸️ **Prompt enable/disable toggle.** Users can now enable or disable prompts directly from the prompts list using a toggle switch, without needing to delete and recreate them. Inactive prompts display an "Inactive" badge and are still visible in the list. [Commit](https://github.com/open-webui/open-webui/commit/094ed0b48cb86b9b6aff3c93f522072d11230761) +- 🗑️ **Memory deletion.** Agents can now delete specific memories that are no longer relevant, duplicated, or incorrect, giving better control over stored memory content. [Commit](https://github.com/open-webui/open-webui/commit/094ed0b48cb86b9b6aff3c93f522072d11230761) +- 📋 **Memory listing.** Agents can now list all stored memories, enabling them to identify which memories to manage or delete based on the complete memory inventory. [Commit](https://github.com/open-webui/open-webui/commit/094ed0b48cb86b9b6aff3c93f522072d11230761) +- 📦 **Auto pip install toggle.** Administrators can now disable automatic pip package installation from function frontmatter requirements using the ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS environment variable, providing more control over function dependency management. [Commit](https://github.com/open-webui/open-webui/commit/8bfab327ec5f635f9fe93c26efd198712ff7116d) +- 🔗 **Anthropic Messages API proxy.** A new API endpoint now supports the Anthropic Messages API format, allowing tools like Claude Code to authenticate through Open WebUI and access configured models. Tool calls are now properly supported in streaming responses with correct multi-block indexing, and error status from tools is propagated correctly. The endpoint converts requests to OpenAI format internally, routes them through the existing chat pipeline, and returns responses in Anthropic format. [#21390](https://github.com/open-webui/open-webui/discussions/21390), [Commit](https://github.com/open-webui/open-webui/commit/91a0301c9e22e93295a7c471d83592a802560795), [Commit](https://github.com/open-webui/open-webui/commit/a9312d25373d3aa161788598f87180b8db11c5b6) +- 👥 **Multi-device OAuth sessions.** Users can now stay logged in on multiple devices simultaneously with OAuth, as re-logging in no longer terminates existing sessions. The oldest sessions are automatically pruned when the session limit is exceeded. [#21647](https://github.com/open-webui/open-webui/issues/21647), [Commit](https://github.com/open-webui/open-webui/commit/ae05586fdabf318d551b53ede41575355d3b9e2b) +- 🔐 **OAuth group default share setting.** Administrators can now configure the default sharing setting for OAuth-created groups using the OAUTH_GROUP_DEFAULT_SHARE environment variable, allowing control over whether new groups default to private or shared with members. [#21679](https://github.com/open-webui/open-webui/pull/21679), [Commit](https://github.com/open-webui/open-webui/commit/4b9f821b58007d4efa4aa16a4995b23126e08a88) +- 🔧 **Knowledge base import behavior.** The web content import endpoint now supports a configurable overwrite flag, allowing users to add multiple URLs to the same knowledge base instead of replacing existing content. [#21613](https://github.com/open-webui/open-webui/pull/21613), [#21336](https://github.com/open-webui/open-webui/issues/21336), [Commit](https://github.com/open-webui/open-webui/commit/4bef69cc6344ff809090441aa6bced573a2aa838) +- 🧩 **Skill JSON import support.** Skills can now be imported from both JSON and Markdown files. [#21511](https://github.com/open-webui/open-webui/issues/21511) +- 🔍 **You.com web search provider.** A new web search provider option for You.com is now available, giving users another search engine choice for web-enabled models. The You.com provider enriches search results by including both descriptions and snippets for better context. [#21599](https://github.com/open-webui/open-webui/pull/21599) +- 🚀 **Message list performance.** Loading conversation history when sending messages is now significantly faster, improving response latency before the model starts generating. This also speeds up chat search and RAG context building. [#21588](https://github.com/open-webui/open-webui/pull/21588) +- 🎯 **Concurrent embedding request control.** Administrators can now control the maximum number of concurrent embedding API requests using the RAG_EMBEDDING_CONCURRENT_REQUESTS environment variable, helping manage API rate limits while maintaining embedding performance. [#21662](https://github.com/open-webui/open-webui/pull/21662), [Commit](https://github.com/open-webui/open-webui/commit/5d4547f934b6fbe751bb2041f9597fe11ddf8e43) +- ⚡ **Message upsert optimization.** Loading chat data during message saving is now significantly faster by eliminating a redundant database call that occurred on every message upsert, which happens many times during streaming responses. [#21592](https://github.com/open-webui/open-webui/pull/21592) +- ⚡ **Message send optimization.** Loading chat data during message sending is now significantly faster by eliminating unnecessary full conversation history loads. The system now uses targeted queries that fetch only the needed data instead of loading entire chat objects with all message history. [#21596](https://github.com/open-webui/open-webui/pull/21596) +- 🚀 **Tag filtering optimization.** Chat search with tag filtering now uses more efficient database queries, making filtered searches significantly faster. [Commit](https://github.com/open-webui/open-webui/commit/139f02a9d9fa2ffffcc96aa0de8af8ef51b6bcf2) +- ⚡ **Shared chat loading optimization.** The shared chats endpoint now loads only the needed columns instead of the full conversation history, making shared chat listings significantly faster. [#21614](https://github.com/open-webui/open-webui/pull/21614) +- 🗂️ **Archived and pinned chat loading.** Loading archived and pinned chat lists is now significantly faster by loading only the needed columns instead of full conversation data. [#21591](https://github.com/open-webui/open-webui/pull/21591) +- 💨 **Chat title query optimization.** Retrieving chat titles now queries only the title column instead of the entire conversation history, making title lookups significantly faster and reducing database load. [#21590](https://github.com/open-webui/open-webui/pull/21590) +- 🗄️ **Batch access grants for multiple resources.** Loading channels, knowledge bases, models, notes, prompts, skills, and tools now uses batch database queries for access grants instead of individual queries per item, significantly reducing database load. For 30 items, this reduces approximately 31 queries to just 3. [#21616](https://github.com/open-webui/open-webui/pull/21616) +- 📋 **Notes list payload optimization.** Notes list and search endpoints now return only a 200-character preview instead of the full note content, reducing response payload from ~167 MB to ~10 KB for 60 notes and eliminating N+1 queries for access grants. The Notes tab now loads in seconds instead of tens of seconds. [#21549](https://github.com/open-webui/open-webui/pull/21549) +- ⚡ **Tools list performance.** Loading the tools list is now significantly faster by deferring content and specs fields from database queries, and using cached tool modules instead of reloading them for each request. [Commit](https://github.com/open-webui/open-webui/commit/b48594a16680cc77921a4ed1a11ffa07df7edc60) +- 📝 **Group description display.** The admin groups list now shows each group's description, making it easier for administrators to identify groups at a glance. +- 🏷️ **Sort by dropdown.** Administrators can now sort groups using a dropdown menu with options for Name or Members, replacing the previous clickable column headers. +- 📶 **Admin groups list sorting.** The Group and Users columns in the admin groups list are now clickable for sorting, allowing administrators to sort groups alphabetically by name or numerically by member count. [#21692](https://github.com/open-webui/open-webui/pull/21692) +- 🔽 **Rich UI auto-scroll.** The view now automatically scrolls to action-generated Rich UI content once it renders, ensuring users can see the results without manually scrolling. [#21698](https://github.com/open-webui/open-webui/pull/21698), [#21482](https://github.com/open-webui/open-webui/discussions/21482) +- 📊 **Admin analytics toggle.** Administrators can now enable or disable the analytics feature using the ENABLE_ADMIN_ANALYTICS environment variable, giving more control over available admin features. [#21651](https://github.com/open-webui/open-webui/pull/21651), [Commit](https://github.com/open-webui/open-webui/commit/35598b8017557258b8c9ee3469d320adb0140751) +- 📊 **Analytics sorting enhancement.** The Analytics dashboard now supports sorting by Tokens column for both Model Usage and User Usage tables, and the Share/Percentage columns are now clickable for sorting. Administrators can more easily identify the most token-consuming models and users. [Commit](https://github.com/open-webui/open-webui/commit/053a33631f575ae1ad3123190a9e820b4057f62d) +- 📑 **Fetch URL citation sources.** When models fetch URLs during tool calling, the fetched URLs now appear as clickable citation sources in the UI with content previews, matching the existing behavior of web search and knowledge file tools. [#21669](https://github.com/open-webui/open-webui/pull/21669) +- 🔗 **Admin settings tab navigation.** The admin settings sidebar now supports native browser tab opening, allowing users to middle-click or right-click to open settings pages in new tabs. The navigation was converted from button-based to anchor-based elements. [#21721](https://github.com/open-webui/open-webui/pull/21721) +- 🏷️ **Model visibility badges.** The Admin Settings Models page now displays Public or Private badges directly on each model, making it easy to identify model access levels at a glance without opening the edit screen. [#21732](https://github.com/open-webui/open-webui/issues/21732), [Commit](https://github.com/open-webui/open-webui/commit/29217cb430bd47827ebb20782b264ae7b0f233bb) +- 🛠️ **Global model defaults.** Administrators can now configure default metadata and parameters that automatically apply to all models, reducing manual configuration for newly discovered models. Default capabilities (like vision, web search, code interpreter) and parameters (like temperature, max_tokens) can be set globally in Admin Settings, with per-model overrides still available. [#20658](https://github.com/open-webui/open-webui/issues/20658), [Commit](https://github.com/open-webui/open-webui/commit/c341f97cfe15510b7d128bd84f1e607b5289b957) +- 💬 **Plaintext tool output display.** Tool outputs that are plain strings now display naturally in a monospace block instead of quoted/escaped format, making multi-line string outputs easier to read. [#21553](https://github.com/open-webui/open-webui/issues/21553), [Commit](https://github.com/open-webui/open-webui/commit/3ad2ea6f2839e97e53f00fd797a9e083ff78d88e) +- 🔐 **Event call input masking.** Functions can now request masked password input in confirmation dialogs, allowing sensitive data entry to be hidden from view. This extends the existing masking feature from user valves to event calls. [#21540](https://github.com/open-webui/open-webui/issues/21540), [Commit](https://github.com/open-webui/open-webui/commit/4853ededcabcd76d9bd2036181486cd3a41458a1) +- 🗂️ **JSON logging support.** Administrators can now enable JSON-formatted logging by setting the LOG_FORMAT environment variable to "json", making logs suitable for log aggregators like Loki, Fluentd, CloudWatch, and Datadog. [#21747](https://github.com/open-webui/open-webui/pull/21747) +- ♿ **UI accessibility improvements.** Screen reader users can now navigate the interface more easily with improved keyboard navigation in dialogs and proper ARIA labels on all interactive elements. Added aria-labels to close, back, and action buttons across various components, and improved semantic HTML and screen reader support across auth, sidebar, chat, and notification components, addressing WCAG compliance. Added aria-labels to search inputs, select fields, and modals in admin and user settings, and improved accessibility for text inputs, rating components, citations, and web search results. Added aria-labels to workspace components including Knowledge, Models, Prompts, Skills, and Tools pages for improved screen reader support. [#21706](https://github.com/open-webui/open-webui/pull/21706), [#21705](https://github.com/open-webui/open-webui/pull/21705), [#21710](https://github.com/open-webui/open-webui/pull/21710), [#21709](https://github.com/open-webui/open-webui/pull/21709), [#21717](https://github.com/open-webui/open-webui/pull/21717), [#21715](https://github.com/open-webui/open-webui/pull/21715), [#21708](https://github.com/open-webui/open-webui/pull/21708), [#21719](https://github.com/open-webui/open-webui/pull/21719) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for Finnish, French, Portuguese (Brazil), Simplified Chinese, and Traditional Chinese were enhanced and expanded. + +### Fixed + +- 💥 **Admin functions page crash fix.** The admin Functions tab no longer crashes when clicked, fixing a null reference error that occurred while the functions list was loading. [#21661](https://github.com/open-webui/open-webui/pull/21661), [Commit](https://github.com/open-webui/open-webui/commit/8265422ba0660e7ba2192eb19efd70f8be652748) +- 💀 **Cyclic chat history deadlock fix.** Chat histories with circular parent-child message references no longer cause the backend to freeze when syncing usage stats. The system now detects and safely aborts when encountering cyclic message references. [#21681](https://github.com/open-webui/open-webui/pull/21681) +- 🔀 **Model fallback routing fix.** Custom model fallback now works correctly across all model types, preventing "Model not found" errors when the fallback model uses a different backend (pipe, Ollama, or OpenAI). [#21736](https://github.com/open-webui/open-webui/pull/21736) +- 🐛 **Default model selection fix.** Admin-configured default models are now properly respected when starting new chats instead of being overwritten by the first available model. [#21736](https://github.com/open-webui/open-webui/pull/21736) +- 👁️ **Scroll jumping fix.** Deleting a message pair after stopping generation no longer causes the chat to visually jump around, making message deletion smoother. [#21743](https://github.com/open-webui/open-webui/pull/21743), [Commit](https://github.com/open-webui/open-webui/commit/1f474187a77d2c8a392f00d86f48eb3cb3a18b88) +- 💬 **New chat message handling fix.** Fixed a bug where clicking "New Chat" after sending a message would silently drop subsequent messages. The system now properly clears pending message queues when starting a new conversation. [#21731](https://github.com/open-webui/open-webui/pull/21731) +- 🔍 **RAG template mutation fix.** Fixed a bug where RAG template text was recursively injected into user messages during multiple sequential tool calls, causing message content to grow exponentially and potentially confuse the model. The system now preserves the original user message before tool-calling loops and correctly accumulates citation sources. [#21663](https://github.com/open-webui/open-webui/issues/21663), [#21668](https://github.com/open-webui/open-webui/pull/21668), [Commit](https://github.com/open-webui/open-webui/commit/becac2b2b7af8aacadbfc9b7cee2024cf7ed6acc) +- 🔒 **Iframe sandbox security.** Embedded tools can no longer submit forms or access same-origin content by default, improving security for users. [#21529](https://github.com/open-webui/open-webui/pull/21529) +- 🔐 **Signup race condition fix.** Fixed a security vulnerability where multiple admin accounts could be created on fresh deployments when running multiple uvicorn workers. The signup handler now properly handles concurrent requests during first-user registration, preventing unauthorized admin privilege escalation. [#21631](https://github.com/open-webui/open-webui/pull/21631) +- 🔐 **LDAP optional fields fix.** LDAP configuration now properly accepts empty Application DN and password values, allowing LDAP authentication to work without these optional fields. Previously, empty values caused authentication failures. [Commit](https://github.com/open-webui/open-webui/commit/e1fa42d48a15c8b496a887ecfa32fc01cfd74b36) +- 🛠️ **API tools fix.** The /api/v1/chat/completions endpoint now properly respects caller-provided tools instead of overriding them with server-side tools, fixing issues where external agents like Claude Code or Cursor would receive unexpected tool advertisements. [#21557](https://github.com/open-webui/open-webui/issues/21557), [#21555](https://github.com/open-webui/open-webui/pull/21555) +- ⏱️ **Embeddings and proxy timeout fix.** The embeddings and OpenAI proxy endpoints now properly honor the AIOHTTP_CLIENT_TIMEOUT environment variable, instead of using default timeouts that could cause requests to hang. [#21558](https://github.com/open-webui/open-webui/pull/21558) +- 📄 **Text file type detection fix.** TypeScript and other text files that were mis-detected as video files based on their extension are now correctly identified and processed as text files, fixing upload rejections for .ts files. [#21454](https://github.com/open-webui/open-webui/issues/21454), [Commit](https://github.com/open-webui/open-webui/commit/f651809001ba8e40ba5f416773c1aa6f082a6c46) +- 🗄️ **File access control respect.** The files list and search endpoints now properly respect the BYPASS_ADMIN_ACCESS_CONTROL setting, ensuring admins only see their own files when the setting is disabled, consistent with other endpoints. [#21595](https://github.com/open-webui/open-webui/pull/21595), [#21589](https://github.com/open-webui/open-webui/issues/21589) +- 🗄️ **PostgreSQL workspace cloning.** Cloning workspace models now works correctly on PostgreSQL databases by generating proper unique IDs for access grants instead of using potentially duplicate or invalid IDs. [Commit](https://github.com/open-webui/open-webui/commit/3dd44c4f1931d13bfd46062291c6f23b33dde003) +- 🔓 **MCP SSL verification fix.** MCP tool connections now properly respect the AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL environment variable to disable SSL verification, instead of always verifying SSL certificates. [Commit](https://github.com/open-webui/open-webui/commit/af5661c2c807465f5600899e8c1a421f96cd7a8c), [#21481](https://github.com/open-webui/open-webui/issues/21481) +- 🔒 **Model default feature permissions.** Model default features like code interpreter, web search, and image generation now respect global configuration and user permission settings, preventing disabled features from appearing in the chat input. [#21690](https://github.com/open-webui/open-webui/pull/21690) +- 🔍 **Model selector typing fix.** The model selector list no longer disappears or becomes grayed out when typing quickly in the search field, thanks to improved virtual scroll handling. [#21659](https://github.com/open-webui/open-webui/pull/21659) +- ⛔ **Disabled model cloning prevention.** Disabled models can no longer be cloned as workspace models, preventing invalid empty configurations from being created. The Clone option is now hidden for inactive models. [#21724](https://github.com/open-webui/open-webui/pull/21724) +- 🔧 **SCIM parameter handling.** The SCIM Users and Groups endpoints now accept out-of-range startIndex and count values by clamping them to valid ranges instead of returning errors, in compliance with RFC 7644. [#21577](https://github.com/open-webui/open-webui/pull/21577) +- 🔍 **Hybrid search result fix.** Hybrid search now returns correct results after fixing a bug where query result unpacking order was mismatched, causing search results to appear empty. [#21562](https://github.com/open-webui/open-webui/pull/21562) +- 🛠️ **Imported items display.** Imported functions and tools now appear immediately in the list after import, without requiring a page reload. [#21593](https://github.com/open-webui/open-webui/issues/21593) +- 🔄 **WebSocket race condition fix.** Collaborative note saves no longer crash with errors when users disconnect before pending saves complete, preventing AttributeError exceptions and excessive logging. [#21601](https://github.com/open-webui/open-webui/issues/21601), [Commit](https://github.com/open-webui/open-webui/commit/0a700aafe46dfea2cf9721bb81725d2582b0d781) +- ✋ **Drag-and-drop overlay fix.** The "Add Files" overlay no longer remains stuck on screen when dragging files back out of the chat window in Mozilla Firefox. [#21664](https://github.com/open-webui/open-webui/pull/21664) +- 👁️ **Group search visibility fix.** Groups now appear correctly in access control search results, even when the search doesn't match any users. [#21691](https://github.com/open-webui/open-webui/pull/21691) +- 🖱️ **User menu drag and click fixes.** Fixed draggable ghost images when dragging menu items and eliminated phantom link clicks that occurred when dragging outside dropdown menus. [#21699](https://github.com/open-webui/open-webui/pull/21699) +- 🧭 **Admin and workspace nav drag fix.** Fixed ghost drag images when dragging top navigation tabs in the Admin and Workspace panels by adding proper drag constraints and text selection prevention. [#21701](https://github.com/open-webui/open-webui/pull/21701) +- 🎮 **Playground nav drag fix.** Fixed ghost drag images when dragging top navigation tabs in the Playground panel by adding proper drag constraints and text selection prevention. [#21704](https://github.com/open-webui/open-webui/pull/21704) +- ✋ **Dropdown menu drag fix.** Dropdown menu items can no longer be accidentally dragged as ghost images when highlighting text, making menu interactions smoother. [#21713](https://github.com/open-webui/open-webui/pull/21713) +- 🗂️ **Folder menu drag fix.** Folder dropdown menu items can no longer be accidentally highlighted or dragged as ghost images, making folder options behave like standard menus. [#21753](https://github.com/open-webui/open-webui/pull/21753) +- 📝 **Console log spam fix.** Requesting deleted or missing files no longer floods the backend console with Python traceback logs, thanks to proper exception handling for expected 404 errors. [#21687](https://github.com/open-webui/open-webui/pull/21687) +- 🐛 **Firefox avatar overflow fix.** Fixed a visual bug in Firefox where broken model or user avatar images would display overflowing alt text that overlapped adjacent labels on the Analytics and Leaderboard pages. Failed avatar images now properly show fallback icons instead. [#21730](https://github.com/open-webui/open-webui/pull/21730) +- 🎨 **Dark mode select background fix.** Fixed an issue where select inputs and dropdown menus had inconsistent lighter background colors in dark mode by removing conflicting dark theme overrides, ensuring a cohesive transparent look. [#21728](https://github.com/open-webui/open-webui/pull/21728) +- 💾 **Prompt import fix.** Importing prompts that were previously exported no longer fails with a "[object Object]" error toast, making prompt backup and restore work correctly. [#21594](https://github.com/open-webui/open-webui/issues/21594) +- 🔧 **Ollama reasoning effort fix.** Reasoning effort now works correctly with Ollama models that require string values ("low", "medium", "high") instead of boolean, fixing "invalid option provided" errors when using models like GPT-OSS. [#20921](https://github.com/open-webui/open-webui/issues/20921), [#20928](https://github.com/open-webui/open-webui/pull/20928), [Commit](https://github.com/open-webui/open-webui/commit/30a13b9b2fb2c6da7e1ddbf52edb93a58d09cc56) +- 🔍 **Hybrid search deduplication fix.** Hybrid search now correctly deduplicates results using content hashes, preventing duplicate chunks from appearing when using enriched text for BM25 search. [Commit](https://github.com/open-webui/open-webui/commit/d9fd2a3f30481efa24cc54193bf2f67fd0299b52) +- 📋 **SQLAlchemy warning fix.** Fixed a SQLAlchemy warning that appeared in logs when deleting shared chats, improving log clarity. [Commit](https://github.com/open-webui/open-webui/commit/0185f3340d2778f3b75a8036b0e81a0aec78037f) + +### Changed + +- 🎯 **Prompt suggestions relocated.** Prompt suggestions have been moved from Admin Panel - Settings - Interface to Admin Panel - Settings - Models, where they can now be configured per-model or globally via the new model defaults. +- 📢 **Banners relocated.** Banners configuration has been moved from Admin Panel - Settings - Interface to Admin Panel - Settings - General. + +## [0.8.3] - 2026-02-17 + +### Added + +- ✏️ **Model edit shortcut.** Users can now edit models directly from the model selector dropdown menu, making it faster to modify model settings without navigating to separate admin or workspace pages. [Commit](https://github.com/open-webui/open-webui/commit/519ff40cb69cdc1d215cee369e9db70ff7438153) +- 🎨 **Image edit API background support.** The image edit API now supports the background parameter for OpenAI's gpt-image-1 model, enabling background transparency control ("transparent", "opaque", "auto") when the feature is exposed in the UI. [#21459](https://github.com/open-webui/open-webui/pull/21459) +- ⚡ **Faster model filtering.** Model access control filtering no longer makes a redundant database query to re-fetch model info that is already available in memory, reducing latency when loading model lists for non-admin users. [Commit](https://github.com/open-webui/open-webui/commit/34cd3d79e8688f589e3dd2f03415f8a8f9a13115) +- 🔧 **Tool call display improvements.** Tool call results now display arguments in a cleaner key-value format instead of raw JSON, with a responsive layout that shows only the tool name on narrow screens and the full label on wider screens, preventing text wrapping to multiple lines. [Commit](https://github.com/open-webui/open-webui/commit/2ce935bdb10d2b26b230cd54cb649f5c667ed96a) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 Translations for Portuguese (Brazil), Simplified Chinese, and Traditional Chinese were enhanced and expanded. + +### Fixed + +- 📧 **USER_EMAIL variable fix.** The {{USER_EMAIL}} template variable now correctly returns the user's email address instead of "Unknown" in prompts. [#21479](https://github.com/open-webui/open-webui/pull/21479), [#21465](https://github.com/open-webui/open-webui/issues/21465) +- 🖼️ **Image and file attachment handling fixes.** Uploaded images are now correctly sent to vision-enabled models, and file attachments now work even when no user text is entered alongside a system prompt. This fixes two issues where the backend was not properly processing file attachments: images weren't converted to the expected format for API requests, and file context was dropped when the user sent only a file without accompanying text. [Commit](https://github.com/open-webui/open-webui/commit/f1053d94c7ef7b8b78682dd73586b65a84d202a1), [#21477](https://github.com/open-webui/open-webui/issues/21477), [#21457](https://github.com/open-webui/open-webui/issues/21457) +- 🛡️ **Missing function error handling.** Models that reference deleted functions no longer cause the entire /api/models endpoint to crash; instead, the missing functions are skipped and logged, allowing the rest of the models to load successfully. [#21476](https://github.com/open-webui/open-webui/pull/21476), [#21464](https://github.com/open-webui/open-webui/issues/21464) +- 🚀 **Startup model pre-fetch error handling.** If model pre-fetching fails during app startup, the application now logs a warning and continues instead of crashing entirely. [Commit](https://github.com/open-webui/open-webui/commit/337109e99ce390f55a9085d0a301853637923779) +- ⚙️ **Function module loading error handling.** Function modules that fail to load during startup or model processing are now caught and logged, preventing crashes when models reference functions with loading errors. [Commit](https://github.com/open-webui/open-webui/commit/15b893e651de71b033408e1b713e0b51f6829ab8) +- 🗄️ **PostgreSQL group query fix.** The '/api/v1/groups/' endpoint no longer fails with a GROUP BY error when using PostgreSQL; member counts are now calculated using correlated subqueries for better database compatibility. [#21458](https://github.com/open-webui/open-webui/pull/21458), [#21467](https://github.com/open-webui/open-webui/issues/21467) + +## [0.8.2] - 2026-02-16 + +### Added + +- 🧠 **Skill content handling.** User-selected skills now have their full content injected into the chat, while model-attached skills only display name and description in the available skills list. This allows users to override skill behavior while model-attached skills remain flexible. [Commit](https://github.com/open-webui/open-webui/commit/393c0071dc612c5ac982fb37dfc0288cb9911439) +- ⚙️ **Chat toggles now control built-in tools.** Users can now disable web search, image generation, and code execution on a per-conversation basis, even when those tools are enabled as builtin tools on the model. [#20641](https://github.com/open-webui/open-webui/issues/20641), [#21318](https://github.com/open-webui/open-webui/discussions/21318), [Commit](https://github.com/open-webui/open-webui/commit/c46ef3b63bcc1e2e9adbdd18fab82c4bbe33ff6c), [Commit](https://github.com/open-webui/open-webui/commit/f1a1e64d2e9ad953b2bc2a9543e9a308b7c669c8) +- 🖼️ **Image preview in file modal.** Images uploaded to chats can now be previewed directly in the file management modal, making it easier to identify and manage image files. [#21413](https://github.com/open-webui/open-webui/issues/21413), [Commit](https://github.com/open-webui/open-webui/commit/e1b3e7252c1896c04d498547908f0fce111434e1) +- 🏷️ **Batch tag operations.** Tag creation, deletion, and orphan cleanup for chats now use batch database queries instead of per-tag loops, significantly reducing database round trips when updating, archiving, or deleting chats with multiple tags. [Commit](https://github.com/open-webui/open-webui/commit/c748c3ede) +- 💨 **Faster group list loading.** Group lists and search results now load with a single database query that joins member counts, replacing the previous pattern of fetching groups first and then counting members in a separate batch query. [Commit](https://github.com/open-webui/open-webui/commit/33308022f) +- 🔐 **Skills sharing permissions.** Administrators can now control skills sharing and public sharing permissions per-group, matching the existing capabilities for tools, knowledge, and prompts. [Commit](https://github.com/open-webui/open-webui/commit/88401e91c) +- ⚡ **Long content truncation in preview modals.** Citation and file content modals now truncate markdown-rendered content at 10,000 characters with a "Show all" expansion button, preventing UI jank when previewing very large documents. +- 🌐 **Translation updates.** Translations for Spanish and German were enhanced and expanded. + +### Fixed + +- 🔐 **OAuth session error handling.** Corrupted OAuth sessions are now gracefully handled and automatically cleaned up instead of causing errors. [Commit](https://github.com/open-webui/open-webui/commit/7e224e4a536b07ec008613f06592e34050e7067c) +- 🐛 **Task model selector validation.** The task model selector in admin settings now correctly accepts models based on the new access grants system instead of rejecting all models with an incorrect error. [Commit](https://github.com/open-webui/open-webui/commit/9a2595f0706d0c9d809ae7746001cf799f98db1d) +- 🔗 **Tool call message preservation.** Models no longer hallucinate tool outputs in multi-turn conversations because tool call history is now properly preserved instead of being merged into assistant messages. [#21098](https://github.com/open-webui/open-webui/discussions/21098), [#20600](https://github.com/open-webui/open-webui/issues/20600), [Commit](https://github.com/open-webui/open-webui/commit/f2aca781c87244cffc130aa2722e700c19a81d66) +- 🔧 **Tool server startup initialization.** External tool servers configured via the "TOOL_SERVER_CONNECTIONS" environment variable now initialize automatically on startup, eliminating the need to manually visit the Admin Panel and save for tools to become available. This enables proper GitOps and containerized deployments. [#18140](https://github.com/open-webui/open-webui/issues/18140), [#20914](https://github.com/open-webui/open-webui/pull/20914), [Commit](https://github.com/open-webui/open-webui/commit/f20cc6d7e6da493eb75ca1618f5cbd068fa57684) +- ♻️ **Resource handle cleanup.** File handles are now properly closed during audio transcription and pipeline uploads, preventing resource leaks that could cause system instability over time. [#21411](https://github.com/open-webui/open-webui/issues/21411) +- ⌨️ **Strikethrough shortcut conflict fix.** Pressing Ctrl+Shift+S to toggle the sidebar no longer causes text to become struck through in the chat input, by disabling the TipTap Strike extension's default keyboard shortcut when rich text mode is off. [Commit](https://github.com/open-webui/open-webui/commit/38ae91ae2) +- 🔧 **Tool call finish_reason fix.** API responses now correctly set finish_reason to "tool_calls" instead of "stop" when tool calls are present, fixing an issue where external API clients (such as OpenCode) would halt prematurely after tool execution when routing Ollama models through the Open WebUI API. [#20896](https://github.com/open-webui/open-webui/issues/20896) + +## [0.8.1] - 2026-02-14 + +### Added + +- 🚀 **Channel user active status.** Checking user active status in channels is now faster thanks to optimized database queries. [Commit](https://github.com/open-webui/open-webui/commit/ca6b18ab5cb94153a9dae233f975d36bf6b19b76) +- 🔗 **Responses API endpoint with model routing.** The OpenAI API proxy now supports a /responses endpoint that routes requests to the correct backend based on the model field in the request, instead of always using the first configured endpoint. This enables support for backends like vLLM that provide /skills and /v1/responses endpoints. [Commit](https://github.com/open-webui/open-webui/commit/abc9b63093d65f4d74342db85b7d5df1809aa0f0), [Commit](https://github.com/open-webui/open-webui/commit/79ecbfc757f0642740d0e44fab98263d84295490) +- ⚡ **Model and prompt list optimization.** Improved performance when loading models and prompts by pre-fetching user group IDs once instead of making multiple database queries. [Commit](https://github.com/open-webui/open-webui/commit/20de5a87da0c12e4052b50887a42ddd7228c5ef5) +- 🗄️ **Batch access control queries.** Improved performance when loading models, prompts, and knowledge bases by replacing multiple individual access checks with single batch queries, significantly reducing database load for large deployments. [Commit](https://github.com/open-webui/open-webui/commit/589c4e64c1b7bb7a7a5abc20382b92fb860e28c2) +- 💨 **Faster user list loading.** User lists now load significantly faster by deferring profile image loading; images are fetched separately in parallel by the browser, improving caching and reducing database load. [Commit](https://github.com/open-webui/open-webui/commit/b7549d2f6ca2843661ec79a5a1e55da9e7553368) +- 🔍 **Web search result count.** The built-in search_web tool now respects the admin-configured "Search Result Count" setting instead of always returning 5 results when using Native Function Calling mode. [#21373](https://github.com/open-webui/open-webui/pull/21373), [#21371](https://github.com/open-webui/open-webui/issues/21371) +- 🔐 **SCIM externalId support.** SCIM-enabled deployments can now store and manage externalId for user provisioning, enabling better integration with identity providers like Microsoft Entra ID and Okta. [#21099](https://github.com/open-webui/open-webui/pull/21099), [#21280](https://github.com/open-webui/open-webui/issues/21280), [Commit](https://github.com/open-webui/open-webui/commit/d1d1efe212b16e0052359991d67fd813125077e8) +- 🌐 **Translation updates.** Portuguese (Brazil) translations were updated. + +### Fixed + +- 🛡️ **Public sharing security fix.** Fixed a security issue where users with write access could see the Public sharing option regardless of their actual public sharing permission, and direct API calls could bypass frontend sharing restrictions. [#21358](https://github.com/open-webui/open-webui/pull/21358), [#21356](https://github.com/open-webui/open-webui/issues/21356) +- 🔒 **Direct model access control fix.** Model access control changes now persist correctly for direct Ollama and OpenAI models that don't have database entries, and error messages display properly instead of showing "[object Object]". [Commit](https://github.com/open-webui/open-webui/commit/f027a01ab2ff3b6175af3dd13a4478c265c0544a), [#21377](https://github.com/open-webui/open-webui/issues/21377) +- 💭 **Reasoning trace rendering performance.** Reasoning traces from models now render properly without being split into many fragments, preventing browser slowdowns during streaming responses. [#21348](https://github.com/open-webui/open-webui/issues/21348), [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a) +- 🖥️ **ARM device compatibility fix.** Fixed an issue where upgrading to 0.8.0 would fail to start on ARM devices (like Raspberry Pi 4) due to torch 2.10.0 causing SIGILL errors; now pinned to torch<=2.9.1. [#21385](https://github.com/open-webui/open-webui/pull/21385), [#21349](https://github.com/open-webui/open-webui/issues/21349) +- 🗄️ **Skills PostgreSQL compatibility fix.** Fixed a PostgreSQL compatibility issue where creating or listing skills would fail with a TypeError, while SQLite worked correctly. [#21372](https://github.com/open-webui/open-webui/pull/21372), [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa), [#21365](https://github.com/open-webui/open-webui/issues/21365) +- 🗄️ **PostgreSQL analytics query fix.** Fixed an issue where retrieving chat IDs by model ID would fail on PostgreSQL due to incompatible DISTINCT ordering, while SQLite worked correctly. [#21347](https://github.com/open-webui/open-webui/issues/21347), [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e) +- 🗃️ **SQLite cascade delete fix.** Deleting chats now properly removes all associated messages in SQLite, matching PostgreSQL behavior and preventing orphaned data. [#21362](https://github.com/open-webui/open-webui/pull/21362) +- ☁️ **Ollama Cloud model naming fix.** Fixed an issue where using Ollama Cloud models would fail with "Model not found" errors because ":latest" was incorrectly appended to model names. [#21386](https://github.com/open-webui/open-webui/issues/21386) +- 🛠️ **Knowledge selector tooltip z-index.** Fixed an issue where tooltips in the "Select Knowledge" dropdown were hidden behind the menu, making it difficult to read knowledge item names and descriptions. [#21375](https://github.com/open-webui/open-webui/pull/21375) +- 🎯 **Model selector scroll position.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91) +- 🐛 **Sync modal unexpected appearance.** Fixed an issue where the Sync Modal would appear unexpectedly after enabling the "Community Sharing" feature if the user had previously visited the app with the sync parameter. [#21376](https://github.com/open-webui/open-webui/pull/21376) +- 🎨 **Knowledge collection layout fix.** Fixed a layout issue in the Knowledge integration menu where long collection names caused indentation artifacts and now properly truncate with ellipsis. [#21374](https://github.com/open-webui/open-webui/pull/21374) +- 📝 **Metadata processing crash fix.** Fixed a latent bug where processing document metadata containing certain keys (content, pages, tables, paragraphs, sections, figures) would cause a RuntimeError due to dictionary mutation during iteration. [#21105](https://github.com/open-webui/open-webui/pull/21105) +- 🔑 **Password validation regex fix.** Fixed the password validation regex by adding the raw string prefix, ensuring escape sequences like \d and \w are interpreted correctly. [#21400](https://github.com/open-webui/open-webui/pull/21400), [#21399](https://github.com/open-webui/open-webui/issues/21399) + +### Changed + +- ⚠️ **Database Migrations:** This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility. + +## [0.8.0] - 2026-02-12 + +### Added + +- 📊 **Analytics dashboard.** Administrators now have access to an Analytics dashboard showing model usage statistics, token consumption by model and user, user activity rankings, and time-series charts with hourly or daily granularity; clicking any model opens a detail view with feedback history, associated tags, and chat browser, and results can be filtered by user group. [#21106](https://github.com/open-webui/open-webui/pull/21106), [Commit](https://github.com/open-webui/open-webui/commit/68a1e87b66a7ec8831d5ed52940c4ef110e3e264), [Commit](https://github.com/open-webui/open-webui/commit/e62649f94044abfed4d7d60647a2050383a67e3d) +- 🎯 **Experimental support for Skills.** Open WebUI now supports the Skill standard — allowing users to create and manage reusable AI skills with detailed instructions, reference them in chats using the "$" command, or attach them to specific models for automatic context in conversations. [#21312](https://github.com/open-webui/open-webui/pull/21312) +- 🧪 **Experimental support for Open Responses protocol.** Connections can now be configured to use the experimental Open Responses protocol instead of Chat Completions, enabling native support for extended thinking, streaming reasoning tokens, and richer tool call handling for compatible providers. [Commit](https://github.com/open-webui/open-webui/commit/d2c695eb11ddca9fc93499bb0c3fcafcff7099b5), [Commit](https://github.com/open-webui/open-webui/commit/90a057f4005c000bda6ff8703e13e529190af73a), [Commit](https://github.com/open-webui/open-webui/commit/0dc74a8a2e7adb76fb503ef0cd3c02daddd2f4bb), [Commit](https://github.com/open-webui/open-webui/commit/ea9c58ea80646cef05e06d0beaf5e81cc2f78cb1), [Commit](https://github.com/open-webui/open-webui/commit/6ffce4bccdc13b8b61a8b286e34094c981932eda), [Commit](https://github.com/open-webui/open-webui/commit/6719558150920f570d8febe021da65903e53c976), [Commit](https://github.com/open-webui/open-webui/commit/117c091b95a1b1a76a31c31b97304bac289d6f18), [Commit](https://github.com/open-webui/open-webui/commit/aa8c2959ca8476f269786e1317fb6d2938abd3f9), [Commit](https://github.com/open-webui/open-webui/commit/e2d09ac36174de48a7d85bafc8d3291c9ffe44cd) +- 👥 **Redesigned access control UI.** The access control UI was redesigned with a more intuitive interface that makes it easier to add multiple groups at once. [#21277](https://github.com/open-webui/open-webui/pull/21277) +- 👤 **Per-user resource sharing.** Resources including knowledge bases, prompts, models, tools, channels, and base models can now be shared directly to individual users alongside the existing per-group sharing capability. [#21277](https://github.com/open-webui/open-webui/pull/21277) +- 📨 **Message queuing.** Messages can now be queued while a response is generating rather than being blocked, allowing you to continue your train of thought; queued messages are automatically combined and sent when generation completes, and can be edited, deleted, or sent immediately from the input area. [Commit](https://github.com/open-webui/open-webui/commit/62750b8980ef0a3f2da7bc64b5416706a7495686), [Commit](https://github.com/open-webui/open-webui/commit/d3f2cf74748db42311ca04a56ccd1ea15399eca0) +- 💡 **Active task sidebar indicator.** Users can now see which chats have active tasks running directly in the sidebar. [Commit](https://github.com/open-webui/open-webui/commit/48522271586a5bf24b649610f03b4ffd8afb2782) +- 📝 **Prompt version control.** Prompts now include version control with full history tracking, allowing users to commit changes with messages, view past versions, compare differences between versions, and roll back to previous versions when needed. [#20945](https://github.com/open-webui/open-webui/pull/20945) +- 🏷️ **Prompt tags.** Prompts can now be organized with tags, and users can filter the prompt workspace by tag to quickly find related prompts across large collections. [#20945](https://github.com/open-webui/open-webui/pull/20945) +- 🐍 **Native function calling code execution.** Code execution now works with Native function calling mode, allowing models to autonomously run Python code for calculations, data analysis, and visualizations without requiring Default mode. [#20592](https://github.com/open-webui/open-webui/pull/20592), [Docs:#998](https://github.com/open-webui/docs/pull/998) +- 🚀 **Async web search.** Web search operations now run asynchronously in the background, allowing users to continue interacting with the application while searches complete. [#20630](https://github.com/open-webui/open-webui/pull/20630) +- ⚡ **Search debouncing.** Search operations across the application now respond more efficiently with debouncing that reduces unnecessary server requests while typing, improving responsiveness when searching users, groups, functions, tools, prompts, knowledge bases, notes, and when using the knowledge and prompts commands in chat. [#20982](https://github.com/open-webui/open-webui/pull/20982), [Commit](https://github.com/open-webui/open-webui/commit/36766f157d46102fd76c526b42579400ca70de50), [Commit](https://github.com/open-webui/open-webui/commit/fa859de460376782bd0fa35512c8426c9cd0462c), [Commit](https://github.com/open-webui/open-webui/commit/57ec2aa088ffd5a8c3553c53d39799497ff70479) +- 🤝 **Shared chats management.** Users can now view and manage all their shared chats from Settings, with options to copy share links or unshare conversations they no longer want public. [Commit](https://github.com/open-webui/open-webui/commit/a10ac774ab5d47b505e840b029c0c0340002508b) +- 📁 **User file management.** Users can now view, search, and delete all their uploaded files from Settings, providing centralized file management in one place. [Commit](https://github.com/open-webui/open-webui/commit/93ed4ae2cda2f4311143e51f586aaa73b83a37a7), [#21047](https://github.com/open-webui/open-webui/pull/21047) +- 🗑️ **Shift-click quick delete.** Files in the File Manager can now be quickly deleted by holding Shift and clicking the delete button, bypassing the confirmation dialog for faster bulk cleanup. [#21044](https://github.com/open-webui/open-webui/pull/21044) +- ⌨️ **Model selector shortcut.** The model selector can now be opened with Ctrl+Shift+M keyboard shortcut. [#21130](https://github.com/open-webui/open-webui/pull/21130) +- 🧠 **Smarter knowledge vs web search.** Models now choose more intelligently between knowledge base search and web search rather than always trying knowledge first. [#21115](https://github.com/open-webui/open-webui/pull/21115) +- 🌍 **Community model reviews.** Users can now access community reviews for models directly from the model selector menu and are prompted to leave reviews after rating responses, with administrators able to disable this via the "Community Sharing" setting. [Commit](https://github.com/open-webui/open-webui/commit/bc90463ea60c9a66accb1fd242cf1853910ca838) +- 📄 **Prompts workspace pagination.** The prompts workspace now includes pagination for large prompt collections, loading 30 prompts at a time with search, filtering, and sorting capabilities for improved performance and navigation. [Commit](https://github.com/open-webui/open-webui/commit/36766f157d46102fd76c526b42579400ca70de50) +- 🎨 **Action function HTML rendering.** Action functions can now render rich HTML content directly in chat as embedded iframes, matching the capabilities that tools already had and eliminating the need for action authors to inject codeblocks. [#21294](https://github.com/open-webui/open-webui/pull/21294), [Commit](https://github.com/open-webui/open-webui/commit/60ada21c152ed642971429fdbe88dcbf478cf83a) +- 🔒 **Password-masked valve fields.** Tool and function developers can now mark sensitive fields as passwords, which are automatically masked in the settings UI to prevent shoulder surfing and accidental exposure. [#20852](https://github.com/open-webui/open-webui/issues/20852), [Commit](https://github.com/open-webui/open-webui/commit/8c70453b2e3a6958437d951751e84acbbaafd9aa) +- 📋 **Prompt quick copy.** Prompts in the workspace now include a quick copy button for easily copying prompt content to the clipboard. [Commit](https://github.com/open-webui/open-webui/commit/78f856e2049991441a3469230ae52799cb86954e) +- 🔔 **Dismissible notification toasts.** Notification toasts for new messages and other events now include a close button that appears on hover, allowing users to dismiss them immediately instead of waiting for auto-dismissal. [#21056](https://github.com/open-webui/open-webui/issues/21056), [Commit](https://github.com/open-webui/open-webui/commit/73bb600034c8532e30726129743a5ffe9002c5fb) +- 🔔 **Temporary chat notification privacy.** Notifications from temporary chats now only appear on the device where the chat is running, preventing privacy leaks across logged-in sessions. [#21292](https://github.com/open-webui/open-webui/pull/21292) +- 💡 **Null chat title fallback.** Notifications without chat titles now display "New Chat" instead of showing null. [#21292](https://github.com/open-webui/open-webui/pull/21292) +- 🖼️ **Concurrent image editing.** Image editing operations with multiple images now complete faster by loading all images concurrently instead of sequentially. [#20911](https://github.com/open-webui/open-webui/pull/20911) +- 📧 **USER_EMAIL template variable.** Users can now reference their email address in prompts and system messages using the "{{USER_EMAIL}}" template variable. [#20881](https://github.com/open-webui/open-webui/pull/20881) +- 🔤 **Alphabetical tool ordering.** Tools and Functions in the Chat Controls sidebar now appear in alphabetical order, making it easier to locate specific tools when working with multiple integrations. [#20871](https://github.com/open-webui/open-webui/pull/20871) +- 👁️ **Model list status filtering.** Administrators can now filter the model list by status (enabled, disabled, visible, hidden) and bulk enable or disable all filtered models at once. [#20553](https://github.com/open-webui/open-webui/issues/20553), [#20774](https://github.com/open-webui/open-webui/issues/20774), [Commit](https://github.com/open-webui/open-webui/commit/96a9696383d450dad2cbb230f3756ebfa258e029) +- ⚙️ **Per-model built-in tool toggles.** Administrators can now enable or disable individual built-in tools for each model, including time utilities, memory, chat history, notes, knowledge base, and channels. [#20641](https://github.com/open-webui/open-webui/issues/20641), [Commit](https://github.com/open-webui/open-webui/commit/c46ef3b63bcc1e2e9adbdd18fab82c4bbe33ff6c) +- 📑 **PDF loading modes.** Administrators can now choose between "page" and "single" PDF loading modes, allowing documents to be processed as individual pages or as complete documents for better chunking across page boundaries. [Commit](https://github.com/open-webui/open-webui/commit/ecbdef732bc71a07c21bbb679edb420f26eac181) +- 📑 **Model Settings pagination.** Administrators can now navigate large model lists more efficiently in Model Settings, with pagination displaying 30 models per page for smoother navigation. [Commit](https://github.com/open-webui/open-webui/commit/2f584c9f88aeb34ece07b10d05794020d1d656b8) +- 📌 **Pin read-only models.** Users can now pin read-only models from the workspace. [#21308](https://github.com/open-webui/open-webui/issues/21308), [Commit](https://github.com/open-webui/open-webui/commit/97331bf11d41ca54e47f86777fb8dbd73988c631) +- 🔍 **Yandex search provider.** Administrators can now configure Yandex as a web search provider, expanding search engine options for retrieval-augmented generation. [#20922](https://github.com/open-webui/open-webui/pull/20922) +- 🔐 **Custom password hints.** Administrators can now provide custom password requirement hints to users via the "PASSWORD_VALIDATION_HINT" environment variable, making it clearer what password criteria must be met during signup or password changes. [#20647](https://github.com/open-webui/open-webui/issues/20647), [#20650](https://github.com/open-webui/open-webui/pull/20650) +- 🔑 **OAuth token exchange.** Administrators can now enable OAuth token exchange via "ENABLE_OAUTH_TOKEN_EXCHANGE", allowing external applications to authenticate users by exchanging OAuth provider tokens for Open WebUI session tokens. [Commit](https://github.com/open-webui/open-webui/commit/655420fd25ed0ea872954baa485030079c00c10e) +- 🗄️ **Weaviate custom endpoints.** Administrators can now connect to self-hosted Weaviate deployments with separate HTTP and gRPC endpoints via new environment variables. [#20620](https://github.com/open-webui/open-webui/pull/20620) +- 🛡️ **MCP custom SSL certificates.** Administrators can now connect to MCP servers with self-signed or custom SSL certificates via the "AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL" environment variable. [#20875](https://github.com/open-webui/open-webui/issues/20875), [Commit](https://github.com/open-webui/open-webui/commit/c7f996d593e4bb48103b91316204fe7e50e25b35) +- 🗃️ **Redis Sentinel reconnection delay.** Administrators using Redis Sentinel can now configure a reconnection delay via "REDIS_RECONNECT_DELAY" to prevent retry exhaustion during failover elections. [#21021](https://github.com/open-webui/open-webui/pull/21021) +- 📡 **Custom user info headers.** Administrators can now customize the header names used when forwarding user information to external services, enabling compatibility with services like AWS Bedrock AgentCore that require specific header prefixes. [Commit](https://github.com/open-webui/open-webui/commit/6c0f886cdf4b4249dca29e9340b3b998a7262d61) +- 🔗 **Forward user info to tool servers.** User identity and chat context can now be forwarded to MCP servers and external tool servers when "ENABLE_FORWARD_USER_INFO_HEADERS" is enabled, allowing tool providers to implement per-user authorization, auditing, and rate limiting. [#21092](https://github.com/open-webui/open-webui/pull/21092), [Commit](https://github.com/open-webui/open-webui/commit/2c37daef86a058e370151ecead17f10078102307) +- 📬 **External tool event emitters.** External tools (OpenAPI/MCP) can now send tool events back to Open WebUI using the event emitter endpoint, as message ID is now forwarded alongside chat ID when "ENABLE_FORWARD_USER_INFO_HEADERS" is enabled. [#21214](https://github.com/open-webui/open-webui/pull/21214) +- 📥 **Playground chat export.** Administrators can now export playground chats as JSON or plain text files, allowing them to save their conversations for backup or sharing outside the platform. [Commit](https://github.com/open-webui/open-webui/commit/8e2b0b6fd2ac99c833a110e2bc6aa655f1682669) +- 🖼️ **Images playground.** Administrators can now test image generation and editing directly in a new Images playground, with support for uploading source images for edits and downloading results. [Commit](https://github.com/open-webui/open-webui/commit/94302de49b27bdf1df86b5c26f2cafb98f964e52) +- 🛠️ **Dynamic dropdown valve fields.** Tool and function developers can now create dropdown fields with dynamically-generated options that update based on runtime context, such as available models or user permissions. [Commit](https://github.com/open-webui/open-webui/commit/474427c67e953bb9f7d122757a756a639214e0b2) +- 🏎️ **Faster profile updates.** User profile updates and role changes are now faster by eliminating redundant database queries. [#21011](https://github.com/open-webui/open-webui/pull/21011) +- 🔑 **Faster authentication.** User authentication is now 34% faster by combining database lookups into a single query. [#21010](https://github.com/open-webui/open-webui/pull/21010) +- 🔋 **Faster chat completions.** Chat completions and embeddings now respond much faster by checking the model cache before fetching model lists, reducing Time To First Token from several seconds to subsecond for most requests. [#20886](https://github.com/open-webui/open-webui/pull/20886), [#20069](https://github.com/open-webui/open-webui/discussions/20069) +- 🏎️ **Faster Redis model list loading.** Model list loading is now significantly faster when using Redis with many models, reducing API response latency by caching configuration values locally instead of making repeated Redis lookups on every model iteration. [#21306](https://github.com/open-webui/open-webui/pull/21306) +- 💨 **Faster knowledge base file batch-add.** Batch-adding files to knowledge bases is now faster with a single database query instead of one query per file. [#21006](https://github.com/open-webui/open-webui/pull/21006) +- ⚡ **Smoother model selector dropdown.** The model selector dropdown now renders smoothly even with hundreds of models, eliminating the lag and freezing that occurred when opening the dropdown with large model lists. [Commit](https://github.com/open-webui/open-webui/commit/4331029926245b7b74fa8e254610c91400b239b0) +- 🚗 **Faster model visibility toggling.** Toggling model visibility in the admin panel is now faster with optimized database access. [#21009](https://github.com/open-webui/open-webui/pull/21009) +- 💾 **Faster model access control checks.** Model access control checks are now faster by batch-fetching model info and group memberships upfront instead of querying for each model. [#21008](https://github.com/open-webui/open-webui/pull/21008) +- ⚙️ **Faster model list and imports.** Model list loading and model imports are now faster by eliminating redundant database queries. [#21004](https://github.com/open-webui/open-webui/pull/21004) +- 🏃 **Faster SCIM group member lookups.** SCIM group member lookups are now up to 13x faster by batching user queries instead of fetching each member individually. [#21005](https://github.com/open-webui/open-webui/pull/21005) +- 💨 **Batched group member counts.** Group member counts are now fetched in a single batch query when loading group lists, eliminating redundant database lookups. [Commit](https://github.com/open-webui/open-webui/commit/96c07f44a8f5e6346b2ea6ac529ff4ec3c47e90a) +- 💨 **Faster bulk operations.** Bulk feedback deletion and group member removal are now 4-5x faster with optimized batch operations. [#21019](https://github.com/open-webui/open-webui/pull/21019) +- 🧠 **Faster memory updates.** Memory updates are now up to 39% faster by eliminating redundant database queries. [#21013](https://github.com/open-webui/open-webui/pull/21013) +- ⚙️ **Faster filter function loading.** Filter function loading is now faster by batching database queries instead of fetching each function individually. [#21018](https://github.com/open-webui/open-webui/pull/21018) +- 🖼️ **Image model regex configuration.** Administrators can now configure which image generation models support auto-sizing and URL responses via new regex environment variables, improving compatibility with LiteLLM and other proxies that use prefixed model names. [#21126](https://github.com/open-webui/open-webui/pull/21126), [Commit](https://github.com/open-webui/open-webui/commit/ecf3fa2feb28e74ff6c17ca97d94581f316da56a) +- 🎁 **Easter eggs toggle.** Administrators can now control the visibility of easter egg features via the "ENABLE_EASTER_EGGS" environment variable. [Commit](https://github.com/open-webui/open-webui/commit/907dba4517903e5646e40223a0edca26a7107bc8) +- 🔌 **Independent access control updates.** API endpoints now support independent access control updates for models, tools, knowledge bases, and notes, enabling finer-grained permission management. [Commit](https://github.com/open-webui/open-webui/commit/0044902c082f8475336cc7d5c57fe3f35ab0555d), [Commit](https://github.com/open-webui/open-webui/commit/c259c878060af1b03b702c943e8813d7b4fc3199), [Commit](https://github.com/open-webui/open-webui/commit/e3a825769063cee486650cc2eb9a032676e630c5) +- ♿ **Screen reader accessibility.** Screen reader users now hear the password field label only once on the login page, improving form navigation for assistive technology users. [Commit](https://github.com/open-webui/open-webui/commit/1441d0d735c7a1470070b33327e1dd4dc5ca1131) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Catalan, Finnish, Irish, French, German, Japanese, Latvian, Polish, Portuguese (Brazil), Simplified Chinese, Slovak, Spanish, and Traditional Chinese were enhanced and expanded. + +### Fixed + +- ⚡ **Connection pool exhaustion fix.** Database connection pool exhaustion and timeout errors during concurrent usage have been resolved by releasing connections before chat completion requests and embedding operations for memory and knowledge base processing. [#20569](https://github.com/open-webui/open-webui/pull/20569), [#20570](https://github.com/open-webui/open-webui/pull/20570), [#20571](https://github.com/open-webui/open-webui/pull/20571), [#20572](https://github.com/open-webui/open-webui/pull/20572), [#20573](https://github.com/open-webui/open-webui/pull/20573), [#20574](https://github.com/open-webui/open-webui/pull/20574), [#20575](https://github.com/open-webui/open-webui/pull/20575), [#20576](https://github.com/open-webui/open-webui/pull/20576), [#20577](https://github.com/open-webui/open-webui/pull/20577), [#20578](https://github.com/open-webui/open-webui/pull/20578), [#20579](https://github.com/open-webui/open-webui/pull/20579), [#20580](https://github.com/open-webui/open-webui/pull/20580), [#20581](https://github.com/open-webui/open-webui/pull/20581), [Commit](https://github.com/open-webui/open-webui/commit/7da37b4f66b9b2e821796b06b75e03cb0237e0a9), [Commit](https://github.com/open-webui/open-webui/commit/9af40624c5f0f8f7f640a11356e167543b07b2bb) +- 🚫 **LDAP authentication hang fix.** LDAP authentication no longer freezes the entire service when logging in with non-existent accounts, preventing application hangs. [Commit](https://github.com/open-webui/open-webui/commit/a4281f6a7fbc9764b57830e4ef81bb780aa34af9), [#21300](https://github.com/open-webui/open-webui/issues/21300) +- ✅ **Trusted Header auto-registration fix.** Trusted Header Authentication now properly auto-registers new users after the first login, assigning the configured default role instead of failing for users not yet in the database. [Commit](https://github.com/open-webui/open-webui/commit/9b30e8f6894c8c8bad0a9ce4693eab810962adc9) +- 🛡️ **SSRF protection for image loading.** External image loading now validates URLs before fetching to prevent SSRF attacks against local and private network addresses. [Commit](https://github.com/open-webui/open-webui/commit/ce50d9bac4f30b054b09a2fbda52569b73ea591c) +- 🛡️ **Malformed Authorization header fix.** Malformed Authorization headers no longer cause server crashes; requests are now handled gracefully instead of returning HTTP 500 errors. [#20938](https://github.com/open-webui/open-webui/issues/20938), [Commit](https://github.com/open-webui/open-webui/commit/7e79f8d1c6b5a02f1a46e792540c6bbf7bed8edc) +- 🚪 **Channel notification access control.** Users without channel permissions can no longer access channels through notifications, properly enforcing access controls across all channel entry points. [#20883](https://github.com/open-webui/open-webui/pull/20883), [#20789](https://github.com/open-webui/open-webui/discussions/20789) +- 🐛 **Ollama model name suffix fix.** Ollama-compatible providers that do not use ":latest" in model names can now successfully chat, fixing errors where model names were incorrectly appended with ":latest" suffixes. [#21331](https://github.com/open-webui/open-webui/issues/21331), [Commit](https://github.com/open-webui/open-webui/commit/05ae44b98dc279ee12cc8eab17278ccbfec60301) +- ♻️ **Streaming connection cleanup.** Streaming responses now properly clean up network connections when interrupted, preventing "Unclosed client session" errors from accumulating over time. [#20889](https://github.com/open-webui/open-webui/pull/20889), [#17058](https://github.com/open-webui/open-webui/issues/17058) +- 💾 **Inline image context exhaustion fix.** Inline images no longer exhaust the model's context window by including their full base64 data in chat metadata, preventing premature context exhaustion with image-heavy conversations. [#20916](https://github.com/open-webui/open-webui/pull/20916) +- 🚀 **Status indicator GPU usage fix.** High GPU usage caused by the user online status indicator animation has been resolved, reducing consumption from 35-40% to near-zero in browsers with hardware acceleration. [#21062](https://github.com/open-webui/open-webui/issues/21062), [Commit](https://github.com/open-webui/open-webui/commit/938d1b0743c64f0ce513d68e57dfbb86987cb06b) +- 🔧 **Async pipeline operations.** Pipeline operations now run asynchronously instead of blocking the FastAPI event loop, allowing the server to handle other requests while waiting for external pipeline API calls. [#20910](https://github.com/open-webui/open-webui/pull/20910) +- 🔌 **MCP tools regression fix.** MCP tools now work reliably again after a regression in v0.7.2 that caused "cannot pickle '\_asyncio.Future' object" errors when attempting to use MCP servers in chat. [#20629](https://github.com/open-webui/open-webui/issues/20629), [#20500](https://github.com/open-webui/open-webui/issues/20500), [Commit](https://github.com/open-webui/open-webui/commit/886c12c5664bc2dd73313330f61c2257169da6d1) +- 🔗 **Function chat ID propagation fix.** Functions now reliably receive the chat identifier during internal task invocations like web search query generation, RAG query generation, and image prompt generation, enabling stateful functions to maintain consistent per-chat state without fragmentation. [#20563](https://github.com/open-webui/open-webui/issues/20563), [#20585](https://github.com/open-webui/open-webui/pull/20585) +- 💻 **Markdown fence code execution fix.** Code execution now works reliably when models wrap code in markdown fences, automatically stripping the backticks before execution to prevent syntax errors that affected most non-GPT models. [#20941](https://github.com/open-webui/open-webui/issues/20941), [Commit](https://github.com/open-webui/open-webui/commit/4a5516775927aaf002212f2e09c55a17c699bc46), [Commit](https://github.com/open-webui/open-webui/commit/683438b418fb3b453a8ad88c1ba1a9944eac3593) +- 💻 **ANSI code execution fix.** Code execution is now reliable when LLMs include ANSI terminal color codes in their output, preventing random failures that previously caused syntax errors. [#21091](https://github.com/open-webui/open-webui/issues/21091), [Commit](https://github.com/open-webui/open-webui/commit/b1737040a7d3bb5efcfe0f1432e89d7e82e51d2d) +- 🗨️ **Incomplete model metadata crash fix.** Starting chats with models that have incomplete metadata information no longer crashes the application. [#20565](https://github.com/open-webui/open-webui/issues/20565), [Commit](https://github.com/open-webui/open-webui/commit/14f6747dfc66fb7e942b930650286012121e5262) +- 💬 **Unavailable model crash fix.** Adding message pairs with Ctrl+Shift+Enter no longer crashes when the chat's model is unavailable, showing a helpful error message instead. [#20663](https://github.com/open-webui/open-webui/pull/20663) +- 📚 **Knowledge base file upload fix.** Uploading files to knowledge bases now works correctly, fixing database mapping errors that prevented file uploads. [#20925](https://github.com/open-webui/open-webui/issues/20925), [#20931](https://github.com/open-webui/open-webui/pull/20931) +- 🧠 **Knowledge base query type fix.** Knowledge base queries no longer fail intermittently when models send tool call parameters as strings instead of their expected types. [#20705](https://github.com/open-webui/open-webui/pull/20705) +- 📚 **Knowledge base reindex fix.** Reindexing knowledge base files now works correctly instead of failing with duplicate content errors. [#20854](https://github.com/open-webui/open-webui/issues/20854), [#20857](https://github.com/open-webui/open-webui/pull/20857) +- 🔧 **Multi-worker knowledge base timeout fix.** In multi-worker deployments, uploading very large documents to knowledge bases no longer causes workers to be killed by health check timeouts, and administrators can now configure a custom embedding timeout via "RAG_EMBEDDING_TIMEOUT". [#21158](https://github.com/open-webui/open-webui/pull/21158), [Discussion](https://github.com/open-webui/open-webui/discussions/21151), [Commit](https://github.com/open-webui/open-webui/commit/c653e4ec54d070aee5e9568d016daebb61f06632) +- 🌅 **Dark mode icon inversion fix.** Icons in chat and action menus are now displayed correctly in dark mode, fixing an issue where PNG icons with "svg" in their base64 encoding were randomly inverted. [#21272](https://github.com/open-webui/open-webui/pull/21272), [Commit](https://github.com/open-webui/open-webui/commit/0a44d80252afae73de4098ab1c3eb6cf54157fd6) +- 🛠️ **Admin model write permission fix.** Fixed the admin panel allowing models to be assigned write permissions, since users with write permission are not admins and cannot write. [Commit](https://github.com/open-webui/open-webui/commit/4aedfdc5471a1f13c1084b34b48ea3ed6311cd42) +- 🛠️ **Prompt access control save fix.** Prompt access control settings are now saved correctly when modifying resource permissions. [Commit](https://github.com/open-webui/open-webui/commit/30f72672fac2579c267a076e6ba89dfe1812137b) +- ✏️ **Knowledge base file edit fix.** Editing files within knowledge bases now saves correctly and can be used for retrieval, fixing a silent failure where the save appeared successful but the file could not be searched. [Commit](https://github.com/open-webui/open-webui/commit/f9ab66f51a52388a4eb084c8f69044e79bf5cb04) +- 🖼️ **Reasoning section artifact rendering fix.** Code blocks within model reasoning sections no longer incorrectly render as interactive artifacts, ensuring only intended output displays as previews. [#20801](https://github.com/open-webui/open-webui/issues/20801), [#20877](https://github.com/open-webui/open-webui/pull/20877), [Commit](https://github.com/open-webui/open-webui/commit/4c6f100b5fe2145a3d676b70b5f7c0e7f07cee20) +- 🔐 **Group resource sharing fix.** Sharing resources with groups now works correctly, fixing database errors and an issue where models shared with read-only access were not visible to group members. [#20666](https://github.com/open-webui/open-webui/issues/20666), [#21043](https://github.com/open-webui/open-webui/issues/21043), [Commit](https://github.com/open-webui/open-webui/commit/5a075a2c836e46b83f8710285f09aff1f6125072) +- 🔑 **Docling API key fix.** Docling API key authentication now works correctly by using the proper "X-Api-Key" header format instead of the incorrect "Bearer" authorization prefix. [#20652](https://github.com/open-webui/open-webui/pull/20652) +- 🔌 **MCP OAuth 2.1 fix.** MCP OAuth 2.1 authentication now works correctly, resolving connection verification failures and 401 errors during the authorization callback. [#20808](https://github.com/open-webui/open-webui/issues/20808), [#20828](https://github.com/open-webui/open-webui/issues/20828), [Commit](https://github.com/open-webui/open-webui/commit/8eebc2aea63b7045e61c9689a65a2dfa9c797bcb) +- 💻 **MATLAB syntax highlighting.** MATLAB code blocks now display with proper syntax highlighting in chat messages. [#20719](https://github.com/open-webui/open-webui/issues/20719), [#20773](https://github.com/open-webui/open-webui/pull/20773) +- 📊 **CSV export HTML entity decoding.** Exporting tables to CSV now properly decodes HTML entities, ensuring special characters display correctly in the exported file. [#20688](https://github.com/open-webui/open-webui/pull/20688) +- 📄 **Markdown Header Text Splitter persistence.** The "Markdown Header Text Splitter" document setting now persists correctly when disabled, preventing it from reverting to enabled after page refresh. [#20929](https://github.com/open-webui/open-webui/issues/20929), [#20930](https://github.com/open-webui/open-webui/pull/20930) +- 🔌 **Audio service timeout handling.** Audio transcription and text-to-speech requests now have proper timeouts, preventing the UI from freezing when external services don't respond. [#21055](https://github.com/open-webui/open-webui/pull/21055) +- 💬 **Reference Chats visibility fix.** The "Reference Chats" option now appears in the message input menu even when the sidebar is collapsed, fixing the issue where it was hidden on mobile devices and at first load. [#20827](https://github.com/open-webui/open-webui/issues/20827), [Commit](https://github.com/open-webui/open-webui/commit/a3600e8b219fc4c019b95258d16bd3e2827490c6) +- 🔍 **Chat search self-exclusion.** The "search_chats" builtin tool now excludes the current conversation from search results, preventing redundant matches. [#20718](https://github.com/open-webui/open-webui/issues/20718), [Commit](https://github.com/open-webui/open-webui/commit/1a4bdd2b30017d901b9cac1e2e10684ec1edd062) +- 📚 **Knowledge base pagination fix.** Paginating through knowledge base files no longer shows duplicates or skips files when multiple documents share the same update timestamp. [#20846](https://github.com/open-webui/open-webui/issues/20846), [Commit](https://github.com/open-webui/open-webui/commit/a9a0ce6beaa286cc18eff24b518a6f3d7a560e2f) +- 📋 **Batch file error reporting.** Batch file processing operations now return properly structured error information when failures occur, making it clearer what went wrong during multi-file operations. [#20795](https://github.com/open-webui/open-webui/issues/20795), [Commit](https://github.com/open-webui/open-webui/commit/68b2872ed645cffb641fa5a21a784d6e9ea0d72b) +- ⚙️ **Persistent config with Redis fix.** Configuration values now respect the "ENABLE_PERSISTENT_CONFIG" setting when Redis is used, ensuring environment variables are reloaded on restart when persistent config is disabled. [#20830](https://github.com/open-webui/open-webui/issues/20830), [Commit](https://github.com/open-webui/open-webui/commit/5d48e48e15b003874cc821d896998a01e87580a0) +- 🔧 **Engine.IO logging fix.** The "WEBSOCKET_SERVER_ENGINEIO_LOGGING" environment variable now works correctly, allowing administrators to configure Engine.IO logging independently from general websocket logging. [#20727](https://github.com/open-webui/open-webui/pull/20727), [Commit](https://github.com/open-webui/open-webui/commit/5cfb7a08cbde5d39aaf4097b849a80da87c30d66) +- 🌐 **French language default fix.** Browsers requesting French language now default to French (France) instead of French (Canada), matching standard language preference expectations. [#20603](https://github.com/open-webui/open-webui/pull/20603), [Commit](https://github.com/open-webui/open-webui/commit/4d9a7cc6c0adea54b58046c576250a0c3ae7b512) +- 🔘 **Firefox delete button fix.** Pressing Enter after clicking delete buttons no longer incorrectly retriggers confirmation modals in Firefox. [Commit](https://github.com/open-webui/open-webui/commit/57a2024c58b9c674f2ae08eeb552994ef1796888) +- 🌍 **RTL table rendering fix.** Chat markdown tables now correctly display right-to-left when containing RTL language content (Arabic, Hebrew, Farsi, etc.), matching the "Auto" direction setting behavior. [#21160](https://github.com/open-webui/open-webui/issues/21160), [Commit](https://github.com/open-webui/open-webui/commit/284b97bd84c824013ad00ea07621192ec69a5e93) +- 🔒 **Write permission enforcement for tools.** Users without write permissions are now properly prevented from editing tools, with a clear error message displayed when attempting unauthorized edits. [Commit](https://github.com/open-webui/open-webui/commit/85e92fe3b062ae669985c09495f6ff1baf8176ab), [Commit](https://github.com/open-webui/open-webui/commit/91faa9fd5a1cfc5d3ab531d2d91d28db52bcc702) +- 🛡️ **Chat Valves permission enforcement.** The "Allow Chat Valves" permission is now properly enforced in the integrations menu, preventing users from bypassing access restrictions. [#20691](https://github.com/open-webui/open-webui/pull/20691) +- 📝 **Audit log browser session fix.** Audit logs now properly capture all user activity including browser-based sessions, not just API key requests. [#20651](https://github.com/open-webui/open-webui/issues/20651), [Commit](https://github.com/open-webui/open-webui/commit/86e6b2b68b85e958188881785495030de1a30402), [Commit](https://github.com/open-webui/open-webui/commit/ee5fd1246cb3f8f16ca5cbb24feeea43b7800dcb) +- 🎨 **Long model name truncation.** Long model names and IDs in the admin panel now truncate properly to prevent visual overflow, with full names visible on hover. [#20696](https://github.com/open-webui/open-webui/pull/20696) +- 👥 **Admin user filter pagination fix.** Filtering users in the admin panel now automatically resets to page 1, preventing empty results when searching from pages beyond the first. [#20723](https://github.com/open-webui/open-webui/pull/20723), [Commit](https://github.com/open-webui/open-webui/commit/be75bc506adb048ef11b1612c0e3662511c920d0) +- 🔎 **Username search on workspace pages.** Searching for users by username now works correctly on Models, Knowledge, and Functions workspace pages, making it easier to find resources owned by specific users. [#20780](https://github.com/open-webui/open-webui/pull/20780) +- 🗑️ **File deletion orphaned embeddings fix.** Deleting files now properly removes associated knowledge base embeddings, preventing orphaned data from accumulating. [Commit](https://github.com/open-webui/open-webui/commit/93ed4ae2cda2f4311143e51f586aaa73b83a37a7) +- 🧹 **Event listener memory leak fix.** Memory leaks caused by event listeners not being cleaned up during navigation have been resolved. [#20913](https://github.com/open-webui/open-webui/pull/20913) +- 🐳 **Docker Ollama update fix.** Ollama can now be updated within Docker containers after adding a missing zstd dependency. [#20994](https://github.com/open-webui/open-webui/issues/20994), [#21052](https://github.com/open-webui/open-webui/pull/21052) +- 📝 **Workspace duplicate API request fix.** The prompts, knowledge, and models workspaces no longer make duplicate API requests when loading. [Commit](https://github.com/open-webui/open-webui/commit/ab5dfbda54664c9278b0d807ba06cad94edd798f), [Commit](https://github.com/open-webui/open-webui/commit/e5dbfc420dd3e7f6ba047a3e11584449ff0742b4) +- 📡 **OpenTelemetry Redis cluster fix.** OpenTelemetry instrumentation now works correctly with Redis cluster mode deployments. [#21129](https://github.com/open-webui/open-webui/pull/21129) +- 🐳 **Airgapped NLTK tokenizer fix.** Document extraction now works reliably in airgapped environments after container restarts by bundling NLTK tokenizer data in the Docker image. [#21165](https://github.com/open-webui/open-webui/pull/21165), [#21150](https://github.com/open-webui/open-webui/issues/21150) +- 💬 **Channel model mention crash fix.** Mentioning a model in channels no longer crashes when older thread messages have missing data. [#21112](https://github.com/open-webui/open-webui/pull/21112) +- 🔧 **OpenAPI tool import fix.** Importing OpenAPI tool specifications no longer crashes when parameters lack explicit name fields, fixing compatibility with complex request body definitions. [#21121](https://github.com/open-webui/open-webui/pull/21121), [Commit](https://github.com/open-webui/open-webui/commit/8e79b3d0bc4903f30e747b663ac818976618c83c) +- 🌐 **Webpage attachment content fix.** Attaching webpages to chats now retrieves full content instead of only metadata, fixing an unawaited coroutine in SSL certificate verification. [#21166](https://github.com/open-webui/open-webui/issues/21166), [Commit](https://github.com/open-webui/open-webui/commit/a214ec40ea00eebcba49570647ca6ab8f61765d5) +- 💾 **File upload settings persistence.** File upload settings (Max Upload Size, Max File Count, Image Compression dimensions) now persist correctly and are no longer erased when updating other RAG configuration settings. [#21057](https://github.com/open-webui/open-webui/issues/21057), [Commit](https://github.com/open-webui/open-webui/commit/258454276e1ef8ded24968515f7bf5e1833ca011) +- 📦 **Tool call expand/collapse fix.** Tool call results in chat can now be expanded and collapsed again after a recent refactor disabled this behavior. [#21205](https://github.com/open-webui/open-webui/pull/21205) +- 🪛 **Disabled API endpoint bypass fix.** Fixed Ollama/OpenAI API endpoints bypassing 'ENABLE_OLLAMA_API' and 'ENABLE_OPENAI_API' flags when the 'url_idx' parameter was provided. Endpoints now properly return a 503 error with a clear "API is disabled" message instead of attempting to connect and logging confusing connection errors. +- 🛠️ **OpenSearch 3.0 compatibility fix.** Document uploads to knowledge bases now work correctly when using OpenSearch backend with opensearch-py >= 3.0.0, fixing a TypeError that previously caused failures. [#21248](https://github.com/open-webui/open-webui/pull/21248), [#20649](https://github.com/open-webui/open-webui/issues/20649) +- 📱 **Gboard multi-line paste fix.** Multi-line text pasted from Gboard on Android now inserts correctly instead of being replaced with a single newline, fixing a bug where the keyboard's clipboard suggestion strip sent text via 'insertText' events instead of standard paste events. [#21265](https://github.com/open-webui/open-webui/pull/21265) +- 🔧 **Batch embeddings endpoint fix.** The '/api/embeddings' endpoint now correctly returns separate embeddings for each input string when processing batch requests to Ollama providers. [Commit](https://github.com/open-webui/open-webui/commit/8fd5c06e5bf7e0ccbda15d83338912ea17f66783), [#21279](https://github.com/open-webui/open-webui/issues/21279) +- 🗝️ **SSL verification for embeddings.** SSL certificate verification now respects the "AIOHTTP_CLIENT_SESSION_SSL" setting for OpenAI and Azure OpenAI embedding requests, allowing connections to self-signed certificate endpoints when disabled. [Commit](https://github.com/open-webui/open-webui/commit/cd31b8301b38bfa86872608cfbd022ff74e3ae52) +- 🔧 **Tool call HTML entity fix.** Models now receive properly formatted tool call results in multi-turn conversations, fixing an issue where HTML entities caused malformed content that was hard to parse. [#20755](https://github.com/open-webui/open-webui/pull/20755) +- 💾 **Duplicate inline image context fix.** Inline images no longer exhaust the model's context window by including their full base64 data in chat metadata, preventing premature context exhaustion with image-heavy conversations. [#20916](https://github.com/open-webui/open-webui/pull/20916) +- 🐛 **OpenAI model cache lookup fix.** The OpenAI API router model lookup was corrected to use the proper model identifier when checking the cache, ensuring consistent and correct model retrieval during chat completions. [#21327](https://github.com/open-webui/open-webui/pull/21327) +- 🐛 **Ollama latest suffix fix.** Ollama-compatible providers that don't use ":latest" in model names can now successfully chat, fixing errors where model names were incorrectly appended with ":latest" suffixes. [#21331](https://github.com/open-webui/open-webui/issues/21331), [Commit](https://github.com/open-webui/open-webui/commit/05ae44b98dc279ee12cc8eab17278ccbfec60301) +- ⛔ **OpenAI endpoint detection fix.** OpenAI API endpoint detection was corrected to use exact hostname matching instead of substring matching, preventing third-party providers with similar URL patterns from being incorrectly filtered. [Commit](https://github.com/open-webui/open-webui/commit/423d8b18170a0b92b582aba6ef7bb9ba173e876e) +- 🛠️ **RedisCluster task stopping fix.** Task stopping now works correctly in RedisCluster deployments, fixing an issue where tasks would remain active after cancellation attempts. [#20803](https://github.com/open-webui/open-webui/pull/20803), [Commit](https://github.com/open-webui/open-webui/commit/0dcbd05e2436929ae9d2c559a204844ae0239b57) +- 📎 **Citation parsing error fix.** Citation parsing no longer crashes when builtin tools return error responses, fixing AttributeError issues when tools like search_web fail. [#21071](https://github.com/open-webui/open-webui/pull/21071) + +### Changed + +- ‼️ **Database Migration Required** — This release includes database schema changes; multi-worker, multi-server, or load-balanced deployments must update all instances simultaneously rather than performing rolling updates, as running mixed versions will cause application failures due to schema incompatibility between old and new instances. +- ⚠️ **Chat Message Table Migration** — This release includes a new chat message table migration that can take a significant amount of time to complete in larger deployments with extensive chat histories. Administrators should plan for adequate maintenance windows and allow the migration to complete fully without interruption. Running the migration with insufficient time or resources may result in data integrity issues. +- 🔗 **Prompt ID-based URLs.** Prompts now use unique ID-based URLs instead of command-based URLs, allowing more flexible command renaming without breaking saved links or integrations. [#20945](https://github.com/open-webui/open-webui/pull/20945) + ## [0.7.2] - 2026-01-10 ### Fixed @@ -3432,7 +4632,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality. - - Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag. - When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`. diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT b/CONTRIBUTOR_LICENSE_AGREEMENT index ca9f48b02e..594e1506ac 100644 --- a/CONTRIBUTOR_LICENSE_AGREEMENT +++ b/CONTRIBUTOR_LICENSE_AGREEMENT @@ -1,7 +1,7 @@ -# Open WebUI Contributor License Agreement +# Contributor License Agreement -By submitting my contributions to Open WebUI, I grant Open WebUI full freedom to use my work in any way they choose, under any terms they like, both now and in the future. This approach helps ensure the project remains unified, flexible, and easy to maintain, while empowering Open WebUI to respond quickly to the needs of its users and the wider community. +By submitting my contributions to this repository in any form, I grant Open WebUI Inc. a perpetual, worldwide, irrevocable, royalty-free license, under copyright and patent, to use, modify, distribute, sublicense, and commercialize my work under any terms they choose, both now and in the future. -Taking part in this process means my work can be seamlessly integrated and combined with others, ensuring longevity and adaptability for everyone who benefits from the Open WebUI project. This collaborative approach strengthens the project’s future and helps guarantee that improvements can always be shared and distributed in the most effective way possible. +I represent that my contributions are my original work (or that I have sufficient rights to grant this license) and that I have the authority to enter into this agreement. **_To the fullest extent permitted by law, my contributions are provided on an “as is” basis, with no warranties or guarantees of any kind, and I disclaim any liability for any issues or damages arising from their use or incorporation into the project, regardless of the type of legal claim._** \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ac81a3943c..36e29e7069 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH} RUN npm run build ######## WebUI backend ######## -FROM python:3.11.14-slim-bookworm AS base +FROM python:3.11-slim-bookworm AS base # Use args ARG USE_CUDA @@ -127,33 +127,41 @@ RUN chown -R $UID:$GID /app $HOME RUN apt-get update && \ apt-get install -y --no-install-recommends \ git build-essential pandoc gcc netcat-openbsd curl jq \ + libmariadb-dev \ python3-dev \ - ffmpeg libsm6 libxext6 \ + ffmpeg libsm6 libxext6 zstd \ && rm -rf /var/lib/apt/lists/* # install python dependencies COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt -RUN pip3 install --no-cache-dir uv && \ +# Set UV_LINK_MODE to copy to prevent 0-byte file corruption in QEMU arm64 cross-builds +ENV UV_LINK_MODE=copy + +RUN set -e; \ + pip3 install --no-cache-dir uv; \ if [ "$USE_CUDA" = "true" ]; then \ # If you use CUDA the whisper and embedding model will be downloaded on first use - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \ - uv pip install --system -r requirements.txt --no-cache-dir && \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \ + # fix: pin torch<=2.9.1 - torch 2.10.0 aarch64 wheels cause SIGILL on ARM devices (RPi 4 Cortex-A72) #21349 + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir; \ + uv pip install --system -r requirements.txt --no-cache-dir; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ + python -c "import nltk; nltk.download('punkt_tab')"; \ else \ - pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \ - uv pip install --system -r requirements.txt --no-cache-dir && \ + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir; \ + uv pip install --system -r requirements.txt --no-cache-dir; \ if [ "$USE_SLIM" != "true" ]; then \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ + python -c "import nltk; nltk.download('punkt_tab')"; \ fi; \ fi; \ - mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \ + mkdir -p /app/backend/data; chown -R $UID:$GID /app/backend/data/; \ rm -rf /var/lib/apt/lists/*; # Install Ollama if requested diff --git a/LICENSE b/LICENSE index faa0129c65..99f39e7fef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,5 @@ +Open WebUI License + Copyright (c) 2023- Open WebUI Inc. [Created by Timothy Jaeryang Baek] All rights reserved. @@ -15,11 +17,27 @@ modification, are permitted provided that the following conditions are met: contributors may be used to endorse or promote products derived from this software without specific prior written permission. -4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below. +4. Notwithstanding any other provision of this License, and as a material + condition of the rights granted herein, licensees are strictly prohibited + from altering, removing, obscuring, or replacing any "Open WebUI" + branding, including but not limited to the name, logo, or any visual, + textual, or symbolic identifiers that distinguish the software and its + interfaces, in any deployment or distribution, except in the following + circumstances: (i) deployments or distributions where the total number + of end users (defined as individual natural persons with direct access + to the application) does not exceed fifty (50) within any rolling + thirty (30) day period; (ii) the licensee has obtained specific prior + written permission from the copyright holder; or (iii) where the + licensee has obtained a duly executed enterprise license expressly + permitting such modification. For all other cases, any removal or + alteration of the "Open WebUI" branding shall constitute a material + breach of license. -5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license. +Materials governed by prior licenses retain those original license +terms, as specified in LICENSE_HISTORY. -6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions. +By contributing to this project, you agree to the project's Contributor +License Agreement (CONTRIBUTOR_LICENSE_AGREEMENT). THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE diff --git a/README.md b/README.md index a783db5c7e..3c4bee98c9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ![Open WebUI Banner](./banner.png) -**Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**. +**Open WebUI is an [extensible](https://docs.openwebui.com/features/extensibility/plugin), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**. Passionate about open-source AI? [Join our team →](https://careers.openwebui.com/) @@ -47,7 +47,7 @@ For more information, be sure to check out our [Open WebUI Documentation](https: - 💾 **Persistent Artifact Storage**: Built-in key-value storage API for artifacts, enabling features like journals, trackers, leaderboards, and collaborative tools with both personal and shared data scopes across sessions. -- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support using your choice of 9 vector databases and multiple content extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the `#` command before a query. +- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support using your choice of 9 vector databases and multiple content extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, PaddleOCR-vl, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the `#` command before a query. - 🔍 **Web Search for RAG**: Perform web searches using 15+ providers including `SearXNG`, `Google PSE`, `Brave Search`, `Kagi`, `Mojeek`, `Tavily`, `Perplexity`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `SearchApi`, `SerpApi`, `Bing`, `Jina`, `Exa`, `Sougou`, `Azure AI Search`, and `Ollama Cloud`, injecting results directly into your chat experience. @@ -172,8 +172,6 @@ After installation, you can access Open WebUI at [http://localhost:3000](http:// We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance. -Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/advanced-topics/development) for instructions on setting up a local development environment. - ### Troubleshooting Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s). diff --git a/backend/dev.sh b/backend/dev.sh index 042fbd9efa..838b93f653 100755 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -1,3 +1,3 @@ export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080" PORT="${PORT:-8080}" -uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload +uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" --reload diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py index 967a49de8f..92cadefe7c 100644 --- a/backend/open_webui/__init__.py +++ b/backend/open_webui/__init__.py @@ -1,103 +1,103 @@ import base64 import os import random +import sys from pathlib import Path +from typing import Annotated import typer import uvicorn -from typing import Optional -from typing_extensions import Annotated app = typer.Typer() -KEY_FILE = Path.cwd() / ".webui_secret_key" +KEY_FILE = Path.cwd() / '.webui_secret_key' -def version_callback(value: bool): +def version_callback(value: bool) -> None: if value: from open_webui.env import VERSION - typer.echo(f"Open WebUI version: {VERSION}") + typer.echo(f'Open WebUI version: {VERSION}') raise typer.Exit() @app.command() def main( - version: Annotated[ - Optional[bool], typer.Option("--version", callback=version_callback) - ] = None, + version: Annotated[bool | None, typer.Option('--version', callback=version_callback)] = None, ): pass @app.command() def serve( - host: str = "0.0.0.0", + host: str = '0.0.0.0', port: int = 8080, ): - os.environ["FROM_INIT_PY"] = "true" - if os.getenv("WEBUI_SECRET_KEY") is None: - typer.echo( - "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable." - ) + os.environ['FROM_INIT_PY'] = 'true' + if os.getenv('WEBUI_SECRET_KEY') is None: + typer.echo('Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.') if not KEY_FILE.exists(): - typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}") + typer.echo(f'Generating a new secret key and saving it to {KEY_FILE}') KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12))) - typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}") - os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text() + typer.echo(f'Loading WEBUI_SECRET_KEY from {KEY_FILE}') + os.environ['WEBUI_SECRET_KEY'] = KEY_FILE.read_text() - if os.getenv("USE_CUDA_DOCKER", "false") == "true": - typer.echo( - "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries." - ) - LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":") - os.environ["LD_LIBRARY_PATH"] = ":".join( + if os.getenv('USE_CUDA_DOCKER', 'false') == 'true': + typer.echo('CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries.') + LD_LIBRARY_PATH = os.getenv('LD_LIBRARY_PATH', '').split(':') + os.environ['LD_LIBRARY_PATH'] = ':'.join( LD_LIBRARY_PATH + [ - "/usr/local/lib/python3.11/site-packages/torch/lib", - "/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib", + '/usr/local/lib/python3.11/site-packages/torch/lib', + '/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib', ] ) try: import torch - assert torch.cuda.is_available(), "CUDA not available" - typer.echo("CUDA seems to be working") + assert torch.cuda.is_available(), 'CUDA not available' + typer.echo('CUDA seems to be working') except Exception as e: typer.echo( - "Error when testing CUDA but USE_CUDA_DOCKER is true. " - "Resetting USE_CUDA_DOCKER to false and removing " - f"LD_LIBRARY_PATH modifications: {e}" + 'Error when testing CUDA but USE_CUDA_DOCKER is true. ' + 'Resetting USE_CUDA_DOCKER to false and removing ' + f'LD_LIBRARY_PATH modifications: {e}' ) - os.environ["USE_CUDA_DOCKER"] = "false" - os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH) + os.environ['USE_CUDA_DOCKER'] = 'false' + os.environ['LD_LIBRARY_PATH'] = ':'.join(LD_LIBRARY_PATH) - import open_webui.main # we need set environment variables before importing main + import open_webui.main # noqa: F401 from open_webui.env import UVICORN_WORKERS # Import the workers setting + # On Windows, uvicorn's default loop factory hardcodes ProactorEventLoop, + # which is incompatible with psycopg v3 async. Setting loop='none' lets + # asyncio.run() respect the WindowsSelectorEventLoopPolicy set in db.py. + loop = 'none' if sys.platform == 'win32' else 'auto' + uvicorn.run( - "open_webui.main:app", + 'open_webui.main:app', host=host, port=port, - forwarded_allow_ips="*", + forwarded_allow_ips='*', workers=UVICORN_WORKERS, + loop=loop, ) @app.command() def dev( - host: str = "0.0.0.0", + host: str = '0.0.0.0', port: int = 8080, reload: bool = True, ): uvicorn.run( - "open_webui.main:app", + 'open_webui.main:app', host=host, port=port, reload=reload, - forwarded_allow_ips="*", + forwarded_allow_ips='*', ) -if __name__ == "__main__": +if __name__ == '__main__': app() diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index dab5b6cfe8..bec7052190 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1,832 +1,158 @@ +from __future__ import annotations + +import base64 import json import logging import os import shutil -import base64 -import redis - +import socket +from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path -from typing import Generic, Union, Optional, TypeVar +from typing import Optional, Union from urllib.parse import urlparse +import redis import requests -from pydantic import BaseModel -from sqlalchemy import JSON, Column, DateTime, Integer, func from authlib.integrations.starlette_client import OAuth - +from pydantic import BaseModel from open_webui.env import ( DATA_DIR, DATABASE_URL, ENABLE_DB_MIGRATIONS, ENV, - REDIS_URL, - REDIS_KEY_PREFIX, - REDIS_SENTINEL_HOSTS, - REDIS_SENTINEL_PORT, FRONTEND_BUILD_DIR, OFFLINE_MODE, OPEN_WEBUI_DIR, + REDIS_KEY_PREFIX, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_PORT, + REDIS_URL, WEBUI_AUTH, WEBUI_FAVICON_URL, WEBUI_NAME, log, ) -from open_webui.internal.db import Base, get_db -from open_webui.utils.redis import get_redis_connection +from open_webui.internal.config import ( + STATE as _state, +) +from open_webui.internal.config import ( + AppConfig, + ConfigVar, +) + +# ── Persistent configuration layer ────────────────────────────────────────── +from open_webui.internal.config import ( # noqa: F401 + ConfigTable as Config, +) +from open_webui.internal.config import ( + _all_configs as PERSISTENT_CONFIG_REGISTRY, +) +from open_webui.internal.config import ( + initialize as _initialize_config, +) + + +def get_config(): + return _state.snapshot + + +def save_to_db(data): + _state.persist(data) + + +async def async_save_to_db(data): + await _state.persist_async(data) + + +def save_config(config): + try: + _state.persist(config) + for s in PERSISTENT_CONFIG_REGISTRY: + s.refresh() + except Exception: + log.exception('Failed to save config') + return False + return True + + +async def async_save_config(config): + try: + await _state.persist_async(config) + for s in PERSISTENT_CONFIG_REGISTRY: + s.refresh() + except Exception: + log.exception('Failed to save config') + return False + return True + + +def reset_config(): + _state.clear() + + +async def async_reset_config(): + await _state.clear_async() + + +def get_config_value(config_path: str): + return _state.read(config_path) class EndpointFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: - return record.getMessage().find("/health") == -1 + return record.getMessage().find('/health') == -1 -# Filter out /endpoint -logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) +logging.getLogger('uvicorn.access').addFilter(EndpointFilter()) #################################### -# Config helpers +# Initialization #################################### -# Function to run the alembic migrations def run_migrations(): - log.info("Running migrations") + log.info('Running migrations') try: from alembic import command - from alembic.config import Config + from alembic.config import Config as AlembicConfig - alembic_cfg = Config(OPEN_WEBUI_DIR / "alembic.ini") + alembic_cfg = AlembicConfig(OPEN_WEBUI_DIR / 'alembic.ini') - # Set the script location dynamically - migrations_path = OPEN_WEBUI_DIR / "migrations" - alembic_cfg.set_main_option("script_location", str(migrations_path)) + migrations_path = OPEN_WEBUI_DIR / 'migrations' + alembic_cfg.set_main_option('script_location', str(migrations_path)) - command.upgrade(alembic_cfg, "head") + command.upgrade(alembic_cfg, 'head') except Exception as e: - log.exception(f"Error running migrations: {e}") + log.exception(f'Error running migrations: {e}') if ENABLE_DB_MIGRATIONS: run_migrations() -class Config(Base): - __tablename__ = "config" - - id = Column(Integer, primary_key=True) - data = Column(JSON, nullable=False) - version = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, nullable=False, server_default=func.now()) - updated_at = Column(DateTime, nullable=True, onupdate=func.now()) +# Migrate legacy config.json → database on first run +if os.path.exists(f'{DATA_DIR}/config.json'): + with open(f'{DATA_DIR}/config.json', 'r') as _f: + save_to_db(json.load(_f)) + os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json') -def load_json_config(): - with open(f"{DATA_DIR}/config.json", "r") as file: - return json.load(file) +ENABLE_PERSISTENT_CONFIG = os.getenv('ENABLE_PERSISTENT_CONFIG', 'True').lower() == 'true' +ENABLE_OAUTH_PERSISTENT_CONFIG = os.getenv('ENABLE_OAUTH_PERSISTENT_CONFIG', 'False').lower() == 'true' - -def save_to_db(data): - with get_db() as db: - existing_config = db.query(Config).first() - if not existing_config: - new_config = Config(data=data, version=0) - db.add(new_config) - else: - existing_config.data = data - existing_config.updated_at = datetime.now() - db.add(existing_config) - db.commit() - - -def reset_config(): - with get_db() as db: - db.query(Config).delete() - db.commit() - - -# When initializing, check if config.json exists and migrate it to the database -if os.path.exists(f"{DATA_DIR}/config.json"): - data = load_json_config() - save_to_db(data) - os.rename(f"{DATA_DIR}/config.json", f"{DATA_DIR}/old_config.json") - -DEFAULT_CONFIG = { - "version": 0, - "ui": {}, -} - - -def get_config(): - with get_db() as db: - config_entry = db.query(Config).order_by(Config.id.desc()).first() - return config_entry.data if config_entry else DEFAULT_CONFIG - - -CONFIG_DATA = get_config() - - -def get_config_value(config_path: str): - path_parts = config_path.split(".") - cur_config = CONFIG_DATA - for key in path_parts: - if key in cur_config: - cur_config = cur_config[key] - else: - return None - return cur_config - - -PERSISTENT_CONFIG_REGISTRY = [] - - -def save_config(config): - global CONFIG_DATA - global PERSISTENT_CONFIG_REGISTRY - try: - save_to_db(config) - CONFIG_DATA = config - - # Trigger updates on all registered PersistentConfig entries - for config_item in PERSISTENT_CONFIG_REGISTRY: - config_item.update() - except Exception as e: - log.exception(e) - return False - return True - - -T = TypeVar("T") - -ENABLE_PERSISTENT_CONFIG = ( - os.environ.get("ENABLE_PERSISTENT_CONFIG", "True").lower() == "true" +# Bootstrap the persistent config subsystem +CONFIG_DATA = _initialize_config( + enable_persistent=ENABLE_PERSISTENT_CONFIG, + enable_oauth_persistent=ENABLE_OAUTH_PERSISTENT_CONFIG, ) - -class PersistentConfig(Generic[T]): - def __init__(self, env_name: str, config_path: str, env_value: T): - self.env_name = env_name - self.config_path = config_path - self.env_value = env_value - self.config_value = get_config_value(config_path) - - if self.config_value is not None and ENABLE_PERSISTENT_CONFIG: - if ( - self.config_path.startswith("oauth.") - and not ENABLE_OAUTH_PERSISTENT_CONFIG - ): - log.info( - f"Skipping loading of '{env_name}' as OAuth persistent config is disabled" - ) - self.value = env_value - else: - log.info(f"'{env_name}' loaded from the latest database entry") - self.value = self.config_value - else: - self.value = env_value - - PERSISTENT_CONFIG_REGISTRY.append(self) - - def __str__(self): - return str(self.value) - - @property - def __dict__(self): - raise TypeError( - "PersistentConfig object cannot be converted to dict, use config_get or .value instead." - ) - - def __getattribute__(self, item): - if item == "__dict__": - raise TypeError( - "PersistentConfig object cannot be converted to dict, use config_get or .value instead." - ) - return super().__getattribute__(item) - - def update(self): - new_value = get_config_value(self.config_path) - if new_value is not None: - self.value = new_value - log.info(f"Updated {self.env_name} to new value {self.value}") - - def save(self): - log.info(f"Saving '{self.env_name}' to the database") - path_parts = self.config_path.split(".") - sub_config = CONFIG_DATA - for key in path_parts[:-1]: - if key not in sub_config: - sub_config[key] = {} - sub_config = sub_config[key] - sub_config[path_parts[-1]] = self.value - save_to_db(CONFIG_DATA) - self.config_value = self.value - - -class AppConfig: - _redis: Union[redis.Redis, redis.cluster.RedisCluster] = None - _redis_key_prefix: str - - _state: dict[str, PersistentConfig] - - def __init__( - self, - redis_url: Optional[str] = None, - redis_sentinels: Optional[list] = [], - redis_cluster: Optional[bool] = False, - redis_key_prefix: str = "open-webui", - ): - if redis_url: - super().__setattr__("_redis_key_prefix", redis_key_prefix) - super().__setattr__( - "_redis", - get_redis_connection( - redis_url, - redis_sentinels, - redis_cluster, - decode_responses=True, - ), - ) - - super().__setattr__("_state", {}) - - def __setattr__(self, key, value): - if isinstance(value, PersistentConfig): - self._state[key] = value - else: - self._state[key].value = value - self._state[key].save() - - if self._redis: - redis_key = f"{self._redis_key_prefix}:config:{key}" - self._redis.set(redis_key, json.dumps(self._state[key].value)) - - def __getattr__(self, key): - if key not in self._state: - raise AttributeError(f"Config key '{key}' not found") - - # If Redis is available, check for an updated value - if self._redis: - redis_key = f"{self._redis_key_prefix}:config:{key}" - redis_value = self._redis.get(redis_key) - - if redis_value is not None: - try: - decoded_value = json.loads(redis_value) - - # Update the in-memory value if different - if self._state[key].value != decoded_value: - self._state[key].value = decoded_value - log.info(f"Updated {key} from Redis: {decoded_value}") - - except json.JSONDecodeError: - log.error(f"Invalid JSON format in Redis for {key}: {redis_value}") - - return self._state[key].value - - -#################################### -# WEBUI_AUTH (Required for security) -#################################### - -ENABLE_API_KEYS = PersistentConfig( - "ENABLE_API_KEYS", - "auth.enable_api_keys", - os.environ.get("ENABLE_API_KEYS", "False").lower() == "true", -) - -ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = PersistentConfig( - "ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS", - "auth.api_key.endpoint_restrictions", - os.environ.get( - "ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS", - os.environ.get("ENABLE_API_KEY_ENDPOINT_RESTRICTIONS", "False"), - ).lower() - == "true", -) - -API_KEYS_ALLOWED_ENDPOINTS = PersistentConfig( - "API_KEYS_ALLOWED_ENDPOINTS", - "auth.api_key.allowed_endpoints", - os.environ.get( - "API_KEYS_ALLOWED_ENDPOINTS", os.environ.get("API_KEY_ALLOWED_ENDPOINTS", "") - ), -) - -JWT_EXPIRES_IN = PersistentConfig( - "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "4w") -) - -if JWT_EXPIRES_IN.value == "-1": - log.warning( - "⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n" - " See: https://docs.openwebui.com/getting-started/env-configuration\n" - ) - -#################################### -# OAuth config -#################################### - -ENABLE_OAUTH_PERSISTENT_CONFIG = ( - os.environ.get("ENABLE_OAUTH_PERSISTENT_CONFIG", "False").lower() == "true" -) - -ENABLE_OAUTH_SIGNUP = PersistentConfig( - "ENABLE_OAUTH_SIGNUP", - "oauth.enable_signup", - os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true", -) - - -OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig( - "OAUTH_MERGE_ACCOUNTS_BY_EMAIL", - "oauth.merge_accounts_by_email", - os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true", -) - -OAUTH_PROVIDERS = {} - -GOOGLE_CLIENT_ID = PersistentConfig( - "GOOGLE_CLIENT_ID", - "oauth.google.client_id", - os.environ.get("GOOGLE_CLIENT_ID", ""), -) - -GOOGLE_CLIENT_SECRET = PersistentConfig( - "GOOGLE_CLIENT_SECRET", - "oauth.google.client_secret", - os.environ.get("GOOGLE_CLIENT_SECRET", ""), -) - - -GOOGLE_OAUTH_SCOPE = PersistentConfig( - "GOOGLE_OAUTH_SCOPE", - "oauth.google.scope", - os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"), -) - -GOOGLE_REDIRECT_URI = PersistentConfig( - "GOOGLE_REDIRECT_URI", - "oauth.google.redirect_uri", - os.environ.get("GOOGLE_REDIRECT_URI", ""), -) - -MICROSOFT_CLIENT_ID = PersistentConfig( - "MICROSOFT_CLIENT_ID", - "oauth.microsoft.client_id", - os.environ.get("MICROSOFT_CLIENT_ID", ""), -) - -MICROSOFT_CLIENT_SECRET = PersistentConfig( - "MICROSOFT_CLIENT_SECRET", - "oauth.microsoft.client_secret", - os.environ.get("MICROSOFT_CLIENT_SECRET", ""), -) - -MICROSOFT_CLIENT_TENANT_ID = PersistentConfig( - "MICROSOFT_CLIENT_TENANT_ID", - "oauth.microsoft.tenant_id", - os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""), -) - -MICROSOFT_CLIENT_LOGIN_BASE_URL = PersistentConfig( - "MICROSOFT_CLIENT_LOGIN_BASE_URL", - "oauth.microsoft.login_base_url", - os.environ.get( - "MICROSOFT_CLIENT_LOGIN_BASE_URL", "https://login.microsoftonline.com" - ), -) - -MICROSOFT_CLIENT_PICTURE_URL = PersistentConfig( - "MICROSOFT_CLIENT_PICTURE_URL", - "oauth.microsoft.picture_url", - os.environ.get( - "MICROSOFT_CLIENT_PICTURE_URL", - "https://graph.microsoft.com/v1.0/me/photo/$value", - ), -) - - -MICROSOFT_OAUTH_SCOPE = PersistentConfig( - "MICROSOFT_OAUTH_SCOPE", - "oauth.microsoft.scope", - os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"), -) - -MICROSOFT_REDIRECT_URI = PersistentConfig( - "MICROSOFT_REDIRECT_URI", - "oauth.microsoft.redirect_uri", - os.environ.get("MICROSOFT_REDIRECT_URI", ""), -) - -GITHUB_CLIENT_ID = PersistentConfig( - "GITHUB_CLIENT_ID", - "oauth.github.client_id", - os.environ.get("GITHUB_CLIENT_ID", ""), -) - -GITHUB_CLIENT_SECRET = PersistentConfig( - "GITHUB_CLIENT_SECRET", - "oauth.github.client_secret", - os.environ.get("GITHUB_CLIENT_SECRET", ""), -) - -GITHUB_CLIENT_SCOPE = PersistentConfig( - "GITHUB_CLIENT_SCOPE", - "oauth.github.scope", - os.environ.get("GITHUB_CLIENT_SCOPE", "user:email"), -) - -GITHUB_CLIENT_REDIRECT_URI = PersistentConfig( - "GITHUB_CLIENT_REDIRECT_URI", - "oauth.github.redirect_uri", - os.environ.get("GITHUB_CLIENT_REDIRECT_URI", ""), -) - -OAUTH_CLIENT_ID = PersistentConfig( - "OAUTH_CLIENT_ID", - "oauth.oidc.client_id", - os.environ.get("OAUTH_CLIENT_ID", ""), -) - -OAUTH_CLIENT_SECRET = PersistentConfig( - "OAUTH_CLIENT_SECRET", - "oauth.oidc.client_secret", - os.environ.get("OAUTH_CLIENT_SECRET", ""), -) - -OPENID_PROVIDER_URL = PersistentConfig( - "OPENID_PROVIDER_URL", - "oauth.oidc.provider_url", - os.environ.get("OPENID_PROVIDER_URL", ""), -) - -OPENID_REDIRECT_URI = PersistentConfig( - "OPENID_REDIRECT_URI", - "oauth.oidc.redirect_uri", - os.environ.get("OPENID_REDIRECT_URI", ""), -) - -OAUTH_SCOPES = PersistentConfig( - "OAUTH_SCOPES", - "oauth.oidc.scopes", - os.environ.get("OAUTH_SCOPES", "openid email profile"), -) - -OAUTH_TIMEOUT = PersistentConfig( - "OAUTH_TIMEOUT", - "oauth.oidc.oauth_timeout", - os.environ.get("OAUTH_TIMEOUT", ""), -) - -OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = PersistentConfig( - "OAUTH_TOKEN_ENDPOINT_AUTH_METHOD", - "oauth.oidc.token_endpoint_auth_method", - os.environ.get("OAUTH_TOKEN_ENDPOINT_AUTH_METHOD", None), -) - -OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig( - "OAUTH_CODE_CHALLENGE_METHOD", - "oauth.oidc.code_challenge_method", - os.environ.get("OAUTH_CODE_CHALLENGE_METHOD", None), -) - -OAUTH_PROVIDER_NAME = PersistentConfig( - "OAUTH_PROVIDER_NAME", - "oauth.oidc.provider_name", - os.environ.get("OAUTH_PROVIDER_NAME", "SSO"), -) - -OAUTH_SUB_CLAIM = PersistentConfig( - "OAUTH_SUB_CLAIM", - "oauth.oidc.sub_claim", - os.environ.get("OAUTH_SUB_CLAIM", None), -) - -OAUTH_USERNAME_CLAIM = PersistentConfig( - "OAUTH_USERNAME_CLAIM", - "oauth.oidc.username_claim", - os.environ.get("OAUTH_USERNAME_CLAIM", "name"), -) - - -OAUTH_PICTURE_CLAIM = PersistentConfig( - "OAUTH_PICTURE_CLAIM", - "oauth.oidc.avatar_claim", - os.environ.get("OAUTH_PICTURE_CLAIM", "picture"), -) - -OAUTH_EMAIL_CLAIM = PersistentConfig( - "OAUTH_EMAIL_CLAIM", - "oauth.oidc.email_claim", - os.environ.get("OAUTH_EMAIL_CLAIM", "email"), -) - -OAUTH_GROUPS_CLAIM = PersistentConfig( - "OAUTH_GROUPS_CLAIM", - "oauth.oidc.group_claim", - os.environ.get("OAUTH_GROUPS_CLAIM", os.environ.get("OAUTH_GROUP_CLAIM", "groups")), -) - -FEISHU_CLIENT_ID = PersistentConfig( - "FEISHU_CLIENT_ID", - "oauth.feishu.client_id", - os.environ.get("FEISHU_CLIENT_ID", ""), -) - -FEISHU_CLIENT_SECRET = PersistentConfig( - "FEISHU_CLIENT_SECRET", - "oauth.feishu.client_secret", - os.environ.get("FEISHU_CLIENT_SECRET", ""), -) - -FEISHU_OAUTH_SCOPE = PersistentConfig( - "FEISHU_OAUTH_SCOPE", - "oauth.feishu.scope", - os.environ.get("FEISHU_OAUTH_SCOPE", "contact:user.base:readonly"), -) - -FEISHU_REDIRECT_URI = PersistentConfig( - "FEISHU_REDIRECT_URI", - "oauth.feishu.redirect_uri", - os.environ.get("FEISHU_REDIRECT_URI", ""), -) - -ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig( - "ENABLE_OAUTH_ROLE_MANAGEMENT", - "oauth.enable_role_mapping", - os.environ.get("ENABLE_OAUTH_ROLE_MANAGEMENT", "False").lower() == "true", -) - -ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig( - "ENABLE_OAUTH_GROUP_MANAGEMENT", - "oauth.enable_group_mapping", - os.environ.get("ENABLE_OAUTH_GROUP_MANAGEMENT", "False").lower() == "true", -) - -ENABLE_OAUTH_GROUP_CREATION = PersistentConfig( - "ENABLE_OAUTH_GROUP_CREATION", - "oauth.enable_group_creation", - os.environ.get("ENABLE_OAUTH_GROUP_CREATION", "False").lower() == "true", -) - - -OAUTH_BLOCKED_GROUPS = PersistentConfig( - "OAUTH_BLOCKED_GROUPS", - "oauth.blocked_groups", - os.environ.get("OAUTH_BLOCKED_GROUPS", "[]"), -) - -OAUTH_GROUPS_SEPARATOR = os.environ.get("OAUTH_GROUPS_SEPARATOR", ";") - -OAUTH_ROLES_CLAIM = PersistentConfig( - "OAUTH_ROLES_CLAIM", - "oauth.roles_claim", - os.environ.get("OAUTH_ROLES_CLAIM", "roles"), -) - -OAUTH_ROLES_SEPARATOR = os.environ.get("OAUTH_ROLES_SEPARATOR", ",") - -OAUTH_ALLOWED_ROLES = PersistentConfig( - "OAUTH_ALLOWED_ROLES", - "oauth.allowed_roles", - [ - role.strip() - for role in os.environ.get( - "OAUTH_ALLOWED_ROLES", f"user{OAUTH_ROLES_SEPARATOR}admin" - ).split(OAUTH_ROLES_SEPARATOR) - if role - ], -) - -OAUTH_ADMIN_ROLES = PersistentConfig( - "OAUTH_ADMIN_ROLES", - "oauth.admin_roles", - [ - role.strip() - for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split( - OAUTH_ROLES_SEPARATOR - ) - if role - ], -) - -OAUTH_ALLOWED_DOMAINS = PersistentConfig( - "OAUTH_ALLOWED_DOMAINS", - "oauth.allowed_domains", - [ - domain.strip() - for domain in os.environ.get("OAUTH_ALLOWED_DOMAINS", "*").split(",") - ], -) - -OAUTH_UPDATE_PICTURE_ON_LOGIN = PersistentConfig( - "OAUTH_UPDATE_PICTURE_ON_LOGIN", - "oauth.update_picture_on_login", - os.environ.get("OAUTH_UPDATE_PICTURE_ON_LOGIN", "False").lower() == "true", -) - -OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = ( - os.environ.get("OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID", "False").lower() - == "true" -) - -OAUTH_AUDIENCE = PersistentConfig( - "OAUTH_AUDIENCE", - "oauth.audience", - os.environ.get("OAUTH_AUDIENCE", ""), -) - - -def load_oauth_providers(): - OAUTH_PROVIDERS.clear() - if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value: - - def google_oauth_register(oauth: OAuth): - client = oauth.register( - name="google", - client_id=GOOGLE_CLIENT_ID.value, - client_secret=GOOGLE_CLIENT_SECRET.value, - server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", - client_kwargs={ - "scope": GOOGLE_OAUTH_SCOPE.value, - **( - {"timeout": int(OAUTH_TIMEOUT.value)} - if OAUTH_TIMEOUT.value - else {} - ), - }, - redirect_uri=GOOGLE_REDIRECT_URI.value, - ) - return client - - OAUTH_PROVIDERS["google"] = { - "redirect_uri": GOOGLE_REDIRECT_URI.value, - "register": google_oauth_register, - } - - if ( - MICROSOFT_CLIENT_ID.value - and MICROSOFT_CLIENT_SECRET.value - and MICROSOFT_CLIENT_TENANT_ID.value - ): - - def microsoft_oauth_register(oauth: OAuth): - client = oauth.register( - name="microsoft", - client_id=MICROSOFT_CLIENT_ID.value, - client_secret=MICROSOFT_CLIENT_SECRET.value, - server_metadata_url=f"{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}", - client_kwargs={ - "scope": MICROSOFT_OAUTH_SCOPE.value, - **( - {"timeout": int(OAUTH_TIMEOUT.value)} - if OAUTH_TIMEOUT.value - else {} - ), - }, - redirect_uri=MICROSOFT_REDIRECT_URI.value, - ) - return client - - OAUTH_PROVIDERS["microsoft"] = { - "redirect_uri": MICROSOFT_REDIRECT_URI.value, - "picture_url": MICROSOFT_CLIENT_PICTURE_URL.value, - "register": microsoft_oauth_register, - } - - if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value: - - def github_oauth_register(oauth: OAuth): - client = oauth.register( - name="github", - client_id=GITHUB_CLIENT_ID.value, - client_secret=GITHUB_CLIENT_SECRET.value, - access_token_url="https://github.com/login/oauth/access_token", - authorize_url="https://github.com/login/oauth/authorize", - api_base_url="https://api.github.com", - userinfo_endpoint="https://api.github.com/user", - client_kwargs={ - "scope": GITHUB_CLIENT_SCOPE.value, - **( - {"timeout": int(OAUTH_TIMEOUT.value)} - if OAUTH_TIMEOUT.value - else {} - ), - }, - redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value, - ) - return client - - OAUTH_PROVIDERS["github"] = { - "redirect_uri": GITHUB_CLIENT_REDIRECT_URI.value, - "register": github_oauth_register, - "sub_claim": "id", - } - - if ( - OAUTH_CLIENT_ID.value - and (OAUTH_CLIENT_SECRET.value or OAUTH_CODE_CHALLENGE_METHOD.value) - and OPENID_PROVIDER_URL.value - ): - - def oidc_oauth_register(oauth: OAuth): - client_kwargs = { - "scope": OAUTH_SCOPES.value, - **( - { - "token_endpoint_auth_method": OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value - } - if OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value - else {} - ), - **( - {"timeout": int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {} - ), - } - - if ( - OAUTH_CODE_CHALLENGE_METHOD.value - and OAUTH_CODE_CHALLENGE_METHOD.value == "S256" - ): - client_kwargs["code_challenge_method"] = "S256" - elif OAUTH_CODE_CHALLENGE_METHOD.value: - raise Exception( - 'Code challenge methods other than "%s" not supported. Given: "%s"' - % ("S256", OAUTH_CODE_CHALLENGE_METHOD.value) - ) - - client = oauth.register( - name="oidc", - client_id=OAUTH_CLIENT_ID.value, - client_secret=OAUTH_CLIENT_SECRET.value, - server_metadata_url=OPENID_PROVIDER_URL.value, - client_kwargs=client_kwargs, - redirect_uri=OPENID_REDIRECT_URI.value, - ) - return client - - OAUTH_PROVIDERS["oidc"] = { - "name": OAUTH_PROVIDER_NAME.value, - "redirect_uri": OPENID_REDIRECT_URI.value, - "register": oidc_oauth_register, - } - - if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value: - - def feishu_oauth_register(oauth: OAuth): - client = oauth.register( - name="feishu", - client_id=FEISHU_CLIENT_ID.value, - client_secret=FEISHU_CLIENT_SECRET.value, - access_token_url="https://open.feishu.cn/open-apis/authen/v2/oauth/token", - authorize_url="https://accounts.feishu.cn/open-apis/authen/v1/authorize", - api_base_url="https://open.feishu.cn/open-apis", - userinfo_endpoint="https://open.feishu.cn/open-apis/authen/v1/user_info", - client_kwargs={ - "scope": FEISHU_OAUTH_SCOPE.value, - **( - {"timeout": int(OAUTH_TIMEOUT.value)} - if OAUTH_TIMEOUT.value - else {} - ), - }, - redirect_uri=FEISHU_REDIRECT_URI.value, - ) - return client - - OAUTH_PROVIDERS["feishu"] = { - "register": feishu_oauth_register, - "sub_claim": "user_id", - } - - configured_providers = [] - if GOOGLE_CLIENT_ID.value: - configured_providers.append("Google") - if MICROSOFT_CLIENT_ID.value: - configured_providers.append("Microsoft") - if GITHUB_CLIENT_ID.value: - configured_providers.append("GitHub") - if FEISHU_CLIENT_ID.value: - configured_providers.append("Feishu") - - if configured_providers and not OPENID_PROVIDER_URL.value: - provider_list = ", ".join(configured_providers) - log.warning( - f"⚠️ OAuth providers configured ({provider_list}) but OPENID_PROVIDER_URL not set - logout will not work!" - ) - log.warning( - f"Set OPENID_PROVIDER_URL to your OAuth provider's OpenID Connect discovery endpoint to fix logout functionality." - ) - - -load_oauth_providers() - #################################### # Static DIR #################################### -STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve() +STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')).resolve() try: if STATIC_DIR.exists(): @@ -839,117 +165,67 @@ try: except Exception as e: pass -for file_path in (FRONTEND_BUILD_DIR / "static").glob("**/*"): +for file_path in (FRONTEND_BUILD_DIR / 'static').glob('**/*'): if file_path.is_file(): - target_path = STATIC_DIR / file_path.relative_to( - (FRONTEND_BUILD_DIR / "static") - ) + target_path = STATIC_DIR / file_path.relative_to((FRONTEND_BUILD_DIR / 'static')) target_path.parent.mkdir(parents=True, exist_ok=True) try: shutil.copyfile(file_path, target_path) except Exception as e: - logging.error(f"An error occurred: {e}") + logging.error(f'An error occurred: {e}') -frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" +frontend_favicon = FRONTEND_BUILD_DIR / 'static' / 'favicon.png' if frontend_favicon.exists(): try: - shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png") + shutil.copyfile(frontend_favicon, STATIC_DIR / 'favicon.png') except Exception as e: - logging.error(f"An error occurred: {e}") + logging.error(f'An error occurred: {e}') -frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png" +frontend_splash = FRONTEND_BUILD_DIR / 'static' / 'splash.png' if frontend_splash.exists(): try: - shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png") + shutil.copyfile(frontend_splash, STATIC_DIR / 'splash.png') except Exception as e: - logging.error(f"An error occurred: {e}") + logging.error(f'An error occurred: {e}') -frontend_loader = FRONTEND_BUILD_DIR / "static" / "loader.js" +frontend_loader = FRONTEND_BUILD_DIR / 'static' / 'loader.js' if frontend_loader.exists(): try: - shutil.copyfile(frontend_loader, STATIC_DIR / "loader.js") + shutil.copyfile(frontend_loader, STATIC_DIR / 'loader.js') except Exception as e: - logging.error(f"An error occurred: {e}") + logging.error(f'An error occurred: {e}') -#################################### -# CUSTOM_NAME (Legacy) -#################################### +# --- Storage Provider --- -CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") +STORAGE_PROVIDER = os.getenv('STORAGE_PROVIDER', 'local') # defaults to local, s3 +STORAGE_LOCAL_CACHE = os.getenv('STORAGE_LOCAL_CACHE', 'true').lower() == 'true' -if CUSTOM_NAME: - try: - r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") - data = r.json() - if r.ok: - if "logo" in data: - WEBUI_FAVICON_URL = url = ( - f"https://api.openwebui.com{data['logo']}" - if data["logo"][0] == "/" - else data["logo"] - ) +S3_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY_ID', None) +S3_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', None) +S3_REGION_NAME = os.getenv('S3_REGION_NAME', None) +S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', None) +S3_KEY_PREFIX = os.getenv('S3_KEY_PREFIX', None) +S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', None) +S3_USE_ACCELERATE_ENDPOINT = os.getenv('S3_USE_ACCELERATE_ENDPOINT', 'false').lower() == 'true' +S3_ADDRESSING_STYLE = os.getenv('S3_ADDRESSING_STYLE', None) +S3_ENABLE_TAGGING = os.getenv('S3_ENABLE_TAGGING', 'false').lower() == 'true' - r = requests.get(url, stream=True) - if r.status_code == 200: - with open(f"{STATIC_DIR}/favicon.png", "wb") as f: - r.raw.decode_content = True - shutil.copyfileobj(r.raw, f) +GCS_BUCKET_NAME = os.getenv('GCS_BUCKET_NAME', None) +GOOGLE_APPLICATION_CREDENTIALS_JSON = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON', None) - if "splash" in data: - url = ( - f"https://api.openwebui.com{data['splash']}" - if data["splash"][0] == "/" - else data["splash"] - ) - - r = requests.get(url, stream=True) - if r.status_code == 200: - with open(f"{STATIC_DIR}/splash.png", "wb") as f: - r.raw.decode_content = True - shutil.copyfileobj(r.raw, f) - - WEBUI_NAME = data["name"] - except Exception as e: - log.exception(e) - pass - - -#################################### -# STORAGE PROVIDER -#################################### - -STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "local") # defaults to local, s3 - -S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None) -S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None) -S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None) -S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None) -S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None) -S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None) -S3_USE_ACCELERATE_ENDPOINT = ( - os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "false").lower() == "true" -) -S3_ADDRESSING_STYLE = os.environ.get("S3_ADDRESSING_STYLE", None) -S3_ENABLE_TAGGING = os.getenv("S3_ENABLE_TAGGING", "false").lower() == "true" - -GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None) -GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get( - "GOOGLE_APPLICATION_CREDENTIALS_JSON", None -) - -AZURE_STORAGE_ENDPOINT = os.environ.get("AZURE_STORAGE_ENDPOINT", None) -AZURE_STORAGE_CONTAINER_NAME = os.environ.get("AZURE_STORAGE_CONTAINER_NAME", None) -AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None) +AZURE_STORAGE_ENDPOINT = os.getenv('AZURE_STORAGE_ENDPOINT', None) +AZURE_STORAGE_CONTAINER_NAME = os.getenv('AZURE_STORAGE_CONTAINER_NAME', None) +AZURE_STORAGE_KEY = os.getenv('AZURE_STORAGE_KEY', None) #################################### # File Upload DIR #################################### -UPLOAD_DIR = DATA_DIR / "uploads" +UPLOAD_DIR = DATA_DIR / 'uploads' UPLOAD_DIR.mkdir(parents=True, exist_ok=True) @@ -957,75 +233,133 @@ UPLOAD_DIR.mkdir(parents=True, exist_ok=True) # Cache DIR #################################### -CACHE_DIR = DATA_DIR / "cache" +CACHE_DIR = DATA_DIR / 'cache' CACHE_DIR.mkdir(parents=True, exist_ok=True) +#################################### +# CUSTOM_NAME (Legacy) +#################################### + +CUSTOM_NAME = os.getenv('CUSTOM_NAME', '') + +if CUSTOM_NAME: + try: + r = requests.get(f'https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}') + data = r.json() + if r.ok: + if 'logo' in data: + WEBUI_FAVICON_URL = url = ( + f'https://api.openwebui.com{data["logo"]}' if data['logo'][0] == '/' else data['logo'] + ) + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(f'{STATIC_DIR}/favicon.png', 'wb') as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + if 'splash' in data: + url = f'https://api.openwebui.com{data["splash"]}' if data['splash'][0] == '/' else data['splash'] + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(f'{STATIC_DIR}/splash.png', 'wb') as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + WEBUI_NAME = data['name'] + except Exception as e: + log.exception(e) + pass + + #################################### # DIRECT CONNECTIONS #################################### -ENABLE_DIRECT_CONNECTIONS = PersistentConfig( - "ENABLE_DIRECT_CONNECTIONS", - "direct.enable", - os.environ.get("ENABLE_DIRECT_CONNECTIONS", "False").lower() == "true", +ENABLE_DIRECT_CONNECTIONS = ConfigVar( + 'ENABLE_DIRECT_CONNECTIONS', + 'direct.enable', + os.getenv('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true', ) #################################### # OLLAMA_BASE_URL #################################### -ENABLE_OLLAMA_API = PersistentConfig( - "ENABLE_OLLAMA_API", - "ollama.enable", - os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true", +ENABLE_OLLAMA_API = ConfigVar( + 'ENABLE_OLLAMA_API', + 'ollama.enable', + os.getenv('ENABLE_OLLAMA_API', 'True').lower() == 'true', ) -OLLAMA_API_BASE_URL = os.environ.get( - "OLLAMA_API_BASE_URL", "http://localhost:11434/api" -) +OLLAMA_API_BASE_URL = os.getenv('OLLAMA_API_BASE_URL', 'http://localhost:11434/api') -OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") +OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', '') if OLLAMA_BASE_URL: # Remove trailing slash - OLLAMA_BASE_URL = ( - OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith("/") else OLLAMA_BASE_URL - ) + OLLAMA_BASE_URL = OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith('/') else OLLAMA_BASE_URL -K8S_FLAG = os.environ.get("K8S_FLAG", "") -USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false") +K8S_FLAG = os.getenv('K8S_FLAG', '') +USE_OLLAMA_DOCKER = os.getenv('USE_OLLAMA_DOCKER', 'false') -if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": - OLLAMA_BASE_URL = ( - OLLAMA_API_BASE_URL[:-4] - if OLLAMA_API_BASE_URL.endswith("/api") - else OLLAMA_API_BASE_URL - ) +if OLLAMA_BASE_URL == '' and OLLAMA_API_BASE_URL != '': + OLLAMA_BASE_URL = OLLAMA_API_BASE_URL[:-4] if OLLAMA_API_BASE_URL.endswith('/api') else OLLAMA_API_BASE_URL -if ENV == "prod": - if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG: - if USE_OLLAMA_DOCKER.lower() == "true": +if ENV == 'prod': + if OLLAMA_BASE_URL == '/ollama' and not K8S_FLAG: + if USE_OLLAMA_DOCKER.lower() == 'true': # if you use all-in-one docker container (Open WebUI + Ollama) # with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434 - OLLAMA_BASE_URL = "http://localhost:11434" + OLLAMA_BASE_URL = 'http://localhost:11434' else: - OLLAMA_BASE_URL = "http://host.docker.internal:11434" + OLLAMA_BASE_URL = 'http://host.docker.internal:11434' elif K8S_FLAG: - OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" + OLLAMA_BASE_URL = 'http://ollama-service.open-webui.svc.cluster.local:11434' -OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") -OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL +def _resolve_ollama_base_url(url: str) -> str: + """If the default Ollama port (11434) is unreachable, try the fallback port (12434).""" -OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] -OLLAMA_BASE_URLS = PersistentConfig( - "OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS -) + def reachable(host: str, port: int) -> bool: + try: + with socket.create_connection((host, port), timeout=1.0): + return True + except (OSError, TimeoutError): + return False -OLLAMA_API_CONFIGS = PersistentConfig( - "OLLAMA_API_CONFIGS", - "ollama.api_configs", + host = urlparse(url).hostname or 'localhost' + + with ThreadPoolExecutor(max_workers=2) as pool: + default = pool.submit(reachable, host, 11434) + fallback = pool.submit(reachable, host, 12434) + + if not default.result() and fallback.result(): + url = url.replace(':11434', ':12434') + log.info(f'Ollama port 11434 unreachable on {host}, falling back to 12434') + elif not default.result(): + log.info(f'Ollama ports 11434 and 12434 both unreachable on {host}') + + return url + + +# Auto-resolve Ollama port when no explicit URL was provided by the user. +# The Dockerfile default is "/ollama" which the block above rewrites to :11434. +if os.getenv('OLLAMA_BASE_URL', '') in ('', '/ollama') and not os.getenv('OLLAMA_BASE_URLS', ''): + OLLAMA_BASE_URL = _resolve_ollama_base_url(OLLAMA_BASE_URL) + + +OLLAMA_BASE_URLS = os.getenv('OLLAMA_BASE_URLS', '') +OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != '' else OLLAMA_BASE_URL + +OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(';')] +OLLAMA_BASE_URLS = ConfigVar('OLLAMA_BASE_URLS', 'ollama.base_urls', OLLAMA_BASE_URLS) + +OLLAMA_API_CONFIGS = ConfigVar( + 'OLLAMA_API_CONFIGS', + 'ollama.api_configs', {}, ) @@ -1034,72 +368,63 @@ OLLAMA_API_CONFIGS = PersistentConfig( #################################### -ENABLE_OPENAI_API = PersistentConfig( - "ENABLE_OPENAI_API", - "openai.enable", - os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true", +ENABLE_OPENAI_API = ConfigVar( + 'ENABLE_OPENAI_API', + 'openai.enable', + os.getenv('ENABLE_OPENAI_API', 'True').lower() == 'true', ) -OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") -OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") +OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '') +OPENAI_API_BASE_URL = os.getenv('OPENAI_API_BASE_URL', '') -GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") -GEMINI_API_BASE_URL = os.environ.get("GEMINI_API_BASE_URL", "") +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY', '') +GEMINI_API_BASE_URL = os.getenv('GEMINI_API_BASE_URL', '') -if OPENAI_API_BASE_URL == "": - OPENAI_API_BASE_URL = "https://api.openai.com/v1" +if OPENAI_API_BASE_URL == '': + OPENAI_API_BASE_URL = 'https://api.openai.com/v1' else: - if OPENAI_API_BASE_URL.endswith("/"): + if OPENAI_API_BASE_URL.endswith('/'): OPENAI_API_BASE_URL = OPENAI_API_BASE_URL[:-1] -OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") -OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY +OPENAI_API_KEYS = os.getenv('OPENAI_API_KEYS', '') +OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != '' else OPENAI_API_KEY -OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")] -OPENAI_API_KEYS = PersistentConfig( - "OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS -) +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(';')] +OPENAI_API_KEYS = ConfigVar('OPENAI_API_KEYS', 'openai.api_keys', OPENAI_API_KEYS) -OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") -OPENAI_API_BASE_URLS = ( - OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL -) +OPENAI_API_BASE_URLS = os.getenv('OPENAI_API_BASE_URLS', '') +OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != '' else OPENAI_API_BASE_URL OPENAI_API_BASE_URLS = [ - url.strip() if url != "" else "https://api.openai.com/v1" - for url in OPENAI_API_BASE_URLS.split(";") + url.strip() if url != '' else 'https://api.openai.com/v1' for url in OPENAI_API_BASE_URLS.split(';') ] -OPENAI_API_BASE_URLS = PersistentConfig( - "OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS -) +OPENAI_API_BASE_URLS = ConfigVar('OPENAI_API_BASE_URLS', 'openai.api_base_urls', OPENAI_API_BASE_URLS) -OPENAI_API_CONFIGS = PersistentConfig( - "OPENAI_API_CONFIGS", - "openai.api_configs", +OPENAI_API_CONFIGS = ConfigVar( + 'OPENAI_API_CONFIGS', + 'openai.api_configs', {}, ) # Get the actual OpenAI API key based on the base URL -OPENAI_API_KEY = "" +OPENAI_API_KEY = '' try: - OPENAI_API_KEY = OPENAI_API_KEYS.value[ - OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1") - ] + OPENAI_API_KEY = OPENAI_API_KEYS.value[OPENAI_API_BASE_URLS.value.index('https://api.openai.com/v1')] except Exception: pass -OPENAI_API_BASE_URL = "https://api.openai.com/v1" +OPENAI_API_BASE_URL = 'https://api.openai.com/v1' #################################### # MODELS #################################### -ENABLE_BASE_MODELS_CACHE = PersistentConfig( - "ENABLE_BASE_MODELS_CACHE", - "models.base_models_cache", - os.environ.get("ENABLE_BASE_MODELS_CACHE", "False").lower() == "true", +ENABLE_BASE_MODELS_CACHE = ConfigVar( + 'ENABLE_BASE_MODELS_CACHE', + 'models.base_models_cache', + os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true', ) @@ -1108,1200 +433,368 @@ ENABLE_BASE_MODELS_CACHE = PersistentConfig( #################################### try: - tool_server_connections = json.loads( - os.environ.get("TOOL_SERVER_CONNECTIONS", "[]") - ) + tool_server_connections = json.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]')) except Exception as e: - log.exception(f"Error loading TOOL_SERVER_CONNECTIONS: {e}") + log.exception(f'Error loading TOOL_SERVER_CONNECTIONS: {e}') tool_server_connections = [] -TOOL_SERVER_CONNECTIONS = PersistentConfig( - "TOOL_SERVER_CONNECTIONS", - "tool_server.connections", +TOOL_SERVER_CONNECTIONS = ConfigVar( + 'TOOL_SERVER_CONNECTIONS', + 'tool_server.connections', tool_server_connections, ) +OAUTH_CLIENT_TIMEOUT = ConfigVar( + 'OAUTH_CLIENT_TIMEOUT', + 'oauth.client.timeout', + os.getenv('OAUTH_CLIENT_TIMEOUT', ''), +) + #################################### -# WEBUI +# TERMINAL_SERVER #################################### +terminal_server_connections = json.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]')) -WEBUI_URL = PersistentConfig("WEBUI_URL", "webui.url", os.environ.get("WEBUI_URL", "")) - - -ENABLE_SIGNUP = PersistentConfig( - "ENABLE_SIGNUP", - "ui.enable_signup", - ( - False - if not WEBUI_AUTH - else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true" - ), -) - -ENABLE_LOGIN_FORM = PersistentConfig( - "ENABLE_LOGIN_FORM", - "ui.ENABLE_LOGIN_FORM", - os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true", -) - -ENABLE_PASSWORD_AUTH = os.environ.get("ENABLE_PASSWORD_AUTH", "True").lower() == "true" - -DEFAULT_LOCALE = PersistentConfig( - "DEFAULT_LOCALE", - "ui.default_locale", - os.environ.get("DEFAULT_LOCALE", ""), -) - -DEFAULT_MODELS = PersistentConfig( - "DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None) -) - -DEFAULT_PINNED_MODELS = PersistentConfig( - "DEFAULT_PINNED_MODELS", - "ui.default_pinned_models", - os.environ.get("DEFAULT_PINNED_MODELS", None), +TERMINAL_SERVER_CONNECTIONS = ConfigVar( + 'TERMINAL_SERVER_CONNECTIONS', + 'terminal_server.connections', + terminal_server_connections, ) try: - default_prompt_suggestions = json.loads( - os.environ.get("DEFAULT_PROMPT_SUGGESTIONS", "[]") - ) -except Exception as e: - log.exception(f"Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}") - default_prompt_suggestions = [] -if default_prompt_suggestions == []: - default_prompt_suggestions = [ - { - "title": ["Help me study", "vocabulary for a college entrance exam"], - "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.", - }, - { - "title": ["Give me ideas", "for what to do with my kids' art"], - "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.", - }, - { - "title": ["Tell me a fun fact", "about the Roman Empire"], - "content": "Tell me a random fun fact about the Roman Empire", - }, - { - "title": ["Show me a code snippet", "of a website's sticky header"], - "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.", - }, - { - "title": [ - "Explain options trading", - "if I'm familiar with buying and selling stocks", - ], - "content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.", - }, - { - "title": ["Overcome procrastination", "give me tips"], - "content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?", - }, - ] - -DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig( - "DEFAULT_PROMPT_SUGGESTIONS", - "ui.prompt_suggestions", - default_prompt_suggestions, -) - -MODEL_ORDER_LIST = PersistentConfig( - "MODEL_ORDER_LIST", - "ui.model_order_list", - [], -) - -DEFAULT_USER_ROLE = PersistentConfig( - "DEFAULT_USER_ROLE", - "ui.default_user_role", - os.getenv("DEFAULT_USER_ROLE", "pending"), -) - -DEFAULT_GROUP_ID = PersistentConfig( - "DEFAULT_GROUP_ID", - "ui.default_group_id", - os.environ.get("DEFAULT_GROUP_ID", ""), -) - -PENDING_USER_OVERLAY_TITLE = PersistentConfig( - "PENDING_USER_OVERLAY_TITLE", - "ui.pending_user_overlay_title", - os.environ.get("PENDING_USER_OVERLAY_TITLE", ""), -) - -PENDING_USER_OVERLAY_CONTENT = PersistentConfig( - "PENDING_USER_OVERLAY_CONTENT", - "ui.pending_user_overlay_content", - os.environ.get("PENDING_USER_OVERLAY_CONTENT", ""), -) - - -RESPONSE_WATERMARK = PersistentConfig( - "RESPONSE_WATERMARK", - "ui.watermark", - os.environ.get("RESPONSE_WATERMARK", ""), -) - - -USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true" -) - -USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT", "False").lower() == "true" -) - -USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT", "False").lower() == "true" -) - - -USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = ( - os.environ.get( - "USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING", "False" - ).lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = ( - os.environ.get( - "USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING", "False" - ).lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = ( - os.environ.get( - "USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING", "False" - ).lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = ( - os.environ.get( - "USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING", "False" - ).lower() - == "true" -) - - -USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = ( - os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING", "False").lower() - == "true" -) - -USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = ( - os.environ.get( - "USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING", "False" - ).lower() - == "true" -) - - -USER_PERMISSIONS_NOTES_ALLOW_SHARING = ( - os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_SHARING", "False").lower() == "true" -) - -USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( - os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING", "False").lower() - == "true" -) - - -USER_PERMISSIONS_CHAT_CONTROLS = ( - os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_VALVES = ( - os.environ.get("USER_PERMISSIONS_CHAT_VALVES", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = ( - os.environ.get("USER_PERMISSIONS_CHAT_SYSTEM_PROMPT", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_PARAMS = ( - os.environ.get("USER_PERMISSIONS_CHAT_PARAMS", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_FILE_UPLOAD = ( - os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_DELETE = ( - os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_DELETE_MESSAGE = ( - os.environ.get("USER_PERMISSIONS_CHAT_DELETE_MESSAGE", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE = ( - os.environ.get("USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE = ( - os.environ.get("USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE", "True").lower() - == "true" -) - -USER_PERMISSIONS_CHAT_RATE_RESPONSE = ( - os.environ.get("USER_PERMISSIONS_CHAT_RATE_RESPONSE", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_EDIT = ( - os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_SHARE = ( - os.environ.get("USER_PERMISSIONS_CHAT_SHARE", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_EXPORT = ( - os.environ.get("USER_PERMISSIONS_CHAT_EXPORT", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_STT = ( - os.environ.get("USER_PERMISSIONS_CHAT_STT", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_TTS = ( - os.environ.get("USER_PERMISSIONS_CHAT_TTS", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_CALL = ( - os.environ.get("USER_PERMISSIONS_CHAT_CALL", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = ( - os.environ.get("USER_PERMISSIONS_CHAT_MULTIPLE_MODELS", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_TEMPORARY = ( - os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true" -) - -USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = ( - os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED", "False").lower() - == "true" -) - - -USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = ( - os.environ.get("USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS", "False").lower() - == "true" -) - -USER_PERMISSIONS_FEATURES_WEB_SEARCH = ( - os.environ.get("USER_PERMISSIONS_FEATURES_WEB_SEARCH", "True").lower() == "true" -) - -USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = ( - os.environ.get("USER_PERMISSIONS_FEATURES_IMAGE_GENERATION", "True").lower() - == "true" -) - -USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = ( - os.environ.get("USER_PERMISSIONS_FEATURES_CODE_INTERPRETER", "True").lower() - == "true" -) - -USER_PERMISSIONS_FEATURES_FOLDERS = ( - os.environ.get("USER_PERMISSIONS_FEATURES_FOLDERS", "True").lower() == "true" -) - -USER_PERMISSIONS_FEATURES_NOTES = ( - os.environ.get("USER_PERMISSIONS_FEATURES_NOTES", "True").lower() == "true" -) - -USER_PERMISSIONS_FEATURES_CHANNELS = ( - os.environ.get("USER_PERMISSIONS_FEATURES_CHANNELS", "True").lower() == "true" -) - -USER_PERMISSIONS_FEATURES_API_KEYS = ( - os.environ.get("USER_PERMISSIONS_FEATURES_API_KEYS", "False").lower() == "true" -) - -USER_PERMISSIONS_FEATURES_MEMORIES = ( - os.environ.get("USER_PERMISSIONS_FEATURES_MEMORIES", "True").lower() == "true" -) - - -USER_PERMISSIONS_SETTINGS_INTERFACE = ( - os.environ.get("USER_PERMISSIONS_SETTINGS_INTERFACE", "True").lower() == "true" -) - - -DEFAULT_USER_PERMISSIONS = { - "workspace": { - "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS, - "knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS, - "prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS, - "tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS, - "models_import": USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT, - "models_export": USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT, - "prompts_import": USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT, - "prompts_export": USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT, - "tools_import": USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT, - "tools_export": USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT, - }, - "sharing": { - "models": USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING, - "public_models": USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING, - "knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING, - "public_knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING, - "prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING, - "public_prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING, - "tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING, - "public_tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING, - "notes": USER_PERMISSIONS_NOTES_ALLOW_SHARING, - "public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, - }, - "chat": { - "controls": USER_PERMISSIONS_CHAT_CONTROLS, - "valves": USER_PERMISSIONS_CHAT_VALVES, - "system_prompt": USER_PERMISSIONS_CHAT_SYSTEM_PROMPT, - "params": USER_PERMISSIONS_CHAT_PARAMS, - "file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD, - "delete": USER_PERMISSIONS_CHAT_DELETE, - "delete_message": USER_PERMISSIONS_CHAT_DELETE_MESSAGE, - "continue_response": USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE, - "regenerate_response": USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE, - "rate_response": USER_PERMISSIONS_CHAT_RATE_RESPONSE, - "edit": USER_PERMISSIONS_CHAT_EDIT, - "share": USER_PERMISSIONS_CHAT_SHARE, - "export": USER_PERMISSIONS_CHAT_EXPORT, - "stt": USER_PERMISSIONS_CHAT_STT, - "tts": USER_PERMISSIONS_CHAT_TTS, - "call": USER_PERMISSIONS_CHAT_CALL, - "multiple_models": USER_PERMISSIONS_CHAT_MULTIPLE_MODELS, - "temporary": USER_PERMISSIONS_CHAT_TEMPORARY, - "temporary_enforced": USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED, - }, - "features": { - # General features - "api_keys": USER_PERMISSIONS_FEATURES_API_KEYS, - "notes": USER_PERMISSIONS_FEATURES_NOTES, - "folders": USER_PERMISSIONS_FEATURES_FOLDERS, - "channels": USER_PERMISSIONS_FEATURES_CHANNELS, - "direct_tool_servers": USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS, - # Chat features - "web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH, - "image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION, - "code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER, - "memories": USER_PERMISSIONS_FEATURES_MEMORIES, - }, - "settings": { - "interface": USER_PERMISSIONS_SETTINGS_INTERFACE, - }, -} - -USER_PERMISSIONS = PersistentConfig( - "USER_PERMISSIONS", - "user.permissions", - DEFAULT_USER_PERMISSIONS, -) - -ENABLE_FOLDERS = PersistentConfig( - "ENABLE_FOLDERS", - "folders.enable", - os.environ.get("ENABLE_FOLDERS", "True").lower() == "true", -) - -FOLDER_MAX_FILE_COUNT = PersistentConfig( - "FOLDER_MAX_FILE_COUNT", - "folders.max_file_count", - os.environ.get("FOLDER_MAX_FILE_COUNT", ""), -) - -ENABLE_CHANNELS = PersistentConfig( - "ENABLE_CHANNELS", - "channels.enable", - os.environ.get("ENABLE_CHANNELS", "False").lower() == "true", -) - -ENABLE_NOTES = PersistentConfig( - "ENABLE_NOTES", - "notes.enable", - os.environ.get("ENABLE_NOTES", "True").lower() == "true", -) - -ENABLE_USER_STATUS = PersistentConfig( - "ENABLE_USER_STATUS", - "users.enable_status", - os.environ.get("ENABLE_USER_STATUS", "True").lower() == "true", -) - -ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig( - "ENABLE_EVALUATION_ARENA_MODELS", - "evaluation.arena.enable", - os.environ.get("ENABLE_EVALUATION_ARENA_MODELS", "True").lower() == "true", -) -EVALUATION_ARENA_MODELS = PersistentConfig( - "EVALUATION_ARENA_MODELS", - "evaluation.arena.models", - [], -) - -DEFAULT_ARENA_MODEL = { - "id": "arena-model", - "name": "Arena Model", - "meta": { - "profile_image_url": "/favicon.png", - "description": "Submit your questions to anonymous AI chatbots and vote on the best response.", - "model_ids": None, - }, -} - -WEBHOOK_URL = PersistentConfig( - "WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "") -) - -ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true" - -ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = ( - os.environ.get("ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS", "True").lower() == "true" -) - -BYPASS_ADMIN_ACCESS_CONTROL = ( - os.environ.get( - "BYPASS_ADMIN_ACCESS_CONTROL", - os.environ.get("ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS", "True"), - ).lower() - == "true" -) - -ENABLE_ADMIN_CHAT_ACCESS = ( - os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true" -) - -ENABLE_COMMUNITY_SHARING = PersistentConfig( - "ENABLE_COMMUNITY_SHARING", - "ui.enable_community_sharing", - os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true", -) - -ENABLE_MESSAGE_RATING = PersistentConfig( - "ENABLE_MESSAGE_RATING", - "ui.enable_message_rating", - os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true", -) - -ENABLE_USER_WEBHOOKS = PersistentConfig( - "ENABLE_USER_WEBHOOKS", - "ui.enable_user_webhooks", - os.environ.get("ENABLE_USER_WEBHOOKS", "True").lower() == "true", -) - -# FastAPI / AnyIO settings -THREAD_POOL_SIZE = os.getenv("THREAD_POOL_SIZE", None) - -if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str): - try: - THREAD_POOL_SIZE = int(THREAD_POOL_SIZE) - except ValueError: - log.warning( - f"THREAD_POOL_SIZE is not a valid integer: {THREAD_POOL_SIZE}. Defaulting to None." - ) - THREAD_POOL_SIZE = None - - -def validate_cors_origin(origin): - parsed_url = urlparse(origin) - - # Check if the scheme is either http or https, or a custom scheme - schemes = ["http", "https"] + CORS_ALLOW_CUSTOM_SCHEME - if parsed_url.scheme not in schemes: - raise ValueError( - f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' and CORS_ALLOW_CUSTOM_SCHEME are allowed." - ) - - # Ensure that the netloc (domain + port) is present, indicating it's a valid URL - if not parsed_url.netloc: - raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.") - - -# For production, you should only need one host as -# fastapi serves the svelte-kit built frontend and backend from the same host and port. -# To test CORS_ALLOW_ORIGIN locally, you can set something like -# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080 -# in your .env file depending on your frontend port, 5173 in this case. -CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";") - -# Allows custom URL schemes (e.g., app://) to be used as origins for CORS. -# Useful for local development or desktop clients with schemes like app:// or other custom protocols. -# Provide a semicolon-separated list of allowed schemes in the environment variable CORS_ALLOW_CUSTOM_SCHEMES. -CORS_ALLOW_CUSTOM_SCHEME = os.environ.get("CORS_ALLOW_CUSTOM_SCHEME", "").split(";") - -if CORS_ALLOW_ORIGIN == ["*"]: - log.warning( - "\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n" - ) -else: - # You have to pick between a single wildcard or a list of origins. - # Doing both will result in CORS errors in the browser. - for origin in CORS_ALLOW_ORIGIN: - validate_cors_origin(origin) - - -class BannerModel(BaseModel): - id: str - type: str - title: Optional[str] = None - content: str - dismissible: bool - timestamp: int - - -try: - banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]")) - banners = [BannerModel(**banner) for banner in banners] -except Exception as e: - log.exception(f"Error loading WEBUI_BANNERS: {e}") - banners = [] - -WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners) - - -SHOW_ADMIN_DETAILS = PersistentConfig( - "SHOW_ADMIN_DETAILS", - "auth.admin.show", - os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true", -) - -ADMIN_EMAIL = PersistentConfig( - "ADMIN_EMAIL", - "auth.admin.email", - os.environ.get("ADMIN_EMAIL", None), -) - - -#################################### -# TASKS -#################################### - - -TASK_MODEL = PersistentConfig( - "TASK_MODEL", - "task.model.default", - os.environ.get("TASK_MODEL", ""), -) - -TASK_MODEL_EXTERNAL = PersistentConfig( - "TASK_MODEL_EXTERNAL", - "task.model.external", - os.environ.get("TASK_MODEL_EXTERNAL", ""), -) - -TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( - "TITLE_GENERATION_PROMPT_TEMPLATE", - "task.title.prompt_template", - os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""), -) - -DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task: -Generate a concise, 3-5 word title with an emoji summarizing the chat history. -### Guidelines: -- The title should clearly represent the main theme or subject of the conversation. -- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting. -- Write the title in the chat's primary language; default to English if multilingual. -- Prioritize accuracy over excessive creativity; keep it clear and simple. -- Your entire response must consist solely of the JSON object, without any introductory or concluding text. -- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text. -- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure. -### Output: -JSON format: { "title": "your concise title here" } -### Examples: -- { "title": "📉 Stock Market Trends" }, -- { "title": "🍪 Perfect Chocolate Chip Recipe" }, -- { "title": "Evolution of Music Streaming" }, -- { "title": "Remote Work Productivity Tips" }, -- { "title": "Artificial Intelligence in Healthcare" }, -- { "title": "🎮 Video Game Development Insights" } -### Chat History: - -{{MESSAGES:END:2}} -""" - -TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig( - "TAGS_GENERATION_PROMPT_TEMPLATE", - "task.tags.prompt_template", - os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""), -) - -DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task: -Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags. - -### Guidelines: -- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education) -- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation -- If content is too short (less than 3 messages) or too diverse, use only ["General"] -- Use the chat's primary language; default to English if multilingual -- Prioritize accuracy over specificity - -### Output: -JSON format: { "tags": ["tag1", "tag2", "tag3"] } - -### Chat History: - -{{MESSAGES:END:6}} -""" - -IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = PersistentConfig( - "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE", - "task.image.prompt_template", - os.environ.get("IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE", ""), -) - -DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task: -Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements. - -### Guidelines: -- Be descriptive and detailed, focusing on the most important aspects of the image. -- Avoid making assumptions or adding information not present in the image. -- Use the chat's primary language; default to English if multilingual. -- If the image is too complex, focus on the most prominent elements. - -### Output: -Strictly return in JSON format: -{ - "prompt": "Your detailed description here." -} - -### Chat History: - -{{MESSAGES:END:6}} -""" - - -FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = PersistentConfig( - "FOLLOW_UP_GENERATION_PROMPT_TEMPLATE", - "task.follow_up.prompt_template", - os.environ.get("FOLLOW_UP_GENERATION_PROMPT_TEMPLATE", ""), -) - -DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task: -Suggest 3-5 relevant follow-up questions or prompts that the user might naturally ask next in this conversation as a **user**, based on the chat history, to help continue or deepen the discussion. -### Guidelines: -- Write all follow-up questions from the user’s point of view, directed to the assistant. -- Make questions concise, clear, and directly related to the discussed topic(s). -- Only suggest follow-ups that make sense given the chat content and do not repeat what was already covered. -- If the conversation is very short or not specific, suggest more general (but relevant) follow-ups the user might ask. -- Use the conversation's primary language; default to English if multilingual. -- Response must be a JSON array of strings, no extra text or formatting. -### Output: -JSON format: { "follow_ups": ["Question 1?", "Question 2?", "Question 3?"] } -### Chat History: - -{{MESSAGES:END:6}} -""" - -ENABLE_FOLLOW_UP_GENERATION = PersistentConfig( - "ENABLE_FOLLOW_UP_GENERATION", - "task.follow_up.enable", - os.environ.get("ENABLE_FOLLOW_UP_GENERATION", "True").lower() == "true", -) - -ENABLE_TAGS_GENERATION = PersistentConfig( - "ENABLE_TAGS_GENERATION", - "task.tags.enable", - os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true", -) - -ENABLE_TITLE_GENERATION = PersistentConfig( - "ENABLE_TITLE_GENERATION", - "task.title.enable", - os.environ.get("ENABLE_TITLE_GENERATION", "True").lower() == "true", -) - - -ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig( - "ENABLE_SEARCH_QUERY_GENERATION", - "task.query.search.enable", - os.environ.get("ENABLE_SEARCH_QUERY_GENERATION", "True").lower() == "true", -) - -ENABLE_RETRIEVAL_QUERY_GENERATION = PersistentConfig( - "ENABLE_RETRIEVAL_QUERY_GENERATION", - "task.query.retrieval.enable", - os.environ.get("ENABLE_RETRIEVAL_QUERY_GENERATION", "True").lower() == "true", -) - - -QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig( - "QUERY_GENERATION_PROMPT_TEMPLATE", - "task.query.prompt_template", - os.environ.get("QUERY_GENERATION_PROMPT_TEMPLATE", ""), -) - -DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task: -Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list. - -### Guidelines: -- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited. -- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic. -- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }. -- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information. -- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions. -- Today's date is: {{CURRENT_DATE}}. -- Always prioritize providing actionable and broad queries that maximize informational coverage. - -### Output: -Strictly return in JSON format: -{ - "queries": ["query1", "query2"] -} - -### Chat History: - -{{MESSAGES:END:6}} - -""" - -ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig( - "ENABLE_AUTOCOMPLETE_GENERATION", - "task.autocomplete.enable", - os.environ.get("ENABLE_AUTOCOMPLETE_GENERATION", "False").lower() == "true", -) - -AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig( - "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH", - "task.autocomplete.input_max_length", - int(os.environ.get("AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH", "-1")), -) - -AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( - "AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE", - "task.autocomplete.prompt_template", - os.environ.get("AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE", ""), -) - - -DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task: -You are an autocompletion system. Continue the text in `` based on the **completion type** in `` and the given language. - -### **Instructions**: -1. Analyze `` for context and meaning. -2. Use `` to guide your output: - - **General**: Provide a natural, concise continuation. - - **Search Query**: Complete as if generating a realistic search query. -3. Start as if you are directly continuing ``. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text. -4. Ensure the continuation: - - Flows naturally from ``. - - Avoids repetition, overexplaining, or unrelated ideas. -5. If unsure, return: `{ "text": "" }`. - -### **Output Rules**: -- Respond only in JSON format: `{ "text": "" }`. - -### **Examples**: -#### Example 1: -Input: -General -The sun was setting over the horizon, painting the sky -Output: -{ "text": "with vibrant shades of orange and pink." } - -#### Example 2: -Input: -Search Query -Top-rated restaurants in -Output: -{ "text": "New York City for Italian cuisine." } - ---- -### Context: - -{{MESSAGES:END:6}} - -{{TYPE}} -{{PROMPT}} -#### Output: -""" - - -VOICE_MODE_PROMPT_TEMPLATE = PersistentConfig( - "VOICE_MODE_PROMPT_TEMPLATE", - "task.voice.prompt_template", - os.environ.get("VOICE_MODE_PROMPT_TEMPLATE", ""), -) - -DEFAULT_VOICE_MODE_PROMPT_TEMPLATE = """You are a friendly, concise voice assistant. - -Everything you say will be spoken aloud. -Keep responses short, clear, and natural. - -STYLE: -- Use simple words and short sentences. -- Sound warm and conversational. -- Avoid long explanations, lists, or complex phrasing. - -BEHAVIOR: -- Give the quickest helpful answer first. -- Offer extra detail only if needed. -- Ask for clarification only when necessary. - -VOICE OPTIMIZATION: -- Break information into small, easy-to-hear chunks. -- Avoid dense wording or anything that sounds like reading text. - -ERROR HANDLING: -- If unsure, say so briefly and offer options. -- If something is unsafe or impossible, decline kindly and suggest a safe alternative. - -Stay consistent, helpful, and easy to listen to.""" - -TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig( - "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", - "task.tools.prompt_template", - os.environ.get("TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", ""), -) - - -DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}} - -Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines: - -- Return only the JSON object, without any additional text or explanation. - -- If no tools match the query, return an empty array: - { - "tool_calls": [] - } - -- If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include: - - "name": The tool's name. - - "parameters": A dictionary of required parameters and their corresponding values. - -The format for the JSON response is strictly: -{ - "tool_calls": [ - {"name": "toolName1", "parameters": {"key1": "value1"}}, - {"name": "toolName2", "parameters": {"key2": "value2"}} - ] -}""" - - -DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱). - -Message: ```{{prompt}}```""" - -DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}" - -Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability. - -Responses from models: {{responses}}""" - + TERMINAL_PROXY_HEADERS = json.loads(os.getenv('TERMINAL_PROXY_HEADERS', '{}')) +except Exception: + TERMINAL_PROXY_HEADERS = {} #################################### # Code Interpreter #################################### -ENABLE_CODE_EXECUTION = PersistentConfig( - "ENABLE_CODE_EXECUTION", - "code_execution.enable", - os.environ.get("ENABLE_CODE_EXECUTION", "True").lower() == "true", +ENABLE_CODE_EXECUTION = ConfigVar( + 'ENABLE_CODE_EXECUTION', + 'code_execution.enable', + os.getenv('ENABLE_CODE_EXECUTION', 'True').lower() == 'true', ) -CODE_EXECUTION_ENGINE = PersistentConfig( - "CODE_EXECUTION_ENGINE", - "code_execution.engine", - os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"), +CODE_EXECUTION_ENGINE = ConfigVar( + 'CODE_EXECUTION_ENGINE', + 'code_execution.engine', + os.getenv('CODE_EXECUTION_ENGINE', 'pyodide'), ) -CODE_EXECUTION_JUPYTER_URL = PersistentConfig( - "CODE_EXECUTION_JUPYTER_URL", - "code_execution.jupyter.url", - os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""), +CODE_EXECUTION_JUPYTER_URL = ConfigVar( + 'CODE_EXECUTION_JUPYTER_URL', + 'code_execution.jupyter.url', + os.getenv('CODE_EXECUTION_JUPYTER_URL', ''), ) -CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig( - "CODE_EXECUTION_JUPYTER_AUTH", - "code_execution.jupyter.auth", - os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""), +CODE_EXECUTION_JUPYTER_AUTH = ConfigVar( + 'CODE_EXECUTION_JUPYTER_AUTH', + 'code_execution.jupyter.auth', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH', ''), ) -CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig( - "CODE_EXECUTION_JUPYTER_AUTH_TOKEN", - "code_execution.jupyter.auth_token", - os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""), +CODE_EXECUTION_JUPYTER_AUTH_TOKEN = ConfigVar( + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN', + 'code_execution.jupyter.auth_token', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''), ) -CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig( - "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", - "code_execution.jupyter.auth_password", - os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), +CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ConfigVar( + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', + 'code_execution.jupyter.auth_password', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''), ) -CODE_EXECUTION_JUPYTER_TIMEOUT = PersistentConfig( - "CODE_EXECUTION_JUPYTER_TIMEOUT", - "code_execution.jupyter.timeout", - int(os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60")), +CODE_EXECUTION_JUPYTER_TIMEOUT = ConfigVar( + 'CODE_EXECUTION_JUPYTER_TIMEOUT', + 'code_execution.jupyter.timeout', + int(os.getenv('CODE_EXECUTION_JUPYTER_TIMEOUT', '60')), ) -ENABLE_CODE_INTERPRETER = PersistentConfig( - "ENABLE_CODE_INTERPRETER", - "code_interpreter.enable", - os.environ.get("ENABLE_CODE_INTERPRETER", "True").lower() == "true", +ENABLE_CODE_INTERPRETER = ConfigVar( + 'ENABLE_CODE_INTERPRETER', + 'code_interpreter.enable', + os.getenv('ENABLE_CODE_INTERPRETER', 'True').lower() == 'true', ) -ENABLE_MEMORIES = PersistentConfig( - "ENABLE_MEMORIES", - "memories.enable", - os.environ.get("ENABLE_MEMORIES", "True").lower() == "true", +ENABLE_MEMORIES = ConfigVar( + 'ENABLE_MEMORIES', + 'memories.enable', + os.getenv('ENABLE_MEMORIES', 'True').lower() == 'true', ) -CODE_INTERPRETER_ENGINE = PersistentConfig( - "CODE_INTERPRETER_ENGINE", - "code_interpreter.engine", - os.environ.get("CODE_INTERPRETER_ENGINE", "pyodide"), +CODE_INTERPRETER_ENGINE = ConfigVar( + 'CODE_INTERPRETER_ENGINE', + 'code_interpreter.engine', + os.getenv('CODE_INTERPRETER_ENGINE', 'pyodide'), ) -CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig( - "CODE_INTERPRETER_PROMPT_TEMPLATE", - "code_interpreter.prompt_template", - os.environ.get("CODE_INTERPRETER_PROMPT_TEMPLATE", ""), +CODE_INTERPRETER_PROMPT_TEMPLATE = ConfigVar( + 'CODE_INTERPRETER_PROMPT_TEMPLATE', + 'code_interpreter.prompt_template', + os.getenv('CODE_INTERPRETER_PROMPT_TEMPLATE', ''), ) -CODE_INTERPRETER_JUPYTER_URL = PersistentConfig( - "CODE_INTERPRETER_JUPYTER_URL", - "code_interpreter.jupyter.url", - os.environ.get( - "CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "") +CODE_INTERPRETER_JUPYTER_URL = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_URL', + 'code_interpreter.jupyter.url', + os.getenv('CODE_INTERPRETER_JUPYTER_URL', os.getenv('CODE_EXECUTION_JUPYTER_URL', '')), +) + +CODE_INTERPRETER_JUPYTER_AUTH = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_AUTH', + 'code_interpreter.jupyter.auth', + os.getenv( + 'CODE_INTERPRETER_JUPYTER_AUTH', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH', ''), ), ) -CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig( - "CODE_INTERPRETER_JUPYTER_AUTH", - "code_interpreter.jupyter.auth", - os.environ.get( - "CODE_INTERPRETER_JUPYTER_AUTH", - os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""), - ), -) - -CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig( - "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", - "code_interpreter.jupyter.auth_token", - os.environ.get( - "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", - os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""), +CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN', + 'code_interpreter.jupyter.auth_token', + os.getenv( + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''), ), ) -CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig( - "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", - "code_interpreter.jupyter.auth_password", - os.environ.get( - "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", - os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), +CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD', + 'code_interpreter.jupyter.auth_password', + os.getenv( + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''), ), ) -CODE_INTERPRETER_JUPYTER_TIMEOUT = PersistentConfig( - "CODE_INTERPRETER_JUPYTER_TIMEOUT", - "code_interpreter.jupyter.timeout", +CODE_INTERPRETER_JUPYTER_TIMEOUT = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_TIMEOUT', + 'code_interpreter.jupyter.timeout', int( - os.environ.get( - "CODE_INTERPRETER_JUPYTER_TIMEOUT", - os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60"), + os.getenv( + 'CODE_INTERPRETER_JUPYTER_TIMEOUT', + os.getenv('CODE_EXECUTION_JUPYTER_TIMEOUT', '60'), ) ), ) CODE_INTERPRETER_BLOCKED_MODULES = [ - library.strip() - for library in os.environ.get("CODE_INTERPRETER_BLOCKED_MODULES", "").split(",") - if library.strip() + library.strip() for library in os.getenv('CODE_INTERPRETER_BLOCKED_MODULES', '').split(',') if library.strip() ] DEFAULT_CODE_INTERPRETER_PROMPT = """ -#### Tools Available +#### Code Interpreter -1. **Code Interpreter**: `` - - You have access to a Python shell that runs directly in the user's browser, enabling fast execution of code for analysis, calculations, or problem-solving. Use it in this response. - - The Python code you write can incorporate a wide array of libraries, handle data manipulation or visualization, perform API calls for web-related tasks, or tackle virtually any computational challenge. Use this flexibility to **think outside the box, craft elegant solutions, and harness Python's full potential**. - - To use it, **you must enclose your code within `` XML tags** and stop right away. If you don't, the code won't execute. - - When writing code in the code_interpreter XML tag, Do NOT use the triple backticks code block for markdown formatting, example: ```py # python code ``` will cause an error because it is markdown formatting, it is not python code. - - When coding, **always aim to print meaningful outputs** (e.g., results, tables, summaries, or visuals) to better interpret and verify the findings. Avoid relying on implicit outputs; prioritize explicit and clear print statements so the results are effectively communicated to the user. - - After obtaining the printed output, **always provide a concise analysis, interpretation, or next steps to help the user understand the findings or refine the outcome further.** - - If the results are unclear, unexpected, or require validation, refine the code and execute it again as needed. Always aim to deliver meaningful insights from the results, iterating if necessary. - - **If a link to an image, audio, or any file is provided in markdown format in the output, ALWAYS regurgitate word for word, explicitly display it as part of the response to ensure the user can access it easily, do NOT change the link.** - - All responses should be communicated in the chat's primary language, ensuring seamless understanding. If the chat is multilingual, default to English for clarity. +You have access to a Python code interpreter via: `` -Ensure that the tools are effectively utilized to achieve the highest-quality analysis for the user.""" +- The Python shell runs directly in the user's browser for fast execution of analysis, calculations, or problem-solving. Use it in this response. +- You can use a wide array of libraries for data manipulation, visualization, API calls, or any computational task. Think outside the box and harness Python's full potential. +- **You must enclose your code within `` XML tags** and stop right away. If you don't, the code won't execute. +- Do NOT use triple backticks (```py ... ```) inside the XML tags — that is markdown formatting, not executable Python code. +- **Always print meaningful outputs** (results, tables, summaries, visuals). Avoid implicit outputs; use explicit print statements. +- After obtaining output, **provide a concise analysis, interpretation, or next steps** to help the user understand the findings. +- If results are unclear or unexpected, refine the code and re-execute. Iterate until you deliver meaningful insights. +- **If a link to an image, audio, or any file appears in the output, display it exactly as-is** in your response so the user can access it. Do not modify the link. +- Respond in the chat's primary language. Default to English if multilingual. + +Ensure the code interpreter is effectively utilized to achieve the highest-quality analysis for the user.""" + +# Appended to the code interpreter prompt only when engine is pyodide (not jupyter) +CODE_INTERPRETER_PYODIDE_PROMPT = """ + +##### Pyodide Environment + +- This Python environment runs via Pyodide in the browser. **Do not install packages** — `pip install`, `subprocess`, and `micropip.install()` are not available. +- If a required library is unavailable, use an alternative approach with available modules. Do not attempt to install anything. + +##### Persistent File System + +- User-uploaded files are available at `/mnt/uploads/`. When the user asks you to work with their files, read from this directory. +- You can also write output files to `/mnt/uploads/` so the user can access and download them from the file browser. +- The file system persists across code executions within the same session. +- Use `import os; os.listdir('/mnt/uploads')` to discover available files.""" #################################### # Vector Database #################################### -VECTOR_DB = os.environ.get("VECTOR_DB", "chroma") +VECTOR_DB = os.getenv('VECTOR_DB', 'chroma') # Chroma -CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" +CHROMA_DATA_PATH = f'{DATA_DIR}/vector_db' -if VECTOR_DB == "chroma": +if VECTOR_DB == 'chroma': import chromadb - CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT) - CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE) - CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "") - CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000")) - CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "") - CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get( - "CHROMA_CLIENT_AUTH_CREDENTIALS", "" - ) + CHROMA_TENANT = os.getenv('CHROMA_TENANT', chromadb.DEFAULT_TENANT) + CHROMA_DATABASE = os.getenv('CHROMA_DATABASE', chromadb.DEFAULT_DATABASE) + CHROMA_HTTP_HOST = os.getenv('CHROMA_HTTP_HOST', '') + CHROMA_HTTP_PORT = int(os.getenv('CHROMA_HTTP_PORT', '8000')) + CHROMA_CLIENT_AUTH_PROVIDER = os.getenv('CHROMA_CLIENT_AUTH_PROVIDER', '') + CHROMA_CLIENT_AUTH_CREDENTIALS = os.getenv('CHROMA_CLIENT_AUTH_CREDENTIALS', '') # Comma-separated list of header=value pairs - CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "") + CHROMA_HTTP_HEADERS = os.getenv('CHROMA_HTTP_HEADERS', '') if CHROMA_HTTP_HEADERS: - CHROMA_HTTP_HEADERS = dict( - [pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")] - ) + CHROMA_HTTP_HEADERS = dict([pair.split('=') for pair in CHROMA_HTTP_HEADERS.split(',')]) else: CHROMA_HTTP_HEADERS = None - CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true" + CHROMA_HTTP_SSL = os.getenv('CHROMA_HTTP_SSL', 'false').lower() == 'true' # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2) + +# MariaDB Vector (mariadb-vector) +MARIADB_VECTOR_DB_URL = os.getenv('MARIADB_VECTOR_DB_URL', '').strip() + +MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int( + os.getenv('MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536').strip() or '1536' +) + +# Distance strategy: +# - cosine => vec_distance_cosine(...) +# - euclidean => vec_distance_euclidean(...) +MARIADB_VECTOR_DISTANCE_STRATEGY = os.getenv('MARIADB_VECTOR_DISTANCE_STRATEGY', 'cosine').strip().lower() + +# HNSW M parameter (MariaDB VECTOR INDEX ... M=) +MARIADB_VECTOR_INDEX_M = int(os.getenv('MARIADB_VECTOR_INDEX_M', '8').strip() or '8') + +# Pooling (MariaDB-Vector) +MARIADB_VECTOR_POOL_SIZE = os.getenv('MARIADB_VECTOR_POOL_SIZE', None) + +if MARIADB_VECTOR_POOL_SIZE != None: + try: + MARIADB_VECTOR_POOL_SIZE = int(MARIADB_VECTOR_POOL_SIZE) + except Exception: + MARIADB_VECTOR_POOL_SIZE = None + +MARIADB_VECTOR_POOL_MAX_OVERFLOW = os.getenv('MARIADB_VECTOR_POOL_MAX_OVERFLOW', 0) + +if MARIADB_VECTOR_POOL_MAX_OVERFLOW == '': + MARIADB_VECTOR_POOL_MAX_OVERFLOW = 0 +else: + try: + MARIADB_VECTOR_POOL_MAX_OVERFLOW = int(MARIADB_VECTOR_POOL_MAX_OVERFLOW) + except Exception: + MARIADB_VECTOR_POOL_MAX_OVERFLOW = 0 + +MARIADB_VECTOR_POOL_TIMEOUT = os.getenv('MARIADB_VECTOR_POOL_TIMEOUT', 30) + +if MARIADB_VECTOR_POOL_TIMEOUT == '': + MARIADB_VECTOR_POOL_TIMEOUT = 30 +else: + try: + MARIADB_VECTOR_POOL_TIMEOUT = int(MARIADB_VECTOR_POOL_TIMEOUT) + except Exception: + MARIADB_VECTOR_POOL_TIMEOUT = 30 + +MARIADB_VECTOR_POOL_RECYCLE = os.getenv('MARIADB_VECTOR_POOL_RECYCLE', 3600) + +if MARIADB_VECTOR_POOL_RECYCLE == '': + MARIADB_VECTOR_POOL_RECYCLE = 3600 +else: + try: + MARIADB_VECTOR_POOL_RECYCLE = int(MARIADB_VECTOR_POOL_RECYCLE) + except Exception: + MARIADB_VECTOR_POOL_RECYCLE = 3600 + +ENABLE_MARIADB_VECTOR = True +if VECTOR_DB == 'mariadb-vector': + if not MARIADB_VECTOR_DB_URL: + ENABLE_MARIADB_VECTOR = False + else: + try: + parsed = urlparse(MARIADB_VECTOR_DB_URL) + scheme = (parsed.scheme or '').lower() + # Require official driver so VECTOR binds as float32 bytes correctly + if scheme != 'mariadb+mariadbconnector': + ENABLE_MARIADB_VECTOR = False + except Exception: + ENABLE_MARIADB_VECTOR = False + + # Milvus -MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db") -MILVUS_DB = os.environ.get("MILVUS_DB", "default") -MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None) -MILVUS_INDEX_TYPE = os.environ.get("MILVUS_INDEX_TYPE", "HNSW") -MILVUS_METRIC_TYPE = os.environ.get("MILVUS_METRIC_TYPE", "COSINE") -MILVUS_HNSW_M = int(os.environ.get("MILVUS_HNSW_M", "16")) -MILVUS_HNSW_EFCONSTRUCTION = int(os.environ.get("MILVUS_HNSW_EFCONSTRUCTION", "100")) -MILVUS_IVF_FLAT_NLIST = int(os.environ.get("MILVUS_IVF_FLAT_NLIST", "128")) -MILVUS_DISKANN_MAX_DEGREE = int(os.environ.get("MILVUS_DISKANN_MAX_DEGREE", "56")) -MILVUS_DISKANN_SEARCH_LIST_SIZE = int( - os.environ.get("MILVUS_DISKANN_SEARCH_LIST_SIZE", "100") -) -ENABLE_MILVUS_MULTITENANCY_MODE = ( - os.environ.get("ENABLE_MILVUS_MULTITENANCY_MODE", "false").lower() == "true" -) +MILVUS_URI = os.getenv('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db') +MILVUS_DB = os.getenv('MILVUS_DB', 'default') +MILVUS_TOKEN = os.getenv('MILVUS_TOKEN', None) +MILVUS_INDEX_TYPE = os.getenv('MILVUS_INDEX_TYPE', 'HNSW') +MILVUS_METRIC_TYPE = os.getenv('MILVUS_METRIC_TYPE', 'COSINE') +MILVUS_HNSW_M = int(os.getenv('MILVUS_HNSW_M', '16')) +MILVUS_HNSW_EFCONSTRUCTION = int(os.getenv('MILVUS_HNSW_EFCONSTRUCTION', '100')) +MILVUS_IVF_FLAT_NLIST = int(os.getenv('MILVUS_IVF_FLAT_NLIST', '128')) +MILVUS_DISKANN_MAX_DEGREE = int(os.getenv('MILVUS_DISKANN_MAX_DEGREE', '56')) +MILVUS_DISKANN_SEARCH_LIST_SIZE = int(os.getenv('MILVUS_DISKANN_SEARCH_LIST_SIZE', '100')) +ENABLE_MILVUS_MULTITENANCY_MODE = os.getenv('ENABLE_MILVUS_MULTITENANCY_MODE', 'false').lower() == 'true' # Hyphens not allowed, need to use underscores in collection names -MILVUS_COLLECTION_PREFIX = os.environ.get("MILVUS_COLLECTION_PREFIX", "open_webui") +MILVUS_COLLECTION_PREFIX = os.getenv('MILVUS_COLLECTION_PREFIX', 'open_webui') # Qdrant -QDRANT_URI = os.environ.get("QDRANT_URI", None) -QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None) -QDRANT_ON_DISK = os.environ.get("QDRANT_ON_DISK", "false").lower() == "true" -QDRANT_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "false").lower() == "true" -QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334")) -QDRANT_TIMEOUT = int(os.environ.get("QDRANT_TIMEOUT", "5")) -QDRANT_HNSW_M = int(os.environ.get("QDRANT_HNSW_M", "16")) -ENABLE_QDRANT_MULTITENANCY_MODE = ( - os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "true").lower() == "true" -) -QDRANT_COLLECTION_PREFIX = os.environ.get("QDRANT_COLLECTION_PREFIX", "open-webui") +QDRANT_URI = os.getenv('QDRANT_URI', None) +QDRANT_API_KEY = os.getenv('QDRANT_API_KEY', None) +QDRANT_ON_DISK = os.getenv('QDRANT_ON_DISK', 'false').lower() == 'true' +QDRANT_PREFER_GRPC = os.getenv('QDRANT_PREFER_GRPC', 'false').lower() == 'true' +QDRANT_GRPC_PORT = int(os.getenv('QDRANT_GRPC_PORT', '6334')) +QDRANT_TIMEOUT = int(os.getenv('QDRANT_TIMEOUT', '5')) +QDRANT_HNSW_M = int(os.getenv('QDRANT_HNSW_M', '16')) +ENABLE_QDRANT_MULTITENANCY_MODE = os.getenv('ENABLE_QDRANT_MULTITENANCY_MODE', 'true').lower() == 'true' +QDRANT_COLLECTION_PREFIX = os.getenv('QDRANT_COLLECTION_PREFIX', 'open-webui') -WEAVIATE_HTTP_HOST = os.environ.get("WEAVIATE_HTTP_HOST", "") -WEAVIATE_HTTP_PORT = int(os.environ.get("WEAVIATE_HTTP_PORT", "8080")) -WEAVIATE_GRPC_PORT = int(os.environ.get("WEAVIATE_GRPC_PORT", "50051")) -WEAVIATE_API_KEY = os.environ.get("WEAVIATE_API_KEY") +WEAVIATE_HTTP_HOST = os.getenv('WEAVIATE_HTTP_HOST', '') +WEAVIATE_GRPC_HOST = os.getenv('WEAVIATE_GRPC_HOST', '') +WEAVIATE_HTTP_PORT = int(os.getenv('WEAVIATE_HTTP_PORT', '8080')) +WEAVIATE_GRPC_PORT = int(os.getenv('WEAVIATE_GRPC_PORT', '50051')) +WEAVIATE_API_KEY = os.getenv('WEAVIATE_API_KEY') +WEAVIATE_HTTP_SECURE = os.getenv('WEAVIATE_HTTP_SECURE', 'false').lower() == 'true' +WEAVIATE_GRPC_SECURE = os.getenv('WEAVIATE_GRPC_SECURE', 'false').lower() == 'true' +WEAVIATE_SKIP_INIT_CHECKS = os.getenv('WEAVIATE_SKIP_INIT_CHECKS', 'false').lower() == 'true' # OpenSearch -OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200") -OPENSEARCH_SSL = os.environ.get("OPENSEARCH_SSL", "true").lower() == "true" -OPENSEARCH_CERT_VERIFY = ( - os.environ.get("OPENSEARCH_CERT_VERIFY", "false").lower() == "true" -) -OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None) -OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None) +OPENSEARCH_URI = os.getenv('OPENSEARCH_URI', 'https://localhost:9200') +OPENSEARCH_SSL = os.getenv('OPENSEARCH_SSL', 'true').lower() == 'true' +OPENSEARCH_CERT_VERIFY = os.getenv('OPENSEARCH_CERT_VERIFY', 'false').lower() == 'true' +OPENSEARCH_USERNAME = os.getenv('OPENSEARCH_USERNAME', None) +OPENSEARCH_PASSWORD = os.getenv('OPENSEARCH_PASSWORD', None) # ElasticSearch -ELASTICSEARCH_URL = os.environ.get("ELASTICSEARCH_URL", "https://localhost:9200") -ELASTICSEARCH_CA_CERTS = os.environ.get("ELASTICSEARCH_CA_CERTS", None) -ELASTICSEARCH_API_KEY = os.environ.get("ELASTICSEARCH_API_KEY", None) -ELASTICSEARCH_USERNAME = os.environ.get("ELASTICSEARCH_USERNAME", None) -ELASTICSEARCH_PASSWORD = os.environ.get("ELASTICSEARCH_PASSWORD", None) -ELASTICSEARCH_CLOUD_ID = os.environ.get("ELASTICSEARCH_CLOUD_ID", None) -SSL_ASSERT_FINGERPRINT = os.environ.get("SSL_ASSERT_FINGERPRINT", None) -ELASTICSEARCH_INDEX_PREFIX = os.environ.get( - "ELASTICSEARCH_INDEX_PREFIX", "open_webui_collections" -) +ELASTICSEARCH_URL = os.getenv('ELASTICSEARCH_URL', 'https://localhost:9200') +ELASTICSEARCH_CA_CERTS = os.getenv('ELASTICSEARCH_CA_CERTS', None) +ELASTICSEARCH_API_KEY = os.getenv('ELASTICSEARCH_API_KEY', None) +ELASTICSEARCH_USERNAME = os.getenv('ELASTICSEARCH_USERNAME', None) +ELASTICSEARCH_PASSWORD = os.getenv('ELASTICSEARCH_PASSWORD', None) +ELASTICSEARCH_CLOUD_ID = os.getenv('ELASTICSEARCH_CLOUD_ID', None) +SSL_ASSERT_FINGERPRINT = os.getenv('SSL_ASSERT_FINGERPRINT', None) +ELASTICSEARCH_INDEX_PREFIX = os.getenv('ELASTICSEARCH_INDEX_PREFIX', 'open_webui_collections') # Pgvector -PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL) -if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"): +PGVECTOR_DB_URL = os.getenv('PGVECTOR_DB_URL', DATABASE_URL) +if VECTOR_DB == 'pgvector' and not PGVECTOR_DB_URL.startswith('postgres'): raise ValueError( - "Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database." + 'Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database.' ) -PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int( - os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536") -) +PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(os.getenv('PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH', '1536')) -PGVECTOR_USE_HALFVEC = os.getenv("PGVECTOR_USE_HALFVEC", "false").lower() == "true" +PGVECTOR_USE_HALFVEC = os.getenv('PGVECTOR_USE_HALFVEC', 'false').lower() == 'true' if PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH > 2000 and not PGVECTOR_USE_HALFVEC: raise ValueError( - "PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH is set to " - f"{PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH}, which exceeds the 2000 dimension limit of the " + 'PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH is set to ' + f'{PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH}, which exceeds the 2000 dimension limit of the ' "'vector' type. Set PGVECTOR_USE_HALFVEC=true to enable the 'halfvec' " - "type required for high-dimensional embeddings." + 'type required for high-dimensional embeddings.' ) -PGVECTOR_CREATE_EXTENSION = ( - os.getenv("PGVECTOR_CREATE_EXTENSION", "true").lower() == "true" -) -PGVECTOR_PGCRYPTO = os.getenv("PGVECTOR_PGCRYPTO", "false").lower() == "true" -PGVECTOR_PGCRYPTO_KEY = os.getenv("PGVECTOR_PGCRYPTO_KEY", None) +PGVECTOR_CREATE_EXTENSION = os.getenv('PGVECTOR_CREATE_EXTENSION', 'true').lower() == 'true' +PGVECTOR_PGCRYPTO = os.getenv('PGVECTOR_PGCRYPTO', 'false').lower() == 'true' +PGVECTOR_PGCRYPTO_KEY = os.getenv('PGVECTOR_PGCRYPTO_KEY', None) if PGVECTOR_PGCRYPTO and not PGVECTOR_PGCRYPTO_KEY: - raise ValueError( - "PGVECTOR_PGCRYPTO is enabled but PGVECTOR_PGCRYPTO_KEY is not set. Please provide a valid key." - ) + raise ValueError('PGVECTOR_PGCRYPTO is enabled but PGVECTOR_PGCRYPTO_KEY is not set. Please provide a valid key.') -PGVECTOR_POOL_SIZE = os.environ.get("PGVECTOR_POOL_SIZE", None) +PGVECTOR_POOL_SIZE = os.getenv('PGVECTOR_POOL_SIZE', None) if PGVECTOR_POOL_SIZE != None: try: @@ -2309,9 +802,9 @@ if PGVECTOR_POOL_SIZE != None: except Exception: PGVECTOR_POOL_SIZE = None -PGVECTOR_POOL_MAX_OVERFLOW = os.environ.get("PGVECTOR_POOL_MAX_OVERFLOW", 0) +PGVECTOR_POOL_MAX_OVERFLOW = os.getenv('PGVECTOR_POOL_MAX_OVERFLOW', 0) -if PGVECTOR_POOL_MAX_OVERFLOW == "": +if PGVECTOR_POOL_MAX_OVERFLOW == '': PGVECTOR_POOL_MAX_OVERFLOW = 0 else: try: @@ -2319,9 +812,9 @@ else: except Exception: PGVECTOR_POOL_MAX_OVERFLOW = 0 -PGVECTOR_POOL_TIMEOUT = os.environ.get("PGVECTOR_POOL_TIMEOUT", 30) +PGVECTOR_POOL_TIMEOUT = os.getenv('PGVECTOR_POOL_TIMEOUT', 30) -if PGVECTOR_POOL_TIMEOUT == "": +if PGVECTOR_POOL_TIMEOUT == '': PGVECTOR_POOL_TIMEOUT = 30 else: try: @@ -2329,9 +822,9 @@ else: except Exception: PGVECTOR_POOL_TIMEOUT = 30 -PGVECTOR_POOL_RECYCLE = os.environ.get("PGVECTOR_POOL_RECYCLE", 3600) +PGVECTOR_POOL_RECYCLE = os.getenv('PGVECTOR_POOL_RECYCLE', 3600) -if PGVECTOR_POOL_RECYCLE == "": +if PGVECTOR_POOL_RECYCLE == '': PGVECTOR_POOL_RECYCLE = 3600 else: try: @@ -2339,13 +832,13 @@ else: except Exception: PGVECTOR_POOL_RECYCLE = 3600 -PGVECTOR_INDEX_METHOD = os.getenv("PGVECTOR_INDEX_METHOD", "").strip().lower() -if PGVECTOR_INDEX_METHOD not in ("ivfflat", "hnsw", ""): - PGVECTOR_INDEX_METHOD = "" +PGVECTOR_INDEX_METHOD = os.getenv('PGVECTOR_INDEX_METHOD', '').strip().lower() +if PGVECTOR_INDEX_METHOD not in ('ivfflat', 'hnsw', ''): + PGVECTOR_INDEX_METHOD = '' -PGVECTOR_HNSW_M = os.environ.get("PGVECTOR_HNSW_M", 16) +PGVECTOR_HNSW_M = os.getenv('PGVECTOR_HNSW_M', 16) -if PGVECTOR_HNSW_M == "": +if PGVECTOR_HNSW_M == '': PGVECTOR_HNSW_M = 16 else: try: @@ -2353,9 +846,9 @@ else: except Exception: PGVECTOR_HNSW_M = 16 -PGVECTOR_HNSW_EF_CONSTRUCTION = os.environ.get("PGVECTOR_HNSW_EF_CONSTRUCTION", 64) +PGVECTOR_HNSW_EF_CONSTRUCTION = os.getenv('PGVECTOR_HNSW_EF_CONSTRUCTION', 64) -if PGVECTOR_HNSW_EF_CONSTRUCTION == "": +if PGVECTOR_HNSW_EF_CONSTRUCTION == '': PGVECTOR_HNSW_EF_CONSTRUCTION = 64 else: try: @@ -2363,9 +856,9 @@ else: except Exception: PGVECTOR_HNSW_EF_CONSTRUCTION = 64 -PGVECTOR_IVFFLAT_LISTS = os.environ.get("PGVECTOR_IVFFLAT_LISTS", 100) +PGVECTOR_IVFFLAT_LISTS = os.getenv('PGVECTOR_IVFFLAT_LISTS', 100) -if PGVECTOR_IVFFLAT_LISTS == "": +if PGVECTOR_IVFFLAT_LISTS == '': PGVECTOR_IVFFLAT_LISTS = 100 else: try: @@ -2374,13 +867,11 @@ else: PGVECTOR_IVFFLAT_LISTS = 100 # openGauss -OPENGAUSS_DB_URL = os.environ.get("OPENGAUSS_DB_URL", DATABASE_URL) +OPENGAUSS_DB_URL = os.getenv('OPENGAUSS_DB_URL', DATABASE_URL) -OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int( - os.environ.get("OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH", "1536") -) +OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int(os.getenv('OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH', '1536')) -OPENGAUSS_POOL_SIZE = os.environ.get("OPENGAUSS_POOL_SIZE", None) +OPENGAUSS_POOL_SIZE = os.getenv('OPENGAUSS_POOL_SIZE', None) if OPENGAUSS_POOL_SIZE != None: try: @@ -2388,9 +879,9 @@ if OPENGAUSS_POOL_SIZE != None: except Exception: OPENGAUSS_POOL_SIZE = None -OPENGAUSS_POOL_MAX_OVERFLOW = os.environ.get("OPENGAUSS_POOL_MAX_OVERFLOW", 0) +OPENGAUSS_POOL_MAX_OVERFLOW = os.getenv('OPENGAUSS_POOL_MAX_OVERFLOW', 0) -if OPENGAUSS_POOL_MAX_OVERFLOW == "": +if OPENGAUSS_POOL_MAX_OVERFLOW == '': OPENGAUSS_POOL_MAX_OVERFLOW = 0 else: try: @@ -2398,9 +889,9 @@ else: except Exception: OPENGAUSS_POOL_MAX_OVERFLOW = 0 -OPENGAUSS_POOL_TIMEOUT = os.environ.get("OPENGAUSS_POOL_TIMEOUT", 30) +OPENGAUSS_POOL_TIMEOUT = os.getenv('OPENGAUSS_POOL_TIMEOUT', 30) -if OPENGAUSS_POOL_TIMEOUT == "": +if OPENGAUSS_POOL_TIMEOUT == '': OPENGAUSS_POOL_TIMEOUT = 30 else: try: @@ -2408,9 +899,9 @@ else: except Exception: OPENGAUSS_POOL_TIMEOUT = 30 -OPENGAUSS_POOL_RECYCLE = os.environ.get("OPENGAUSS_POOL_RECYCLE", 3600) +OPENGAUSS_POOL_RECYCLE = os.getenv('OPENGAUSS_POOL_RECYCLE', 3600) -if OPENGAUSS_POOL_RECYCLE == "": +if OPENGAUSS_POOL_RECYCLE == '': OPENGAUSS_POOL_RECYCLE = 3600 else: try: @@ -2419,43 +910,50 @@ else: OPENGAUSS_POOL_RECYCLE = 3600 # Pinecone -PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY", None) -PINECONE_ENVIRONMENT = os.environ.get("PINECONE_ENVIRONMENT", None) -PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME", "open-webui-index") -PINECONE_DIMENSION = int(os.getenv("PINECONE_DIMENSION", 1536)) # or 3072, 1024, 768 -PINECONE_METRIC = os.getenv("PINECONE_METRIC", "cosine") -PINECONE_CLOUD = os.getenv("PINECONE_CLOUD", "aws") # or "gcp" or "azure" +PINECONE_API_KEY = os.getenv('PINECONE_API_KEY', None) +PINECONE_ENVIRONMENT = os.getenv('PINECONE_ENVIRONMENT', None) +PINECONE_INDEX_NAME = os.getenv('PINECONE_INDEX_NAME', 'open-webui-index') +PINECONE_DIMENSION = int(os.getenv('PINECONE_DIMENSION', 1536)) # or 3072, 1024, 768 +PINECONE_METRIC = os.getenv('PINECONE_METRIC', 'cosine') +PINECONE_CLOUD = os.getenv('PINECONE_CLOUD', 'aws') # or "gcp" or "azure" # ORACLE23AI (Oracle23ai Vector Search) -ORACLE_DB_USE_WALLET = os.environ.get("ORACLE_DB_USE_WALLET", "false").lower() == "true" -ORACLE_DB_USER = os.environ.get("ORACLE_DB_USER", None) # -ORACLE_DB_PASSWORD = os.environ.get("ORACLE_DB_PASSWORD", None) # -ORACLE_DB_DSN = os.environ.get("ORACLE_DB_DSN", None) # -ORACLE_WALLET_DIR = os.environ.get("ORACLE_WALLET_DIR", None) -ORACLE_WALLET_PASSWORD = os.environ.get("ORACLE_WALLET_PASSWORD", None) -ORACLE_VECTOR_LENGTH = os.environ.get("ORACLE_VECTOR_LENGTH", 768) +ORACLE_DB_USE_WALLET = os.getenv('ORACLE_DB_USE_WALLET', 'false').lower() == 'true' +ORACLE_DB_USER = os.getenv('ORACLE_DB_USER', None) # +ORACLE_DB_PASSWORD = os.getenv('ORACLE_DB_PASSWORD', None) # +ORACLE_DB_DSN = os.getenv('ORACLE_DB_DSN', None) # +ORACLE_WALLET_DIR = os.getenv('ORACLE_WALLET_DIR', None) +ORACLE_WALLET_PASSWORD = os.getenv('ORACLE_WALLET_PASSWORD', None) +ORACLE_VECTOR_LENGTH = os.getenv('ORACLE_VECTOR_LENGTH', 768) -ORACLE_DB_POOL_MIN = int(os.environ.get("ORACLE_DB_POOL_MIN", 2)) -ORACLE_DB_POOL_MAX = int(os.environ.get("ORACLE_DB_POOL_MAX", 10)) -ORACLE_DB_POOL_INCREMENT = int(os.environ.get("ORACLE_DB_POOL_INCREMENT", 1)) +ORACLE_DB_POOL_MIN = int(os.getenv('ORACLE_DB_POOL_MIN', 2)) +ORACLE_DB_POOL_MAX = int(os.getenv('ORACLE_DB_POOL_MAX', 10)) +ORACLE_DB_POOL_INCREMENT = int(os.getenv('ORACLE_DB_POOL_INCREMENT', 1)) -if VECTOR_DB == "oracle23ai": +if VECTOR_DB == 'oracle23ai': if not ORACLE_DB_USER or not ORACLE_DB_PASSWORD or not ORACLE_DB_DSN: - raise ValueError( - "Oracle23ai requires setting ORACLE_DB_USER, ORACLE_DB_PASSWORD, and ORACLE_DB_DSN." - ) + raise ValueError('Oracle23ai requires setting ORACLE_DB_USER, ORACLE_DB_PASSWORD, and ORACLE_DB_DSN.') if ORACLE_DB_USE_WALLET and (not ORACLE_WALLET_DIR or not ORACLE_WALLET_PASSWORD): raise ValueError( - "Oracle23ai requires setting ORACLE_WALLET_DIR and ORACLE_WALLET_PASSWORD when using wallet authentication." + 'Oracle23ai requires setting ORACLE_WALLET_DIR and ORACLE_WALLET_PASSWORD when using wallet authentication.' ) -log.info(f"VECTOR_DB: {VECTOR_DB}") +log.info(f'VECTOR_DB: {VECTOR_DB}') # S3 Vector -S3_VECTOR_BUCKET_NAME = os.environ.get("S3_VECTOR_BUCKET_NAME", None) -S3_VECTOR_REGION = os.environ.get("S3_VECTOR_REGION", None) +S3_VECTOR_BUCKET_NAME = os.getenv('S3_VECTOR_BUCKET_NAME', None) +S3_VECTOR_REGION = os.getenv('S3_VECTOR_REGION', None) + +# Valkey Vector Store +VALKEY_URL = os.getenv('VALKEY_URL', '') +VALKEY_COLLECTION_PREFIX = os.getenv('VALKEY_COLLECTION_PREFIX', 'open_webui') +VALKEY_INDEX_TYPE = os.getenv('VALKEY_INDEX_TYPE', 'HNSW').upper() +VALKEY_DISTANCE_METRIC = os.getenv('VALKEY_DISTANCE_METRIC', 'COSINE').upper() +VALKEY_HNSW_M = int(os.getenv('VALKEY_HNSW_M', '16')) +VALKEY_HNSW_EF_CONSTRUCTION = int(os.getenv('VALKEY_HNSW_EF_CONSTRUCTION', '200')) +VALKEY_HNSW_EF_RUNTIME = int(os.getenv('VALKEY_HNSW_EF_RUNTIME', '10')) #################################### # Information Retrieval (RAG) @@ -2463,465 +961,460 @@ S3_VECTOR_REGION = os.environ.get("S3_VECTOR_REGION", None) # If configured, Google Drive will be available as an upload option. -ENABLE_GOOGLE_DRIVE_INTEGRATION = PersistentConfig( - "ENABLE_GOOGLE_DRIVE_INTEGRATION", - "google_drive.enable", - os.getenv("ENABLE_GOOGLE_DRIVE_INTEGRATION", "False").lower() == "true", +ENABLE_GOOGLE_DRIVE_INTEGRATION = ConfigVar( + 'ENABLE_GOOGLE_DRIVE_INTEGRATION', + 'google_drive.enable', + os.getenv('ENABLE_GOOGLE_DRIVE_INTEGRATION', 'False').lower() == 'true', ) -GOOGLE_DRIVE_CLIENT_ID = PersistentConfig( - "GOOGLE_DRIVE_CLIENT_ID", - "google_drive.client_id", - os.environ.get("GOOGLE_DRIVE_CLIENT_ID", ""), +GOOGLE_DRIVE_CLIENT_ID = ConfigVar( + 'GOOGLE_DRIVE_CLIENT_ID', + 'google_drive.client_id', + os.getenv('GOOGLE_DRIVE_CLIENT_ID', ''), ) -GOOGLE_DRIVE_API_KEY = PersistentConfig( - "GOOGLE_DRIVE_API_KEY", - "google_drive.api_key", - os.environ.get("GOOGLE_DRIVE_API_KEY", ""), +GOOGLE_DRIVE_API_KEY = ConfigVar( + 'GOOGLE_DRIVE_API_KEY', + 'google_drive.api_key', + os.getenv('GOOGLE_DRIVE_API_KEY', ''), ) -ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig( - "ENABLE_ONEDRIVE_INTEGRATION", - "onedrive.enable", - os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true", +ENABLE_ONEDRIVE_INTEGRATION = ConfigVar( + 'ENABLE_ONEDRIVE_INTEGRATION', + 'onedrive.enable', + os.getenv('ENABLE_ONEDRIVE_INTEGRATION', 'False').lower() == 'true', ) -ENABLE_ONEDRIVE_PERSONAL = ( - os.environ.get("ENABLE_ONEDRIVE_PERSONAL", "True").lower() == "true" +ONEDRIVE_CLIENT_ID = os.getenv('ONEDRIVE_CLIENT_ID', '') +ONEDRIVE_CLIENT_ID_PERSONAL = os.getenv('ONEDRIVE_CLIENT_ID_PERSONAL', ONEDRIVE_CLIENT_ID) +ONEDRIVE_CLIENT_ID_BUSINESS = os.getenv('ONEDRIVE_CLIENT_ID_BUSINESS', ONEDRIVE_CLIENT_ID) + +ENABLE_ONEDRIVE_PERSONAL = os.getenv('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true' and bool( + ONEDRIVE_CLIENT_ID_PERSONAL ) -ENABLE_ONEDRIVE_BUSINESS = ( - os.environ.get("ENABLE_ONEDRIVE_BUSINESS", "True").lower() == "true" +ENABLE_ONEDRIVE_BUSINESS = os.getenv('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true' and bool( + ONEDRIVE_CLIENT_ID_BUSINESS ) -ONEDRIVE_CLIENT_ID = os.environ.get("ONEDRIVE_CLIENT_ID", "") -ONEDRIVE_CLIENT_ID_PERSONAL = os.environ.get( - "ONEDRIVE_CLIENT_ID_PERSONAL", ONEDRIVE_CLIENT_ID -) -ONEDRIVE_CLIENT_ID_BUSINESS = os.environ.get( - "ONEDRIVE_CLIENT_ID_BUSINESS", ONEDRIVE_CLIENT_ID +ONEDRIVE_SHAREPOINT_URL = ConfigVar( + 'ONEDRIVE_SHAREPOINT_URL', + 'onedrive.sharepoint_url', + os.getenv('ONEDRIVE_SHAREPOINT_URL', ''), ) -ONEDRIVE_SHAREPOINT_URL = PersistentConfig( - "ONEDRIVE_SHAREPOINT_URL", - "onedrive.sharepoint_url", - os.environ.get("ONEDRIVE_SHAREPOINT_URL", ""), -) - -ONEDRIVE_SHAREPOINT_TENANT_ID = PersistentConfig( - "ONEDRIVE_SHAREPOINT_TENANT_ID", - "onedrive.sharepoint_tenant_id", - os.environ.get("ONEDRIVE_SHAREPOINT_TENANT_ID", ""), +ONEDRIVE_SHAREPOINT_TENANT_ID = ConfigVar( + 'ONEDRIVE_SHAREPOINT_TENANT_ID', + 'onedrive.sharepoint_tenant_id', + os.getenv('ONEDRIVE_SHAREPOINT_TENANT_ID', ''), ) # RAG Content Extraction -CONTENT_EXTRACTION_ENGINE = PersistentConfig( - "CONTENT_EXTRACTION_ENGINE", - "rag.CONTENT_EXTRACTION_ENGINE", - os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(), +CONTENT_EXTRACTION_ENGINE = ConfigVar( + 'CONTENT_EXTRACTION_ENGINE', + 'rag.CONTENT_EXTRACTION_ENGINE', + os.getenv('CONTENT_EXTRACTION_ENGINE', '').lower(), ) -DATALAB_MARKER_API_KEY = PersistentConfig( - "DATALAB_MARKER_API_KEY", - "rag.datalab_marker_api_key", - os.environ.get("DATALAB_MARKER_API_KEY", ""), +DATALAB_MARKER_API_KEY = ConfigVar( + 'DATALAB_MARKER_API_KEY', + 'rag.datalab_marker_api_key', + os.getenv('DATALAB_MARKER_API_KEY', ''), ) -DATALAB_MARKER_API_BASE_URL = PersistentConfig( - "DATALAB_MARKER_API_BASE_URL", - "rag.datalab_marker_api_base_url", - os.environ.get("DATALAB_MARKER_API_BASE_URL", ""), +DATALAB_MARKER_API_BASE_URL = ConfigVar( + 'DATALAB_MARKER_API_BASE_URL', + 'rag.datalab_marker_api_base_url', + os.getenv('DATALAB_MARKER_API_BASE_URL', ''), ) -DATALAB_MARKER_ADDITIONAL_CONFIG = PersistentConfig( - "DATALAB_MARKER_ADDITIONAL_CONFIG", - "rag.datalab_marker_additional_config", - os.environ.get("DATALAB_MARKER_ADDITIONAL_CONFIG", ""), +DATALAB_MARKER_ADDITIONAL_CONFIG = ConfigVar( + 'DATALAB_MARKER_ADDITIONAL_CONFIG', + 'rag.datalab_marker_additional_config', + os.getenv('DATALAB_MARKER_ADDITIONAL_CONFIG', ''), ) -DATALAB_MARKER_USE_LLM = PersistentConfig( - "DATALAB_MARKER_USE_LLM", - "rag.DATALAB_MARKER_USE_LLM", - os.environ.get("DATALAB_MARKER_USE_LLM", "false").lower() == "true", +DATALAB_MARKER_USE_LLM = ConfigVar( + 'DATALAB_MARKER_USE_LLM', + 'rag.DATALAB_MARKER_USE_LLM', + os.getenv('DATALAB_MARKER_USE_LLM', 'false').lower() == 'true', ) -DATALAB_MARKER_SKIP_CACHE = PersistentConfig( - "DATALAB_MARKER_SKIP_CACHE", - "rag.datalab_marker_skip_cache", - os.environ.get("DATALAB_MARKER_SKIP_CACHE", "false").lower() == "true", +DATALAB_MARKER_SKIP_CACHE = ConfigVar( + 'DATALAB_MARKER_SKIP_CACHE', + 'rag.datalab_marker_skip_cache', + os.getenv('DATALAB_MARKER_SKIP_CACHE', 'false').lower() == 'true', ) -DATALAB_MARKER_FORCE_OCR = PersistentConfig( - "DATALAB_MARKER_FORCE_OCR", - "rag.datalab_marker_force_ocr", - os.environ.get("DATALAB_MARKER_FORCE_OCR", "false").lower() == "true", +DATALAB_MARKER_FORCE_OCR = ConfigVar( + 'DATALAB_MARKER_FORCE_OCR', + 'rag.datalab_marker_force_ocr', + os.getenv('DATALAB_MARKER_FORCE_OCR', 'false').lower() == 'true', ) -DATALAB_MARKER_PAGINATE = PersistentConfig( - "DATALAB_MARKER_PAGINATE", - "rag.datalab_marker_paginate", - os.environ.get("DATALAB_MARKER_PAGINATE", "false").lower() == "true", +DATALAB_MARKER_PAGINATE = ConfigVar( + 'DATALAB_MARKER_PAGINATE', + 'rag.datalab_marker_paginate', + os.getenv('DATALAB_MARKER_PAGINATE', 'false').lower() == 'true', ) -DATALAB_MARKER_STRIP_EXISTING_OCR = PersistentConfig( - "DATALAB_MARKER_STRIP_EXISTING_OCR", - "rag.datalab_marker_strip_existing_ocr", - os.environ.get("DATALAB_MARKER_STRIP_EXISTING_OCR", "false").lower() == "true", +DATALAB_MARKER_STRIP_EXISTING_OCR = ConfigVar( + 'DATALAB_MARKER_STRIP_EXISTING_OCR', + 'rag.datalab_marker_strip_existing_ocr', + os.getenv('DATALAB_MARKER_STRIP_EXISTING_OCR', 'false').lower() == 'true', ) -DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = PersistentConfig( - "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", - "rag.datalab_marker_disable_image_extraction", - os.environ.get("DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", "false").lower() - == "true", +DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = ConfigVar( + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', + 'rag.datalab_marker_disable_image_extraction', + os.getenv('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', 'false').lower() == 'true', ) -DATALAB_MARKER_FORMAT_LINES = PersistentConfig( - "DATALAB_MARKER_FORMAT_LINES", - "rag.datalab_marker_format_lines", - os.environ.get("DATALAB_MARKER_FORMAT_LINES", "false").lower() == "true", +DATALAB_MARKER_FORMAT_LINES = ConfigVar( + 'DATALAB_MARKER_FORMAT_LINES', + 'rag.datalab_marker_format_lines', + os.getenv('DATALAB_MARKER_FORMAT_LINES', 'false').lower() == 'true', ) -DATALAB_MARKER_OUTPUT_FORMAT = PersistentConfig( - "DATALAB_MARKER_OUTPUT_FORMAT", - "rag.datalab_marker_output_format", - os.environ.get("DATALAB_MARKER_OUTPUT_FORMAT", "markdown"), +DATALAB_MARKER_OUTPUT_FORMAT = ConfigVar( + 'DATALAB_MARKER_OUTPUT_FORMAT', + 'rag.datalab_marker_output_format', + os.getenv('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown'), ) -MINERU_API_MODE = PersistentConfig( - "MINERU_API_MODE", - "rag.mineru_api_mode", - os.environ.get("MINERU_API_MODE", "local"), # "local" or "cloud" +MINERU_API_MODE = ConfigVar( + 'MINERU_API_MODE', + 'rag.mineru_api_mode', + os.getenv('MINERU_API_MODE', 'local'), # "local" or "cloud" ) -MINERU_API_URL = PersistentConfig( - "MINERU_API_URL", - "rag.mineru_api_url", - os.environ.get("MINERU_API_URL", "http://localhost:8000"), +MINERU_API_URL = ConfigVar( + 'MINERU_API_URL', + 'rag.mineru_api_url', + os.getenv('MINERU_API_URL', 'http://localhost:8000'), ) -MINERU_API_TIMEOUT = PersistentConfig( - "MINERU_API_TIMEOUT", - "rag.mineru_api_timeout", - os.environ.get("MINERU_API_TIMEOUT", "300"), +MINERU_API_TIMEOUT = ConfigVar( + 'MINERU_API_TIMEOUT', + 'rag.mineru_api_timeout', + os.getenv('MINERU_API_TIMEOUT', '300'), ) -MINERU_API_KEY = PersistentConfig( - "MINERU_API_KEY", - "rag.mineru_api_key", - os.environ.get("MINERU_API_KEY", ""), +MINERU_API_KEY = ConfigVar( + 'MINERU_API_KEY', + 'rag.mineru_api_key', + os.getenv('MINERU_API_KEY', ''), ) -mineru_params = os.getenv("MINERU_PARAMS", "") +mineru_params = os.getenv('MINERU_PARAMS', '') try: mineru_params = json.loads(mineru_params) except json.JSONDecodeError: mineru_params = {} -MINERU_PARAMS = PersistentConfig( - "MINERU_PARAMS", - "rag.mineru_params", +MINERU_PARAMS = ConfigVar( + 'MINERU_PARAMS', + 'rag.mineru_params', mineru_params, ) -EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig( - "EXTERNAL_DOCUMENT_LOADER_URL", - "rag.external_document_loader_url", - os.environ.get("EXTERNAL_DOCUMENT_LOADER_URL", ""), +MINERU_FILE_EXTENSIONS = ConfigVar( + 'MINERU_FILE_EXTENSIONS', + 'rag.mineru_file_extensions', + [ext.strip() for ext in os.getenv('MINERU_FILE_EXTENSIONS', 'pdf').split(',') if ext.strip()], ) -EXTERNAL_DOCUMENT_LOADER_API_KEY = PersistentConfig( - "EXTERNAL_DOCUMENT_LOADER_API_KEY", - "rag.external_document_loader_api_key", - os.environ.get("EXTERNAL_DOCUMENT_LOADER_API_KEY", ""), +EXTERNAL_DOCUMENT_LOADER_URL = ConfigVar( + 'EXTERNAL_DOCUMENT_LOADER_URL', + 'rag.external_document_loader_url', + os.getenv('EXTERNAL_DOCUMENT_LOADER_URL', ''), ) -TIKA_SERVER_URL = PersistentConfig( - "TIKA_SERVER_URL", - "rag.tika_server_url", - os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment +EXTERNAL_DOCUMENT_LOADER_API_KEY = ConfigVar( + 'EXTERNAL_DOCUMENT_LOADER_API_KEY', + 'rag.external_document_loader_api_key', + os.getenv('EXTERNAL_DOCUMENT_LOADER_API_KEY', ''), ) -DOCLING_SERVER_URL = PersistentConfig( - "DOCLING_SERVER_URL", - "rag.docling_server_url", - os.getenv("DOCLING_SERVER_URL", "http://docling:5001"), +TIKA_SERVER_URL = ConfigVar( + 'TIKA_SERVER_URL', + 'rag.tika_server_url', + os.getenv('TIKA_SERVER_URL', 'http://tika:9998'), # Default for sidecar deployment ) -DOCLING_API_KEY = PersistentConfig( - "DOCLING_API_KEY", - "rag.docling_api_key", - os.getenv("DOCLING_API_KEY", ""), +DOCLING_SERVER_URL = ConfigVar( + 'DOCLING_SERVER_URL', + 'rag.docling_server_url', + os.getenv('DOCLING_SERVER_URL', 'http://docling:5001'), ) -docling_params = os.getenv("DOCLING_PARAMS", "") +DOCLING_API_KEY = ConfigVar( + 'DOCLING_API_KEY', + 'rag.docling_api_key', + os.getenv('DOCLING_API_KEY', ''), +) + +docling_params = os.getenv('DOCLING_PARAMS', '') try: docling_params = json.loads(docling_params) except json.JSONDecodeError: docling_params = {} -DOCLING_PARAMS = PersistentConfig( - "DOCLING_PARAMS", - "rag.docling_params", +DOCLING_PARAMS = ConfigVar( + 'DOCLING_PARAMS', + 'rag.docling_params', docling_params, ) -DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig( - "DOCUMENT_INTELLIGENCE_ENDPOINT", - "rag.document_intelligence_endpoint", - os.getenv("DOCUMENT_INTELLIGENCE_ENDPOINT", ""), +DOCUMENT_INTELLIGENCE_ENDPOINT = ConfigVar( + 'DOCUMENT_INTELLIGENCE_ENDPOINT', + 'rag.document_intelligence_endpoint', + os.getenv('DOCUMENT_INTELLIGENCE_ENDPOINT', ''), ) -DOCUMENT_INTELLIGENCE_KEY = PersistentConfig( - "DOCUMENT_INTELLIGENCE_KEY", - "rag.document_intelligence_key", - os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""), +DOCUMENT_INTELLIGENCE_KEY = ConfigVar( + 'DOCUMENT_INTELLIGENCE_KEY', + 'rag.document_intelligence_key', + os.getenv('DOCUMENT_INTELLIGENCE_KEY', ''), ) -DOCUMENT_INTELLIGENCE_MODEL = PersistentConfig( - "DOCUMENT_INTELLIGENCE_MODEL", - "rag.document_intelligence_model", - os.getenv("DOCUMENT_INTELLIGENCE_MODEL", "prebuilt-layout"), +DOCUMENT_INTELLIGENCE_MODEL = ConfigVar( + 'DOCUMENT_INTELLIGENCE_MODEL', + 'rag.document_intelligence_model', + os.getenv('DOCUMENT_INTELLIGENCE_MODEL', 'prebuilt-layout'), ) -MISTRAL_OCR_API_BASE_URL = PersistentConfig( - "MISTRAL_OCR_API_BASE_URL", - "rag.MISTRAL_OCR_API_BASE_URL", - os.getenv("MISTRAL_OCR_API_BASE_URL", "https://api.mistral.ai/v1"), +MISTRAL_OCR_API_BASE_URL = ConfigVar( + 'MISTRAL_OCR_API_BASE_URL', + 'rag.MISTRAL_OCR_API_BASE_URL', + os.getenv('MISTRAL_OCR_API_BASE_URL', 'https://api.mistral.ai/v1'), ) -MISTRAL_OCR_API_KEY = PersistentConfig( - "MISTRAL_OCR_API_KEY", - "rag.mistral_ocr_api_key", - os.getenv("MISTRAL_OCR_API_KEY", ""), +MISTRAL_OCR_API_KEY = ConfigVar( + 'MISTRAL_OCR_API_KEY', + 'rag.mistral_ocr_api_key', + os.getenv('MISTRAL_OCR_API_KEY', ''), ) -BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig( - "BYPASS_EMBEDDING_AND_RETRIEVAL", - "rag.bypass_embedding_and_retrieval", - os.environ.get("BYPASS_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true", +PADDLEOCR_VL_BASE_URL = ConfigVar( + 'PADDLEOCR_VL_BASE_URL', + 'rag.paddleocr_vl_base_url', + os.getenv('PADDLEOCR_VL_BASE_URL', 'http://localhost:8080'), +) + +PADDLEOCR_VL_TOKEN = ConfigVar( + 'PADDLEOCR_VL_TOKEN', + 'rag.paddleocr_vl_token', + os.getenv('PADDLEOCR_VL_TOKEN', ''), +) + +BYPASS_EMBEDDING_AND_RETRIEVAL = ConfigVar( + 'BYPASS_EMBEDDING_AND_RETRIEVAL', + 'rag.bypass_embedding_and_retrieval', + os.getenv('BYPASS_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true', ) -RAG_TOP_K = PersistentConfig( - "RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3")) +RAG_TOP_K = ConfigVar('RAG_TOP_K', 'rag.top_k', int(os.getenv('RAG_TOP_K', '3'))) +RAG_TOP_K_RERANKER = ConfigVar( + 'RAG_TOP_K_RERANKER', + 'rag.top_k_reranker', + int(os.getenv('RAG_TOP_K_RERANKER', '3')), ) -RAG_TOP_K_RERANKER = PersistentConfig( - "RAG_TOP_K_RERANKER", - "rag.top_k_reranker", - int(os.environ.get("RAG_TOP_K_RERANKER", "3")), +RAG_RELEVANCE_THRESHOLD = ConfigVar( + 'RAG_RELEVANCE_THRESHOLD', + 'rag.relevance_threshold', + float(os.getenv('RAG_RELEVANCE_THRESHOLD', '0.0')), ) -RAG_RELEVANCE_THRESHOLD = PersistentConfig( - "RAG_RELEVANCE_THRESHOLD", - "rag.relevance_threshold", - float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")), -) -RAG_HYBRID_BM25_WEIGHT = PersistentConfig( - "RAG_HYBRID_BM25_WEIGHT", - "rag.hybrid_bm25_weight", - float(os.environ.get("RAG_HYBRID_BM25_WEIGHT", "0.5")), +RAG_HYBRID_BM25_WEIGHT = ConfigVar( + 'RAG_HYBRID_BM25_WEIGHT', + 'rag.hybrid_bm25_weight', + float(os.getenv('RAG_HYBRID_BM25_WEIGHT', '0.5')), ) -ENABLE_RAG_HYBRID_SEARCH = PersistentConfig( - "ENABLE_RAG_HYBRID_SEARCH", - "rag.enable_hybrid_search", - os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true", +ENABLE_RAG_HYBRID_SEARCH = ConfigVar( + 'ENABLE_RAG_HYBRID_SEARCH', + 'rag.enable_hybrid_search', + os.getenv('ENABLE_RAG_HYBRID_SEARCH', '').lower() == 'true', ) -ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = PersistentConfig( - "ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS", - "rag.enable_hybrid_search_enriched_texts", - os.environ.get("ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS", "False").lower() - == "true", +ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ConfigVar( + 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', + 'rag.enable_hybrid_search_enriched_texts', + os.getenv('ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', 'False').lower() == 'true', ) -RAG_FULL_CONTEXT = PersistentConfig( - "RAG_FULL_CONTEXT", - "rag.full_context", - os.getenv("RAG_FULL_CONTEXT", "False").lower() == "true", +RAG_FULL_CONTEXT = ConfigVar( + 'RAG_FULL_CONTEXT', + 'rag.full_context', + os.getenv('RAG_FULL_CONTEXT', 'False').lower() == 'true', ) -RAG_FILE_MAX_COUNT = PersistentConfig( - "RAG_FILE_MAX_COUNT", - "rag.file.max_count", - ( - int(os.environ.get("RAG_FILE_MAX_COUNT")) - if os.environ.get("RAG_FILE_MAX_COUNT") - else None - ), +RAG_FILE_MAX_COUNT = ConfigVar( + 'RAG_FILE_MAX_COUNT', + 'rag.file.max_count', + (int(os.getenv('RAG_FILE_MAX_COUNT')) if os.getenv('RAG_FILE_MAX_COUNT') else None), ) -RAG_FILE_MAX_SIZE = PersistentConfig( - "RAG_FILE_MAX_SIZE", - "rag.file.max_size", - ( - int(os.environ.get("RAG_FILE_MAX_SIZE")) - if os.environ.get("RAG_FILE_MAX_SIZE") - else None - ), +RAG_FILE_MAX_SIZE = ConfigVar( + 'RAG_FILE_MAX_SIZE', + 'rag.file.max_size', + (int(os.getenv('RAG_FILE_MAX_SIZE')) if os.getenv('RAG_FILE_MAX_SIZE') else None), ) -FILE_IMAGE_COMPRESSION_WIDTH = PersistentConfig( - "FILE_IMAGE_COMPRESSION_WIDTH", - "file.image_compression_width", - ( - int(os.environ.get("FILE_IMAGE_COMPRESSION_WIDTH")) - if os.environ.get("FILE_IMAGE_COMPRESSION_WIDTH") - else None - ), +FILE_IMAGE_COMPRESSION_WIDTH = ConfigVar( + 'FILE_IMAGE_COMPRESSION_WIDTH', + 'file.image_compression_width', + (int(os.getenv('FILE_IMAGE_COMPRESSION_WIDTH')) if os.getenv('FILE_IMAGE_COMPRESSION_WIDTH') else None), ) -FILE_IMAGE_COMPRESSION_HEIGHT = PersistentConfig( - "FILE_IMAGE_COMPRESSION_HEIGHT", - "file.image_compression_height", - ( - int(os.environ.get("FILE_IMAGE_COMPRESSION_HEIGHT")) - if os.environ.get("FILE_IMAGE_COMPRESSION_HEIGHT") - else None - ), +FILE_IMAGE_COMPRESSION_HEIGHT = ConfigVar( + 'FILE_IMAGE_COMPRESSION_HEIGHT', + 'file.image_compression_height', + (int(os.getenv('FILE_IMAGE_COMPRESSION_HEIGHT')) if os.getenv('FILE_IMAGE_COMPRESSION_HEIGHT') else None), ) -RAG_ALLOWED_FILE_EXTENSIONS = PersistentConfig( - "RAG_ALLOWED_FILE_EXTENSIONS", - "rag.file.allowed_extensions", - [ - ext.strip() - for ext in os.environ.get("RAG_ALLOWED_FILE_EXTENSIONS", "").split(",") - if ext.strip() - ], +RAG_ALLOWED_FILE_EXTENSIONS = ConfigVar( + 'RAG_ALLOWED_FILE_EXTENSIONS', + 'rag.file.allowed_extensions', + [ext.strip() for ext in os.getenv('RAG_ALLOWED_FILE_EXTENSIONS', '').split(',') if ext.strip()], ) -RAG_EMBEDDING_ENGINE = PersistentConfig( - "RAG_EMBEDDING_ENGINE", - "rag.embedding_engine", - os.environ.get("RAG_EMBEDDING_ENGINE", ""), +RAG_EMBEDDING_ENGINE = ConfigVar( + 'RAG_EMBEDDING_ENGINE', + 'rag.embedding_engine', + os.getenv('RAG_EMBEDDING_ENGINE', ''), ) -PDF_EXTRACT_IMAGES = PersistentConfig( - "PDF_EXTRACT_IMAGES", - "rag.pdf_extract_images", - os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true", +PDF_EXTRACT_IMAGES = ConfigVar( + 'PDF_EXTRACT_IMAGES', + 'rag.pdf_extract_images', + os.getenv('PDF_EXTRACT_IMAGES', 'False').lower() == 'true', ) -RAG_EMBEDDING_MODEL = PersistentConfig( - "RAG_EMBEDDING_MODEL", - "rag.embedding_model", - os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"), +PDF_LOADER_MODE = ConfigVar( + 'PDF_LOADER_MODE', + 'rag.pdf_loader_mode', + os.getenv('PDF_LOADER_MODE', 'page'), ) -log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}") + +RAG_EMBEDDING_MODEL = ConfigVar( + 'RAG_EMBEDDING_MODEL', + 'rag.embedding_model', + os.getenv('RAG_EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2'), +) +log.info(f'Embedding model set: {RAG_EMBEDDING_MODEL.value}') RAG_EMBEDDING_MODEL_AUTO_UPDATE = ( - not OFFLINE_MODE - and os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "True").lower() == "true" + not OFFLINE_MODE and os.getenv('RAG_EMBEDDING_MODEL_AUTO_UPDATE', 'True').lower() == 'true' ) -RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = ( - os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true" +RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = os.getenv('RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true' + +RAG_EMBEDDING_BATCH_SIZE = ConfigVar( + 'RAG_EMBEDDING_BATCH_SIZE', + 'rag.embedding_batch_size', + int(os.getenv('RAG_EMBEDDING_BATCH_SIZE') or os.getenv('RAG_EMBEDDING_OPENAI_BATCH_SIZE', '1')), ) -RAG_EMBEDDING_BATCH_SIZE = PersistentConfig( - "RAG_EMBEDDING_BATCH_SIZE", - "rag.embedding_batch_size", - int( - os.environ.get("RAG_EMBEDDING_BATCH_SIZE") - or os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1") - ), +ENABLE_ASYNC_EMBEDDING = ConfigVar( + 'ENABLE_ASYNC_EMBEDDING', + 'rag.enable_async_embedding', + os.getenv('ENABLE_ASYNC_EMBEDDING', 'True').lower() == 'true', ) -ENABLE_ASYNC_EMBEDDING = PersistentConfig( - "ENABLE_ASYNC_EMBEDDING", - "rag.enable_async_embedding", - os.environ.get("ENABLE_ASYNC_EMBEDDING", "True").lower() == "true", +RAG_EMBEDDING_CONCURRENT_REQUESTS = ConfigVar( + 'RAG_EMBEDDING_CONCURRENT_REQUESTS', + 'rag.embedding_concurrent_requests', + int(os.getenv('RAG_EMBEDDING_CONCURRENT_REQUESTS', '0')), ) -RAG_EMBEDDING_QUERY_PREFIX = os.environ.get("RAG_EMBEDDING_QUERY_PREFIX", None) +RAG_EMBEDDING_QUERY_PREFIX = os.getenv('RAG_EMBEDDING_QUERY_PREFIX', None) -RAG_EMBEDDING_CONTENT_PREFIX = os.environ.get("RAG_EMBEDDING_CONTENT_PREFIX", None) +RAG_EMBEDDING_CONTENT_PREFIX = os.getenv('RAG_EMBEDDING_CONTENT_PREFIX', None) -RAG_EMBEDDING_PREFIX_FIELD_NAME = os.environ.get( - "RAG_EMBEDDING_PREFIX_FIELD_NAME", None +RAG_EMBEDDING_PREFIX_FIELD_NAME = os.getenv('RAG_EMBEDDING_PREFIX_FIELD_NAME', None) + +RAG_RERANKING_ENGINE = ConfigVar( + 'RAG_RERANKING_ENGINE', + 'rag.reranking_engine', + os.getenv('RAG_RERANKING_ENGINE', ''), ) -RAG_RERANKING_ENGINE = PersistentConfig( - "RAG_RERANKING_ENGINE", - "rag.reranking_engine", - os.environ.get("RAG_RERANKING_ENGINE", ""), +RAG_RERANKING_MODEL = ConfigVar( + 'RAG_RERANKING_MODEL', + 'rag.reranking_model', + os.getenv('RAG_RERANKING_MODEL', ''), ) - -RAG_RERANKING_MODEL = PersistentConfig( - "RAG_RERANKING_MODEL", - "rag.reranking_model", - os.environ.get("RAG_RERANKING_MODEL", ""), -) -if RAG_RERANKING_MODEL.value != "": - log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}") +if RAG_RERANKING_MODEL.value != '': + log.info(f'Reranking model set: {RAG_RERANKING_MODEL.value}') RAG_RERANKING_MODEL_AUTO_UPDATE = ( - not OFFLINE_MODE - and os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "True").lower() == "true" + not OFFLINE_MODE and os.getenv('RAG_RERANKING_MODEL_AUTO_UPDATE', 'True').lower() == 'true' ) -RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = ( - os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true" +RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = os.getenv('RAG_RERANKING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true' + +RAG_RERANKING_BATCH_SIZE = ConfigVar( + 'RAG_RERANKING_BATCH_SIZE', + 'rag.reranking_batch_size', + int(os.getenv('RAG_RERANKING_BATCH_SIZE', '32')), ) -RAG_EXTERNAL_RERANKER_URL = PersistentConfig( - "RAG_EXTERNAL_RERANKER_URL", - "rag.external_reranker_url", - os.environ.get("RAG_EXTERNAL_RERANKER_URL", ""), +RAG_EXTERNAL_RERANKER_URL = ConfigVar( + 'RAG_EXTERNAL_RERANKER_URL', + 'rag.external_reranker_url', + os.getenv('RAG_EXTERNAL_RERANKER_URL', ''), ) -RAG_EXTERNAL_RERANKER_API_KEY = PersistentConfig( - "RAG_EXTERNAL_RERANKER_API_KEY", - "rag.external_reranker_api_key", - os.environ.get("RAG_EXTERNAL_RERANKER_API_KEY", ""), +RAG_EXTERNAL_RERANKER_API_KEY = ConfigVar( + 'RAG_EXTERNAL_RERANKER_API_KEY', + 'rag.external_reranker_api_key', + os.getenv('RAG_EXTERNAL_RERANKER_API_KEY', ''), ) -RAG_EXTERNAL_RERANKER_TIMEOUT = PersistentConfig( - "RAG_EXTERNAL_RERANKER_TIMEOUT", - "rag.external_reranker_timeout", - os.environ.get("RAG_EXTERNAL_RERANKER_TIMEOUT", ""), +RAG_EXTERNAL_RERANKER_TIMEOUT = ConfigVar( + 'RAG_EXTERNAL_RERANKER_TIMEOUT', + 'rag.external_reranker_timeout', + os.getenv('RAG_EXTERNAL_RERANKER_TIMEOUT', ''), ) -RAG_TEXT_SPLITTER = PersistentConfig( - "RAG_TEXT_SPLITTER", - "rag.text_splitter", - os.environ.get("RAG_TEXT_SPLITTER", ""), +RAG_TEXT_SPLITTER = ConfigVar( + 'RAG_TEXT_SPLITTER', + 'rag.text_splitter', + os.getenv('RAG_TEXT_SPLITTER', ''), ) -ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = PersistentConfig( - "ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER", - "rag.enable_markdown_header_text_splitter", - os.environ.get("ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER", "True").lower() == "true", +ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ConfigVar( + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', + 'rag.enable_markdown_header_text_splitter', + os.getenv('ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', 'True').lower() == 'true', ) -TIKTOKEN_CACHE_DIR = os.environ.get("TIKTOKEN_CACHE_DIR", f"{CACHE_DIR}/tiktoken") -TIKTOKEN_ENCODING_NAME = PersistentConfig( - "TIKTOKEN_ENCODING_NAME", - "rag.tiktoken_encoding_name", - os.environ.get("TIKTOKEN_ENCODING_NAME", "cl100k_base"), +TIKTOKEN_CACHE_DIR = os.getenv('TIKTOKEN_CACHE_DIR', f'{CACHE_DIR}/tiktoken') +TIKTOKEN_ENCODING_NAME = ConfigVar( + 'TIKTOKEN_ENCODING_NAME', + 'rag.tiktoken_encoding_name', + os.getenv('TIKTOKEN_ENCODING_NAME', 'cl100k_base'), ) -CHUNK_SIZE = PersistentConfig( - "CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000")) +CHUNK_SIZE = ConfigVar('CHUNK_SIZE', 'rag.chunk_size', int(os.getenv('CHUNK_SIZE', '1000'))) + +CHUNK_MIN_SIZE_TARGET = ConfigVar( + 'CHUNK_MIN_SIZE_TARGET', + 'rag.chunk_min_size_target', + int(os.getenv('CHUNK_MIN_SIZE_TARGET', '0')), ) -CHUNK_MIN_SIZE_TARGET = PersistentConfig( - "CHUNK_MIN_SIZE_TARGET", - "rag.chunk_min_size_target", - int(os.environ.get("CHUNK_MIN_SIZE_TARGET", "0")), -) - -CHUNK_OVERLAP = PersistentConfig( - "CHUNK_OVERLAP", - "rag.chunk_overlap", - int(os.environ.get("CHUNK_OVERLAP", "100")), +CHUNK_OVERLAP = ConfigVar( + 'CHUNK_OVERLAP', + 'rag.chunk_overlap', + int(os.getenv('CHUNK_OVERLAP', '100')), ) DEFAULT_RAG_TEMPLATE = """### Task: @@ -2950,86 +1443,82 @@ Provide a clear and direct response to the user's query, including inline citati """ -RAG_TEMPLATE = PersistentConfig( - "RAG_TEMPLATE", - "rag.template", - os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE), +RAG_TEMPLATE = ConfigVar( + 'RAG_TEMPLATE', + 'rag.template', + os.getenv('RAG_TEMPLATE', DEFAULT_RAG_TEMPLATE), ) -RAG_OPENAI_API_BASE_URL = PersistentConfig( - "RAG_OPENAI_API_BASE_URL", - "rag.openai_api_base_url", - os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +RAG_OPENAI_API_BASE_URL = ConfigVar( + 'RAG_OPENAI_API_BASE_URL', + 'rag.openai_api_base_url', + os.getenv('RAG_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), ) -RAG_OPENAI_API_KEY = PersistentConfig( - "RAG_OPENAI_API_KEY", - "rag.openai_api_key", - os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY), +RAG_OPENAI_API_KEY = ConfigVar( + 'RAG_OPENAI_API_KEY', + 'rag.openai_api_key', + os.getenv('RAG_OPENAI_API_KEY', OPENAI_API_KEY), ) -RAG_AZURE_OPENAI_BASE_URL = PersistentConfig( - "RAG_AZURE_OPENAI_BASE_URL", - "rag.azure_openai.base_url", - os.getenv("RAG_AZURE_OPENAI_BASE_URL", ""), +RAG_AZURE_OPENAI_BASE_URL = ConfigVar( + 'RAG_AZURE_OPENAI_BASE_URL', + 'rag.azure_openai.base_url', + os.getenv('RAG_AZURE_OPENAI_BASE_URL', ''), ) -RAG_AZURE_OPENAI_API_KEY = PersistentConfig( - "RAG_AZURE_OPENAI_API_KEY", - "rag.azure_openai.api_key", - os.getenv("RAG_AZURE_OPENAI_API_KEY", ""), +RAG_AZURE_OPENAI_API_KEY = ConfigVar( + 'RAG_AZURE_OPENAI_API_KEY', + 'rag.azure_openai.api_key', + os.getenv('RAG_AZURE_OPENAI_API_KEY', ''), ) -RAG_AZURE_OPENAI_API_VERSION = PersistentConfig( - "RAG_AZURE_OPENAI_API_VERSION", - "rag.azure_openai.api_version", - os.getenv("RAG_AZURE_OPENAI_API_VERSION", ""), +RAG_AZURE_OPENAI_API_VERSION = ConfigVar( + 'RAG_AZURE_OPENAI_API_VERSION', + 'rag.azure_openai.api_version', + os.getenv('RAG_AZURE_OPENAI_API_VERSION', ''), ) -RAG_OLLAMA_BASE_URL = PersistentConfig( - "RAG_OLLAMA_BASE_URL", - "rag.ollama.url", - os.getenv("RAG_OLLAMA_BASE_URL", OLLAMA_BASE_URL), +RAG_OLLAMA_BASE_URL = ConfigVar( + 'RAG_OLLAMA_BASE_URL', + 'rag.ollama.url', + os.getenv('RAG_OLLAMA_BASE_URL', OLLAMA_BASE_URL), ) -RAG_OLLAMA_API_KEY = PersistentConfig( - "RAG_OLLAMA_API_KEY", - "rag.ollama.key", - os.getenv("RAG_OLLAMA_API_KEY", ""), +RAG_OLLAMA_API_KEY = ConfigVar( + 'RAG_OLLAMA_API_KEY', + 'rag.ollama.key', + os.getenv('RAG_OLLAMA_API_KEY', ''), ) -ENABLE_RAG_LOCAL_WEB_FETCH = ( - os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true" -) +ENABLE_RAG_LOCAL_WEB_FETCH = os.getenv('ENABLE_RAG_LOCAL_WEB_FETCH', 'False').lower() == 'true' DEFAULT_WEB_FETCH_FILTER_LIST = [ - "!169.254.169.254", - "!fd00:ec2::254", - "!metadata.google.internal", - "!metadata.azure.com", - "!100.100.100.200", + '!169.254.169.254', + '!fd00:ec2::254', + '!metadata.google.internal', + '!metadata.azure.com', + '!100.100.100.200', ] -web_fetch_filter_list = os.getenv("WEB_FETCH_FILTER_LIST", "") -if web_fetch_filter_list == "": +web_fetch_filter_list = os.getenv('WEB_FETCH_FILTER_LIST', '') +if web_fetch_filter_list == '': web_fetch_filter_list = [] else: - web_fetch_filter_list = [ - item.strip() for item in web_fetch_filter_list.split(",") if item.strip() - ] + web_fetch_filter_list = [item.strip() for item in web_fetch_filter_list.split(',') if item.strip()] WEB_FETCH_FILTER_LIST = list(set(DEFAULT_WEB_FETCH_FILTER_LIST + web_fetch_filter_list)) -YOUTUBE_LOADER_LANGUAGE = PersistentConfig( - "YOUTUBE_LOADER_LANGUAGE", - "rag.youtube_loader_language", - os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","), +YOUTUBE_LOADER_LANGUAGE = ConfigVar( + 'YOUTUBE_LOADER_LANGUAGE', + 'rag.youtube_loader_language', + os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(','), ) -YOUTUBE_LOADER_PROXY_URL = PersistentConfig( - "YOUTUBE_LOADER_PROXY_URL", - "rag.youtube_loader_proxy_url", - os.getenv("YOUTUBE_LOADER_PROXY_URL", ""), +YOUTUBE_LOADER_PROXY_URL = ConfigVar( + 'YOUTUBE_LOADER_PROXY_URL', + 'rag.youtube_loader_proxy_url', + os.getenv('YOUTUBE_LOADER_PROXY_URL', ''), ) @@ -3037,438 +1526,496 @@ YOUTUBE_LOADER_PROXY_URL = PersistentConfig( # Web Search (RAG) #################################### -ENABLE_WEB_SEARCH = PersistentConfig( - "ENABLE_WEB_SEARCH", - "rag.web.search.enable", - os.getenv("ENABLE_WEB_SEARCH", "False").lower() == "true", +ENABLE_WEB_SEARCH = ConfigVar( + 'ENABLE_WEB_SEARCH', + 'rag.web.search.enable', + os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true', ) -WEB_SEARCH_ENGINE = PersistentConfig( - "WEB_SEARCH_ENGINE", - "rag.web.search.engine", - os.getenv("WEB_SEARCH_ENGINE", ""), +WEB_SEARCH_ENGINE = ConfigVar( + 'WEB_SEARCH_ENGINE', + 'rag.web.search.engine', + os.getenv('WEB_SEARCH_ENGINE', ''), ) -BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig( - "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", - "rag.web.search.bypass_embedding_and_retrieval", - os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true", +BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = ConfigVar( + 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL', + 'rag.web.search.bypass_embedding_and_retrieval', + os.getenv('BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true', ) -BYPASS_WEB_SEARCH_WEB_LOADER = PersistentConfig( - "BYPASS_WEB_SEARCH_WEB_LOADER", - "rag.web.search.bypass_web_loader", - os.getenv("BYPASS_WEB_SEARCH_WEB_LOADER", "False").lower() == "true", +BYPASS_WEB_SEARCH_WEB_LOADER = ConfigVar( + 'BYPASS_WEB_SEARCH_WEB_LOADER', + 'rag.web.search.bypass_web_loader', + os.getenv('BYPASS_WEB_SEARCH_WEB_LOADER', 'False').lower() == 'true', ) -WEB_SEARCH_RESULT_COUNT = PersistentConfig( - "WEB_SEARCH_RESULT_COUNT", - "rag.web.search.result_count", - int(os.getenv("WEB_SEARCH_RESULT_COUNT", "3")), +WEB_SEARCH_RESULT_COUNT = ConfigVar( + 'WEB_SEARCH_RESULT_COUNT', + 'rag.web.search.result_count', + int(os.getenv('WEB_SEARCH_RESULT_COUNT', '3')), ) -# You can provide a list of your own websites to filter after performing a web search. -# This ensures the highest level of safety and reliability of the information sources. -WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( - "WEB_SEARCH_DOMAIN_FILTER_LIST", - "rag.web.search.domain.filter_list", - [ +try: + web_search_domain_filter_list = json.loads(os.getenv('WEB_SEARCH_DOMAIN_FILTER_LIST', '[]')) +except Exception as e: + web_search_domain_filter_list = [ # "wikipedia.com", # "wikimedia.org", # "wikidata.org", # "!stackoverflow.com", - ], + ] + +# You can provide a list of your own websites to filter after performing a web search. +# This ensures the highest level of safety and reliability of the information sources. +WEB_SEARCH_DOMAIN_FILTER_LIST = ConfigVar( + 'WEB_SEARCH_DOMAIN_FILTER_LIST', + 'rag.web.search.domain.filter_list', + web_search_domain_filter_list, ) -WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig( - "WEB_SEARCH_CONCURRENT_REQUESTS", - "rag.web.search.concurrent_requests", - int(os.getenv("WEB_SEARCH_CONCURRENT_REQUESTS", "0")), +WEB_SEARCH_CONCURRENT_REQUESTS = ConfigVar( + 'WEB_SEARCH_CONCURRENT_REQUESTS', + 'rag.web.search.concurrent_requests', + int(os.getenv('WEB_SEARCH_CONCURRENT_REQUESTS', '0')), +) + +WEB_FETCH_MAX_CONTENT_LENGTH = ConfigVar( + 'WEB_FETCH_MAX_CONTENT_LENGTH', + 'rag.web.fetch.max_content_length', + (int(os.getenv('WEB_FETCH_MAX_CONTENT_LENGTH')) if os.getenv('WEB_FETCH_MAX_CONTENT_LENGTH') else None), +) + +WEB_LOADER_ENGINE = ConfigVar( + 'WEB_LOADER_ENGINE', + 'rag.web.loader.engine', + os.getenv('WEB_LOADER_ENGINE', ''), ) -WEB_LOADER_ENGINE = PersistentConfig( - "WEB_LOADER_ENGINE", - "rag.web.loader.engine", - os.environ.get("WEB_LOADER_ENGINE", ""), +WEB_LOADER_CONCURRENT_REQUESTS = ConfigVar( + 'WEB_LOADER_CONCURRENT_REQUESTS', + 'rag.web.loader.concurrent_requests', + int(os.getenv('WEB_LOADER_CONCURRENT_REQUESTS', '10')), +) + +WEB_LOADER_TIMEOUT = ConfigVar( + 'WEB_LOADER_TIMEOUT', + 'rag.web.loader.timeout', + os.getenv('WEB_LOADER_TIMEOUT', ''), ) -WEB_LOADER_CONCURRENT_REQUESTS = PersistentConfig( - "WEB_LOADER_CONCURRENT_REQUESTS", - "rag.web.loader.concurrent_requests", - int(os.getenv("WEB_LOADER_CONCURRENT_REQUESTS", "10")), +ENABLE_WEB_LOADER_SSL_VERIFICATION = ConfigVar( + 'ENABLE_WEB_LOADER_SSL_VERIFICATION', + 'rag.web.loader.ssl_verification', + os.getenv('ENABLE_WEB_LOADER_SSL_VERIFICATION', 'True').lower() == 'true', ) -WEB_LOADER_TIMEOUT = PersistentConfig( - "WEB_LOADER_TIMEOUT", - "rag.web.loader.timeout", - os.getenv("WEB_LOADER_TIMEOUT", ""), +WEB_SEARCH_TRUST_ENV = ConfigVar( + 'WEB_SEARCH_TRUST_ENV', + 'rag.web.search.trust_env', + os.getenv('WEB_SEARCH_TRUST_ENV', 'True').lower() == 'true', ) -ENABLE_WEB_LOADER_SSL_VERIFICATION = PersistentConfig( - "ENABLE_WEB_LOADER_SSL_VERIFICATION", - "rag.web.loader.ssl_verification", - os.environ.get("ENABLE_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true", +OLLAMA_CLOUD_WEB_SEARCH_API_KEY = ConfigVar( + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY', + 'rag.web.search.ollama_cloud_api_key', + os.getenv('OLLAMA_CLOUD_API_KEY', ''), ) -WEB_SEARCH_TRUST_ENV = PersistentConfig( - "WEB_SEARCH_TRUST_ENV", - "rag.web.search.trust_env", - os.getenv("WEB_SEARCH_TRUST_ENV", "False").lower() == "true", +SEARXNG_QUERY_URL = ConfigVar( + 'SEARXNG_QUERY_URL', + 'rag.web.search.searxng_query_url', + os.getenv('SEARXNG_QUERY_URL', ''), ) - -OLLAMA_CLOUD_WEB_SEARCH_API_KEY = PersistentConfig( - "OLLAMA_CLOUD_WEB_SEARCH_API_KEY", - "rag.web.search.ollama_cloud_api_key", - os.getenv("OLLAMA_CLOUD_API_KEY", ""), +SEARXNG_LANGUAGE = ConfigVar( + 'SEARXNG_LANGUAGE', + 'rag.web.search.searxng_language', + os.getenv('SEARXNG_LANGUAGE', 'all'), ) -SEARXNG_QUERY_URL = PersistentConfig( - "SEARXNG_QUERY_URL", - "rag.web.search.searxng_query_url", - os.getenv("SEARXNG_QUERY_URL", ""), +YACY_QUERY_URL = ConfigVar( + 'YACY_QUERY_URL', + 'rag.web.search.yacy_query_url', + os.getenv('YACY_QUERY_URL', ''), ) -SEARXNG_LANGUAGE = PersistentConfig( - "SEARXNG_LANGUAGE", - "rag.web.search.searxng_language", - os.getenv("SEARXNG_LANGUAGE", "all"), +YACY_USERNAME = ConfigVar( + 'YACY_USERNAME', + 'rag.web.search.yacy_username', + os.getenv('YACY_USERNAME', ''), ) -YACY_QUERY_URL = PersistentConfig( - "YACY_QUERY_URL", - "rag.web.search.yacy_query_url", - os.getenv("YACY_QUERY_URL", ""), +YACY_PASSWORD = ConfigVar( + 'YACY_PASSWORD', + 'rag.web.search.yacy_password', + os.getenv('YACY_PASSWORD', ''), ) -YACY_USERNAME = PersistentConfig( - "YACY_USERNAME", - "rag.web.search.yacy_username", - os.getenv("YACY_USERNAME", ""), +GOOGLE_PSE_API_KEY = ConfigVar( + 'GOOGLE_PSE_API_KEY', + 'rag.web.search.google_pse_api_key', + os.getenv('GOOGLE_PSE_API_KEY', ''), ) -YACY_PASSWORD = PersistentConfig( - "YACY_PASSWORD", - "rag.web.search.yacy_password", - os.getenv("YACY_PASSWORD", ""), +GOOGLE_PSE_ENGINE_ID = ConfigVar( + 'GOOGLE_PSE_ENGINE_ID', + 'rag.web.search.google_pse_engine_id', + os.getenv('GOOGLE_PSE_ENGINE_ID', ''), ) -GOOGLE_PSE_API_KEY = PersistentConfig( - "GOOGLE_PSE_API_KEY", - "rag.web.search.google_pse_api_key", - os.getenv("GOOGLE_PSE_API_KEY", ""), +BRAVE_SEARCH_API_KEY = ConfigVar( + 'BRAVE_SEARCH_API_KEY', + 'rag.web.search.brave_search_api_key', + os.getenv('BRAVE_SEARCH_API_KEY', ''), ) -GOOGLE_PSE_ENGINE_ID = PersistentConfig( - "GOOGLE_PSE_ENGINE_ID", - "rag.web.search.google_pse_engine_id", - os.getenv("GOOGLE_PSE_ENGINE_ID", ""), +BRAVE_SEARCH_CONTEXT_TOKENS = ConfigVar( + 'BRAVE_SEARCH_CONTEXT_TOKENS', + 'rag.web.search.brave_search_context_tokens', + int(os.getenv('BRAVE_SEARCH_CONTEXT_TOKENS', '8192')), ) -BRAVE_SEARCH_API_KEY = PersistentConfig( - "BRAVE_SEARCH_API_KEY", - "rag.web.search.brave_search_api_key", - os.getenv("BRAVE_SEARCH_API_KEY", ""), +KAGI_SEARCH_API_KEY = ConfigVar( + 'KAGI_SEARCH_API_KEY', + 'rag.web.search.kagi_search_api_key', + os.getenv('KAGI_SEARCH_API_KEY', ''), ) -KAGI_SEARCH_API_KEY = PersistentConfig( - "KAGI_SEARCH_API_KEY", - "rag.web.search.kagi_search_api_key", - os.getenv("KAGI_SEARCH_API_KEY", ""), +MOJEEK_SEARCH_API_KEY = ConfigVar( + 'MOJEEK_SEARCH_API_KEY', + 'rag.web.search.mojeek_search_api_key', + os.getenv('MOJEEK_SEARCH_API_KEY', ''), ) -MOJEEK_SEARCH_API_KEY = PersistentConfig( - "MOJEEK_SEARCH_API_KEY", - "rag.web.search.mojeek_search_api_key", - os.getenv("MOJEEK_SEARCH_API_KEY", ""), +BOCHA_SEARCH_API_KEY = ConfigVar( + 'BOCHA_SEARCH_API_KEY', + 'rag.web.search.bocha_search_api_key', + os.getenv('BOCHA_SEARCH_API_KEY', ''), ) -BOCHA_SEARCH_API_KEY = PersistentConfig( - "BOCHA_SEARCH_API_KEY", - "rag.web.search.bocha_search_api_key", - os.getenv("BOCHA_SEARCH_API_KEY", ""), +SERPSTACK_API_KEY = ConfigVar( + 'SERPSTACK_API_KEY', + 'rag.web.search.serpstack_api_key', + os.getenv('SERPSTACK_API_KEY', ''), ) -SERPSTACK_API_KEY = PersistentConfig( - "SERPSTACK_API_KEY", - "rag.web.search.serpstack_api_key", - os.getenv("SERPSTACK_API_KEY", ""), +SERPSTACK_HTTPS = ConfigVar( + 'SERPSTACK_HTTPS', + 'rag.web.search.serpstack_https', + os.getenv('SERPSTACK_HTTPS', 'True').lower() == 'true', ) -SERPSTACK_HTTPS = PersistentConfig( - "SERPSTACK_HTTPS", - "rag.web.search.serpstack_https", - os.getenv("SERPSTACK_HTTPS", "True").lower() == "true", +SERPER_API_KEY = ConfigVar( + 'SERPER_API_KEY', + 'rag.web.search.serper_api_key', + os.getenv('SERPER_API_KEY', ''), ) -SERPER_API_KEY = PersistentConfig( - "SERPER_API_KEY", - "rag.web.search.serper_api_key", - os.getenv("SERPER_API_KEY", ""), +SERPLY_API_KEY = ConfigVar( + 'SERPLY_API_KEY', + 'rag.web.search.serply_api_key', + os.getenv('SERPLY_API_KEY', ''), ) -SERPLY_API_KEY = PersistentConfig( - "SERPLY_API_KEY", - "rag.web.search.serply_api_key", - os.getenv("SERPLY_API_KEY", ""), +DDGS_BACKEND = ConfigVar( + 'DDGS_BACKEND', + 'rag.web.search.ddgs_backend', + os.getenv('DDGS_BACKEND', 'auto'), ) -DDGS_BACKEND = PersistentConfig( - "DDGS_BACKEND", - "rag.web.search.ddgs_backend", - os.getenv("DDGS_BACKEND", "auto"), +JINA_API_KEY = ConfigVar( + 'JINA_API_KEY', + 'rag.web.search.jina_api_key', + os.getenv('JINA_API_KEY', ''), ) -JINA_API_KEY = PersistentConfig( - "JINA_API_KEY", - "rag.web.search.jina_api_key", - os.getenv("JINA_API_KEY", ""), +JINA_API_BASE_URL = ConfigVar( + 'JINA_API_BASE_URL', + 'rag.web.search.jina_api_base_url', + os.getenv('JINA_API_BASE_URL', ''), ) -JINA_API_BASE_URL = PersistentConfig( - "JINA_API_BASE_URL", - "rag.web.search.jina_api_base_url", - os.getenv("JINA_API_BASE_URL", ""), +SEARCHAPI_API_KEY = ConfigVar( + 'SEARCHAPI_API_KEY', + 'rag.web.search.searchapi_api_key', + os.getenv('SEARCHAPI_API_KEY', ''), ) -SEARCHAPI_API_KEY = PersistentConfig( - "SEARCHAPI_API_KEY", - "rag.web.search.searchapi_api_key", - os.getenv("SEARCHAPI_API_KEY", ""), +SEARCHAPI_ENGINE = ConfigVar( + 'SEARCHAPI_ENGINE', + 'rag.web.search.searchapi_engine', + os.getenv('SEARCHAPI_ENGINE', ''), ) -SEARCHAPI_ENGINE = PersistentConfig( - "SEARCHAPI_ENGINE", - "rag.web.search.searchapi_engine", - os.getenv("SEARCHAPI_ENGINE", ""), +SERPAPI_API_KEY = ConfigVar( + 'SERPAPI_API_KEY', + 'rag.web.search.serpapi_api_key', + os.getenv('SERPAPI_API_KEY', ''), ) -SERPAPI_API_KEY = PersistentConfig( - "SERPAPI_API_KEY", - "rag.web.search.serpapi_api_key", - os.getenv("SERPAPI_API_KEY", ""), +SERPAPI_ENGINE = ConfigVar( + 'SERPAPI_ENGINE', + 'rag.web.search.serpapi_engine', + os.getenv('SERPAPI_ENGINE', ''), ) -SERPAPI_ENGINE = PersistentConfig( - "SERPAPI_ENGINE", - "rag.web.search.serpapi_engine", - os.getenv("SERPAPI_ENGINE", ""), +BING_SEARCH_V7_ENDPOINT = ConfigVar( + 'BING_SEARCH_V7_ENDPOINT', + 'rag.web.search.bing_search_v7_endpoint', + os.getenv('BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search'), ) -BING_SEARCH_V7_ENDPOINT = PersistentConfig( - "BING_SEARCH_V7_ENDPOINT", - "rag.web.search.bing_search_v7_endpoint", - os.environ.get( - "BING_SEARCH_V7_ENDPOINT", "https://api.bing.microsoft.com/v7.0/search" - ), +BING_SEARCH_V7_SUBSCRIPTION_KEY = ConfigVar( + 'BING_SEARCH_V7_SUBSCRIPTION_KEY', + 'rag.web.search.bing_search_v7_subscription_key', + os.getenv('BING_SEARCH_V7_SUBSCRIPTION_KEY', ''), ) -BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig( - "BING_SEARCH_V7_SUBSCRIPTION_KEY", - "rag.web.search.bing_search_v7_subscription_key", - os.environ.get("BING_SEARCH_V7_SUBSCRIPTION_KEY", ""), +AZURE_AI_SEARCH_API_KEY = ConfigVar( + 'AZURE_AI_SEARCH_API_KEY', + 'rag.web.search.azure_ai_search_api_key', + os.getenv('AZURE_AI_SEARCH_API_KEY', ''), ) -AZURE_AI_SEARCH_API_KEY = PersistentConfig( - "AZURE_AI_SEARCH_API_KEY", - "rag.web.search.azure_ai_search_api_key", - os.environ.get("AZURE_AI_SEARCH_API_KEY", ""), +AZURE_AI_SEARCH_ENDPOINT = ConfigVar( + 'AZURE_AI_SEARCH_ENDPOINT', + 'rag.web.search.azure_ai_search_endpoint', + os.getenv('AZURE_AI_SEARCH_ENDPOINT', ''), ) -AZURE_AI_SEARCH_ENDPOINT = PersistentConfig( - "AZURE_AI_SEARCH_ENDPOINT", - "rag.web.search.azure_ai_search_endpoint", - os.environ.get("AZURE_AI_SEARCH_ENDPOINT", ""), +AZURE_AI_SEARCH_INDEX_NAME = ConfigVar( + 'AZURE_AI_SEARCH_INDEX_NAME', + 'rag.web.search.azure_ai_search_index_name', + os.getenv('AZURE_AI_SEARCH_INDEX_NAME', ''), ) -AZURE_AI_SEARCH_INDEX_NAME = PersistentConfig( - "AZURE_AI_SEARCH_INDEX_NAME", - "rag.web.search.azure_ai_search_index_name", - os.environ.get("AZURE_AI_SEARCH_INDEX_NAME", ""), +EXA_API_KEY = ConfigVar( + 'EXA_API_KEY', + 'rag.web.search.exa_api_key', + os.getenv('EXA_API_KEY', ''), ) -EXA_API_KEY = PersistentConfig( - "EXA_API_KEY", - "rag.web.search.exa_api_key", - os.getenv("EXA_API_KEY", ""), +PERPLEXITY_API_KEY = ConfigVar( + 'PERPLEXITY_API_KEY', + 'rag.web.search.perplexity_api_key', + os.getenv('PERPLEXITY_API_KEY', ''), ) -PERPLEXITY_API_KEY = PersistentConfig( - "PERPLEXITY_API_KEY", - "rag.web.search.perplexity_api_key", - os.getenv("PERPLEXITY_API_KEY", ""), +PERPLEXITY_MODEL = ConfigVar( + 'PERPLEXITY_MODEL', + 'rag.web.search.perplexity_model', + os.getenv('PERPLEXITY_MODEL', 'sonar'), ) -PERPLEXITY_MODEL = PersistentConfig( - "PERPLEXITY_MODEL", - "rag.web.search.perplexity_model", - os.getenv("PERPLEXITY_MODEL", "sonar"), +PERPLEXITY_SEARCH_CONTEXT_USAGE = ConfigVar( + 'PERPLEXITY_SEARCH_CONTEXT_USAGE', + 'rag.web.search.perplexity_search_context_usage', + os.getenv('PERPLEXITY_SEARCH_CONTEXT_USAGE', 'medium'), ) -PERPLEXITY_SEARCH_CONTEXT_USAGE = PersistentConfig( - "PERPLEXITY_SEARCH_CONTEXT_USAGE", - "rag.web.search.perplexity_search_context_usage", - os.getenv("PERPLEXITY_SEARCH_CONTEXT_USAGE", "medium"), +PERPLEXITY_SEARCH_API_URL = ConfigVar( + 'PERPLEXITY_SEARCH_API_URL', + 'rag.web.search.perplexity_search_api_url', + os.getenv('PERPLEXITY_SEARCH_API_URL', 'https://api.perplexity.ai/search'), ) -PERPLEXITY_SEARCH_API_URL = PersistentConfig( - "PERPLEXITY_SEARCH_API_URL", - "rag.web.search.perplexity_search_api_url", - os.getenv("PERPLEXITY_SEARCH_API_URL", "https://api.perplexity.ai/search"), +SOUGOU_API_SID = ConfigVar( + 'SOUGOU_API_SID', + 'rag.web.search.sougou_api_sid', + os.getenv('SOUGOU_API_SID', ''), ) -SOUGOU_API_SID = PersistentConfig( - "SOUGOU_API_SID", - "rag.web.search.sougou_api_sid", - os.getenv("SOUGOU_API_SID", ""), +SOUGOU_API_SK = ConfigVar( + 'SOUGOU_API_SK', + 'rag.web.search.sougou_api_sk', + os.getenv('SOUGOU_API_SK', ''), ) -SOUGOU_API_SK = PersistentConfig( - "SOUGOU_API_SK", - "rag.web.search.sougou_api_sk", - os.getenv("SOUGOU_API_SK", ""), +TAVILY_API_KEY = ConfigVar( + 'TAVILY_API_KEY', + 'rag.web.search.tavily_api_key', + os.getenv('TAVILY_API_KEY', ''), ) -TAVILY_API_KEY = PersistentConfig( - "TAVILY_API_KEY", - "rag.web.search.tavily_api_key", - os.getenv("TAVILY_API_KEY", ""), +TAVILY_EXTRACT_DEPTH = ConfigVar( + 'TAVILY_EXTRACT_DEPTH', + 'rag.web.search.tavily_extract_depth', + os.getenv('TAVILY_EXTRACT_DEPTH', 'basic'), ) -TAVILY_EXTRACT_DEPTH = PersistentConfig( - "TAVILY_EXTRACT_DEPTH", - "rag.web.search.tavily_extract_depth", - os.getenv("TAVILY_EXTRACT_DEPTH", "basic"), +PLAYWRIGHT_WS_URL = ConfigVar( + 'PLAYWRIGHT_WS_URL', + 'rag.web.loader.playwright_ws_url', + os.getenv('PLAYWRIGHT_WS_URL', ''), ) -PLAYWRIGHT_WS_URL = PersistentConfig( - "PLAYWRIGHT_WS_URL", - "rag.web.loader.playwright_ws_url", - os.environ.get("PLAYWRIGHT_WS_URL", ""), +PLAYWRIGHT_TIMEOUT = ConfigVar( + 'PLAYWRIGHT_TIMEOUT', + 'rag.web.loader.playwright_timeout', + int(os.getenv('PLAYWRIGHT_TIMEOUT', '10000')), ) -PLAYWRIGHT_TIMEOUT = PersistentConfig( - "PLAYWRIGHT_TIMEOUT", - "rag.web.loader.playwright_timeout", - int(os.environ.get("PLAYWRIGHT_TIMEOUT", "10000")), +FIRECRAWL_API_KEY = ConfigVar( + 'FIRECRAWL_API_KEY', + 'rag.web.loader.firecrawl_api_key', + os.getenv('FIRECRAWL_API_KEY', ''), ) -FIRECRAWL_API_KEY = PersistentConfig( - "FIRECRAWL_API_KEY", - "rag.web.loader.firecrawl_api_key", - os.environ.get("FIRECRAWL_API_KEY", ""), +FIRECRAWL_API_BASE_URL = ConfigVar( + 'FIRECRAWL_API_BASE_URL', + 'rag.web.loader.firecrawl_api_url', + os.getenv('FIRECRAWL_API_BASE_URL', 'https://api.firecrawl.dev'), ) -FIRECRAWL_API_BASE_URL = PersistentConfig( - "FIRECRAWL_API_BASE_URL", - "rag.web.loader.firecrawl_api_url", - os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"), +FIRECRAWL_TIMEOUT = ConfigVar( + 'FIRECRAWL_TIMEOUT', + 'rag.web.loader.firecrawl_timeout', + os.getenv('FIRECRAWL_TIMEOUT', ''), ) -FIRECRAWL_TIMEOUT = PersistentConfig( - "FIRECRAWL_TIMEOUT", - "rag.web.loader.firecrawl_timeout", - os.environ.get("FIRECRAWL_TIMEOUT", ""), +EXTERNAL_WEB_SEARCH_URL = ConfigVar( + 'EXTERNAL_WEB_SEARCH_URL', + 'rag.web.search.external_web_search_url', + os.getenv('EXTERNAL_WEB_SEARCH_URL', ''), ) -EXTERNAL_WEB_SEARCH_URL = PersistentConfig( - "EXTERNAL_WEB_SEARCH_URL", - "rag.web.search.external_web_search_url", - os.environ.get("EXTERNAL_WEB_SEARCH_URL", ""), +EXTERNAL_WEB_SEARCH_API_KEY = ConfigVar( + 'EXTERNAL_WEB_SEARCH_API_KEY', + 'rag.web.search.external_web_search_api_key', + os.getenv('EXTERNAL_WEB_SEARCH_API_KEY', ''), ) -EXTERNAL_WEB_SEARCH_API_KEY = PersistentConfig( - "EXTERNAL_WEB_SEARCH_API_KEY", - "rag.web.search.external_web_search_api_key", - os.environ.get("EXTERNAL_WEB_SEARCH_API_KEY", ""), +EXTERNAL_WEB_LOADER_URL = ConfigVar( + 'EXTERNAL_WEB_LOADER_URL', + 'rag.web.loader.external_web_loader_url', + os.getenv('EXTERNAL_WEB_LOADER_URL', ''), ) -EXTERNAL_WEB_LOADER_URL = PersistentConfig( - "EXTERNAL_WEB_LOADER_URL", - "rag.web.loader.external_web_loader_url", - os.environ.get("EXTERNAL_WEB_LOADER_URL", ""), +EXTERNAL_WEB_LOADER_API_KEY = ConfigVar( + 'EXTERNAL_WEB_LOADER_API_KEY', + 'rag.web.loader.external_web_loader_api_key', + os.getenv('EXTERNAL_WEB_LOADER_API_KEY', ''), ) -EXTERNAL_WEB_LOADER_API_KEY = PersistentConfig( - "EXTERNAL_WEB_LOADER_API_KEY", - "rag.web.loader.external_web_loader_api_key", - os.environ.get("EXTERNAL_WEB_LOADER_API_KEY", ""), +YANDEX_WEB_SEARCH_URL = ConfigVar( + 'YANDEX_WEB_SEARCH_URL', + 'rag.web.search.yandex_web_search_url', + os.getenv('YANDEX_WEB_SEARCH_URL', ''), +) + +YANDEX_WEB_SEARCH_API_KEY = ConfigVar( + 'YANDEX_WEB_SEARCH_API_KEY', + 'rag.web.search.yandex_web_search_api_key', + os.getenv('YANDEX_WEB_SEARCH_API_KEY', ''), +) + +YANDEX_WEB_SEARCH_CONFIG = ConfigVar( + 'YANDEX_WEB_SEARCH_CONFIG', + 'rag.web.search.yandex_web_search_config', + os.getenv('YANDEX_WEB_SEARCH_CONFIG', ''), +) + +YOUCOM_API_KEY = ConfigVar( + 'YOUCOM_API_KEY', + 'rag.web.search.youcom_api_key', + os.getenv('YOUCOM_API_KEY', ''), +) + +LINKUP_API_KEY = ConfigVar( + 'LINKUP_API_KEY', + 'rag.web.search.linkup_api_key', + os.getenv('LINKUP_API_KEY', ''), +) + +linkup_search_params = os.getenv('LINKUP_SEARCH_PARAMS', '') +try: + linkup_search_params = json.loads(linkup_search_params) +except json.JSONDecodeError: + linkup_search_params = {} + +LINKUP_SEARCH_PARAMS = ConfigVar( + 'LINKUP_SEARCH_PARAMS', + 'rag.web.search.linkup_search_params', + linkup_search_params, ) #################################### # Images #################################### -ENABLE_IMAGE_GENERATION = PersistentConfig( - "ENABLE_IMAGE_GENERATION", - "image_generation.enable", - os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true", +ENABLE_IMAGE_GENERATION = ConfigVar( + 'ENABLE_IMAGE_GENERATION', + 'image_generation.enable', + os.getenv('ENABLE_IMAGE_GENERATION', '').lower() == 'true', ) -IMAGE_GENERATION_ENGINE = PersistentConfig( - "IMAGE_GENERATION_ENGINE", - "image_generation.engine", - os.getenv("IMAGE_GENERATION_ENGINE", "openai"), +IMAGE_GENERATION_ENGINE = ConfigVar( + 'IMAGE_GENERATION_ENGINE', + 'image_generation.engine', + os.getenv('IMAGE_GENERATION_ENGINE', 'openai'), ) -IMAGE_GENERATION_MODEL = PersistentConfig( - "IMAGE_GENERATION_MODEL", - "image_generation.model", - os.getenv("IMAGE_GENERATION_MODEL", ""), +IMAGE_GENERATION_MODEL = ConfigVar( + 'IMAGE_GENERATION_MODEL', + 'image_generation.model', + os.getenv('IMAGE_GENERATION_MODEL', ''), ) -IMAGE_SIZE = PersistentConfig( - "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512") +# Regex pattern for models that support IMAGE_SIZE = "auto". +IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN', '^gpt-image') + +# Regex pattern for models that return URLs instead of base64 data. +IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN = os.getenv('IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN', '^gpt-image') + +IMAGE_SIZE = ConfigVar('IMAGE_SIZE', 'image_generation.size', os.getenv('IMAGE_SIZE', '512x512')) + +IMAGE_STEPS = ConfigVar('IMAGE_STEPS', 'image_generation.steps', int(os.getenv('IMAGE_STEPS', 50))) + +ENABLE_IMAGE_PROMPT_GENERATION = ConfigVar( + 'ENABLE_IMAGE_PROMPT_GENERATION', + 'image_generation.prompt.enable', + os.getenv('ENABLE_IMAGE_PROMPT_GENERATION', 'true').lower() == 'true', ) -IMAGE_STEPS = PersistentConfig( - "IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50)) +AUTOMATIC1111_BASE_URL = ConfigVar( + 'AUTOMATIC1111_BASE_URL', + 'image_generation.automatic1111.base_url', + os.getenv('AUTOMATIC1111_BASE_URL', ''), +) +AUTOMATIC1111_API_AUTH = ConfigVar( + 'AUTOMATIC1111_API_AUTH', + 'image_generation.automatic1111.api_auth', + os.getenv('AUTOMATIC1111_API_AUTH', ''), ) -ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig( - "ENABLE_IMAGE_PROMPT_GENERATION", - "image_generation.prompt.enable", - os.environ.get("ENABLE_IMAGE_PROMPT_GENERATION", "true").lower() == "true", -) - -AUTOMATIC1111_BASE_URL = PersistentConfig( - "AUTOMATIC1111_BASE_URL", - "image_generation.automatic1111.base_url", - os.getenv("AUTOMATIC1111_BASE_URL", ""), -) -AUTOMATIC1111_API_AUTH = PersistentConfig( - "AUTOMATIC1111_API_AUTH", - "image_generation.automatic1111.api_auth", - os.getenv("AUTOMATIC1111_API_AUTH", ""), -) - -automatic1111_params = os.getenv("AUTOMATIC1111_PARAMS", "") +automatic1111_params = os.getenv('AUTOMATIC1111_PARAMS', '') try: automatic1111_params = json.loads(automatic1111_params) except json.JSONDecodeError: automatic1111_params = {} -AUTOMATIC1111_PARAMS = PersistentConfig( - "AUTOMATIC1111_PARAMS", - "image_generation.automatic1111.api_params", +AUTOMATIC1111_PARAMS = ConfigVar( + 'AUTOMATIC1111_PARAMS', + 'image_generation.automatic1111.api_params', automatic1111_params, ) -COMFYUI_BASE_URL = PersistentConfig( - "COMFYUI_BASE_URL", - "image_generation.comfyui.base_url", - os.getenv("COMFYUI_BASE_URL", ""), +COMFYUI_BASE_URL = ConfigVar( + 'COMFYUI_BASE_URL', + 'image_generation.comfyui.base_url', + os.getenv('COMFYUI_BASE_URL', ''), ) -COMFYUI_API_KEY = PersistentConfig( - "COMFYUI_API_KEY", - "image_generation.comfyui.api_key", - os.getenv("COMFYUI_API_KEY", ""), +COMFYUI_API_KEY = ConfigVar( + 'COMFYUI_API_KEY', + 'image_generation.comfyui.api_key', + os.getenv('COMFYUI_API_KEY', ''), ) COMFYUI_DEFAULT_WORKFLOW = """ @@ -3582,147 +2129,143 @@ COMFYUI_DEFAULT_WORKFLOW = """ """ -COMFYUI_WORKFLOW = PersistentConfig( - "COMFYUI_WORKFLOW", - "image_generation.comfyui.workflow", - os.getenv("COMFYUI_WORKFLOW", COMFYUI_DEFAULT_WORKFLOW), +COMFYUI_WORKFLOW = ConfigVar( + 'COMFYUI_WORKFLOW', + 'image_generation.comfyui.workflow', + os.getenv('COMFYUI_WORKFLOW', COMFYUI_DEFAULT_WORKFLOW), ) -comfyui_workflow_nodes = os.getenv("COMFYUI_WORKFLOW_NODES", "") +comfyui_workflow_nodes = os.getenv('COMFYUI_WORKFLOW_NODES', '') try: comfyui_workflow_nodes = json.loads(comfyui_workflow_nodes) except json.JSONDecodeError: comfyui_workflow_nodes = [] -COMFYUI_WORKFLOW_NODES = PersistentConfig( - "COMFYUI_WORKFLOW_NODES", - "image_generation.comfyui.nodes", +COMFYUI_WORKFLOW_NODES = ConfigVar( + 'COMFYUI_WORKFLOW_NODES', + 'image_generation.comfyui.nodes', comfyui_workflow_nodes, ) -IMAGES_OPENAI_API_BASE_URL = PersistentConfig( - "IMAGES_OPENAI_API_BASE_URL", - "image_generation.openai.api_base_url", - os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +IMAGES_OPENAI_API_BASE_URL = ConfigVar( + 'IMAGES_OPENAI_API_BASE_URL', + 'image_generation.openai.api_base_url', + os.getenv('IMAGES_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), ) -IMAGES_OPENAI_API_VERSION = PersistentConfig( - "IMAGES_OPENAI_API_VERSION", - "image_generation.openai.api_version", - os.getenv("IMAGES_OPENAI_API_VERSION", ""), +IMAGES_OPENAI_API_VERSION = ConfigVar( + 'IMAGES_OPENAI_API_VERSION', + 'image_generation.openai.api_version', + os.getenv('IMAGES_OPENAI_API_VERSION', ''), ) -IMAGES_OPENAI_API_KEY = PersistentConfig( - "IMAGES_OPENAI_API_KEY", - "image_generation.openai.api_key", - os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY), +IMAGES_OPENAI_API_KEY = ConfigVar( + 'IMAGES_OPENAI_API_KEY', + 'image_generation.openai.api_key', + os.getenv('IMAGES_OPENAI_API_KEY', OPENAI_API_KEY), ) -images_openai_params = os.getenv("IMAGES_OPENAI_PARAMS", "") +images_openai_params = os.getenv('IMAGES_OPENAI_PARAMS', '') try: images_openai_params = json.loads(images_openai_params) except json.JSONDecodeError: images_openai_params = {} -IMAGES_OPENAI_API_PARAMS = PersistentConfig( - "IMAGES_OPENAI_API_PARAMS", "image_generation.openai.params", images_openai_params +IMAGES_OPENAI_API_PARAMS = ConfigVar('IMAGES_OPENAI_API_PARAMS', 'image_generation.openai.params', images_openai_params) + + +IMAGES_GEMINI_API_BASE_URL = ConfigVar( + 'IMAGES_GEMINI_API_BASE_URL', + 'image_generation.gemini.api_base_url', + os.getenv('IMAGES_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL), +) +IMAGES_GEMINI_API_KEY = ConfigVar( + 'IMAGES_GEMINI_API_KEY', + 'image_generation.gemini.api_key', + os.getenv('IMAGES_GEMINI_API_KEY', GEMINI_API_KEY), +) + +IMAGES_GEMINI_ENDPOINT_METHOD = ConfigVar( + 'IMAGES_GEMINI_ENDPOINT_METHOD', + 'image_generation.gemini.endpoint_method', + os.getenv('IMAGES_GEMINI_ENDPOINT_METHOD', ''), +) + +ENABLE_IMAGE_EDIT = ConfigVar( + 'ENABLE_IMAGE_EDIT', + 'images.edit.enable', + os.getenv('ENABLE_IMAGE_EDIT', '').lower() == 'true', +) + +IMAGE_EDIT_ENGINE = ConfigVar( + 'IMAGE_EDIT_ENGINE', + 'images.edit.engine', + os.getenv('IMAGE_EDIT_ENGINE', 'openai'), +) + +IMAGE_EDIT_MODEL = ConfigVar( + 'IMAGE_EDIT_MODEL', + 'images.edit.model', + os.getenv('IMAGE_EDIT_MODEL', ''), +) + +IMAGE_EDIT_SIZE = ConfigVar('IMAGE_EDIT_SIZE', 'images.edit.size', os.getenv('IMAGE_EDIT_SIZE', '')) + +IMAGES_EDIT_OPENAI_API_BASE_URL = ConfigVar( + 'IMAGES_EDIT_OPENAI_API_BASE_URL', + 'images.edit.openai.api_base_url', + os.getenv('IMAGES_EDIT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) +IMAGES_EDIT_OPENAI_API_VERSION = ConfigVar( + 'IMAGES_EDIT_OPENAI_API_VERSION', + 'images.edit.openai.api_version', + os.getenv('IMAGES_EDIT_OPENAI_API_VERSION', ''), +) + +IMAGES_EDIT_OPENAI_API_KEY = ConfigVar( + 'IMAGES_EDIT_OPENAI_API_KEY', + 'images.edit.openai.api_key', + os.getenv('IMAGES_EDIT_OPENAI_API_KEY', OPENAI_API_KEY), +) + +IMAGES_EDIT_GEMINI_API_BASE_URL = ConfigVar( + 'IMAGES_EDIT_GEMINI_API_BASE_URL', + 'images.edit.gemini.api_base_url', + os.getenv('IMAGES_EDIT_GEMINI_API_BASE_URL', GEMINI_API_BASE_URL), +) +IMAGES_EDIT_GEMINI_API_KEY = ConfigVar( + 'IMAGES_EDIT_GEMINI_API_KEY', + 'images.edit.gemini.api_key', + os.getenv('IMAGES_EDIT_GEMINI_API_KEY', GEMINI_API_KEY), ) -IMAGES_GEMINI_API_BASE_URL = PersistentConfig( - "IMAGES_GEMINI_API_BASE_URL", - "image_generation.gemini.api_base_url", - os.getenv("IMAGES_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL), +IMAGES_EDIT_COMFYUI_BASE_URL = ConfigVar( + 'IMAGES_EDIT_COMFYUI_BASE_URL', + 'images.edit.comfyui.base_url', + os.getenv('IMAGES_EDIT_COMFYUI_BASE_URL', ''), ) -IMAGES_GEMINI_API_KEY = PersistentConfig( - "IMAGES_GEMINI_API_KEY", - "image_generation.gemini.api_key", - os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY), +IMAGES_EDIT_COMFYUI_API_KEY = ConfigVar( + 'IMAGES_EDIT_COMFYUI_API_KEY', + 'images.edit.comfyui.api_key', + os.getenv('IMAGES_EDIT_COMFYUI_API_KEY', ''), ) -IMAGES_GEMINI_ENDPOINT_METHOD = PersistentConfig( - "IMAGES_GEMINI_ENDPOINT_METHOD", - "image_generation.gemini.endpoint_method", - os.getenv("IMAGES_GEMINI_ENDPOINT_METHOD", ""), +IMAGES_EDIT_COMFYUI_WORKFLOW = ConfigVar( + 'IMAGES_EDIT_COMFYUI_WORKFLOW', + 'images.edit.comfyui.workflow', + os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW', ''), ) -ENABLE_IMAGE_EDIT = PersistentConfig( - "ENABLE_IMAGE_EDIT", - "images.edit.enable", - os.environ.get("ENABLE_IMAGE_EDIT", "").lower() == "true", -) - -IMAGE_EDIT_ENGINE = PersistentConfig( - "IMAGE_EDIT_ENGINE", - "images.edit.engine", - os.getenv("IMAGE_EDIT_ENGINE", "openai"), -) - -IMAGE_EDIT_MODEL = PersistentConfig( - "IMAGE_EDIT_MODEL", - "images.edit.model", - os.getenv("IMAGE_EDIT_MODEL", ""), -) - -IMAGE_EDIT_SIZE = PersistentConfig( - "IMAGE_EDIT_SIZE", "images.edit.size", os.getenv("IMAGE_EDIT_SIZE", "") -) - -IMAGES_EDIT_OPENAI_API_BASE_URL = PersistentConfig( - "IMAGES_EDIT_OPENAI_API_BASE_URL", - "images.edit.openai.api_base_url", - os.getenv("IMAGES_EDIT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), -) -IMAGES_EDIT_OPENAI_API_VERSION = PersistentConfig( - "IMAGES_EDIT_OPENAI_API_VERSION", - "images.edit.openai.api_version", - os.getenv("IMAGES_EDIT_OPENAI_API_VERSION", ""), -) - -IMAGES_EDIT_OPENAI_API_KEY = PersistentConfig( - "IMAGES_EDIT_OPENAI_API_KEY", - "images.edit.openai.api_key", - os.getenv("IMAGES_EDIT_OPENAI_API_KEY", OPENAI_API_KEY), -) - -IMAGES_EDIT_GEMINI_API_BASE_URL = PersistentConfig( - "IMAGES_EDIT_GEMINI_API_BASE_URL", - "images.edit.gemini.api_base_url", - os.getenv("IMAGES_EDIT_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL), -) -IMAGES_EDIT_GEMINI_API_KEY = PersistentConfig( - "IMAGES_EDIT_GEMINI_API_KEY", - "images.edit.gemini.api_key", - os.getenv("IMAGES_EDIT_GEMINI_API_KEY", GEMINI_API_KEY), -) - - -IMAGES_EDIT_COMFYUI_BASE_URL = PersistentConfig( - "IMAGES_EDIT_COMFYUI_BASE_URL", - "images.edit.comfyui.base_url", - os.getenv("IMAGES_EDIT_COMFYUI_BASE_URL", ""), -) -IMAGES_EDIT_COMFYUI_API_KEY = PersistentConfig( - "IMAGES_EDIT_COMFYUI_API_KEY", - "images.edit.comfyui.api_key", - os.getenv("IMAGES_EDIT_COMFYUI_API_KEY", ""), -) - -IMAGES_EDIT_COMFYUI_WORKFLOW = PersistentConfig( - "IMAGES_EDIT_COMFYUI_WORKFLOW", - "images.edit.comfyui.workflow", - os.getenv("IMAGES_EDIT_COMFYUI_WORKFLOW", ""), -) - -images_edit_comfyui_workflow_nodes = os.getenv("IMAGES_EDIT_COMFYUI_WORKFLOW_NODES", "") +images_edit_comfyui_workflow_nodes = os.getenv('IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', '') try: images_edit_comfyui_workflow_nodes = json.loads(images_edit_comfyui_workflow_nodes) except json.JSONDecodeError: images_edit_comfyui_workflow_nodes = [] -IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig( - "IMAGES_EDIT_COMFYUI_WORKFLOW_NODES", - "images.edit.comfyui.nodes", +IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = ConfigVar( + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', + 'images.edit.comfyui.nodes', images_edit_comfyui_workflow_nodes, ) @@ -3731,294 +2274,1768 @@ IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = PersistentConfig( #################################### # Transcription -WHISPER_MODEL = PersistentConfig( - "WHISPER_MODEL", - "audio.stt.whisper_model", - os.getenv("WHISPER_MODEL", "base"), +WHISPER_MODEL = ConfigVar( + 'WHISPER_MODEL', + 'audio.stt.whisper_model', + os.getenv('WHISPER_MODEL', 'base'), ) -WHISPER_COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "int8") -WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models") -WHISPER_MODEL_AUTO_UPDATE = ( - not OFFLINE_MODE - and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true" -) +WHISPER_COMPUTE_TYPE = os.getenv('WHISPER_COMPUTE_TYPE', 'int8') +WHISPER_MODEL_DIR = os.getenv('WHISPER_MODEL_DIR', f'{CACHE_DIR}/whisper/models') +WHISPER_MODEL_AUTO_UPDATE = not OFFLINE_MODE and os.getenv('WHISPER_MODEL_AUTO_UPDATE', '').lower() == 'true' -WHISPER_VAD_FILTER = os.getenv("WHISPER_VAD_FILTER", "False").lower() == "true" +WHISPER_VAD_FILTER = os.getenv('WHISPER_VAD_FILTER', 'False').lower() == 'true' -WHISPER_MULTILINGUAL = os.getenv("WHISPER_MULTILINGUAL", "False").lower() == "true" +WHISPER_MULTILINGUAL = os.getenv('WHISPER_MULTILINGUAL', 'False').lower() == 'true' -WHISPER_LANGUAGE = os.getenv("WHISPER_LANGUAGE", "").lower() or None +WHISPER_LANGUAGE = os.getenv('WHISPER_LANGUAGE', '').lower() or None # Add Deepgram configuration -DEEPGRAM_API_KEY = PersistentConfig( - "DEEPGRAM_API_KEY", - "audio.stt.deepgram.api_key", - os.getenv("DEEPGRAM_API_KEY", ""), +DEEPGRAM_API_KEY = ConfigVar( + 'DEEPGRAM_API_KEY', + 'audio.stt.deepgram.api_key', + os.getenv('DEEPGRAM_API_KEY', ''), ) # ElevenLabs configuration -ELEVENLABS_API_BASE_URL = os.getenv( - "ELEVENLABS_API_BASE_URL", "https://api.elevenlabs.io" +ELEVENLABS_API_BASE_URL = os.getenv('ELEVENLABS_API_BASE_URL', 'https://api.elevenlabs.io') + +AUDIO_STT_OPENAI_API_BASE_URL = ConfigVar( + 'AUDIO_STT_OPENAI_API_BASE_URL', + 'audio.stt.openai.api_base_url', + os.getenv('AUDIO_STT_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), ) -AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( - "AUDIO_STT_OPENAI_API_BASE_URL", - "audio.stt.openai.api_base_url", - os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +AUDIO_STT_OPENAI_API_KEY = ConfigVar( + 'AUDIO_STT_OPENAI_API_KEY', + 'audio.stt.openai.api_key', + os.getenv('AUDIO_STT_OPENAI_API_KEY', OPENAI_API_KEY), ) -AUDIO_STT_OPENAI_API_KEY = PersistentConfig( - "AUDIO_STT_OPENAI_API_KEY", - "audio.stt.openai.api_key", - os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY), +AUDIO_STT_ENGINE = ConfigVar( + 'AUDIO_STT_ENGINE', + 'audio.stt.engine', + os.getenv('AUDIO_STT_ENGINE', ''), ) -AUDIO_STT_ENGINE = PersistentConfig( - "AUDIO_STT_ENGINE", - "audio.stt.engine", - os.getenv("AUDIO_STT_ENGINE", ""), +AUDIO_STT_MODEL = ConfigVar( + 'AUDIO_STT_MODEL', + 'audio.stt.model', + os.getenv('AUDIO_STT_MODEL', ''), ) -AUDIO_STT_MODEL = PersistentConfig( - "AUDIO_STT_MODEL", - "audio.stt.model", - os.getenv("AUDIO_STT_MODEL", ""), -) - -AUDIO_STT_SUPPORTED_CONTENT_TYPES = PersistentConfig( - "AUDIO_STT_SUPPORTED_CONTENT_TYPES", - "audio.stt.supported_content_types", +AUDIO_STT_SUPPORTED_CONTENT_TYPES = ConfigVar( + 'AUDIO_STT_SUPPORTED_CONTENT_TYPES', + 'audio.stt.supported_content_types', [ content_type.strip() - for content_type in os.environ.get( - "AUDIO_STT_SUPPORTED_CONTENT_TYPES", "" - ).split(",") + for content_type in os.getenv('AUDIO_STT_SUPPORTED_CONTENT_TYPES', '').split(',') if content_type.strip() ], ) -AUDIO_STT_AZURE_API_KEY = PersistentConfig( - "AUDIO_STT_AZURE_API_KEY", - "audio.stt.azure.api_key", - os.getenv("AUDIO_STT_AZURE_API_KEY", ""), +AUDIO_STT_ALLOWED_EXTENSIONS = ConfigVar( + 'AUDIO_STT_ALLOWED_EXTENSIONS', + 'audio.stt.allowed_extensions', + [ + ext.strip() + for ext in os.getenv( + 'AUDIO_STT_ALLOWED_EXTENSIONS', + 'mp3,wav,m4a,webm,ogg,flac,mp4,mpga,mpeg', + ).split(',') + if ext.strip() + ], ) -AUDIO_STT_AZURE_REGION = PersistentConfig( - "AUDIO_STT_AZURE_REGION", - "audio.stt.azure.region", - os.getenv("AUDIO_STT_AZURE_REGION", ""), +AUDIO_STT_AZURE_API_KEY = ConfigVar( + 'AUDIO_STT_AZURE_API_KEY', + 'audio.stt.azure.api_key', + os.getenv('AUDIO_STT_AZURE_API_KEY', ''), ) -AUDIO_STT_AZURE_LOCALES = PersistentConfig( - "AUDIO_STT_AZURE_LOCALES", - "audio.stt.azure.locales", - os.getenv("AUDIO_STT_AZURE_LOCALES", ""), +AUDIO_STT_AZURE_REGION = ConfigVar( + 'AUDIO_STT_AZURE_REGION', + 'audio.stt.azure.region', + os.getenv('AUDIO_STT_AZURE_REGION', ''), ) -AUDIO_STT_AZURE_BASE_URL = PersistentConfig( - "AUDIO_STT_AZURE_BASE_URL", - "audio.stt.azure.base_url", - os.getenv("AUDIO_STT_AZURE_BASE_URL", ""), +AUDIO_STT_AZURE_LOCALES = ConfigVar( + 'AUDIO_STT_AZURE_LOCALES', + 'audio.stt.azure.locales', + os.getenv('AUDIO_STT_AZURE_LOCALES', ''), ) -AUDIO_STT_AZURE_MAX_SPEAKERS = PersistentConfig( - "AUDIO_STT_AZURE_MAX_SPEAKERS", - "audio.stt.azure.max_speakers", - os.getenv("AUDIO_STT_AZURE_MAX_SPEAKERS", ""), +AUDIO_STT_AZURE_BASE_URL = ConfigVar( + 'AUDIO_STT_AZURE_BASE_URL', + 'audio.stt.azure.base_url', + os.getenv('AUDIO_STT_AZURE_BASE_URL', ''), ) -AUDIO_STT_MISTRAL_API_KEY = PersistentConfig( - "AUDIO_STT_MISTRAL_API_KEY", - "audio.stt.mistral.api_key", - os.getenv("AUDIO_STT_MISTRAL_API_KEY", ""), +AUDIO_STT_AZURE_MAX_SPEAKERS = ConfigVar( + 'AUDIO_STT_AZURE_MAX_SPEAKERS', + 'audio.stt.azure.max_speakers', + os.getenv('AUDIO_STT_AZURE_MAX_SPEAKERS', ''), ) -AUDIO_STT_MISTRAL_API_BASE_URL = PersistentConfig( - "AUDIO_STT_MISTRAL_API_BASE_URL", - "audio.stt.mistral.api_base_url", - os.getenv("AUDIO_STT_MISTRAL_API_BASE_URL", "https://api.mistral.ai/v1"), +AUDIO_STT_MISTRAL_API_KEY = ConfigVar( + 'AUDIO_STT_MISTRAL_API_KEY', + 'audio.stt.mistral.api_key', + os.getenv('AUDIO_STT_MISTRAL_API_KEY', ''), ) -AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = PersistentConfig( - "AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS", - "audio.stt.mistral.use_chat_completions", - os.getenv("AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS", "false").lower() == "true", +AUDIO_STT_MISTRAL_API_BASE_URL = ConfigVar( + 'AUDIO_STT_MISTRAL_API_BASE_URL', + 'audio.stt.mistral.api_base_url', + os.getenv('AUDIO_STT_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1'), ) -AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig( - "AUDIO_TTS_OPENAI_API_BASE_URL", - "audio.tts.openai.api_base_url", - os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), -) -AUDIO_TTS_OPENAI_API_KEY = PersistentConfig( - "AUDIO_TTS_OPENAI_API_KEY", - "audio.tts.openai.api_key", - os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY), +AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = ConfigVar( + 'AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS', + 'audio.stt.mistral.use_chat_completions', + os.getenv('AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS', 'false').lower() == 'true', ) -audio_tts_openai_params = os.getenv("AUDIO_TTS_OPENAI_PARAMS", "") +AUDIO_TTS_OPENAI_API_BASE_URL = ConfigVar( + 'AUDIO_TTS_OPENAI_API_BASE_URL', + 'audio.tts.openai.api_base_url', + os.getenv('AUDIO_TTS_OPENAI_API_BASE_URL', OPENAI_API_BASE_URL), +) +AUDIO_TTS_OPENAI_API_KEY = ConfigVar( + 'AUDIO_TTS_OPENAI_API_KEY', + 'audio.tts.openai.api_key', + os.getenv('AUDIO_TTS_OPENAI_API_KEY', OPENAI_API_KEY), +) + +audio_tts_openai_params = os.getenv('AUDIO_TTS_OPENAI_PARAMS', '') try: audio_tts_openai_params = json.loads(audio_tts_openai_params) except json.JSONDecodeError: audio_tts_openai_params = {} -AUDIO_TTS_OPENAI_PARAMS = PersistentConfig( - "AUDIO_TTS_OPENAI_PARAMS", - "audio.tts.openai.params", +AUDIO_TTS_OPENAI_PARAMS = ConfigVar( + 'AUDIO_TTS_OPENAI_PARAMS', + 'audio.tts.openai.params', audio_tts_openai_params, ) -AUDIO_TTS_API_KEY = PersistentConfig( - "AUDIO_TTS_API_KEY", - "audio.tts.api_key", - os.getenv("AUDIO_TTS_API_KEY", ""), +AUDIO_TTS_API_KEY = ConfigVar( + 'AUDIO_TTS_API_KEY', + 'audio.tts.api_key', + os.getenv('AUDIO_TTS_API_KEY', ''), ) -AUDIO_TTS_ENGINE = PersistentConfig( - "AUDIO_TTS_ENGINE", - "audio.tts.engine", - os.getenv("AUDIO_TTS_ENGINE", ""), +AUDIO_TTS_ENGINE = ConfigVar( + 'AUDIO_TTS_ENGINE', + 'audio.tts.engine', + os.getenv('AUDIO_TTS_ENGINE', ''), ) -AUDIO_TTS_MODEL = PersistentConfig( - "AUDIO_TTS_MODEL", - "audio.tts.model", - os.getenv("AUDIO_TTS_MODEL", "tts-1"), # OpenAI default model +AUDIO_TTS_MODEL = ConfigVar( + 'AUDIO_TTS_MODEL', + 'audio.tts.model', + os.getenv('AUDIO_TTS_MODEL', 'tts-1'), # OpenAI default model ) -AUDIO_TTS_VOICE = PersistentConfig( - "AUDIO_TTS_VOICE", - "audio.tts.voice", - os.getenv("AUDIO_TTS_VOICE", "alloy"), # OpenAI default voice +AUDIO_TTS_VOICE = ConfigVar( + 'AUDIO_TTS_VOICE', + 'audio.tts.voice', + os.getenv('AUDIO_TTS_VOICE', 'alloy'), # OpenAI default voice ) -AUDIO_TTS_SPLIT_ON = PersistentConfig( - "AUDIO_TTS_SPLIT_ON", - "audio.tts.split_on", - os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"), +AUDIO_TTS_SPLIT_ON = ConfigVar( + 'AUDIO_TTS_SPLIT_ON', + 'audio.tts.split_on', + os.getenv('AUDIO_TTS_SPLIT_ON', 'punctuation'), ) -AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig( - "AUDIO_TTS_AZURE_SPEECH_REGION", - "audio.tts.azure.speech_region", - os.getenv("AUDIO_TTS_AZURE_SPEECH_REGION", ""), +AUDIO_TTS_AZURE_SPEECH_REGION = ConfigVar( + 'AUDIO_TTS_AZURE_SPEECH_REGION', + 'audio.tts.azure.speech_region', + os.getenv('AUDIO_TTS_AZURE_SPEECH_REGION', ''), ) -AUDIO_TTS_AZURE_SPEECH_BASE_URL = PersistentConfig( - "AUDIO_TTS_AZURE_SPEECH_BASE_URL", - "audio.tts.azure.speech_base_url", - os.getenv("AUDIO_TTS_AZURE_SPEECH_BASE_URL", ""), +AUDIO_TTS_AZURE_SPEECH_BASE_URL = ConfigVar( + 'AUDIO_TTS_AZURE_SPEECH_BASE_URL', + 'audio.tts.azure.speech_base_url', + os.getenv('AUDIO_TTS_AZURE_SPEECH_BASE_URL', ''), ) -AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig( - "AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", - "audio.tts.azure.speech_output_format", +AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = ConfigVar( + 'AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT', + 'audio.tts.azure.speech_output_format', + os.getenv('AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT', 'audio-24khz-160kbitrate-mono-mp3'), +) + +AUDIO_TTS_MISTRAL_API_KEY = ConfigVar( + 'AUDIO_TTS_MISTRAL_API_KEY', + 'audio.tts.mistral.api_key', + os.getenv('AUDIO_TTS_MISTRAL_API_KEY', ''), +) + +AUDIO_TTS_MISTRAL_API_BASE_URL = ConfigVar( + 'AUDIO_TTS_MISTRAL_API_BASE_URL', + 'audio.tts.mistral.api_base_url', + os.getenv('AUDIO_TTS_MISTRAL_API_BASE_URL', 'https://api.mistral.ai/v1'), +) + +#################################### +# WEBUI +#################################### + + +WEBUI_URL = ConfigVar('WEBUI_URL', 'webui.url', os.getenv('WEBUI_URL', '')) + + +ENABLE_SIGNUP = ConfigVar( + 'ENABLE_SIGNUP', + 'ui.enable_signup', + (False if not WEBUI_AUTH else os.getenv('ENABLE_SIGNUP', 'True').lower() == 'true'), +) + +ENABLE_LOGIN_FORM = ConfigVar( + 'ENABLE_LOGIN_FORM', + 'ui.enable_login_form', + os.getenv('ENABLE_LOGIN_FORM', 'True').lower() == 'true', +) + +ENABLE_PASSWORD_CHANGE_FORM = ConfigVar( + 'ENABLE_PASSWORD_CHANGE_FORM', + 'ui.enable_password_change_form', + os.getenv('ENABLE_PASSWORD_CHANGE_FORM', 'True').lower() == 'true', +) + +ENABLE_PASSWORD_AUTH = os.getenv('ENABLE_PASSWORD_AUTH', 'True').lower() == 'true' + +DEFAULT_LOCALE = ConfigVar( + 'DEFAULT_LOCALE', + 'ui.default_locale', + os.getenv('DEFAULT_LOCALE', ''), +) + +DEFAULT_MODELS = ConfigVar('DEFAULT_MODELS', 'ui.default_models', os.getenv('DEFAULT_MODELS', None)) + +DEFAULT_PINNED_MODELS = ConfigVar( + 'DEFAULT_PINNED_MODELS', + 'ui.default_pinned_models', + os.getenv('DEFAULT_PINNED_MODELS', None), +) + +try: + default_prompt_suggestions = json.loads(os.getenv('DEFAULT_PROMPT_SUGGESTIONS', '[]')) +except Exception as e: + log.exception(f'Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}') + default_prompt_suggestions = [] +if default_prompt_suggestions == []: + default_prompt_suggestions = [ + { + 'title': ['Help me study', 'vocabulary for a college entrance exam'], + 'content': "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.", + }, + { + 'title': ['Give me ideas', "for what to do with my kids' art"], + 'content': "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.", + }, + { + 'title': ['Tell me a fun fact', 'about the Roman Empire'], + 'content': 'Tell me a random fun fact about the Roman Empire', + }, + { + 'title': ['Show me a code snippet', "of a website's sticky header"], + 'content': "Show me a code snippet of a website's sticky header in CSS and JavaScript.", + }, + { + 'title': [ + 'Explain options trading', + "if I'm familiar with buying and selling stocks", + ], + 'content': "Explain options trading in simple terms if I'm familiar with buying and selling stocks.", + }, + { + 'title': ['Overcome procrastination', 'give me tips'], + 'content': 'Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?', + }, + ] + +DEFAULT_PROMPT_SUGGESTIONS = ConfigVar( + 'DEFAULT_PROMPT_SUGGESTIONS', + 'ui.prompt_suggestions', + default_prompt_suggestions, +) + +MODEL_ORDER_LIST = ConfigVar( + 'MODEL_ORDER_LIST', + 'ui.model_order_list', + [], +) + +try: + default_model_metadata = json.loads(os.getenv('DEFAULT_MODEL_METADATA', '{}')) +except Exception as e: + log.exception(f'Error loading DEFAULT_MODEL_METADATA: {e}') + default_model_metadata = {} + +DEFAULT_MODEL_METADATA = ConfigVar( + 'DEFAULT_MODEL_METADATA', + 'models.default_metadata', + default_model_metadata, +) + +try: + default_model_params = json.loads(os.getenv('DEFAULT_MODEL_PARAMS', '{}')) +except Exception as e: + log.exception(f'Error loading DEFAULT_MODEL_PARAMS: {e}') + default_model_params = {} + +DEFAULT_MODEL_PARAMS = ConfigVar( + 'DEFAULT_MODEL_PARAMS', + 'models.default_params', + default_model_params, +) + +DEFAULT_USER_ROLE = ConfigVar( + 'DEFAULT_USER_ROLE', + 'ui.default_user_role', + os.getenv('DEFAULT_USER_ROLE', 'pending'), +) + +DEFAULT_GROUP_ID = ConfigVar( + 'DEFAULT_GROUP_ID', + 'ui.default_group_id', + os.getenv('DEFAULT_GROUP_ID', ''), +) + +PENDING_USER_OVERLAY_TITLE = ConfigVar( + 'PENDING_USER_OVERLAY_TITLE', + 'ui.pending_user_overlay_title', + os.getenv('PENDING_USER_OVERLAY_TITLE', ''), +) + +PENDING_USER_OVERLAY_CONTENT = ConfigVar( + 'PENDING_USER_OVERLAY_CONTENT', + 'ui.pending_user_overlay_content', + os.getenv('PENDING_USER_OVERLAY_CONTENT', ''), +) + + +RESPONSE_WATERMARK = ConfigVar( + 'RESPONSE_WATERMARK', + 'ui.watermark', + os.getenv('RESPONSE_WATERMARK', ''), +) + +IFRAME_CSP = os.getenv('IFRAME_CSP', '') + +USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_NOTES_ALLOW_SHARING = os.getenv('USER_PERMISSIONS_NOTES_ALLOW_SHARING', 'False').lower() == 'true' + +USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = ( + os.getenv('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true' +) + + +USER_PERMISSIONS_CHAT_CONTROLS = os.getenv('USER_PERMISSIONS_CHAT_CONTROLS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_VALVES = os.getenv('USER_PERMISSIONS_CHAT_VALVES', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = os.getenv('USER_PERMISSIONS_CHAT_SYSTEM_PROMPT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_PARAMS = os.getenv('USER_PERMISSIONS_CHAT_PARAMS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_FILE_UPLOAD = os.getenv('USER_PERMISSIONS_CHAT_FILE_UPLOAD', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_WEB_UPLOAD = os.getenv('USER_PERMISSIONS_CHAT_WEB_UPLOAD', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_DELETE = os.getenv('USER_PERMISSIONS_CHAT_DELETE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_DELETE_MESSAGE = os.getenv('USER_PERMISSIONS_CHAT_DELETE_MESSAGE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE = os.getenv('USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE = ( + os.getenv('USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE', 'True').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_RATE_RESPONSE = os.getenv('USER_PERMISSIONS_CHAT_RATE_RESPONSE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_EDIT = os.getenv('USER_PERMISSIONS_CHAT_EDIT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_SHARE = os.getenv('USER_PERMISSIONS_CHAT_SHARE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_EXPORT = os.getenv('USER_PERMISSIONS_CHAT_EXPORT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_STT = os.getenv('USER_PERMISSIONS_CHAT_STT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TTS = os.getenv('USER_PERMISSIONS_CHAT_TTS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_CALL = os.getenv('USER_PERMISSIONS_CHAT_CALL', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = os.getenv('USER_PERMISSIONS_CHAT_MULTIPLE_MODELS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TEMPORARY = os.getenv('USER_PERMISSIONS_CHAT_TEMPORARY', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = ( + os.getenv('USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = ( + os.getenv('USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_WEB_SEARCH = os.getenv('USER_PERMISSIONS_FEATURES_WEB_SEARCH', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = ( + os.getenv('USER_PERMISSIONS_FEATURES_IMAGE_GENERATION', 'True').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = ( + os.getenv('USER_PERMISSIONS_FEATURES_CODE_INTERPRETER', 'True').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_FOLDERS = os.getenv('USER_PERMISSIONS_FEATURES_FOLDERS', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_NOTES = os.getenv('USER_PERMISSIONS_FEATURES_NOTES', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_CHANNELS = os.getenv('USER_PERMISSIONS_FEATURES_CHANNELS', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_API_KEYS = os.getenv('USER_PERMISSIONS_FEATURES_API_KEYS', 'False').lower() == 'true' + +USER_PERMISSIONS_FEATURES_MEMORIES = os.getenv('USER_PERMISSIONS_FEATURES_MEMORIES', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_AUTOMATIONS = os.getenv('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true' + +USER_PERMISSIONS_FEATURES_CALENDAR = os.getenv('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true' + + +USER_PERMISSIONS_SETTINGS_INTERFACE = os.getenv('USER_PERMISSIONS_SETTINGS_INTERFACE', 'True').lower() == 'true' + + +DEFAULT_USER_PERMISSIONS = { + 'workspace': { + 'models': USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS, + 'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS, + 'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS, + 'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS, + 'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS, + 'models_import': USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT, + 'models_export': USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT, + 'prompts_import': USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT, + 'prompts_export': USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT, + 'tools_import': USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT, + 'tools_export': USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT, + }, + 'sharing': { + 'models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING, + 'public_models': USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING, + 'knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING, + 'public_knowledge': USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING, + 'prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING, + 'public_prompts': USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING, + 'tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING, + 'public_tools': USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING, + 'skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING, + 'public_skills': USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING, + 'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING, + 'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING, + 'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING, + 'public_calendars': USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING, + }, + 'access_grants': { + 'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS, + }, + 'chat': { + 'controls': USER_PERMISSIONS_CHAT_CONTROLS, + 'valves': USER_PERMISSIONS_CHAT_VALVES, + 'system_prompt': USER_PERMISSIONS_CHAT_SYSTEM_PROMPT, + 'params': USER_PERMISSIONS_CHAT_PARAMS, + 'file_upload': USER_PERMISSIONS_CHAT_FILE_UPLOAD, + 'web_upload': USER_PERMISSIONS_CHAT_WEB_UPLOAD, + 'delete': USER_PERMISSIONS_CHAT_DELETE, + 'delete_message': USER_PERMISSIONS_CHAT_DELETE_MESSAGE, + 'continue_response': USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE, + 'regenerate_response': USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE, + 'rate_response': USER_PERMISSIONS_CHAT_RATE_RESPONSE, + 'edit': USER_PERMISSIONS_CHAT_EDIT, + 'share': USER_PERMISSIONS_CHAT_SHARE, + 'export': USER_PERMISSIONS_CHAT_EXPORT, + 'stt': USER_PERMISSIONS_CHAT_STT, + 'tts': USER_PERMISSIONS_CHAT_TTS, + 'call': USER_PERMISSIONS_CHAT_CALL, + 'multiple_models': USER_PERMISSIONS_CHAT_MULTIPLE_MODELS, + 'temporary': USER_PERMISSIONS_CHAT_TEMPORARY, + 'temporary_enforced': USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED, + }, + 'features': { + # General features + 'api_keys': USER_PERMISSIONS_FEATURES_API_KEYS, + 'notes': USER_PERMISSIONS_FEATURES_NOTES, + 'folders': USER_PERMISSIONS_FEATURES_FOLDERS, + 'channels': USER_PERMISSIONS_FEATURES_CHANNELS, + 'direct_tool_servers': USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS, + # Chat features + 'web_search': USER_PERMISSIONS_FEATURES_WEB_SEARCH, + 'image_generation': USER_PERMISSIONS_FEATURES_IMAGE_GENERATION, + 'code_interpreter': USER_PERMISSIONS_FEATURES_CODE_INTERPRETER, + 'memories': USER_PERMISSIONS_FEATURES_MEMORIES, + 'automations': USER_PERMISSIONS_FEATURES_AUTOMATIONS, + 'calendar': USER_PERMISSIONS_FEATURES_CALENDAR, + }, + 'settings': { + 'interface': USER_PERMISSIONS_SETTINGS_INTERFACE, + }, +} + +USER_PERMISSIONS = ConfigVar( + 'USER_PERMISSIONS', + 'user.permissions', + DEFAULT_USER_PERMISSIONS, +) + +ENABLE_FOLDERS = ConfigVar( + 'ENABLE_FOLDERS', + 'folders.enable', + os.getenv('ENABLE_FOLDERS', 'True').lower() == 'true', +) + +FOLDER_MAX_FILE_COUNT = ConfigVar( + 'FOLDER_MAX_FILE_COUNT', + 'folders.max_file_count', + os.getenv('FOLDER_MAX_FILE_COUNT', ''), +) + +ENABLE_CHANNELS = ConfigVar( + 'ENABLE_CHANNELS', + 'channels.enable', + os.getenv('ENABLE_CHANNELS', 'False').lower() == 'true', +) + +ENABLE_CALENDAR = ConfigVar( + 'ENABLE_CALENDAR', + 'calendar.enable', + os.getenv('ENABLE_CALENDAR', 'True').lower() == 'true', +) + +ENABLE_AUTOMATIONS = ConfigVar( + 'ENABLE_AUTOMATIONS', + 'automations.enable', + os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true', +) + +AUTOMATION_MAX_COUNT = ConfigVar( + 'AUTOMATION_MAX_COUNT', + 'automations.max_count', + os.getenv('AUTOMATION_MAX_COUNT', ''), +) + +AUTOMATION_MIN_INTERVAL = ConfigVar( + 'AUTOMATION_MIN_INTERVAL', + 'automations.min_interval', + os.getenv('AUTOMATION_MIN_INTERVAL', ''), +) + +ENABLE_NOTES = ConfigVar( + 'ENABLE_NOTES', + 'notes.enable', + os.getenv('ENABLE_NOTES', 'True').lower() == 'true', +) + +ENABLE_USER_STATUS = ConfigVar( + 'ENABLE_USER_STATUS', + 'users.enable_status', + os.getenv('ENABLE_USER_STATUS', 'True').lower() == 'true', +) + +ENABLE_EVALUATION_ARENA_MODELS = ConfigVar( + 'ENABLE_EVALUATION_ARENA_MODELS', + 'evaluation.arena.enable', + os.getenv('ENABLE_EVALUATION_ARENA_MODELS', 'True').lower() == 'true', +) +EVALUATION_ARENA_MODELS = ConfigVar( + 'EVALUATION_ARENA_MODELS', + 'evaluation.arena.models', + [], +) + +DEFAULT_ARENA_MODEL = { + 'id': 'arena-model', + 'name': 'Arena Model', + 'meta': { + 'profile_image_url': '/favicon.png', + 'description': 'Submit your questions to anonymous AI chatbots and vote on the best response.', + 'model_ids': None, + }, +} + +WEBHOOK_URL = ConfigVar('WEBHOOK_URL', 'webhook_url', os.getenv('WEBHOOK_URL', '')) + +ENABLE_ADMIN_EXPORT = os.getenv('ENABLE_ADMIN_EXPORT', 'True').lower() == 'true' + +ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = os.getenv('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True').lower() == 'true' + +BYPASS_ADMIN_ACCESS_CONTROL = ( os.getenv( - "AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3" + 'BYPASS_ADMIN_ACCESS_CONTROL', + os.getenv('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True'), + ).lower() + == 'true' +) + +ENABLE_ADMIN_CHAT_ACCESS = os.getenv('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true' + +ENABLE_ADMIN_ANALYTICS = os.getenv('ENABLE_ADMIN_ANALYTICS', 'True').lower() == 'true' + +ENABLE_COMMUNITY_SHARING = ConfigVar( + 'ENABLE_COMMUNITY_SHARING', + 'ui.enable_community_sharing', + os.getenv('ENABLE_COMMUNITY_SHARING', 'True').lower() == 'true', +) + +ENABLE_MESSAGE_RATING = ConfigVar( + 'ENABLE_MESSAGE_RATING', + 'ui.enable_message_rating', + os.getenv('ENABLE_MESSAGE_RATING', 'True').lower() == 'true', +) + +ENABLE_USER_WEBHOOKS = ConfigVar( + 'ENABLE_USER_WEBHOOKS', + 'ui.enable_user_webhooks', + os.getenv('ENABLE_USER_WEBHOOKS', 'False').lower() == 'true', +) + +# FastAPI / AnyIO settings +THREAD_POOL_SIZE = os.getenv('THREAD_POOL_SIZE', None) + +if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str): + try: + THREAD_POOL_SIZE = int(THREAD_POOL_SIZE) + except ValueError: + log.warning(f'THREAD_POOL_SIZE is not a valid integer: {THREAD_POOL_SIZE}. Defaulting to None.') + THREAD_POOL_SIZE = None + + +def validate_cors_origin(origin): + parsed_url = urlparse(origin) + + # Check if the scheme is either http or https, or a custom scheme + schemes = ['http', 'https'] + CORS_ALLOW_CUSTOM_SCHEME + if parsed_url.scheme not in schemes: + raise ValueError( + f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' and CORS_ALLOW_CUSTOM_SCHEME are allowed." + ) + + # Ensure that the netloc (domain + port) is present, indicating it's a valid URL + if not parsed_url.netloc: + raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.") + + +# For production, you should only need one host as +# fastapi serves the svelte-kit built frontend and backend from the same host and port. +# To test CORS_ALLOW_ORIGIN locally, you can set something like +# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080 +# in your .env file depending on your frontend port, 5173 in this case. +CORS_ALLOW_ORIGIN = os.getenv('CORS_ALLOW_ORIGIN', '*').split(';') + +# Allows custom URL schemes (e.g., app://) to be used as origins for CORS. +# Useful for local development or desktop clients with schemes like app:// or other custom protocols. +# Provide a semicolon-separated list of allowed schemes in the environment variable CORS_ALLOW_CUSTOM_SCHEMES. +CORS_ALLOW_CUSTOM_SCHEME = os.getenv('CORS_ALLOW_CUSTOM_SCHEME', '').split(';') + +if CORS_ALLOW_ORIGIN == ['*']: + log.warning("\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n") +else: + # You have to pick between a single wildcard or a list of origins. + # Doing both will result in CORS errors in the browser. + for origin in CORS_ALLOW_ORIGIN: + validate_cors_origin(origin) + + +class BannerModel(BaseModel): + id: str + type: str + title: str | None = None + content: str + dismissible: bool + timestamp: int + + +try: + banners = json.loads(os.getenv('WEBUI_BANNERS', '[]')) + banners = [BannerModel(**banner) for banner in banners] +except Exception as e: + log.exception(f'Error loading WEBUI_BANNERS: {e}') + banners = [] + +WEBUI_BANNERS = ConfigVar('WEBUI_BANNERS', 'ui.banners', banners) + + +SHOW_ADMIN_DETAILS = ConfigVar( + 'SHOW_ADMIN_DETAILS', + 'auth.admin.show', + os.getenv('SHOW_ADMIN_DETAILS', 'true').lower() == 'true', +) + +ADMIN_EMAIL = ConfigVar( + 'ADMIN_EMAIL', + 'auth.admin.email', + os.getenv('ADMIN_EMAIL', None), +) + + +#################################### +# TASKS +#################################### + + +TASK_MODEL = ConfigVar( + 'TASK_MODEL', + 'task.model.default', + os.getenv('TASK_MODEL', ''), +) + +TASK_MODEL_EXTERNAL = ConfigVar( + 'TASK_MODEL_EXTERNAL', + 'task.model.external', + os.getenv('TASK_MODEL_EXTERNAL', ''), +) + +TITLE_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'TITLE_GENERATION_PROMPT_TEMPLATE', + 'task.title.prompt_template', + os.getenv('TITLE_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate a concise, 3-5 word title with an emoji summarizing the chat history. +### Guidelines: +- The title should clearly represent the main theme or subject of the conversation. +- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting. +- Write the title in the chat's primary language; default to English if multilingual. +- Prioritize accuracy over excessive creativity; keep it clear and simple. +- Your entire response must consist solely of the JSON object, without any introductory or concluding text. +- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text. +- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure. +### Output: +JSON format: { "title": "your concise title here" } +### Examples: +- { "title": "📉 Stock Market Trends" }, +- { "title": "🍪 Perfect Chocolate Chip Recipe" }, +- { "title": "Evolution of Music Streaming" }, +- { "title": "Remote Work Productivity Tips" }, +- { "title": "Artificial Intelligence in Healthcare" }, +- { "title": "🎮 Video Game Development Insights" } +### Chat History: + +{{MESSAGES:END:2}} +""" + +TAGS_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'TAGS_GENERATION_PROMPT_TEMPLATE', + 'task.tags.prompt_template', + os.getenv('TAGS_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags. + +### Guidelines: +- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education) +- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation +- If content is too short (less than 3 messages) or too diverse, use only ["General"] +- Use the chat's primary language; default to English if multilingual +- Prioritize accuracy over specificity + +### Output: +JSON format: { "tags": ["tag1", "tag2", "tag3"] } + +### Chat History: + +{{MESSAGES:END:6}} +""" + +IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', + 'task.image.prompt_template', + os.getenv('IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task: +Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements. + +### Guidelines: +- Be descriptive and detailed, focusing on the most important aspects of the image. +- Avoid making assumptions or adding information not present in the image. +- Use the chat's primary language; default to English if multilingual. +- If the image is too complex, focus on the most prominent elements. + +### Output: +Strictly return in JSON format: +{ + "prompt": "Your detailed description here." +} + +### Chat History: + +{{MESSAGES:END:6}} +""" + + +FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', + 'task.follow_up.prompt_template', + os.getenv('FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task: +Suggest 3-5 relevant follow-up questions or prompts that the user might naturally ask next in this conversation as a **user**, based on the chat history, to help continue or deepen the discussion. +### Guidelines: +- Write all follow-up questions from the user’s point of view, directed to the assistant. +- Make questions concise, clear, and directly related to the discussed topic(s). +- Only suggest follow-ups that make sense given the chat content and do not repeat what was already covered. +- If the conversation is very short or not specific, suggest more general (but relevant) follow-ups the user might ask. +- Use the conversation's primary language; default to English if multilingual. +- Response must be a JSON object with a "follow_ups" key containing an array of strings, no extra text or formatting. +### Output: +JSON format: { "follow_ups": ["Question 1?", "Question 2?", "Question 3?"] } +### Chat History: + +{{MESSAGES:END:6}} +""" + +ENABLE_FOLLOW_UP_GENERATION = ConfigVar( + 'ENABLE_FOLLOW_UP_GENERATION', + 'task.follow_up.enable', + os.getenv('ENABLE_FOLLOW_UP_GENERATION', 'True').lower() == 'true', +) + +ENABLE_TAGS_GENERATION = ConfigVar( + 'ENABLE_TAGS_GENERATION', + 'task.tags.enable', + os.getenv('ENABLE_TAGS_GENERATION', 'True').lower() == 'true', +) + +ENABLE_TITLE_GENERATION = ConfigVar( + 'ENABLE_TITLE_GENERATION', + 'task.title.enable', + os.getenv('ENABLE_TITLE_GENERATION', 'True').lower() == 'true', +) + + +ENABLE_SEARCH_QUERY_GENERATION = ConfigVar( + 'ENABLE_SEARCH_QUERY_GENERATION', + 'task.query.search.enable', + os.getenv('ENABLE_SEARCH_QUERY_GENERATION', 'True').lower() == 'true', +) + +ENABLE_RETRIEVAL_QUERY_GENERATION = ConfigVar( + 'ENABLE_RETRIEVAL_QUERY_GENERATION', + 'task.query.retrieval.enable', + os.getenv('ENABLE_RETRIEVAL_QUERY_GENERATION', 'True').lower() == 'true', +) + + +QUERY_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'QUERY_GENERATION_PROMPT_TEMPLATE', + 'task.query.prompt_template', + os.getenv('QUERY_GENERATION_PROMPT_TEMPLATE', ''), +) + +DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task: +Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list. + +### Guidelines: +- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited. +- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic. +- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }. +- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information. +- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions. +- Today's date is: {{CURRENT_DATE}}. +- Always prioritize providing actionable and broad queries that maximize informational coverage. + +### Output: +Strictly return in JSON format: +{ + "queries": ["query1", "query2"] +} + +### Chat History: + +{{MESSAGES:END:6}} + +""" + +ENABLE_AUTOCOMPLETE_GENERATION = ConfigVar( + 'ENABLE_AUTOCOMPLETE_GENERATION', + 'task.autocomplete.enable', + os.getenv('ENABLE_AUTOCOMPLETE_GENERATION', 'False').lower() == 'true', +) + +AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = ConfigVar( + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', + 'task.autocomplete.input_max_length', + int(os.getenv('AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', '-1')), +) + +AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', + 'task.autocomplete.prompt_template', + os.getenv('AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', ''), +) + + +DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task: +You are an autocompletion system. Continue the text in `` based on the **completion type** in `` and the given language. + +### **Instructions**: +1. Analyze `` for context and meaning. +2. Use `` to guide your output: + - **General**: Provide a natural, concise continuation. + - **Search Query**: Complete as if generating a realistic search query. +3. Start as if you are directly continuing ``. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text. +4. Ensure the continuation: + - Flows naturally from ``. + - Avoids repetition, overexplaining, or unrelated ideas. +5. If unsure, return: `{ "text": "" }`. + +### **Output Rules**: +- Respond only in JSON format: `{ "text": "" }`. + +### **Examples**: +#### Example 1: +Input: +General +The sun was setting over the horizon, painting the sky +Output: +{ "text": "with vibrant shades of orange and pink." } + +#### Example 2: +Input: +Search Query +Top-rated restaurants in +Output: +{ "text": "New York City for Italian cuisine." } + +--- +### Context: + +{{MESSAGES:END:6}} + +{{TYPE}} +{{PROMPT}} +#### Output: +""" + + +VOICE_MODE_PROMPT_TEMPLATE = ConfigVar( + 'VOICE_MODE_PROMPT_TEMPLATE', + 'task.voice.prompt_template', + os.getenv('VOICE_MODE_PROMPT_TEMPLATE', ''), +) + +ENABLE_VOICE_MODE_PROMPT = ConfigVar( + 'ENABLE_VOICE_MODE_PROMPT', + 'task.voice.prompt.enable', + os.getenv('ENABLE_VOICE_MODE_PROMPT', 'True').lower() == 'true', +) + +DEFAULT_VOICE_MODE_PROMPT_TEMPLATE = """You are a friendly, concise voice assistant. + +Everything you say will be spoken aloud. +Keep responses short, clear, and natural. + +STYLE: +- Use simple words and short sentences. +- Sound warm and conversational. +- Avoid long explanations, lists, or complex phrasing. + +BEHAVIOR: +- Give the quickest helpful answer first. +- Offer extra detail only if needed. +- Ask for clarification only when necessary. + +VOICE OPTIMIZATION: +- Break information into small, easy-to-hear chunks. +- Avoid dense wording or anything that sounds like reading text. + +ERROR HANDLING: +- If unsure, say so briefly and offer options. +- If something is unsafe or impossible, decline kindly and suggest a safe alternative. + +Stay consistent, helpful, and easy to listen to.""" + +TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ConfigVar( + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', + 'task.tools.prompt_template', + os.getenv('TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', ''), +) + + +DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}} + +Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines: + +- Return only the JSON object, without any additional text or explanation. + +- If no tools match the query, return an empty array: + { + "tool_calls": [] + } + +- If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include: + - "name": The tool's name. + - "parameters": A dictionary of required parameters and their corresponding values. + +The format for the JSON response is strictly: +{ + "tool_calls": [ + {"name": "toolName1", "parameters": {"key1": "value1"}}, + {"name": "toolName2", "parameters": {"key2": "value2"}} + ] +}""" + + +DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱). + +Message: ```{{prompt}}```""" + +DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}" + +Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability. + +Responses from models: {{responses}}""" + + +#################################### +# Auth +#################################### + +ENABLE_API_KEYS = ConfigVar( + 'ENABLE_API_KEYS', + 'auth.enable_api_keys', + os.getenv('ENABLE_API_KEYS', 'False').lower() == 'true', +) + +ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ConfigVar( + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS', + 'auth.api_key.endpoint_restrictions', + os.getenv( + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS', + os.getenv('ENABLE_API_KEY_ENDPOINT_RESTRICTIONS', 'False'), + ).lower() + == 'true', +) + +API_KEYS_ALLOWED_ENDPOINTS = ConfigVar( + 'API_KEYS_ALLOWED_ENDPOINTS', + 'auth.api_key.allowed_endpoints', + os.getenv('API_KEYS_ALLOWED_ENDPOINTS', os.getenv('API_KEY_ALLOWED_ENDPOINTS', '')), +) + +JWT_EXPIRES_IN = ConfigVar('JWT_EXPIRES_IN', 'auth.jwt_expiry', os.getenv('JWT_EXPIRES_IN', '4w')) + +if JWT_EXPIRES_IN.value == '-1': + log.warning( + "⚠️ SECURITY WARNING: JWT_EXPIRES_IN is set to '-1'\n" + ' See: https://docs.openwebui.com/reference/env-configuration\n' + ) + +#################################### +# OAuth config +#################################### + +ENABLE_OAUTH_SIGNUP = ConfigVar( + 'ENABLE_OAUTH_SIGNUP', + 'oauth.enable_signup', + os.getenv('ENABLE_OAUTH_SIGNUP', 'False').lower() == 'true', +) + +OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = ConfigVar( + 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', + 'oauth.refresh_token_include_scope', + os.getenv('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'False').lower() == 'true', +) + + +OAUTH_MERGE_ACCOUNTS_BY_EMAIL = ConfigVar( + 'OAUTH_MERGE_ACCOUNTS_BY_EMAIL', + 'oauth.merge_accounts_by_email', + os.getenv('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true', +) + +OAUTH_PROVIDERS = {} + +GOOGLE_CLIENT_ID = ConfigVar( + 'GOOGLE_CLIENT_ID', + 'oauth.google.client_id', + os.getenv('GOOGLE_CLIENT_ID', ''), +) + +GOOGLE_CLIENT_SECRET = ConfigVar( + 'GOOGLE_CLIENT_SECRET', + 'oauth.google.client_secret', + os.getenv('GOOGLE_CLIENT_SECRET', ''), +) + + +GOOGLE_OAUTH_SCOPE = ConfigVar( + 'GOOGLE_OAUTH_SCOPE', + 'oauth.google.scope', + os.getenv('GOOGLE_OAUTH_SCOPE', 'openid email profile'), +) + +GOOGLE_REDIRECT_URI = ConfigVar( + 'GOOGLE_REDIRECT_URI', + 'oauth.google.redirect_uri', + os.getenv('GOOGLE_REDIRECT_URI', ''), +) + +GOOGLE_OAUTH_AUTHORIZE_PARAMS = {} +_google_oauth_authorize_params = os.getenv('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '') +if _google_oauth_authorize_params: + try: + _parsed = json.loads(_google_oauth_authorize_params) + if isinstance(_parsed, dict): + GOOGLE_OAUTH_AUTHORIZE_PARAMS = _parsed + else: + log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') + +MICROSOFT_CLIENT_ID = ConfigVar( + 'MICROSOFT_CLIENT_ID', + 'oauth.microsoft.client_id', + os.getenv('MICROSOFT_CLIENT_ID', ''), +) + +MICROSOFT_CLIENT_SECRET = ConfigVar( + 'MICROSOFT_CLIENT_SECRET', + 'oauth.microsoft.client_secret', + os.getenv('MICROSOFT_CLIENT_SECRET', ''), +) + +MICROSOFT_CLIENT_TENANT_ID = ConfigVar( + 'MICROSOFT_CLIENT_TENANT_ID', + 'oauth.microsoft.tenant_id', + os.getenv('MICROSOFT_CLIENT_TENANT_ID', ''), +) + +MICROSOFT_CLIENT_LOGIN_BASE_URL = ConfigVar( + 'MICROSOFT_CLIENT_LOGIN_BASE_URL', + 'oauth.microsoft.login_base_url', + os.getenv('MICROSOFT_CLIENT_LOGIN_BASE_URL', 'https://login.microsoftonline.com'), +) + +MICROSOFT_CLIENT_PICTURE_URL = ConfigVar( + 'MICROSOFT_CLIENT_PICTURE_URL', + 'oauth.microsoft.picture_url', + os.getenv( + 'MICROSOFT_CLIENT_PICTURE_URL', + 'https://graph.microsoft.com/v1.0/me/photo/$value', ), ) +MICROSOFT_OAUTH_SCOPE = ConfigVar( + 'MICROSOFT_OAUTH_SCOPE', + 'oauth.microsoft.scope', + os.getenv('MICROSOFT_OAUTH_SCOPE', 'openid email profile'), +) + +MICROSOFT_REDIRECT_URI = ConfigVar( + 'MICROSOFT_REDIRECT_URI', + 'oauth.microsoft.redirect_uri', + os.getenv('MICROSOFT_REDIRECT_URI', ''), +) + +GITHUB_CLIENT_ID = ConfigVar( + 'GITHUB_CLIENT_ID', + 'oauth.github.client_id', + os.getenv('GITHUB_CLIENT_ID', ''), +) + +GITHUB_CLIENT_SECRET = ConfigVar( + 'GITHUB_CLIENT_SECRET', + 'oauth.github.client_secret', + os.getenv('GITHUB_CLIENT_SECRET', ''), +) + +GITHUB_CLIENT_SCOPE = ConfigVar( + 'GITHUB_CLIENT_SCOPE', + 'oauth.github.scope', + os.getenv('GITHUB_CLIENT_SCOPE', 'user:email'), +) + +GITHUB_CLIENT_REDIRECT_URI = ConfigVar( + 'GITHUB_CLIENT_REDIRECT_URI', + 'oauth.github.redirect_uri', + os.getenv('GITHUB_CLIENT_REDIRECT_URI', ''), +) + +OAUTH_CLIENT_ID = ConfigVar( + 'OAUTH_CLIENT_ID', + 'oauth.oidc.client_id', + os.getenv('OAUTH_CLIENT_ID', ''), +) + +OAUTH_CLIENT_SECRET = ConfigVar( + 'OAUTH_CLIENT_SECRET', + 'oauth.oidc.client_secret', + os.getenv('OAUTH_CLIENT_SECRET', ''), +) + +OPENID_PROVIDER_URL = ConfigVar( + 'OPENID_PROVIDER_URL', + 'oauth.oidc.provider_url', + os.getenv('OPENID_PROVIDER_URL', ''), +) + +OPENID_END_SESSION_ENDPOINT = ConfigVar( + 'OPENID_END_SESSION_ENDPOINT', + 'oauth.oidc.end_session_endpoint', + os.getenv('OPENID_END_SESSION_ENDPOINT', ''), +) + +OPENID_REDIRECT_URI = ConfigVar( + 'OPENID_REDIRECT_URI', + 'oauth.oidc.redirect_uri', + os.getenv('OPENID_REDIRECT_URI', ''), +) + +OAUTH_SCOPES = ConfigVar( + 'OAUTH_SCOPES', + 'oauth.oidc.scopes', + os.getenv('OAUTH_SCOPES', 'openid email profile'), +) + +OAUTH_TIMEOUT = ConfigVar( + 'OAUTH_TIMEOUT', + 'oauth.oidc.oauth_timeout', + os.getenv('OAUTH_TIMEOUT', ''), +) + +OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = ConfigVar( + 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', + 'oauth.oidc.token_endpoint_auth_method', + os.getenv('OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', None), +) + +OAUTH_CODE_CHALLENGE_METHOD = ConfigVar( + 'OAUTH_CODE_CHALLENGE_METHOD', + 'oauth.oidc.code_challenge_method', + os.getenv('OAUTH_CODE_CHALLENGE_METHOD', None), +) + +OAUTH_PROVIDER_NAME = ConfigVar( + 'OAUTH_PROVIDER_NAME', + 'oauth.oidc.provider_name', + os.getenv('OAUTH_PROVIDER_NAME', 'SSO'), +) + +OAUTH_SUB_CLAIM = ConfigVar( + 'OAUTH_SUB_CLAIM', + 'oauth.oidc.sub_claim', + os.getenv('OAUTH_SUB_CLAIM', None), +) + +OAUTH_USERNAME_CLAIM = ConfigVar( + 'OAUTH_USERNAME_CLAIM', + 'oauth.oidc.username_claim', + os.getenv('OAUTH_USERNAME_CLAIM', 'name'), +) + + +OAUTH_PICTURE_CLAIM = ConfigVar( + 'OAUTH_PICTURE_CLAIM', + 'oauth.oidc.avatar_claim', + os.getenv('OAUTH_PICTURE_CLAIM', 'picture'), +) + +OAUTH_EMAIL_CLAIM = ConfigVar( + 'OAUTH_EMAIL_CLAIM', + 'oauth.oidc.email_claim', + os.getenv('OAUTH_EMAIL_CLAIM', 'email'), +) + +OAUTH_GROUPS_CLAIM = ConfigVar( + 'OAUTH_GROUPS_CLAIM', + 'oauth.oidc.group_claim', + os.getenv('OAUTH_GROUPS_CLAIM', os.getenv('OAUTH_GROUP_CLAIM', 'groups')), +) + +FEISHU_CLIENT_ID = ConfigVar( + 'FEISHU_CLIENT_ID', + 'oauth.feishu.client_id', + os.getenv('FEISHU_CLIENT_ID', ''), +) + +FEISHU_CLIENT_SECRET = ConfigVar( + 'FEISHU_CLIENT_SECRET', + 'oauth.feishu.client_secret', + os.getenv('FEISHU_CLIENT_SECRET', ''), +) + +FEISHU_OAUTH_SCOPE = ConfigVar( + 'FEISHU_OAUTH_SCOPE', + 'oauth.feishu.scope', + os.getenv('FEISHU_OAUTH_SCOPE', 'contact:user.base:readonly'), +) + +FEISHU_REDIRECT_URI = ConfigVar( + 'FEISHU_REDIRECT_URI', + 'oauth.feishu.redirect_uri', + os.getenv('FEISHU_REDIRECT_URI', ''), +) + +ENABLE_OAUTH_ROLE_MANAGEMENT = ConfigVar( + 'ENABLE_OAUTH_ROLE_MANAGEMENT', + 'oauth.enable_role_mapping', + os.getenv('ENABLE_OAUTH_ROLE_MANAGEMENT', 'False').lower() == 'true', +) + +ENABLE_OAUTH_GROUP_MANAGEMENT = ConfigVar( + 'ENABLE_OAUTH_GROUP_MANAGEMENT', + 'oauth.enable_group_mapping', + os.getenv('ENABLE_OAUTH_GROUP_MANAGEMENT', 'False').lower() == 'true', +) + +ENABLE_OAUTH_GROUP_CREATION = ConfigVar( + 'ENABLE_OAUTH_GROUP_CREATION', + 'oauth.enable_group_creation', + os.getenv('ENABLE_OAUTH_GROUP_CREATION', 'False').lower() == 'true', +) + + +oauth_group_default_share = os.getenv('OAUTH_GROUP_DEFAULT_SHARE', 'true').strip().lower() +OAUTH_GROUP_DEFAULT_SHARE = ConfigVar( + 'OAUTH_GROUP_DEFAULT_SHARE', + 'oauth.group_default_share', + ('members' if oauth_group_default_share == 'members' else oauth_group_default_share == 'true'), +) + + +OAUTH_BLOCKED_GROUPS = ConfigVar( + 'OAUTH_BLOCKED_GROUPS', + 'oauth.blocked_groups', + os.getenv('OAUTH_BLOCKED_GROUPS', '[]'), +) + +OAUTH_GROUPS_SEPARATOR = os.getenv('OAUTH_GROUPS_SEPARATOR', ';') + +OAUTH_ROLES_CLAIM = ConfigVar( + 'OAUTH_ROLES_CLAIM', + 'oauth.roles_claim', + os.getenv('OAUTH_ROLES_CLAIM', 'roles'), +) + +OAUTH_ROLES_SEPARATOR = os.getenv('OAUTH_ROLES_SEPARATOR', ',') + +OAUTH_ALLOWED_ROLES = ConfigVar( + 'OAUTH_ALLOWED_ROLES', + 'oauth.allowed_roles', + [ + role.strip() + for role in os.getenv('OAUTH_ALLOWED_ROLES', f'user{OAUTH_ROLES_SEPARATOR}admin').split(OAUTH_ROLES_SEPARATOR) + if role + ], +) + +OAUTH_ADMIN_ROLES = ConfigVar( + 'OAUTH_ADMIN_ROLES', + 'oauth.admin_roles', + [role.strip() for role in os.getenv('OAUTH_ADMIN_ROLES', 'admin').split(OAUTH_ROLES_SEPARATOR) if role], +) + +OAUTH_ALLOWED_DOMAINS = ConfigVar( + 'OAUTH_ALLOWED_DOMAINS', + 'oauth.allowed_domains', + [domain.strip() for domain in os.getenv('OAUTH_ALLOWED_DOMAINS', '*').split(',')], +) + +OAUTH_UPDATE_PICTURE_ON_LOGIN = ConfigVar( + 'OAUTH_UPDATE_PICTURE_ON_LOGIN', + 'oauth.update_picture_on_login', + os.getenv('OAUTH_UPDATE_PICTURE_ON_LOGIN', 'False').lower() == 'true', +) + +OAUTH_UPDATE_NAME_ON_LOGIN = ConfigVar( + 'OAUTH_UPDATE_NAME_ON_LOGIN', + 'oauth.update_name_on_login', + os.getenv('OAUTH_UPDATE_NAME_ON_LOGIN', 'False').lower() == 'true', +) + +OAUTH_UPDATE_EMAIL_ON_LOGIN = ConfigVar( + 'OAUTH_UPDATE_EMAIL_ON_LOGIN', + 'oauth.update_email_on_login', + os.getenv('OAUTH_UPDATE_EMAIL_ON_LOGIN', 'False').lower() == 'true', +) + +OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID = ( + os.getenv('OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID', 'False').lower() == 'true' +) + +OAUTH_AUDIENCE = ConfigVar( + 'OAUTH_AUDIENCE', + 'oauth.audience', + os.getenv('OAUTH_AUDIENCE', ''), +) + +OAUTH_AUTHORIZE_PARAMS = {} +_oauth_authorize_params = os.getenv('OAUTH_AUTHORIZE_PARAMS', '') +if _oauth_authorize_params: + try: + _parsed = json.loads(_oauth_authorize_params) + if isinstance(_parsed, dict): + OAUTH_AUTHORIZE_PARAMS = _parsed + else: + log.warning('OAUTH_AUTHORIZE_PARAMS must be a JSON object, ignoring') + except (json.JSONDecodeError, TypeError): + log.warning('OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') + + +def load_oauth_providers(): + OAUTH_PROVIDERS.clear() + if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value: + + def google_oauth_register(oauth: OAuth): + client = oauth.register( + name='google', + client_id=GOOGLE_CLIENT_ID.value, + client_secret=GOOGLE_CLIENT_SECRET.value, + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={ + 'scope': GOOGLE_OAUTH_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=GOOGLE_REDIRECT_URI.value, + **({'authorize_params': GOOGLE_OAUTH_AUTHORIZE_PARAMS} if GOOGLE_OAUTH_AUTHORIZE_PARAMS else {}), + ) + return client + + OAUTH_PROVIDERS['google'] = { + 'register': google_oauth_register, + } + + if MICROSOFT_CLIENT_ID.value and MICROSOFT_CLIENT_SECRET.value and MICROSOFT_CLIENT_TENANT_ID.value: + + def microsoft_oauth_register(oauth: OAuth): + client = oauth.register( + name='microsoft', + client_id=MICROSOFT_CLIENT_ID.value, + client_secret=MICROSOFT_CLIENT_SECRET.value, + server_metadata_url=f'{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}', + client_kwargs={ + 'scope': MICROSOFT_OAUTH_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=MICROSOFT_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['microsoft'] = { + 'picture_url': MICROSOFT_CLIENT_PICTURE_URL.value, + 'register': microsoft_oauth_register, + } + + if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value: + + def github_oauth_register(oauth: OAuth): + client = oauth.register( + name='github', + client_id=GITHUB_CLIENT_ID.value, + client_secret=GITHUB_CLIENT_SECRET.value, + access_token_url='https://github.com/login/oauth/access_token', + authorize_url='https://github.com/login/oauth/authorize', + api_base_url='https://api.github.com', + userinfo_endpoint='https://api.github.com/user', + client_kwargs={ + 'scope': GITHUB_CLIENT_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['github'] = { + 'register': github_oauth_register, + 'sub_claim': 'id', + } + + if ( + OAUTH_CLIENT_ID.value + and (OAUTH_CLIENT_SECRET.value or OAUTH_CODE_CHALLENGE_METHOD.value) + and OPENID_PROVIDER_URL.value + ): + + def oidc_oauth_register(oauth: OAuth): + client_kwargs = { + 'scope': OAUTH_SCOPES.value, + **( + {'token_endpoint_auth_method': OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value} + if OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value + else {} + ), + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + } + + if OAUTH_CODE_CHALLENGE_METHOD.value and OAUTH_CODE_CHALLENGE_METHOD.value == 'S256': + client_kwargs['code_challenge_method'] = 'S256' + elif OAUTH_CODE_CHALLENGE_METHOD.value: + raise Exception( + 'Code challenge methods other than "%s" not supported. Given: "%s"' + % ('S256', OAUTH_CODE_CHALLENGE_METHOD.value) + ) + + client = oauth.register( + name='oidc', + client_id=OAUTH_CLIENT_ID.value, + client_secret=OAUTH_CLIENT_SECRET.value, + server_metadata_url=OPENID_PROVIDER_URL.value, + client_kwargs=client_kwargs, + redirect_uri=OPENID_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['oidc'] = { + 'name': OAUTH_PROVIDER_NAME.value, + 'register': oidc_oauth_register, + } + + if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value: + + def feishu_oauth_register(oauth: OAuth): + client = oauth.register( + name='feishu', + client_id=FEISHU_CLIENT_ID.value, + client_secret=FEISHU_CLIENT_SECRET.value, + access_token_url='https://open.feishu.cn/open-apis/authen/v2/oauth/token', + authorize_url='https://accounts.feishu.cn/open-apis/authen/v1/authorize', + api_base_url='https://open.feishu.cn/open-apis', + userinfo_endpoint='https://open.feishu.cn/open-apis/authen/v1/user_info', + client_kwargs={ + 'scope': FEISHU_OAUTH_SCOPE.value, + **({'timeout': int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}), + }, + redirect_uri=FEISHU_REDIRECT_URI.value, + ) + return client + + OAUTH_PROVIDERS['feishu'] = { + 'register': feishu_oauth_register, + 'sub_claim': 'user_id', + } + + configured_providers = [] + if GOOGLE_CLIENT_ID.value: + configured_providers.append('Google') + if MICROSOFT_CLIENT_ID.value: + configured_providers.append('Microsoft') + if GITHUB_CLIENT_ID.value: + configured_providers.append('GitHub') + if FEISHU_CLIENT_ID.value: + configured_providers.append('Feishu') + + if configured_providers and not OPENID_PROVIDER_URL.value and not OPENID_END_SESSION_ENDPOINT.value: + provider_list = ', '.join(configured_providers) + log.warning( + f'⚠️ OAuth providers configured ({provider_list}) but OPENID_PROVIDER_URL not set - logout will not work!' + ) + log.warning( + f"Set OPENID_PROVIDER_URL to your OAuth provider's OpenID Connect discovery endpoint," + f' or set OPENID_END_SESSION_ENDPOINT to a custom logout URL to fix logout functionality.' + ) + + +load_oauth_providers() + #################################### # LDAP #################################### -ENABLE_LDAP = PersistentConfig( - "ENABLE_LDAP", - "ldap.enable", - os.environ.get("ENABLE_LDAP", "false").lower() == "true", +ENABLE_LDAP = ConfigVar( + 'ENABLE_LDAP', + 'ldap.enable', + os.getenv('ENABLE_LDAP', 'false').lower() == 'true', ) -LDAP_SERVER_LABEL = PersistentConfig( - "LDAP_SERVER_LABEL", - "ldap.server.label", - os.environ.get("LDAP_SERVER_LABEL", "LDAP Server"), +LDAP_SERVER_LABEL = ConfigVar( + 'LDAP_SERVER_LABEL', + 'ldap.server.label', + os.getenv('LDAP_SERVER_LABEL', 'LDAP Server'), ) -LDAP_SERVER_HOST = PersistentConfig( - "LDAP_SERVER_HOST", - "ldap.server.host", - os.environ.get("LDAP_SERVER_HOST", "localhost"), +LDAP_SERVER_HOST = ConfigVar( + 'LDAP_SERVER_HOST', + 'ldap.server.host', + os.getenv('LDAP_SERVER_HOST', 'localhost'), ) -LDAP_SERVER_PORT = PersistentConfig( - "LDAP_SERVER_PORT", - "ldap.server.port", - int(os.environ.get("LDAP_SERVER_PORT", "389")), +LDAP_SERVER_PORT = ConfigVar( + 'LDAP_SERVER_PORT', + 'ldap.server.port', + int(os.getenv('LDAP_SERVER_PORT', '389')), ) -LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig( - "LDAP_ATTRIBUTE_FOR_MAIL", - "ldap.server.attribute_for_mail", - os.environ.get("LDAP_ATTRIBUTE_FOR_MAIL", "mail"), +LDAP_ATTRIBUTE_FOR_MAIL = ConfigVar( + 'LDAP_ATTRIBUTE_FOR_MAIL', + 'ldap.server.attribute_for_mail', + os.getenv('LDAP_ATTRIBUTE_FOR_MAIL', 'mail'), ) -LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig( - "LDAP_ATTRIBUTE_FOR_USERNAME", - "ldap.server.attribute_for_username", - os.environ.get("LDAP_ATTRIBUTE_FOR_USERNAME", "uid"), +LDAP_ATTRIBUTE_FOR_USERNAME = ConfigVar( + 'LDAP_ATTRIBUTE_FOR_USERNAME', + 'ldap.server.attribute_for_username', + os.getenv('LDAP_ATTRIBUTE_FOR_USERNAME', 'uid'), ) -LDAP_APP_DN = PersistentConfig( - "LDAP_APP_DN", "ldap.server.app_dn", os.environ.get("LDAP_APP_DN", "") +LDAP_APP_DN = ConfigVar('LDAP_APP_DN', 'ldap.server.app_dn', os.getenv('LDAP_APP_DN', '')) + +LDAP_APP_PASSWORD = ConfigVar( + 'LDAP_APP_PASSWORD', + 'ldap.server.app_password', + os.getenv('LDAP_APP_PASSWORD', ''), ) -LDAP_APP_PASSWORD = PersistentConfig( - "LDAP_APP_PASSWORD", - "ldap.server.app_password", - os.environ.get("LDAP_APP_PASSWORD", ""), +LDAP_SEARCH_BASE = ConfigVar('LDAP_SEARCH_BASE', 'ldap.server.users_dn', os.getenv('LDAP_SEARCH_BASE', '')) + +LDAP_SEARCH_FILTERS = ConfigVar( + 'LDAP_SEARCH_FILTER', + 'ldap.server.search_filter', + os.getenv('LDAP_SEARCH_FILTER', os.getenv('LDAP_SEARCH_FILTERS', '')), ) -LDAP_SEARCH_BASE = PersistentConfig( - "LDAP_SEARCH_BASE", "ldap.server.users_dn", os.environ.get("LDAP_SEARCH_BASE", "") +LDAP_USE_TLS = ConfigVar( + 'LDAP_USE_TLS', + 'ldap.server.use_tls', + os.getenv('LDAP_USE_TLS', 'True').lower() == 'true', ) -LDAP_SEARCH_FILTERS = PersistentConfig( - "LDAP_SEARCH_FILTER", - "ldap.server.search_filter", - os.environ.get("LDAP_SEARCH_FILTER", os.environ.get("LDAP_SEARCH_FILTERS", "")), +LDAP_CA_CERT_FILE = ConfigVar( + 'LDAP_CA_CERT_FILE', + 'ldap.server.ca_cert_file', + os.getenv('LDAP_CA_CERT_FILE', ''), ) -LDAP_USE_TLS = PersistentConfig( - "LDAP_USE_TLS", - "ldap.server.use_tls", - os.environ.get("LDAP_USE_TLS", "True").lower() == "true", +LDAP_VALIDATE_CERT = ConfigVar( + 'LDAP_VALIDATE_CERT', + 'ldap.server.validate_cert', + os.getenv('LDAP_VALIDATE_CERT', 'True').lower() == 'true', ) -LDAP_CA_CERT_FILE = PersistentConfig( - "LDAP_CA_CERT_FILE", - "ldap.server.ca_cert_file", - os.environ.get("LDAP_CA_CERT_FILE", ""), +LDAP_CIPHERS = ConfigVar('LDAP_CIPHERS', 'ldap.server.ciphers', os.getenv('LDAP_CIPHERS', 'ALL')) + +ENABLE_LDAP_GROUP_MANAGEMENT = ConfigVar( + 'ENABLE_LDAP_GROUP_MANAGEMENT', + 'ldap.group.enable_management', + os.getenv('ENABLE_LDAP_GROUP_MANAGEMENT', 'False').lower() == 'true', ) -LDAP_VALIDATE_CERT = PersistentConfig( - "LDAP_VALIDATE_CERT", - "ldap.server.validate_cert", - os.environ.get("LDAP_VALIDATE_CERT", "True").lower() == "true", +ENABLE_LDAP_GROUP_CREATION = ConfigVar( + 'ENABLE_LDAP_GROUP_CREATION', + 'ldap.group.enable_creation', + os.getenv('ENABLE_LDAP_GROUP_CREATION', 'False').lower() == 'true', ) -LDAP_CIPHERS = PersistentConfig( - "LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL") -) - -# For LDAP Group Management -ENABLE_LDAP_GROUP_MANAGEMENT = PersistentConfig( - "ENABLE_LDAP_GROUP_MANAGEMENT", - "ldap.group.enable_management", - os.environ.get("ENABLE_LDAP_GROUP_MANAGEMENT", "False").lower() == "true", -) - -ENABLE_LDAP_GROUP_CREATION = PersistentConfig( - "ENABLE_LDAP_GROUP_CREATION", - "ldap.group.enable_creation", - os.environ.get("ENABLE_LDAP_GROUP_CREATION", "False").lower() == "true", -) - -LDAP_ATTRIBUTE_FOR_GROUPS = PersistentConfig( - "LDAP_ATTRIBUTE_FOR_GROUPS", - "ldap.server.attribute_for_groups", - os.environ.get("LDAP_ATTRIBUTE_FOR_GROUPS", "memberOf"), +LDAP_ATTRIBUTE_FOR_GROUPS = ConfigVar( + 'LDAP_ATTRIBUTE_FOR_GROUPS', + 'ldap.server.attribute_for_groups', + os.getenv('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf'), ) diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index 4d39d16cdb..132f3ac19a 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -1,126 +1,121 @@ +from __future__ import annotations + from enum import Enum class MESSAGES(str, Enum): - DEFAULT = lambda msg="": f"{msg if msg else ''}" - MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully." - MODEL_DELETED = ( - lambda model="": f"The model '{model}' has been deleted successfully." - ) + DEFAULT = lambda msg='': f'{msg if msg else ""}' + MODEL_ADDED = lambda model='': f"The model '{model}' has been added successfully." + MODEL_DELETED = lambda model='': f"The model '{model}' has been deleted successfully." class WEBHOOK_MESSAGES(str, Enum): - DEFAULT = lambda msg="": f"{msg if msg else ''}" - USER_SIGNUP = lambda username="": ( - f"New user signed up: {username}" if username else "New user signed up" - ) + DEFAULT = lambda msg='': f'{msg if msg else ""}' + USER_SIGNUP = lambda username='': f'New user signed up: {username}' if username else 'New user signed up' class ERROR_MESSAGES(str, Enum): def __str__(self) -> str: return super().__str__() - DEFAULT = ( - lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}' + DEFAULT = lambda err='': f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}' + ENV_VAR_NOT_FOUND = 'Required environment variable not found. Terminating now.' + CREATE_USER_ERROR = 'Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance.' + DELETE_USER_ERROR = 'Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot.' + EMAIL_MISMATCH = 'Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again.' + EMAIL_TAKEN = 'Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew.' + USERNAME_TAKEN = 'Uh-oh! This username is already registered. Please choose another username.' + PASSWORD_TOO_LONG = ( + 'Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long.' ) - ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." - CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." - DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot." - EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again." - EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." - USERNAME_TAKEN = ( - "Uh-oh! This username is already registered. Please choose another username." - ) - PASSWORD_TOO_LONG = "Uh-oh! The password you entered is too long. Please make sure your password is less than 72 bytes long." - COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." - FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file." + COMMAND_TAKEN = 'Uh-oh! This command is already registered. Please choose another command string.' + FILE_EXISTS = 'Uh-oh! This file is already registered. Please choose another file.' - ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string." - MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string." - NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string." - MODEL_ID_TOO_LONG = "The model id is too long. Please make sure your model id is less than 256 characters long." + ID_TAKEN = 'Uh-oh! This id is already registered. Please choose another id string.' + MODEL_ID_TAKEN = 'Uh-oh! This model id is already registered. Please choose another model id string.' + NAME_TAG_TAKEN = 'Uh-oh! This name tag is already registered. Please choose another name tag string.' + MODEL_ID_TOO_LONG = 'The model id is too long. Please make sure your model id is less than 256 characters long.' - INVALID_TOKEN = ( - "Your session has expired or the token is invalid. Please sign in again." - ) - INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again." + INVALID_TOKEN = 'Your session has expired or the token is invalid. Please sign in again.' + INVALID_CRED = 'The email or password provided is incorrect. Please check for typos and try logging in again.' INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)." - INCORRECT_PASSWORD = ( - "The password provided is incorrect. Please check for typos and try again." + INCORRECT_PASSWORD = 'The password provided is incorrect. Please check for typos and try again.' + INVALID_TRUSTED_HEADER = ( + 'Your provider has not provided a trusted header. Please contact your administrator for assistance.' ) - INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance." EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation." - UNAUTHORIZED = "401 Unauthorized" - ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." - ACTION_PROHIBITED = ( - "The requested action has been restricted as a security measure." + UNAUTHORIZED = '401 Unauthorized' + ACCESS_PROHIBITED = ( + 'You do not have permission to access this resource. Please contact your administrator for assistance.' ) + ACTION_PROHIBITED = 'The requested action has been restricted as a security measure.' - FILE_NOT_SENT = "FILE_NOT_SENT" + FILE_NOT_SENT = 'FILE_NOT_SENT' FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format and try again." NOT_FOUND = "We could not find what you're looking for :/" USER_NOT_FOUND = "We could not find what you're looking for :/" API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." - API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment." + API_KEY_NOT_ALLOWED = 'Use of API key is not enabled in the environment.' - MALICIOUS = "Unusual activities detected, please try again in a few minutes." + MALICIOUS = 'Unusual activities detected, please try again in a few minutes.' - PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." - INCORRECT_FORMAT = ( - lambda err="": f"Invalid format. Please use the correct format{err}" - ) - RATE_LIMIT_EXCEEDED = "API rate limit exceeded" + PANDOC_NOT_INSTALLED = 'Pandoc is not installed on the server. Please contact your administrator for assistance.' + INCORRECT_FORMAT = lambda err='': f'Invalid format. Please use the correct format{err}' + RATE_LIMIT_EXCEEDED = 'API rate limit exceeded' - MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" - OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found" - OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" - CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance." - API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment." + MODEL_NOT_FOUND = lambda name='': f"Model '{name}' was not found" + OPENAI_NOT_FOUND = lambda name='': 'OpenAI API was not found' + OLLAMA_NOT_FOUND = 'WebUI could not connect to Ollama' + CREATE_API_KEY_ERROR = 'Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance.' + API_KEY_CREATION_NOT_ALLOWED = 'API key creation is not allowed in the environment.' - EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." + EMPTY_CONTENT = 'The content provided is empty. Please ensure that there is text or data present before proceeding.' - DB_NOT_SQLITE = "This feature is only available when running with SQLite databases." + DB_NOT_SQLITE = 'This feature is only available with SQLite databases.' - INVALID_URL = ( - "Oops! The URL you provided is invalid. Please double-check and try again." + INVALID_URL = 'The URL you provided is invalid. Please double-check and try again.' + + WEB_SEARCH_ERROR = lambda err='': err if err else 'Something went wrong while searching the web.' + + OLLAMA_API_DISABLED = 'The Ollama API is disabled. Please enable it to use this feature.' + + FILE_TOO_LARGE = lambda size='': ( + f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}." ) - WEB_SEARCH_ERROR = ( - lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}" + DUPLICATE_CONTENT = 'Duplicate content detected. Please provide unique content to proceed.' + FILE_NOT_PROCESSED = ( + 'Extracted content is not available for this file. Please ensure that the file is processed before proceeding.' ) - OLLAMA_API_DISABLED = ( - "The Ollama API is disabled. Please enable it to use this feature." - ) + INVALID_PASSWORD = lambda err='': err if err else 'The password does not meet the required validation criteria.' - FILE_TOO_LARGE = ( - lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}." - ) + AUTOMATION_LIMIT_EXCEEDED = lambda size='': f'Automation limit reached ({size})' + AUTOMATION_TOO_FREQUENT = lambda interval='': f'Schedule too frequent. Minimum interval is {interval} seconds.' + AUTOMATION_INVALID_RRULE = lambda err='': f'Invalid RRULE: {err}' + AUTOMATION_NO_FUTURE_RUNS = 'RRULE has no future occurrences' - DUPLICATE_CONTENT = ( - "Duplicate content detected. Please provide unique content to proceed." - ) - FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding." - - INVALID_PASSWORD = lambda err="": ( - err if err else "The password does not meet the required validation criteria." - ) + FEATURE_DISABLED = lambda name='': f'{name} is disabled' + INPUT_TOO_LONG = lambda size='': f'Input prompt exceeds maximum length of {size}' + SERVER_CONNECTION_ERROR = 'Open WebUI: Server Connection Error' + REQUIRED_FIELD_EMPTY = lambda name='': f'Required field {name} is empty' + OAUTH_NOT_CONFIGURED = lambda name='': f"Provider '{name}' is not configured" class TASKS(str, Enum): def __str__(self) -> str: return super().__str__() - DEFAULT = lambda task="": f"{task if task else 'generation'}" - TITLE_GENERATION = "title_generation" - FOLLOW_UP_GENERATION = "follow_up_generation" - TAGS_GENERATION = "tags_generation" - EMOJI_GENERATION = "emoji_generation" - QUERY_GENERATION = "query_generation" - IMAGE_PROMPT_GENERATION = "image_prompt_generation" - AUTOCOMPLETE_GENERATION = "autocomplete_generation" - FUNCTION_CALLING = "function_calling" - MOA_RESPONSE_GENERATION = "moa_response_generation" + DEFAULT = lambda task='': f'{task if task else "generation"}' + TITLE_GENERATION = 'title_generation' + FOLLOW_UP_GENERATION = 'follow_up_generation' + TAGS_GENERATION = 'tags_generation' + EMOJI_GENERATION = 'emoji_generation' + QUERY_GENERATION = 'query_generation' + IMAGE_PROMPT_GENERATION = 'image_prompt_generation' + AUTOCOMPLETE_GENERATION = 'autocomplete_generation' + FUNCTION_CALLING = 'function_calling' + MOA_RESPONSE_GENERATION = 'moa_response_generation' diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index ff48a3abfe..01b1e9dde3 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -1,19 +1,20 @@ +import datetime as dt import importlib.metadata import json import logging import os import pkgutil -import sys -import shutil -from uuid import uuid4 -from pathlib import Path -from cryptography.hazmat.primitives import serialization import re - +import shutil +import sys +import traceback +from pathlib import Path +from typing import Any, Optional +from uuid import uuid4 import markdown from bs4 import BeautifulSoup -from open_webui.constants import ERROR_MESSAGES +from cryptography.hazmat.primitives import serialization #################################### # Load .env file @@ -34,142 +35,171 @@ BASE_DIR = BACKEND_DIR.parent try: from dotenv import find_dotenv, load_dotenv - load_dotenv(find_dotenv(str(BASE_DIR / ".env"))) + load_dotenv(find_dotenv(str(BASE_DIR / '.env'))) except ImportError: - print("dotenv not installed, skipping...") + print('dotenv not installed, skipping...') -DOCKER = os.environ.get("DOCKER", "False").lower() == "true" +DOCKER = os.getenv('DOCKER', 'False').lower() == 'true' -# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance -USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false") +USE_CUDA = os.getenv('USE_CUDA_DOCKER', 'false') +DEVICE_TYPE = 'cpu' +_cuda_error: Optional[str] = None -if USE_CUDA.lower() == "true": +if USE_CUDA.lower() == 'true': try: - import torch + import torch # noqa: E402 - assert torch.cuda.is_available(), "CUDA not available" - DEVICE_TYPE = "cuda" - except Exception as e: - cuda_error = ( - "Error when testing CUDA but USE_CUDA_DOCKER is true. " - f"Resetting USE_CUDA_DOCKER to false: {e}" - ) - os.environ["USE_CUDA_DOCKER"] = "false" - USE_CUDA = "false" - DEVICE_TYPE = "cpu" -else: - DEVICE_TYPE = "cpu" + if not torch.cuda.is_available(): + raise RuntimeError('CUDA not available') + DEVICE_TYPE = 'cuda' + except Exception as exc: + _cuda_error = f'CUDA unavailable (USE_CUDA_DOCKER=true), falling back to CPU: {exc}' + os.environ['USE_CUDA_DOCKER'] = 'false' + USE_CUDA = 'false' -try: - import torch +if sys.platform == 'darwin' and DEVICE_TYPE == 'cpu': + try: + import torch # noqa: E402 - if torch.backends.mps.is_available() and torch.backends.mps.is_built(): - DEVICE_TYPE = "mps" -except Exception: - pass + if torch.backends.mps.is_available() and torch.backends.mps.is_built(): + DEVICE_TYPE = 'mps' + except Exception: + pass #################################### # LOGGING #################################### -GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper() +_LEVEL_MAP = { + 'DEBUG': 'debug', + 'INFO': 'info', + 'WARNING': 'warn', + 'ERROR': 'error', + 'CRITICAL': 'fatal', +} + + +class JSONFormatter(logging.Formatter): + """Format log records as single-line JSON objects for structured logging.""" + + def format(self, record: logging.LogRecord) -> str: + log_entry: dict[str, Any] = { + 'ts': dt.datetime.fromtimestamp(record.created, tz=dt.UTC).isoformat(timespec='milliseconds'), + 'level': _LEVEL_MAP.get(record.levelname, record.levelname.lower()), + 'msg': record.getMessage(), + 'caller': record.name, + } + + if record.exc_info and record.exc_info[0] is not None: + log_entry['error'] = ''.join(traceback.format_exception(*record.exc_info)).rstrip() + elif record.exc_text: + log_entry['error'] = record.exc_text + + if record.stack_info: + log_entry['stacktrace'] = record.stack_info + + return json.dumps(log_entry, ensure_ascii=False, default=str) + + +LOG_FORMAT = os.getenv('LOG_FORMAT', '').lower() + +GLOBAL_LOG_LEVEL = os.getenv('GLOBAL_LOG_LEVEL', '').upper() if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping(): - logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True) + _log_cfg: dict[str, Any] = {'level': GLOBAL_LOG_LEVEL, 'force': True} + if LOG_FORMAT == 'json': + _json_handler = logging.StreamHandler(sys.stdout) + _json_handler.setFormatter(JSONFormatter()) + _log_cfg['handlers'] = [_json_handler] + else: + _log_cfg['stream'] = sys.stdout + logging.basicConfig(**_log_cfg) else: - GLOBAL_LOG_LEVEL = "INFO" + GLOBAL_LOG_LEVEL = 'INFO' log = logging.getLogger(__name__) -log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") +log.info('GLOBAL_LOG_LEVEL: %s', GLOBAL_LOG_LEVEL) -if "cuda_error" in locals(): - log.exception(cuda_error) - del cuda_error +if _cuda_error: + log.error(_cuda_error) + _cuda_error = None SRC_LOG_LEVELS = {} # Legacy variable, do not remove -WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI") -if WEBUI_NAME != "Open WebUI": - WEBUI_NAME += " (Open WebUI)" - -WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" - -TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "") - #################################### # ENV (dev,test,prod) #################################### -ENV = os.environ.get("ENV", "dev") +ENV = os.getenv('ENV', 'dev') -FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true" +FROM_INIT_PY = os.getenv('FROM_INIT_PY', 'False').lower() == 'true' if FROM_INIT_PY: - PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")} + PACKAGE_DATA = {'version': importlib.metadata.version('open-webui')} else: try: - PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text()) + PACKAGE_DATA = json.loads((BASE_DIR / 'package.json').read_text()) except Exception: - PACKAGE_DATA = {"version": "0.0.0"} + PACKAGE_DATA = {'version': '0.0.0'} -VERSION = PACKAGE_DATA["version"] +VERSION = PACKAGE_DATA['version'] -DEPLOYMENT_ID = os.environ.get("DEPLOYMENT_ID", "") -INSTANCE_ID = os.environ.get("INSTANCE_ID", str(uuid4())) +DEPLOYMENT_ID = os.getenv('DEPLOYMENT_ID', '') +INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4())) -ENABLE_DB_MIGRATIONS = os.environ.get("ENABLE_DB_MIGRATIONS", "True").lower() == "true" +ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true' # Function to parse each section def parse_section(section): items = [] - for li in section.find_all("li"): + for li in section.find_all('li'): # Extract raw HTML string raw_html = str(li) # Extract text without HTML tags - text = li.get_text(separator=" ", strip=True) + text = li.get_text(separator=' ', strip=True) # Split into title and content - parts = text.split(": ", 1) - title = parts[0].strip() if len(parts) > 1 else "" + parts = text.split(': ', 1) + title = parts[0].strip() if len(parts) > 1 else '' content = parts[1].strip() if len(parts) > 1 else text - items.append({"title": title, "content": content, "raw": raw_html}) + items.append({'title': title, 'content': content, 'raw': raw_html}) return items try: - changelog_path = BASE_DIR / "CHANGELOG.md" - with open(str(changelog_path.absolute()), "r", encoding="utf8") as file: + changelog_path = BASE_DIR / 'CHANGELOG.md' + with open(str(changelog_path.absolute()), encoding='utf8') as file: changelog_content = file.read() except Exception: - changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode() + changelog_content = (pkgutil.get_data('open_webui', 'CHANGELOG.md') or b'').decode() # Convert markdown content to HTML html_content = markdown.markdown(changelog_content) # Parse the HTML content -soup = BeautifulSoup(html_content, "html.parser") +soup = BeautifulSoup(html_content, 'html.parser') # Initialize JSON structure changelog_json = {} # Iterate over each version -for version in soup.find_all("h2"): - version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets - date = version.get_text().strip().split(" - ")[1] +for version in soup.find_all('h2'): + version_number = version.get_text().strip().split(' - ')[0][1:-1] # Remove brackets + date = version.get_text().strip().split(' - ')[1] - version_data = {"date": date} + version_data = {'date': date} # Find the next sibling that is a h3 tag (section title) current = version.find_next_sibling() - while current and current.name != "h2": - if current.name == "h3": + while current and current.name != 'h2': + if current.name == 'h3': section_title = current.get_text().lower() # e.g., "added", "fixed" - section_items = parse_section(current.find_next_sibling("ul")) + section_items = parse_section(current.find_next_sibling('ul')) version_data[section_title] = section_items # Move to the next element @@ -179,45 +209,19 @@ for version in soup.find_all("h2"): CHANGELOG = changelog_json -#################################### -# SAFE_MODE -#################################### - -SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true" - - -#################################### -# ENABLE_FORWARD_USER_INFO_HEADERS -#################################### - -ENABLE_FORWARD_USER_INFO_HEADERS = ( - os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true" -) - -# Experimental feature, may be removed in future -ENABLE_STAR_SESSIONS_MIDDLEWARE = ( - os.environ.get("ENABLE_STAR_SESSIONS_MIDDLEWARE", "False").lower() == "true" -) - -#################################### -# WEBUI_BUILD_HASH -#################################### - -WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build") - #################################### # DATA/FRONTEND BUILD DIR #################################### -DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve() +DATA_DIR = Path(os.getenv('DATA_DIR', BACKEND_DIR / 'data')).resolve() if FROM_INIT_PY: - NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve() + NEW_DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')).resolve() NEW_DATA_DIR.mkdir(parents=True, exist_ok=True) # Check if the data directory exists in the package directory if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR: - log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}") + log.info(f'Moving {DATA_DIR} to {NEW_DATA_DIR}') for item in DATA_DIR.iterdir(): dest = NEW_DATA_DIR / item.name if item.is_dir(): @@ -226,157 +230,150 @@ if FROM_INIT_PY: shutil.copy2(item, dest) # Zip the data directory - shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR) + shutil.make_archive(DATA_DIR.parent / 'open_webui_data', 'zip', DATA_DIR) # Remove the old data directory shutil.rmtree(DATA_DIR) - DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")) + DATA_DIR = Path(os.getenv('DATA_DIR', OPEN_WEBUI_DIR / 'data')) -STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")) +STATIC_DIR = Path(os.getenv('STATIC_DIR', OPEN_WEBUI_DIR / 'static')) -FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts")) +FONTS_DIR = Path(os.getenv('FONTS_DIR', OPEN_WEBUI_DIR / 'static' / 'fonts')) -FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve() +FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', BASE_DIR / 'build')).resolve() if FROM_INIT_PY: - FRONTEND_BUILD_DIR = Path( - os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend") - ).resolve() + FRONTEND_BUILD_DIR = Path(os.getenv('FRONTEND_BUILD_DIR', OPEN_WEBUI_DIR / 'frontend')).resolve() #################################### # Database #################################### # Check if the file exists -if os.path.exists(f"{DATA_DIR}/ollama.db"): +if os.path.exists(f'{DATA_DIR}/ollama.db'): # Rename the file - os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db") - log.info("Database migrated from Ollama-WebUI successfully.") + os.rename(f'{DATA_DIR}/ollama.db', f'{DATA_DIR}/webui.db') + log.info('Database migrated from Ollama-WebUI successfully.') else: pass -DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db") +DATABASE_URL = os.getenv('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db') -DATABASE_TYPE = os.environ.get("DATABASE_TYPE") -DATABASE_USER = os.environ.get("DATABASE_USER") -DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD") +DATABASE_TYPE = os.getenv('DATABASE_TYPE') +DATABASE_USER = os.getenv('DATABASE_USER') +DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD') -DATABASE_CRED = "" +DATABASE_CRED = '' if DATABASE_USER: - DATABASE_CRED += f"{DATABASE_USER}" + DATABASE_CRED += f'{DATABASE_USER}' if DATABASE_PASSWORD: - DATABASE_CRED += f":{DATABASE_PASSWORD}" + DATABASE_CRED += f':{DATABASE_PASSWORD}' DB_VARS = { - "db_type": DATABASE_TYPE, - "db_cred": DATABASE_CRED, - "db_host": os.environ.get("DATABASE_HOST"), - "db_port": os.environ.get("DATABASE_PORT"), - "db_name": os.environ.get("DATABASE_NAME"), + 'db_type': DATABASE_TYPE, + 'db_cred': DATABASE_CRED, + 'db_host': os.getenv('DATABASE_HOST'), + 'db_port': os.getenv('DATABASE_PORT'), + 'db_name': os.getenv('DATABASE_NAME'), } if all(DB_VARS.values()): - DATABASE_URL = f"{DB_VARS['db_type']}://{DB_VARS['db_cred']}@{DB_VARS['db_host']}:{DB_VARS['db_port']}/{DB_VARS['db_name']}" -elif DATABASE_TYPE == "sqlite+sqlcipher" and not os.environ.get("DATABASE_URL"): + DATABASE_URL = ( + f'{DB_VARS["db_type"]}://{DB_VARS["db_cred"]}@{DB_VARS["db_host"]}:{DB_VARS["db_port"]}/{DB_VARS["db_name"]}' + ) +elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.getenv('DATABASE_URL'): # Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set - DATABASE_URL = f"sqlite+sqlcipher:///{DATA_DIR}/webui.db" + DATABASE_URL = f'sqlite+sqlcipher:///{DATA_DIR}/webui.db' # Replace the postgres:// with postgresql:// -if "postgres://" in DATABASE_URL: - DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://") +if 'postgres://' in DATABASE_URL: + DATABASE_URL = DATABASE_URL.replace('postgres://', 'postgresql://') -DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None) +DATABASE_SCHEMA = os.getenv('DATABASE_SCHEMA', None) -DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", None) +_pool_size_raw = os.getenv('DATABASE_POOL_SIZE') +try: + DATABASE_POOL_SIZE = int(_pool_size_raw) if _pool_size_raw else None +except (ValueError, TypeError): + DATABASE_POOL_SIZE = None -if DATABASE_POOL_SIZE != None: - try: - DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE) - except Exception: - DATABASE_POOL_SIZE = None - -DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0) - -if DATABASE_POOL_MAX_OVERFLOW == "": +_pool_overflow_raw = os.getenv('DATABASE_POOL_MAX_OVERFLOW', '0') +try: + DATABASE_POOL_MAX_OVERFLOW = int(_pool_overflow_raw) if _pool_overflow_raw else 0 +except (ValueError, TypeError): DATABASE_POOL_MAX_OVERFLOW = 0 -else: - try: - DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW) - except Exception: - DATABASE_POOL_MAX_OVERFLOW = 0 -DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30) - -if DATABASE_POOL_TIMEOUT == "": +_pool_timeout_raw = os.getenv('DATABASE_POOL_TIMEOUT', '30') +try: + DATABASE_POOL_TIMEOUT = int(_pool_timeout_raw) if _pool_timeout_raw else 30 +except (ValueError, TypeError): DATABASE_POOL_TIMEOUT = 30 -else: - try: - DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT) - except Exception: - DATABASE_POOL_TIMEOUT = 30 -DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600) - -if DATABASE_POOL_RECYCLE == "": +_pool_recycle_raw = os.getenv('DATABASE_POOL_RECYCLE', '3600') +try: + DATABASE_POOL_RECYCLE = int(_pool_recycle_raw) if _pool_recycle_raw else 3600 +except (ValueError, TypeError): DATABASE_POOL_RECYCLE = 3600 -else: - try: - DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE) - except Exception: - DATABASE_POOL_RECYCLE = 3600 -DATABASE_ENABLE_SQLITE_WAL = ( - os.environ.get("DATABASE_ENABLE_SQLITE_WAL", "False").lower() == "true" -) +DATABASE_ENABLE_SQLITE_WAL = os.getenv('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true' -DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get( - "DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL", None -) +# SQLite PRAGMA tuning — these defaults are optimised for WAL-mode web-server +# workloads. Each can be overridden via its environment variable. +# Set any value to an empty string to skip that PRAGMA entirely. + +# PRAGMA synchronous: NORMAL (1) is safe with WAL and avoids an fsync per +# transaction. Valid values: OFF (0), NORMAL (1), FULL (2), EXTRA (3). +DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.getenv('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL') + +# PRAGMA busy_timeout (ms): how long a connection waits for a write lock +# before raising SQLITE_BUSY. +DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.getenv('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000') + +# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache. +DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.getenv('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536') + +# PRAGMA temp_store: MEMORY (2) keeps temp tables and indices in RAM. +# Valid values: DEFAULT (0), FILE (1), MEMORY (2). +DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.getenv('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY') + +# PRAGMA mmap_size (bytes): memory-mapped I/O size. 268435456 ≈ 256 MB. +# Set to 0 to disable mmap. +DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.getenv('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456') + +# PRAGMA journal_size_limit (bytes): caps the WAL file size after checkpoint. +# Without this the WAL grows unbounded during write bursts and is never +# truncated. 67108864 ≈ 64 MB. Set to -1 for no limit (SQLite default). +DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.getenv('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864') + +DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.getenv('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None) if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None: try: - DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float( - DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL - ) + DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = float(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL) except Exception: DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = 0.0 -# When enabled, get_db_context reuses existing sessions; set to False to always create new sessions -DATABASE_ENABLE_SESSION_SHARING = ( - os.environ.get("DATABASE_ENABLE_SESSION_SHARING", "False").lower() == "true" -) - -# Enable public visibility of active user count (when disabled, only admins can see it) -ENABLE_PUBLIC_ACTIVE_USERS_COUNT = ( - os.environ.get("ENABLE_PUBLIC_ACTIVE_USERS_COUNT", "True").lower() == "true" -) - -RESET_CONFIG_ON_START = ( - os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true" -) - -ENABLE_REALTIME_CHAT_SAVE = ( - os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true" -) - -ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true" - -RAG_SYSTEM_CONTEXT = os.environ.get("RAG_SYSTEM_CONTEXT", "False").lower() == "true" +DATABASE_ENABLE_SESSION_SHARING = os.getenv('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true' +ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.getenv('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true' +RESET_CONFIG_ON_START = os.getenv('RESET_CONFIG_ON_START', 'False').lower() == 'true' +ENABLE_REALTIME_CHAT_SAVE = os.getenv('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true' +ENABLE_QUERIES_CACHE = os.getenv('ENABLE_QUERIES_CACHE', 'False').lower() == 'true' +RAG_SYSTEM_CONTEXT = os.getenv('RAG_SYSTEM_CONTEXT', 'False').lower() == 'true' #################################### # REDIS #################################### -REDIS_URL = os.environ.get("REDIS_URL", "") -REDIS_CLUSTER = os.environ.get("REDIS_CLUSTER", "False").lower() == "true" +REDIS_URL = os.getenv('REDIS_URL', '') +REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true' -REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui") +REDIS_KEY_PREFIX = os.getenv('REDIS_KEY_PREFIX', 'open-webui') -REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "") -REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379") +REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '') +REDIS_SENTINEL_PORT = os.getenv('REDIS_SENTINEL_PORT', '26379') # Maximum number of retries for Redis operations when using Sentinel fail-over -REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get("REDIS_SENTINEL_MAX_RETRY_COUNT", "2") +REDIS_SENTINEL_MAX_RETRY_COUNT = os.getenv('REDIS_SENTINEL_MAX_RETRY_COUNT', '2') try: REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT) if REDIS_SENTINEL_MAX_RETRY_COUNT < 1: @@ -385,38 +382,262 @@ except ValueError: REDIS_SENTINEL_MAX_RETRY_COUNT = 2 -REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get("REDIS_SOCKET_CONNECT_TIMEOUT", "") +REDIS_SOCKET_CONNECT_TIMEOUT = os.getenv('REDIS_SOCKET_CONNECT_TIMEOUT', '') try: REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT) except ValueError: REDIS_SOCKET_CONNECT_TIMEOUT = None -#################################### -# UVICORN WORKERS -#################################### +# Whether to enable TCP SO_KEEPALIVE on Redis client sockets. Opt-in: +# defaults to off so behavior is unchanged for existing deployments. When +# enabled, the kernel sends TCP keepalive probes on idle connections so +# half-closed sockets (e.g. after a silent firewall/LB reset or a NIC +# flap) are detected before the next command lands on them. +REDIS_SOCKET_KEEPALIVE = os.getenv('REDIS_SOCKET_KEEPALIVE', 'False').lower() == 'true' -# Number of uvicorn worker processes for handling requests -UVICORN_WORKERS = os.environ.get("UVICORN_WORKERS", "1") +# How often (in seconds) redis-py should PING an idle pooled connection +# before reusing it. Opt-in: defaults to unset (empty string) so behavior +# is unchanged for existing deployments. When set, should be shorter than +# the Redis server `timeout` setting and any firewall/LB idle timeout on +# the path to Redis, so stale connections are detected before a real +# command lands on them. Set to 0 or empty to disable. +REDIS_HEALTH_CHECK_INTERVAL = os.getenv('REDIS_HEALTH_CHECK_INTERVAL', '') try: - UVICORN_WORKERS = int(UVICORN_WORKERS) - if UVICORN_WORKERS < 1: - UVICORN_WORKERS = 1 + REDIS_HEALTH_CHECK_INTERVAL = int(REDIS_HEALTH_CHECK_INTERVAL) + if REDIS_HEALTH_CHECK_INTERVAL <= 0: + REDIS_HEALTH_CHECK_INTERVAL = None except ValueError: + REDIS_HEALTH_CHECK_INTERVAL = None + +REDIS_RECONNECT_DELAY = os.getenv('REDIS_RECONNECT_DELAY', '') + +if REDIS_RECONNECT_DELAY == '': + REDIS_RECONNECT_DELAY = None +else: + try: + REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY) + if REDIS_RECONNECT_DELAY < 0: + REDIS_RECONNECT_DELAY = None + except Exception: + REDIS_RECONNECT_DELAY = None + +#################################### +# Uvicorn +#################################### + +try: + UVICORN_WORKERS = max(int(os.getenv('UVICORN_WORKERS', '1')), 1) +except (ValueError, TypeError): UVICORN_WORKERS = 1 - log.info(f"Invalid UVICORN_WORKERS value, defaulting to {UVICORN_WORKERS}") #################################### -# WEBUI_AUTH (Required for security) +# WEBSOCKET SUPPORT #################################### -WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true" +ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true' -ENABLE_INITIAL_ADMIN_SIGNUP = ( - os.environ.get("ENABLE_INITIAL_ADMIN_SIGNUP", "False").lower() == "true" + +WEBSOCKET_MANAGER = os.getenv('WEBSOCKET_MANAGER', '') + +WEBSOCKET_REDIS_OPTIONS = os.getenv('WEBSOCKET_REDIS_OPTIONS', '') + + +if WEBSOCKET_REDIS_OPTIONS == '': + if REDIS_SOCKET_CONNECT_TIMEOUT: + WEBSOCKET_REDIS_OPTIONS = {'socket_connect_timeout': REDIS_SOCKET_CONNECT_TIMEOUT} + else: + log.debug('No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None') + WEBSOCKET_REDIS_OPTIONS = None +else: + try: + WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS) + except Exception: + log.warning('Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None') + WEBSOCKET_REDIS_OPTIONS = None + +WEBSOCKET_REDIS_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL) +WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true' + +websocket_redis_lock_timeout = os.getenv('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60') + +try: + WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout) +except ValueError: + WEBSOCKET_REDIS_LOCK_TIMEOUT = 60 + +WEBSOCKET_SENTINEL_HOSTS = os.getenv('WEBSOCKET_SENTINEL_HOSTS', '') +WEBSOCKET_SENTINEL_PORT = os.getenv('WEBSOCKET_SENTINEL_PORT', '26379') +WEBSOCKET_SERVER_LOGGING = os.getenv('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true' +WEBSOCKET_SERVER_ENGINEIO_LOGGING = ( + os.getenv( + 'WEBSOCKET_SERVER_ENGINEIO_LOGGING', + os.getenv('WEBSOCKET_SERVER_LOGGING', 'False'), + ).lower() + == 'true' ) -ENABLE_SIGNUP_PASSWORD_CONFIRMATION = ( - os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true" +WEBSOCKET_SERVER_PING_TIMEOUT = os.getenv('WEBSOCKET_SERVER_PING_TIMEOUT', '20') +try: + WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT) +except ValueError: + WEBSOCKET_SERVER_PING_TIMEOUT = 20 + +WEBSOCKET_SERVER_PING_INTERVAL = os.getenv('WEBSOCKET_SERVER_PING_INTERVAL', '25') +try: + WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL) +except ValueError: + WEBSOCKET_SERVER_PING_INTERVAL = 25 + +WEBSOCKET_EVENT_CALLER_TIMEOUT = os.getenv('WEBSOCKET_EVENT_CALLER_TIMEOUT', '') + +if WEBSOCKET_EVENT_CALLER_TIMEOUT == '': + WEBSOCKET_EVENT_CALLER_TIMEOUT = None +else: + try: + WEBSOCKET_EVENT_CALLER_TIMEOUT = int(WEBSOCKET_EVENT_CALLER_TIMEOUT) + except ValueError: + WEBSOCKET_EVENT_CALLER_TIMEOUT = 300 + + +REQUESTS_VERIFY = os.getenv('REQUESTS_VERIFY', 'True').lower() == 'true' + +_aiohttp_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT', '') +try: + AIOHTTP_CLIENT_TIMEOUT = int(_aiohttp_timeout_raw) if _aiohttp_timeout_raw else None +except (ValueError, TypeError): + AIOHTTP_CLIENT_TIMEOUT = 300 + + +AIOHTTP_CLIENT_SESSION_SSL = os.getenv('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true' + +# When False (default), outbound HTTP requests do not follow 3xx redirects. +AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.getenv('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true' + +# Optional User-Agent override for outbound web-loader fetches. When set, +# SafeWebBaseLoader sends this value instead of the default python-requests UA +# which is aggressively blocked by Cloudflare, Wikipedia, and similar services. +USER_AGENT = os.getenv('USER_AGENT', '') + +_model_list_timeout_raw = os.getenv( + 'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST', + os.getenv('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'), ) +try: + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(_model_list_timeout_raw) if _model_list_timeout_raw else None +except (ValueError, TypeError): + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10 + +_tool_data_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10') +try: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(_tool_data_timeout_raw) if _tool_data_timeout_raw else None +except (ValueError, TypeError): + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10 + + +AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = os.getenv('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True').lower() == 'true' + +AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '') + +if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '': + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT +else: + try: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = int(AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER) + except Exception: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT + +# Timeout (in seconds) for the MCP session.initialize() handshake. +# The handshake performs a list-tools round-trip and can take tens of +# seconds on cold-start servers or servers exposing many tools. +MCP_INITIALIZE_TIMEOUT = os.getenv('MCP_INITIALIZE_TIMEOUT', '10') +try: + MCP_INITIALIZE_TIMEOUT = int(MCP_INITIALIZE_TIMEOUT) +except (ValueError, TypeError): + MCP_INITIALIZE_TIMEOUT = 10 + + +#################################### +# AIOHTTP Connection Pool +#################################### + +AIOHTTP_POOL_CONNECTIONS = os.getenv('AIOHTTP_POOL_CONNECTIONS', '') +if AIOHTTP_POOL_CONNECTIONS == '': + AIOHTTP_POOL_CONNECTIONS = None +else: + try: + AIOHTTP_POOL_CONNECTIONS = int(AIOHTTP_POOL_CONNECTIONS) + except ValueError: + AIOHTTP_POOL_CONNECTIONS = None + +AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.getenv('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '') +if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '': + AIOHTTP_POOL_CONNECTIONS_PER_HOST = None +else: + try: + AIOHTTP_POOL_CONNECTIONS_PER_HOST = int(AIOHTTP_POOL_CONNECTIONS_PER_HOST) + except ValueError: + AIOHTTP_POOL_CONNECTIONS_PER_HOST = None + +AIOHTTP_POOL_DNS_TTL = os.getenv('AIOHTTP_POOL_DNS_TTL', '300') +try: + AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL) + if AIOHTTP_POOL_DNS_TTL < 0: + AIOHTTP_POOL_DNS_TTL = 300 +except ValueError: + AIOHTTP_POOL_DNS_TTL = 300 + +RAG_EMBEDDING_TIMEOUT = os.getenv('RAG_EMBEDDING_TIMEOUT', '') + +if RAG_EMBEDDING_TIMEOUT == '': + RAG_EMBEDDING_TIMEOUT = None +else: + try: + RAG_EMBEDDING_TIMEOUT = int(RAG_EMBEDDING_TIMEOUT) + except Exception: + RAG_EMBEDDING_TIMEOUT = None + + +#################################### +# Auth +#################################### + +WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true' + +ENABLE_INITIAL_ADMIN_SIGNUP = os.getenv('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true' +ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.getenv('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true' + +#################################### +# Secret key & cookies +#################################### + +# WEBUI_JWT_SECRET_KEY is deprecated; use WEBUI_SECRET_KEY instead. +# No hardcoded fallback by design: the supported start scripts set/auto-generate it; unset is rejected below. +WEBUI_SECRET_KEY = os.getenv( + 'WEBUI_SECRET_KEY', + os.getenv('WEBUI_JWT_SECRET_KEY', ''), +) + +WEBUI_SESSION_COOKIE_SAME_SITE = os.getenv('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax') +WEBUI_SESSION_COOKIE_SECURE = os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true' +WEBUI_AUTH_COOKIE_SAME_SITE = os.getenv('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE) +WEBUI_AUTH_COOKIE_SECURE = ( + os.getenv( + 'WEBUI_AUTH_COOKIE_SECURE', + os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false'), + ).lower() + == 'true' +) + +if WEBUI_AUTH and WEBUI_SECRET_KEY == '': + raise SystemExit( + 'WEBUI_SECRET_KEY is not set. It is a hard requirement when authentication is enabled.\n' + 'The supported start methods set or auto-generate it for you: use start.sh (Linux/macOS), ' + 'start_windows.bat (Windows), or `open-webui serve`.\n' + 'If you start the backend another way (e.g. invoking uvicorn directly, which is unsupported), ' + 'you must set WEBUI_SECRET_KEY yourself to a long random value.\n' + 'See https://docs.openwebui.com/reference/env-configuration#webui_secret_key' + ) + +ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true' #################################### # Admin Account Runtime Creation @@ -424,124 +645,115 @@ ENABLE_SIGNUP_PASSWORD_CONFIRMATION = ( # Optional env vars for creating an admin account on startup # Useful for headless/automated deployments -WEBUI_ADMIN_EMAIL = os.environ.get("WEBUI_ADMIN_EMAIL", "") -WEBUI_ADMIN_PASSWORD = os.environ.get("WEBUI_ADMIN_PASSWORD", "") -WEBUI_ADMIN_NAME = os.environ.get("WEBUI_ADMIN_NAME", "Admin") +WEBUI_ADMIN_EMAIL = os.getenv('WEBUI_ADMIN_EMAIL', '') +WEBUI_ADMIN_PASSWORD = os.getenv('WEBUI_ADMIN_PASSWORD', '') +WEBUI_ADMIN_NAME = os.getenv('WEBUI_ADMIN_NAME', 'Admin') -WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get( - "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None -) -WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None) -WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.environ.get( - "WEBUI_AUTH_TRUSTED_GROUPS_HEADER", None -) +WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None) +WEBUI_AUTH_TRUSTED_NAME_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_NAME_HEADER', None) +WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None) +WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_ROLE_HEADER', None) +# Custom header name for API key authentication. Defaults to 'x-api-key'. +# Useful when Open WebUI sits behind a reverse proxy / API gateway that +# already uses the Authorization header for its own authentication — set +# this to a unique header (e.g. 'X-OpenWebUI-Key') so the middleware +# checks the custom header instead and avoids the 401 short-circuit. +CUSTOM_API_KEY_HEADER = os.getenv('CUSTOM_API_KEY_HEADER', 'x-api-key') -ENABLE_PASSWORD_VALIDATION = ( - os.environ.get("ENABLE_PASSWORD_VALIDATION", "False").lower() == "true" -) -PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get( - "PASSWORD_VALIDATION_REGEX_PATTERN", - "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$", +ENABLE_PASSWORD_VALIDATION = os.getenv('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true' +PASSWORD_VALIDATION_REGEX_PATTERN = os.getenv( + 'PASSWORD_VALIDATION_REGEX_PATTERN', + r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$', ) try: - PASSWORD_VALIDATION_REGEX_PATTERN = rf"{PASSWORD_VALIDATION_REGEX_PATTERN}" + PASSWORD_VALIDATION_REGEX_PATTERN = rf'{PASSWORD_VALIDATION_REGEX_PATTERN}' PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(PASSWORD_VALIDATION_REGEX_PATTERN) except Exception as e: - log.error(f"Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}") - PASSWORD_VALIDATION_REGEX_PATTERN = re.compile( - r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$" - ) + log.error(f'Invalid PASSWORD_VALIDATION_REGEX_PATTERN: {e}') + PASSWORD_VALIDATION_REGEX_PATTERN = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$') + +PASSWORD_VALIDATION_HINT = os.getenv('PASSWORD_VALIDATION_HINT', '') -BYPASS_MODEL_ACCESS_CONTROL = ( - os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true" +BYPASS_MODEL_ACCESS_CONTROL = os.getenv('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true' +BYPASS_RETRIEVAL_ACCESS_CONTROL = os.getenv('BYPASS_RETRIEVAL_ACCESS_CONTROL', 'False').lower() == 'true' + +# When True, collection names that do not match any known file-*, user-memory-*, +# web-search-*, or knowledge-base collection are allowed through access control +# for non-admin users. When False (default), unknown collection names are +# denied — closing the legacy unscoped namespace. +ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS = ( + os.getenv('ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS', 'False').lower() == 'true' ) -WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get( - "WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None -) +# When enabled, skips pydub-based preprocessing (format conversion, compression, +# and chunked splitting) before sending files to processing engines. Useful when +# the upstream provider handles these steps or when ffmpeg is unavailable. +BYPASS_PYDUB_PREPROCESSING = os.getenv('BYPASS_PYDUB_PREPROCESSING', 'False').lower() == 'true' -#################################### -# WEBUI_SECRET_KEY -#################################### +# When disabled (default), the OpenAI catch-all proxy endpoint (/{path:path}) +# is blocked. Enable only if you need direct passthrough to upstream OpenAI- +# compatible APIs for endpoints not natively handled by Open WebUI. +ENABLE_OPENAI_API_PASSTHROUGH = os.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true' -WEBUI_SECRET_KEY = os.environ.get( - "WEBUI_SECRET_KEY", - os.environ.get( - "WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t" - ), # DEPRECATED: remove at next major version -) - -WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax") - -WEBUI_SESSION_COOKIE_SECURE = ( - os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true" -) - -WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get( - "WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE -) - -WEBUI_AUTH_COOKIE_SECURE = ( - os.environ.get( - "WEBUI_AUTH_COOKIE_SECURE", - os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false"), - ).lower() - == "true" -) - -if WEBUI_AUTH and WEBUI_SECRET_KEY == "": - raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) - -ENABLE_COMPRESSION_MIDDLEWARE = ( - os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true" -) +WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None) #################################### # OAUTH Configuration #################################### -ENABLE_OAUTH_EMAIL_FALLBACK = ( - os.environ.get("ENABLE_OAUTH_EMAIL_FALLBACK", "False").lower() == "true" -) +ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true' -ENABLE_OAUTH_ID_TOKEN_COOKIE = ( - os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true" -) +ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true' -OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.environ.get( - "OAUTH_CLIENT_INFO_ENCRYPTION_KEY", WEBUI_SECRET_KEY -) +OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.getenv('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY) -OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get( - "OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY -) +OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.getenv('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY', WEBUI_SECRET_KEY) + +# Maximum number of concurrent OAuth sessions per user per provider +# This prevents unbounded session growth while allowing multi-device usage +OAUTH_MAX_SESSIONS_PER_USER = int(os.getenv('OAUTH_MAX_SESSIONS_PER_USER', '10')) + +# Token Exchange Configuration +# Allows external apps to exchange OAuth tokens for OpenWebUI tokens +ENABLE_OAUTH_TOKEN_EXCHANGE = os.getenv('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true' + +# Back-Channel Logout Configuration +# When enabled, exposes POST /oauth/backchannel-logout for IdP-initiated logout +# per OpenID Connect Back-Channel Logout 1.0 spec. +# Requires Redis for JWT revocation. +ENABLE_OAUTH_BACKCHANNEL_LOGOUT = os.getenv('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true' #################################### # SCIM Configuration #################################### -ENABLE_SCIM = ( - os.environ.get("ENABLE_SCIM", os.environ.get("SCIM_ENABLED", "False")).lower() - == "true" -) -SCIM_TOKEN = os.environ.get("SCIM_TOKEN", "") +ENABLE_SCIM = os.getenv('ENABLE_SCIM', os.getenv('SCIM_ENABLED', 'False')).lower() == 'true' +SCIM_TOKEN = os.getenv('SCIM_TOKEN', '') +SCIM_AUTH_PROVIDER = os.getenv('SCIM_AUTH_PROVIDER', '') + +if ENABLE_SCIM and not SCIM_AUTH_PROVIDER: + log.warning( + 'SCIM is enabled but SCIM_AUTH_PROVIDER is not set. ' + "Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') " + 'to enable externalId storage.' + ) #################################### # LICENSE_KEY #################################### -LICENSE_KEY = os.environ.get("LICENSE_KEY", "") +LICENSE_KEY = os.getenv('LICENSE_KEY', '') LICENSE_BLOB = None -LICENSE_BLOB_PATH = os.environ.get("LICENSE_BLOB_PATH", DATA_DIR / "l.data") +LICENSE_BLOB_PATH = os.getenv('LICENSE_BLOB_PATH', DATA_DIR / 'l.data') if LICENSE_BLOB_PATH and os.path.exists(LICENSE_BLOB_PATH): - with open(LICENSE_BLOB_PATH, "rb") as f: + with open(LICENSE_BLOB_PATH, 'rb') as f: LICENSE_BLOB = f.read() -LICENSE_PUBLIC_KEY = os.environ.get("LICENSE_PUBLIC_KEY", "") +LICENSE_PUBLIC_KEY = os.getenv('LICENSE_PUBLIC_KEY', '') pk = None if LICENSE_PUBLIC_KEY: @@ -550,22 +762,84 @@ if LICENSE_PUBLIC_KEY: -----BEGIN PUBLIC KEY----- {LICENSE_PUBLIC_KEY} -----END PUBLIC KEY----- -""".encode( - "utf-8" - ) +""".encode() ) +#################################### +# WEBUI Identity +#################################### + +WEBUI_NAME = os.getenv('WEBUI_NAME', 'Open WebUI') +if WEBUI_NAME != 'Open WebUI': + WEBUI_NAME += ' (Open WebUI)' + +WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png' +WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build') +TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '') + +#################################### +# Feature flags +#################################### + +SAFE_MODE = os.getenv('SAFE_MODE', 'False').lower() == 'true' +ENABLE_EASTER_EGGS = os.getenv('ENABLE_EASTER_EGGS', 'True').lower() == 'true' +ENABLE_STAR_SESSIONS_MIDDLEWARE = os.getenv('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true' +ENABLE_KB_EXEC = os.getenv('ENABLE_KB_EXEC', 'False').lower() == 'true' + +ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.getenv('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true' +PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( + t.strip() + for t in os.getenv( + 'PROFILE_IMAGE_ALLOWED_MIME_TYPES', + 'image/png,image/jpeg,image/gif,image/webp', + ).split(',') + if t.strip() +) + +# Max stored length (bytes) of a data:image profile URI; bounds Postgres/Redis +# bloat from inline avatars and model icons. Unset (default) disables the cap. +_profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip() +PROFILE_IMAGE_MAX_DATA_URI_SIZE = ( + int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None +) + +#################################### +# Forward Headers +#################################### + +ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true' + +FORWARD_USER_INFO_HEADER_USER_NAME = os.getenv('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name') +FORWARD_USER_INFO_HEADER_USER_ID = os.getenv('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id') +FORWARD_USER_INFO_HEADER_USER_EMAIL = os.getenv('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email') +FORWARD_USER_INFO_HEADER_USER_ROLE = os.getenv('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role') +FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id') +FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id') + +#################################### +# Progressive Web App +#################################### + +EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None) + +#################################### +# GROUP DEFAULTS +#################################### + +# Controls the default "Who can share to this group" setting for new groups. +# Env var values: "true" (anyone), "false" (no one), "members" (only group members). +_default_group_share = os.getenv('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower() +DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true' + #################################### # MODELS #################################### -ENABLE_CUSTOM_MODEL_FALLBACK = ( - os.environ.get("ENABLE_CUSTOM_MODEL_FALLBACK", "False").lower() == "true" -) +ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true' -MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1") -if MODELS_CACHE_TTL == "": +MODELS_CACHE_TTL = os.getenv('MODELS_CACHE_TTL', '1') +if MODELS_CACHE_TTL == '': MODELS_CACHE_TTL = None else: try: @@ -579,207 +853,98 @@ else: #################################### ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = ( - os.environ.get("ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION", "False").lower() - == "true" + os.getenv('ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION', 'False').lower() == 'true' ) -CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get( - "CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE", "1" +# When enabled, uses a hardcoded extension-to-MIME dictionary as a last-resort +# fallback when both mimetypes.guess_type() and file.meta.content_type fail to +# determine the content type. This can help on minimal container images (e.g. +# wolfi-base) that lack /etc/mime.types AND have legacy files without stored +# content_type metadata. +ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK = ( + os.getenv('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true' ) -if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == "": +CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.getenv('CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE', '1') + +if CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE == '': CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1 else: try: - CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int( - CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE - ) + CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = int(CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE) except Exception: CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1 -CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get( - "CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES", "30" +# Maximum tool-call iterations per chat response. Set to -1 for unlimited. +# The old CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES name is accepted as a fallback. +CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = os.getenv( + 'CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS', + os.getenv('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '256'), ) -if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == "": - CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30 +if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == '': + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256 else: try: - CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES) + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = int(CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS) except Exception: - CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 30 + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256 + +# -1 means unlimited (no cap). +if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == -1: + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = None -CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.environ.get( - "CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE", "" -) +# WARNING: Experimental. Only enable if your upstream Responses API endpoint +# supports stateful sessions (i.e. server-side response storage with +# previous_response_id anchoring). Most proxies and third-party endpoints +# are stateless and will break if this is enabled. +ENABLE_RESPONSES_API_STATEFUL = os.getenv('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true' -if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == "": + +CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.getenv('CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE', '') + +if CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE == '': CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None else: try: - CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int( - CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE - ) + CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = int(CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE) except Exception: CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = None -#################################### -# WEBSOCKET SUPPORT -#################################### - -ENABLE_WEBSOCKET_SUPPORT = ( - os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true" -) - - -WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "") - -WEBSOCKET_REDIS_OPTIONS = os.environ.get("WEBSOCKET_REDIS_OPTIONS", "") - - -if WEBSOCKET_REDIS_OPTIONS == "": - if REDIS_SOCKET_CONNECT_TIMEOUT: - WEBSOCKET_REDIS_OPTIONS = { - "socket_connect_timeout": REDIS_SOCKET_CONNECT_TIMEOUT - } - else: - log.debug("No WEBSOCKET_REDIS_OPTIONS provided, defaulting to None") - WEBSOCKET_REDIS_OPTIONS = None -else: - try: - WEBSOCKET_REDIS_OPTIONS = json.loads(WEBSOCKET_REDIS_OPTIONS) - except Exception: - log.warning("Invalid WEBSOCKET_REDIS_OPTIONS, defaulting to None") - WEBSOCKET_REDIS_OPTIONS = None - -WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL) -WEBSOCKET_REDIS_CLUSTER = ( - os.environ.get("WEBSOCKET_REDIS_CLUSTER", str(REDIS_CLUSTER)).lower() == "true" -) - -websocket_redis_lock_timeout = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", "60") - -try: - WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout) -except ValueError: - WEBSOCKET_REDIS_LOCK_TIMEOUT = 60 - -WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "") -WEBSOCKET_SENTINEL_PORT = os.environ.get("WEBSOCKET_SENTINEL_PORT", "26379") -WEBSOCKET_SERVER_LOGGING = ( - os.environ.get("WEBSOCKET_SERVER_LOGGING", "False").lower() == "true" -) -WEBSOCKET_SERVER_ENGINEIO_LOGGING = ( - os.environ.get("WEBSOCKET_SERVER_LOGGING", "False").lower() == "true" -) -WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get("WEBSOCKET_SERVER_PING_TIMEOUT", "20") -try: - WEBSOCKET_SERVER_PING_TIMEOUT = int(WEBSOCKET_SERVER_PING_TIMEOUT) -except ValueError: - WEBSOCKET_SERVER_PING_TIMEOUT = 20 - -WEBSOCKET_SERVER_PING_INTERVAL = os.environ.get("WEBSOCKET_SERVER_PING_INTERVAL", "25") -try: - WEBSOCKET_SERVER_PING_INTERVAL = int(WEBSOCKET_SERVER_PING_INTERVAL) -except ValueError: - WEBSOCKET_SERVER_PING_INTERVAL = 25 - - -REQUESTS_VERIFY = os.environ.get("REQUESTS_VERIFY", "True").lower() == "true" - -AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "") - -if AIOHTTP_CLIENT_TIMEOUT == "": - AIOHTTP_CLIENT_TIMEOUT = None -else: - try: - AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT) - except Exception: - AIOHTTP_CLIENT_TIMEOUT = 300 - - -AIOHTTP_CLIENT_SESSION_SSL = ( - os.environ.get("AIOHTTP_CLIENT_SESSION_SSL", "True").lower() == "true" -) - -AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get( - "AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST", - os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "10"), -) - -if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "": - AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None -else: - try: - AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) - except Exception: - AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 10 - - -AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get( - "AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA", "10" -) - -if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA == "": - AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = None -else: - try: - AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int( - AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA - ) - except Exception: - AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10 - - -AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = ( - os.environ.get("AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL", "True").lower() == "true" -) - - #################################### # SENTENCE TRANSFORMERS #################################### -SENTENCE_TRANSFORMERS_BACKEND = os.environ.get("SENTENCE_TRANSFORMERS_BACKEND", "") -if SENTENCE_TRANSFORMERS_BACKEND == "": - SENTENCE_TRANSFORMERS_BACKEND = "torch" +SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '') +if SENTENCE_TRANSFORMERS_BACKEND == '': + SENTENCE_TRANSFORMERS_BACKEND = 'torch' -SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get( - "SENTENCE_TRANSFORMERS_MODEL_KWARGS", "" -) -if SENTENCE_TRANSFORMERS_MODEL_KWARGS == "": +SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '') +if SENTENCE_TRANSFORMERS_MODEL_KWARGS == '': SENTENCE_TRANSFORMERS_MODEL_KWARGS = None else: try: - SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads( - SENTENCE_TRANSFORMERS_MODEL_KWARGS - ) + SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_MODEL_KWARGS) except Exception: SENTENCE_TRANSFORMERS_MODEL_KWARGS = None -SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get( - "SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND", "" -) -if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == "": - SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = "torch" +SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '') +if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == '': + SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = 'torch' -SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get( - "SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS", "" -) -if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == "": +SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', '') +if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == '': SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None else: try: - SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads( - SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS - ) + SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS) except Exception: SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None @@ -787,140 +952,111 @@ else: # When enabled (default), scores are normalized to 0-1 range for proper # relevance threshold behavior with MS MARCO models. SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION = ( - os.environ.get( - "SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION", "True" - ).lower() - == "true" + os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true' ) +#################################### +# TOOLS/FUNCTIONS PIP OPTIONS +#################################### + +ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = ( + os.getenv('ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS', 'True').lower() == 'true' +) + +PIP_OPTIONS = os.getenv('PIP_OPTIONS', '').split() +PIP_PACKAGE_INDEX_OPTIONS = os.getenv('PIP_PACKAGE_INDEX_OPTIONS', '').split() + + #################################### # OFFLINE_MODE #################################### -ENABLE_VERSION_UPDATE_CHECK = ( - os.environ.get("ENABLE_VERSION_UPDATE_CHECK", "true").lower() == "true" -) -OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true" +ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true' +OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true' if OFFLINE_MODE: - os.environ["HF_HUB_OFFLINE"] = "1" + os.environ['HF_HUB_OFFLINE'] = '1' ENABLE_VERSION_UPDATE_CHECK = False #################################### -# AUDIT LOGGING +# Audit logging #################################### -ENABLE_AUDIT_STDOUT = os.getenv("ENABLE_AUDIT_STDOUT", "False").lower() == "true" -ENABLE_AUDIT_LOGS_FILE = os.getenv("ENABLE_AUDIT_LOGS_FILE", "True").lower() == "true" +ENABLE_AUDIT_STDOUT = os.getenv('ENABLE_AUDIT_STDOUT', 'False').lower() == 'true' +ENABLE_AUDIT_LOGS_FILE = os.getenv('ENABLE_AUDIT_LOGS_FILE', 'True').lower() == 'true' # Where to store log file # Defaults to the DATA_DIR/audit.log. To set AUDIT_LOGS_FILE_PATH you need to # provide the whole path, like: /app/audit.log -AUDIT_LOGS_FILE_PATH = os.getenv("AUDIT_LOGS_FILE_PATH", f"{DATA_DIR}/audit.log") +AUDIT_LOGS_FILE_PATH = os.getenv('AUDIT_LOGS_FILE_PATH', f'{DATA_DIR}/audit.log') # Maximum size of a file before rotating into a new log file -AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB") +AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv('AUDIT_LOG_FILE_ROTATION_SIZE', '10MB') # Comma separated list of logger names to use for audit logging # Default is "uvicorn.access" which is the access log for Uvicorn # You can add more logger names to this list if you want to capture more logs -AUDIT_UVICORN_LOGGER_NAMES = os.getenv( - "AUDIT_UVICORN_LOGGER_NAMES", "uvicorn.access" -).split(",") +AUDIT_UVICORN_LOGGER_NAMES = os.getenv('AUDIT_UVICORN_LOGGER_NAMES', 'uvicorn.access').split(',') # METADATA | REQUEST | REQUEST_RESPONSE -AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "NONE").upper() +AUDIT_LOG_LEVEL = os.getenv('AUDIT_LOG_LEVEL', 'NONE').upper() try: - MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048) + MAX_BODY_LOG_SIZE = int(os.getenv('MAX_BODY_LOG_SIZE') or 2048) except ValueError: MAX_BODY_LOG_SIZE = 2048 # Comma separated list for urls to exclude from audit -AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split( - "," -) +AUDIT_EXCLUDED_PATHS = os.getenv('AUDIT_EXCLUDED_PATHS', '/chats,/chat,/folders').split(',') AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS] -AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS] +AUDIT_EXCLUDED_PATHS = [path.lstrip('/') for path in AUDIT_EXCLUDED_PATHS] + +# Comma separated list of urls to include in audit (whitelist mode) +# When set, only these paths are audited and AUDIT_EXCLUDED_PATHS is ignored +AUDIT_INCLUDED_PATHS = os.getenv('AUDIT_INCLUDED_PATHS', '').split(',') +AUDIT_INCLUDED_PATHS = [path.strip() for path in AUDIT_INCLUDED_PATHS] +AUDIT_INCLUDED_PATHS = [path.lstrip('/') for path in AUDIT_INCLUDED_PATHS if path] + +# When enabled, GET requests are also audited (disabled by default to avoid log noise) +ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lower() == 'true' #################################### # OPENTELEMETRY #################################### -ENABLE_OTEL = os.environ.get("ENABLE_OTEL", "False").lower() == "true" -ENABLE_OTEL_TRACES = os.environ.get("ENABLE_OTEL_TRACES", "False").lower() == "true" -ENABLE_OTEL_METRICS = os.environ.get("ENABLE_OTEL_METRICS", "False").lower() == "true" -ENABLE_OTEL_LOGS = os.environ.get("ENABLE_OTEL_LOGS", "False").lower() == "true" +ENABLE_OTEL = os.getenv('ENABLE_OTEL', 'False').lower() == 'true' +ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true' +ENABLE_OTEL_METRICS = os.getenv('ENABLE_OTEL_METRICS', 'False').lower() == 'true' +ENABLE_OTEL_LOGS = os.getenv('ENABLE_OTEL_LOGS', 'False').lower() == 'true' -OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get( - "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317" -) -OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.environ.get( - "OTEL_METRICS_EXPORTER_OTLP_ENDPOINT", OTEL_EXPORTER_OTLP_ENDPOINT -) -OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.environ.get( - "OTEL_LOGS_EXPORTER_OTLP_ENDPOINT", OTEL_EXPORTER_OTLP_ENDPOINT -) -OTEL_EXPORTER_OTLP_INSECURE = ( - os.environ.get("OTEL_EXPORTER_OTLP_INSECURE", "False").lower() == "true" -) +OTEL_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317') +OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT) +OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT) +OTEL_EXPORTER_OTLP_INSECURE = os.getenv('OTEL_EXPORTER_OTLP_INSECURE', 'False').lower() == 'true' OTEL_METRICS_EXPORTER_OTLP_INSECURE = ( - os.environ.get( - "OTEL_METRICS_EXPORTER_OTLP_INSECURE", str(OTEL_EXPORTER_OTLP_INSECURE) - ).lower() - == "true" + os.getenv('OTEL_METRICS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true' ) OTEL_LOGS_EXPORTER_OTLP_INSECURE = ( - os.environ.get( - "OTEL_LOGS_EXPORTER_OTLP_INSECURE", str(OTEL_EXPORTER_OTLP_INSECURE) - ).lower() - == "true" + os.getenv('OTEL_LOGS_EXPORTER_OTLP_INSECURE', str(OTEL_EXPORTER_OTLP_INSECURE)).lower() == 'true' ) -OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui") -OTEL_RESOURCE_ATTRIBUTES = os.environ.get( - "OTEL_RESOURCE_ATTRIBUTES", "" -) # e.g. key1=val1,key2=val2 -OTEL_TRACES_SAMPLER = os.environ.get( - "OTEL_TRACES_SAMPLER", "parentbased_always_on" -).lower() -OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "") -OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "") +OTEL_SERVICE_NAME = os.getenv('OTEL_SERVICE_NAME', 'open-webui') +OTEL_RESOURCE_ATTRIBUTES = os.getenv('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2 +OTEL_TRACES_SAMPLER = os.getenv('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower() +OTEL_BASIC_AUTH_USERNAME = os.getenv('OTEL_BASIC_AUTH_USERNAME', '') +OTEL_BASIC_AUTH_PASSWORD = os.getenv('OTEL_BASIC_AUTH_PASSWORD', '') +OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.getenv('OTEL_METRICS_EXPORT_INTERVAL_MILLIS', '10000')) -OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get( - "OTEL_METRICS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME -) -OTEL_METRICS_BASIC_AUTH_PASSWORD = os.environ.get( - "OTEL_METRICS_BASIC_AUTH_PASSWORD", OTEL_BASIC_AUTH_PASSWORD -) -OTEL_LOGS_BASIC_AUTH_USERNAME = os.environ.get( - "OTEL_LOGS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME -) -OTEL_LOGS_BASIC_AUTH_PASSWORD = os.environ.get( - "OTEL_LOGS_BASIC_AUTH_PASSWORD", OTEL_BASIC_AUTH_PASSWORD -) +OTEL_METRICS_BASIC_AUTH_USERNAME = os.getenv('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME) +OTEL_METRICS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD) +OTEL_LOGS_BASIC_AUTH_USERNAME = os.getenv('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME) +OTEL_LOGS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_LOGS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD) -OTEL_OTLP_SPAN_EXPORTER = os.environ.get( - "OTEL_OTLP_SPAN_EXPORTER", "grpc" +OTEL_OTLP_SPAN_EXPORTER = os.getenv('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http + +OTEL_METRICS_OTLP_SPAN_EXPORTER = os.getenv( + 'OTEL_METRICS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER ).lower() # grpc or http -OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get( - "OTEL_METRICS_OTLP_SPAN_EXPORTER", OTEL_OTLP_SPAN_EXPORTER +OTEL_LOGS_OTLP_SPAN_EXPORTER = os.getenv( + 'OTEL_LOGS_OTLP_SPAN_EXPORTER', OTEL_OTLP_SPAN_EXPORTER ).lower() # grpc or http - -OTEL_LOGS_OTLP_SPAN_EXPORTER = os.environ.get( - "OTEL_LOGS_OTLP_SPAN_EXPORTER", OTEL_OTLP_SPAN_EXPORTER -).lower() # grpc or http - -#################################### -# TOOLS/FUNCTIONS PIP OPTIONS -#################################### - -PIP_OPTIONS = os.getenv("PIP_OPTIONS", "").split() -PIP_PACKAGE_INDEX_OPTIONS = os.getenv("PIP_PACKAGE_INDEX_OPTIONS", "").split() - - -#################################### -# PROGRESSIVE WEB APP OPTIONS -#################################### - -EXTERNAL_PWA_MANIFEST_URL = os.environ.get("EXTERNAL_PWA_MANIFEST_URL") diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index e1604d126a..4a82cf7f26 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -1,11 +1,10 @@ -import logging -import sys +import asyncio import inspect import json -import asyncio - -from pydantic import BaseModel +import logging +import sys from typing import AsyncGenerator, Generator, Iterator + from fastapi import ( Depends, FastAPI, @@ -16,60 +15,52 @@ from fastapi import ( UploadFile, status, ) +from pydantic import BaseModel from starlette.responses import Response, StreamingResponse - +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.constants import ERROR_MESSAGES +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.models.functions import Functions +from open_webui.models.models import Models +from open_webui.models.users import UserModel from open_webui.socket.main import ( get_event_call, get_event_emitter, ) - - -from open_webui.models.users import UserModel -from open_webui.models.functions import Functions -from open_webui.models.models import Models - -from open_webui.utils.plugin import ( - load_function_module_by_id, - get_function_module_from_cache, -) -from open_webui.utils.tools import get_tools -from open_webui.utils.access_control import has_access - -from open_webui.env import GLOBAL_LOG_LEVEL - +from open_webui.utils.access_control import check_model_access from open_webui.utils.misc import ( add_or_update_system_message, get_last_user_message, - prepend_to_first_user_message_content, openai_chat_chunk_message_template, openai_chat_completion_message_template, + prepend_to_first_user_message_content, ) from open_webui.utils.payload import ( apply_model_params_to_body_openai, apply_system_prompt_to_body, ) - +from open_webui.utils.plugin import ( + get_function_module_from_cache, + load_function_module_by_id, +) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) -def get_function_module_by_id(request: Request, pipe_id: str): - function_module, _, _ = get_function_module_from_cache(request, pipe_id) +async def get_function_module_by_id(request: Request, pipe_id: str): + function_module, _, _ = await get_function_module_from_cache(request, pipe_id) - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + if hasattr(function_module, 'valves') and hasattr(function_module, 'Valves'): Valves = function_module.Valves - valves = Functions.get_function_valves_by_id(pipe_id) + valves = await Functions.get_function_valves_by_id(pipe_id) if valves: try: - function_module.valves = Valves( - **{k: v for k, v in valves.items() if v is not None} - ) + function_module.valves = Valves(**{k: v for k, v in valves.items() if v is not None}) except Exception as e: - log.exception(f"Error loading valves for function {pipe_id}: {e}") + log.exception(f'Error loading valves for function {pipe_id}: {e}') raise e else: function_module.valves = Valves() @@ -78,19 +69,19 @@ def get_function_module_by_id(request: Request, pipe_id: str): async def get_function_models(request): - pipes = Functions.get_functions_by_type("pipe", active_only=True) + pipes = await Functions.get_functions_by_type('pipe', active_only=True) pipe_models = [] for pipe in pipes: try: - function_module = get_function_module_by_id(request, pipe.id) + function_module = await get_function_module_by_id(request, pipe.id) has_user_valves = False - if hasattr(function_module, "UserValves"): + if hasattr(function_module, 'UserValves'): has_user_valves = True # Check if function is a manifold - if hasattr(function_module, "pipes"): + if hasattr(function_module, 'pipes'): sub_pipes = [] # Handle pipes being a list, sync function, or async function @@ -106,32 +97,30 @@ async def get_function_models(request): log.exception(e) sub_pipes = [] - log.debug( - f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}" - ) + log.debug(f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}") for p in sub_pipes: sub_pipe_id = f'{pipe.id}.{p["id"]}' - sub_pipe_name = p["name"] + sub_pipe_name = p['name'] - if hasattr(function_module, "name"): - sub_pipe_name = f"{function_module.name}{sub_pipe_name}" + if hasattr(function_module, 'name'): + sub_pipe_name = f'{function_module.name}{sub_pipe_name}' - pipe_flag = {"type": pipe.type} + pipe_flag = {'type': pipe.type} pipe_models.append( { - "id": sub_pipe_id, - "name": sub_pipe_name, - "object": "model", - "created": pipe.created_at, - "owned_by": "openai", - "pipe": pipe_flag, - "has_user_valves": has_user_valves, + 'id': sub_pipe_id, + 'name': sub_pipe_name, + 'object': 'model', + 'created': pipe.created_at, + 'owned_by': 'openai', + 'pipe': pipe_flag, + 'has_user_valves': has_user_valves, } ) else: - pipe_flag = {"type": "pipe"} + pipe_flag = {'type': 'pipe'} log.debug( f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" @@ -139,13 +128,13 @@ async def get_function_models(request): pipe_models.append( { - "id": pipe.id, - "name": pipe.name, - "object": "model", - "created": pipe.created_at, - "owned_by": "openai", - "pipe": pipe_flag, - "has_user_valves": has_user_valves, + 'id': pipe.id, + 'name': pipe.name, + 'object': 'model', + 'created': pipe.created_at, + 'owned_by': 'openai', + 'pipe': pipe_flag, + 'has_user_valves': has_user_valves, } ) except Exception as e: @@ -155,9 +144,7 @@ async def get_function_models(request): return pipe_models -async def generate_function_chat_completion( - request, form_data, user, models: dict = {} -): +async def generate_function_chat_completion(request, form_data, user, models: dict = {}): async def execute_pipe(pipe, params): if inspect.iscoroutinefunction(pipe): return await pipe(**params) @@ -168,35 +155,35 @@ async def generate_function_chat_completion( if isinstance(res, str): return res if isinstance(res, Generator): - return "".join(map(str, res)) + return ''.join(map(str, res)) if isinstance(res, AsyncGenerator): - return "".join([str(stream) async for stream in res]) + return ''.join([str(stream) async for stream in res]) def process_line(form_data: dict, line): if isinstance(line, BaseModel): line = line.model_dump_json() - line = f"data: {line}" + line = f'data: {line}' if isinstance(line, dict): - line = f"data: {json.dumps(line)}" + line = f'data: {json.dumps(line)}' try: - line = line.decode("utf-8") + line = line.decode('utf-8') except Exception: pass - if line.startswith("data:"): - return f"{line}\n\n" + if line.startswith('data:'): + return f'{line}\n\n' else: - line = openai_chat_chunk_message_template(form_data["model"], line) - return f"data: {json.dumps(line)}\n\n" + line = openai_chat_chunk_message_template(form_data['model'], line) + return f'data: {json.dumps(line)}\n\n' def get_pipe_id(form_data: dict) -> str: - pipe_id = form_data["model"] - if "." in pipe_id: - pipe_id, _ = pipe_id.split(".", 1) + pipe_id = form_data['model'] + if '.' in pipe_id: + pipe_id, _ = pipe_id.split('.', 1) return pipe_id - def get_function_params(function_module, form_data, user, extra_params=None): + async def get_function_params(function_module, form_data, user, extra_params=None): if extra_params is None: extra_params = {} @@ -204,27 +191,25 @@ async def generate_function_chat_completion( # Get the signature of the function sig = inspect.signature(function_module.pipe) - params = {"body": form_data} | { - k: v for k, v in extra_params.items() if k in sig.parameters - } + params = {'body': form_data} | {k: v for k, v in extra_params.items() if k in sig.parameters} - if "__user__" in params and hasattr(function_module, "UserValves"): - user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id) + if '__user__' in params and hasattr(function_module, 'UserValves'): + user_valves = await Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id) try: - params["__user__"]["valves"] = function_module.UserValves(**user_valves) + params['__user__']['valves'] = function_module.UserValves(**user_valves) except Exception as e: log.exception(e) - params["__user__"]["valves"] = function_module.UserValves() + params['__user__']['valves'] = function_module.UserValves() return params - model_id = form_data.get("model") - model_info = Models.get_model_by_id(model_id) + model_id = form_data.get('model') + model_info = await Models.get_model_by_id(model_id) - metadata = form_data.pop("metadata", {}) + metadata = form_data.pop('metadata', {}) - files = metadata.get("files", []) - tool_ids = metadata.get("tool_ids", []) + files = metadata.get('files', []) + tool_ids = metadata.get('tool_ids', []) # Check if tool_ids is None if tool_ids is None: tool_ids = [] @@ -235,66 +220,73 @@ async def generate_function_chat_completion( __task_body__ = None if metadata: - if all(k in metadata for k in ("session_id", "chat_id", "message_id")): - __event_emitter__ = get_event_emitter(metadata) - __event_call__ = get_event_call(metadata) - __task__ = metadata.get("task", None) - __task_body__ = metadata.get("task_body", None) + if all(k in metadata for k in ('session_id', 'chat_id', 'message_id')): + __event_emitter__ = await get_event_emitter(metadata) + __event_call__ = await get_event_call(metadata) + __task__ = metadata.get('task', None) + __task_body__ = metadata.get('task_body', None) oauth_token = None try: - if request.cookies.get("oauth_session_id", None): + oauth_session_id = request.cookies.get('oauth_session_id', None) + if oauth_session_id: oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, - request.cookies.get("oauth_session_id", None), + oauth_session_id, ) + + # Fallback: no cookie (automation, API key, etc.) — use most recent session + if oauth_token is None: + from open_webui.models.oauth_sessions import OAuthSessions + + sessions = await OAuthSessions.get_sessions_by_user_id(user.id) + if sessions: + best = max(sessions, key=lambda s: s.updated_at) + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + best.id, + ) except Exception as e: - log.error(f"Error getting OAuth token: {e}") + log.error(f'Error getting OAuth token: {e}') extra_params = { - "__event_emitter__": __event_emitter__, - "__event_call__": __event_call__, - "__chat_id__": metadata.get("chat_id", None), - "__session_id__": metadata.get("session_id", None), - "__message_id__": metadata.get("message_id", None), - "__task__": __task__, - "__task_body__": __task_body__, - "__files__": files, - "__user__": user.model_dump() if isinstance(user, UserModel) else {}, - "__metadata__": metadata, - "__oauth_token__": oauth_token, - "__request__": request, + '__event_emitter__': __event_emitter__, + '__event_call__': __event_call__, + '__chat_id__': metadata.get('chat_id', None), + '__session_id__': metadata.get('session_id', None), + '__message_id__': metadata.get('message_id', None), + '__task__': __task__, + '__task_body__': __task_body__, + '__files__': files, + '__user__': user.model_dump() if isinstance(user, UserModel) else {}, + '__metadata__': metadata, + '__oauth_token__': oauth_token, + '__request__': request, } - extra_params["__tools__"] = await get_tools( - request, - tool_ids, - user, - { - **extra_params, - "__model__": models.get(form_data["model"], None), - "__messages__": form_data["messages"], - "__files__": files, - }, - ) + extra_params['__tools__'] = metadata.get('tools', {}) if model_info: if model_info.base_model_id: - form_data["model"] = model_info.base_model_id + form_data['model'] = model_info.base_model_id + + if not BYPASS_MODEL_ACCESS_CONTROL: + bypass = isinstance(user, UserModel) and user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL + await check_model_access(user if isinstance(user, UserModel) else UserModel(**user), model_info, bypass) params = model_info.params.model_dump() if params: - system = params.pop("system", None) + system = params.pop('system', None) form_data = apply_model_params_to_body_openai(params, form_data) - form_data = apply_system_prompt_to_body(system, form_data, metadata, user) + form_data = await apply_system_prompt_to_body(system, form_data, metadata, user) pipe_id = get_pipe_id(form_data) - function_module = get_function_module_by_id(request, pipe_id) + function_module = await get_function_module_by_id(request, pipe_id) pipe = function_module.pipe - params = get_function_params(function_module, form_data, user, extra_params) + params = await get_function_params(function_module, form_data, user, extra_params) - if form_data.get("stream", False): + if form_data.get('stream', False): async def stream_content(): try: @@ -306,17 +298,17 @@ async def generate_function_chat_completion( yield data return if isinstance(res, dict): - yield f"data: {json.dumps(res)}\n\n" + yield f'data: {json.dumps(res)}\n\n' return except Exception as e: - log.error(f"Error: {e}") - yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n" + log.error(f'Error: {e}') + yield f'data: {json.dumps({"error": {"detail": str(e)}})}\n\n' return if isinstance(res, str): - message = openai_chat_chunk_message_template(form_data["model"], res) - yield f"data: {json.dumps(message)}\n\n" + message = openai_chat_chunk_message_template(form_data['model'], res) + yield f'data: {json.dumps(message)}\n\n' if isinstance(res, Iterator): for line in res: @@ -326,22 +318,19 @@ async def generate_function_chat_completion( async for line in res: yield process_line(form_data, line) - if isinstance(res, str) or isinstance(res, Generator): - finish_message = openai_chat_chunk_message_template( - form_data["model"], "" - ) - finish_message["choices"][0]["finish_reason"] = "stop" - yield f"data: {json.dumps(finish_message)}\n\n" - yield "data: [DONE]" + finish_message = openai_chat_chunk_message_template(form_data['model'], '') + finish_message['choices'][0]['finish_reason'] = 'stop' + yield f'data: {json.dumps(finish_message)}\n\n' + yield 'data: [DONE]' - return StreamingResponse(stream_content(), media_type="text/event-stream") + return StreamingResponse(stream_content(), media_type='text/event-stream') else: try: res = await execute_pipe(pipe, params) except Exception as e: - log.error(f"Error: {e}") - return {"error": {"detail": str(e)}} + log.error(f'Error: {e}') + return {'error': {'detail': str(e)}} if isinstance(res, StreamingResponse) or isinstance(res, dict): return res @@ -349,4 +338,4 @@ async def generate_function_chat_completion( return res.model_dump() message = await get_message_content(res) - return openai_chat_completion_message_template(form_data["model"], message) + return openai_chat_completion_message_template(form_data['model'], message) diff --git a/backend/open_webui/internal/config.py b/backend/open_webui/internal/config.py new file mode 100644 index 0000000000..46f5b1b67d --- /dev/null +++ b/backend/open_webui/internal/config.py @@ -0,0 +1,265 @@ +"""Database-backed configuration with environment variable defaults.""" + +from __future__ import annotations + +import asyncio +import json +import logging +from datetime import datetime +from functools import reduce +from typing import Any, Optional, Union + +import redis +from open_webui.internal.db import Base, get_async_db, get_db +from open_webui.utils.redis import get_redis_connection +from sqlalchemy import JSON, Column, DateTime, Integer, func, select + +log = logging.getLogger(__name__) + + +# ── Model ──────────────────────────────────────────────────────────────────── + + +class ConfigTable(Base): + __tablename__ = 'config' + + id = Column(Integer, primary_key=True) + data = Column(JSON, nullable=False) + version = Column(Integer, nullable=False, default=0) + created_at = Column(DateTime, nullable=False, server_default=func.now()) + updated_at = Column(DateTime, nullable=True, onupdate=func.now()) + + +# ── Blob ───────────────────────────────────────────────────────────────────── + + +class ConfigState: + """In-memory mirror of the single-row config JSON blob.""" + + __slots__ = ('_data',) + + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + @property + def snapshot(self) -> dict: + return self._data + + def read(self, path: str) -> Any: + return reduce( + lambda n, k: n.get(k) if isinstance(n, dict) else None, + path.split('.'), + self._data, + ) + + def write(self, path: str, value: Any) -> None: + keys = path.split('.') + reduce(lambda d, k: d.setdefault(k, {}), keys[:-1], self._data)[keys[-1]] = value + + def replace(self, data: dict) -> None: + self._data = data + + def load(self) -> dict: + with get_db() as db: + row = db.query(ConfigTable).order_by(ConfigTable.id.desc()).first() + self._data = row.data if row else {'version': 0, 'ui': {}} + return self._data + + def persist(self, data: dict | None = None) -> None: + if data is not None: + self._data = data + with get_db() as db: + row = db.query(ConfigTable).first() + if row is None: + db.add(ConfigTable(data=self._data, version=0)) + else: + row.data, row.updated_at = self._data, datetime.now() + db.add(row) + db.commit() + + async def persist_async(self, data: dict | None = None) -> None: + if data is not None: + self._data = data + async with get_async_db() as db: + result = await db.execute(select(ConfigTable).limit(1)) + row = result.scalars().first() + if row is None: + db.add(ConfigTable(data=self._data, version=0)) + else: + row.data, row.updated_at = self._data, datetime.now() + db.add(row) + await db.commit() + + def clear(self) -> None: + with get_db() as db: + db.query(ConfigTable).delete() + db.commit() + + async def clear_async(self) -> None: + from sqlalchemy import delete as sa_delete + + async with get_async_db() as db: + await db.execute(sa_delete(ConfigTable)) + await db.commit() + + +STATE = ConfigState() + + +# ── ConfigVar ────────────────────────────────────────────────────────────────── + + +_persist_enabled: bool = True +_oauth_persist_enabled: bool = False +_all_configs: list[ConfigVar] = [] + + +def initialize(*, enable_persistent: bool = True, enable_oauth_persistent: bool = False) -> dict: + global _persist_enabled, _oauth_persist_enabled + _persist_enabled = enable_persistent + _oauth_persist_enabled = enable_oauth_persistent + return STATE.load() + + +class ConfigVar: + __slots__ = ('env_name', 'config_path', 'env_value', 'config_value', 'value') + + def __init__(self, env_name: str, config_path: str, env_value: Any) -> None: + self.env_name = env_name + self.config_path = config_path + self.env_value = env_value + self.config_value = STATE.read(config_path) + + if self.config_value is not None and _persist_enabled: + if config_path.startswith('oauth.') and not _oauth_persist_enabled: + log.info("Skipping DB value for '%s' (OAuth persistence disabled)", env_name) + self.value = env_value + else: + log.info("'%s' loaded from database", env_name) + self.value = self.config_value + else: + self.value = env_value + + _all_configs.append(self) + + def __str__(self) -> str: + return str(self.value) + + def __repr__(self) -> str: + return f'' + + @property + def __dict__(self): # type: ignore[override] + raise TypeError(f"ConfigVar('{self.env_name}') cannot be cast to dict; use .value") + + def __getattribute__(self, item: str): + if item == '__dict__': + raise TypeError('ConfigVar cannot be cast to dict; use .value') + return super().__getattribute__(item) + + def refresh(self) -> None: + current = STATE.read(self.config_path) + if current is not None: + self.value = current + log.info('Refreshed %s → %s', self.env_name, self.value) + + def commit(self) -> None: + log.info("Persisting '%s'", self.env_name) + STATE.write(self.config_path, self.value) + self.config_value = self.value + STATE.persist() + + async def commit_async(self) -> None: + log.info("Persisting '%s'", self.env_name) + STATE.write(self.config_path, self.value) + self.config_value = self.value + await STATE.persist_async() + + +# ── AppConfig ────────────────────────────────────────────────────────── + + +class AppConfig: + """Attribute-style container for ConfigVars with optional Redis sync.""" + + def __init__( + self, + *, + redis_url: Optional[str] = None, + redis_sentinels: Optional[list] = None, + redis_cluster: bool = False, + redis_key_prefix: str = 'open-webui', + ) -> None: + super().__setattr__('_entries', {}) + super().__setattr__('_key_prefix', redis_key_prefix) + + # If sentinels weren't explicitly provided, read from env. + if redis_sentinels is None: + from open_webui.env import REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT + from open_webui.utils.redis import get_sentinels_from_env + + redis_sentinels = get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT) + + rc: Union[redis.Redis, redis.cluster.RedisCluster, None] = None + if redis_url: + rc = get_redis_connection(redis_url, redis_sentinels or [], redis_cluster, decode_responses=True) + super().__setattr__('_rc', rc) + + def __setattr__(self, name: str, value: Any) -> None: + entries: dict = super().__getattribute__('_entries') + + if isinstance(value, ConfigVar): + entries[name] = value + return + + entries[name].value = value + + try: + asyncio.get_running_loop().create_task(self._write_async(name)) + except RuntimeError: + entries[name].commit() + + rc = super().__getattribute__('_rc') + if rc and _persist_enabled: + prefix = super().__getattribute__('_key_prefix') + try: + rc.set(f'{prefix}:config:{name}', json.dumps(entries[name].value)) + except Exception as exc: + log.error("Redis write failed for '%s': %s", name, exc) + + async def _write_async(self, name: str) -> None: + try: + await self._entries[name].commit_async() + except Exception as exc: + log.error("Async persist failed for '%s': %s", name, exc) + + def __getattr__(self, name: str) -> Any: + entries = super().__getattribute__('_entries') + if name not in entries: + raise AttributeError(f"No config key '{name}'") + + rc = super().__getattribute__('_rc') + if rc and _persist_enabled: + prefix = super().__getattribute__('_key_prefix') + try: + raw = rc.get(f'{prefix}:config:{name}') + if raw is not None: + decoded = json.loads(raw) + if entries[name].value != decoded: + entries[name].value = decoded + log.info("Updated '%s' from Redis", name) + except Exception as exc: + log.error("Redis read failed for '%s': %s", name, exc) + + return entries[name].value + + def _sync_to_redis(self) -> None: + rc = super().__getattribute__('_rc') + if not rc or not _persist_enabled: + return + prefix = super().__getattribute__('_key_prefix') + for name, s in super().__getattribute__('_entries').items(): + try: + rc.set(f'{prefix}:config:{name}', json.dumps(s.value)) + except Exception as exc: + log.error("Redis sync failed for '%s': %s", name, exc) diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index 6050e37fa3..496029fe5c 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -1,98 +1,187 @@ -import os +from __future__ import annotations + import json import logging -from contextlib import contextmanager +import os +import sys +from contextlib import asynccontextmanager, contextmanager from typing import Any, Optional +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse -from open_webui.internal.wrappers import register_connection from open_webui.env import ( - OPEN_WEBUI_DIR, - DATABASE_URL, - DATABASE_SCHEMA, + DATABASE_ENABLE_SESSION_SHARING, + DATABASE_ENABLE_SQLITE_WAL, DATABASE_POOL_MAX_OVERFLOW, DATABASE_POOL_RECYCLE, DATABASE_POOL_SIZE, DATABASE_POOL_TIMEOUT, - DATABASE_ENABLE_SQLITE_WAL, - DATABASE_ENABLE_SESSION_SHARING, + DATABASE_SCHEMA, + DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT, + DATABASE_SQLITE_PRAGMA_CACHE_SIZE, + DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT, + DATABASE_SQLITE_PRAGMA_MMAP_SIZE, + DATABASE_SQLITE_PRAGMA_SYNCHRONOUS, + DATABASE_SQLITE_PRAGMA_TEMP_STORE, + DATABASE_URL, ENABLE_DB_MIGRATIONS, + OPEN_WEBUI_DIR, ) -from peewee_migrate import Router -from sqlalchemy import Dialect, create_engine, MetaData, event, types +from sqlalchemy import Dialect, MetaData, create_engine, event, types +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session, sessionmaker, Session -from sqlalchemy.pool import QueuePool, NullPool +from sqlalchemy.orm import Session, scoped_session, sessionmaker +from sqlalchemy.pool import NullPool, QueuePool from sqlalchemy.sql.type_api import _T from typing_extensions import Self log = logging.getLogger(__name__) -class JSONField(types.TypeDecorator): - impl = types.Text +# ── SSL URL normalization (used by sync engine & Alembic migrations) ─ +# +# psycopg2 (sync) needs ``sslmode=`` in the connection string (it does +# not recognise the bare ``ssl=`` key that some ORMs emit). The helpers +# below strip all SSL-related query params, normalise them, and +# reattach them in the canonical libpq form. +# +# The **async** engine now uses psycopg (v3), which speaks libpq +# natively, so it needs no translation at all — the DATABASE_URL is +# passed through as-is. +# ───────────────────────────────────────────────────────────────────── + + +def _pop_first(params: dict[str, list[str]], key: str) -> str | None: + """Pop a single-valued query param, returning ``None`` if absent.""" + values = params.pop(key, None) + return values[0] if values else None + + +def _is_postgres_url(url: str) -> bool: + """Return True if *url* looks like a PostgreSQL connection string.""" + return bool(url) and any(url.startswith(p) for p in ('postgresql://', 'postgresql+', 'postgres://')) + + +def extract_ssl_params_from_url(url: str) -> tuple[str, dict[str, str]]: + """Strip SSL query-string parameters from a PostgreSQL URL. + + Returns ``(url_without_ssl, ssl_dict)`` where *ssl_dict* maps + canonical libpq key names (``sslmode``, ``sslrootcert``, …) to + their values. Non-PostgreSQL URLs are returned unchanged with an + empty dict. + """ + if not _is_postgres_url(url): + return url, {} + + parsed = urlparse(url) + qp = parse_qs(parsed.query, keep_blank_values=True) + + # Prefer sslmode (libpq canonical) over the bare ``ssl`` key. + sslmode_val = _pop_first(qp, 'sslmode') + ssl_val = _pop_first(qp, 'ssl') + ssl_mode = sslmode_val or ssl_val + + ssl_dict: dict[str, str] = {} + if ssl_mode: + ssl_dict['sslmode'] = ssl_mode + for key in ('sslrootcert', 'sslcert', 'sslkey', 'sslcrl'): + val = _pop_first(qp, key) + if val: + ssl_dict[key] = val + + if not ssl_dict: + return url, ssl_dict + + cleaned_query = urlencode(qp, doseq=True) + return urlunparse(parsed._replace(query=cleaned_query)), ssl_dict + + +def reattach_ssl_params_to_url(url_without_ssl: str, ssl_dict: dict[str, str]) -> str: + """Re-append SSL query-string parameters to a cleaned PostgreSQL URL. + + Used for psycopg2/libpq consumers that expect ``sslmode`` and the + certificate-file keys in the connection string. + """ + if not ssl_dict: + return url_without_ssl + + parts = [f'{k}={v}' for k, v in ssl_dict.items() if v] + if not parts: + return url_without_ssl + + sep = '&' if '?' in url_without_ssl else '?' + return f'{url_without_ssl}{sep}{"&".join(parts)}' + + +# Backwards-compatible aliases for external callers. +extract_ssl_mode_from_url = extract_ssl_params_from_url +reattach_ssl_mode_to_url = reattach_ssl_params_to_url + + +class JSONField(types.TypeDecorator): # TEXT-backed JSON storage + """Store arbitrary Python objects as JSON-encoded TEXT. + + Used instead of native JSON columns for portability across SQLite and + PostgreSQL. Values are serialized with ``json.dumps`` on write and + deserialized with ``json.loads`` on read. + """ + + impl = types.UnicodeText cache_ok = True - - def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any: - return json.dumps(value) - - def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any: - if value is not None: - return json.loads(value) - - def copy(self, **kw: Any) -> Self: - return JSONField(self.impl.length) - - def db_value(self, value): - return json.dumps(value) - - def python_value(self, value): - if value is not None: - return json.loads(value) + def process_bind_param(self, value: _T | None, dialect: Dialect) -> Any: + return json.dumps(value) if value is not None else None + def process_result_value(self, value: _T | None, dialect: Dialect) -> Any: + return json.loads(value) if value is not None else None + def copy(self, **kwargs: Any) -> Self: + return JSONField(length=self.impl.length) -# Workaround to handle the peewee migration -# This is required to ensure the peewee migration is handled before the alembic migration -def handle_peewee_migration(DATABASE_URL): - # db = None - try: - # Replace the postgresql:// with postgres:// to handle the peewee migration - db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://")) - migrate_dir = OPEN_WEBUI_DIR / "internal" / "migrations" - router = Router(db, logger=log, migrate_dir=migrate_dir) - router.run() - db.close() +# Normalize SSL params from the URL once; the sync engine needs them +# reattached in canonical libpq form for psycopg2. +_url_without_ssl, _ssl_dict = extract_ssl_params_from_url(DATABASE_URL) - except Exception as e: - log.error(f"Failed to initialize the database connection: {e}") - log.warning( - "Hint: If your database password contains special characters, you may need to URL-encode it." +# For psycopg2 (sync engine), re-append sslmode + cert-file params. +SQLALCHEMY_DATABASE_URL = reattach_ssl_params_to_url(_url_without_ssl, _ssl_dict) if _ssl_dict else DATABASE_URL + + +def _make_async_url(url: str) -> str: + """Convert a sync database URL to its async driver equivalent. + + The async engine uses psycopg (v3) which speaks libpq natively, + so all standard connection-string parameters (``sslmode``, + ``options``, ``target_session_attrs``, etc.) are passed through + without any translation. + """ + if url.startswith('sqlite+sqlcipher://'): + raise ValueError( + 'sqlite+sqlcipher:// URLs are not supported with async engine. ' + 'Use standard sqlite:// or postgresql:// instead.' ) - raise - finally: - # Properly closing the database connection - if db and not db.is_closed(): - db.close() - - # Assert if db connection has been closed - assert db.is_closed(), "Database connection is still open." + if url.startswith('sqlite:///') or url.startswith('sqlite://'): + return url.replace('sqlite://', 'sqlite+aiosqlite://', 1) + # psycopg v3 — auto-selects async mode with create_async_engine + if url.startswith('postgresql+psycopg2://'): + return url.replace('postgresql+psycopg2://', 'postgresql+psycopg://', 1) + if url.startswith('postgresql://'): + return url.replace('postgresql://', 'postgresql+psycopg://', 1) + if url.startswith('postgres://'): + return url.replace('postgres://', 'postgresql+psycopg://', 1) + # For other dialects, return as-is and let SQLAlchemy handle it + return url -if ENABLE_DB_MIGRATIONS: - handle_peewee_migration(DATABASE_URL) - - -SQLALCHEMY_DATABASE_URL = DATABASE_URL +# ============================================================ +# SYNC ENGINE (used only for: startup migrations, config loading, +# Alembic, peewee migration, health checks) +# ============================================================ # Handle SQLCipher URLs -if SQLALCHEMY_DATABASE_URL.startswith("sqlite+sqlcipher://"): - database_password = os.environ.get("DATABASE_PASSWORD") - if not database_password or database_password.strip() == "": - raise ValueError( - "DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs" - ) +if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'): + database_password = os.environ.get('DATABASE_PASSWORD') + if not database_password or database_password.strip() == '': + raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs') # Extract database path from SQLCipher URL - db_path = SQLALCHEMY_DATABASE_URL.replace("sqlite+sqlcipher://", "") + db_path = SQLALCHEMY_DATABASE_URL.replace('sqlite+sqlcipher://', '') # Create a custom creator function that uses sqlcipher3 def create_sqlcipher_connection(): @@ -102,28 +191,63 @@ if SQLALCHEMY_DATABASE_URL.startswith("sqlite+sqlcipher://"): conn.execute(f"PRAGMA key = '{database_password}'") return conn - engine = create_engine( - "sqlite://", # Dummy URL since we're using creator - creator=create_sqlcipher_connection, - echo=False, - ) + # The dummy "sqlite://" URL would cause SQLAlchemy to auto-select + # SingletonThreadPool, which non-deterministically closes in-use + # connections when thread count exceeds pool_size, leading to segfaults + # in the native sqlcipher3 C library. Use NullPool by default for safety, + # or QueuePool if DATABASE_POOL_SIZE is explicitly configured. + if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0: + engine = create_engine( + 'sqlite://', + creator=create_sqlcipher_connection, + pool_size=DATABASE_POOL_SIZE, + max_overflow=DATABASE_POOL_MAX_OVERFLOW, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + poolclass=QueuePool, + echo=False, + ) + else: + engine = create_engine( + 'sqlite://', + creator=create_sqlcipher_connection, + poolclass=NullPool, + echo=False, + ) - log.info("Connected to encrypted SQLite database using SQLCipher") + log.info('Connected to encrypted SQLite database using SQLCipher') -elif "sqlite" in SQLALCHEMY_DATABASE_URL: - engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} - ) +elif 'sqlite' in SQLALCHEMY_DATABASE_URL: + engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False}) - def on_connect(dbapi_connection, connection_record): + def _apply_sqlite_pragmas(dbapi_connection): + """Apply all configured SQLite PRAGMAs to a raw DBAPI connection.""" cursor = dbapi_connection.cursor() if DATABASE_ENABLE_SQLITE_WAL: - cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute('PRAGMA journal_mode=WAL') else: - cursor.execute("PRAGMA journal_mode=DELETE") + cursor.execute('PRAGMA journal_mode=DELETE') + + # Each PRAGMA is skipped when its env var is empty, allowing opt-out. + if DATABASE_SQLITE_PRAGMA_SYNCHRONOUS: + cursor.execute(f'PRAGMA synchronous={DATABASE_SQLITE_PRAGMA_SYNCHRONOUS}') + if DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT: + cursor.execute(f'PRAGMA busy_timeout={DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT}') + if DATABASE_SQLITE_PRAGMA_CACHE_SIZE: + cursor.execute(f'PRAGMA cache_size={DATABASE_SQLITE_PRAGMA_CACHE_SIZE}') + if DATABASE_SQLITE_PRAGMA_TEMP_STORE: + cursor.execute(f'PRAGMA temp_store={DATABASE_SQLITE_PRAGMA_TEMP_STORE}') + if DATABASE_SQLITE_PRAGMA_MMAP_SIZE: + cursor.execute(f'PRAGMA mmap_size={DATABASE_SQLITE_PRAGMA_MMAP_SIZE}') + if DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT: + cursor.execute(f'PRAGMA journal_size_limit={DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT}') cursor.close() - event.listen(engine, "connect", on_connect) + def on_connect(dbapi_connection, connection_record): + _apply_sqlite_pragmas(dbapi_connection) + + event.listen(engine, 'connect', on_connect) else: if isinstance(DATABASE_POOL_SIZE, int): if DATABASE_POOL_SIZE > 0: @@ -137,22 +261,20 @@ else: poolclass=QueuePool, ) else: - engine = create_engine( - SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool - ) + engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool) else: engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) -SessionLocal = sessionmaker( - autocommit=False, autoflush=False, bind=engine, expire_on_commit=False -) +# Sync session — used ONLY for startup config loading (config.py runs at import time) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) metadata_obj = MetaData(schema=DATABASE_SCHEMA) Base = declarative_base(metadata=metadata_obj) ScopedSession = scoped_session(SessionLocal) def get_session(): + """Sync session generator — used ONLY for startup/config operations.""" db = SessionLocal() try: yield db @@ -163,10 +285,96 @@ def get_session(): get_db = contextmanager(get_session) -@contextmanager -def get_db_context(db: Optional[Session] = None): - if isinstance(db, Session) and DATABASE_ENABLE_SESSION_SHARING: +# ============================================================ +# ASYNC ENGINE (used for ALL runtime database operations) +# ============================================================ + +# psycopg (v3) speaks libpq natively — the full DATABASE_URL is passed +# through as-is. SSL params, ``options``, ``target_session_attrs``, etc. +# all work without any stripping or translation. +ASYNC_SQLALCHEMY_DATABASE_URL = _make_async_url(SQLALCHEMY_DATABASE_URL) + +# psycopg v3 cannot run in async mode under Windows' default +# ProactorEventLoop — switch to SelectorEventLoop before creating +# the async engine. This runs at import time, which is early enough +# to cover every entry point (workers, reload, direct invocations). +if sys.platform == 'win32' and _is_postgres_url(DATABASE_URL): + import asyncio + + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL: + # Generous default — async coroutines + no session sharing = high connection demand. + _sqlite_pool_size = DATABASE_POOL_SIZE if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0 else 512 + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + connect_args={'check_same_thread': False}, + pool_size=_sqlite_pool_size, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + ) + + @event.listens_for(async_engine.sync_engine, 'connect') + def _set_sqlite_pragmas(dbapi_connection, connection_record): + _apply_sqlite_pragmas(dbapi_connection) +else: + if isinstance(DATABASE_POOL_SIZE, int): + if DATABASE_POOL_SIZE > 0: + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + pool_size=DATABASE_POOL_SIZE, + max_overflow=DATABASE_POOL_MAX_OVERFLOW, + pool_timeout=DATABASE_POOL_TIMEOUT, + pool_recycle=DATABASE_POOL_RECYCLE, + pool_pre_ping=True, + ) + else: + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + pool_pre_ping=True, + poolclass=NullPool, + ) + else: + async_engine = create_async_engine( + ASYNC_SQLALCHEMY_DATABASE_URL, + pool_pre_ping=True, + ) + + +AsyncSessionLocal = async_sessionmaker( + bind=async_engine, + class_=AsyncSession, + autocommit=False, + autoflush=False, + expire_on_commit=False, +) + + +async def get_async_session(): + """Async session generator for FastAPI Depends().""" + async with AsyncSessionLocal() as db: + try: + yield db + finally: + await db.close() + + +@asynccontextmanager +async def get_async_db(): + """Async context manager for use outside of FastAPI dependency injection.""" + async with AsyncSessionLocal() as db: + try: + yield db + finally: + await db.close() + + +@asynccontextmanager +async def get_async_db_context(db: AsyncSession | None = None): + """Async context manager that reuses an existing session if provided and session sharing is enabled.""" + if isinstance(db, AsyncSession) and DATABASE_ENABLE_SESSION_SHARING: yield db else: - with get_db() as session: + async with get_async_db() as session: yield session diff --git a/backend/open_webui/internal/migrations/001_initial_schema.py b/backend/open_webui/internal/migrations/001_initial_schema.py deleted file mode 100644 index 93f278f15b..0000000000 --- a/backend/open_webui/internal/migrations/001_initial_schema.py +++ /dev/null @@ -1,254 +0,0 @@ -"""Peewee migrations -- 001_initial_schema.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - # We perform different migrations for SQLite and other databases - # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite - # will require per-database SQL queries. - # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base - # schema instead of trying to migrate from an older schema. - if isinstance(database, pw.SqliteDatabase): - migrate_sqlite(migrator, database, fake=fake) - else: - migrate_external(migrator, database, fake=fake) - - -def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): - @migrator.create_model - class Auth(pw.Model): - id = pw.CharField(max_length=255, unique=True) - email = pw.CharField(max_length=255) - password = pw.CharField(max_length=255) - active = pw.BooleanField() - - class Meta: - table_name = "auth" - - @migrator.create_model - class Chat(pw.Model): - id = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - title = pw.CharField() - chat = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "chat" - - @migrator.create_model - class ChatIdTag(pw.Model): - id = pw.CharField(max_length=255, unique=True) - tag_name = pw.CharField(max_length=255) - chat_id = pw.CharField(max_length=255) - user_id = pw.CharField(max_length=255) - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "chatidtag" - - @migrator.create_model - class Document(pw.Model): - id = pw.AutoField() - collection_name = pw.CharField(max_length=255, unique=True) - name = pw.CharField(max_length=255, unique=True) - title = pw.CharField() - filename = pw.CharField() - content = pw.TextField(null=True) - user_id = pw.CharField(max_length=255) - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "document" - - @migrator.create_model - class Modelfile(pw.Model): - id = pw.AutoField() - tag_name = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - modelfile = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "modelfile" - - @migrator.create_model - class Prompt(pw.Model): - id = pw.AutoField() - command = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - title = pw.CharField() - content = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "prompt" - - @migrator.create_model - class Tag(pw.Model): - id = pw.CharField(max_length=255, unique=True) - name = pw.CharField(max_length=255) - user_id = pw.CharField(max_length=255) - data = pw.TextField(null=True) - - class Meta: - table_name = "tag" - - @migrator.create_model - class User(pw.Model): - id = pw.CharField(max_length=255, unique=True) - name = pw.CharField(max_length=255) - email = pw.CharField(max_length=255) - role = pw.CharField(max_length=255) - profile_image_url = pw.CharField(max_length=255) - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "user" - - -def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False): - @migrator.create_model - class Auth(pw.Model): - id = pw.CharField(max_length=255, unique=True) - email = pw.CharField(max_length=255) - password = pw.TextField() - active = pw.BooleanField() - - class Meta: - table_name = "auth" - - @migrator.create_model - class Chat(pw.Model): - id = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - title = pw.TextField() - chat = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "chat" - - @migrator.create_model - class ChatIdTag(pw.Model): - id = pw.CharField(max_length=255, unique=True) - tag_name = pw.CharField(max_length=255) - chat_id = pw.CharField(max_length=255) - user_id = pw.CharField(max_length=255) - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "chatidtag" - - @migrator.create_model - class Document(pw.Model): - id = pw.AutoField() - collection_name = pw.CharField(max_length=255, unique=True) - name = pw.CharField(max_length=255, unique=True) - title = pw.TextField() - filename = pw.TextField() - content = pw.TextField(null=True) - user_id = pw.CharField(max_length=255) - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "document" - - @migrator.create_model - class Modelfile(pw.Model): - id = pw.AutoField() - tag_name = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - modelfile = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "modelfile" - - @migrator.create_model - class Prompt(pw.Model): - id = pw.AutoField() - command = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - title = pw.TextField() - content = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "prompt" - - @migrator.create_model - class Tag(pw.Model): - id = pw.CharField(max_length=255, unique=True) - name = pw.CharField(max_length=255) - user_id = pw.CharField(max_length=255) - data = pw.TextField(null=True) - - class Meta: - table_name = "tag" - - @migrator.create_model - class User(pw.Model): - id = pw.CharField(max_length=255, unique=True) - name = pw.CharField(max_length=255) - email = pw.CharField(max_length=255) - role = pw.CharField(max_length=255) - profile_image_url = pw.TextField() - timestamp = pw.BigIntegerField() - - class Meta: - table_name = "user" - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_model("user") - - migrator.remove_model("tag") - - migrator.remove_model("prompt") - - migrator.remove_model("modelfile") - - migrator.remove_model("document") - - migrator.remove_model("chatidtag") - - migrator.remove_model("chat") - - migrator.remove_model("auth") diff --git a/backend/open_webui/internal/migrations/002_add_local_sharing.py b/backend/open_webui/internal/migrations/002_add_local_sharing.py deleted file mode 100644 index e93501aeec..0000000000 --- a/backend/open_webui/internal/migrations/002_add_local_sharing.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - migrator.add_fields( - "chat", share_id=pw.CharField(max_length=255, null=True, unique=True) - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_fields("chat", "share_id") diff --git a/backend/open_webui/internal/migrations/003_add_auth_api_key.py b/backend/open_webui/internal/migrations/003_add_auth_api_key.py deleted file mode 100644 index 07144f3aca..0000000000 --- a/backend/open_webui/internal/migrations/003_add_auth_api_key.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - migrator.add_fields( - "user", api_key=pw.CharField(max_length=255, null=True, unique=True) - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_fields("user", "api_key") diff --git a/backend/open_webui/internal/migrations/004_add_archived.py b/backend/open_webui/internal/migrations/004_add_archived.py deleted file mode 100644 index d01c06b4e6..0000000000 --- a/backend/open_webui/internal/migrations/004_add_archived.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - migrator.add_fields("chat", archived=pw.BooleanField(default=False)) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_fields("chat", "archived") diff --git a/backend/open_webui/internal/migrations/005_add_updated_at.py b/backend/open_webui/internal/migrations/005_add_updated_at.py deleted file mode 100644 index 950866ef02..0000000000 --- a/backend/open_webui/internal/migrations/005_add_updated_at.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - if isinstance(database, pw.SqliteDatabase): - migrate_sqlite(migrator, database, fake=fake) - else: - migrate_external(migrator, database, fake=fake) - - -def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): - # Adding fields created_at and updated_at to the 'chat' table - migrator.add_fields( - "chat", - created_at=pw.DateTimeField(null=True), # Allow null for transition - updated_at=pw.DateTimeField(null=True), # Allow null for transition - ) - - # Populate the new fields from an existing 'timestamp' field - migrator.sql( - "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL" - ) - - # Now that the data has been copied, remove the original 'timestamp' field - migrator.remove_fields("chat", "timestamp") - - # Update the fields to be not null now that they are populated - migrator.change_fields( - "chat", - created_at=pw.DateTimeField(null=False), - updated_at=pw.DateTimeField(null=False), - ) - - -def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False): - # Adding fields created_at and updated_at to the 'chat' table - migrator.add_fields( - "chat", - created_at=pw.BigIntegerField(null=True), # Allow null for transition - updated_at=pw.BigIntegerField(null=True), # Allow null for transition - ) - - # Populate the new fields from an existing 'timestamp' field - migrator.sql( - "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL" - ) - - # Now that the data has been copied, remove the original 'timestamp' field - migrator.remove_fields("chat", "timestamp") - - # Update the fields to be not null now that they are populated - migrator.change_fields( - "chat", - created_at=pw.BigIntegerField(null=False), - updated_at=pw.BigIntegerField(null=False), - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - if isinstance(database, pw.SqliteDatabase): - rollback_sqlite(migrator, database, fake=fake) - else: - rollback_external(migrator, database, fake=fake) - - -def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): - # Recreate the timestamp field initially allowing null values for safe transition - migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True)) - - # Copy the earliest created_at date back into the new timestamp field - # This assumes created_at was originally a copy of timestamp - migrator.sql("UPDATE chat SET timestamp = created_at") - - # Remove the created_at and updated_at fields - migrator.remove_fields("chat", "created_at", "updated_at") - - # Finally, alter the timestamp field to not allow nulls if that was the original setting - migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False)) - - -def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False): - # Recreate the timestamp field initially allowing null values for safe transition - migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True)) - - # Copy the earliest created_at date back into the new timestamp field - # This assumes created_at was originally a copy of timestamp - migrator.sql("UPDATE chat SET timestamp = created_at") - - # Remove the created_at and updated_at fields - migrator.remove_fields("chat", "created_at", "updated_at") - - # Finally, alter the timestamp field to not allow nulls if that was the original setting - migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False)) diff --git a/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py b/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py deleted file mode 100644 index caca14d323..0000000000 --- a/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - # Alter the tables with timestamps - migrator.change_fields( - "chatidtag", - timestamp=pw.BigIntegerField(), - ) - migrator.change_fields( - "document", - timestamp=pw.BigIntegerField(), - ) - migrator.change_fields( - "modelfile", - timestamp=pw.BigIntegerField(), - ) - migrator.change_fields( - "prompt", - timestamp=pw.BigIntegerField(), - ) - migrator.change_fields( - "user", - timestamp=pw.BigIntegerField(), - ) - # Alter the tables with varchar to text where necessary - migrator.change_fields( - "auth", - password=pw.TextField(), - ) - migrator.change_fields( - "chat", - title=pw.TextField(), - ) - migrator.change_fields( - "document", - title=pw.TextField(), - filename=pw.TextField(), - ) - migrator.change_fields( - "prompt", - title=pw.TextField(), - ) - migrator.change_fields( - "user", - profile_image_url=pw.TextField(), - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - if isinstance(database, pw.SqliteDatabase): - # Alter the tables with timestamps - migrator.change_fields( - "chatidtag", - timestamp=pw.DateField(), - ) - migrator.change_fields( - "document", - timestamp=pw.DateField(), - ) - migrator.change_fields( - "modelfile", - timestamp=pw.DateField(), - ) - migrator.change_fields( - "prompt", - timestamp=pw.DateField(), - ) - migrator.change_fields( - "user", - timestamp=pw.DateField(), - ) - migrator.change_fields( - "auth", - password=pw.CharField(max_length=255), - ) - migrator.change_fields( - "chat", - title=pw.CharField(), - ) - migrator.change_fields( - "document", - title=pw.CharField(), - filename=pw.CharField(), - ) - migrator.change_fields( - "prompt", - title=pw.CharField(), - ) - migrator.change_fields( - "user", - profile_image_url=pw.CharField(), - ) diff --git a/backend/open_webui/internal/migrations/007_add_user_last_active_at.py b/backend/open_webui/internal/migrations/007_add_user_last_active_at.py deleted file mode 100644 index dd176ba73e..0000000000 --- a/backend/open_webui/internal/migrations/007_add_user_last_active_at.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - # Adding fields created_at and updated_at to the 'user' table - migrator.add_fields( - "user", - created_at=pw.BigIntegerField(null=True), # Allow null for transition - updated_at=pw.BigIntegerField(null=True), # Allow null for transition - last_active_at=pw.BigIntegerField(null=True), # Allow null for transition - ) - - # Populate the new fields from an existing 'timestamp' field - migrator.sql( - 'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL' - ) - - # Now that the data has been copied, remove the original 'timestamp' field - migrator.remove_fields("user", "timestamp") - - # Update the fields to be not null now that they are populated - migrator.change_fields( - "user", - created_at=pw.BigIntegerField(null=False), - updated_at=pw.BigIntegerField(null=False), - last_active_at=pw.BigIntegerField(null=False), - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - # Recreate the timestamp field initially allowing null values for safe transition - migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True)) - - # Copy the earliest created_at date back into the new timestamp field - # This assumes created_at was originally a copy of timestamp - migrator.sql('UPDATE "user" SET timestamp = created_at') - - # Remove the created_at and updated_at fields - migrator.remove_fields("user", "created_at", "updated_at", "last_active_at") - - # Finally, alter the timestamp field to not allow nulls if that was the original setting - migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False)) diff --git a/backend/open_webui/internal/migrations/008_add_memory.py b/backend/open_webui/internal/migrations/008_add_memory.py deleted file mode 100644 index 9307aa4d5c..0000000000 --- a/backend/open_webui/internal/migrations/008_add_memory.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - @migrator.create_model - class Memory(pw.Model): - id = pw.CharField(max_length=255, unique=True) - user_id = pw.CharField(max_length=255) - content = pw.TextField(null=False) - updated_at = pw.BigIntegerField(null=False) - created_at = pw.BigIntegerField(null=False) - - class Meta: - table_name = "memory" - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_model("memory") diff --git a/backend/open_webui/internal/migrations/009_add_models.py b/backend/open_webui/internal/migrations/009_add_models.py deleted file mode 100644 index 548ec7cdca..0000000000 --- a/backend/open_webui/internal/migrations/009_add_models.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Peewee migrations -- 009_add_models.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - @migrator.create_model - class Model(pw.Model): - id = pw.TextField(unique=True) - user_id = pw.TextField() - base_model_id = pw.TextField(null=True) - - name = pw.TextField() - - meta = pw.TextField() - params = pw.TextField() - - created_at = pw.BigIntegerField(null=False) - updated_at = pw.BigIntegerField(null=False) - - class Meta: - table_name = "model" - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_model("model") diff --git a/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py b/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py deleted file mode 100644 index 322ddd44ec..0000000000 --- a/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Peewee migrations -- 009_add_models.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator -import json - -from open_webui.utils.misc import parse_ollama_modelfile - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - # Fetch data from 'modelfile' table and insert into 'model' table - migrate_modelfile_to_model(migrator, database) - # Drop the 'modelfile' table - migrator.remove_model("modelfile") - - -def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database): - ModelFile = migrator.orm["modelfile"] - Model = migrator.orm["model"] - - modelfiles = ModelFile.select() - - for modelfile in modelfiles: - # Extract and transform data in Python - - modelfile.modelfile = json.loads(modelfile.modelfile) - meta = json.dumps( - { - "description": modelfile.modelfile.get("desc"), - "profile_image_url": modelfile.modelfile.get("imageUrl"), - "ollama": {"modelfile": modelfile.modelfile.get("content")}, - "suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"), - "categories": modelfile.modelfile.get("categories"), - "user": {**modelfile.modelfile.get("user", {}), "community": True}, - } - ) - - info = parse_ollama_modelfile(modelfile.modelfile.get("content")) - - # Insert the processed data into the 'model' table - Model.create( - id=f"ollama-{modelfile.tag_name}", - user_id=modelfile.user_id, - base_model_id=info.get("base_model_id"), - name=modelfile.modelfile.get("title"), - meta=meta, - params=json.dumps(info.get("params", {})), - created_at=modelfile.timestamp, - updated_at=modelfile.timestamp, - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - recreate_modelfile_table(migrator, database) - move_data_back_to_modelfile(migrator, database) - migrator.remove_model("model") - - -def recreate_modelfile_table(migrator: Migrator, database: pw.Database): - query = """ - CREATE TABLE IF NOT EXISTS modelfile ( - user_id TEXT, - tag_name TEXT, - modelfile JSON, - timestamp BIGINT - ) - """ - migrator.sql(query) - - -def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database): - Model = migrator.orm["model"] - Modelfile = migrator.orm["modelfile"] - - models = Model.select() - - for model in models: - # Extract and transform data in Python - meta = json.loads(model.meta) - - modelfile_data = { - "title": model.name, - "desc": meta.get("description"), - "imageUrl": meta.get("profile_image_url"), - "content": meta.get("ollama", {}).get("modelfile"), - "suggestionPrompts": meta.get("suggestion_prompts"), - "categories": meta.get("categories"), - "user": {k: v for k, v in meta.get("user", {}).items() if k != "community"}, - } - - # Insert the processed data back into the 'modelfile' table - Modelfile.create( - user_id=model.user_id, - tag_name=model.id, - modelfile=modelfile_data, - timestamp=model.created_at, - ) diff --git a/backend/open_webui/internal/migrations/011_add_user_settings.py b/backend/open_webui/internal/migrations/011_add_user_settings.py deleted file mode 100644 index a1620dcada..0000000000 --- a/backend/open_webui/internal/migrations/011_add_user_settings.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - # Adding fields settings to the 'user' table - migrator.add_fields("user", settings=pw.TextField(null=True)) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - # Remove the settings field - migrator.remove_fields("user", "settings") diff --git a/backend/open_webui/internal/migrations/012_add_tools.py b/backend/open_webui/internal/migrations/012_add_tools.py deleted file mode 100644 index 4a68eea552..0000000000 --- a/backend/open_webui/internal/migrations/012_add_tools.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Peewee migrations -- 009_add_models.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - @migrator.create_model - class Tool(pw.Model): - id = pw.TextField(unique=True) - user_id = pw.TextField() - - name = pw.TextField() - content = pw.TextField() - specs = pw.TextField() - - meta = pw.TextField() - - created_at = pw.BigIntegerField(null=False) - updated_at = pw.BigIntegerField(null=False) - - class Meta: - table_name = "tool" - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_model("tool") diff --git a/backend/open_webui/internal/migrations/013_add_user_info.py b/backend/open_webui/internal/migrations/013_add_user_info.py deleted file mode 100644 index 0f68669cca..0000000000 --- a/backend/open_webui/internal/migrations/013_add_user_info.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Peewee migrations -- 002_add_local_sharing.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - # Adding fields info to the 'user' table - migrator.add_fields("user", info=pw.TextField(null=True)) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - # Remove the settings field - migrator.remove_fields("user", "info") diff --git a/backend/open_webui/internal/migrations/014_add_files.py b/backend/open_webui/internal/migrations/014_add_files.py deleted file mode 100644 index 5e1acf0ad8..0000000000 --- a/backend/open_webui/internal/migrations/014_add_files.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Peewee migrations -- 009_add_models.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - @migrator.create_model - class File(pw.Model): - id = pw.TextField(unique=True) - user_id = pw.TextField() - filename = pw.TextField() - meta = pw.TextField() - created_at = pw.BigIntegerField(null=False) - - class Meta: - table_name = "file" - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_model("file") diff --git a/backend/open_webui/internal/migrations/015_add_functions.py b/backend/open_webui/internal/migrations/015_add_functions.py deleted file mode 100644 index 8316a9333b..0000000000 --- a/backend/open_webui/internal/migrations/015_add_functions.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Peewee migrations -- 009_add_models.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - @migrator.create_model - class Function(pw.Model): - id = pw.TextField(unique=True) - user_id = pw.TextField() - - name = pw.TextField() - type = pw.TextField() - - content = pw.TextField() - meta = pw.TextField() - - created_at = pw.BigIntegerField(null=False) - updated_at = pw.BigIntegerField(null=False) - - class Meta: - table_name = "function" - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_model("function") diff --git a/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py b/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py deleted file mode 100644 index e3af521b7e..0000000000 --- a/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Peewee migrations -- 009_add_models.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - migrator.add_fields("tool", valves=pw.TextField(null=True)) - migrator.add_fields("function", valves=pw.TextField(null=True)) - migrator.add_fields("function", is_active=pw.BooleanField(default=False)) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_fields("tool", "valves") - migrator.remove_fields("function", "valves") - migrator.remove_fields("function", "is_active") diff --git a/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py b/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py deleted file mode 100644 index eaa3fa5fe5..0000000000 --- a/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Peewee migrations -- 017_add_user_oauth_sub.py. -Some examples (model - class or model name):: - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - migrator.add_fields( - "user", - oauth_sub=pw.TextField(null=True, unique=True), - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_fields("user", "oauth_sub") diff --git a/backend/open_webui/internal/migrations/018_add_function_is_global.py b/backend/open_webui/internal/migrations/018_add_function_is_global.py deleted file mode 100644 index 04cdab7059..0000000000 --- a/backend/open_webui/internal/migrations/018_add_function_is_global.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Peewee migrations -- 017_add_user_oauth_sub.py. - -Some examples (model - class or model name):: - - > Model = migrator.orm['table_name'] # Return model in current state by name - > Model = migrator.ModelClass # Return model in current state by name - - > migrator.sql(sql) # Run custom SQL - > migrator.run(func, *args, **kwargs) # Run python function with the given args - > migrator.create_model(Model) # Create a model (could be used as decorator) - > migrator.remove_model(model, cascade=True) # Remove a model - > migrator.add_fields(model, **fields) # Add fields to a model - > migrator.change_fields(model, **fields) # Change fields - > migrator.remove_fields(model, *field_names, cascade=True) - > migrator.rename_field(model, old_field_name, new_field_name) - > migrator.rename_table(model, new_table_name) - > migrator.add_index(model, *col_names, unique=False) - > migrator.add_not_null(model, *field_names) - > migrator.add_default(model, field_name, default) - > migrator.add_constraint(model, name, sql) - > migrator.drop_index(model, *col_names) - > migrator.drop_not_null(model, *field_names) - > migrator.drop_constraints(model, *constraints) - -""" - -from contextlib import suppress - -import peewee as pw -from peewee_migrate import Migrator - - -with suppress(ImportError): - import playhouse.postgres_ext as pw_pext - - -def migrate(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your migrations here.""" - - migrator.add_fields( - "function", - is_global=pw.BooleanField(default=False), - ) - - -def rollback(migrator: Migrator, database: pw.Database, *, fake=False): - """Write your rollback migrations here.""" - - migrator.remove_fields("function", "is_global") diff --git a/backend/open_webui/internal/wrappers.py b/backend/open_webui/internal/wrappers.py deleted file mode 100644 index 80b1aab8ff..0000000000 --- a/backend/open_webui/internal/wrappers.py +++ /dev/null @@ -1,86 +0,0 @@ -import logging -import os -from contextvars import ContextVar - -from peewee import * -from peewee import InterfaceError as PeeWeeInterfaceError -from peewee import PostgresqlDatabase -from playhouse.db_url import connect, parse -from playhouse.shortcuts import ReconnectMixin - -log = logging.getLogger(__name__) - -db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} -db_state = ContextVar("db_state", default=db_state_default.copy()) - - -class PeeweeConnectionState(object): - def __init__(self, **kwargs): - super().__setattr__("_state", db_state) - super().__init__(**kwargs) - - def __setattr__(self, name, value): - self._state.get()[name] = value - - def __getattr__(self, name): - value = self._state.get()[name] - return value - - -class CustomReconnectMixin(ReconnectMixin): - reconnect_errors = ( - # psycopg2 - (OperationalError, "termin"), - (InterfaceError, "closed"), - # peewee - (PeeWeeInterfaceError, "closed"), - ) - - -class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase): - pass - - -def register_connection(db_url): - # Check if using SQLCipher protocol - if db_url.startswith("sqlite+sqlcipher://"): - database_password = os.environ.get("DATABASE_PASSWORD") - if not database_password or database_password.strip() == "": - raise ValueError( - "DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs" - ) - from playhouse.sqlcipher_ext import SqlCipherDatabase - - # Parse the database path from SQLCipher URL - # Convert sqlite+sqlcipher:///path/to/db.sqlite to /path/to/db.sqlite - db_path = db_url.replace("sqlite+sqlcipher://", "") - - # Use Peewee's native SqlCipherDatabase with encryption - db = SqlCipherDatabase(db_path, passphrase=database_password) - db.autoconnect = True - db.reuse_if_open = True - log.info("Connected to encrypted SQLite database using SQLCipher") - - else: - # Standard database connection (existing logic) - db = connect(db_url, unquote_user=True, unquote_password=True) - if isinstance(db, PostgresqlDatabase): - # Enable autoconnect for SQLite databases, managed by Peewee - db.autoconnect = True - db.reuse_if_open = True - log.info("Connected to PostgreSQL database") - - # Get the connection details - connection = parse(db_url, unquote_user=True, unquote_password=True) - - # Use our custom database class that supports reconnection - db = ReconnectingPostgresqlDatabase(**connection) - db.connect(reuse_if_open=True) - elif isinstance(db, SqliteDatabase): - # Enable autoconnect for SQLite databases, managed by Peewee - db.autoconnect = True - db.reuse_if_open = True - log.info("Connected to SQLite database") - else: - raise ValueError("Unsupported database connection") - return db diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index c1f45eadcd..c2de774931 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1,31 +1,26 @@ +from __future__ import annotations + import asyncio import inspect import json import logging import mimetypes import os +import random +import re import shutil import sys import time -import random -import re +from contextlib import asynccontextmanager +from typing import Optional +from urllib.parse import parse_qs, urlencode, urlparse from uuid import uuid4 - -from contextlib import asynccontextmanager -from urllib.parse import urlencode, parse_qs, urlparse -from pydantic import BaseModel -from sqlalchemy import text - -from typing import Optional -from aiocache import cached import aiohttp import anyio.to_thread -import requests -from redis import Redis - - +from aiocache import cached from fastapi import ( + BackgroundTasks, Depends, FastAPI, File, @@ -33,517 +28,564 @@ from fastapi import ( HTTPException, Request, UploadFile, - status, applications, - BackgroundTasks, + status, ) -from fastapi.openapi.docs import get_swagger_ui_html - from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles - -from starlette_compress import CompressMiddleware - +from pydantic import BaseModel +from redis import Redis +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.datastructures import Headers from starlette.exceptions import HTTPException as StarletteHTTPException -from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.sessions import SessionMiddleware from starlette.responses import Response, StreamingResponse -from starlette.datastructures import Headers - +from starlette_compress import CompressMiddleware +from starsessions import ( + SessionAutoloadMiddleware, +) from starsessions import ( SessionMiddleware as StarSessionsMiddleware, - SessionAutoloadMiddleware, ) from starsessions.stores.redis import RedisStore -from open_webui.utils import logger -from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware -from open_webui.utils.logger import start_logger -from open_webui.socket.main import ( - MODELS, - app as socket_app, - periodic_usage_pool_cleanup, - get_event_emitter, - get_models_in_use, -) -from open_webui.routers import ( - audio, - images, - ollama, - openai, - retrieval, - pipelines, - tasks, - auths, - channels, - chats, - notes, - folders, - configs, - groups, - files, - functions, - memories, - models, - knowledge, - prompts, - evaluations, - tools, - users, - utils, - scim, -) - -from open_webui.routers.retrieval import ( - get_embedding_function, - get_reranking_function, - get_ef, - get_rf, -) - - -from sqlalchemy.orm import Session -from open_webui.internal.db import ScopedSession, engine, get_session - -from open_webui.models.functions import Functions -from open_webui.models.models import Models -from open_webui.models.users import UserModel, Users -from open_webui.models.chats import Chats - from open_webui.config import ( - # Ollama - ENABLE_OLLAMA_API, - OLLAMA_BASE_URLS, - OLLAMA_API_CONFIGS, - # OpenAI - ENABLE_OPENAI_API, - OPENAI_API_BASE_URLS, - OPENAI_API_KEYS, - OPENAI_API_CONFIGS, - # Direct Connections - ENABLE_DIRECT_CONNECTIONS, - # Model list - ENABLE_BASE_MODELS_CACHE, - # Thread pool size for FastAPI/AnyIO - THREAD_POOL_SIZE, - # Tool Server Configs - TOOL_SERVER_CONNECTIONS, - # Code Execution - ENABLE_CODE_EXECUTION, - CODE_EXECUTION_ENGINE, - CODE_EXECUTION_JUPYTER_URL, - CODE_EXECUTION_JUPYTER_AUTH, - CODE_EXECUTION_JUPYTER_AUTH_TOKEN, - CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, - CODE_EXECUTION_JUPYTER_TIMEOUT, - ENABLE_CODE_INTERPRETER, - CODE_INTERPRETER_ENGINE, - CODE_INTERPRETER_PROMPT_TEMPLATE, - CODE_INTERPRETER_JUPYTER_URL, - CODE_INTERPRETER_JUPYTER_AUTH, - CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, - CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, - CODE_INTERPRETER_JUPYTER_TIMEOUT, - ENABLE_MEMORIES, + ADMIN_EMAIL, + API_KEYS_ALLOWED_ENDPOINTS, + AUDIO_STT_ALLOWED_EXTENSIONS, + AUDIO_STT_AZURE_API_KEY, + AUDIO_STT_AZURE_BASE_URL, + AUDIO_STT_AZURE_LOCALES, + AUDIO_STT_AZURE_MAX_SPEAKERS, + AUDIO_STT_AZURE_REGION, + # Audio + AUDIO_STT_ENGINE, + AUDIO_STT_MISTRAL_API_BASE_URL, + AUDIO_STT_MISTRAL_API_KEY, + AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + AUDIO_STT_MODEL, + AUDIO_STT_OPENAI_API_BASE_URL, + AUDIO_STT_OPENAI_API_KEY, + AUDIO_STT_SUPPORTED_CONTENT_TYPES, + AUDIO_TTS_API_KEY, + AUDIO_TTS_AZURE_SPEECH_BASE_URL, + AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, + AUDIO_TTS_AZURE_SPEECH_REGION, + AUDIO_TTS_ENGINE, + AUDIO_TTS_MISTRAL_API_BASE_URL, + AUDIO_TTS_MISTRAL_API_KEY, + AUDIO_TTS_MODEL, + AUDIO_TTS_OPENAI_API_BASE_URL, + AUDIO_TTS_OPENAI_API_KEY, + AUDIO_TTS_OPENAI_PARAMS, + AUDIO_TTS_SPLIT_ON, + AUDIO_TTS_VOICE, + AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, # Image AUTOMATIC1111_API_AUTH, AUTOMATIC1111_BASE_URL, AUTOMATIC1111_PARAMS, - COMFYUI_BASE_URL, + AUTOMATION_MAX_COUNT, + AUTOMATION_MIN_INTERVAL, + BING_SEARCH_V7_ENDPOINT, + BING_SEARCH_V7_SUBSCRIPTION_KEY, + BOCHA_SEARCH_API_KEY, + BRAVE_SEARCH_API_KEY, + BRAVE_SEARCH_CONTEXT_TOKENS, + BYPASS_ADMIN_ACCESS_CONTROL, + BYPASS_EMBEDDING_AND_RETRIEVAL, + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + BYPASS_WEB_SEARCH_WEB_LOADER, + CACHE_DIR, + CHUNK_MIN_SIZE_TARGET, + CHUNK_OVERLAP, + CHUNK_SIZE, + CODE_EXECUTION_ENGINE, + CODE_EXECUTION_JUPYTER_AUTH, + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + CODE_EXECUTION_JUPYTER_TIMEOUT, + CODE_EXECUTION_JUPYTER_URL, + CODE_INTERPRETER_ENGINE, + CODE_INTERPRETER_JUPYTER_AUTH, + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + CODE_INTERPRETER_JUPYTER_TIMEOUT, + CODE_INTERPRETER_JUPYTER_URL, + CODE_INTERPRETER_PROMPT_TEMPLATE, COMFYUI_API_KEY, + COMFYUI_BASE_URL, COMFYUI_WORKFLOW, COMFYUI_WORKFLOW_NODES, - ENABLE_IMAGE_GENERATION, - ENABLE_IMAGE_PROMPT_GENERATION, - IMAGE_GENERATION_ENGINE, - IMAGE_GENERATION_MODEL, - IMAGE_SIZE, - IMAGE_STEPS, - IMAGES_OPENAI_API_BASE_URL, - IMAGES_OPENAI_API_VERSION, - IMAGES_OPENAI_API_KEY, - IMAGES_OPENAI_API_PARAMS, - IMAGES_GEMINI_API_BASE_URL, - IMAGES_GEMINI_API_KEY, - IMAGES_GEMINI_ENDPOINT_METHOD, - ENABLE_IMAGE_EDIT, - IMAGE_EDIT_ENGINE, - IMAGE_EDIT_MODEL, - IMAGE_EDIT_SIZE, - IMAGES_EDIT_OPENAI_API_BASE_URL, - IMAGES_EDIT_OPENAI_API_KEY, - IMAGES_EDIT_OPENAI_API_VERSION, - IMAGES_EDIT_GEMINI_API_BASE_URL, - IMAGES_EDIT_GEMINI_API_KEY, - IMAGES_EDIT_COMFYUI_BASE_URL, - IMAGES_EDIT_COMFYUI_API_KEY, - IMAGES_EDIT_COMFYUI_WORKFLOW, - IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, - # Audio - AUDIO_STT_ENGINE, - AUDIO_STT_MODEL, - AUDIO_STT_SUPPORTED_CONTENT_TYPES, - AUDIO_STT_OPENAI_API_BASE_URL, - AUDIO_STT_OPENAI_API_KEY, - AUDIO_STT_AZURE_API_KEY, - AUDIO_STT_AZURE_REGION, - AUDIO_STT_AZURE_LOCALES, - AUDIO_STT_AZURE_BASE_URL, - AUDIO_STT_AZURE_MAX_SPEAKERS, - AUDIO_STT_MISTRAL_API_KEY, - AUDIO_STT_MISTRAL_API_BASE_URL, - AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, - AUDIO_TTS_ENGINE, - AUDIO_TTS_MODEL, - AUDIO_TTS_VOICE, - AUDIO_TTS_OPENAI_API_BASE_URL, - AUDIO_TTS_OPENAI_API_KEY, - AUDIO_TTS_OPENAI_PARAMS, - AUDIO_TTS_API_KEY, - AUDIO_TTS_SPLIT_ON, - AUDIO_TTS_AZURE_SPEECH_REGION, - AUDIO_TTS_AZURE_SPEECH_BASE_URL, - AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, - PLAYWRIGHT_WS_URL, - PLAYWRIGHT_TIMEOUT, - FIRECRAWL_API_BASE_URL, - FIRECRAWL_API_KEY, - FIRECRAWL_TIMEOUT, - WEB_LOADER_ENGINE, - WEB_LOADER_CONCURRENT_REQUESTS, - WEB_LOADER_TIMEOUT, - WHISPER_MODEL, - WHISPER_VAD_FILTER, - WHISPER_LANGUAGE, - DEEPGRAM_API_KEY, - WHISPER_MODEL_AUTO_UPDATE, - WHISPER_MODEL_DIR, - # Retrieval - RAG_TEMPLATE, - DEFAULT_RAG_TEMPLATE, - RAG_FULL_CONTEXT, - BYPASS_EMBEDDING_AND_RETRIEVAL, - RAG_EMBEDDING_MODEL, - RAG_EMBEDDING_MODEL_AUTO_UPDATE, - RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, - RAG_RERANKING_ENGINE, - RAG_RERANKING_MODEL, - RAG_EXTERNAL_RERANKER_URL, - RAG_EXTERNAL_RERANKER_API_KEY, - RAG_EXTERNAL_RERANKER_TIMEOUT, - RAG_RERANKING_MODEL_AUTO_UPDATE, - RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, - RAG_EMBEDDING_ENGINE, - RAG_EMBEDDING_BATCH_SIZE, - ENABLE_ASYNC_EMBEDDING, - RAG_TOP_K, - RAG_TOP_K_RERANKER, - RAG_RELEVANCE_THRESHOLD, - RAG_HYBRID_BM25_WEIGHT, - RAG_ALLOWED_FILE_EXTENSIONS, - RAG_FILE_MAX_COUNT, - RAG_FILE_MAX_SIZE, - FILE_IMAGE_COMPRESSION_WIDTH, - FILE_IMAGE_COMPRESSION_HEIGHT, - RAG_OPENAI_API_BASE_URL, - RAG_OPENAI_API_KEY, - RAG_AZURE_OPENAI_BASE_URL, - RAG_AZURE_OPENAI_API_KEY, - RAG_AZURE_OPENAI_API_VERSION, - RAG_OLLAMA_BASE_URL, - RAG_OLLAMA_API_KEY, - CHUNK_OVERLAP, - CHUNK_MIN_SIZE_TARGET, - CHUNK_SIZE, CONTENT_EXTRACTION_ENGINE, - DATALAB_MARKER_API_KEY, - DATALAB_MARKER_API_BASE_URL, + CORS_ALLOW_ORIGIN, DATALAB_MARKER_ADDITIONAL_CONFIG, - DATALAB_MARKER_SKIP_CACHE, - DATALAB_MARKER_FORCE_OCR, - DATALAB_MARKER_PAGINATE, - DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_API_BASE_URL, + DATALAB_MARKER_API_KEY, DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_FORCE_OCR, DATALAB_MARKER_FORMAT_LINES, DATALAB_MARKER_OUTPUT_FORMAT, - MINERU_API_MODE, - MINERU_API_URL, - MINERU_API_KEY, - MINERU_API_TIMEOUT, - MINERU_PARAMS, + DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_STRIP_EXISTING_OCR, DATALAB_MARKER_USE_LLM, - EXTERNAL_DOCUMENT_LOADER_URL, - EXTERNAL_DOCUMENT_LOADER_API_KEY, - TIKA_SERVER_URL, - DOCLING_SERVER_URL, + DDGS_BACKEND, + DEEPGRAM_API_KEY, + DEFAULT_ARENA_MODEL, + DEFAULT_GROUP_ID, + DEFAULT_LOCALE, + DEFAULT_MODEL_METADATA, + DEFAULT_MODEL_PARAMS, + DEFAULT_MODELS, + DEFAULT_PINNED_MODELS, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_RAG_TEMPLATE, + DEFAULT_USER_ROLE, DOCLING_API_KEY, DOCLING_PARAMS, + DOCLING_SERVER_URL, DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_KEY, DOCUMENT_INTELLIGENCE_MODEL, - MISTRAL_OCR_API_BASE_URL, - MISTRAL_OCR_API_KEY, - RAG_TEXT_SPLITTER, + ENABLE_ADMIN_ANALYTICS, + # Admin + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_EXPORT, + ENABLE_API_KEYS, + ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + ENABLE_ASYNC_EMBEDDING, + ENABLE_AUTOCOMPLETE_GENERATION, + ENABLE_AUTOMATIONS, + # Model list + ENABLE_BASE_MODELS_CACHE, + ENABLE_CALENDAR, + ENABLE_CHANNELS, + # Code Execution + ENABLE_CODE_EXECUTION, + ENABLE_CODE_INTERPRETER, + ENABLE_COMMUNITY_SHARING, + # Direct Connections + ENABLE_DIRECT_CONNECTIONS, + ENABLE_EVALUATION_ARENA_MODELS, + ENABLE_FOLDERS, + ENABLE_FOLLOW_UP_GENERATION, + ENABLE_GOOGLE_DRIVE_INTEGRATION, + ENABLE_IMAGE_EDIT, + ENABLE_IMAGE_GENERATION, + ENABLE_IMAGE_PROMPT_GENERATION, + # WebUI (LDAP) + ENABLE_LDAP, + ENABLE_LDAP_GROUP_CREATION, + # LDAP Group Management + ENABLE_LDAP_GROUP_MANAGEMENT, + ENABLE_LOGIN_FORM, ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, - TIKTOKEN_ENCODING_NAME, - PDF_EXTRACT_IMAGES, - YOUTUBE_LOADER_LANGUAGE, - YOUTUBE_LOADER_PROXY_URL, - # Retrieval (Web Search) - ENABLE_WEB_SEARCH, - WEB_SEARCH_ENGINE, - BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, - BYPASS_WEB_SEARCH_WEB_LOADER, - WEB_SEARCH_RESULT_COUNT, - WEB_SEARCH_CONCURRENT_REQUESTS, - WEB_SEARCH_TRUST_ENV, - WEB_SEARCH_DOMAIN_FILTER_LIST, - OLLAMA_CLOUD_WEB_SEARCH_API_KEY, - JINA_API_KEY, - JINA_API_BASE_URL, - SEARCHAPI_API_KEY, - SEARCHAPI_ENGINE, - SERPAPI_API_KEY, - SERPAPI_ENGINE, - SEARXNG_QUERY_URL, - SEARXNG_LANGUAGE, - YACY_QUERY_URL, - YACY_USERNAME, - YACY_PASSWORD, - SERPER_API_KEY, - SERPLY_API_KEY, - DDGS_BACKEND, - SERPSTACK_API_KEY, - SERPSTACK_HTTPS, - TAVILY_API_KEY, - TAVILY_EXTRACT_DEPTH, - BING_SEARCH_V7_ENDPOINT, - BING_SEARCH_V7_SUBSCRIPTION_KEY, - BRAVE_SEARCH_API_KEY, - EXA_API_KEY, - PERPLEXITY_API_KEY, - PERPLEXITY_MODEL, - PERPLEXITY_SEARCH_CONTEXT_USAGE, - PERPLEXITY_SEARCH_API_URL, - SOUGOU_API_SID, - SOUGOU_API_SK, - KAGI_SEARCH_API_KEY, - MOJEEK_SEARCH_API_KEY, - BOCHA_SEARCH_API_KEY, - GOOGLE_PSE_API_KEY, - GOOGLE_PSE_ENGINE_ID, - GOOGLE_DRIVE_CLIENT_ID, - GOOGLE_DRIVE_API_KEY, - ENABLE_ONEDRIVE_INTEGRATION, - ONEDRIVE_CLIENT_ID_PERSONAL, - ONEDRIVE_CLIENT_ID_BUSINESS, - ONEDRIVE_SHAREPOINT_URL, - ONEDRIVE_SHAREPOINT_TENANT_ID, - ENABLE_ONEDRIVE_PERSONAL, + ENABLE_MEMORIES, + ENABLE_MESSAGE_RATING, + ENABLE_NOTES, + # WebUI (OAuth) + ENABLE_OAUTH_ROLE_MANAGEMENT, + # Ollama + ENABLE_OLLAMA_API, ENABLE_ONEDRIVE_BUSINESS, + ENABLE_ONEDRIVE_INTEGRATION, + ENABLE_ONEDRIVE_PERSONAL, + # OpenAI + ENABLE_OPENAI_API, + ENABLE_PASSWORD_CHANGE_FORM, ENABLE_RAG_HYBRID_SEARCH, ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, ENABLE_RAG_LOCAL_WEB_FETCH, - ENABLE_WEB_LOADER_SSL_VERIFICATION, - ENABLE_GOOGLE_DRIVE_INTEGRATION, - UPLOAD_DIR, - EXTERNAL_WEB_SEARCH_URL, - EXTERNAL_WEB_SEARCH_API_KEY, - EXTERNAL_WEB_LOADER_URL, - EXTERNAL_WEB_LOADER_API_KEY, - # WebUI - WEBUI_AUTH, - WEBUI_NAME, - WEBUI_BANNERS, - WEBHOOK_URL, - ADMIN_EMAIL, - SHOW_ADMIN_DETAILS, - JWT_EXPIRES_IN, + ENABLE_RETRIEVAL_QUERY_GENERATION, + ENABLE_SEARCH_QUERY_GENERATION, ENABLE_SIGNUP, - ENABLE_LOGIN_FORM, - ENABLE_API_KEYS, - ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, - API_KEYS_ALLOWED_ENDPOINTS, - ENABLE_FOLDERS, - FOLDER_MAX_FILE_COUNT, - ENABLE_CHANNELS, - ENABLE_NOTES, + ENABLE_TAGS_GENERATION, + ENABLE_TITLE_GENERATION, ENABLE_USER_STATUS, - ENABLE_COMMUNITY_SHARING, - ENABLE_MESSAGE_RATING, ENABLE_USER_WEBHOOKS, - ENABLE_EVALUATION_ARENA_MODELS, - BYPASS_ADMIN_ACCESS_CONTROL, - USER_PERMISSIONS, - DEFAULT_USER_ROLE, - DEFAULT_GROUP_ID, - PENDING_USER_OVERLAY_CONTENT, - PENDING_USER_OVERLAY_TITLE, - DEFAULT_PROMPT_SUGGESTIONS, - DEFAULT_MODELS, - DEFAULT_PINNED_MODELS, - DEFAULT_ARENA_MODEL, - MODEL_ORDER_LIST, - EVALUATION_ARENA_MODELS, - # WebUI (OAuth) - ENABLE_OAUTH_ROLE_MANAGEMENT, - OAUTH_ROLES_CLAIM, - OAUTH_EMAIL_CLAIM, - OAUTH_PICTURE_CLAIM, - OAUTH_USERNAME_CLAIM, - OAUTH_ALLOWED_ROLES, - OAUTH_ADMIN_ROLES, - # WebUI (LDAP) - ENABLE_LDAP, - LDAP_SERVER_LABEL, - LDAP_SERVER_HOST, - LDAP_SERVER_PORT, - LDAP_ATTRIBUTE_FOR_MAIL, - LDAP_ATTRIBUTE_FOR_USERNAME, - LDAP_SEARCH_FILTERS, - LDAP_SEARCH_BASE, - LDAP_APP_DN, - LDAP_APP_PASSWORD, - LDAP_USE_TLS, - LDAP_CA_CERT_FILE, - LDAP_VALIDATE_CERT, - LDAP_CIPHERS, - # LDAP Group Management - ENABLE_LDAP_GROUP_MANAGEMENT, - ENABLE_LDAP_GROUP_CREATION, - LDAP_ATTRIBUTE_FOR_GROUPS, + ENABLE_VOICE_MODE_PROMPT, + ENABLE_WEB_LOADER_SSL_VERIFICATION, + # Retrieval (Web Search) + ENABLE_WEB_SEARCH, # Misc ENV, - CACHE_DIR, - STATIC_DIR, + EVALUATION_ARENA_MODELS, + EXA_API_KEY, + EXTERNAL_DOCUMENT_LOADER_API_KEY, + EXTERNAL_DOCUMENT_LOADER_URL, + EXTERNAL_WEB_LOADER_API_KEY, + EXTERNAL_WEB_LOADER_URL, + EXTERNAL_WEB_SEARCH_API_KEY, + EXTERNAL_WEB_SEARCH_URL, + FILE_IMAGE_COMPRESSION_HEIGHT, + FILE_IMAGE_COMPRESSION_WIDTH, + FIRECRAWL_API_BASE_URL, + FIRECRAWL_API_KEY, + FIRECRAWL_TIMEOUT, + FOLDER_MAX_FILE_COUNT, + FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, FRONTEND_BUILD_DIR, - CORS_ALLOW_ORIGIN, - DEFAULT_LOCALE, + GOOGLE_DRIVE_API_KEY, + GOOGLE_DRIVE_CLIENT_ID, + GOOGLE_PSE_API_KEY, + GOOGLE_PSE_ENGINE_ID, + IFRAME_CSP, + IMAGE_EDIT_ENGINE, + IMAGE_EDIT_MODEL, + IMAGE_EDIT_SIZE, + IMAGE_GENERATION_ENGINE, + IMAGE_GENERATION_MODEL, + IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + IMAGE_SIZE, + IMAGE_STEPS, + IMAGES_EDIT_COMFYUI_API_KEY, + IMAGES_EDIT_COMFYUI_BASE_URL, + IMAGES_EDIT_COMFYUI_WORKFLOW, + IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + IMAGES_EDIT_GEMINI_API_BASE_URL, + IMAGES_EDIT_GEMINI_API_KEY, + IMAGES_EDIT_OPENAI_API_BASE_URL, + IMAGES_EDIT_OPENAI_API_KEY, + IMAGES_EDIT_OPENAI_API_VERSION, + IMAGES_GEMINI_API_BASE_URL, + IMAGES_GEMINI_API_KEY, + IMAGES_GEMINI_ENDPOINT_METHOD, + IMAGES_OPENAI_API_BASE_URL, + IMAGES_OPENAI_API_KEY, + IMAGES_OPENAI_API_PARAMS, + IMAGES_OPENAI_API_VERSION, + JINA_API_BASE_URL, + JINA_API_KEY, + JWT_EXPIRES_IN, + KAGI_SEARCH_API_KEY, + LDAP_APP_DN, + LDAP_APP_PASSWORD, + LDAP_ATTRIBUTE_FOR_GROUPS, + LDAP_ATTRIBUTE_FOR_MAIL, + LDAP_ATTRIBUTE_FOR_USERNAME, + LDAP_CA_CERT_FILE, + LDAP_CIPHERS, + LDAP_SEARCH_BASE, + LDAP_SEARCH_FILTERS, + LDAP_SERVER_HOST, + LDAP_SERVER_LABEL, + LDAP_SERVER_PORT, + LDAP_USE_TLS, + LDAP_VALIDATE_CERT, + MINERU_API_KEY, + MINERU_API_MODE, + MINERU_API_TIMEOUT, + MINERU_API_URL, + MINERU_FILE_EXTENSIONS, + MINERU_PARAMS, + MISTRAL_OCR_API_BASE_URL, + MISTRAL_OCR_API_KEY, + MODEL_ORDER_LIST, + MOJEEK_SEARCH_API_KEY, + OAUTH_ADMIN_ROLES, + OAUTH_ALLOWED_ROLES, + OAUTH_EMAIL_CLAIM, + OAUTH_PICTURE_CLAIM, OAUTH_PROVIDERS, - WEBUI_URL, + OAUTH_ROLES_CLAIM, + OAUTH_SUB_CLAIM, + OAUTH_USERNAME_CLAIM, + OLLAMA_API_CONFIGS, + OLLAMA_BASE_URLS, + OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + ONEDRIVE_CLIENT_ID_BUSINESS, + ONEDRIVE_CLIENT_ID_PERSONAL, + ONEDRIVE_SHAREPOINT_TENANT_ID, + ONEDRIVE_SHAREPOINT_URL, + OPENAI_API_BASE_URLS, + OPENAI_API_CONFIGS, + OPENAI_API_KEYS, + PADDLEOCR_VL_BASE_URL, + PADDLEOCR_VL_TOKEN, + PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE, + PENDING_USER_OVERLAY_CONTENT, + PENDING_USER_OVERLAY_TITLE, + PERPLEXITY_API_KEY, + PERPLEXITY_MODEL, + PERPLEXITY_SEARCH_API_URL, + PERPLEXITY_SEARCH_CONTEXT_USAGE, + PLAYWRIGHT_TIMEOUT, + PLAYWRIGHT_WS_URL, + QUERY_GENERATION_PROMPT_TEMPLATE, + RAG_ALLOWED_FILE_EXTENSIONS, + RAG_AZURE_OPENAI_API_KEY, + RAG_AZURE_OPENAI_API_VERSION, + RAG_AZURE_OPENAI_BASE_URL, + RAG_EMBEDDING_BATCH_SIZE, + RAG_EMBEDDING_CONCURRENT_REQUESTS, + RAG_EMBEDDING_ENGINE, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_EXTERNAL_RERANKER_API_KEY, + RAG_EXTERNAL_RERANKER_TIMEOUT, + RAG_EXTERNAL_RERANKER_URL, + RAG_FILE_MAX_COUNT, + RAG_FILE_MAX_SIZE, + RAG_FULL_CONTEXT, + RAG_HYBRID_BM25_WEIGHT, + RAG_OLLAMA_API_KEY, + RAG_OLLAMA_BASE_URL, + RAG_OPENAI_API_BASE_URL, + RAG_OPENAI_API_KEY, + RAG_RELEVANCE_THRESHOLD, + RAG_RERANKING_BATCH_SIZE, + RAG_RERANKING_ENGINE, + RAG_RERANKING_MODEL, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + # Retrieval + RAG_TEMPLATE, + RAG_TEXT_SPLITTER, + RAG_TOP_K, + RAG_TOP_K_RERANKER, RESPONSE_WATERMARK, - # Admin - ENABLE_ADMIN_CHAT_ACCESS, - BYPASS_ADMIN_ACCESS_CONTROL, - ENABLE_ADMIN_EXPORT, + SEARCHAPI_API_KEY, + SEARCHAPI_ENGINE, + SEARXNG_LANGUAGE, + SEARXNG_QUERY_URL, + SERPAPI_API_KEY, + SERPAPI_ENGINE, + SERPER_API_KEY, + SERPLY_API_KEY, + SERPSTACK_API_KEY, + SERPSTACK_HTTPS, + SHOW_ADMIN_DETAILS, + SOUGOU_API_SID, + SOUGOU_API_SK, + STATIC_DIR, + TAGS_GENERATION_PROMPT_TEMPLATE, # Tasks TASK_MODEL, TASK_MODEL_EXTERNAL, - ENABLE_TAGS_GENERATION, - ENABLE_TITLE_GENERATION, - ENABLE_FOLLOW_UP_GENERATION, - ENABLE_SEARCH_QUERY_GENERATION, - ENABLE_RETRIEVAL_QUERY_GENERATION, - ENABLE_AUTOCOMPLETE_GENERATION, + TAVILY_API_KEY, + TAVILY_EXTRACT_DEPTH, + # Terminal Server + TERMINAL_SERVER_CONNECTIONS, + # Thread pool size for FastAPI/AnyIO + THREAD_POOL_SIZE, + TIKA_SERVER_URL, + TIKTOKEN_ENCODING_NAME, TITLE_GENERATION_PROMPT_TEMPLATE, - FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, - TAGS_GENERATION_PROMPT_TEMPLATE, - IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + # Tool Server Configs + TOOL_SERVER_CONNECTIONS, TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + UPLOAD_DIR, + USER_PERMISSIONS, VOICE_MODE_PROMPT_TEMPLATE, - QUERY_GENERATION_PROMPT_TEMPLATE, - AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, - AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + WEB_FETCH_MAX_CONTENT_LENGTH, + WEB_LOADER_CONCURRENT_REQUESTS, + WEB_LOADER_ENGINE, + WEB_LOADER_TIMEOUT, + WEB_SEARCH_CONCURRENT_REQUESTS, + WEB_SEARCH_DOMAIN_FILTER_LIST, + WEB_SEARCH_ENGINE, + WEB_SEARCH_RESULT_COUNT, + WEB_SEARCH_TRUST_ENV, + WEBHOOK_URL, + # WebUI + WEBUI_AUTH, + WEBUI_BANNERS, + WEBUI_NAME, + WEBUI_URL, + WHISPER_LANGUAGE, + WHISPER_MODEL, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + WHISPER_VAD_FILTER, + YACY_PASSWORD, + YACY_QUERY_URL, + YACY_USERNAME, + YANDEX_WEB_SEARCH_API_KEY, + YANDEX_WEB_SEARCH_CONFIG, + YANDEX_WEB_SEARCH_URL, + YOUCOM_API_KEY, + LINKUP_API_KEY, + LINKUP_SEARCH_PARAMS, + YOUTUBE_LOADER_LANGUAGE, + YOUTUBE_LOADER_PROXY_URL, AppConfig, + async_reset_config, reset_config, ) +from open_webui.constants import ERROR_MESSAGES, TASKS from open_webui.env import ( - ENABLE_CUSTOM_MODEL_FALLBACK, - LICENSE_KEY, + AIOHTTP_CLIENT_SESSION_SSL, AUDIT_EXCLUDED_PATHS, + AUDIT_INCLUDED_PATHS, AUDIT_LOG_LEVEL, + BYPASS_MODEL_ACCESS_CONTROL, CHANGELOG, - REDIS_URL, + DEPLOYMENT_ID, + ENABLE_AUDIT_GET_REQUESTS, + ENABLE_COMPRESSION_MIDDLEWARE, + ENABLE_CUSTOM_MODEL_FALLBACK, + ENABLE_EASTER_EGGS, + # OAuth Back-Channel Logout + ENABLE_OAUTH_BACKCHANNEL_LOGOUT, + ENABLE_OTEL, + ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + # SCIM + ENABLE_SCIM, + ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + ENABLE_STAR_SESSIONS_MIDDLEWARE, + ENABLE_VERSION_UPDATE_CHECK, + ENABLE_WEBSOCKET_SUPPORT, + EXTERNAL_PWA_MANIFEST_URL, + GLOBAL_LOG_LEVEL, + INSTANCE_ID, + LICENSE_KEY, + LOG_FORMAT, + MAX_BODY_LOG_SIZE, + # Redis REDIS_CLUSTER, REDIS_KEY_PREFIX, - REDIS_SENTINEL_HOSTS, - REDIS_SENTINEL_PORT, - GLOBAL_LOG_LEVEL, - MAX_BODY_LOG_SIZE, + REDIS_URL, + RESET_CONFIG_ON_START, SAFE_MODE, + SCIM_TOKEN, VERSION, - DEPLOYMENT_ID, - INSTANCE_ID, + # Admin Account Runtime Creation + WEBUI_ADMIN_EMAIL, + WEBUI_ADMIN_NAME, + WEBUI_ADMIN_PASSWORD, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, WEBUI_BUILD_HASH, WEBUI_SECRET_KEY, WEBUI_SESSION_COOKIE_SAME_SITE, WEBUI_SESSION_COOKIE_SECURE, - ENABLE_SIGNUP_PASSWORD_CONFIRMATION, - WEBUI_AUTH_TRUSTED_EMAIL_HEADER, - WEBUI_AUTH_TRUSTED_NAME_HEADER, - WEBUI_AUTH_SIGNOUT_REDIRECT_URL, - # SCIM - ENABLE_SCIM, - SCIM_TOKEN, - ENABLE_COMPRESSION_MIDDLEWARE, - ENABLE_WEBSOCKET_SUPPORT, - BYPASS_MODEL_ACCESS_CONTROL, - RESET_CONFIG_ON_START, - ENABLE_VERSION_UPDATE_CHECK, - ENABLE_OTEL, - EXTERNAL_PWA_MANIFEST_URL, - AIOHTTP_CLIENT_SESSION_SSL, - ENABLE_STAR_SESSIONS_MIDDLEWARE, - ENABLE_PUBLIC_ACTIVE_USERS_COUNT, - # Admin Account Runtime Creation - WEBUI_ADMIN_EMAIL, - WEBUI_ADMIN_PASSWORD, - WEBUI_ADMIN_NAME, ) - - -from open_webui.utils.models import ( - get_all_models, - get_all_base_models, - check_model_access, - get_filtered_models, +from open_webui.internal.db import ScopedSession, engine, get_async_session +from open_webui.models.chats import ChatForm, Chats +from open_webui.models.functions import Functions +from open_webui.models.models import Models +from open_webui.models.users import UserModel, Users +from open_webui.routers import ( + analytics, + audio, + auths, + automations, + calendar, + channels, + chats, + configs, + evaluations, + files, + folders, + functions, + groups, + images, + knowledge, + memories, + models, + notes, + ollama, + openai, + pipelines, + prompts, + retrieval, + scim, + skills, + tasks, + terminals, + tools, + users, + utils, +) +from open_webui.routers.retrieval import ( + get_ef, + get_embedding_function, + get_reranking_function, + get_rf, +) +from open_webui.socket.main import ( + MODELS, + get_event_emitter, + get_models_in_use, + get_user_id_from_session_pool, + periodic_session_pool_cleanup, + periodic_usage_pool_cleanup, +) +from open_webui.socket.main import ( + app as socket_app, +) +from open_webui.tasks import ( + cleanup_task, + create_task, + has_active_tasks, + list_task_ids_by_item_id, + list_tasks, + stop_item_tasks, + stop_task, +) # Import from tasks.py +from open_webui.utils import logger +from open_webui.utils.actions import chat_action as chat_action_handler +from open_webui.utils.asgi_middleware import ( + AuthTokenMiddleware, + CommitSessionMiddleware, + RedirectMiddleware, + WebsocketUpgradeGuardMiddleware, +) +from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware +from open_webui.utils.auth import ( + create_admin_user, + decode_token, + get_admin_user, + get_http_authorization_cred, + get_license_data, + get_verified_user, +) +from open_webui.utils.chat import ( + chat_completed as chat_completed_handler, ) from open_webui.utils.chat import ( generate_chat_completion as chat_completion_handler, - chat_completed as chat_completed_handler, - chat_action as chat_action_handler, ) from open_webui.utils.embeddings import generate_embeddings -from open_webui.utils.middleware import process_chat_payload, process_chat_response -from open_webui.utils.access_control import has_access - -from open_webui.utils.auth import ( - get_license_data, - get_http_authorization_cred, - decode_token, - get_admin_user, - get_verified_user, - create_admin_user, +from open_webui.utils.logger import start_logger +from open_webui.utils.middleware import ( + build_chat_response_context, + process_chat_payload, + process_chat_response, +) +from open_webui.utils.models import ( + check_model_access, + get_all_base_models, + get_all_models, + get_filtered_models, +) +from open_webui.utils.oauth import ( + OAuthClientInformationFull, + OAuthClientManager, + OAuthManager, + decrypt_data, + encrypt_data, + get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, + resolve_oauth_client_info, ) from open_webui.utils.plugin import install_tool_and_function_dependencies -from open_webui.utils.oauth import ( - get_oauth_client_info_with_dynamic_client_registration, - encrypt_data, - decrypt_data, - OAuthManager, - OAuthClientManager, - OAuthClientInformationFull, -) +from open_webui.utils.redis import get_redis_client, get_redis_connection from open_webui.utils.security_headers import SecurityHeadersMiddleware -from open_webui.utils.redis import get_redis_connection - -from open_webui.tasks import ( - list_task_ids_by_item_id, - create_task, - stop_task, - list_tasks, -) # Import from tasks.py - -from open_webui.utils.redis import get_sentinels_from_env - - -from open_webui.constants import ERROR_MESSAGES - +from open_webui.utils.session_pool import get_session +from open_webui.utils.tools import set_terminal_servers, set_tool_servers if SAFE_MODE: - print("SAFE MODE ENABLED") - Functions.deactivate_all_functions() + print('SAFE MODE ENABLED') + # Functions.deactivate_all_functions() is awaited in lifespan below logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) @@ -555,17 +597,17 @@ class SPAStaticFiles(StaticFiles): return await super().get_response(path, scope) except (HTTPException, StarletteHTTPException) as ex: if ex.status_code == 404: - if path.endswith(".js"): + if path.endswith('.js'): # Return 404 for javascript files raise ex else: - return await super().get_response("index.html", scope) + return await super().get_response('index.html', scope) else: raise ex -print( - rf""" +if LOG_FORMAT != 'json': + banner = rf""" ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║ @@ -575,81 +617,135 @@ print( v{VERSION} - building the best AI user interface. -{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""} +{f'Commit: {WEBUI_BUILD_HASH}' if WEBUI_BUILD_HASH != 'dev-build' else ''} https://github.com/open-webui/open-webui """ -) + try: + print(banner) + except UnicodeEncodeError: + # Stdout can't encode the box-drawing banner (Windows cp1252, redirected/headless stdout); fall back to ASCII. + print(f'Open WebUI v{VERSION} - building the best AI user interface.\nhttps://github.com/open-webui/open-webui') @asynccontextmanager async def lifespan(app: FastAPI): + # Store reference to main event loop for sync->async calls (e.g., embedding generation) + # This allows sync functions to schedule work on the main loop without blocking health checks + app.state.main_loop = asyncio.get_running_loop() + app.state.instance_id = INSTANCE_ID start_logger() if RESET_CONFIG_ON_START: - reset_config() + await async_reset_config() if LICENSE_KEY: get_license_data(app, LICENSE_KEY) # Create admin account from env vars if specified and no users exist if WEBUI_ADMIN_EMAIL and WEBUI_ADMIN_PASSWORD: - if create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME): + if await create_admin_user(WEBUI_ADMIN_EMAIL, WEBUI_ADMIN_PASSWORD, WEBUI_ADMIN_NAME): # Disable signup since we now have an admin app.state.config.ENABLE_SIGNUP = False + if SAFE_MODE: + await Functions.deactivate_all_functions() + # This should be blocking (sync) so functions are not deactivated on first /get_models calls # when the first user lands on the / route. - log.info("Installing external dependencies of functions and tools...") - install_tool_and_function_dependencies() - - app.state.redis = get_redis_connection( - redis_url=REDIS_URL, - redis_sentinels=get_sentinels_from_env( - REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT - ), - redis_cluster=REDIS_CLUSTER, - async_mode=True, - ) + log.info('Installing external dependencies of functions and tools...') + await install_tool_and_function_dependencies() + app.state.redis = get_redis_client(async_mode=True) if THREAD_POOL_SIZE and THREAD_POOL_SIZE > 0: limiter = anyio.to_thread.current_default_thread_limiter() limiter.total_tokens = THREAD_POOL_SIZE asyncio.create_task(periodic_usage_pool_cleanup()) + asyncio.create_task(periodic_session_pool_cleanup()) + + from open_webui.utils.automations import scheduler_worker_loop + + asyncio.create_task(scheduler_worker_loop(app)) if app.state.config.ENABLE_BASE_MODELS_CACHE: - await get_all_models( - Request( - # Creating a mock request object to pass to get_all_models - { - "type": "http", - "asgi.version": "3.0", - "asgi.spec_version": "2.0", - "method": "GET", - "path": "/internal", - "query_string": b"", - "headers": Headers({}).raw, - "client": ("127.0.0.1", 12345), - "server": ("127.0.0.1", 80), - "scheme": "http", - "app": app, - } - ), - None, + try: + await get_all_models( + Request( + # Creating a mock request object to pass to get_all_models + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } + ), + None, + ) + except Exception as e: + log.warning(f'Failed to pre-fetch models at startup: {e}') + + # Pre-fetch tool server specs so the first request doesn't pay the latency cost + if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: + mock_request = Request( + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': app, + } ) + log.info('Initializing tool servers...') + try: + await set_tool_servers(mock_request) + log.info(f'Initialized {len(app.state.TOOL_SERVERS)} tool server(s)') + except Exception as e: + log.warning(f'Failed to initialize tool servers at startup: {e}') + + try: + await set_terminal_servers(mock_request) + log.info(f'Initialized {len(app.state.TERMINAL_SERVERS)} terminal server(s)') + except Exception as e: + log.warning(f'Failed to initialize terminal servers at startup: {e}') + + # Mark application as ready to accept traffic from a startup perspective. + app.state.startup_complete = True + yield + # Shutdown: clean up shared resources + from open_webui.utils.session_pool import close_session + + await close_session() + + app = FastAPI( - title="Open WebUI", - docs_url="/docs" if ENV == "dev" else None, - openapi_url="/openapi.json" if ENV == "dev" else None, + title='Open WebUI', + docs_url='/docs' if ENV == 'dev' else None, + openapi_url='/openapi.json' if ENV == 'dev' else None, redoc_url=None, lifespan=lifespan, ) +# Used by readiness checks to gate traffic until startup work is done. +app.state.startup_complete = False + # For Open WebUI OIDC/OAuth2 oauth_manager = OAuthManager(app) app.state.oauth_manager = oauth_manager @@ -661,7 +757,6 @@ app.state.oauth_client_manager = oauth_client_manager app.state.instance_id = None app.state.config = AppConfig( redis_url=REDIS_URL, - redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT), redis_cluster=REDIS_CLUSTER, redis_key_prefix=REDIS_KEY_PREFIX, ) @@ -718,6 +813,15 @@ app.state.OPENAI_MODELS = {} app.state.config.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS app.state.TOOL_SERVERS = [] +######################################## +# +# TERMINAL SERVER +# +######################################## + +app.state.config.TERMINAL_SERVER_CONNECTIONS = TERMINAL_SERVER_CONNECTIONS +app.state.TERMINAL_SERVERS = [] + ######################################## # # DIRECT CONNECTIONS @@ -753,11 +857,10 @@ app.state.BASE_MODELS = [] app.state.config.WEBUI_URL = WEBUI_URL app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM +app.state.config.ENABLE_PASSWORD_CHANGE_FORM = ENABLE_PASSWORD_CHANGE_FORM app.state.config.ENABLE_API_KEYS = ENABLE_API_KEYS -app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ( - ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS -) +app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS app.state.config.API_KEYS_ALLOWED_ENDPOINTS = API_KEYS_ALLOWED_ENDPOINTS app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN @@ -769,6 +872,8 @@ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL app.state.config.DEFAULT_MODELS = DEFAULT_MODELS app.state.config.DEFAULT_PINNED_MODELS = DEFAULT_PINNED_MODELS app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST +app.state.config.DEFAULT_MODEL_METADATA = DEFAULT_MODEL_METADATA +app.state.config.DEFAULT_MODEL_PARAMS = DEFAULT_MODEL_PARAMS app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS @@ -787,7 +892,11 @@ app.state.config.BANNERS = WEBUI_BANNERS app.state.config.ENABLE_FOLDERS = ENABLE_FOLDERS app.state.config.FOLDER_MAX_FILE_COUNT = FOLDER_MAX_FILE_COUNT +app.state.config.ENABLE_AUTOMATIONS = ENABLE_AUTOMATIONS +app.state.config.AUTOMATION_MAX_COUNT = AUTOMATION_MAX_COUNT +app.state.config.AUTOMATION_MIN_INTERVAL = AUTOMATION_MIN_INTERVAL app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS +app.state.config.ENABLE_CALENDAR = ENABLE_CALENDAR app.state.config.ENABLE_NOTES = ENABLE_NOTES app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING @@ -797,6 +906,22 @@ app.state.config.ENABLE_USER_STATUS = ENABLE_USER_STATUS app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS +# Migrate legacy access_control → access_grants on boot +from open_webui.utils.access_control import has_permission, migrate_access_control + +connections = app.state.config.TOOL_SERVER_CONNECTIONS +if any('access_control' in c.get('config', {}) for c in connections): + for connection in connections: + migrate_access_control(connection.get('config', {})) + app.state.config.TOOL_SERVER_CONNECTIONS = connections + +arena_models = app.state.config.EVALUATION_ARENA_MODELS +if any('access_control' in m.get('meta', {}) for m in arena_models): + for model in arena_models: + migrate_access_control(model.get('meta', {})) + app.state.config.EVALUATION_ARENA_MODELS = arena_models + +app.state.config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM @@ -863,9 +988,7 @@ app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = FILE_IMAGE_COMPRESSION_HEIGHT app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH -app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ( - ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS -) +app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE @@ -876,9 +999,7 @@ app.state.config.DATALAB_MARKER_SKIP_CACHE = DATALAB_MARKER_SKIP_CACHE app.state.config.DATALAB_MARKER_FORCE_OCR = DATALAB_MARKER_FORCE_OCR app.state.config.DATALAB_MARKER_PAGINATE = DATALAB_MARKER_PAGINATE app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR = DATALAB_MARKER_STRIP_EXISTING_OCR -app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = ( - DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION -) +app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION app.state.config.DATALAB_MARKER_FORMAT_LINES = DATALAB_MARKER_FORMAT_LINES app.state.config.DATALAB_MARKER_USE_LLM = DATALAB_MARKER_USE_LLM app.state.config.DATALAB_MARKER_OUTPUT_FORMAT = DATALAB_MARKER_OUTPUT_FORMAT @@ -893,16 +1014,17 @@ app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY app.state.config.DOCUMENT_INTELLIGENCE_MODEL = DOCUMENT_INTELLIGENCE_MODEL app.state.config.MISTRAL_OCR_API_BASE_URL = MISTRAL_OCR_API_BASE_URL app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY +app.state.config.PADDLEOCR_VL_BASE_URL = PADDLEOCR_VL_BASE_URL +app.state.config.PADDLEOCR_VL_TOKEN = PADDLEOCR_VL_TOKEN app.state.config.MINERU_API_MODE = MINERU_API_MODE app.state.config.MINERU_API_URL = MINERU_API_URL app.state.config.MINERU_API_KEY = MINERU_API_KEY app.state.config.MINERU_API_TIMEOUT = MINERU_API_TIMEOUT app.state.config.MINERU_PARAMS = MINERU_PARAMS +app.state.config.MINERU_FILE_EXTENSIONS = MINERU_FILE_EXTENSIONS app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER -app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ( - ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER -) +app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME @@ -915,12 +1037,14 @@ app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE app.state.config.ENABLE_ASYNC_EMBEDDING = ENABLE_ASYNC_EMBEDDING +app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = RAG_EMBEDDING_CONCURRENT_REQUESTS app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT = RAG_EXTERNAL_RERANKER_TIMEOUT +app.state.config.RAG_RERANKING_BATCH_SIZE = RAG_RERANKING_BATCH_SIZE app.state.config.RAG_TEMPLATE = RAG_TEMPLATE @@ -935,6 +1059,7 @@ app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES +app.state.config.PDF_LOADER_MODE = PDF_LOADER_MODE app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL @@ -945,15 +1070,14 @@ app.state.config.WEB_SEARCH_ENGINE = WEB_SEARCH_ENGINE app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = WEB_SEARCH_DOMAIN_FILTER_LIST app.state.config.WEB_SEARCH_RESULT_COUNT = WEB_SEARCH_RESULT_COUNT app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = WEB_SEARCH_CONCURRENT_REQUESTS +app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH = WEB_FETCH_MAX_CONTENT_LENGTH app.state.config.WEB_LOADER_ENGINE = WEB_LOADER_ENGINE app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = WEB_LOADER_CONCURRENT_REQUESTS app.state.config.WEB_LOADER_TIMEOUT = WEB_LOADER_TIMEOUT app.state.config.WEB_SEARCH_TRUST_ENV = WEB_SEARCH_TRUST_ENV -app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = ( - BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL -) +app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = BYPASS_WEB_SEARCH_WEB_LOADER app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION @@ -968,6 +1092,7 @@ app.state.config.YACY_PASSWORD = YACY_PASSWORD app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY +app.state.config.BRAVE_SEARCH_CONTEXT_TOKENS = BRAVE_SEARCH_CONTEXT_TOKENS app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY @@ -996,6 +1121,12 @@ app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY +app.state.config.YANDEX_WEB_SEARCH_URL = YANDEX_WEB_SEARCH_URL +app.state.config.YANDEX_WEB_SEARCH_API_KEY = YANDEX_WEB_SEARCH_API_KEY +app.state.config.YANDEX_WEB_SEARCH_CONFIG = YANDEX_WEB_SEARCH_CONFIG +app.state.config.YOUCOM_API_KEY = YOUCOM_API_KEY +app.state.config.LINKUP_API_KEY = LINKUP_API_KEY +app.state.config.LINKUP_SEARCH_PARAMS = LINKUP_SEARCH_PARAMS app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL @@ -1014,13 +1145,8 @@ app.state.YOUTUBE_LOADER_TRANSLATION = None try: - app.state.ef = get_ef( - app.state.config.RAG_EMBEDDING_ENGINE, app.state.config.RAG_EMBEDDING_MODEL - ) - if ( - app.state.config.ENABLE_RAG_HYBRID_SEARCH - and not app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL - ): + app.state.ef = get_ef(app.state.config.RAG_EMBEDDING_ENGINE, app.state.config.RAG_EMBEDDING_MODEL) + if app.state.config.ENABLE_RAG_HYBRID_SEARCH and not app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: app.state.rf = get_rf( app.state.config.RAG_RERANKING_ENGINE, app.state.config.RAG_RERANKING_MODEL, @@ -1031,7 +1157,7 @@ try: else: app.state.rf = None except Exception as e: - log.error(f"Error updating models: {e}") + log.error(f'Error updating models: {e}') pass @@ -1041,35 +1167,37 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( embedding_function=app.state.ef, url=( app.state.config.RAG_OPENAI_API_BASE_URL - if app.state.config.RAG_EMBEDDING_ENGINE == "openai" + if app.state.config.RAG_EMBEDDING_ENGINE == 'openai' else ( app.state.config.RAG_OLLAMA_BASE_URL - if app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' else app.state.config.RAG_AZURE_OPENAI_BASE_URL ) ), key=( app.state.config.RAG_OPENAI_API_KEY - if app.state.config.RAG_EMBEDDING_ENGINE == "openai" + if app.state.config.RAG_EMBEDDING_ENGINE == 'openai' else ( app.state.config.RAG_OLLAMA_API_KEY - if app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + if app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' else app.state.config.RAG_AZURE_OPENAI_API_KEY ) ), embedding_batch_size=app.state.config.RAG_EMBEDDING_BATCH_SIZE, azure_api_version=( app.state.config.RAG_AZURE_OPENAI_API_VERSION - if app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" + if app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' else None ), enable_async=app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, ) app.state.RERANKING_FUNCTION = get_reranking_function( app.state.config.RAG_RERANKING_ENGINE, app.state.config.RAG_RERANKING_MODEL, reranking_function=app.state.rf, + reranking_batch_size=app.state.config.RAG_RERANKING_BATCH_SIZE, ) ######################################## @@ -1083,9 +1211,7 @@ app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN -app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( - CODE_EXECUTION_JUPYTER_AUTH_PASSWORD -) +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = CODE_EXECUTION_JUPYTER_AUTH_PASSWORD app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER @@ -1094,12 +1220,8 @@ app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMP app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH -app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = ( - CODE_INTERPRETER_JUPYTER_AUTH_TOKEN -) -app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ( - CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD -) +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = CODE_INTERPRETER_JUPYTER_AUTH_TOKEN +app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT ######################################## @@ -1160,6 +1282,7 @@ app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = IMAGES_EDIT_COMFYUI_WORKFL app.state.config.STT_ENGINE = AUDIO_STT_ENGINE app.state.config.STT_MODEL = AUDIO_STT_MODEL app.state.config.STT_SUPPORTED_CONTENT_TYPES = AUDIO_STT_SUPPORTED_CONTENT_TYPES +app.state.config.STT_ALLOWED_EXTENSIONS = AUDIO_STT_ALLOWED_EXTENSIONS app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY @@ -1175,9 +1298,7 @@ app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS app.state.config.AUDIO_STT_MISTRAL_API_KEY = AUDIO_STT_MISTRAL_API_KEY app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = AUDIO_STT_MISTRAL_API_BASE_URL -app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = ( - AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS -) +app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE @@ -1196,6 +1317,9 @@ app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION app.state.config.TTS_AZURE_SPEECH_BASE_URL = AUDIO_TTS_AZURE_SPEECH_BASE_URL app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT +app.state.config.TTS_MISTRAL_API_KEY = AUDIO_TTS_MISTRAL_API_KEY +app.state.config.TTS_MISTRAL_API_BASE_URL = AUDIO_TTS_MISTRAL_API_BASE_URL + app.state.faster_whisper_model = None app.state.speech_synthesiser = None @@ -1223,24 +1347,15 @@ app.state.config.ENABLE_FOLLOW_UP_GENERATION = ENABLE_FOLLOW_UP_GENERATION app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE -app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = ( - IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE -) -app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = ( - FOLLOW_UP_GENERATION_PROMPT_TEMPLATE -) +app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE +app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = FOLLOW_UP_GENERATION_PROMPT_TEMPLATE -app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ( - TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE -) +app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE -app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = ( - AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE -) -app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = ( - AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH -) +app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE +app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH app.state.config.VOICE_MODE_PROMPT_TEMPLATE = VOICE_MODE_PROMPT_TEMPLATE +app.state.config.ENABLE_VOICE_MODE_PROMPT = ENABLE_VOICE_MODE_PROMPT ######################################## @@ -1256,191 +1371,83 @@ if ENABLE_COMPRESSION_MIDDLEWARE: app.add_middleware(CompressMiddleware) -class RedirectMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next): - # Check if the request is a GET request - if request.method == "GET": - path = request.url.path - query_params = dict(parse_qs(urlparse(str(request.url)).query)) - - redirect_params = {} - - # Check for the specific watch path and the presence of 'v' parameter - if path.endswith("/watch") and "v" in query_params: - # Extract the first 'v' parameter - youtube_video_id = query_params["v"][0] - redirect_params["youtube"] = youtube_video_id - - if "shared" in query_params and len(query_params["shared"]) > 0: - # PWA share_target support - - text = query_params["shared"][0] - if text: - urls = re.match(r"https://\S+", text) - if urls: - from open_webui.retrieval.loaders.youtube import _parse_video_id - - if youtube_video_id := _parse_video_id(urls[0]): - redirect_params["youtube"] = youtube_video_id - else: - redirect_params["load-url"] = urls[0] - else: - redirect_params["q"] = text - - if redirect_params: - redirect_url = f"/?{urlencode(redirect_params)}" - return RedirectResponse(url=redirect_url) - - # Proceed with the normal flow of other requests - response = await call_next(request) - return response - - +# All HTTP middlewares below are pure-ASGI implementations. The previous +# `BaseHTTPMiddleware` / `@app.middleware('http')` versions wrapped the +# downstream app in an anyio task group whose cancel scope cancelled +# in-flight DB calls (and any other awaits) on client disconnect / +# response completion — which surfaced as noisy SQLAlchemy +# `terminate_force_close` tracebacks under aiosqlite and as random +# CancelledError storms across the request path. See +# `open_webui.utils.asgi_middleware` for the rationale. app.add_middleware(RedirectMiddleware) app.add_middleware(SecurityHeadersMiddleware) - - -class APIKeyRestrictionMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request: Request, call_next): - auth_header = request.headers.get("Authorization") - token = None - - if auth_header: - scheme, token = auth_header.split(" ") - - # Only apply restrictions if an sk- API key is used - if token and token.startswith("sk-"): - # Check if restrictions are enabled - if request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS: - allowed_paths = [ - path.strip() - for path in str( - request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS - ).split(",") - if path.strip() - ] - - request_path = request.url.path - - # Match exact path or prefix path - is_allowed = any( - request_path == allowed or request_path.startswith(allowed + "/") - for allowed in allowed_paths - ) - - if not is_allowed: - return JSONResponse( - status_code=status.HTTP_403_FORBIDDEN, - content={ - "detail": "API key not allowed to access this endpoint." - }, - ) - - response = await call_next(request) - return response - - -app.add_middleware(APIKeyRestrictionMiddleware) - - -@app.middleware("http") -async def commit_session_after_request(request: Request, call_next): - response = await call_next(request) - # log.debug("Commit session after request") - ScopedSession.commit() - return response - - -@app.middleware("http") -async def check_url(request: Request, call_next): - start_time = int(time.time()) - request.state.token = get_http_authorization_cred( - request.headers.get("Authorization") - ) - - request.state.enable_api_keys = app.state.config.ENABLE_API_KEYS - response = await call_next(request) - process_time = int(time.time()) - start_time - response.headers["X-Process-Time"] = str(process_time) - return response - - -@app.middleware("http") -async def inspect_websocket(request: Request, call_next): - if ( - "/ws/socket.io" in request.url.path - and request.query_params.get("transport") == "websocket" - ): - upgrade = (request.headers.get("Upgrade") or "").lower() - connection = (request.headers.get("Connection") or "").lower().split(",") - # Check that there's the correct headers for an upgrade, else reject the connection - # This is to work around this upstream issue: https://github.com/miguelgrinberg/python-engineio/issues/367 - if upgrade != "websocket" or "upgrade" not in connection: - return JSONResponse( - status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": "Invalid WebSocket upgrade request"}, - ) - return await call_next(request) +app.add_middleware(CommitSessionMiddleware) +app.add_middleware(AuthTokenMiddleware, fastapi_app=app) +app.add_middleware(WebsocketUpgradeGuardMiddleware) app.add_middleware( CORSMiddleware, allow_origins=CORS_ALLOW_ORIGIN, allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], + allow_methods=['*'], + allow_headers=['*'], ) -app.mount("/ws", socket_app) +app.mount('/ws', socket_app) -app.include_router(ollama.router, prefix="/ollama", tags=["ollama"]) -app.include_router(openai.router, prefix="/openai", tags=["openai"]) +app.include_router(ollama.router, prefix='/ollama', tags=['ollama']) +app.include_router(openai.router, prefix='/openai', tags=['openai']) -app.include_router(pipelines.router, prefix="/api/v1/pipelines", tags=["pipelines"]) -app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) -app.include_router(images.router, prefix="/api/v1/images", tags=["images"]) +app.include_router(pipelines.router, prefix='/api/v1/pipelines', tags=['pipelines']) +app.include_router(tasks.router, prefix='/api/v1/tasks', tags=['tasks']) +app.include_router(images.router, prefix='/api/v1/images', tags=['images']) -app.include_router(audio.router, prefix="/api/v1/audio", tags=["audio"]) -app.include_router(retrieval.router, prefix="/api/v1/retrieval", tags=["retrieval"]) +app.include_router(audio.router, prefix='/api/v1/audio', tags=['audio']) +app.include_router(retrieval.router, prefix='/api/v1/retrieval', tags=['retrieval']) -app.include_router(configs.router, prefix="/api/v1/configs", tags=["configs"]) +app.include_router(configs.router, prefix='/api/v1/configs', tags=['configs']) -app.include_router(auths.router, prefix="/api/v1/auths", tags=["auths"]) -app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) +app.include_router(auths.router, prefix='/api/v1/auths', tags=['auths']) +app.include_router(users.router, prefix='/api/v1/users', tags=['users']) -app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"]) -app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"]) -app.include_router(notes.router, prefix="/api/v1/notes", tags=["notes"]) +app.include_router(channels.router, prefix='/api/v1/channels', tags=['channels']) +app.include_router(chats.router, prefix='/api/v1/chats', tags=['chats']) +app.include_router(notes.router, prefix='/api/v1/notes', tags=['notes']) -app.include_router(models.router, prefix="/api/v1/models", tags=["models"]) -app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"]) -app.include_router(prompts.router, prefix="/api/v1/prompts", tags=["prompts"]) -app.include_router(tools.router, prefix="/api/v1/tools", tags=["tools"]) +app.include_router(models.router, prefix='/api/v1/models', tags=['models']) +app.include_router(knowledge.router, prefix='/api/v1/knowledge', tags=['knowledge']) +app.include_router(prompts.router, prefix='/api/v1/prompts', tags=['prompts']) +app.include_router(tools.router, prefix='/api/v1/tools', tags=['tools']) +app.include_router(skills.router, prefix='/api/v1/skills', tags=['skills']) -app.include_router(memories.router, prefix="/api/v1/memories", tags=["memories"]) -app.include_router(folders.router, prefix="/api/v1/folders", tags=["folders"]) -app.include_router(groups.router, prefix="/api/v1/groups", tags=["groups"]) -app.include_router(files.router, prefix="/api/v1/files", tags=["files"]) -app.include_router(functions.router, prefix="/api/v1/functions", tags=["functions"]) -app.include_router( - evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"] -) -app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"]) +app.include_router(memories.router, prefix='/api/v1/memories', tags=['memories']) +app.include_router(folders.router, prefix='/api/v1/folders', tags=['folders']) +app.include_router(groups.router, prefix='/api/v1/groups', tags=['groups']) +app.include_router(files.router, prefix='/api/v1/files', tags=['files']) +app.include_router(functions.router, prefix='/api/v1/functions', tags=['functions']) +app.include_router(evaluations.router, prefix='/api/v1/evaluations', tags=['evaluations']) +if ENABLE_ADMIN_ANALYTICS: + app.include_router(analytics.router, prefix='/api/v1/analytics', tags=['analytics']) +app.include_router(utils.router, prefix='/api/v1/utils', tags=['utils']) +app.include_router(terminals.router, prefix='/api/v1/terminals', tags=['terminals']) +app.include_router(automations.router, prefix='/api/v1/automations', tags=['automations']) +app.include_router(calendar.router, prefix='/api/v1/calendars', tags=['calendars']) # SCIM 2.0 API for identity management if ENABLE_SCIM: - app.include_router(scim.router, prefix="/api/v1/scim/v2", tags=["scim"]) + app.include_router(scim.router, prefix='/api/v1/scim/v2', tags=['scim']) try: audit_level = AuditLevel(AUDIT_LOG_LEVEL) except ValueError as e: - logger.error(f"Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}") + logger.error(f'Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}') audit_level = AuditLevel.NONE if audit_level != AuditLevel.NONE: @@ -1448,6 +1455,8 @@ if audit_level != AuditLevel.NONE: AuditLoggingMiddleware, audit_level=audit_level, excluded_paths=AUDIT_EXCLUDED_PATHS, + included_paths=AUDIT_INCLUDED_PATHS, + audit_get_requests=ENABLE_AUDIT_GET_REQUESTS, max_body_size=MAX_BODY_LOG_SIZE, ) ################################## @@ -1457,35 +1466,30 @@ if audit_level != AuditLevel.NONE: ################################## -@app.get("/api/models") -@app.get("/api/v1/models") # Experimental: Compatibility with OpenAI API -async def get_models( - request: Request, refresh: bool = False, user=Depends(get_verified_user) -): +@app.get('/api/models') +@app.get('/api/v1/models') # Experimental: Compatibility with OpenAI API +async def get_models(request: Request, refresh: bool = False, user=Depends(get_verified_user)): all_models = await get_all_models(request, refresh=refresh, user=user) models = [] for model in all_models: # Filter out filter pipelines - if "pipeline" in model and model["pipeline"].get("type", None) == "filter": + if 'pipeline' in model and model['pipeline'].get('type', None) == 'filter': continue # Remove profile image URL to reduce payload size - if model.get("info", {}).get("meta", {}).get("profile_image_url"): - model["info"]["meta"].pop("profile_image_url", None) + if model.get('info', {}).get('meta', {}).get('profile_image_url'): + model['info']['meta'].pop('profile_image_url', None) try: - model_tags = [ - tag.get("name") - for tag in model.get("info", {}).get("meta", {}).get("tags", []) - ] - tags = [tag.get("name") for tag in model.get("tags", [])] + model_tags = [tag.get('name') for tag in model.get('info', {}).get('meta', {}).get('tags', [])] + tags = [tag.get('name') for tag in model.get('tags', [])] tags = list(set(model_tags + tags)) - model["tags"] = [{"name": tag} for tag in tags] + model['tags'] = [{'name': tag} for tag in tags] except Exception as e: - log.debug(f"Error processing model tags: {e}") - model["tags"] = [] + log.debug(f'Error processing model tags: {e}') + model['tags'] = [] pass models.append(model) @@ -1496,23 +1500,125 @@ async def get_models( # Sort models by order list priority, with fallback for those not in the list models.sort( key=lambda model: ( - model_order_dict.get(model.get("id", ""), float("inf")), - (model.get("name", "") or ""), + model_order_dict.get(model.get('id', ''), float('inf')), + (model.get('name', '') or ''), ) ) - models = get_filtered_models(models, user) + models = await get_filtered_models(models, user) log.debug( - f"/api/models returned filtered models accessible to the user: {json.dumps([model.get('id') for model in models])}" + f'/api/models returned filtered models accessible to the user: {json.dumps([model.get("id") for model in models])}' ) - return {"data": models} + return {'data': models} -@app.get("/api/models/base") +@app.get('/api/models/base') async def get_base_models(request: Request, user=Depends(get_admin_user)): models = await get_all_base_models(request, user=user) - return {"data": models} + return {'data': models} + + +class ModelUnloadForm(BaseModel): + model: str + + +@app.post('/api/models/unload') +async def unload_model(request: Request, form_data: ModelUnloadForm, user=Depends(get_admin_user)): + """ + Unified model unload endpoint. + Resolves the provider that owns the model and calls its native unload mechanism. + Supports: Ollama (keep_alive=0) and llama.cpp (/models/unload). + """ + model_id = form_data.model + + # --- Ollama provider --- + ollama_models = getattr(request.app.state, 'OLLAMA_MODELS', None) or {} + if model_id in ollama_models: + url_indices = ollama_models[model_id].get('urls', []) + errors = [] + for idx in url_indices: + url = request.app.state.config.OLLAMA_BASE_URLS[idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), + ) + key = api_config.get('key', None) + + prefix_id = api_config.get('prefix_id', None) + actual_model = model_id + if prefix_id and actual_model.startswith(f'{prefix_id}.'): + actual_model = actual_model[len(f'{prefix_id}.') :] + + payload = json.dumps({'model': actual_model, 'keep_alive': 0, 'prompt': ''}) + + try: + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + headers = { + 'Content-Type': 'application/json', + **({'Authorization': f'Bearer {key}'} if key else {}), + } + async with session.post( + f'{url}/api/generate', + data=payload, + headers=headers, + ) as r: + if not r.ok: + errors.append({'url_idx': idx, 'error': await r.text()}) + except Exception as e: + log.exception(f'Failed to unload model on Ollama node {idx}: {e}') + errors.append({'url_idx': idx, 'error': str(e)}) + + if errors: + raise HTTPException( + status_code=500, + detail=f'Failed to unload model on {len(errors)} node(s): {errors}', + ) + return {'status': True} + + # --- OpenAI-compatible providers --- + openai_models = getattr(request.app.state, 'OPENAI_MODELS', None) or {} + if model_id in openai_models: + model_info = openai_models[model_id] + idx = model_info.get('urlIdx') + api_config = request.app.state.config.OPENAI_API_CONFIGS.get(str(idx), {}) + provider = api_config.get('provider', '') + base_url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = ( + request.app.state.config.OPENAI_API_KEYS[idx] if idx < len(request.app.state.config.OPENAI_API_KEYS) else '' + ) + + if provider == 'llama.cpp': + root_url = base_url.rstrip('/').removesuffix('/v1') + try: + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + headers = { + 'Content-Type': 'application/json', + **({'Authorization': f'Bearer {key}'} if key else {}), + } + async with session.post( + f'{root_url}/models/unload', + json={'model': model_id}, + headers=headers, + ) as r: + if not r.ok: + detail = await r.text() + raise HTTPException(status_code=r.status, detail=detail) + return await r.json() + except HTTPException: + raise + except Exception as e: + log.exception(f'Failed to unload model via llama.cpp: {e}') + raise HTTPException(status_code=500, detail=str(e)) + else: + raise HTTPException( + status_code=400, + detail=f'Provider "{provider or "default"}" does not support model unloading', + ) + + raise HTTPException(status_code=404, detail=f'Model "{model_id}" not found') ################################## @@ -1520,11 +1626,9 @@ async def get_base_models(request: Request, user=Depends(get_admin_user)): ################################## -@app.post("/api/embeddings") -@app.post("/api/v1/embeddings") # Experimental: Compatibility with OpenAI API -async def embeddings( - request: Request, form_data: dict, user=Depends(get_verified_user) -): +@app.post('/api/embeddings') +@app.post('/api/v1/embeddings') # Experimental: Compatibility with OpenAI API +async def embeddings(request: Request, form_data: dict, user=Depends(get_verified_user)): """ OpenAI-compatible embeddings endpoint. @@ -1547,8 +1651,8 @@ async def embeddings( return await generate_embeddings(request, form_data, user) -@app.post("/api/chat/completions") -@app.post("/api/v1/chat/completions") # Experimental: Compatibility with OpenAI API +@app.post('/api/chat/completions') +@app.post('/api/v1/chat/completions') # Experimental: Compatibility with OpenAI API async def chat_completion( request: Request, form_data: dict, @@ -1557,26 +1661,24 @@ async def chat_completion( if not request.app.state.MODELS: await get_all_models(request, user=user) - model_id = form_data.get("model", None) - model_item = form_data.pop("model_item", {}) - tasks = form_data.pop("background_tasks", None) + model_id = form_data.get('model', None) + model_item = form_data.pop('model_item', {}) + tasks = form_data.pop('background_tasks', None) metadata = {} try: model_info = None - if not model_item.get("direct", False): + if not model_item.get('direct', False): if model_id not in request.app.state.MODELS: - raise Exception("Model not found") + raise Exception('Model not found') model = request.app.state.MODELS[model_id] - model_info = Models.get_model_by_id(model_id) + model_info = await Models.get_model_by_id(model_id) # Check if user has access to the model - if not BYPASS_MODEL_ACCESS_CONTROL and ( - user.role != "admin" or not BYPASS_ADMIN_ACCESS_CONTROL - ): + if not BYPASS_MODEL_ACCESS_CONTROL and (user.role != 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL): try: - check_model_access(user, model) + await check_model_access(user, model) except Exception as e: raise e else: @@ -1585,220 +1687,549 @@ async def chat_completion( request.state.direct = True request.state.model = model - model_info_params = ( - model_info.params.model_dump() if model_info and model_info.params else {} - ) + # Model params: global defaults as base, per-model overrides win + default_model_params = getattr(request.app.state.config, 'DEFAULT_MODEL_PARAMS', None) or {} + model_info_params = { + **default_model_params, + **(model_info.params.model_dump() if model_info and model_info.params else {}), + } # Check base model existence for custom models - if model_info_params.get("base_model_id"): - base_model_id = model_info_params.get("base_model_id") + if model_info and model_info.base_model_id: + base_model_id = model_info.base_model_id if base_model_id not in request.app.state.MODELS: if ENABLE_CUSTOM_MODEL_FALLBACK: - default_models = ( - request.app.state.config.DEFAULT_MODELS or "" - ).split(",") + default_models = (request.app.state.config.DEFAULT_MODELS or '').split(',') - fallback_model_id = ( - default_models[0].strip() if default_models[0] else None - ) + fallback_model_id = default_models[0].strip() if default_models[0] else None - if fallback_model_id: - request.base_model_id = fallback_model_id + if fallback_model_id and fallback_model_id in request.app.state.MODELS: + # Update model and form_data so routing uses the fallback model's type + model = request.app.state.MODELS[fallback_model_id] + form_data['model'] = fallback_model_id else: - raise Exception("Model not found") + raise Exception('Model not found') else: - raise Exception("Model not found") + raise Exception('Model not found') # Chat Params - stream_delta_chunk_size = form_data.get("params", {}).get( - "stream_delta_chunk_size" - ) - reasoning_tags = form_data.get("params", {}).get("reasoning_tags") + stream_delta_chunk_size = form_data.get('params', {}).get('stream_delta_chunk_size') + reasoning_tags = form_data.get('params', {}).get('reasoning_tags') # Model Params - if model_info_params.get("stream_response") is not None: - form_data["stream"] = model_info_params.get("stream_response") + if model_info_params.get('stream_response') is not None: + form_data['stream'] = model_info_params.get('stream_response') - if model_info_params.get("stream_delta_chunk_size"): - stream_delta_chunk_size = model_info_params.get("stream_delta_chunk_size") + if model_info_params.get('stream_delta_chunk_size'): + stream_delta_chunk_size = model_info_params.get('stream_delta_chunk_size') - if model_info_params.get("reasoning_tags") is not None: - reasoning_tags = model_info_params.get("reasoning_tags") + if model_info_params.get('reasoning_tags') is not None: + reasoning_tags = model_info_params.get('reasoning_tags') + + # parent_id signals intent: + # null → new chat (root message, no parent) + # value → follow-up (user message's parentId = prev assistant) + # absent → legacy caller, no chat management + is_new_chat = 'parent_id' in form_data and form_data['parent_id'] is None and not form_data.get('chat_id') + parent_id = form_data.pop('parent_id', None) + form_data.pop('new_chat', None) # Legacy field + + # Multi-model: {model_id: assistant_message_id} + # Single-model fallback: built from 'model' + 'id' + message_ids = form_data.pop('message_ids', None) + if not message_ids: + message_ids = {model_id: form_data.pop('id', None)} + else: + form_data.pop('id', None) + + user_message = form_data.pop('user_message', None) or form_data.pop('parent_message', None) + + # Drop tool_servers if caller lacks features.direct_tool_servers — + # mirrors the storage-side strip in user/settings/update. + tool_servers = form_data.pop('tool_servers', None) + if ( + tool_servers + and user.role != 'admin' + and not await has_permission( + user.id, + 'features.direct_tool_servers', + request.app.state.config.USER_PERMISSIONS, + ) + ): + tool_servers = None metadata = { - "user_id": user.id, - "chat_id": form_data.pop("chat_id", None), - "message_id": form_data.pop("id", None), - "parent_message": form_data.pop("parent_message", None), - "parent_message_id": form_data.pop("parent_id", None), - "session_id": form_data.pop("session_id", None), - "filter_ids": form_data.pop("filter_ids", []), - "tool_ids": form_data.get("tool_ids", None), - "tool_servers": form_data.pop("tool_servers", None), - "files": form_data.get("files", None), - "features": form_data.get("features", {}), - "variables": form_data.get("variables", {}), - "model": model, - "direct": model_item.get("direct", False), - "params": { - "stream_delta_chunk_size": stream_delta_chunk_size, - "reasoning_tags": reasoning_tags, - "function_calling": ( - "native" + 'user_id': user.id, + 'chat_id': form_data.pop('chat_id', None) or '', + 'user_message': user_message, + 'user_message_id': user_message.get('id') if user_message else None, + 'assistant_message_id': form_data.pop('assistant_message_id', None), + 'session_id': form_data.pop('session_id', None), + 'folder_id': form_data.pop('folder_id', None), + 'filter_ids': form_data.pop('filter_ids', []), + 'tool_ids': form_data.get('tool_ids', None), + 'tool_servers': tool_servers, + 'files': form_data.get('files', None), + 'features': form_data.get('features', {}), + 'variables': form_data.get('variables', {}), + 'model': model, + 'direct': model_item.get('direct', False), + 'params': { + 'stream_delta_chunk_size': stream_delta_chunk_size, + 'reasoning_tags': reasoning_tags, + 'function_calling': ( + 'native' if ( - form_data.get("params", {}).get("function_calling") == "native" - or model_info_params.get("function_calling") == "native" + form_data.get('params', {}).get('function_calling') == 'native' + or model_info_params.get('function_calling') == 'native' ) - else "default" + else 'default' ), }, } - if metadata.get("chat_id") and user: - if not metadata["chat_id"].startswith( - "local:" - ): # temporary chats are not stored + if is_new_chat: + metadata['chat_id'] = str(uuid4()) - # Verify chat ownership - chat = Chats.get_chat_by_id_and_user_id(metadata["chat_id"], user.id) - if chat is None and user.role != "admin": # admins can access any chat - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=ERROR_MESSAGES.DEFAULT(), + if metadata.get('chat_id') and user: + chat_id = metadata['chat_id'] + if not chat_id.startswith('local:') and not chat_id.startswith( + 'channel:' + ): # temporary/channel chats are not stored + if is_new_chat: + # Build the full history upfront with ALL assistant placeholders + user_message = metadata.get('user_message') or {} + user_message_id = user_message.get('id') if user_message else None + + history_messages = {} + all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] + + if user_message_id and user_message: + user_message['childrenIds'] = all_assistant_ids + history_messages[user_message_id] = user_message + + for target_model_id, assistant_message_id in message_ids.items(): + if assistant_message_id: + history_messages[assistant_message_id] = { + 'id': assistant_message_id, + 'parentId': user_message_id, + 'childrenIds': [], + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': target_model_id, + 'timestamp': int(time.time()), + } + + await Chats.insert_new_chat( + chat_id, + user.id, + ChatForm( + chat={ + 'id': chat_id, + 'title': 'New Chat', + 'models': list(message_ids.keys()), + 'history': { + 'currentId': all_assistant_ids[0] if all_assistant_ids else user_message_id, + 'messages': history_messages, + }, + 'messages': [ + {'role': 'user', 'content': user_message.get('content', '')}, + ] + if user_message_id + else [], + 'files': metadata.get('files') or [], + 'tags': [], + 'timestamp': int(time.time() * 1000), + }, + folder_id=metadata.get('folder_id'), + ), ) - # Insert chat files from parent message if any - parent_message = metadata.get("parent_message") or {} - parent_message_files = parent_message.get("files", []) - if parent_message_files: - try: - Chats.insert_chat_files( - metadata["chat_id"], - parent_message.get("id"), - [ - file_item.get("id") - for file_item in parent_message_files - if file_item.get("type") == "file" - ], - user.id, + # Insert chat files from user message if any + user_message_files = user_message.get('files', []) + if user_message_files: + try: + await Chats.insert_chat_files( + chat_id, + user_message_id, + [ + file_item.get('id') + for file_item in user_message_files + if file_item.get('type') == 'file' + ], + user.id, + ) + except Exception as e: + log.debug(f'Error inserting chat files: {e}') + pass + else: + # Existing chat — verify ownership + if not await Chats.is_chat_owner(chat_id, user.id) and user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.DEFAULT(), ) - except Exception as e: - log.debug(f"Error inserting chat files: {e}") - pass + + # Persist chat-level files (knowledge collections, docs, etc.) + # The old frontend saveChatHandler did this on every message; + # now the backend owns persistence. + chat_files = metadata.get('files') + if chat_files is not None: + existing_chat = await Chats.get_chat_by_id(chat_id) + if existing_chat: + updated = {**existing_chat.chat, 'files': chat_files} + await Chats.update_chat_by_id(chat_id, updated) + + # Save user message to DB + user_message = metadata.get('user_message') or {} + if user_message and user_message.get('id'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + user_message['id'], + user_message, + ) + + # Link grandparent → user message (childrenIds) + grandparent_id = user_message.get('parentId') + if grandparent_id: + grandparent = await Chats.get_message_by_id_and_message_id(chat_id, grandparent_id) + if grandparent: + child_ids = grandparent.get('childrenIds', []) + if user_message['id'] not in child_ids: + child_ids.append(user_message['id']) + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, grandparent_id, {'childrenIds': child_ids} + ) + + # Insert chat files from user message if any + user_message_files = user_message.get('files', []) + if user_message_files: + try: + await Chats.insert_chat_files( + chat_id, + user_message.get('id'), + [ + file_item.get('id') + for file_item in user_message_files + if file_item.get('type') == 'file' + ], + user.id, + ) + except Exception as e: + log.debug(f'Error inserting chat files: {e}') + pass + + # Save ALL assistant placeholders + user_message_id = metadata.get('user_message_id') + all_assistant_ids = [assistant_id for assistant_id in message_ids.values() if assistant_id] + + # Link user message → all assistant messages (childrenIds) + if user_message_id and all_assistant_ids: + existing_user_message = await Chats.get_message_by_id_and_message_id(chat_id, user_message_id) + if existing_user_message: + child_ids = existing_user_message.get('childrenIds', []) + for assistant_id in all_assistant_ids: + if assistant_id not in child_ids: + child_ids.append(assistant_id) + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + user_message_id, + {'childrenIds': child_ids}, + ) + + # Save each assistant placeholder + for target_model_id, assistant_message_id in message_ids.items(): + if assistant_message_id: + await Chats.upsert_message_to_chat_by_id_and_message_id( + chat_id, + assistant_message_id, + { + 'id': assistant_message_id, + 'parentId': user_message_id, + 'childrenIds': [], + 'role': 'assistant', + 'content': '', + 'done': False, + 'model': target_model_id, + 'timestamp': int(time.time()), + }, + ) request.state.metadata = metadata - form_data["metadata"] = metadata + form_data['metadata'] = metadata + except HTTPException: + raise except Exception as e: - log.debug(f"Error processing chat metadata: {e}") + log.warning(f'Error processing chat metadata: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) - async def process_chat(request, form_data, user, metadata, model): + async def process_chat(request, form_data, user, metadata, model, tasks=None): try: - form_data, metadata, events = await process_chat_payload( - request, form_data, user, metadata, model - ) + form_data, metadata, events = await process_chat_payload(request, form_data, user, metadata, model) response = await chat_completion_handler(request, form_data, user) - if metadata.get("chat_id") and metadata.get("message_id"): - try: - if not metadata["chat_id"].startswith("local:"): - Chats.upsert_message_to_chat_by_id_and_message_id( - metadata["chat_id"], - metadata["message_id"], - { - "parentId": metadata.get("parent_message_id", None), - "model": model_id, - }, - ) - except: - pass - return await process_chat_response( - request, response, form_data, user, metadata, model, events, tasks - ) + # When the upstream provider returns an error (e.g. HTTP 400 + # content-filter, quota exceeded), generate_chat_completion + # returns a JSONResponse instead of raising. Detect this and + # raise so the except-block below emits chat:message:error + + # chat:tasks:cancel, unblocking the frontend. + if isinstance(response, JSONResponse) and response.status_code >= 400: + try: + error_body = json.loads(response.body.decode('utf-8', 'replace')) + detail = error_body.get('error', error_body) if isinstance(error_body, dict) else error_body + if isinstance(detail, dict): + detail = detail.get('message', detail.get('detail', str(detail))) + except Exception: + detail = f'Provider returned HTTP {response.status_code}' + raise Exception(detail) + + ctx = await build_chat_response_context(request, form_data, user, model, metadata, tasks, events) + + return await process_chat_response(response, ctx) except asyncio.CancelledError: - log.info("Chat processing was cancelled") + log.info('Chat processing was cancelled') try: - event_emitter = get_event_emitter(metadata) - await asyncio.shield( - event_emitter( - {"type": "chat:tasks:cancel"}, - ) - ) - except Exception as e: + + async def emit_cancel_event(): + event_emitter = await get_event_emitter(metadata) + if event_emitter: + await event_emitter({'type': 'chat:tasks:cancel'}) + + await asyncio.shield(emit_cancel_event()) + except Exception: pass - finally: - raise # re-raise to ensure proper task cancellation handling + raise # re-raise to ensure proper task cancellation handling except Exception as e: - log.debug(f"Error processing chat payload: {e}") - if metadata.get("chat_id") and metadata.get("message_id"): + error_detail = e.detail if isinstance(e, HTTPException) else str(e) + log.error('Error processing chat payload: %s', error_detail) + if metadata.get('chat_id') and metadata.get('message_id'): # Update the chat message with the error try: - if not metadata["chat_id"].startswith("local:"): - Chats.upsert_message_to_chat_by_id_and_message_id( - metadata["chat_id"], - metadata["message_id"], + if not metadata.get('chat_id', '').startswith('local:') and not metadata.get('chat_id', '').startswith('channel:'): + await Chats.upsert_message_to_chat_by_id_and_message_id( + metadata['chat_id'], + metadata['message_id'], { - "parentId": metadata.get("parent_message_id", None), - "error": {"content": str(e)}, + 'parentId': metadata.get('user_message_id', None), + 'error': {'content': error_detail}, }, ) - event_emitter = get_event_emitter(metadata) - await event_emitter( - { - "type": "chat:message:error", - "data": {"error": {"content": str(e)}}, - } - ) - await event_emitter( - {"type": "chat:tasks:cancel"}, - ) + event_emitter = await get_event_emitter(metadata) + if event_emitter: + await event_emitter( + { + 'type': 'chat:message:error', + 'data': {'error': {'content': error_detail}}, + } + ) + await event_emitter( + {'type': 'chat:tasks:cancel'}, + ) - except: + except Exception: pass + else: + # No chat_id/message_id → legacy/direct API path with no + # WebSocket error channel. We must surface the error as + # a proper HTTP response; without this the function would + # return None which FastAPI serializes as null. #23924 + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=error_detail, + ) finally: + # Clean up MCP clients. Each client is isolated so one + # failure doesn't skip the rest. + # + # NOTE: asyncio.wait_for() / asyncio.shield() must NOT be used + # here — they create new asyncio Tasks, which violate anyio + # cancel-scope task-ownership rules when the MCPClient's + # exit_stack contains anyio transport resources (streamable_http). + # Exiting those cancel scopes from the wrong task raises + # "Attempted to exit a cancel scope that isn't the current + # task's current cancel scope", which propagates as a + # BaseException through the finally block, discards the response + # return value, and surfaces as a 500 "No response returned." + # MCPClient.disconnect() already catches BaseException internally. try: - if mcp_clients := metadata.get("mcp_clients"): - for client in reversed(mcp_clients.values()): - await client.disconnect() - except Exception as e: - log.debug(f"Error cleaning up: {e}") + if mcp_clients := metadata.get('mcp_clients'): + for client in reversed(list(mcp_clients.values())): + try: + await client.disconnect() + except BaseException as e: + log.debug(f'Error disconnecting MCP client: {e}') + except BaseException as e: + log.debug(f'Error cleaning up MCP clients: {e}') + + # Deregister this task, then emit chat:active=false if no others remain + try: + chat_id = metadata.get('chat_id') + task_id = metadata.get('task_id') + if chat_id and task_id: + await cleanup_task(request.app.state.redis, task_id, chat_id) + if not await has_active_tasks(request.app.state.redis, chat_id): + event_emitter = await get_event_emitter(metadata, update_db=False) + if event_emitter: + try: + await asyncio.shield(event_emitter({'type': 'chat:active', 'data': {'active': False}})) + except asyncio.CancelledError: + pass + except Exception: pass - if ( - metadata.get("session_id") - and metadata.get("chat_id") - and metadata.get("message_id") - ): - # Asynchronous Chat Processing - task_id, _ = await create_task( - request.app.state.redis, - process_chat(request, form_data, user, metadata, model), - id=metadata["chat_id"], - ) - return {"status": True, "task_id": task_id} + # Fan out: one task per model + if metadata.get('session_id') and metadata.get('chat_id'): + task_ids = [] + chat_id = metadata['chat_id'] + + for idx, (target_model_id, assistant_message_id) in enumerate(message_ids.items()): + if not assistant_message_id: + continue + + # Per-model metadata: own message_id + model + per_model_metadata = { + **metadata, + 'message_id': assistant_message_id, + } + + # Per-model form_data: own model + model_form_data = { + **form_data, + 'model': target_model_id, + 'metadata': per_model_metadata, + } + + # Resolve the model object for this specific model + resolved_model = request.app.state.MODELS.get(target_model_id, model) + + # Only the first model runs title/tags generation; + # subsequent models only run follow-ups. + task_id, _ = await create_task( + request.app.state.redis, + process_chat( + request, + model_form_data, + user, + per_model_metadata, + resolved_model, + tasks + if idx == 0 + else { + k: v + for k, v in (tasks or {}).items() + if k not in (TASKS.TITLE_GENERATION, TASKS.TAGS_GENERATION) + } + or None, + ), + id=chat_id, + ) + per_model_metadata['task_id'] = task_id + task_ids.append(task_id) + + # Emit chat:active=true + if task_ids: + event_emitter = await get_event_emitter( + {**metadata, 'message_id': list(message_ids.values())[0]}, + update_db=False, + ) + if event_emitter: + await event_emitter({'type': 'chat:active', 'data': {'active': True}}) + + return { + 'status': True, + 'task_ids': task_ids, + 'chat_id': chat_id, + } else: - return await process_chat(request, form_data, user, metadata, model) + # Legacy/direct: single model, synchronous + metadata['message_id'] = list(message_ids.values())[0] + return await process_chat(request, form_data, user, metadata, model, tasks) # Alias for chat_completion (Legacy) generate_chat_completions = chat_completion generate_chat_completion = chat_completion +# Expose as app.state so internal callers (e.g. automations) can +# use the full pipeline without importing from main.py (avoids circular deps). +app.state.CHAT_COMPLETION_HANDLER = chat_completion -@app.post("/api/chat/completed") -async def chat_completed( - request: Request, form_data: dict, user=Depends(get_verified_user) + +################################## +# +# Anthropic Messages API Compatible Endpoint +# +################################## + + +from open_webui.utils.anthropic import ( + convert_anthropic_to_openai_payload, + convert_openai_to_anthropic_response, + openai_stream_to_anthropic_stream, +) + + +@app.post('/api/message') +@app.post('/api/v1/messages') # Anthropic Messages API compatible endpoint +async def generate_messages( + request: Request, + form_data: dict, + user=Depends(get_verified_user), ): - try: - model_item = form_data.pop("model_item", {}) + """ + Anthropic Messages API compatible endpoint. - if model_item.get("direct", False): + Accepts the Anthropic Messages API format, converts internally to OpenAI + Chat Completions format, routes through the existing chat completion + pipeline, then converts the response back to Anthropic Messages format. + + Supports both streaming and non-streaming requests. + All models configured in Open WebUI are accessible via this endpoint. + + Authentication: Supports both standard Authorization header and + Anthropic's x-api-key header (via middleware translation). + """ + # Convert Anthropic payload to OpenAI format + requested_model = form_data.get('model', '') + + openai_payload = convert_anthropic_to_openai_payload(form_data) + + # Route through the existing chat_completion handler + response = await chat_completion(request, openai_payload, user) + + # Convert response back to Anthropic format + if isinstance(response, StreamingResponse): + # Streaming response: wrap the generator to convert SSE format + return StreamingResponse( + openai_stream_to_anthropic_stream(response.body_iterator, model=requested_model), + media_type='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + ) + elif isinstance(response, dict): + return convert_openai_to_anthropic_response(response, model=requested_model) + else: + # Passthrough for error responses (JSONResponse, PlainTextResponse, etc.) + return response + + +@app.post('/api/chat/completed') +async def chat_completed(request: Request, form_data: dict, user=Depends(get_verified_user)): + """Deprecated: outlet filters now run inline during chat completion. + Kept for backward compatibility with external integrations.""" + try: + model_item = form_data.pop('model_item', {}) + + if model_item.get('direct', False): request.state.direct = True request.state.model = model_item @@ -1810,14 +2241,12 @@ async def chat_completed( ) -@app.post("/api/chat/actions/{action_id}") -async def chat_action( - request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user) -): +@app.post('/api/chat/actions/{action_id}') +async def chat_action(request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user)): try: - model_item = form_data.pop("model_item", {}) + model_item = form_data.pop('model_item', {}) - if model_item.get("direct", False): + if model_item.get('direct', False): request.state.direct = True request.state.model = model_item @@ -1829,10 +2258,8 @@ async def chat_action( ) -@app.post("/api/tasks/stop/{task_id}") -async def stop_task_endpoint( - request: Request, task_id: str, user=Depends(get_verified_user) -): +@app.post('/api/tasks/stop/{task_id}') +async def stop_task_endpoint(request: Request, task_id: str, user=Depends(get_admin_user)): try: result = await stop_task(request.app.state.redis, task_id) return result @@ -1840,23 +2267,42 @@ async def stop_task_endpoint( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) -@app.get("/api/tasks") -async def list_tasks_endpoint(request: Request, user=Depends(get_verified_user)): - return {"tasks": await list_tasks(request.app.state.redis)} +@app.get('/api/tasks') +async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)): + return {'tasks': await list_tasks(request.app.state.redis)} -@app.get("/api/tasks/chat/{chat_id}") -async def list_tasks_by_chat_id_endpoint( - request: Request, chat_id: str, user=Depends(get_verified_user) -): - chat = Chats.get_chat_by_id(chat_id) - if chat is None or chat.user_id != user.id: - return {"task_ids": []} +@app.get('/api/tasks/chat/{chat_id:path}') +async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:') or chat_id.startswith('channel:'): + socket_id = chat_id[len('local:') :] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + return {'task_ids': []} + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + return {'task_ids': []} task_ids = await list_task_ids_by_item_id(request.app.state.redis, chat_id) - log.debug(f"Task IDs for chat {chat_id}: {task_ids}") - return {"task_ids": task_ids} + log.debug(f'Task IDs for chat {chat_id}: {task_ids}') + return {'task_ids': task_ids} + + +@app.post('/api/tasks/chat/{chat_id:path}/stop') +async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)): + if chat_id.startswith('local:') or chat_id.startswith('channel:'): + socket_id = chat_id[len('local:') :] + owner_id = get_user_id_from_session_pool(socket_id) + if owner_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + else: + chat = await Chats.get_chat_by_id(chat_id) + if chat is None or (chat.user_id != user.id and user.role != 'admin'): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + result = await stop_item_tasks(request.app.state.redis, chat_id) + return result ################################## @@ -1866,19 +2312,19 @@ async def list_tasks_by_chat_id_endpoint( ################################## -@app.get("/api/config") +@app.get('/api/config') async def get_app_config(request: Request): user = None token = None - auth_header = request.headers.get("Authorization") + auth_header = request.headers.get('Authorization') if auth_header: cred = get_http_authorization_cred(auth_header) if cred: token = cred.credentials - if not token and "token" in request.cookies: - token = request.cookies.get("token") + if not token and 'token' in request.cookies: + token = request.cookies.get('token') if token: try: @@ -1887,65 +2333,67 @@ async def get_app_config(request: Request): log.debug(e) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token", + detail='Invalid token', ) - if data is not None and "id" in data: - user = Users.get_user_by_id(data["id"]) + if data is not None and 'id' in data: + user = await Users.get_user_by_id(data['id']) - user_count = Users.get_num_users() onboarding = False - if user is None: - onboarding = user_count == 0 + onboarding = not await Users.has_users() + + user_count = await Users.get_num_users() if app.state.LICENSE_METADATA else None return { - **({"onboarding": True} if onboarding else {}), - "status": True, - "name": app.state.WEBUI_NAME, - "version": VERSION, - "default_locale": str(DEFAULT_LOCALE), - "oauth": { - "providers": { - name: config.get("name", name) - for name, config in OAUTH_PROVIDERS.items() - } - }, - "features": { - "auth": WEBUI_AUTH, - "auth_trusted_header": bool(app.state.AUTH_TRUSTED_EMAIL_HEADER), - "enable_signup_password_confirmation": ENABLE_SIGNUP_PASSWORD_CONFIRMATION, - "enable_ldap": app.state.config.ENABLE_LDAP, - "enable_api_keys": app.state.config.ENABLE_API_KEYS, - "enable_signup": app.state.config.ENABLE_SIGNUP, - "enable_login_form": app.state.config.ENABLE_LOGIN_FORM, - "enable_websocket": ENABLE_WEBSOCKET_SUPPORT, - "enable_version_update_check": ENABLE_VERSION_UPDATE_CHECK, - "enable_public_active_users_count": ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + **({'onboarding': True} if onboarding else {}), + 'status': True, + 'name': app.state.WEBUI_NAME, + 'version': VERSION, + 'default_locale': str(DEFAULT_LOCALE), + 'oauth': {'providers': {name: config.get('name', name) for name, config in OAUTH_PROVIDERS.items()}}, + 'features': { + # --- Public: required by login/signup page pre-auth --- + 'auth': WEBUI_AUTH, + 'auth_trusted_header': bool(app.state.AUTH_TRUSTED_EMAIL_HEADER), + 'enable_signup_password_confirmation': ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + 'enable_ldap': app.state.config.ENABLE_LDAP, + 'enable_signup': app.state.config.ENABLE_SIGNUP, + 'enable_login_form': app.state.config.ENABLE_LOGIN_FORM, + 'enable_websocket': ENABLE_WEBSOCKET_SUPPORT, + # --- Authenticated: only consumed by logged-in frontend --- **( { - "enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS, - "enable_folders": app.state.config.ENABLE_FOLDERS, - "folder_max_file_count": app.state.config.FOLDER_MAX_FILE_COUNT, - "enable_channels": app.state.config.ENABLE_CHANNELS, - "enable_notes": app.state.config.ENABLE_NOTES, - "enable_web_search": app.state.config.ENABLE_WEB_SEARCH, - "enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION, - "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER, - "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION, - "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, - "enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING, - "enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING, - "enable_user_webhooks": app.state.config.ENABLE_USER_WEBHOOKS, - "enable_user_status": app.state.config.ENABLE_USER_STATUS, - "enable_admin_export": ENABLE_ADMIN_EXPORT, - "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, - "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, - "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, - "enable_memories": app.state.config.ENABLE_MEMORIES, + 'enable_api_keys': app.state.config.ENABLE_API_KEYS, + 'enable_password_change_form': app.state.config.ENABLE_PASSWORD_CHANGE_FORM, + 'enable_version_update_check': ENABLE_VERSION_UPDATE_CHECK, + 'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + 'enable_easter_eggs': ENABLE_EASTER_EGGS, + 'enable_direct_connections': app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'enable_folders': app.state.config.ENABLE_FOLDERS, + 'folder_max_file_count': app.state.config.FOLDER_MAX_FILE_COUNT, + 'enable_channels': app.state.config.ENABLE_CHANNELS, + 'enable_calendar': app.state.config.ENABLE_CALENDAR, + 'enable_automations': app.state.config.ENABLE_AUTOMATIONS, + 'enable_notes': app.state.config.ENABLE_NOTES, + 'enable_web_search': app.state.config.ENABLE_WEB_SEARCH, + 'enable_code_execution': app.state.config.ENABLE_CODE_EXECUTION, + 'enable_code_interpreter': app.state.config.ENABLE_CODE_INTERPRETER, + 'enable_image_generation': app.state.config.ENABLE_IMAGE_GENERATION, + 'enable_autocomplete_generation': app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'enable_community_sharing': app.state.config.ENABLE_COMMUNITY_SHARING, + 'enable_message_rating': app.state.config.ENABLE_MESSAGE_RATING, + 'enable_user_webhooks': app.state.config.ENABLE_USER_WEBHOOKS, + 'enable_user_status': app.state.config.ENABLE_USER_STATUS, + 'enable_admin_export': ENABLE_ADMIN_EXPORT, + 'enable_admin_chat_access': ENABLE_ADMIN_CHAT_ACCESS, + 'enable_admin_analytics': ENABLE_ADMIN_ANALYTICS, + 'enable_google_drive_integration': app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'enable_onedrive_integration': app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + 'enable_memories': app.state.config.ENABLE_MEMORIES, **( { - "enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL, - "enable_onedrive_business": ENABLE_ONEDRIVE_BUSINESS, + 'enable_onedrive_personal': ENABLE_ONEDRIVE_PERSONAL, + 'enable_onedrive_business': ENABLE_ONEDRIVE_BUSINESS, } if app.state.config.ENABLE_ONEDRIVE_INTEGRATION else {} @@ -1957,77 +2405,75 @@ async def get_app_config(request: Request): }, **( { - "default_models": app.state.config.DEFAULT_MODELS, - "default_pinned_models": app.state.config.DEFAULT_PINNED_MODELS, - "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS, - "user_count": user_count, - "code": { - "engine": app.state.config.CODE_EXECUTION_ENGINE, + 'default_models': app.state.config.DEFAULT_MODELS, + 'default_pinned_models': app.state.config.DEFAULT_PINNED_MODELS, + 'default_prompt_suggestions': app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + **({'user_count': user_count} if user_count is not None else {}), + 'code': { + 'engine': app.state.config.CODE_EXECUTION_ENGINE, + 'interpreter_engine': app.state.config.CODE_INTERPRETER_ENGINE, }, - "audio": { - "tts": { - "engine": app.state.config.TTS_ENGINE, - "voice": app.state.config.TTS_VOICE, - "split_on": app.state.config.TTS_SPLIT_ON, + 'audio': { + 'tts': { + 'engine': app.state.config.TTS_ENGINE, + 'voice': app.state.config.TTS_VOICE, + 'split_on': app.state.config.TTS_SPLIT_ON, }, - "stt": { - "engine": app.state.config.STT_ENGINE, + 'stt': { + 'engine': app.state.config.STT_ENGINE, }, }, - "file": { - "max_size": app.state.config.FILE_MAX_SIZE, - "max_count": app.state.config.FILE_MAX_COUNT, - "image_compression": { - "width": app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, - "height": app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + 'file': { + 'max_size': app.state.config.FILE_MAX_SIZE, + 'max_count': app.state.config.FILE_MAX_COUNT, + 'image_compression': { + 'width': app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'height': app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, }, }, - "permissions": {**app.state.config.USER_PERMISSIONS}, - "google_drive": { - "client_id": GOOGLE_DRIVE_CLIENT_ID.value, - "api_key": GOOGLE_DRIVE_API_KEY.value, + 'permissions': {**app.state.config.USER_PERMISSIONS}, + 'google_drive': { + 'client_id': GOOGLE_DRIVE_CLIENT_ID.value, + 'api_key': GOOGLE_DRIVE_API_KEY.value, }, - "onedrive": { - "client_id_personal": ONEDRIVE_CLIENT_ID_PERSONAL, - "client_id_business": ONEDRIVE_CLIENT_ID_BUSINESS, - "sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value, - "sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value, + 'onedrive': { + 'client_id_personal': ONEDRIVE_CLIENT_ID_PERSONAL, + 'client_id_business': ONEDRIVE_CLIENT_ID_BUSINESS, + 'sharepoint_url': ONEDRIVE_SHAREPOINT_URL.value, + 'sharepoint_tenant_id': ONEDRIVE_SHAREPOINT_TENANT_ID.value, }, - "ui": { - "pending_user_overlay_title": app.state.config.PENDING_USER_OVERLAY_TITLE, - "pending_user_overlay_content": app.state.config.PENDING_USER_OVERLAY_CONTENT, - "response_watermark": app.state.config.RESPONSE_WATERMARK, + 'ui': { + 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, + 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'response_watermark': app.state.config.RESPONSE_WATERMARK, + 'iframe_csp': IFRAME_CSP, }, - "license_metadata": app.state.LICENSE_METADATA, + 'license_metadata': app.state.LICENSE_METADATA, **( { - "active_entries": app.state.USER_COUNT, + 'active_entries': app.state.USER_COUNT, } - if user.role == "admin" + if user.role == 'admin' else {} ), } - if user is not None and (user.role in ["admin", "user"]) + if user is not None and (user.role in ['admin', 'user']) else { **( { - "ui": { - "pending_user_overlay_title": app.state.config.PENDING_USER_OVERLAY_TITLE, - "pending_user_overlay_content": app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'ui': { + 'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE, + 'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT, } } - if user and user.role == "pending" + if user and user.role == 'pending' else {} ), **( { - "metadata": { - "login_footer": app.state.LICENSE_METADATA.get( - "login_footer", "" - ), - "auth_logo_position": app.state.LICENSE_METADATA.get( - "auth_logo_position", "" - ), + 'metadata': { + 'login_footer': app.state.LICENSE_METADATA.get('login_footer', ''), + 'auth_logo_position': app.state.LICENSE_METADATA.get('auth_logo_position', ''), } } if app.state.LICENSE_METADATA @@ -2042,58 +2488,56 @@ class UrlForm(BaseModel): url: str -@app.get("/api/webhook") +@app.get('/api/webhook') async def get_webhook_url(user=Depends(get_admin_user)): return { - "url": app.state.config.WEBHOOK_URL, + 'url': app.state.config.WEBHOOK_URL, } -@app.post("/api/webhook") +@app.post('/api/webhook') async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): app.state.config.WEBHOOK_URL = form_data.url app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL - return {"url": app.state.config.WEBHOOK_URL} + return {'url': app.state.config.WEBHOOK_URL} -@app.get("/api/version") +@app.get('/api/version') async def get_app_version(): return { - "version": VERSION, - "deployment_id": DEPLOYMENT_ID, + 'version': VERSION, + 'deployment_id': DEPLOYMENT_ID, } -@app.get("/api/version/updates") +@app.get('/api/version/updates') async def get_app_latest_release_version(user=Depends(get_verified_user)): if not ENABLE_VERSION_UPDATE_CHECK: - log.debug( - f"Version update check is disabled, returning current version as latest version" - ) - return {"current": VERSION, "latest": VERSION} + log.debug(f'Version update check is disabled, returning current version as latest version') + return {'current': VERSION, 'latest': VERSION} try: timeout = aiohttp.ClientTimeout(total=1) async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with session.get( - "https://api.github.com/repos/open-webui/open-webui/releases/latest", + 'https://api.github.com/repos/open-webui/open-webui/releases/latest', ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: response.raise_for_status() data = await response.json() - latest_version = data["tag_name"] + latest_version = data['tag_name'] - return {"current": VERSION, "latest": latest_version[1:]} + return {'current': VERSION, 'latest': latest_version[1:]} except Exception as e: log.debug(e) - return {"current": VERSION, "latest": VERSION} + return {'current': VERSION, 'latest': VERSION} -@app.get("/api/changelog") +@app.get('/api/changelog') async def get_app_changelog(): return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5} -@app.get("/api/usage") +@app.get('/api/usage') async def get_current_usage(user=Depends(get_verified_user)): """ Get current usage statistics for Open WebUI. @@ -2101,139 +2545,150 @@ async def get_current_usage(user=Depends(get_verified_user)): """ try: # If public visibility is disabled, only allow admins to access this endpoint - if not ENABLE_PUBLIC_ACTIVE_USERS_COUNT and user.role != "admin": + if not ENABLE_PUBLIC_ACTIVE_USERS_COUNT and user.role != 'admin': raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Access denied. Only administrators can view usage statistics.", + detail='Access denied. Only administrators can view usage statistics.', ) return { - "model_ids": get_models_in_use(), - "user_count": Users.get_active_user_count(), + 'model_ids': get_models_in_use(), + 'user_count': await Users.get_active_user_count(), } except HTTPException: raise except Exception as e: - log.error(f"Error getting usage statistics: {e}") - raise HTTPException(status_code=500, detail="Internal Server Error") + log.error(f'Error getting usage statistics: {e}') + raise HTTPException(status_code=500, detail='Internal Server Error') -############################ -# OAuth Login & Callback -############################ +# --- OAuth Login & Callback --- # Initialize OAuth client manager with any MCP tool servers using OAuth 2.1 if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0: for tool_server_connection in app.state.config.TOOL_SERVER_CONNECTIONS: - if tool_server_connection.get("type", "openapi") == "mcp": - server_id = tool_server_connection.get("info", {}).get("id") - auth_type = tool_server_connection.get("auth_type", "none") - - if server_id and auth_type == "oauth_2.1": - oauth_client_info = tool_server_connection.get("info", {}).get( - "oauth_client_info", "" - ) + if tool_server_connection.get('type', 'openapi') == 'mcp': + server_id = tool_server_connection.get('info', {}).get('id') + auth_type = tool_server_connection.get('auth_type', 'none') + if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'): try: - oauth_client_info = decrypt_data(oauth_client_info) + oauth_client_info = resolve_oauth_client_info(tool_server_connection) app.state.oauth_client_manager.add_client( - f"mcp:{server_id}", + f'mcp:{server_id}', OAuthClientInformationFull(**oauth_client_info), ) except Exception as e: - log.error( - f"Error adding OAuth client for MCP tool server {server_id}: {e}" - ) + log.error(f'Error adding OAuth client for MCP tool server {server_id}: {e}') pass try: if ENABLE_STAR_SESSIONS_MIDDLEWARE: redis_session_store = RedisStore( url=REDIS_URL, - prefix=(f"{REDIS_KEY_PREFIX}:session:" if REDIS_KEY_PREFIX else "session:"), + prefix=(f'{REDIS_KEY_PREFIX}:session:' if REDIS_KEY_PREFIX else 'session:'), ) app.add_middleware(SessionAutoloadMiddleware) app.add_middleware( StarSessionsMiddleware, store=redis_session_store, - cookie_name="owui-session", + cookie_name='owui-session', cookie_same_site=WEBUI_SESSION_COOKIE_SAME_SITE, cookie_https_only=WEBUI_SESSION_COOKIE_SECURE, ) - log.info("Using Redis for session") + log.info('Using Redis for session') else: - raise ValueError("No Redis URL provided") + raise ValueError('No Redis URL provided') except Exception as e: app.add_middleware( SessionMiddleware, secret_key=WEBUI_SECRET_KEY, - session_cookie="owui-session", + session_cookie='owui-session', same_site=WEBUI_SESSION_COOKIE_SAME_SITE, https_only=WEBUI_SESSION_COOKIE_SECURE, ) async def register_client(request, client_id: str) -> bool: - server_type, server_id = client_id.split(":", 1) + server_type, server_id = client_id.split(':', 1) connection = None connection_idx = None for idx, conn in enumerate(request.app.state.config.TOOL_SERVER_CONNECTIONS or []): - if conn.get("type", "openapi") == server_type: - info = conn.get("info", {}) - if info.get("id") == server_id: + if conn.get('type', 'openapi') == server_type: + info = conn.get('info', {}) + if info.get('id') == server_id: connection = conn connection_idx = idx break if connection is None or connection_idx is None: - log.warning( - f"Unable to locate MCP tool server configuration for client {client_id} during re-registration" - ) + log.warning(f'Unable to locate MCP tool server configuration for client {client_id} during re-registration') return False - server_url = connection.get("url") - oauth_server_key = (connection.get("config") or {}).get("oauth_server_key") + server_url = connection.get('url') + auth_type = connection.get('auth_type', 'none') + oauth_server_key = (connection.get('config') or {}).get('oauth_server_key') try: - oauth_client_info = ( - await get_oauth_client_info_with_dynamic_client_registration( + if auth_type == 'oauth_2.1_static': + # Static credentials: rebuild from admin-provided credentials + fresh metadata + info = connection.get('info', {}) + oauth_client_id = info.get('oauth_client_id') or '' + oauth_client_secret = info.get('oauth_client_secret') or '' + if not oauth_client_id or not oauth_client_secret: + # Fall back to blob for backward compatibility + existing_client_info = info.get('oauth_client_info', '') + if not existing_client_info: + log.error(f'No stored OAuth client info for static client {client_id}') + return False + existing_data = decrypt_data(existing_client_info) + oauth_client_id = oauth_client_id or existing_data.get('client_id', '') + oauth_client_secret = oauth_client_secret or existing_data.get('client_secret', '') + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + client_id, + server_url, + oauth_client_id=oauth_client_id, + oauth_client_secret=oauth_client_secret, + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( request, client_id, server_url, oauth_server_key, ) - ) except Exception as e: - log.error(f"Dynamic client re-registration failed for {client_id}: {e}") + log.error(f'OAuth client re-registration failed for {client_id}: {e}') return False try: - request.app.state.config.TOOL_SERVER_CONNECTIONS[connection_idx] = { + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + connections[connection_idx] = { **connection, - "info": { - **connection.get("info", {}), - "oauth_client_info": encrypt_data( - oauth_client_info.model_dump(mode="json") - ), + 'info': { + **connection.get('info', {}), + 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), }, } + # Re-assign the full list to trigger AppConfig.__setattr__ → ConfigVar.save() + # (in-place list mutation via list[idx] = ... does not trigger __setattr__) + request.app.state.config.TOOL_SERVER_CONNECTIONS = connections except Exception as e: - log.error( - f"Failed to persist updated OAuth client info for tool server {client_id}: {e}" - ) + log.error(f'Failed to persist updated OAuth client info for tool server {client_id}: {e}') return False oauth_client_manager.remove_client(client_id) oauth_client_manager.add_client(client_id, oauth_client_info) - log.info(f"Re-registered OAuth client {client_id} for tool server") + log.info(f'Re-registered OAuth client {client_id} for tool server') return True -@app.get("/oauth/clients/{client_id}/authorize") +@app.get('/oauth/clients/{client_id}/authorize') async def oauth_client_authorize( client_id: str, request: Request, @@ -2248,7 +2703,7 @@ async def oauth_client_authorize( if not await oauth_client_manager._preflight_authorization_url(client, client_info): log.info( - "Detected invalid OAuth client %s; attempting re-registration", + 'Detected invalid OAuth client %s; attempting re-registration', client_id, ) @@ -2256,7 +2711,7 @@ async def oauth_client_authorize( if not registered: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to re-register OAuth client", + detail='Failed to re-register OAuth client', ) client = oauth_client_manager.get_client(client_id) @@ -2264,21 +2719,19 @@ async def oauth_client_authorize( if client is None or client_info is None: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="OAuth client unavailable after re-registration", + detail='OAuth client unavailable after re-registration', ) - if not await oauth_client_manager._preflight_authorization_url( - client, client_info - ): + if not await oauth_client_manager._preflight_authorization_url(client, client_info): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="OAuth client registration is still invalid after re-registration", + detail='OAuth client registration is still invalid after re-registration', ) return await oauth_client_manager.handle_authorize(request, client_id=client_id) -@app.get("/oauth/clients/{client_id}/callback") +@app.get('/oauth/clients/{client_id}/callback') async def oauth_client_callback( client_id: str, request: Request, @@ -2293,63 +2746,87 @@ async def oauth_client_callback( ) -@app.get("/oauth/{provider}/login") +@app.get('/oauth/{provider}/login') async def oauth_login(provider: str, request: Request): return await oauth_manager.handle_login(request, provider) -# OAuth login logic is as follows: -# 1. Attempt to find a user with matching subject ID, tied to the provider -# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth -# - This is considered insecure in general, as OAuth providers do not always verify email addresses -# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user -# - Email addresses are considered unique, so we fail registration if the email address is already taken -@app.get("/oauth/{provider}/login/callback") -@app.get("/oauth/{provider}/callback") # Legacy endpoint +@app.get('/oauth/{provider}/login/callback') +@app.get('/oauth/{provider}/callback') # Legacy endpoint async def oauth_login_callback( provider: str, request: Request, response: Response, - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): + """Handle the OAuth provider callback. + + Resolution order: + 1. Match by subject ID bound to the provider. + 2. If ``OAUTH_MERGE_ACCOUNTS_BY_EMAIL`` is enabled, match by email + (note: some providers do not verify email addresses). + 3. If no match and ``ENABLE_OAUTH_SIGNUP`` is enabled, create a new user + (fails if the email is already registered). + """ return await oauth_manager.handle_callback(request, provider, response, db=db) -@app.get("/manifest.json") +############################ +# OIDC Back-Channel Logout +############################ + + +@app.post('/oauth/backchannel-logout') +async def oauth_backchannel_logout( + request: Request, + db: AsyncSession = Depends(get_async_session), +): + if not ENABLE_OAUTH_BACKCHANNEL_LOGOUT: + raise HTTPException(status_code=404) + return await oauth_manager.handle_backchannel_logout(request, db=db) + + +@app.get('/manifest.json') async def get_manifest_json(): if app.state.EXTERNAL_PWA_MANIFEST_URL: - return requests.get(app.state.EXTERNAL_PWA_MANIFEST_URL).json() + session = await get_session() + async with session.get( + app.state.EXTERNAL_PWA_MANIFEST_URL, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return await r.json() else: return { - "name": app.state.WEBUI_NAME, - "short_name": app.state.WEBUI_NAME, - "description": f"{app.state.WEBUI_NAME} is an open, extensible, user-friendly interface for AI that adapts to your workflow.", - "start_url": "/", - "display": "standalone", - "background_color": "#343541", - "icons": [ + 'name': app.state.WEBUI_NAME, + 'short_name': app.state.WEBUI_NAME, + 'description': f'{app.state.WEBUI_NAME} is an open, extensible, user-friendly interface for AI that adapts to your workflow.', + 'start_url': '/', + 'display': 'standalone', + 'background_color': '#343541', + 'icons': [ { - "src": "/static/logo.png", - "type": "image/png", - "sizes": "500x500", - "purpose": "any", + 'src': '/static/logo.png', + 'type': 'image/png', + 'sizes': '500x500', + 'purpose': 'any', }, { - "src": "/static/logo.png", - "type": "image/png", - "sizes": "500x500", - "purpose": "maskable", + 'src': '/static/logo.png', + 'type': 'image/png', + 'sizes': '500x500', + 'purpose': 'maskable', }, ], - "share_target": { - "action": "/", - "method": "GET", - "params": {"text": "shared"}, + 'share_target': { + 'action': '/', + 'method': 'GET', + 'params': {'text': 'shared'}, }, } -@app.get("/opensearch.xml") +@app.get('/opensearch.xml') async def get_opensearch_xml(): xml_content = rf""" @@ -2357,61 +2834,139 @@ async def get_opensearch_xml(): Search {app.state.WEBUI_NAME} UTF-8 {app.state.config.WEBUI_URL}/static/favicon.png - + {app.state.config.WEBUI_URL} """ - return Response(content=xml_content, media_type="application/xml") + return Response(content=xml_content, media_type='application/xml') -@app.get("/health") +def _sync_db_ping() -> None: + """Verify the database is reachable with a simple SELECT 1. + + Uses a raw connection from the engine pool instead of the thread-local + ScopedSession. This is necessary because CommitSessionMiddleware + deliberately skips healthcheck paths (/health, /ready, /health/db), + so any ScopedSession opened on a healthcheck worker thread is never + rolled back or removed. If the session ever enters an invalid state + (e.g. after a transient connection error), it stays broken on that + thread permanently, causing PendingRollbackError on every subsequent + probe — exactly the failure reported in #24605. + + A raw ``engine.connect()`` context manager obtains a fresh connection + from the pool, executes the ping, and deterministically returns the + connection regardless of success or failure. + """ + with engine.connect() as conn: + conn.execute(text('SELECT 1')) + + +async def async_db_ping() -> None: + await asyncio.to_thread(_sync_db_ping) + + +@app.get('/health') async def healthcheck(): - return {"status": True} + return {'status': True} -@app.get("/health/db") -async def healthcheck_with_db(): - ScopedSession.execute(text("SELECT 1;")).all() - return {"status": True} +@app.get('/ready') +async def readiness_check(): + """ + Returns 200 only when the application is ready to accept traffic. + """ + + # Ensure application startup work has completed + if not getattr(app.state, 'startup_complete', False): + log.info('Readiness check failed: startup not complete') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Startup not complete', + ) + + # Check database connectivity + try: + await async_db_ping() + except Exception as e: + log.warning(f'Readiness check DB ping failed: {e!r}') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Database not ready', + ) + + # Check Redis connectivity if configured + redis = app.state.redis + if redis is not None: + try: + pong = await redis.ping() + if pong is False: + raise Exception('Redis PING returned False') + except Exception as e: + log.warning(f'Readiness check Redis ping failed: {e!r}') + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail='Redis not ready', + ) + + return {'status': True} -app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +@app.get('/health/db') +async def check_db_health(): + """Verify database connectivity by issuing a lightweight ping.""" + await async_db_ping() + return {'status': True} +# --- static assets & files --- +# Serve build-time static assets (CSS, JS, images, favicon, etc.) +app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static') -@app.get("/cache/{path:path}") +@app.get('/cache/{path:path}') async def serve_cache_file( path: str, user=Depends(get_verified_user), ): + """Serve cached files (e.g. tool outputs) with path-traversal protection. + + Only ``image/*``, ``audio/*``, and ``video/*`` MIME types are served inline; + everything else gets a ``Content-Disposition: attachment`` header to prevent + XSS from user-generated HTML stored in the cache directory. + """ file_path = os.path.abspath(os.path.join(CACHE_DIR, path)) - # prevent path traversal - if not file_path.startswith(os.path.abspath(CACHE_DIR)): - raise HTTPException(status_code=404, detail="File not found") + # trailing os.sep is required: without it, a path resolving to a sibling + # whose name starts with the cache-dir basename (e.g. cache_backup) passes + cache_root = os.path.abspath(CACHE_DIR) + os.sep + if not file_path.startswith(cache_root): + raise HTTPException(status_code=404, detail='File not found') if not os.path.isfile(file_path): - raise HTTPException(status_code=404, detail="File not found") - return FileResponse(file_path) + raise HTTPException(status_code=404, detail='File not found') + + mime, _ = mimetypes.guess_type(file_path) + inline_safe = mime and mime.split('/', 1)[0] in {'image', 'audio', 'video'} + headers = {'X-Content-Type-Options': 'nosniff'} + if not inline_safe: + headers['Content-Disposition'] = f'attachment; filename="{os.path.basename(file_path)}"' + return FileResponse(file_path, headers=headers) def swagger_ui_html(*args, **kwargs): return get_swagger_ui_html( *args, **kwargs, - swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js", - swagger_css_url="/static/swagger-ui/swagger-ui.css", - swagger_favicon_url="/static/swagger-ui/favicon.png", + swagger_js_url='/static/swagger-ui/swagger-ui-bundle.js', + swagger_css_url='/static/swagger-ui/swagger-ui.css', + swagger_favicon_url='/static/swagger-ui/favicon.png', ) applications.get_swagger_ui_html = swagger_ui_html if os.path.exists(FRONTEND_BUILD_DIR): - mimetypes.add_type("text/javascript", ".js") + mimetypes.add_type('text/javascript', '.js') app.mount( - "/", + '/', SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), - name="spa-static-files", + name='spa-static-files', ) else: - log.warning( - f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only." - ) + log.warning(f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only.") diff --git a/backend/open_webui/migrations/env.py b/backend/open_webui/migrations/env.py index 7db9251282..648c32ef74 100644 --- a/backend/open_webui/migrations/env.py +++ b/backend/open_webui/migrations/env.py @@ -1,108 +1,65 @@ -from logging.config import fileConfig - -from alembic import context +from __future__ import annotations +# Alembic environment configuration runner. +# Coordinates database migrations in both offline and online execution modes. +import logging.config +import logging +import alembic.context +from open_webui.env import DATABASE_PASSWORD, DATABASE_URL, LOG_FORMAT +from open_webui.internal.db import extract_ssl_params_from_url, reattach_ssl_params_to_url from open_webui.models.auths import Auth -from open_webui.env import DATABASE_URL, DATABASE_PASSWORD -from sqlalchemy import engine_from_config, pool, create_engine - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name, disable_existing_loggers=False) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = Auth.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - -DB_URL = DATABASE_URL - -if DB_URL: - config.set_main_option("sqlalchemy.url", DB_URL.replace("%", "%%")) - - +from open_webui.models.calendar import Calendar, CalendarEvent, CalendarEventAttendee # noqa: F401 +from sqlalchemy import create_engine, engine_from_config, pool +alembic_config = alembic.context.config +if alembic_config.config_file_name: + logging.config.fileConfig(alembic_config.config_file_name, disable_existing_loggers=False) +if LOG_FORMAT == 'json': + from open_webui.env import JSONFormatter + for log_handler in logging.root.handlers: + log_handler.setFormatter(JSONFormatter()) +migration_metadata = Auth.metadata +target_db_url = DATABASE_URL +base_url, ssl_query_params = extract_ssl_params_from_url(target_db_url) +if ssl_query_params: + target_db_url = reattach_ssl_params_to_url(base_url, ssl_query_params) +if target_db_url: + alembic_config.set_main_option('sqlalchemy.url', target_db_url.replace('%', '%%')) def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - # Handle SQLCipher URLs - if DB_URL and DB_URL.startswith("sqlite+sqlcipher://"): - if not DATABASE_PASSWORD or DATABASE_PASSWORD.strip() == "": - raise ValueError( - "DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs" - ) - - # Extract database path from SQLCipher URL - db_path = DB_URL.replace("sqlite+sqlcipher://", "") - if db_path.startswith("/"): - db_path = db_path[1:] # Remove leading slash for relative paths - - # Create a custom creator function that uses sqlcipher3 - def create_sqlcipher_connection(): + """Execute Alembic migrations in offline mode (outputs raw SQL DDL).""" + db_connection_url = alembic_config.get_main_option('sqlalchemy.url') + alembic.context.configure(url=db_connection_url, target_metadata=migration_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}) + with alembic.context.begin_transaction(): + alembic.context.run_migrations() +def _get_engine_connectable(): + """Build the database engine based on target URL and authentication credentials.""" + if target_db_url and target_db_url.startswith('sqlite+sqlcipher://'): + if not DATABASE_PASSWORD or not DATABASE_PASSWORD.strip(): + raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs') + raw_db_path = target_db_url.replace('sqlite+sqlcipher://', '') + if raw_db_path.startswith('/'): + raw_db_path = raw_db_path[1:] + def _sqlite_cipher_creator(): import sqlcipher3 - - conn = sqlcipher3.connect(db_path, check_same_thread=False) - conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'") - return conn - - connectable = create_engine( - "sqlite://", # Dummy URL since we're using creator - creator=create_sqlcipher_connection, - echo=False, + cipher_conn = sqlcipher3.connect(raw_db_path, check_same_thread=False) + cipher_conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'") + return cipher_conn + return create_engine('sqlite://', creator=_sqlite_cipher_creator, echo=False) + return engine_from_config( + alembic_config.get_section(alembic_config.config_ini_section, {}), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) +def run_migrations_online() -> None: + """Execute migrations against a live database connection.""" + live_connectable = _get_engine_connectable() + with live_connectable.connect() as live_connection: + alembic.context.configure( + connection=live_connection, + target_metadata=migration_metadata, ) - else: - # Standard database connection (existing logic) - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() + with alembic.context.begin_transaction(): + alembic.context.run_migrations() +# Alembic execution entrypoint branch +if alembic.context.is_offline_mode(): + run_migrations_offline() # run in offline mode +if not alembic.context.is_offline_mode(): + run_migrations_online() # run in online mode diff --git a/backend/open_webui/migrations/util.py b/backend/open_webui/migrations/util.py index 955066602a..5734b8a0d6 100644 --- a/backend/open_webui/migrations/util.py +++ b/backend/open_webui/migrations/util.py @@ -1,15 +1,18 @@ -from alembic import op -from sqlalchemy import Inspector +from __future__ import annotations + +"""Alembic migration utilities.""" + +from alembic import op # noqa: E402 — alembic runtime context +from sqlalchemy import inspect # metadata inspection +# --- database helper functions --- +def get_existing_tables() -> set[str]: + """Return table names already present in the database.""" + conn = op.get_bind() + return set(inspect(conn).get_table_names()) -def get_existing_tables(): - con = op.get_bind() - inspector = Inspector.from_engine(con) - tables = set(inspector.get_table_names()) - return tables - - -def get_revision_id(): +def get_revision_id() -> str: + """Generate a short random revision identifier.""" import uuid - return str(uuid.uuid4()).replace("-", "")[:12] + return uuid.uuid4().hex[:12] diff --git a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py index 29af427108..ca3313c7f0 100644 --- a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py +++ b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py @@ -6,41 +6,57 @@ Create Date: 2025-08-13 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "018012973d35" -down_revision = "d31026856c01" +revision = '018012973d35' +down_revision = 'd31026856c01' branch_labels = None depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + + def _idx_exists(table, idx_name): + return any(i['name'] == idx_name for i in inspector.get_indexes(table)) + # Chat table indexes - op.create_index("folder_id_idx", "chat", ["folder_id"]) - op.create_index("user_id_pinned_idx", "chat", ["user_id", "pinned"]) - op.create_index("user_id_archived_idx", "chat", ["user_id", "archived"]) - op.create_index("updated_at_user_id_idx", "chat", ["updated_at", "user_id"]) - op.create_index("folder_id_user_id_idx", "chat", ["folder_id", "user_id"]) + if not _idx_exists('chat', 'folder_id_idx'): + op.create_index('folder_id_idx', 'chat', ['folder_id']) + if not _idx_exists('chat', 'user_id_pinned_idx'): + op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned']) + if not _idx_exists('chat', 'user_id_archived_idx'): + op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived']) + if not _idx_exists('chat', 'updated_at_user_id_idx'): + op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id']) + if not _idx_exists('chat', 'folder_id_user_id_idx'): + op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id']) # Tag table index - op.create_index("user_id_idx", "tag", ["user_id"]) + if not _idx_exists('tag', 'user_id_idx'): + op.create_index('user_id_idx', 'tag', ['user_id']) - # Function table index - op.create_index("is_global_idx", "function", ["is_global"]) + # Function table index (only if is_global column exists — added by a later migration) + conn = op.get_bind() + inspector = sa.inspect(conn) + func_cols = {c['name'] for c in inspector.get_columns('function')} + if 'is_global' in func_cols and not _idx_exists('function', 'is_global_idx'): + op.create_index('is_global_idx', 'function', ['is_global']) def downgrade(): # Chat table indexes - op.drop_index("folder_id_idx", table_name="chat") - op.drop_index("user_id_pinned_idx", table_name="chat") - op.drop_index("user_id_archived_idx", table_name="chat") - op.drop_index("updated_at_user_id_idx", table_name="chat") - op.drop_index("folder_id_user_id_idx", table_name="chat") + op.drop_index('folder_id_idx', table_name='chat') + op.drop_index('user_id_pinned_idx', table_name='chat') + op.drop_index('user_id_archived_idx', table_name='chat') + op.drop_index('updated_at_user_id_idx', table_name='chat') + op.drop_index('folder_id_user_id_idx', table_name='chat') # Tag table index - op.drop_index("user_id_idx", table_name="tag") + op.drop_index('user_id_idx', table_name='tag') # Function table index - op.drop_index("is_global_idx", table_name="function") + op.drop_index('is_global_idx', table_name='function') diff --git a/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py b/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py index 8a0ab1b491..00d36375d0 100644 --- a/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py +++ b/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py @@ -6,15 +6,15 @@ Create Date: 2024-10-09 21:02:35.241684 """ -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import table, select, update, column -from sqlalchemy.engine.reflection import Inspector - import json -revision = "1af9b942657b" -down_revision = "242a2047eae0" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.sql import column, select, table, update + +revision = '1af9b942657b' +down_revision = '242a2047eae0' branch_labels = None depends_on = None @@ -25,43 +25,40 @@ def upgrade(): inspector = Inspector.from_engine(conn) # Clean up potential leftover temp table from previous failures - conn.execute(sa.text("DROP TABLE IF EXISTS _alembic_tmp_tag")) + conn.execute(sa.text('DROP TABLE IF EXISTS _alembic_tmp_tag')) # Check if the 'tag' table exists tables = inspector.get_table_names() # Step 1: Modify Tag table using batch mode for SQLite support - if "tag" in tables: + if 'tag' in tables: # Get the current columns in the 'tag' table - columns = [col["name"] for col in inspector.get_columns("tag")] + columns = [col['name'] for col in inspector.get_columns('tag')] # Get any existing unique constraints on the 'tag' table - current_constraints = inspector.get_unique_constraints("tag") + current_constraints = inspector.get_unique_constraints('tag') - with op.batch_alter_table("tag", schema=None) as batch_op: + with op.batch_alter_table('tag', schema=None) as batch_op: # Check if the unique constraint already exists - if not any( - constraint["name"] == "uq_id_user_id" - for constraint in current_constraints - ): + if not any(constraint['name'] == 'uq_id_user_id' for constraint in current_constraints): # Create unique constraint if it doesn't exist - batch_op.create_unique_constraint("uq_id_user_id", ["id", "user_id"]) + batch_op.create_unique_constraint('uq_id_user_id', ['id', 'user_id']) # Check if the 'data' column exists before trying to drop it - if "data" in columns: - batch_op.drop_column("data") + if 'data' in columns: + batch_op.drop_column('data') # Check if the 'meta' column needs to be created - if "meta" not in columns: + if 'meta' not in columns: # Add the 'meta' column if it doesn't already exist - batch_op.add_column(sa.Column("meta", sa.JSON(), nullable=True)) + batch_op.add_column(sa.Column('meta', sa.JSON(), nullable=True)) tag = table( - "tag", - column("id", sa.String()), - column("name", sa.String()), - column("user_id", sa.String()), - column("meta", sa.JSON()), + 'tag', + column('id', sa.String()), + column('name', sa.String()), + column('user_id', sa.String()), + column('meta', sa.JSON()), ) # Step 2: Migrate tags @@ -70,12 +67,12 @@ def upgrade(): tag_updates = {} for row in result: - new_id = row.name.replace(" ", "_").lower() + new_id = row.name.replace(' ', '_').lower() tag_updates[row.id] = new_id for tag_id, new_tag_id in tag_updates.items(): - print(f"Updating tag {tag_id} to {new_tag_id}") - if new_tag_id == "pinned": + print(f'Updating tag {tag_id} to {new_tag_id}') + if new_tag_id == 'pinned': # delete tag delete_stmt = sa.delete(tag).where(tag.c.id == tag_id) conn.execute(delete_stmt) @@ -86,9 +83,7 @@ def upgrade(): if existing_tag_result: # Handle duplicate case: the new_tag_id already exists - print( - f"Tag {new_tag_id} already exists. Removing current tag with ID {tag_id} to avoid duplicates." - ) + print(f'Tag {new_tag_id} already exists. Removing current tag with ID {tag_id} to avoid duplicates.') # Option 1: Delete the current tag if an update to new_tag_id would cause duplication delete_stmt = sa.delete(tag).where(tag.c.id == tag_id) conn.execute(delete_stmt) @@ -98,19 +93,18 @@ def upgrade(): conn.execute(update_stmt) # Add columns `pinned` and `meta` to 'chat' - op.add_column("chat", sa.Column("pinned", sa.Boolean(), nullable=True)) - op.add_column( - "chat", sa.Column("meta", sa.JSON(), nullable=False, server_default="{}") - ) + chat_columns = {c['name'] for c in inspector.get_columns('chat')} + if 'pinned' not in chat_columns: + op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True)) + if 'meta' not in chat_columns: + op.add_column('chat', sa.Column('meta', sa.JSON(), nullable=False, server_default='{}')) - chatidtag = table( - "chatidtag", column("chat_id", sa.String()), column("tag_name", sa.String()) - ) + chatidtag = table('chatidtag', column('chat_id', sa.String()), column('tag_name', sa.String())) chat = table( - "chat", - column("id", sa.String()), - column("pinned", sa.Boolean()), - column("meta", sa.JSON()), + 'chat', + column('id', sa.String()), + column('pinned', sa.Boolean()), + column('meta', sa.JSON()), ) # Fetch existing tags @@ -120,29 +114,27 @@ def upgrade(): chat_updates = {} for row in result: chat_id = row.chat_id - tag_name = row.tag_name.replace(" ", "_").lower() + tag_name = row.tag_name.replace(' ', '_').lower() - if tag_name == "pinned": + if tag_name == 'pinned': # Specifically handle 'pinned' tag if chat_id not in chat_updates: - chat_updates[chat_id] = {"pinned": True, "meta": {}} + chat_updates[chat_id] = {'pinned': True, 'meta': {}} else: - chat_updates[chat_id]["pinned"] = True + chat_updates[chat_id]['pinned'] = True else: if chat_id not in chat_updates: - chat_updates[chat_id] = {"pinned": False, "meta": {"tags": [tag_name]}} + chat_updates[chat_id] = {'pinned': False, 'meta': {'tags': [tag_name]}} else: - tags = chat_updates[chat_id]["meta"].get("tags", []) + tags = chat_updates[chat_id]['meta'].get('tags', []) tags.append(tag_name) - chat_updates[chat_id]["meta"]["tags"] = list(set(tags)) + chat_updates[chat_id]['meta']['tags'] = list(set(tags)) # Update chats based on accumulated changes for chat_id, updates in chat_updates.items(): update_stmt = sa.update(chat).where(chat.c.id == chat_id) - update_stmt = update_stmt.values( - meta=updates.get("meta", {}), pinned=updates.get("pinned", False) - ) + update_stmt = update_stmt.values(meta=updates.get('meta', {}), pinned=updates.get('pinned', False)) conn.execute(update_stmt) pass diff --git a/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py b/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py index 6017da3169..e1c42bfb70 100644 --- a/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py +++ b/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py @@ -6,14 +6,14 @@ Create Date: 2024-10-09 21:02:35.241684 """ -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import table, select, update - import json -revision = "242a2047eae0" -down_revision = "6a39f3d8e55c" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.sql import select, table, update + +revision = '242a2047eae0' +down_revision = '6a39f3d8e55c' branch_labels = None depends_on = None @@ -22,86 +22,79 @@ def upgrade(): conn = op.get_bind() inspector = sa.inspect(conn) - columns = inspector.get_columns("chat") - column_dict = {col["name"]: col for col in columns} + columns = inspector.get_columns('chat') + column_dict = {col['name']: col for col in columns} - chat_column = column_dict.get("chat") - old_chat_exists = "old_chat" in column_dict + chat_column = column_dict.get('chat') + old_chat_exists = 'old_chat' in column_dict if chat_column: - if isinstance(chat_column["type"], sa.Text): + if isinstance(chat_column['type'], sa.Text): print("Converting 'chat' column to JSON") if old_chat_exists: print("Dropping old 'old_chat' column") - op.drop_column("chat", "old_chat") + op.drop_column('chat', 'old_chat') # Step 1: Rename current 'chat' column to 'old_chat' print("Renaming 'chat' column to 'old_chat'") - op.alter_column( - "chat", "chat", new_column_name="old_chat", existing_type=sa.Text() - ) + op.alter_column('chat', 'chat', new_column_name='old_chat', existing_type=sa.Text()) # Step 2: Add new 'chat' column of type JSON print("Adding new 'chat' column of type JSON") - op.add_column("chat", sa.Column("chat", sa.JSON(), nullable=True)) + op.add_column('chat', sa.Column('chat', sa.JSON(), nullable=True)) else: # If the column is already JSON, no need to do anything pass - # Step 3: Migrate data from 'old_chat' to 'chat' - chat_table = table( - "chat", - sa.Column("id", sa.String(), primary_key=True), - sa.Column("old_chat", sa.Text()), - sa.Column("chat", sa.JSON()), - ) - - # - Selecting all data from the table - connection = op.get_bind() - results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat)) - for row in results: - try: - # Convert text JSON to actual JSON object, assuming the text is in JSON format - json_data = json.loads(row.old_chat) - except json.JSONDecodeError: - json_data = None # Handle cases where the text cannot be converted to JSON - - connection.execute( - sa.update(chat_table) - .where(chat_table.c.id == row.id) - .values(chat=json_data) + # Step 3: Migrate data from 'old_chat' to 'chat' (only if old_chat exists) + # Re-check columns after potential rename above + current_cols = {c['name'] for c in inspector.get_columns('chat')} + if 'old_chat' in current_cols: + chat_table = table( + 'chat', + sa.Column('id', sa.String(), primary_key=True), + sa.Column('old_chat', sa.Text()), + sa.Column('chat', sa.JSON()), ) - # Step 4: Drop 'old_chat' column - print("Dropping 'old_chat' column") - op.drop_column("chat", "old_chat") + # - Selecting all data from the table + connection = op.get_bind() + results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat)) + for row in results: + try: + # Convert text JSON to actual JSON object, assuming the text is in JSON format + json_data = json.loads(row.old_chat) + except json.JSONDecodeError: + json_data = None # Handle cases where the text cannot be converted to JSON + + connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(chat=json_data)) + + # Step 4: Drop 'old_chat' column + print("Dropping 'old_chat' column") + op.drop_column('chat', 'old_chat') def downgrade(): # Step 1: Add 'old_chat' column back as Text - op.add_column("chat", sa.Column("old_chat", sa.Text(), nullable=True)) + op.add_column('chat', sa.Column('old_chat', sa.Text(), nullable=True)) # Step 2: Convert 'chat' JSON data back to text and store in 'old_chat' chat_table = table( - "chat", - sa.Column("id", sa.String(), primary_key=True), - sa.Column("chat", sa.JSON()), - sa.Column("old_chat", sa.Text()), + 'chat', + sa.Column('id', sa.String(), primary_key=True), + sa.Column('chat', sa.JSON()), + sa.Column('old_chat', sa.Text()), ) connection = op.get_bind() results = connection.execute(select(chat_table.c.id, chat_table.c.chat)) for row in results: text_data = json.dumps(row.chat) if row.chat is not None else None - connection.execute( - sa.update(chat_table) - .where(chat_table.c.id == row.id) - .values(old_chat=text_data) - ) + connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(old_chat=text_data)) # Step 3: Remove the new 'chat' JSON column - op.drop_column("chat", "chat") + op.drop_column('chat', 'chat') # Step 4: Rename 'old_chat' back to 'chat' - op.alter_column("chat", "old_chat", new_column_name="chat", existing_type=sa.Text()) + op.alter_column('chat', 'old_chat', new_column_name='chat', existing_type=sa.Text()) diff --git a/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py b/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py index 2d72583ebe..1c0cb6da81 100644 --- a/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py +++ b/backend/open_webui/migrations/versions/2f1211949ecc_update_message_and_channel_member_table.py @@ -8,96 +8,100 @@ Create Date: 2025-11-27 03:07:56.200231 from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa import open_webui.internal.db - +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "2f1211949ecc" -down_revision: Union[str, None] = "37f288994c47" +revision: str = '2f1211949ecc' +down_revision: Union[str, None] = '37f288994c47' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + # New columns to be added to channel_member table - op.add_column("channel_member", sa.Column("status", sa.Text(), nullable=True)) - op.add_column( - "channel_member", - sa.Column( - "is_active", - sa.Boolean(), - nullable=False, - default=True, - server_default=sa.sql.expression.true(), - ), - ) - - op.add_column( - "channel_member", - sa.Column( - "is_channel_muted", - sa.Boolean(), - nullable=False, - default=False, - server_default=sa.sql.expression.false(), - ), - ) - op.add_column( - "channel_member", - sa.Column( - "is_channel_pinned", - sa.Boolean(), - nullable=False, - default=False, - server_default=sa.sql.expression.false(), - ), - ) - - op.add_column("channel_member", sa.Column("data", sa.JSON(), nullable=True)) - op.add_column("channel_member", sa.Column("meta", sa.JSON(), nullable=True)) - - op.add_column( - "channel_member", sa.Column("joined_at", sa.BigInteger(), nullable=False) - ) - op.add_column( - "channel_member", sa.Column("left_at", sa.BigInteger(), nullable=True) - ) - - op.add_column( - "channel_member", sa.Column("last_read_at", sa.BigInteger(), nullable=True) - ) - - op.add_column( - "channel_member", sa.Column("updated_at", sa.BigInteger(), nullable=True) - ) + cm_cols = {c['name'] for c in inspector.get_columns('channel_member')} + if 'status' not in cm_cols: + op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True)) + if 'is_active' not in cm_cols: + op.add_column( + 'channel_member', + sa.Column( + 'is_active', + sa.Boolean(), + nullable=False, + default=True, + server_default=sa.sql.expression.true(), + ), + ) + if 'is_channel_muted' not in cm_cols: + op.add_column( + 'channel_member', + sa.Column( + 'is_channel_muted', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + if 'is_channel_pinned' not in cm_cols: + op.add_column( + 'channel_member', + sa.Column( + 'is_channel_pinned', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + if 'data' not in cm_cols: + op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True)) + if 'meta' not in cm_cols: + op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True)) + if 'joined_at' not in cm_cols: + op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False)) + if 'left_at' not in cm_cols: + op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True)) + if 'last_read_at' not in cm_cols: + op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) + if 'updated_at' not in cm_cols: + op.add_column('channel_member', sa.Column('updated_at', sa.BigInteger(), nullable=True)) # New columns to be added to message table - op.add_column( - "message", - sa.Column( - "is_pinned", - sa.Boolean(), - nullable=False, - default=False, - server_default=sa.sql.expression.false(), - ), - ) - op.add_column("message", sa.Column("pinned_at", sa.BigInteger(), nullable=True)) - op.add_column("message", sa.Column("pinned_by", sa.Text(), nullable=True)) + msg_cols = {c['name'] for c in inspector.get_columns('message')} + if 'is_pinned' not in msg_cols: + op.add_column( + 'message', + sa.Column( + 'is_pinned', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + if 'pinned_at' not in msg_cols: + op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True)) + if 'pinned_by' not in msg_cols: + op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True)) def downgrade() -> None: - op.drop_column("channel_member", "updated_at") - op.drop_column("channel_member", "last_read_at") + op.drop_column('channel_member', 'updated_at') + op.drop_column('channel_member', 'last_read_at') - op.drop_column("channel_member", "meta") - op.drop_column("channel_member", "data") + op.drop_column('channel_member', 'meta') + op.drop_column('channel_member', 'data') - op.drop_column("channel_member", "is_channel_pinned") - op.drop_column("channel_member", "is_channel_muted") + op.drop_column('channel_member', 'is_channel_pinned') + op.drop_column('channel_member', 'is_channel_muted') - op.drop_column("message", "pinned_by") - op.drop_column("message", "pinned_at") - op.drop_column("message", "is_pinned") + op.drop_column('message', 'pinned_by') + op.drop_column('message', 'pinned_at') + op.drop_column('message', 'is_pinned') diff --git a/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py new file mode 100644 index 0000000000..69dd73c700 --- /dev/null +++ b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py @@ -0,0 +1,262 @@ +"""Add prompt history table + +Revision ID: 374d2f66af06 +Revises: c440947495f3 +Create Date: 2026-01-23 17:15:00.000000 + +""" + +import uuid +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = '374d2f66af06' +down_revision: Union[str, None] = 'c440947495f3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + # If the final state already exists (prompt has 'id' PK + prompt_history exists), + # the migration completed successfully on a prior run — nothing to do. + if 'prompt_history' in existing_tables and 'prompt_new' not in existing_tables: + # prompt_history exists and prompt_new was already renamed → done + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'id' in prompt_cols and 'version_id' in prompt_cols: + return + + # Step 1: Read existing data from OLD table (schema: command as PK) + # Only read if the old-schema prompt table still exists (has 'command' but no 'version_id') + existing_prompts = [] + if 'prompt' in existing_tables and 'prompt_new' not in existing_tables: + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'command' in prompt_cols and 'version_id' not in prompt_cols: + old_prompt_table = sa.table( + 'prompt', + sa.column('command', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('title', sa.Text()), + sa.column('content', sa.Text()), + sa.column('timestamp', sa.BigInteger()), + sa.column('access_control', sa.JSON()), + ) + try: + existing_prompts = conn.execute( + sa.select( + old_prompt_table.c.command, + old_prompt_table.c.user_id, + old_prompt_table.c.title, + old_prompt_table.c.content, + old_prompt_table.c.timestamp, + old_prompt_table.c.access_control, + ) + ).fetchall() + except Exception: + existing_prompts = [] + + # Step 2: Create new prompt table with 'id' as PRIMARY KEY (if not already created) + if 'prompt_new' not in existing_tables: + op.create_table( + 'prompt_new', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('command', sa.String(), unique=True, index=True), + sa.Column('user_id', sa.String(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'), + sa.Column('version_id', sa.Text(), nullable=True), + sa.Column('tags', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + # Step 3: Create prompt_history table (if not already created) + if 'prompt_history' not in existing_tables: + op.create_table( + 'prompt_history', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('prompt_id', sa.Text(), nullable=False, index=True), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('snapshot', sa.JSON(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('commit_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + + # Step 4: Migrate data (only if we have old data to migrate) + if existing_prompts: + prompt_new_table = sa.table( + 'prompt_new', + sa.column('id', sa.Text()), + sa.column('command', sa.String()), + sa.column('user_id', sa.String()), + sa.column('name', sa.Text()), + sa.column('content', sa.Text()), + sa.column('data', sa.JSON()), + sa.column('meta', sa.JSON()), + sa.column('access_control', sa.JSON()), + sa.column('is_active', sa.Boolean()), + sa.column('version_id', sa.Text()), + sa.column('tags', sa.JSON()), + sa.column('created_at', sa.BigInteger()), + sa.column('updated_at', sa.BigInteger()), + ) + + prompt_history_table = sa.table( + 'prompt_history', + sa.column('id', sa.Text()), + sa.column('prompt_id', sa.Text()), + sa.column('parent_id', sa.Text()), + sa.column('snapshot', sa.JSON()), + sa.column('user_id', sa.Text()), + sa.column('commit_message', sa.Text()), + sa.column('created_at', sa.BigInteger()), + ) + + for row in existing_prompts: + command = row[0] + user_id = row[1] + title = row[2] + content = row[3] + timestamp = row[4] + access_control = row[5] + + new_uuid = str(uuid.uuid4()) + history_uuid = str(uuid.uuid4()) + clean_command = command[1:] if command and command.startswith('/') else command + + # Insert into prompt_new + conn.execute( + sa.insert(prompt_new_table).values( + id=new_uuid, + command=clean_command, + user_id=user_id, + name=title, + content=content, + data={}, + meta={}, + access_control=access_control, + is_active=True, + version_id=history_uuid, + tags=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + + # Create initial history entry + conn.execute( + sa.insert(prompt_history_table).values( + id=history_uuid, + prompt_id=new_uuid, + parent_id=None, + snapshot={ + 'name': title, + 'content': content, + 'command': clean_command, + 'data': {}, + 'meta': {}, + 'access_control': access_control, + }, + user_id=user_id, + commit_message=None, + created_at=timestamp, + ) + ) + + # Step 5: Replace old table with new one (only if prompt_new exists) + # Re-check tables after potential creation above + inspector.clear_cache() + current_tables = set(inspector.get_table_names()) + if 'prompt_new' in current_tables: + if 'prompt' in current_tables: + op.drop_table('prompt') + op.rename_table('prompt_new', 'prompt') + + +def downgrade() -> None: + conn = op.get_bind() + + # Step 1: Read new data + prompt_table = sa.table( + 'prompt', + sa.column('command', sa.String()), + sa.column('name', sa.Text()), + sa.column('created_at', sa.BigInteger()), + sa.column('user_id', sa.Text()), + sa.column('content', sa.Text()), + sa.column('access_control', sa.JSON()), + ) + + try: + current_data = conn.execute( + sa.select( + prompt_table.c.command, + prompt_table.c.name, + prompt_table.c.created_at, + prompt_table.c.user_id, + prompt_table.c.content, + prompt_table.c.access_control, + ) + ).fetchall() + except Exception: + current_data = [] + + # Step 2: Drop history and table + op.drop_table('prompt_history') + op.drop_table('prompt') + + # Step 3: Recreate old table (command as PK?) + # Assuming old schema: + op.create_table( + 'prompt', + sa.Column('command', sa.String(), primary_key=True), + sa.Column('user_id', sa.String()), + sa.Column('title', sa.Text()), + sa.Column('content', sa.Text()), + sa.Column('timestamp', sa.BigInteger()), + sa.Column('access_control', sa.JSON()), + sa.Column('id', sa.Integer(), nullable=True), + ) + + # Step 4: Restore data + old_prompt_table = sa.table( + 'prompt', + sa.column('command', sa.String()), + sa.column('user_id', sa.String()), + sa.column('title', sa.Text()), + sa.column('content', sa.Text()), + sa.column('timestamp', sa.BigInteger()), + sa.column('access_control', sa.JSON()), + ) + + for row in current_data: + command = row[0] + name = row[1] + created_at = row[2] + user_id = row[3] + content = row[4] + access_control = row[5] + + # Restore leading / + old_command = '/' + command if command and not command.startswith('/') else command + + conn.execute( + sa.insert(old_prompt_table).values( + command=old_command, + user_id=user_id, + title=name, + content=content, + timestamp=created_at, + access_control=access_control, + ) + ) diff --git a/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py index 16fb0e85eb..aaebaacb49 100644 --- a/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py +++ b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py @@ -6,65 +6,63 @@ Create Date: 2024-12-30 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "3781e22d8b01" -down_revision = "7826ab40b532" +revision = '3781e22d8b01' +down_revision = '7826ab40b532' branch_labels = None depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + # Add 'type' column to the 'channel' table - op.add_column( - "channel", - sa.Column( - "type", - sa.Text(), - nullable=True, - ), - ) + channel_cols = {c['name'] for c in inspector.get_columns('channel')} + if 'type' not in channel_cols: + op.add_column( + 'channel', + sa.Column( + 'type', + sa.Text(), + nullable=True, + ), + ) # Add 'parent_id' column to the 'message' table for threads - op.add_column( - "message", - sa.Column("parent_id", sa.Text(), nullable=True), - ) + message_cols = {c['name'] for c in inspector.get_columns('message')} + if 'parent_id' not in message_cols: + op.add_column( + 'message', + sa.Column('parent_id', sa.Text(), nullable=True), + ) - op.create_table( - "message_reaction", - sa.Column( - "id", sa.Text(), nullable=False, primary_key=True, unique=True - ), # Unique reaction ID - sa.Column("user_id", sa.Text(), nullable=False), # User who reacted - sa.Column( - "message_id", sa.Text(), nullable=False - ), # Message that was reacted to - sa.Column( - "name", sa.Text(), nullable=False - ), # Reaction name (e.g. "thumbs_up") - sa.Column( - "created_at", sa.BigInteger(), nullable=True - ), # Timestamp of when the reaction was added - ) + if 'message_reaction' not in existing_tables: + op.create_table( + 'message_reaction', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Unique reaction ID + sa.Column('user_id', sa.Text(), nullable=False), # User who reacted + sa.Column('message_id', sa.Text(), nullable=False), # Message that was reacted to + sa.Column('name', sa.Text(), nullable=False), # Reaction name (e.g. "thumbs_up") + sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the reaction was added + ) - op.create_table( - "channel_member", - sa.Column( - "id", sa.Text(), nullable=False, primary_key=True, unique=True - ), # Record ID for the membership row - sa.Column("channel_id", sa.Text(), nullable=False), # Associated channel - sa.Column("user_id", sa.Text(), nullable=False), # Associated user - sa.Column( - "created_at", sa.BigInteger(), nullable=True - ), # Timestamp of when the user joined the channel - ) + if 'channel_member' not in existing_tables: + op.create_table( + 'channel_member', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Record ID for the membership row + sa.Column('channel_id', sa.Text(), nullable=False), # Associated channel + sa.Column('user_id', sa.Text(), nullable=False), # Associated user + sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the user joined the channel + ) def downgrade(): # Revert 'type' column addition to the 'channel' table - op.drop_column("channel", "type") - op.drop_column("message", "parent_id") - op.drop_table("message_reaction") - op.drop_table("channel_member") + op.drop_column('channel', 'type') + op.drop_column('message', 'parent_id') + op.drop_table('message_reaction') + op.drop_table('channel_member') diff --git a/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py b/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py index 0c5cec1941..daf0aef172 100644 --- a/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py +++ b/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py @@ -6,69 +6,73 @@ Create Date: 2025-11-17 03:45:25.123939 """ -import uuid -import time import json +import time +import uuid from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. -revision: str = "37f288994c47" -down_revision: Union[str, None] = "a5c220713937" +revision: str = '37f288994c47' +down_revision: Union[str, None] = 'a5c220713937' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'group_member' in existing_tables: + return # Already created — skip everything + # 1. Create new table op.create_table( - "group_member", - sa.Column("id", sa.Text(), primary_key=True, unique=True, nullable=False), + 'group_member', + sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False), sa.Column( - "group_id", + 'group_id', sa.Text(), - sa.ForeignKey("group.id", ondelete="CASCADE"), + sa.ForeignKey('group.id', ondelete='CASCADE'), nullable=False, ), sa.Column( - "user_id", + 'user_id', sa.Text(), - sa.ForeignKey("user.id", ondelete="CASCADE"), + sa.ForeignKey('user.id', ondelete='CASCADE'), nullable=False, ), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.UniqueConstraint("group_id", "user_id", name="uq_group_member_group_user"), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.UniqueConstraint('group_id', 'user_id', name='uq_group_member_group_user'), ) connection = op.get_bind() # 2. Read existing group with user_ids JSON column group_table = sa.Table( - "group", + 'group', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("user_ids", sa.JSON()), # JSON stored as text in SQLite + PG + sa.Column('id', sa.Text()), + sa.Column('user_ids', sa.JSON()), # JSON stored as text in SQLite + PG ) - results = connection.execute( - sa.select(group_table.c.id, group_table.c.user_ids) - ).fetchall() + results = connection.execute(sa.select(group_table.c.id, group_table.c.user_ids)).fetchall() print(results) # 3. Insert members into group_member table gm_table = sa.Table( - "group_member", + 'group_member', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("group_id", sa.Text()), - sa.Column("user_id", sa.Text()), - sa.Column("created_at", sa.BigInteger()), - sa.Column("updated_at", sa.BigInteger()), + sa.Column('id', sa.Text()), + sa.Column('group_id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('created_at', sa.BigInteger()), + sa.Column('updated_at', sa.BigInteger()), ) now = int(time.time()) @@ -87,11 +91,11 @@ def upgrade() -> None: rows = [ { - "id": str(uuid.uuid4()), - "group_id": group_id, - "user_id": uid, - "created_at": now, - "updated_at": now, + 'id': str(uuid.uuid4()), + 'group_id': group_id, + 'user_id': uid, + 'created_at': now, + 'updated_at': now, } for uid in user_ids ] @@ -100,47 +104,41 @@ def upgrade() -> None: connection.execute(gm_table.insert(), rows) # 4. Optionally drop the old column - with op.batch_alter_table("group") as batch: - batch.drop_column("user_ids") + with op.batch_alter_table('group') as batch: + batch.drop_column('user_ids') def downgrade(): # Reverse: restore user_ids column - with op.batch_alter_table("group") as batch: - batch.add_column(sa.Column("user_ids", sa.JSON())) + with op.batch_alter_table('group') as batch: + batch.add_column(sa.Column('user_ids', sa.JSON())) connection = op.get_bind() gm_table = sa.Table( - "group_member", + 'group_member', sa.MetaData(), - sa.Column("group_id", sa.Text()), - sa.Column("user_id", sa.Text()), - sa.Column("created_at", sa.BigInteger()), - sa.Column("updated_at", sa.BigInteger()), + sa.Column('group_id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('created_at', sa.BigInteger()), + sa.Column('updated_at', sa.BigInteger()), ) group_table = sa.Table( - "group", + 'group', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("user_ids", sa.JSON()), + sa.Column('id', sa.Text()), + sa.Column('user_ids', sa.JSON()), ) # Build JSON arrays again results = connection.execute(sa.select(group_table.c.id)).fetchall() for (group_id,) in results: - members = connection.execute( - sa.select(gm_table.c.user_id).where(gm_table.c.group_id == group_id) - ).fetchall() + members = connection.execute(sa.select(gm_table.c.user_id).where(gm_table.c.group_id == group_id)).fetchall() member_ids = [m[0] for m in members] - connection.execute( - group_table.update() - .where(group_table.c.id == group_id) - .values(user_ids=member_ids) - ) + connection.execute(group_table.update().where(group_table.c.id == group_id).values(user_ids=member_ids)) # Drop the new table - op.drop_table("group_member") + op.drop_table('group_member') diff --git a/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py index 264ce13b41..b212c7f43a 100644 --- a/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py +++ b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py @@ -8,73 +8,57 @@ Create Date: 2025-09-08 14:19:59.583921 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. -revision: str = "38d63c18f30f" -down_revision: Union[str, None] = "3af16a1c9fb6" +revision: str = '38d63c18f30f' +down_revision: Union[str, None] = '3af16a1c9fb6' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - # Ensure 'id' column in 'user' table is unique and primary key (ForeignKey constraint) inspector = sa.inspect(op.get_bind()) - columns = inspector.get_columns("user") + existing_tables = set(inspector.get_table_names()) - pk_columns = inspector.get_pk_constraint("user")["constrained_columns"] - id_column = next((col for col in columns if col["name"] == "id"), None) + # ── Create oauth_session table (idempotent) ─────────────────────── + if 'oauth_session' not in existing_tables: + op.create_table( + 'oauth_session', + sa.Column('id', sa.Text(), primary_key=True, nullable=False, unique=True), + sa.Column( + 'user_id', + sa.Text(), + sa.ForeignKey('user.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('provider', sa.Text(), nullable=False), + sa.Column('token', sa.Text(), nullable=False), + sa.Column('expires_at', sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) - if id_column and not id_column.get("unique", False): - unique_constraints = inspector.get_unique_constraints("user") - unique_columns = {tuple(u["column_names"]) for u in unique_constraints} + # Create indexes (idempotent — no-ops when table was just created + # with the columns above, and safe to call if indexes already exist). + existing_indexes = { + idx['name'] for idx in inspector.get_indexes('oauth_session') + } if 'oauth_session' in existing_tables else set() - with op.batch_alter_table("user") as batch_op: - # If primary key is wrong, drop it - if pk_columns and pk_columns != ["id"]: - batch_op.drop_constraint( - inspector.get_pk_constraint("user")["name"], type_="primary" - ) - - # Add unique constraint if missing - if ("id",) not in unique_columns: - batch_op.create_unique_constraint("uq_user_id", ["id"]) - - # Re-create correct primary key - batch_op.create_primary_key("pk_user_id", ["id"]) - - # Create oauth_session table - op.create_table( - "oauth_session", - sa.Column("id", sa.Text(), primary_key=True, nullable=False, unique=True), - sa.Column( - "user_id", - sa.Text(), - sa.ForeignKey("user.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("provider", sa.Text(), nullable=False), - sa.Column("token", sa.Text(), nullable=False), - sa.Column("expires_at", sa.BigInteger(), nullable=False), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=False), - ) - - # Create indexes for better performance - op.create_index("idx_oauth_session_user_id", "oauth_session", ["user_id"]) - op.create_index("idx_oauth_session_expires_at", "oauth_session", ["expires_at"]) - op.create_index( - "idx_oauth_session_user_provider", "oauth_session", ["user_id", "provider"] - ) + if 'idx_oauth_session_user_id' not in existing_indexes: + op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id']) + if 'idx_oauth_session_expires_at' not in existing_indexes: + op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at']) + if 'idx_oauth_session_user_provider' not in existing_indexes: + op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider']) def downgrade() -> None: # Drop indexes first - op.drop_index("idx_oauth_session_user_provider", table_name="oauth_session") - op.drop_index("idx_oauth_session_expires_at", table_name="oauth_session") - op.drop_index("idx_oauth_session_user_id", table_name="oauth_session") + op.drop_index('idx_oauth_session_user_provider', table_name='oauth_session') + op.drop_index('idx_oauth_session_expires_at', table_name='oauth_session') + op.drop_index('idx_oauth_session_user_id', table_name='oauth_session') # Drop the table - op.drop_table("oauth_session") + op.drop_table('oauth_session') diff --git a/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py b/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py index 6e010424b0..8aaa4d4d47 100644 --- a/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py +++ b/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py @@ -6,15 +6,15 @@ Create Date: 2024-10-09 21:02:35.241684 """ -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import table, select, update, column -from sqlalchemy.engine.reflection import Inspector - import json -revision = "3ab32c4b8f59" -down_revision = "1af9b942657b" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.sql import column, select, table, update + +revision = '3ab32c4b8f59' +down_revision = '1af9b942657b' branch_labels = None depends_on = None @@ -24,58 +24,55 @@ def upgrade(): inspector = Inspector.from_engine(conn) # Inspecting the 'tag' table constraints and structure - existing_pk = inspector.get_pk_constraint("tag") - unique_constraints = inspector.get_unique_constraints("tag") - existing_indexes = inspector.get_indexes("tag") + existing_pk = inspector.get_pk_constraint('tag') + unique_constraints = inspector.get_unique_constraints('tag') + existing_indexes = inspector.get_indexes('tag') - print(f"Primary Key: {existing_pk}") - print(f"Unique Constraints: {unique_constraints}") - print(f"Indexes: {existing_indexes}") + print(f'Primary Key: {existing_pk}') + print(f'Unique Constraints: {unique_constraints}') + print(f'Indexes: {existing_indexes}') - with op.batch_alter_table("tag", schema=None) as batch_op: + with op.batch_alter_table('tag', schema=None) as batch_op: # Drop existing primary key constraint if it exists - if existing_pk and existing_pk.get("constrained_columns"): - pk_name = existing_pk.get("name") + if existing_pk and existing_pk.get('constrained_columns'): + pk_name = existing_pk.get('name') if pk_name: - print(f"Dropping primary key constraint: {pk_name}") - batch_op.drop_constraint(pk_name, type_="primary") + print(f'Dropping primary key constraint: {pk_name}') + batch_op.drop_constraint(pk_name, type_='primary') # Now create the new primary key with the combination of 'id' and 'user_id' print("Creating new primary key with 'id' and 'user_id'.") - batch_op.create_primary_key("pk_id_user_id", ["id", "user_id"]) + batch_op.create_primary_key('pk_id_user_id', ['id', 'user_id']) # Drop unique constraints that could conflict with the new primary key for constraint in unique_constraints: if ( - constraint["name"] == "uq_id_user_id" + constraint['name'] == 'uq_id_user_id' ): # Adjust this name according to what is actually returned by the inspector - print(f"Dropping unique constraint: {constraint['name']}") - batch_op.drop_constraint(constraint["name"], type_="unique") + print(f'Dropping unique constraint: {constraint["name"]}') + batch_op.drop_constraint(constraint['name'], type_='unique') for index in existing_indexes: - if index["unique"]: - if not any( - constraint["name"] == index["name"] - for constraint in unique_constraints - ): + if index['unique']: + if not any(constraint['name'] == index['name'] for constraint in unique_constraints): # You are attempting to drop unique indexes - print(f"Dropping unique index: {index['name']}") - batch_op.drop_index(index["name"]) + print(f'Dropping unique index: {index["name"]}') + batch_op.drop_index(index['name']) def downgrade(): conn = op.get_bind() inspector = Inspector.from_engine(conn) - current_pk = inspector.get_pk_constraint("tag") + current_pk = inspector.get_pk_constraint('tag') - with op.batch_alter_table("tag", schema=None) as batch_op: + with op.batch_alter_table('tag', schema=None) as batch_op: # Drop the current primary key first, if it matches the one we know we added in upgrade - if current_pk and "pk_id_user_id" == current_pk.get("name"): - batch_op.drop_constraint("pk_id_user_id", type_="primary") + if current_pk and 'pk_id_user_id' == current_pk.get('name'): + batch_op.drop_constraint('pk_id_user_id', type_='primary') # Restore the original primary key - batch_op.create_primary_key("pk_id", ["id"]) + batch_op.create_primary_key('pk_id', ['id']) # Since primary key on just 'id' is restored, we now add back any unique constraints if necessary - batch_op.create_unique_constraint("uq_id_user_id", ["id", "user_id"]) + batch_op.create_unique_constraint('uq_id_user_id', ['id', 'user_id']) diff --git a/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py b/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py index ab980f27ce..e3767a71a7 100644 --- a/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py +++ b/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py @@ -8,25 +8,33 @@ Create Date: 2025-08-21 02:07:18.078283 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "3af16a1c9fb6" -down_revision: Union[str, None] = "018012973d35" +revision: str = '3af16a1c9fb6' +down_revision: Union[str, None] = '018012973d35' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.add_column("user", sa.Column("username", sa.String(length=50), nullable=True)) - op.add_column("user", sa.Column("bio", sa.Text(), nullable=True)) - op.add_column("user", sa.Column("gender", sa.Text(), nullable=True)) - op.add_column("user", sa.Column("date_of_birth", sa.Date(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + user_cols = {c['name'] for c in inspector.get_columns('user')} + + if 'username' not in user_cols: + op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True)) + if 'bio' not in user_cols: + op.add_column('user', sa.Column('bio', sa.Text(), nullable=True)) + if 'gender' not in user_cols: + op.add_column('user', sa.Column('gender', sa.Text(), nullable=True)) + if 'date_of_birth' not in user_cols: + op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True)) def downgrade() -> None: - op.drop_column("user", "username") - op.drop_column("user", "bio") - op.drop_column("user", "gender") - op.drop_column("user", "date_of_birth") + op.drop_column('user', 'username') + op.drop_column('user', 'bio') + op.drop_column('user', 'gender') + op.drop_column('user', 'date_of_birth') diff --git a/backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py b/backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py new file mode 100644 index 0000000000..ab2b0c9498 --- /dev/null +++ b/backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py @@ -0,0 +1,69 @@ +"""add knowledge_directory table + +Revision ID: 3c9b0ca343fd +Revises: a0b1c2d3e4f5 +Create Date: 2026-05-13 21:58:40.832482 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '3c9b0ca343fd' +down_revision: Union[str, None] = 'a0b1c2d3e4f5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'knowledge_directory' not in existing_tables: + # Create knowledge_directory table + op.create_table( + 'knowledge_directory', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('knowledge_id', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['parent_id'], ['knowledge_directory.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('knowledge_id', 'parent_id', 'name', name='uq_knowledge_directory_knowledge_parent_name'), + ) + op.create_index('ix_knowledge_directory_knowledge_id', 'knowledge_directory', ['knowledge_id']) + op.create_index('ix_knowledge_directory_parent_id', 'knowledge_directory', ['parent_id']) + + # Add directory_id column to knowledge_file + kf_cols = {c['name'] for c in inspector.get_columns('knowledge_file')} + if 'directory_id' not in kf_cols: + with op.batch_alter_table('knowledge_file') as batch: + batch.add_column(sa.Column('directory_id', sa.Text(), nullable=True)) + batch.create_foreign_key( + 'fk_knowledge_file_directory_id', + 'knowledge_directory', + ['directory_id'], + ['id'], + ondelete='SET NULL', + ) + batch.create_index('ix_knowledge_file_directory_id', ['directory_id']) + + +def downgrade() -> None: + # Remove directory_id from knowledge_file + with op.batch_alter_table('knowledge_file') as batch: + batch.drop_index('ix_knowledge_file_directory_id') + batch.drop_constraint('fk_knowledge_file_directory_id', type_='foreignkey') + batch.drop_column('directory_id') + + # Drop knowledge_directory table + op.drop_index('ix_knowledge_directory_parent_id', table_name='knowledge_directory') + op.drop_index('ix_knowledge_directory_knowledge_id', table_name='knowledge_directory') + op.drop_table('knowledge_directory') diff --git a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py index 82249bb278..70c0562749 100644 --- a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py +++ b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py @@ -6,50 +6,56 @@ Create Date: 2025-12-02 06:54:19.401334 """ +import json +import time +import uuid from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa -from sqlalchemy import inspect import open_webui.internal.db - -import time -import json -import uuid +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect # revision identifiers, used by Alembic. -revision: str = "3e0e00844bb0" -down_revision: Union[str, None] = "90ef40d4714e" +revision: str = '3e0e00844bb0' +down_revision: Union[str, None] = '90ef40d4714e' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'knowledge_file' in existing_tables: + return # Already created — skip everything + op.create_table( - "knowledge_file", - sa.Column("id", sa.Text(), primary_key=True), - sa.Column("user_id", sa.Text(), nullable=False), + 'knowledge_file', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), sa.Column( - "knowledge_id", + 'knowledge_id', sa.Text(), - sa.ForeignKey("knowledge.id", ondelete="CASCADE"), + sa.ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False, ), sa.Column( - "file_id", + 'file_id', sa.Text(), - sa.ForeignKey("file.id", ondelete="CASCADE"), + sa.ForeignKey('file.id', ondelete='CASCADE'), nullable=False, ), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), # indexes - sa.Index("ix_knowledge_file_knowledge_id", "knowledge_id"), - sa.Index("ix_knowledge_file_file_id", "file_id"), - sa.Index("ix_knowledge_file_user_id", "user_id"), + sa.Index('ix_knowledge_file_knowledge_id', 'knowledge_id'), + sa.Index('ix_knowledge_file_file_id', 'file_id'), + sa.Index('ix_knowledge_file_user_id', 'user_id'), # unique constraints sa.UniqueConstraint( - "knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file" + 'knowledge_id', 'file_id', name='uq_knowledge_file_knowledge_file' ), # prevent duplicate entries ) @@ -57,35 +63,33 @@ def upgrade() -> None: # 2. Read existing group with user_ids JSON column knowledge_table = sa.Table( - "knowledge", + 'knowledge', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("user_id", sa.Text()), - sa.Column("data", sa.JSON()), # JSON stored as text in SQLite + PG + sa.Column('id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('data', sa.JSON()), # JSON stored as text in SQLite + PG ) results = connection.execute( - sa.select( - knowledge_table.c.id, knowledge_table.c.user_id, knowledge_table.c.data - ) + sa.select(knowledge_table.c.id, knowledge_table.c.user_id, knowledge_table.c.data) ).fetchall() # 3. Insert members into group_member table kf_table = sa.Table( - "knowledge_file", + 'knowledge_file', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("user_id", sa.Text()), - sa.Column("knowledge_id", sa.Text()), - sa.Column("file_id", sa.Text()), - sa.Column("created_at", sa.BigInteger()), - sa.Column("updated_at", sa.BigInteger()), + sa.Column('id', sa.Text()), + sa.Column('user_id', sa.Text()), + sa.Column('knowledge_id', sa.Text()), + sa.Column('file_id', sa.Text()), + sa.Column('created_at', sa.BigInteger()), + sa.Column('updated_at', sa.BigInteger()), ) file_table = sa.Table( - "file", + 'file', sa.MetaData(), - sa.Column("id", sa.Text()), + sa.Column('id', sa.Text()), ) now = int(time.time()) @@ -102,50 +106,48 @@ def upgrade() -> None: if not isinstance(data, dict): continue - file_ids = data.get("file_ids", []) + file_ids = data.get('file_ids', []) for file_id in file_ids: - file_exists = connection.execute( - sa.select(file_table.c.id).where(file_table.c.id == file_id) - ).fetchone() + file_exists = connection.execute(sa.select(file_table.c.id).where(file_table.c.id == file_id)).fetchone() if not file_exists: continue # skip non-existing files row = { - "id": str(uuid.uuid4()), - "user_id": user_id, - "knowledge_id": knowledge_id, - "file_id": file_id, - "created_at": now, - "updated_at": now, + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'knowledge_id': knowledge_id, + 'file_id': file_id, + 'created_at': now, + 'updated_at': now, } connection.execute(kf_table.insert().values(**row)) - with op.batch_alter_table("knowledge") as batch: - batch.drop_column("data") + with op.batch_alter_table('knowledge') as batch: + batch.drop_column('data') def downgrade() -> None: # 1. Add back the old data column - op.add_column("knowledge", sa.Column("data", sa.JSON(), nullable=True)) + op.add_column('knowledge', sa.Column('data', sa.JSON(), nullable=True)) connection = op.get_bind() # 2. Read knowledge_file entries and reconstruct data JSON knowledge_table = sa.Table( - "knowledge", + 'knowledge', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("data", sa.JSON()), + sa.Column('id', sa.Text()), + sa.Column('data', sa.JSON()), ) kf_table = sa.Table( - "knowledge_file", + 'knowledge_file', sa.MetaData(), - sa.Column("id", sa.Text()), - sa.Column("knowledge_id", sa.Text()), - sa.Column("file_id", sa.Text()), + sa.Column('id', sa.Text()), + sa.Column('knowledge_id', sa.Text()), + sa.Column('file_id', sa.Text()), ) results = connection.execute(sa.select(knowledge_table.c.id)).fetchall() @@ -157,13 +159,9 @@ def downgrade() -> None: file_ids_list = [fid for (fid,) in file_ids] - data_json = {"file_ids": file_ids_list} + data_json = {'file_ids': file_ids_list} - connection.execute( - knowledge_table.update() - .where(knowledge_table.c.id == knowledge_id) - .values(data=data_json) - ) + connection.execute(knowledge_table.update().where(knowledge_table.c.id == knowledge_id).values(data=data_json)) # 3. Drop the knowledge_file table - op.drop_table("knowledge_file") + op.drop_table('knowledge_file') diff --git a/backend/open_webui/migrations/versions/461111b60977_add_missing_primary_keys_to_legacy_.py b/backend/open_webui/migrations/versions/461111b60977_add_missing_primary_keys_to_legacy_.py new file mode 100644 index 0000000000..67b737c530 --- /dev/null +++ b/backend/open_webui/migrations/versions/461111b60977_add_missing_primary_keys_to_legacy_.py @@ -0,0 +1,64 @@ +"""add missing primary keys to legacy peewee tables + +Revision ID: 461111b60977 +Revises: 3c9b0ca343fd +Create Date: 2026-05-14 04:38:14.000000 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '461111b60977' +down_revision: Union[str, None] = '3c9b0ca343fd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +# Tables bootstrapped by the old Peewee migration layer that may have +# UNIQUE(id) but no PRIMARY KEY constraint. Fresh Alembic installs +# already have correct PKs from 7e5b5dc7342b_init.py. +# 'tag' uses a composite PK since the same tag name can exist for multiple users. +LEGACY_TABLES = { + 'auth': ['id'], 'chat': ['id'], 'chatidtag': ['id'], 'document': ['id'], + 'file': ['id'], 'function': ['id'], 'memory': ['id'], 'model': ['id'], + 'prompt': ['id'], 'tag': ['id', 'user_id'], 'tool': ['id'], 'user': ['id'], +} + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + for table_name, pk_columns in LEGACY_TABLES.items(): + if table_name not in existing_tables: + continue + + pk = inspector.get_pk_constraint(table_name) + pk_cols = pk.get('constrained_columns', []) + + # Already has the correct PK — nothing to do + if sorted(pk_cols) == sorted(pk_columns): + continue + + # Check that all PK columns exist + columns = {c['name'] for c in inspector.get_columns(table_name)} + if not all(c in columns for c in pk_columns): + continue + + print(f"Promoting UNIQUE(id) -> PRIMARY KEY({', '.join(pk_columns)}) for '{table_name}'") + + conn.execute(sa.text(f'DROP TABLE IF EXISTS _alembic_tmp_{table_name}')) + with op.batch_alter_table(table_name) as batch_op: + # Drop existing PK if any (e.g. on wrong column) + if pk_cols and pk.get('name'): + batch_op.drop_constraint(pk['name'], type_='primary') + + batch_op.create_primary_key(f'pk_{table_name}', pk_columns) + + +def downgrade() -> None: + # Downgrade is a no-op — we don't want to remove PKs + pass diff --git a/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py b/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py index 16f7967c8e..e1e3b32635 100644 --- a/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py +++ b/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py @@ -6,62 +6,78 @@ Create Date: 2024-10-23 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "4ace53fd72c8" -down_revision = "af906e964978" +revision = '4ace53fd72c8' +down_revision = 'af906e964978' branch_labels = None depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = {c['name']: c for c in inspector.get_columns('folder')} + + created_at_col = columns.get('created_at') + if not created_at_col: + return + + # Only convert if still DateTime — skip if already BigInteger + if isinstance(created_at_col['type'], sa.BigInteger): + return + # Perform safe alterations using batch operation - with op.batch_alter_table("folder", schema=None) as batch_op: + with op.batch_alter_table('folder', schema=None) as batch_op: # Step 1: Remove server defaults for created_at and updated_at batch_op.alter_column( - "created_at", + 'created_at', server_default=None, # Removing server default ) batch_op.alter_column( - "updated_at", + 'updated_at', server_default=None, # Removing server default ) # Step 2: Change the column types to BigInteger for created_at batch_op.alter_column( - "created_at", + 'created_at', type_=sa.BigInteger(), existing_type=sa.DateTime(), existing_nullable=False, - postgresql_using="extract(epoch from created_at)::bigint", # Conversion for PostgreSQL + postgresql_using='extract(epoch from created_at)::bigint', # Conversion for PostgreSQL ) # Change the column types to BigInteger for updated_at batch_op.alter_column( - "updated_at", + 'updated_at', type_=sa.BigInteger(), existing_type=sa.DateTime(), existing_nullable=False, - postgresql_using="extract(epoch from updated_at)::bigint", # Conversion for PostgreSQL + postgresql_using='extract(epoch from updated_at)::bigint', # Conversion for PostgreSQL ) def downgrade(): - # Downgrade: Convert columns back to DateTime and restore defaults - with op.batch_alter_table("folder", schema=None) as batch_op: + # Convert columns back to DateTime and restore defaults. Mirrors the + # upgrade's postgresql_using cast — without it, Postgres can't + # auto-cast BigInteger → timestamp and aborts with DatatypeMismatch. + with op.batch_alter_table('folder', schema=None) as batch_op: batch_op.alter_column( - "created_at", + 'created_at', type_=sa.DateTime(), existing_type=sa.BigInteger(), existing_nullable=False, - server_default=sa.func.now(), # Restoring server default on downgrade + server_default=sa.func.now(), + postgresql_using='to_timestamp(created_at)::timestamp without time zone', ) batch_op.alter_column( - "updated_at", + 'updated_at', type_=sa.DateTime(), existing_type=sa.BigInteger(), existing_nullable=False, - server_default=sa.func.now(), # Restoring server default on downgrade - onupdate=sa.func.now(), # Restoring onupdate behavior if it was there + server_default=sa.func.now(), + onupdate=sa.func.now(), + postgresql_using='to_timestamp(updated_at)::timestamp without time zone', ) diff --git a/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py b/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py new file mode 100644 index 0000000000..a7b019f35a --- /dev/null +++ b/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py @@ -0,0 +1,83 @@ +"""add pinned_note table + +Revision ID: 4de81c2a3af1 +Revises: 56359461a091 +Create Date: 2026-05-09 04:29:27.651341 + +""" + +from typing import Sequence, Union + +import open_webui.internal.db +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '4de81c2a3af1' +down_revision: Union[str, None] = '56359461a091' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +import time +import uuid + +from sqlalchemy import insert, select, update +from sqlalchemy.sql import column, table + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'pinned_note' not in existing_tables: + op.create_table( + 'pinned_note', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note'), + ) + + note_table = table('note', column('id', sa.Text), column('user_id', sa.Text), column('is_pinned', sa.Boolean)) + + pinned_note_table = table( + 'pinned_note', + column('id', sa.Text), + column('user_id', sa.Text), + column('note_id', sa.Text), + column('created_at', sa.BigInteger), + ) + + notes = conn.execute(select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True)).fetchall() + + if notes: + now = int(time.time_ns()) + conn.execute( + insert(pinned_note_table), + [{'id': str(uuid.uuid4()), 'user_id': note[1], 'note_id': note[0], 'created_at': now} for note in notes], + ) + + with op.batch_alter_table('note', schema=None) as batch_op: + batch_op.drop_column('is_pinned') + + +def downgrade() -> None: + with op.batch_alter_table('note', schema=None) as batch_op: + batch_op.add_column(sa.Column('is_pinned', sa.Boolean(), nullable=True)) + + conn = op.get_bind() + + note_table = table('note', column('id', sa.Text), column('is_pinned', sa.Boolean)) + + pinned_note_table = table('pinned_note', column('note_id', sa.Text)) + + notes = conn.execute(select(pinned_note_table.c.note_id)).fetchall() + + for note in notes: + conn.execute(update(note_table).where(note_table.c.id == note[0]).values(is_pinned=True)) + + op.drop_table('pinned_note') diff --git a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py new file mode 100644 index 0000000000..571390a6fa --- /dev/null +++ b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py @@ -0,0 +1,108 @@ +"""add calendar tables + +Revision ID: 56359461a091 +Revises: c1d2e3f4a5b6 +Create Date: 2026-04-19 16:20:58.162045 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '56359461a091' +down_revision: Union[str, None] = 'c1d2e3f4a5b6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _index_exists(inspector, index_name, table_name): + """Check if an index already exists on the given table.""" + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + if 'calendar' not in tables: + op.create_table( + 'calendar', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) + + inspector.clear_cache() + if 'calendar' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_user', 'calendar'): + op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False) + + if 'calendar_event' not in tables: + op.create_table( + 'calendar_event', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('calendar_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('start_at', sa.BigInteger(), nullable=False), + sa.Column('end_at', sa.BigInteger(), nullable=True), + sa.Column('all_day', sa.Boolean(), nullable=False), + sa.Column('rrule', sa.Text(), nullable=True), + sa.Column('color', sa.Text(), nullable=True), + sa.Column('location', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_cancelled', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + ) + + inspector.clear_cache() + if 'calendar_event' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_event_calendar', 'calendar_event'): + op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False) + if not _index_exists(inspector, 'ix_calendar_event_user_date', 'calendar_event'): + op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False) + + if 'calendar_event_attendee' not in tables: + op.create_table( + 'calendar_event_attendee', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('event_id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), + ) + + inspector.clear_cache() + if 'calendar_event_attendee' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_event_attendee_user', 'calendar_event_attendee'): + op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False) + + +def downgrade() -> None: + op.drop_index('ix_calendar_event_attendee_user', table_name='calendar_event_attendee') + op.drop_table('calendar_event_attendee') + op.drop_index('ix_calendar_event_user_date', table_name='calendar_event') + op.drop_index('ix_calendar_event_calendar', table_name='calendar_event') + op.drop_table('calendar_event') + op.drop_index('ix_calendar_user', table_name='calendar') + op.drop_table('calendar') diff --git a/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py b/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py index 54176dc46e..4db23c2ace 100644 --- a/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py +++ b/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py @@ -6,43 +6,49 @@ Create Date: 2024-12-22 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "57c599a3cb57" -down_revision = "922e7a387820" +revision = '57c599a3cb57' +down_revision = '922e7a387820' branch_labels = None depends_on = None def upgrade(): - op.create_table( - "channel", - sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True), - sa.Column("user_id", sa.Text()), - sa.Column("name", sa.Text()), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("data", sa.JSON(), nullable=True), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("access_control", sa.JSON(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - op.create_table( - "message", - sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True), - sa.Column("user_id", sa.Text()), - sa.Column("channel_id", sa.Text(), nullable=True), - sa.Column("content", sa.Text()), - sa.Column("data", sa.JSON(), nullable=True), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - ) + if 'channel' not in existing_tables: + op.create_table( + 'channel', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text()), + sa.Column('name', sa.Text()), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + if 'message' not in existing_tables: + op.create_table( + 'message', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text()), + sa.Column('channel_id', sa.Text(), nullable=True), + sa.Column('content', sa.Text()), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) def downgrade(): - op.drop_table("channel") + op.drop_table('channel') - op.drop_table("message") + op.drop_table('message') diff --git a/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py b/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py index 59fe57a421..7f2913bd5e 100644 --- a/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py +++ b/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py @@ -8,47 +8,49 @@ Create Date: 2025-12-10 15:11:39.424601 from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa import open_webui.internal.db - +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "6283dc0e4d8d" -down_revision: Union[str, None] = "3e0e00844bb0" +revision: str = '6283dc0e4d8d' +down_revision: Union[str, None] = '3e0e00844bb0' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.create_table( - "channel_file", - sa.Column("id", sa.Text(), primary_key=True), - sa.Column("user_id", sa.Text(), nullable=False), - sa.Column( - "channel_id", - sa.Text(), - sa.ForeignKey("channel.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "file_id", - sa.Text(), - sa.ForeignKey("file.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=False), - # indexes - sa.Index("ix_channel_file_channel_id", "channel_id"), - sa.Index("ix_channel_file_file_id", "file_id"), - sa.Index("ix_channel_file_user_id", "user_id"), - # unique constraints - sa.UniqueConstraint( - "channel_id", "file_id", name="uq_channel_file_channel_file" - ), # prevent duplicate entries - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'channel_file' not in existing_tables: + op.create_table( + 'channel_file', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'channel_id', + sa.Text(), + sa.ForeignKey('channel.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column( + 'file_id', + sa.Text(), + sa.ForeignKey('file.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + # indexes + sa.Index('ix_channel_file_channel_id', 'channel_id'), + sa.Index('ix_channel_file_file_id', 'file_id'), + sa.Index('ix_channel_file_user_id', 'user_id'), + # unique constraints + sa.UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'), # prevent duplicate entries + ) def downgrade() -> None: - op.drop_table("channel_file") + op.drop_table('channel_file') diff --git a/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py b/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py index 881e6ae641..b1fd2ab40a 100644 --- a/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py +++ b/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py @@ -6,75 +6,80 @@ Create Date: 2024-10-01 14:02:35.241684 """ -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import table, column, select import json +import sqlalchemy as sa +from alembic import op +from sqlalchemy.sql import column, select, table -revision = "6a39f3d8e55c" -down_revision = "c0fbf31ca0db" +revision = '6a39f3d8e55c' +down_revision = 'c0fbf31ca0db' branch_labels = None depends_on = None def upgrade(): - # Creating the 'knowledge' table - print("Creating knowledge table") - knowledge_table = op.create_table( - "knowledge", - sa.Column("id", sa.Text(), primary_key=True), - sa.Column("user_id", sa.Text(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("data", sa.JSON(), nullable=True), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - print("Migrating data from document table to knowledge table") - # Representation of the existing 'document' table - document_table = table( - "document", - column("collection_name", sa.String()), - column("user_id", sa.String()), - column("name", sa.String()), - column("title", sa.Text()), - column("content", sa.Text()), - column("timestamp", sa.BigInteger()), - ) - - # Select all from existing document table - documents = op.get_bind().execute( - select( - document_table.c.collection_name, - document_table.c.user_id, - document_table.c.name, - document_table.c.title, - document_table.c.content, - document_table.c.timestamp, + if 'knowledge' not in existing_tables: + # Creating the 'knowledge' table + print('Creating knowledge table') + knowledge_table = op.create_table( + 'knowledge', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=True), ) - ) - # Insert data into knowledge table from document table - for doc in documents: - op.get_bind().execute( - knowledge_table.insert().values( - id=doc.collection_name, - user_id=doc.user_id, - description=doc.name, - meta={ - "legacy": True, - "document": True, - "tags": json.loads(doc.content or "{}").get("tags", []), - }, - name=doc.title, - created_at=doc.timestamp, - updated_at=doc.timestamp, # using created_at for both created_at and updated_at in project + print('Migrating data from document table to knowledge table') + # Representation of the existing 'document' table + document_table = table( + 'document', + column('collection_name', sa.String()), + column('user_id', sa.String()), + column('name', sa.String()), + column('title', sa.Text()), + column('content', sa.Text()), + column('timestamp', sa.BigInteger()), + ) + + # Select all from existing document table + documents = conn.execute( + select( + document_table.c.collection_name, + document_table.c.user_id, + document_table.c.name, + document_table.c.title, + document_table.c.content, + document_table.c.timestamp, ) ) + # Insert data into knowledge table from document table + for doc in documents: + conn.execute( + knowledge_table.insert().values( + id=doc.collection_name, + user_id=doc.user_id, + description=doc.name, + meta={ + 'legacy': True, + 'document': True, + 'tags': json.loads(doc.content or '{}').get('tags', []), + }, + name=doc.title, + created_at=doc.timestamp, + updated_at=doc.timestamp, + ) + ) + def downgrade(): - op.drop_table("knowledge") + op.drop_table('knowledge') diff --git a/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py b/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py index c8afe9d51a..b3313e3762 100644 --- a/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py +++ b/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py @@ -6,21 +6,26 @@ Create Date: 2024-12-23 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "7826ab40b532" -down_revision = "57c599a3cb57" +revision = '7826ab40b532' +down_revision = '57c599a3cb57' branch_labels = None depends_on = None def upgrade(): - op.add_column( - "file", - sa.Column("access_control", sa.JSON(), nullable=True), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + file_cols = {c['name'] for c in inspector.get_columns('file')} + + if 'access_control' not in file_cols: + op.add_column( + 'file', + sa.Column('access_control', sa.JSON(), nullable=True), + ) def downgrade(): - op.drop_column("file", "access_control") + op.drop_column('file', 'access_control') diff --git a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py index 9e56282ef0..8f0500d8ec 100644 --- a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py +++ b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py @@ -1,204 +1,187 @@ -"""init - -Revision ID: 7e5b5dc7342b -Revises: -Create Date: 2024-06-24 13:15:33.808998 - -""" - -from typing import Sequence, Union - +# Initial bootstrap migration version. +# Revision ID: 7e5b5dc7342b +# Revises: (none) +# Created on: 2024-06-24 13:15:33.808998 +from __future__ import annotations +from typing import Sequence +import open_webui.internal.db # noqa: F401 import sqlalchemy as sa from alembic import op - -import open_webui.internal.db from open_webui.internal.db import JSONField from open_webui.migrations.util import get_existing_tables - -# revision identifiers, used by Alembic. revision: str = "7e5b5dc7342b" -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None +# Initial schema table declarations +_INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [ + ( + 'auth', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('email', sa.String(), nullable=True), + sa.Column('password', sa.Text(), nullable=True), + sa.Column('active', sa.Boolean(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'chat', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('chat', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('share_id', sa.Text(), nullable=True), + sa.Column('archived', sa.Boolean(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('share_id')], + ), + ( + 'chatidtag', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('tag_name', sa.String(), nullable=True), + sa.Column('chat_id', sa.String(), nullable=True), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('timestamp', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'document', + [ + sa.Column('collection_name', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('filename', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('timestamp', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('collection_name'), sa.UniqueConstraint('name')], + ), + ( + 'file', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('filename', sa.Text(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'function', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('type', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('valves', JSONField(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_global', sa.Boolean(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'memory', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'model', + [ + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=True), + sa.Column('base_model_id', sa.Text(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('params', JSONField(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'prompt', + [ + sa.Column('command', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('timestamp', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('command')], + ), + ( + 'tag', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('data', sa.Text(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'tool', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('user_id', sa.String(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('specs', JSONField(), nullable=True), + sa.Column('meta', JSONField(), nullable=True), + sa.Column('valves', JSONField(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + ], + [sa.PrimaryKeyConstraint('id')], + ), + ( + 'user', + [ + sa.Column('id', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('email', sa.String(), nullable=True), + sa.Column('role', sa.String(), nullable=True), + sa.Column('profile_image_url', sa.Text(), nullable=True), + sa.Column('last_active_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('api_key', sa.String(), nullable=True), + sa.Column('settings', JSONField(), nullable=True), + sa.Column('info', JSONField(), nullable=True), + sa.Column('oauth_sub', sa.Text(), nullable=True), + ], + [ + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('api_key'), + sa.UniqueConstraint('oauth_sub'), + ], + ), +] +# --- migration execution --- +def upgrade() -> None: # deploy initial schema tables existing_tables = set(get_existing_tables()) - - # ### commands auto generated by Alembic - please adjust! ### - if "auth" not in existing_tables: - op.create_table( - "auth", - sa.Column("id", sa.String(), nullable=False), - sa.Column("email", sa.String(), nullable=True), - sa.Column("password", sa.Text(), nullable=True), - sa.Column("active", sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "chat" not in existing_tables: - op.create_table( - "chat", - sa.Column("id", sa.String(), nullable=False), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("title", sa.Text(), nullable=True), - sa.Column("chat", sa.Text(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.Column("share_id", sa.Text(), nullable=True), - sa.Column("archived", sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("share_id"), - ) - - if "chatidtag" not in existing_tables: - op.create_table( - "chatidtag", - sa.Column("id", sa.String(), nullable=False), - sa.Column("tag_name", sa.String(), nullable=True), - sa.Column("chat_id", sa.String(), nullable=True), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("timestamp", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "document" not in existing_tables: - op.create_table( - "document", - sa.Column("collection_name", sa.String(), nullable=False), - sa.Column("name", sa.String(), nullable=True), - sa.Column("title", sa.Text(), nullable=True), - sa.Column("filename", sa.Text(), nullable=True), - sa.Column("content", sa.Text(), nullable=True), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("timestamp", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("collection_name"), - sa.UniqueConstraint("name"), - ) - - if "file" not in existing_tables: - op.create_table( - "file", - sa.Column("id", sa.String(), nullable=False), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("filename", sa.Text(), nullable=True), - sa.Column("meta", JSONField(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "function" not in existing_tables: - op.create_table( - "function", - sa.Column("id", sa.String(), nullable=False), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("name", sa.Text(), nullable=True), - sa.Column("type", sa.Text(), nullable=True), - sa.Column("content", sa.Text(), nullable=True), - sa.Column("meta", JSONField(), nullable=True), - sa.Column("valves", JSONField(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("is_global", sa.Boolean(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "memory" not in existing_tables: - op.create_table( - "memory", - sa.Column("id", sa.String(), nullable=False), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("content", sa.Text(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "model" not in existing_tables: - op.create_table( - "model", - sa.Column("id", sa.Text(), nullable=False), - sa.Column("user_id", sa.Text(), nullable=True), - sa.Column("base_model_id", sa.Text(), nullable=True), - sa.Column("name", sa.Text(), nullable=True), - sa.Column("params", JSONField(), nullable=True), - sa.Column("meta", JSONField(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "prompt" not in existing_tables: - op.create_table( - "prompt", - sa.Column("command", sa.String(), nullable=False), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("title", sa.Text(), nullable=True), - sa.Column("content", sa.Text(), nullable=True), - sa.Column("timestamp", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("command"), - ) - - if "tag" not in existing_tables: - op.create_table( - "tag", - sa.Column("id", sa.String(), nullable=False), - sa.Column("name", sa.String(), nullable=True), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("data", sa.Text(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "tool" not in existing_tables: - op.create_table( - "tool", - sa.Column("id", sa.String(), nullable=False), - sa.Column("user_id", sa.String(), nullable=True), - sa.Column("name", sa.Text(), nullable=True), - sa.Column("content", sa.Text(), nullable=True), - sa.Column("specs", JSONField(), nullable=True), - sa.Column("meta", JSONField(), nullable=True), - sa.Column("valves", JSONField(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - - if "user" not in existing_tables: - op.create_table( - "user", - sa.Column("id", sa.String(), nullable=False), - sa.Column("name", sa.String(), nullable=True), - sa.Column("email", sa.String(), nullable=True), - sa.Column("role", sa.String(), nullable=True), - sa.Column("profile_image_url", sa.Text(), nullable=True), - sa.Column("last_active_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("api_key", sa.String(), nullable=True), - sa.Column("settings", JSONField(), nullable=True), - sa.Column("info", JSONField(), nullable=True), - sa.Column("oauth_sub", sa.Text(), nullable=True), - sa.PrimaryKeyConstraint("id"), - sa.UniqueConstraint("api_key"), - sa.UniqueConstraint("oauth_sub"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("user") - op.drop_table("tool") - op.drop_table("tag") - op.drop_table("prompt") - op.drop_table("model") - op.drop_table("memory") - op.drop_table("function") - op.drop_table("file") - op.drop_table("document") - op.drop_table("chatidtag") - op.drop_table("chat") - op.drop_table("auth") - # ### end Alembic commands ### + for name, columns, constraints in _INITIAL_TABLES: + if name not in existing_tables: + op.create_table(name, *columns, *constraints) +# --- rollback function --- +def downgrade() -> None: # rollback initial schema tables + for table_name, _, _ in reversed(_INITIAL_TABLES): + op.drop_table(table_name) diff --git a/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py b/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py index 181b280666..2294f89562 100644 --- a/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py +++ b/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py @@ -8,42 +8,46 @@ Create Date: 2025-12-10 16:07:58.001282 from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa import open_webui.internal.db - +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "81cc2ce44d79" -down_revision: Union[str, None] = "6283dc0e4d8d" +revision: str = '81cc2ce44d79' +down_revision: Union[str, None] = '6283dc0e4d8d' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + # Add message_id column to channel_file table - with op.batch_alter_table("channel_file", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "message_id", - sa.Text(), - sa.ForeignKey( - "message.id", ondelete="CASCADE", name="fk_channel_file_message_id" - ), - nullable=True, + cf_cols = {c['name'] for c in inspector.get_columns('channel_file')} + if 'message_id' not in cf_cols: + with op.batch_alter_table('channel_file', schema=None) as batch_op: + batch_op.add_column( + sa.Column( + 'message_id', + sa.Text(), + sa.ForeignKey('message.id', ondelete='CASCADE', name='fk_channel_file_message_id'), + nullable=True, + ) ) - ) # Add data column to knowledge table - with op.batch_alter_table("knowledge", schema=None) as batch_op: - batch_op.add_column(sa.Column("data", sa.JSON(), nullable=True)) + k_cols = {c['name'] for c in inspector.get_columns('knowledge')} + if 'data' not in k_cols: + with op.batch_alter_table('knowledge', schema=None) as batch_op: + batch_op.add_column(sa.Column('data', sa.JSON(), nullable=True)) def downgrade() -> None: # Remove message_id column from channel_file table - with op.batch_alter_table("channel_file", schema=None) as batch_op: - batch_op.drop_column("message_id") + with op.batch_alter_table('channel_file', schema=None) as batch_op: + batch_op.drop_column('message_id') # Remove data column from knowledge table - with op.batch_alter_table("knowledge", schema=None) as batch_op: - batch_op.drop_column("data") + with op.batch_alter_table('knowledge', schema=None) as batch_op: + batch_op.drop_column('data') diff --git a/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py new file mode 100644 index 0000000000..e247e5788e --- /dev/null +++ b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py @@ -0,0 +1,226 @@ +"""Add chat_message table + +Revision ID: 8452d01d26d7 +Revises: 374d2f66af06 +Create Date: 2026-02-01 04:00:00.000000 + +""" + +import json +import logging +import time +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +log = logging.getLogger(__name__) + +revision: str = '8452d01d26d7' +down_revision: Union[str, None] = '374d2f66af06' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +BATCH_SIZE = 5000 + + +def _flush_batch(conn, table, batch): + """ + Insert a batch of messages, falling back to row-by-row on error. + + Tries a single bulk insert first (fast path). If that fails (e.g. due to + a duplicate key), falls back to individual inserts wrapped in savepoints + so the rest of the batch can still succeed. + """ + savepoint = conn.begin_nested() + try: + conn.execute(sa.insert(table), batch) + savepoint.commit() + return len(batch), 0 + except Exception: + savepoint.rollback() + # Batch failed - insert one-by-one to isolate the bad row(s) + inserted = 0 + failed = 0 + for msg in batch: + sp = conn.begin_nested() + try: + conn.execute(sa.insert(table).values(**msg)) + sp.commit() + inserted += 1 + except Exception as e: + sp.rollback() + failed += 1 + log.warning(f'Failed to insert message {msg["id"]}: {e}') + return inserted, failed + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'chat_message' in existing_tables: + return # Already created — skip everything + + # Step 1: Create table + op.create_table( + 'chat_message', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('chat_id', sa.Text(), nullable=False, index=True), + sa.Column('user_id', sa.Text(), index=True), + sa.Column('role', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('content', sa.JSON(), nullable=True), + sa.Column('output', sa.JSON(), nullable=True), + sa.Column('model_id', sa.Text(), nullable=True, index=True), + sa.Column('files', sa.JSON(), nullable=True), + sa.Column('sources', sa.JSON(), nullable=True), + sa.Column('embeds', sa.JSON(), nullable=True), + sa.Column('done', sa.Boolean(), default=True), + sa.Column('status_history', sa.JSON(), nullable=True), + sa.Column('error', sa.JSON(), nullable=True), + sa.Column('usage', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), index=True), + sa.Column('updated_at', sa.BigInteger()), + sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], ondelete='CASCADE'), + ) + + # Create composite indexes + op.create_index('chat_message_chat_parent_idx', 'chat_message', ['chat_id', 'parent_id']) + op.create_index('chat_message_model_created_idx', 'chat_message', ['model_id', 'created_at']) + op.create_index('chat_message_user_created_idx', 'chat_message', ['user_id', 'created_at']) + + # Step 2: Backfill from existing chats + chat_table = sa.table( + 'chat', + sa.column('id', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('chat', sa.JSON()), + ) + + chat_message_table = sa.table( + 'chat_message', + sa.column('id', sa.Text()), + sa.column('chat_id', sa.Text()), + sa.column('user_id', sa.Text()), + sa.column('role', sa.Text()), + sa.column('parent_id', sa.Text()), + sa.column('content', sa.JSON()), + sa.column('output', sa.JSON()), + sa.column('model_id', sa.Text()), + sa.column('files', sa.JSON()), + sa.column('sources', sa.JSON()), + sa.column('embeds', sa.JSON()), + sa.column('done', sa.Boolean()), + sa.column('status_history', sa.JSON()), + sa.column('error', sa.JSON()), + sa.column('usage', sa.JSON()), + sa.column('created_at', sa.BigInteger()), + sa.column('updated_at', sa.BigInteger()), + ) + + # Stream rows instead of loading all into memory: + # - yield_per: fetches rows in chunks via cursor.fetchmany() (all backends) + # - stream_results: enables server-side cursors on PostgreSQL (no-op on SQLite) + result = conn.execute( + sa.select(chat_table.c.id, chat_table.c.user_id, chat_table.c.chat) + .where(~chat_table.c.user_id.like('shared-%')) + .execution_options(yield_per=1000, stream_results=True) + ) + + now = int(time.time()) + messages_batch = [] + total_inserted = 0 + total_failed = 0 + + for chat_row in result: + chat_id = chat_row[0] + user_id = chat_row[1] + chat_data = chat_row[2] + + if not chat_data: + continue + + # Handle both string and dict chat data + if isinstance(chat_data, str): + try: + chat_data = json.loads(chat_data) + except Exception: + continue + + history = chat_data.get('history', {}) + if not isinstance(history, dict): + continue + + messages = history.get('messages', {}) + if not isinstance(messages, dict): + continue + + for message_id, message in messages.items(): + if not isinstance(message, dict): + continue + + role = message.get('role') + if not role: + continue + + timestamp = message.get('timestamp', now) + + try: + timestamp = int(float(timestamp)) + except Exception as e: + timestamp = now + + # Normalize timestamp: convert ms to seconds, validate range + if timestamp > 10_000_000_000: + timestamp = timestamp // 1000 + # Must be after 2020 and not too far in the future + if timestamp < 1577836800 or timestamp > now + 86400: + timestamp = now + + messages_batch.append( + { + 'id': f'{chat_id}-{message_id}', + 'chat_id': chat_id, + 'user_id': user_id, + 'role': role, + 'parent_id': message.get('parentId'), + 'content': message.get('content'), + 'output': message.get('output'), + 'model_id': message.get('model'), + 'files': message.get('files'), + 'sources': message.get('sources'), + 'embeds': message.get('embeds'), + 'done': message.get('done', True), + 'status_history': message.get('statusHistory'), + 'error': message.get('error'), + 'usage': message.get('usage'), + 'created_at': timestamp, + 'updated_at': timestamp, + } + ) + + # Flush batch when full + if len(messages_batch) >= BATCH_SIZE: + inserted, failed = _flush_batch(conn, chat_message_table, messages_batch) + total_inserted += inserted + total_failed += failed + if total_inserted % 50000 < BATCH_SIZE: + log.info(f'Migration progress: {total_inserted} messages inserted...') + messages_batch.clear() + + # Flush remaining messages + if messages_batch: + inserted, failed = _flush_batch(conn, chat_message_table, messages_batch) + total_inserted += inserted + total_failed += failed + + log.info(f'Backfilled {total_inserted} messages into chat_message table ({total_failed} failed)') + + +def downgrade() -> None: + op.drop_index('chat_message_user_created_idx', table_name='chat_message') + op.drop_index('chat_message_model_created_idx', table_name='chat_message') + op.drop_index('chat_message_chat_parent_idx', table_name='chat_message') + op.drop_table('chat_message') diff --git a/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py b/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py index 8c52a4b22a..5936667624 100644 --- a/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py +++ b/backend/open_webui/migrations/versions/90ef40d4714e_update_channel_and_channel_members_table.py @@ -8,74 +8,80 @@ Create Date: 2025-11-30 06:33:38.790341 from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa import open_webui.internal.db - +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "90ef40d4714e" -down_revision: Union[str, None] = "b10670c03dd5" +revision: str = '90ef40d4714e' +down_revision: Union[str, None] = 'b10670c03dd5' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + # Update 'channel' table - op.add_column("channel", sa.Column("is_private", sa.Boolean(), nullable=True)) - - op.add_column("channel", sa.Column("archived_at", sa.BigInteger(), nullable=True)) - op.add_column("channel", sa.Column("archived_by", sa.Text(), nullable=True)) - - op.add_column("channel", sa.Column("deleted_at", sa.BigInteger(), nullable=True)) - op.add_column("channel", sa.Column("deleted_by", sa.Text(), nullable=True)) - - op.add_column("channel", sa.Column("updated_by", sa.Text(), nullable=True)) + channel_cols = {c['name'] for c in inspector.get_columns('channel')} + if 'is_private' not in channel_cols: + op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True)) + if 'archived_at' not in channel_cols: + op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True)) + if 'archived_by' not in channel_cols: + op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True)) + if 'deleted_at' not in channel_cols: + op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True)) + if 'deleted_by' not in channel_cols: + op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True)) + if 'updated_by' not in channel_cols: + op.add_column('channel', sa.Column('updated_by', sa.Text(), nullable=True)) # Update 'channel_member' table - op.add_column("channel_member", sa.Column("role", sa.Text(), nullable=True)) - op.add_column("channel_member", sa.Column("invited_by", sa.Text(), nullable=True)) - op.add_column( - "channel_member", sa.Column("invited_at", sa.BigInteger(), nullable=True) - ) + cm_cols = {c['name'] for c in inspector.get_columns('channel_member')} + if 'role' not in cm_cols: + op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True)) + if 'invited_by' not in cm_cols: + op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True)) + if 'invited_at' not in cm_cols: + op.add_column('channel_member', sa.Column('invited_at', sa.BigInteger(), nullable=True)) # Create 'channel_webhook' table - op.create_table( - "channel_webhook", - sa.Column("id", sa.Text(), primary_key=True, unique=True, nullable=False), - sa.Column("user_id", sa.Text(), nullable=False), - sa.Column( - "channel_id", - sa.Text(), - sa.ForeignKey("channel.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("profile_image_url", sa.Text(), nullable=True), - sa.Column("token", sa.Text(), nullable=False), - sa.Column("last_used_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=False), - ) - - pass + if 'channel_webhook' not in existing_tables: + op.create_table( + 'channel_webhook', + sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'channel_id', + sa.Text(), + sa.ForeignKey('channel.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('profile_image_url', sa.Text(), nullable=True), + sa.Column('token', sa.Text(), nullable=False), + sa.Column('last_used_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) def downgrade() -> None: # Downgrade 'channel' table - op.drop_column("channel", "is_private") - op.drop_column("channel", "archived_at") - op.drop_column("channel", "archived_by") - op.drop_column("channel", "deleted_at") - op.drop_column("channel", "deleted_by") - op.drop_column("channel", "updated_by") + op.drop_column('channel', 'is_private') + op.drop_column('channel', 'archived_at') + op.drop_column('channel', 'archived_by') + op.drop_column('channel', 'deleted_at') + op.drop_column('channel', 'deleted_by') + op.drop_column('channel', 'updated_by') # Downgrade 'channel_member' table - op.drop_column("channel_member", "role") - op.drop_column("channel_member", "invited_by") - op.drop_column("channel_member", "invited_at") + op.drop_column('channel_member', 'role') + op.drop_column('channel_member', 'invited_by') + op.drop_column('channel_member', 'invited_at') # Drop 'channel_webhook' table - op.drop_table("channel_webhook") - - pass + op.drop_table('channel_webhook') diff --git a/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py b/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py index a752115844..4150f5ffdf 100644 --- a/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py +++ b/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py @@ -6,80 +6,65 @@ Create Date: 2024-11-14 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "922e7a387820" -down_revision = "4ace53fd72c8" +revision = '922e7a387820' +down_revision = '4ace53fd72c8' branch_labels = None depends_on = None def upgrade(): - op.create_table( - "group", - sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True), - sa.Column("user_id", sa.Text(), nullable=True), - sa.Column("name", sa.Text(), nullable=True), - sa.Column("description", sa.Text(), nullable=True), - sa.Column("data", sa.JSON(), nullable=True), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("permissions", sa.JSON(), nullable=True), - sa.Column("user_ids", sa.JSON(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'group' not in existing_tables: + op.create_table( + 'group', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text(), nullable=True), + sa.Column('name', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('permissions', sa.JSON(), nullable=True), + sa.Column('user_ids', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) # Add 'access_control' column to 'model' table - op.add_column( - "model", - sa.Column("access_control", sa.JSON(), nullable=True), - ) - - # Add 'is_active' column to 'model' table - op.add_column( - "model", - sa.Column( - "is_active", - sa.Boolean(), - nullable=False, - server_default=sa.sql.expression.true(), - ), - ) + model_cols = {c['name'] for c in inspector.get_columns('model')} + if 'access_control' not in model_cols: + op.add_column('model', sa.Column('access_control', sa.JSON(), nullable=True)) + if 'is_active' not in model_cols: + op.add_column( + 'model', + sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()), + ) # Add 'access_control' column to 'knowledge' table - op.add_column( - "knowledge", - sa.Column("access_control", sa.JSON(), nullable=True), - ) + knowledge_cols = {c['name'] for c in inspector.get_columns('knowledge')} + if 'access_control' not in knowledge_cols: + op.add_column('knowledge', sa.Column('access_control', sa.JSON(), nullable=True)) # Add 'access_control' column to 'prompt' table - op.add_column( - "prompt", - sa.Column("access_control", sa.JSON(), nullable=True), - ) + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'access_control' not in prompt_cols: + op.add_column('prompt', sa.Column('access_control', sa.JSON(), nullable=True)) # Add 'access_control' column to 'tools' table - op.add_column( - "tool", - sa.Column("access_control", sa.JSON(), nullable=True), - ) + tool_cols = {c['name'] for c in inspector.get_columns('tool')} + if 'access_control' not in tool_cols: + op.add_column('tool', sa.Column('access_control', sa.JSON(), nullable=True)) def downgrade(): - op.drop_table("group") - - # Drop 'access_control' column from 'model' table - op.drop_column("model", "access_control") - - # Drop 'is_active' column from 'model' table - op.drop_column("model", "is_active") - - # Drop 'access_control' column from 'knowledge' table - op.drop_column("knowledge", "access_control") - - # Drop 'access_control' column from 'prompt' table - op.drop_column("prompt", "access_control") - - # Drop 'access_control' column from 'tools' table - op.drop_column("tool", "access_control") + op.drop_table('group') + op.drop_column('model', 'access_control') + op.drop_column('model', 'is_active') + op.drop_column('knowledge', 'access_control') + op.drop_column('prompt', 'access_control') + op.drop_column('tool', 'access_control') diff --git a/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py index 8e983a2cff..ae096e0808 100644 --- a/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py +++ b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py @@ -6,28 +6,33 @@ Create Date: 2025-05-03 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "9f0c9cd09105" -down_revision = "3781e22d8b01" +revision = '9f0c9cd09105' +down_revision = '3781e22d8b01' branch_labels = None depends_on = None def upgrade(): - op.create_table( - "note", - sa.Column("id", sa.Text(), nullable=False, primary_key=True, unique=True), - sa.Column("user_id", sa.Text(), nullable=True), - sa.Column("title", sa.Text(), nullable=True), - sa.Column("data", sa.JSON(), nullable=True), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("access_control", sa.JSON(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=True), - sa.Column("updated_at", sa.BigInteger(), nullable=True), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'note' not in existing_tables: + op.create_table( + 'note', + sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), + sa.Column('user_id', sa.Text(), nullable=True), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('access_control', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) def downgrade(): - op.drop_table("note") + op.drop_table('note') diff --git a/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py b/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py new file mode 100644 index 0000000000..b73e654149 --- /dev/null +++ b/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py @@ -0,0 +1,28 @@ +"""Add memory user_id index + +Revision ID: a0b1c2d3e4f5 +Revises: 4de81c2a3af1 +Create Date: 2025-09-15 03:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +revision = 'a0b1c2d3e4f5' +down_revision = '4de81c2a3af1' +branch_labels = None +depends_on = None + + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_indexes = {idx['name'] for idx in inspector.get_indexes('memory')} + + if 'ix_memory_user_id' not in existing_indexes: + op.create_index('ix_memory_user_id', 'memory', ['user_id']) + + +def downgrade(): + op.drop_index('ix_memory_user_id', table_name='memory') diff --git a/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py b/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py new file mode 100644 index 0000000000..1ffce041e1 --- /dev/null +++ b/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py @@ -0,0 +1,44 @@ +"""Add skill table + +Revision ID: a1b2c3d4e5f6 +Revises: f1e2d3c4b5a6 +Create Date: 2026-02-11 09:30:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from open_webui.migrations.util import get_existing_tables + +revision: str = 'a1b2c3d4e5f6' +down_revision: Union[str, None] = 'f1e2d3c4b5a6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + existing_tables = set(get_existing_tables()) + + if 'skill' not in existing_tables: + op.create_table( + 'skill', + sa.Column('id', sa.String(), nullable=False, primary_key=True), + sa.Column('user_id', sa.String(), nullable=False), + sa.Column('name', sa.Text(), nullable=False, unique=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + op.create_index('idx_skill_user_id', 'skill', ['user_id']) + op.create_index('idx_skill_updated_at', 'skill', ['updated_at']) + + +def downgrade() -> None: + op.drop_index('idx_skill_updated_at', table_name='skill') + op.drop_index('idx_skill_user_id', table_name='skill') + op.drop_table('skill') diff --git a/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py b/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py new file mode 100644 index 0000000000..5ccff52312 --- /dev/null +++ b/backend/open_webui/migrations/versions/a3dd5bedd151_add_tasks_and_summary_to_chat.py @@ -0,0 +1,34 @@ +"""Add tasks and summary columns to chat table + +Revision ID: a3dd5bedd151 +Revises: b2c3d4e5f6a7 +Create Date: 2026-03-29 22:15:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'a3dd5bedd151' +down_revision: Union[str, None] = 'b2c3d4e5f6a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('chat')] + + if 'tasks' not in columns: + op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True)) + if 'summary' not in columns: + op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('chat', 'summary') + op.drop_column('chat', 'tasks') diff --git a/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py b/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py index dd2b7d1a68..0108e44614 100644 --- a/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py +++ b/backend/open_webui/migrations/versions/a5c220713937_add_reply_to_id_column_to_message.py @@ -8,27 +8,29 @@ Create Date: 2025-09-27 02:24:18.058455 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "a5c220713937" -down_revision: Union[str, None] = "38d63c18f30f" +revision: str = 'a5c220713937' +down_revision: Union[str, None] = '38d63c18f30f' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + msg_cols = {c['name'] for c in inspector.get_columns('message')} + # Add 'reply_to_id' column to the 'message' table for replying to messages - op.add_column( - "message", - sa.Column("reply_to_id", sa.Text(), nullable=True), - ) - pass + if 'reply_to_id' not in msg_cols: + op.add_column( + 'message', + sa.Column('reply_to_id', sa.Text(), nullable=True), + ) def downgrade() -> None: # Remove 'reply_to_id' column from the 'message' table - op.drop_column("message", "reply_to_id") - - pass + op.drop_column('message', 'reply_to_id') diff --git a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py index 9116aa3884..65dcf6f5f9 100644 --- a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py +++ b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py @@ -6,46 +6,41 @@ Create Date: 2024-10-20 17:02:35.241684 """ -from alembic import op import sqlalchemy as sa +from alembic import op # Revision identifiers, used by Alembic. -revision = "af906e964978" -down_revision = "c29facfe716b" +revision = 'af906e964978' +down_revision = 'c29facfe716b' branch_labels = None depends_on = None def upgrade(): - # ### Create feedback table ### - op.create_table( - "feedback", - sa.Column( - "id", sa.Text(), primary_key=True - ), # Unique identifier for each feedback (TEXT type) - sa.Column( - "user_id", sa.Text(), nullable=True - ), # ID of the user providing the feedback (TEXT type) - sa.Column( - "version", sa.BigInteger(), default=0 - ), # Version of feedback (BIGINT type) - sa.Column("type", sa.Text(), nullable=True), # Type of feedback (TEXT type) - sa.Column("data", sa.JSON(), nullable=True), # Feedback data (JSON type) - sa.Column( - "meta", sa.JSON(), nullable=True - ), # Metadata for feedback (JSON type) - sa.Column( - "snapshot", sa.JSON(), nullable=True - ), # snapshot data for feedback (JSON type) - sa.Column( - "created_at", sa.BigInteger(), nullable=False - ), # Feedback creation timestamp (BIGINT representing epoch) - sa.Column( - "updated_at", sa.BigInteger(), nullable=False - ), # Feedback update timestamp (BIGINT representing epoch) - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'feedback' not in existing_tables: + # ### Create feedback table ### + op.create_table( + 'feedback', + sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type) + sa.Column('user_id', sa.Text(), nullable=True), # ID of the user providing the feedback (TEXT type) + sa.Column('version', sa.BigInteger(), default=0), # Version of feedback (BIGINT type) + sa.Column('type', sa.Text(), nullable=True), # Type of feedback (TEXT type) + sa.Column('data', sa.JSON(), nullable=True), # Feedback data (JSON type) + sa.Column('meta', sa.JSON(), nullable=True), # Metadata for feedback (JSON type) + sa.Column('snapshot', sa.JSON(), nullable=True), # snapshot data for feedback (JSON type) + sa.Column( + 'created_at', sa.BigInteger(), nullable=False + ), # Feedback creation timestamp (BIGINT representing epoch) + sa.Column( + 'updated_at', sa.BigInteger(), nullable=False + ), # Feedback update timestamp (BIGINT representing epoch) + ) def downgrade(): # ### Drop feedback table ### - op.drop_table("feedback") + op.drop_table('feedback') diff --git a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py index f35a382645..ccb12515f1 100644 --- a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py +++ b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py @@ -6,74 +6,97 @@ Create Date: 2025-11-28 04:55:31.737538 """ -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -import open_webui.internal.db import json import time +from typing import Sequence, Union + +import open_webui.internal.db +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision: str = "b10670c03dd5" -down_revision: Union[str, None] = "2f1211949ecc" +revision: str = 'b10670c03dd5' +down_revision: Union[str, None] = '2f1211949ecc' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None +# ── Ad-hoc table references for Core DML ───────────────────────────────── +# These are lightweight table() / column() references used only inside this +# migration for SELECT / UPDATE / INSERT — they do NOT create or alter +# anything on disk. + +_user = sa.table( + 'user', + sa.column('id', sa.Text), + sa.column('oauth_sub', sa.Text), + sa.column('oauth', sa.JSON), + sa.column('api_key', sa.Text), + sa.column('info', sa.Text), + sa.column('settings', sa.Text), +) + +_api_key = sa.table( + 'api_key', + sa.column('id', sa.Text), + sa.column('user_id', sa.Text), + sa.column('key', sa.Text), + sa.column('created_at', sa.BigInteger), + sa.column('updated_at', sa.BigInteger), +) + def _drop_sqlite_indexes_for_column(table_name, column_name, conn): """ - SQLite requires manual removal of any indexes referencing a column - before ALTER TABLE ... DROP COLUMN can succeed. + SQLite requires manual removal of any user-created indexes referencing + a column before ALTER TABLE ... DROP COLUMN can succeed. + + NOTE: PRAGMAs have no Core equivalent — raw text is unavoidable here. """ indexes = conn.execute(sa.text(f"PRAGMA index_list('{table_name}')")).fetchall() for idx in indexes: index_name = idx[1] # index name - # Get indexed columns - idx_info = conn.execute( - sa.text(f"PRAGMA index_info('{index_name}')") - ).fetchall() - - indexed_cols = [row[2] for row in idx_info] # col names + # Skip system-managed autoindexes (PK / UNIQUE constraints) — they + # cannot be dropped directly and will disappear when the column is + # removed via batch_alter_table. + if index_name.startswith('sqlite_autoindex_'): + continue + idx_info = conn.execute(sa.text(f"PRAGMA index_info('{index_name}')")).fetchall() + indexed_cols = [row[2] for row in idx_info] if column_name in indexed_cols: - conn.execute(sa.text(f"DROP INDEX IF EXISTS {index_name}")) + conn.execute(sa.text(f'DROP INDEX IF EXISTS {index_name}')) def _convert_column_to_json(table: str, column: str): conn = op.get_bind() dialect = conn.dialect.name + t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.Text)) + t_json = sa.column(f'{column}_json', sa.JSON) + # SQLite cannot ALTER COLUMN → must recreate column - if dialect == "sqlite": - # 1. Add temporary column - op.add_column(table, sa.Column(f"{column}_json", sa.JSON(), nullable=True)) + if dialect == 'sqlite': + op.add_column(table, sa.Column(f'{column}_json', sa.JSON(), nullable=True)) - # 2. Load old data - rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall() + rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall() - for row in rows: - uid, raw = row + for uid, raw in rows: if raw is None: parsed = None else: try: parsed = json.loads(raw) except Exception: - parsed = None # fallback safe behavior + parsed = None conn.execute( - sa.text(f'UPDATE "{table}" SET {column}_json = :val WHERE id = :id'), - {"val": json.dumps(parsed) if parsed else None, "id": uid}, + sa.update(sa.table(table, sa.column('id'), t_json)) + .where(sa.column('id') == uid) + .values({f'{column}_json': json.dumps(parsed) if parsed else None}) ) - # 3. Drop old TEXT column op.drop_column(table, column) - - # 4. Rename new JSON column → original name - op.alter_column(table, f"{column}_json", new_column_name=column) + op.alter_column(table, f'{column}_json', new_column_name=column) else: # PostgreSQL supports direct CAST @@ -81,7 +104,7 @@ def _convert_column_to_json(table: str, column: str): table, column, type_=sa.JSON(), - postgresql_using=f"{column}::json", + postgresql_using=f'{column}::json', ) @@ -89,163 +112,172 @@ def _convert_column_to_text(table: str, column: str): conn = op.get_bind() dialect = conn.dialect.name - if dialect == "sqlite": - op.add_column(table, sa.Column(f"{column}_text", sa.Text(), nullable=True)) + t = sa.table(table, sa.column('id', sa.Text), sa.column(column)) + t_text = sa.column(f'{column}_text', sa.Text) - rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall() + if dialect == 'sqlite': + op.add_column(table, sa.Column(f'{column}_text', sa.Text(), nullable=True)) + + rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall() for uid, raw in rows: conn.execute( - sa.text(f'UPDATE "{table}" SET {column}_text = :val WHERE id = :id'), - {"val": json.dumps(raw) if raw else None, "id": uid}, + sa.update(sa.table(table, sa.column('id'), t_text)) + .where(sa.column('id') == uid) + .values({f'{column}_text': json.dumps(raw) if raw else None}) ) op.drop_column(table, column) - op.alter_column(table, f"{column}_text", new_column_name=column) + op.alter_column(table, f'{column}_text', new_column_name=column) else: op.alter_column( table, column, type_=sa.Text(), - postgresql_using=f"to_json({column})::text", + postgresql_using=f'to_json({column})::text', ) def upgrade() -> None: - op.add_column( - "user", sa.Column("profile_banner_image_url", sa.Text(), nullable=True) - ) - op.add_column("user", sa.Column("timezone", sa.String(), nullable=True)) - - op.add_column("user", sa.Column("presence_state", sa.String(), nullable=True)) - op.add_column("user", sa.Column("status_emoji", sa.String(), nullable=True)) - op.add_column("user", sa.Column("status_message", sa.Text(), nullable=True)) - op.add_column( - "user", sa.Column("status_expires_at", sa.BigInteger(), nullable=True) - ) - - op.add_column("user", sa.Column("oauth", sa.JSON(), nullable=True)) - - # Convert info (TEXT/JSONField) → JSON - _convert_column_to_json("user", "info") - # Convert settings (TEXT/JSONField) → JSON - _convert_column_to_json("user", "settings") - - op.create_table( - "api_key", - sa.Column("id", sa.Text(), primary_key=True, unique=True), - sa.Column("user_id", sa.Text(), sa.ForeignKey("user.id", ondelete="CASCADE")), - sa.Column("key", sa.Text(), unique=True, nullable=False), - sa.Column("data", sa.JSON(), nullable=True), - sa.Column("expires_at", sa.BigInteger(), nullable=True), - sa.Column("last_used_at", sa.BigInteger(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=False), - ) - conn = op.get_bind() - users = conn.execute( - sa.text('SELECT id, oauth_sub FROM "user" WHERE oauth_sub IS NOT NULL') - ).fetchall() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + user_columns = {c['name'] for c in inspector.get_columns('user')} - for uid, oauth_sub in users: - if oauth_sub: - # Example formats supported: - # provider@sub - # plain sub (stored as {"oidc": {"sub": sub}}) - if "@" in oauth_sub: - provider, sub = oauth_sub.split("@", 1) - else: - provider, sub = "oidc", oauth_sub + # ── Add new columns (idempotent) ────────────────────────────────── + for col_name, col_type in [ + ('profile_banner_image_url', sa.Text()), + ('timezone', sa.String()), + ('presence_state', sa.String()), + ('status_emoji', sa.String()), + ('status_message', sa.Text()), + ('status_expires_at', sa.BigInteger()), + ('oauth', sa.JSON()), + ]: + if col_name not in user_columns: + op.add_column('user', sa.Column(col_name, col_type, nullable=True)) - oauth_json = json.dumps({provider: {"sub": sub}}) - conn.execute( - sa.text('UPDATE "user" SET oauth = :oauth WHERE id = :id'), - {"oauth": oauth_json, "id": uid}, - ) + # Convert info (TEXT/JSONField) → JSON (skip if already JSON) + user_col_types = {c['name']: c['type'] for c in inspector.get_columns('user')} + if isinstance(user_col_types.get('info'), sa.Text): + _convert_column_to_json('user', 'info') + # Convert settings (TEXT/JSONField) → JSON (skip if already JSON) + if isinstance(user_col_types.get('settings'), sa.Text): + _convert_column_to_json('user', 'settings') - users_with_keys = conn.execute( - sa.text('SELECT id, api_key FROM "user" WHERE api_key IS NOT NULL') - ).fetchall() - now = int(time.time()) + # ── Create api_key table (idempotent) ───────────────────────────── + if 'api_key' not in existing_tables: + op.create_table( + 'api_key', + sa.Column('id', sa.Text(), primary_key=True, unique=True), + sa.Column('user_id', sa.Text(), sa.ForeignKey('user.id', ondelete='CASCADE')), + sa.Column('key', sa.Text(), unique=True, nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.Column('expires_at', sa.BigInteger(), nullable=True), + sa.Column('last_used_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) - for uid, api_key in users_with_keys: - if api_key: - conn.execute( - sa.text( - """ - INSERT INTO api_key (id, user_id, key, created_at, updated_at) - VALUES (:id, :user_id, :key, :created_at, :updated_at) - """ - ), - { - "id": f"key_{uid}", - "user_id": uid, - "key": api_key, - "created_at": now, - "updated_at": now, - }, - ) + # ── Migrate oauth_sub → oauth JSON (only if old column still exists) + if 'oauth_sub' in user_columns: + rows = conn.execute( + sa.select(_user.c.id, _user.c.oauth_sub) + .where(_user.c.oauth_sub.is_not(None)) + ).fetchall() - if conn.dialect.name == "sqlite": - _drop_sqlite_indexes_for_column("user", "api_key", conn) - _drop_sqlite_indexes_for_column("user", "oauth_sub", conn) + for uid, oauth_sub in rows: + if oauth_sub: + provider, sub = (oauth_sub.split('@', 1) if '@' in oauth_sub + else ('oidc', oauth_sub)) + conn.execute( + sa.update(_user) + .where(_user.c.id == uid) + .values(oauth=json.dumps({provider: {'sub': sub}})) + ) - with op.batch_alter_table("user") as batch_op: - batch_op.drop_column("api_key") - batch_op.drop_column("oauth_sub") + # ── Migrate api_key column → api_key table (only if old column still exists) + if 'api_key' in user_columns: + rows = conn.execute( + sa.select(_user.c.id, _user.c.api_key) + .where(_user.c.api_key.is_not(None)) + ).fetchall() + now = int(time.time()) + + for uid, key_val in rows: + if key_val: + conn.execute( + sa.insert(_api_key).values( + id=f'key_{uid}', + user_id=uid, + key=key_val, + created_at=now, + updated_at=now, + ) + ) + + # ── Drop legacy columns (idempotent) ────────────────────────────── + cols_to_drop = {'api_key', 'oauth_sub'} & user_columns + if cols_to_drop: + if conn.dialect.name == 'sqlite': + for col in cols_to_drop: + _drop_sqlite_indexes_for_column('user', col, conn) + + with op.batch_alter_table('user') as batch_op: + for col in cols_to_drop: + batch_op.drop_column(col) def downgrade() -> None: - # --- 1. Restore old oauth_sub column --- - op.add_column("user", sa.Column("oauth_sub", sa.Text(), nullable=True)) + op.add_column('user', sa.Column('oauth_sub', sa.Text(), nullable=True)) conn = op.get_bind() - users = conn.execute( - sa.text('SELECT id, oauth FROM "user" WHERE oauth IS NOT NULL') + rows = conn.execute( + sa.select(_user.c.id, _user.c.oauth) + .where(_user.c.oauth.is_not(None)) ).fetchall() - for uid, oauth in users: + for uid, oauth in rows: try: data = json.loads(oauth) provider = list(data.keys())[0] - sub = data[provider].get("sub") - oauth_sub = f"{provider}@{sub}" + sub = data[provider].get('sub') + oauth_sub = f'{provider}@{sub}' except Exception: oauth_sub = None conn.execute( - sa.text('UPDATE "user" SET oauth_sub = :oauth_sub WHERE id = :id'), - {"oauth_sub": oauth_sub, "id": uid}, + sa.update(_user) + .where(_user.c.id == uid) + .values(oauth_sub=oauth_sub) ) - op.drop_column("user", "oauth") + op.drop_column('user', 'oauth') - # --- 2. Restore api_key field --- - op.add_column("user", sa.Column("api_key", sa.String(), nullable=True)) + # --- Restore api_key field --- + op.add_column('user', sa.Column('api_key', sa.String(), nullable=True)) - # Restore values from api_key - keys = conn.execute(sa.text("SELECT user_id, key FROM api_key")).fetchall() + keys = conn.execute(sa.select(_api_key.c.user_id, _api_key.c.key)).fetchall() for uid, key in keys: conn.execute( - sa.text('UPDATE "user" SET api_key = :key WHERE id = :id'), - {"key": key, "id": uid}, + sa.update(_user) + .where(_user.c.id == uid) + .values(api_key=key) ) - # Drop new table - op.drop_table("api_key") + op.drop_table('api_key') - with op.batch_alter_table("user") as batch_op: - batch_op.drop_column("profile_banner_image_url") - batch_op.drop_column("timezone") + with op.batch_alter_table('user') as batch_op: + batch_op.drop_column('profile_banner_image_url') + batch_op.drop_column('timezone') - batch_op.drop_column("presence_state") - batch_op.drop_column("status_emoji") - batch_op.drop_column("status_message") - batch_op.drop_column("status_expires_at") + batch_op.drop_column('presence_state') + batch_op.drop_column('status_emoji') + batch_op.drop_column('status_message') + batch_op.drop_column('status_expires_at') # Convert info (JSON) → TEXT - _convert_column_to_text("user", "info") + _convert_column_to_text('user', 'info') # Convert settings (JSON) → TEXT - _convert_column_to_text("user", "settings") + _convert_column_to_text('user', 'settings') diff --git a/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py b/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py new file mode 100644 index 0000000000..cf942ec8c0 --- /dev/null +++ b/backend/open_webui/migrations/versions/b2c3d4e5f6a7_add_scim_column_to_user_table.py @@ -0,0 +1,31 @@ +"""add scim column to user table + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f6 +Create Date: 2026-02-13 14:19:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'b2c3d4e5f6a7' +down_revision: Union[str, None] = 'a1b2c3d4e5f6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + user_cols = {c['name'] for c in inspector.get_columns('user')} + + if 'scim' not in user_cols: + op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('user', 'scim') diff --git a/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py b/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py new file mode 100644 index 0000000000..fd298b7929 --- /dev/null +++ b/backend/open_webui/migrations/versions/b7c8d9e0f1a2_add_last_read_at_to_chat.py @@ -0,0 +1,31 @@ +"""add last_read_at to chat + +Revision ID: b7c8d9e0f1a2 +Revises: d4e5f6a7b8c9 +Create Date: 2026-04-01 04:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'b7c8d9e0f1a2' +down_revision = 'd4e5f6a7b8c9' +branch_labels = None +depends_on = None + + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('chat')] + + if 'last_read_at' not in columns: + op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) + # Set existing chats to be marked as read + op.execute('UPDATE chat SET last_read_at = updated_at') + + +def downgrade(): + op.drop_column('chat', 'last_read_at') diff --git a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py index 5f7f2abf70..75eada2d3f 100644 --- a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py +++ b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py @@ -12,21 +12,28 @@ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision: str = "c0fbf31ca0db" -down_revision: Union[str, None] = "ca81bd47c050" +revision: str = 'c0fbf31ca0db' +down_revision: Union[str, None] = 'ca81bd47c050' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + file_cols = {c['name'] for c in inspector.get_columns('file')} + # ### commands auto generated by Alembic - please adjust! ### - op.add_column("file", sa.Column("hash", sa.Text(), nullable=True)) - op.add_column("file", sa.Column("data", sa.JSON(), nullable=True)) - op.add_column("file", sa.Column("updated_at", sa.BigInteger(), nullable=True)) + if 'hash' not in file_cols: + op.add_column('file', sa.Column('hash', sa.Text(), nullable=True)) + if 'data' not in file_cols: + op.add_column('file', sa.Column('data', sa.JSON(), nullable=True)) + if 'updated_at' not in file_cols: + op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True)) def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column("file", "updated_at") - op.drop_column("file", "data") - op.drop_column("file", "hash") + op.drop_column('file', 'updated_at') + op.drop_column('file', 'data') + op.drop_column('file', 'hash') diff --git a/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py new file mode 100644 index 0000000000..b70022e168 --- /dev/null +++ b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py @@ -0,0 +1,189 @@ +"""Add shared_chat table and migrate existing shares + +Revision ID: c1d2e3f4a5b6 +Revises: e1f2a3b4c5d6 +Create Date: 2026-04-16 23:00:00.000000 + +""" + +import time +import uuid + +import sqlalchemy as sa +from alembic import op + +revision = 'c1d2e3f4a5b6' +down_revision = 'e1f2a3b4c5d6' +branch_labels = None +depends_on = None + +# Lightweight table references for data migration (no ORM models needed) +chat_t = sa.table( + 'chat', + sa.column('id', sa.Text), + sa.column('user_id', sa.Text), + sa.column('title', sa.Text), + sa.column('chat', sa.JSON), + sa.column('share_id', sa.Text), + sa.column('created_at', sa.BigInteger), + sa.column('updated_at', sa.BigInteger), + sa.column('archived', sa.Boolean), + sa.column('meta', sa.JSON), +) + +shared_chat_t = sa.table( + 'shared_chat', + sa.column('id', sa.Text), + sa.column('chat_id', sa.Text), + sa.column('user_id', sa.Text), + sa.column('title', sa.Text), + sa.column('chat', sa.JSON), + sa.column('created_at', sa.BigInteger), + sa.column('updated_at', sa.BigInteger), +) + +chat_message_t = sa.table( + 'chat_message', + sa.column('chat_id', sa.Text), +) + +access_grant_t = sa.table( + 'access_grant', + sa.column('id', sa.Text), + sa.column('resource_type', sa.Text), + sa.column('resource_id', sa.Text), + sa.column('principal_type', sa.Text), + sa.column('principal_id', sa.Text), + sa.column('permission', sa.Text), + sa.column('created_at', sa.BigInteger), +) + + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + # 1. Create shared_chat table (idempotent) + if 'shared_chat' not in tables: + op.create_table( + 'shared_chat', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('title', sa.Text(), nullable=True), + sa.Column('chat', sa.JSON(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=True), + sa.Column('updated_at', sa.BigInteger(), nullable=True), + ) + + # 2. Migrate existing shared-* rows + shared_rows = conn.execute( + sa.select( + chat_t.c.id, + chat_t.c.user_id, + chat_t.c.title, + chat_t.c.chat, + chat_t.c.created_at, + chat_t.c.updated_at, + ).where(chat_t.c.user_id.like('shared-%')) + ).fetchall() + + for row in shared_rows: + share_token = row.id + original_chat_id = row.user_id.replace('shared-', '', 1) + + # Verify original chat still exists + original = conn.execute(sa.select(chat_t.c.user_id).where(chat_t.c.id == original_chat_id)).fetchone() + + if not original: + continue + + # Check if shared_chat record already exists (idempotent) + existing_shared = conn.execute( + sa.select(shared_chat_t.c.id).where( + shared_chat_t.c.id == share_token + ) + ).fetchone() + + if not existing_shared: + # Insert snapshot into shared_chat + conn.execute( + shared_chat_t.insert().values( + id=share_token, + chat_id=original_chat_id, + user_id=original.user_id, + title=row.title, + chat=row.chat, + created_at=row.created_at, + updated_at=row.updated_at, + ) + ) + + # Check if access_grant record already exists (idempotent) + existing_grant = conn.execute( + sa.select(access_grant_t.c.id).where( + sa.and_( + access_grant_t.c.resource_type == 'shared_chat', + access_grant_t.c.resource_id == original_chat_id, + access_grant_t.c.principal_type == 'user', + access_grant_t.c.principal_id == '*', + access_grant_t.c.permission == 'read', + ) + ) + ).fetchone() + + if not existing_grant: + # Create user:*:read grant for backward compat + conn.execute( + access_grant_t.insert().values( + id=str(uuid.uuid4()), + resource_type='shared_chat', + resource_id=original_chat_id, + principal_type='user', + principal_id='*', + permission='read', + created_at=row.created_at or int(time.time()), + ) + ) + + # 3. Clean up old phantom rows + conn.execute( + chat_message_t.delete().where( + chat_message_t.c.chat_id.in_(sa.select(chat_t.c.id).where(chat_t.c.user_id.like('shared-%'))) + ) + ) + conn.execute(chat_t.delete().where(chat_t.c.user_id.like('shared-%'))) + + +def downgrade(): + conn = op.get_bind() + + shared_rows = conn.execute( + sa.select( + shared_chat_t.c.id, + shared_chat_t.c.chat_id, + shared_chat_t.c.user_id, + shared_chat_t.c.title, + shared_chat_t.c.chat, + shared_chat_t.c.created_at, + shared_chat_t.c.updated_at, + ) + ).fetchall() + + for row in shared_rows: + conn.execute( + chat_t.insert().values( + id=row.id, + user_id=f'shared-{row.chat_id}', + title=row.title, + chat=row.chat, + created_at=row.created_at, + updated_at=row.updated_at, + archived=False, + meta={}, + ) + ) + + conn.execute(access_grant_t.delete().where(access_grant_t.c.resource_type == 'shared_chat')) + op.drop_table('shared_chat') diff --git a/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py b/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py index de82854b88..a08485f87f 100644 --- a/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py +++ b/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py @@ -6,42 +6,45 @@ Create Date: 2024-10-20 17:02:35.241684 """ -from alembic import op -import sqlalchemy as sa import json -from sqlalchemy.sql import table, column -from sqlalchemy import String, Text, JSON, and_ +import sqlalchemy as sa +from alembic import op +from sqlalchemy import JSON, String, Text, and_ +from sqlalchemy.sql import column, table -revision = "c29facfe716b" -down_revision = "c69f45358db4" +revision = 'c29facfe716b' +down_revision = 'c69f45358db4' branch_labels = None depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + file_cols = {c['name'] for c in inspector.get_columns('file')} + # 1. Add the `path` column to the "file" table. - op.add_column("file", sa.Column("path", sa.Text(), nullable=True)) + if 'path' not in file_cols: + op.add_column('file', sa.Column('path', sa.Text(), nullable=True)) # 2. Convert the `meta` column from Text/JSONField to `JSON()` # Use Alembic's default batch_op for dialect compatibility. - with op.batch_alter_table("file", schema=None) as batch_op: + with op.batch_alter_table('file', schema=None) as batch_op: batch_op.alter_column( - "meta", + 'meta', type_=sa.JSON(), existing_type=sa.Text(), existing_nullable=True, nullable=True, - postgresql_using="meta::json", + postgresql_using='meta::json', ) # 3. Migrate legacy data from `meta` JSONField # Fetch and process `meta` data from the table, add values to the new `path` column as necessary. # We will use SQLAlchemy core bindings to ensure safety across different databases. - file_table = table( - "file", column("id", String), column("meta", JSON), column("path", Text) - ) + file_table = table('file', column('id', String), column('meta', JSON), column('path', Text)) # Create connection to the database connection = op.get_bind() @@ -56,24 +59,18 @@ def upgrade(): # Iterate over each row to extract and update the `path` from `meta` column for row in results: - if "path" in row.meta: + if 'path' in row.meta: # Extract the `path` field from the `meta` JSON - path = row.meta.get("path") + path = row.meta.get('path') # Update the `file` table with the new `path` value - connection.execute( - file_table.update() - .where(file_table.c.id == row.id) - .values({"path": path}) - ) + connection.execute(file_table.update().where(file_table.c.id == row.id).values({'path': path})) def downgrade(): # 1. Remove the `path` column - op.drop_column("file", "path") + op.drop_column('file', 'path') # 2. Revert the `meta` column back to Text/JSONField - with op.batch_alter_table("file", schema=None) as batch_op: - batch_op.alter_column( - "meta", type_=sa.Text(), existing_type=sa.JSON(), existing_nullable=True - ) + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.alter_column('meta', type_=sa.Text(), existing_type=sa.JSON(), existing_nullable=True) diff --git a/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py b/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py index 20f4a6d7b6..b1d0859fe1 100644 --- a/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py +++ b/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py @@ -8,50 +8,50 @@ Create Date: 2025-12-21 20:27:41.694897 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. -revision: str = "c440947495f3" -down_revision: Union[str, None] = "81cc2ce44d79" +revision: str = 'c440947495f3' +down_revision: Union[str, None] = '81cc2ce44d79' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.create_table( - "chat_file", - sa.Column("id", sa.Text(), primary_key=True), - sa.Column("user_id", sa.Text(), nullable=False), - sa.Column( - "chat_id", - sa.Text(), - sa.ForeignKey("chat.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "file_id", - sa.Text(), - sa.ForeignKey("file.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("message_id", sa.Text(), nullable=True), - sa.Column("created_at", sa.BigInteger(), nullable=False), - sa.Column("updated_at", sa.BigInteger(), nullable=False), - # indexes - sa.Index("ix_chat_file_chat_id", "chat_id"), - sa.Index("ix_chat_file_file_id", "file_id"), - sa.Index("ix_chat_file_message_id", "message_id"), - sa.Index("ix_chat_file_user_id", "user_id"), - # unique constraints - sa.UniqueConstraint( - "chat_id", "file_id", name="uq_chat_file_chat_file" - ), # prevent duplicate entries - ) - pass + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'chat_file' not in existing_tables: + op.create_table( + 'chat_file', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column( + 'chat_id', + sa.Text(), + sa.ForeignKey('chat.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column( + 'file_id', + sa.Text(), + sa.ForeignKey('file.id', ondelete='CASCADE'), + nullable=False, + ), + sa.Column('message_id', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + # indexes + sa.Index('ix_chat_file_chat_id', 'chat_id'), + sa.Index('ix_chat_file_file_id', 'file_id'), + sa.Index('ix_chat_file_message_id', 'message_id'), + sa.Index('ix_chat_file_user_id', 'user_id'), + # unique constraints + sa.UniqueConstraint('chat_id', 'file_id', name='uq_chat_file_chat_file'), # prevent duplicate entries + ) def downgrade() -> None: - op.drop_table("chat_file") - pass + op.drop_table('chat_file') diff --git a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py index 83e0dc28ed..646074427e 100644 --- a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py +++ b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py @@ -6,45 +6,50 @@ Create Date: 2024-10-16 02:02:35.241684 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "c69f45358db4" -down_revision = "3ab32c4b8f59" +revision = 'c69f45358db4' +down_revision = '3ab32c4b8f59' branch_labels = None depends_on = None def upgrade(): - op.create_table( - "folder", - sa.Column("id", sa.Text(), nullable=False), - sa.Column("parent_id", sa.Text(), nullable=True), - sa.Column("user_id", sa.Text(), nullable=False), - sa.Column("name", sa.Text(), nullable=False), - sa.Column("items", sa.JSON(), nullable=True), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("is_expanded", sa.Boolean(), default=False, nullable=False), - sa.Column( - "created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=False, - server_default=sa.func.now(), - onupdate=sa.func.now(), - ), - sa.PrimaryKeyConstraint("id", "user_id"), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - op.add_column( - "chat", - sa.Column("folder_id", sa.Text(), nullable=True), - ) + if 'folder' not in existing_tables: + op.create_table( + 'folder', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('items', sa.JSON(), nullable=True), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_expanded', sa.Boolean(), default=False, nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now(), nullable=False), + sa.Column( + 'updated_at', + sa.DateTime(), + nullable=False, + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + sa.PrimaryKeyConstraint('id', 'user_id'), + ) + + chat_cols = {c['name'] for c in inspector.get_columns('chat')} + if 'folder_id' not in chat_cols: + op.add_column( + 'chat', + sa.Column('folder_id', sa.Text(), nullable=True), + ) def downgrade(): - op.drop_column("chat", "folder_id") + op.drop_column('chat', 'folder_id') - op.drop_table("folder") + op.drop_table('folder') diff --git a/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py index 1540aa6a7f..ff54232bdd 100644 --- a/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py +++ b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py @@ -1,9 +1,8 @@ -"""Add config table +"""Add config table. Revision ID: ca81bd47c050 Revises: 7e5b5dc7342b Create Date: 2024-08-25 15:26:35.241684 - """ from typing import Sequence, Union @@ -11,31 +10,40 @@ from typing import Sequence, Union import sqlalchemy as sa from alembic import op -# revision identifiers, used by Alembic. -revision: str = "ca81bd47c050" -down_revision: Union[str, None] = "7e5b5dc7342b" +revision: str = 'ca81bd47c050' +down_revision: Union[str, None] = '7e5b5dc7342b' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None -def upgrade(): - op.create_table( - "config", - sa.Column("id", sa.Integer, primary_key=True), - sa.Column("data", sa.JSON(), nullable=False), - sa.Column("version", sa.Integer, nullable=False), - sa.Column( - "created_at", sa.DateTime(), nullable=False, server_default=sa.func.now() - ), - sa.Column( - "updated_at", - sa.DateTime(), - nullable=True, - server_default=sa.func.now(), - onupdate=sa.func.now(), - ), - ) +def upgrade() -> None: + """Create a key-value config table with versioning.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'config' not in existing_tables: + op.create_table( + 'config', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('data', sa.JSON(), nullable=False), + sa.Column('version', sa.Integer, nullable=False), + sa.Column( + 'created_at', + sa.DateTime(), + nullable=False, + server_default=sa.func.now(), + ), + sa.Column( + 'updated_at', + sa.DateTime(), + nullable=True, + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + ) -def downgrade(): - op.drop_table("config") +def downgrade() -> None: + """Drop the config table.""" + op.drop_table('config') diff --git a/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py b/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py index 3c916964e9..60c2e6e9e0 100644 --- a/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py +++ b/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py @@ -6,18 +6,23 @@ Create Date: 2025-07-13 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op -revision = "d31026856c01" -down_revision = "9f0c9cd09105" +revision = 'd31026856c01' +down_revision = '9f0c9cd09105' branch_labels = None depends_on = None def upgrade(): - op.add_column("folder", sa.Column("data", sa.JSON(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + folder_cols = {c['name'] for c in inspector.get_columns('folder')} + + if 'data' not in folder_cols: + op.add_column('folder', sa.Column('data', sa.JSON(), nullable=True)) def downgrade(): - op.drop_column("folder", "data") + op.drop_column('folder', 'data') diff --git a/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py new file mode 100644 index 0000000000..1df1cf7092 --- /dev/null +++ b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py @@ -0,0 +1,75 @@ +"""add automation tables + +Revision ID: d4e5f6a7b8c9 +Revises: f1e2d3c4b5a6 +Create Date: 2026-03-30 +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +revision: str = 'd4e5f6a7b8c9' +down_revision: Union[str, None] = 'a3dd5bedd151' +branch_labels = None +depends_on = None + + +def _index_exists(inspector, index_name, table_name): + """Check if an index already exists on the given table (works for both SQLite and PostgreSQL).""" + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) + + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + if 'automation' not in tables: + op.create_table( + 'automation', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('data', sa.JSON(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, default=True), + sa.Column('last_run_at', sa.BigInteger(), nullable=True), + sa.Column('next_run_at', sa.BigInteger(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + ) + + inspector.clear_cache() + if 'automation' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_automation_next_run', 'automation'): + op.create_index('ix_automation_next_run', 'automation', ['next_run_at']) + + if 'automation_run' not in tables: + op.create_table( + 'automation_run', + sa.Column('id', sa.Text(), primary_key=True), + sa.Column('automation_id', sa.Text(), nullable=False), + sa.Column('chat_id', sa.Text(), nullable=True), + sa.Column('status', sa.Text(), nullable=False), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('created_at', sa.BigInteger(), nullable=False), + ) + + inspector.clear_cache() + if 'automation_run' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_automation_run_automation_id', 'automation_run'): + op.create_index( + 'ix_automation_run_automation_id', + 'automation_run', + ['automation_id'], + ) + + +def downgrade(): + op.drop_index('ix_automation_run_automation_id') + op.drop_table('automation_run') + op.drop_index('ix_automation_next_run') + op.drop_table('automation') diff --git a/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py b/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py new file mode 100644 index 0000000000..7232b11b83 --- /dev/null +++ b/backend/open_webui/migrations/versions/e1f2a3b4c5d6_add_is_pinned_to_note.py @@ -0,0 +1,28 @@ +"""Add is_pinned to note table + +Revision ID: e1f2a3b4c5d6 +Revises: b7c8d9e0f1a2 +Create Date: 2026-04-14 22:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +revision = 'e1f2a3b4c5d6' +down_revision = 'b7c8d9e0f1a2' +branch_labels = None +depends_on = None + + +def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('note')] + + if 'is_pinned' not in columns: + op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True)) + + +def downgrade(): + op.drop_column('note', 'is_pinned') diff --git a/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py b/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py new file mode 100644 index 0000000000..6a33d087b5 --- /dev/null +++ b/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py @@ -0,0 +1,348 @@ +"""Add access_grant table + +Revision ID: f1e2d3c4b5a6 +Revises: 8452d01d26d7 +Create Date: 2026-02-05 10:00:00.000000 + +Migrates from JSON access_control columns to normalized access_grant table. +Access control semantics: +- NULL: Public access (all users can read) -> insert user:* for read +- {}: Private/owner-only (no grants) -> insert nothing +- {read: {...}, write: {...}}: Custom permissions -> insert specific grants +""" + +import time +import uuid +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from open_webui.migrations.util import get_existing_tables + +revision: str = 'f1e2d3c4b5a6' +down_revision: Union[str, None] = '8452d01d26d7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + existing_tables = set(get_existing_tables()) + + # Create access_grant table + if 'access_grant' not in existing_tables: + op.create_table( + 'access_grant', + sa.Column('id', sa.Text(), nullable=False, primary_key=True), + sa.Column('resource_type', sa.Text(), nullable=False), + sa.Column('resource_id', sa.Text(), nullable=False), + sa.Column('principal_type', sa.Text(), nullable=False), + sa.Column('principal_id', sa.Text(), nullable=False), + sa.Column('permission', sa.Text(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.UniqueConstraint( + 'resource_type', + 'resource_id', + 'principal_type', + 'principal_id', + 'permission', + name='uq_access_grant_grant', + ), + ) + op.create_index( + 'idx_access_grant_resource', + 'access_grant', + ['resource_type', 'resource_id'], + ) + op.create_index( + 'idx_access_grant_principal', + 'access_grant', + ['principal_type', 'principal_id'], + ) + + # Backfill existing access_control JSON data + conn = op.get_bind() + + # Tables with access_control JSON columns: (table_name, resource_type) + resource_tables = [ + ('knowledge', 'knowledge'), + ('prompt', 'prompt'), + ('tool', 'tool'), + ('model', 'model'), + ('note', 'note'), + ('channel', 'channel'), + ('file', 'file'), + ] + + now = int(time.time()) + inserted = set() + + for table_name, resource_type in resource_tables: + if table_name not in existing_tables: + continue + + # Check if access_control and id columns exist (may already be dropped on re-run, + # or table may have been rebuilt without id during intermediate migration states) + insp = sa.inspect(conn) + insp.clear_cache() # Ensure fresh metadata after prior migrations that rebuild tables + table_cols = {c['name'] for c in insp.get_columns(table_name)} + if 'access_control' not in table_cols or 'id' not in table_cols: + continue + + # Query all rows + result = conn.execute(sa.text(f'SELECT id, access_control FROM "{table_name}"')) + rows = result.fetchall() + + for row in rows: + resource_id = row[0] + access_control_json = row[1] + + # Handle NULL or JSON "null" = public access (user:* for read) + # Could be Python None (SQL NULL) or string "null" (JSON null) + # EXCEPTION: files with NULL are PRIVATE (owner-only), not public + is_null = ( + access_control_json is None + or access_control_json == 'null' + or (isinstance(access_control_json, str) and access_control_json.strip().lower() == 'null') + ) + if is_null: + # Files: NULL = private (no entry needed, owner has implicit access) + # Other resources: NULL = public (insert user:* for read) + if resource_type == 'file': + continue # Private - no entry needed + + key = (resource_type, resource_id, 'user', '*', 'read') + if key not in inserted: + try: + conn.execute( + sa.text(""" + INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) + VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) + """), + { + 'id': str(uuid.uuid4()), + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': '*', + 'permission': 'read', + 'created_at': now, + }, + ) + inserted.add(key) + except Exception: + pass + continue + + # Handle JSON parsing + if isinstance(access_control_json, str): + import json + + try: + access_control_json = json.loads(access_control_json) + except Exception: + continue + + # Handle {} = private/owner-only - NO entries needed + # Owner access is implicit, no grants to store + if not access_control_json or not isinstance(access_control_json, dict): + continue + + # Check if it's effectively empty (no read/write keys with content) + read_data = access_control_json.get('read', {}) + write_data = access_control_json.get('write', {}) + + has_read_grants = read_data.get('group_ids', []) or read_data.get('user_ids', []) + has_write_grants = write_data.get('group_ids', []) or write_data.get('user_ids', []) + + if not has_read_grants and not has_write_grants: + # Empty permissions = private, no grants needed + continue + + # Extract permissions and insert into access_grant table + for permission in ['read', 'write']: + perm_data = access_control_json.get(permission, {}) + if not perm_data: + continue + + for group_id in perm_data.get('group_ids', []): + key = (resource_type, resource_id, 'group', group_id, permission) + if key in inserted: + continue + try: + conn.execute( + sa.text(""" + INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) + VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) + """), + { + 'id': str(uuid.uuid4()), + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'group', + 'principal_id': group_id, + 'permission': permission, + 'created_at': now, + }, + ) + inserted.add(key) + except Exception: + pass + + for user_id in perm_data.get('user_ids', []): + key = (resource_type, resource_id, 'user', user_id, permission) + if key in inserted: + continue + try: + conn.execute( + sa.text(""" + INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) + VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) + """), + { + 'id': str(uuid.uuid4()), + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': user_id, + 'permission': permission, + 'created_at': now, + }, + ) + inserted.add(key) + except Exception: + pass + + # Drop access_control columns from resource tables (only if column still exists) + inspector = sa.inspect(conn) + for table_name, _ in resource_tables: + if table_name not in existing_tables: + continue + cols = {c['name'] for c in inspector.get_columns(table_name)} + if 'access_control' in cols: + with op.batch_alter_table(table_name) as batch: + batch.drop_column('access_control') + + +def downgrade() -> None: + import json + + conn = op.get_bind() + + # Resource tables mapping: (table_name, resource_type) + resource_tables = [ + ('knowledge', 'knowledge'), + ('prompt', 'prompt'), + ('tool', 'tool'), + ('model', 'model'), + ('note', 'note'), + ('channel', 'channel'), + ('file', 'file'), + ] + + # Step 1: Re-add access_control columns to resource tables + for table_name, _ in resource_tables: + try: + with op.batch_alter_table(table_name) as batch: + batch.add_column(sa.Column('access_control', sa.JSON(), nullable=True)) + except Exception: + pass + + # Step 2: Query access_grant table and reconstruct JSON for each resource + for table_name, resource_type in resource_tables: + try: + # Get all grants for this resource type + result = conn.execute( + sa.text(""" + SELECT resource_id, principal_type, principal_id, permission + FROM access_grant + WHERE resource_type = :resource_type + """), + {'resource_type': resource_type}, + ) + rows = result.fetchall() + except Exception: + continue + + # Group by resource_id and reconstruct JSON structure + resource_grants = {} + for row in rows: + resource_id = row[0] + principal_type = row[1] + principal_id = row[2] + permission = row[3] + + if resource_id not in resource_grants: + resource_grants[resource_id] = { + 'is_public': False, + 'read': {'group_ids': [], 'user_ids': []}, + 'write': {'group_ids': [], 'user_ids': []}, + } + + # Handle public access (user:* for read) + if principal_type == 'user' and principal_id == '*' and permission == 'read': + resource_grants[resource_id]['is_public'] = True + continue + + # Add to appropriate list + if permission in ['read', 'write']: + if principal_type == 'group': + if principal_id not in resource_grants[resource_id][permission]['group_ids']: + resource_grants[resource_id][permission]['group_ids'].append(principal_id) + elif principal_type == 'user': + if principal_id not in resource_grants[resource_id][permission]['user_ids']: + resource_grants[resource_id][permission]['user_ids'].append(principal_id) + + # Step 3: Update each resource with reconstructed JSON + for resource_id, grants in resource_grants.items(): + if grants['is_public']: + # Public = NULL + access_control_value = None + elif ( + not grants['read']['group_ids'] + and not grants['read']['user_ids'] + and not grants['write']['group_ids'] + and not grants['write']['user_ids'] + ): + # No grants = should not happen (would mean no entries), default to {} + access_control_value = json.dumps({}) + else: + # Custom permissions + access_control_value = json.dumps( + { + 'read': grants['read'], + 'write': grants['write'], + } + ) + + try: + conn.execute( + sa.text(f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'), + {'access_control': access_control_value, 'id': resource_id}, + ) + except Exception: + pass + + # Step 4: Set all resources WITHOUT entries to private + # For files: NULL means private (owner-only), so leave as NULL + # For other resources: {} means private, so update to {} + if resource_type != 'file': + try: + conn.execute( + sa.text(f""" + UPDATE "{table_name}" + SET access_control = :private_value + WHERE id NOT IN ( + SELECT DISTINCT resource_id FROM access_grant WHERE resource_type = :resource_type + ) + AND access_control IS NULL + """), + {'private_value': json.dumps({}), 'resource_type': resource_type}, + ) + except Exception: + pass + # For files, NULL stays NULL - no action needed + + # Step 5: Drop the access_grant table + op.drop_index('idx_access_grant_principal', table_name='access_grant') + op.drop_index('idx_access_grant_resource', table_name='access_grant') + op.drop_table('access_grant') diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py new file mode 100644 index 0000000000..1c86dc08e7 --- /dev/null +++ b/backend/open_webui/models/access_grants.py @@ -0,0 +1,880 @@ +import logging +import time +import uuid +from typing import Optional + +from open_webui.internal.db import Base, get_async_db_context +from pydantic import BaseModel, ConfigDict +from sqlalchemy import BigInteger, Column, Text, UniqueConstraint, and_, delete, or_, select +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +#################### +# AccessGrant DB Schema +#################### + + +class AccessGrant(Base): + __tablename__ = 'access_grant' + + id = Column(Text, primary_key=True) + resource_type = Column(Text, nullable=False) # "knowledge", "model", "prompt", "tool", "note", "channel", "file" + resource_id = Column(Text, nullable=False) + principal_type = Column(Text, nullable=False) # "user" or "group" + principal_id = Column(Text, nullable=False) # user_id, group_id, or "*" (wildcard for public) + permission = Column(Text, nullable=False) # "read" or "write" + created_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + UniqueConstraint( + 'resource_type', + 'resource_id', + 'principal_type', + 'principal_id', + 'permission', + name='uq_access_grant_grant', + ), + ) + + +class AccessGrantModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + resource_type: str + resource_id: str + principal_type: str + principal_id: str + permission: str + created_at: int + + +class AccessGrantResponse(BaseModel): + """Slim grant model for API responses — resource context is implicit from the parent.""" + + id: str + principal_type: str + principal_id: str + permission: str + + @classmethod + def from_grant(cls, grant: 'AccessGrantModel') -> 'AccessGrantResponse': + return cls( + id=grant.id, + principal_type=grant.principal_type, + principal_id=grant.principal_id, + permission=grant.permission, + ) + + +#################### +# Conversion utilities +#################### + + +def access_control_to_grants( + resource_type: str, + resource_id: str, + access_control: Optional[dict], +) -> list[dict]: + """ + Convert an old-style access_control JSON dict to a flat list of grant dicts. + + Semantics: + - None → public read (user:* read) — except files which are private + - {} → private/owner-only (no grants) + - {read: {group_ids, user_ids}, write: {group_ids, user_ids}} → specific grants + + Returns a list of dicts with keys: resource_type, resource_id, principal_type, principal_id, permission + """ + grants = [] + + if access_control is None: + # NULL → public read (user:* for read) + # Exception: files with NULL are private (owner-only), no grants needed + if resource_type != 'file': + grants.append( + { + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': '*', + 'permission': 'read', + } + ) + return grants + + # {} → private/owner-only, no grants + if not access_control: + return grants + + # Parse structured permissions + for permission in ['read', 'write']: + perm_data = access_control.get(permission, {}) + if not perm_data: + continue + + for group_id in perm_data.get('group_ids', []): + grants.append( + { + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'group', + 'principal_id': group_id, + 'permission': permission, + } + ) + + for user_id in perm_data.get('user_ids', []): + grants.append( + { + 'resource_type': resource_type, + 'resource_id': resource_id, + 'principal_type': 'user', + 'principal_id': user_id, + 'permission': permission, + } + ) + + return grants + + +def normalize_access_grants(access_grants: Optional[list]) -> list[dict]: + """ + Normalize direct access_grants payloads from API forms. + + Keeps only valid grants and removes duplicates by + (principal_type, principal_id, permission). + """ + if not access_grants: + return [] + + deduped = {} + for grant in access_grants: + if isinstance(grant, BaseModel): + grant = grant.model_dump() + if not isinstance(grant, dict): + continue + + principal_type = grant.get('principal_type') + principal_id = grant.get('principal_id') + permission = grant.get('permission') + + if principal_type not in ('user', 'group'): + continue + if permission not in ('read', 'write'): + continue + if not isinstance(principal_id, str) or not principal_id: + continue + + key = (principal_type, principal_id, permission) + deduped[key] = { + 'id': (grant.get('id') if isinstance(grant.get('id'), str) and grant.get('id') else str(uuid.uuid4())), + 'principal_type': principal_type, + 'principal_id': principal_id, + 'permission': permission, + } + + return list(deduped.values()) + + +def has_public_read_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes wildcard public-read. + """ + for grant in normalize_access_grants(access_grants): + if grant['principal_type'] == 'user' and grant['principal_id'] == '*' and grant['permission'] == 'read': + return True + return False + + +def has_public_write_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes wildcard public-write. + """ + for grant in normalize_access_grants(access_grants): + if grant['principal_type'] == 'user' and grant['principal_id'] == '*' and grant['permission'] == 'write': + return True + return False + + +def has_user_access_grant(access_grants: Optional[list]) -> bool: + """ + Returns True when a direct grant list includes any non-wildcard user grant. + """ + for grant in normalize_access_grants(access_grants): + if grant['principal_type'] == 'user' and grant['principal_id'] != '*': + return True + return False + + +def strip_user_access_grants(access_grants: Optional[list]) -> list: + """ + Remove all non-wildcard user grants from the list. + Keeps group grants and the public wildcard (user:*) intact. + """ + if not access_grants: + return [] + return [ + grant + for grant in access_grants + if not ( + (grant.get('principal_type') if isinstance(grant, dict) else getattr(grant, 'principal_type', None)) + == 'user' + and (grant.get('principal_id') if isinstance(grant, dict) else getattr(grant, 'principal_id', None)) != '*' + ) + ] + + +def grants_to_access_control(grants: list) -> Optional[dict]: + """ + Convert a list of grant objects (AccessGrantModel or AccessGrantResponse) + back to the old-style access_control JSON dict for backward compatibility. + + Semantics: + - [] (empty) → {} (private/owner-only) + - Contains user:*:read → None (public), but write grants are preserved + - Otherwise → {read: {group_ids, user_ids}, write: {group_ids, user_ids}} + + Note: "public" (user:*:read) still allows additional write permissions + to coexist. When the wildcard read is present the function returns None + for the legacy dict, so callers that need write info should inspect the + grants list directly. + """ + if not grants: + return {} # No grants = private/owner-only + + result = { + 'read': {'group_ids': [], 'user_ids': []}, + 'write': {'group_ids': [], 'user_ids': []}, + } + + is_public = False + for grant in grants: + if grant.principal_type == 'user' and grant.principal_id == '*' and grant.permission == 'read': + is_public = True + continue # Don't add wildcard to user_ids list + + if grant.permission not in ('read', 'write'): + continue + + if grant.principal_type == 'group': + if grant.principal_id not in result[grant.permission]['group_ids']: + result[grant.permission]['group_ids'].append(grant.principal_id) + elif grant.principal_type == 'user': + if grant.principal_id not in result[grant.permission]['user_ids']: + result[grant.permission]['user_ids'].append(grant.principal_id) + + if is_public: + return None # Public read access + + return result + + +#################### +# Table Operations +#################### + + +class AccessGrantsTable: + async def grant_access( + self, + resource_type: str, + resource_id: str, + principal_type: str, + principal_id: str, + permission: str, + db: Optional[AsyncSession] = None, + ) -> Optional[AccessGrantModel]: + """Add a single access grant. Idempotent (ignores duplicates).""" + async with get_async_db_context(db) as db: + # Check for existing grant + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + principal_type=principal_type, + principal_id=principal_id, + permission=permission, + ) + ) + existing = result.scalars().first() + if existing: + return AccessGrantModel.model_validate(existing) + + grant = AccessGrant( + id=str(uuid.uuid4()), + resource_type=resource_type, + resource_id=resource_id, + principal_type=principal_type, + principal_id=principal_id, + permission=permission, + created_at=int(time.time()), + ) + db.add(grant) + await db.commit() + await db.refresh(grant) + return AccessGrantModel.model_validate(grant) + + async def revoke_access( + self, + resource_type: str, + resource_id: str, + principal_type: str, + principal_id: str, + permission: str, + db: Optional[AsyncSession] = None, + ) -> bool: + """Remove a single access grant.""" + async with get_async_db_context(db) as db: + result = await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + principal_type=principal_type, + principal_id=principal_id, + permission=permission, + ) + ) + await db.commit() + return result.rowcount > 0 + + async def revoke_all_access( + self, + resource_type: str, + resource_id: str, + db: Optional[AsyncSession] = None, + ) -> int: + """Remove all access grants for a resource.""" + async with get_async_db_context(db) as db: + result = await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + await db.commit() + return result.rowcount + + async def set_access_control( + self, + resource_type: str, + resource_id: str, + access_control: Optional[dict], + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """ + Replace all grants for a resource from an access_control JSON dict. + This is the primary bridge for backward compat with the frontend. + """ + async with get_async_db_context(db) as db: + # Delete all existing grants for this resource + await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + + # Convert JSON to grant dicts + grant_dicts = access_control_to_grants(resource_type, resource_id, access_control) + + # Insert new grants + results = [] + for grant_dict in grant_dicts: + grant = AccessGrant( + id=str(uuid.uuid4()), + **grant_dict, + created_at=int(time.time()), + ) + db.add(grant) + results.append(grant) + + await db.commit() + + return [AccessGrantModel.model_validate(g) for g in results] + + async def set_access_grants( + self, + resource_type: str, + resource_id: str, + access_grants: Optional[list], + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """ + Replace all grants for a resource from a direct access_grants list. + """ + async with get_async_db_context(db) as db: + await db.execute( + delete(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + + normalized_grants = normalize_access_grants(access_grants) + + results = [] + for grant_dict in normalized_grants: + grant = AccessGrant( + id=str(uuid.uuid4()), + resource_type=resource_type, + resource_id=resource_id, + principal_type=grant_dict['principal_type'], + principal_id=grant_dict['principal_id'], + permission=grant_dict['permission'], + created_at=int(time.time()), + ) + db.add(grant) + results.append(grant) + + await db.commit() + return [AccessGrantModel.model_validate(g) for g in results] + + async def get_access_control( + self, + resource_type: str, + resource_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[dict]: + """ + Reconstruct the old-style access_control JSON dict from grants. + For backward compat with the frontend. + """ + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + grants = result.scalars().all() + grant_models = [AccessGrantModel.model_validate(g) for g in grants] + return grants_to_access_control(grant_models) + + async def get_grants_by_resource( + self, + resource_type: str, + resource_id: str, + db: Optional[AsyncSession] = None, + ) -> list[AccessGrantModel]: + """Get all grants for a specific resource.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + ) + ) + grants = result.scalars().all() + return [AccessGrantModel.model_validate(g) for g in grants] + + async def get_grants_by_resources( + self, + resource_type: str, + resource_ids: list[str], + db: Optional[AsyncSession] = None, + ) -> dict[str, list[AccessGrantModel]]: + """Batch-fetch grants for multiple resources. Returns {resource_id: [grants]}.""" + if not resource_ids: + return {} + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id.in_(resource_ids), + ) + ) + grants = result.scalars().all() + result_dict: dict[str, list[AccessGrantModel]] = {rid: [] for rid in resource_ids} + for g in grants: + result_dict[g.resource_id].append(AccessGrantModel.model_validate(g)) + return result_dict + + async def has_access( + self, + user_id: str, + resource_type: str, + resource_id: str, + permission: str = 'read', + user_group_ids: Optional[set[str]] = None, + db: Optional[AsyncSession] = None, + ) -> bool: + """ + Check if a user has the specified permission on a resource. + + Access is granted if any of the following is true: + - There's a grant for user:* (public) with the requested permission + - There's a grant for the specific user with the requested permission + - There's a grant for any of the user's groups with the requested permission + """ + async with get_async_db_context(db) as db: + # Build conditions for matching grants + conditions = [ + # Public access + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ), + # Direct user access + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ), + ] + + # Group access + if user_group_ids is None: + from open_webui.models.groups import Groups + + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + if user_group_ids: + conditions.append( + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(user_group_ids), + ) + ) + + result = await db.execute( + select(AccessGrant) + .filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == resource_id, + AccessGrant.permission == permission, + or_(*conditions), + ) + .limit(1) + ) + grant = result.scalars().first() + return grant is not None + + async def get_accessible_resource_ids( + self, + user_id: str, + resource_type: str, + resource_ids: list[str], + permission: str = 'read', + user_group_ids: Optional[set[str]] = None, + db: Optional[AsyncSession] = None, + ) -> set[str]: + """ + Batch check: return the subset of resource_ids that the user can access. + + This replaces calling has_access() in a loop (N+1) with a single query. + """ + if not resource_ids: + return set() + + async with get_async_db_context(db) as db: + conditions = [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ), + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ), + ] + + if user_group_ids is None: + from open_webui.models.groups import Groups + + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + if user_group_ids: + conditions.append( + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(user_group_ids), + ) + ) + + result = await db.execute( + select(AccessGrant.resource_id) + .filter( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id.in_(resource_ids), + AccessGrant.permission == permission, + or_(*conditions), + ) + .distinct() + ) + rows = result.all() + return {row[0] for row in rows} + + async def get_users_with_access( + self, + resource_type: str, + resource_id: str, + permission: str = 'read', + db: Optional[AsyncSession] = None, + ) -> list: + """ + Get all users who have the specified permission on a resource. + Returns a list of UserModel instances. + """ + from open_webui.models.groups import Groups + from open_webui.models.users import UserModel, Users + + async with get_async_db_context(db) as db: + result = await db.execute( + select(AccessGrant).filter_by( + resource_type=resource_type, + resource_id=resource_id, + permission=permission, + ) + ) + grants = result.scalars().all() + + # Check for public access + for grant in grants: + if grant.principal_type == 'user' and grant.principal_id == '*': + result = await Users.get_users(filter={'roles': ['!pending']}, db=db) + return result.get('users', []) + + user_ids_with_access = set() + + for grant in grants: + if grant.principal_type == 'user': + user_ids_with_access.add(grant.principal_id) + elif grant.principal_type == 'group': + group_user_ids = await Groups.get_group_user_ids_by_id(grant.principal_id, db=db) + if group_user_ids: + user_ids_with_access.update(group_user_ids) + + if not user_ids_with_access: + return [] + + return await Users.get_users_by_user_ids(list(user_ids_with_access), db=db) + + def has_permission_filter( + self, + db, + query, + DocumentModel, + filter: dict, + resource_type: str, + permission: str = 'read', + ): + """ + Apply access control filtering to a SQLAlchemy query by JOINing with access_grant. + + This replaces the old JSON-column-based filtering with a proper relational JOIN. + + Note: This method builds SQLAlchemy expressions and does NOT perform I/O itself, + so it remains synchronous. The caller is responsible for executing the query + asynchronously with `await db.execute(...)`. + """ + group_ids = filter.get('group_ids', []) + user_id = filter.get('user_id') + + if permission == 'read_only': + return self._has_read_only_permission_filter(db, query, DocumentModel, filter, resource_type) + + # Build principal conditions + principal_conditions = [] + + if group_ids or user_id: + # Public access: user:* read + principal_conditions.append( + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ) + ) + + if user_id: + # Owner always has access + principal_conditions.append(DocumentModel.user_id == user_id) + + # Direct user grant + principal_conditions.append( + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ) + + if group_ids: + # Group grants + principal_conditions.append( + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ) + + if not principal_conditions: + return query + + # LEFT JOIN access_grant and filter + # We use a subquery approach to avoid duplicates from multiple matching grants + from sqlalchemy import exists as sa_exists + + grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == permission, + or_( + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ), + *( + [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ] + if user_id + else [] + ), + *( + [ + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ] + if group_ids + else [] + ), + ), + ) + .correlate(DocumentModel) + .exists() + ) + + # Owner OR has a matching grant + owner_or_grant = [grant_exists] + if user_id: + owner_or_grant.append(DocumentModel.user_id == user_id) + + query = query.filter(or_(*owner_or_grant)) + return query + + def _has_read_only_permission_filter( + self, + db, + query, + DocumentModel, + filter: dict, + resource_type: str, + ): + """ + Filter for items where user has read BUT NOT write access. + Public items are NOT considered read_only. + + Note: This method builds SQLAlchemy expressions and does NOT perform I/O itself, + so it remains synchronous. The caller is responsible for executing the query + asynchronously with `await db.execute(...)`. + """ + group_ids = filter.get('group_ids', []) + user_id = filter.get('user_id') + + from sqlalchemy import exists as sa_exists + + # Has read grant (not public) + read_grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == 'read', + or_( + *( + [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ] + if user_id + else [] + ), + *( + [ + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ] + if group_ids + else [] + ), + ), + ) + .correlate(DocumentModel) + .exists() + ) + + # Does NOT have write grant + write_grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == 'write', + or_( + *( + [ + and_( + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == user_id, + ) + ] + if user_id + else [] + ), + *( + [ + and_( + AccessGrant.principal_type == 'group', + AccessGrant.principal_id.in_(group_ids), + ) + ] + if group_ids + else [] + ), + ), + ) + .correlate(DocumentModel) + .exists() + ) + + # Is NOT public + public_grant_exists = ( + select(AccessGrant.id) + .where( + AccessGrant.resource_type == resource_type, + AccessGrant.resource_id == DocumentModel.id, + AccessGrant.permission == 'read', + AccessGrant.principal_type == 'user', + AccessGrant.principal_id == '*', + ) + .correlate(DocumentModel) + .exists() + ) + + conditions = [read_grant_exists, ~write_grant_exists, ~public_grant_exists] + + # Not owner + if user_id: + conditions.append(DocumentModel.user_id != user_id) + + query = query.filter(and_(*conditions)) + return query + + +AccessGrants = AccessGrantsTable() diff --git a/backend/open_webui/models/auths.py b/backend/open_webui/models/auths.py index 93f17dff11..a46826d207 100644 --- a/backend/open_webui/models/auths.py +++ b/backend/open_webui/models/auths.py @@ -1,48 +1,50 @@ +"""Auth credential models and data-access layer.""" + +from __future__ import annotations + import logging import uuid from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from open_webui.models.users import UserModel, UserProfileImageResponse, Users -from pydantic import BaseModel -from sqlalchemy import Boolean, Column, String, Text +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import User, UserModel, UserProfileImageResponse, Users +from open_webui.utils.validate import validate_profile_image_url +from pydantic import BaseModel, field_validator +from sqlalchemy import Boolean, Column, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# DB MODEL -#################### +class Auth(Base): # credential ↔ user linkage + """Maps a user ID to an email/password pair with an active flag.""" -class Auth(Base): - __tablename__ = "auth" + __tablename__ = 'auth' - id = Column(String, primary_key=True, unique=True) - email = Column(String) - password = Column(Text) - active = Column(Boolean) + id = Column(String, primary_key=True, unique=True) # mirrors User.id + email = Column(String) # login address, kept in sync with User.email + password = Column(Text) # argon2 / bcrypt hash + active = Column(Boolean) # account soft-disable toggle class AuthModel(BaseModel): + """Pydantic mirror of the ``auth`` table row.""" + id: str email: str password: str active: bool = True -#################### -# Forms -#################### - - class Token(BaseModel): + """JWT bearer-token response wrapper.""" + token: str token_type: str class ApiKey(BaseModel): - api_key: Optional[str] = None + api_key: str | None = None class SigninResponse(Token, UserProfileImageResponse): @@ -72,135 +74,139 @@ class SignupForm(BaseModel): name: str email: str password: str - profile_image_url: Optional[str] = "/user.png" + profile_image_url: str | None = '/user.png' + + @field_validator('profile_image_url') + @classmethod + def check_profile_image_url(cls, v: str | None) -> str | None: + if v is not None: + return validate_profile_image_url(v) + return v class AddUserForm(SignupForm): - role: Optional[str] = "pending" + role: str | None = 'pending' + + +# --- data-access layer --- class AuthsTable: - def insert_new_auth( + """Provides CRUD operations for the Auth ↔ User lifecycle.""" + + async def insert_new_auth( self, email: str, password: str, name: str, - profile_image_url: str = "/user.png", - role: str = "pending", - oauth: Optional[dict] = None, - db: Optional[Session] = None, - ) -> Optional[UserModel]: - with get_db_context(db) as db: - log.info("insert_new_auth") + profile_image_url: str = '/user.png', + role: str = 'pending', + oauth: dict | None = None, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Create an Auth + User pair inside a single transaction.""" + async with get_async_db_context(db) as session: + log.info('insert_new_auth') - id = str(uuid.uuid4()) + new_id = str(uuid.uuid4()) - auth = AuthModel( - **{"id": id, "email": email, "password": password, "active": True} + credential = Auth( + id=new_id, + email=email, + password=password, + active=True, ) - result = Auth(**auth.model_dump()) - db.add(result) + session.add(credential) - user = Users.insert_new_user( - id, name, email, profile_image_url, role, oauth=oauth, db=db + created_user = await Users.insert_new_user( + new_id, name, email, profile_image_url, role, oauth=oauth, db=session, ) + # persist both records and reload generated defaults + await session.commit() + await session.refresh(credential) + return created_user if credential and created_user else None - db.commit() - db.refresh(result) + async def authenticate_user( + self, email: str, verify_password: callable, db: AsyncSession | None = None, + ) -> UserModel | None: + """Verify email + password credentials and return the matching user.""" + log.info('authenticate_user: %s', email) + resolved = await Users.get_user_by_email(email, db=db) + if not resolved: + return + # load the credential row and verify the password hash + async with get_async_db_context(db) as session: + credential = await session.get(Auth, resolved.id) + if not credential or not credential.active: + return + if not verify_password(credential.password): + return + return resolved - if result and user: - return user - else: - return None - - def authenticate_user( - self, email: str, verify_password: callable, db: Optional[Session] = None - ) -> Optional[UserModel]: - log.info(f"authenticate_user: {email}") - - user = Users.get_user_by_email(email, db=db) - if not user: - return None - - try: - with get_db_context(db) as db: - auth = db.query(Auth).filter_by(id=user.id, active=True).first() - if auth: - if verify_password(auth.password): - return user - else: - return None - else: - return None - except Exception: - return None - - def authenticate_user_by_api_key( - self, api_key: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - log.info(f"authenticate_user_by_api_key: {api_key}") - # if no api_key, return None + async def authenticate_user_by_api_key( + self, api_key: str, db: AsyncSession | None = None, + ) -> UserModel | None: + """Look up the user that owns the given API key.""" + log.info('authenticate_user_by_api_key') if not api_key: - return None + return + # delegate to the Users model for the actual lookup + return await Users.get_user_by_api_key(api_key, db=db) - try: - user = Users.get_user_by_api_key(api_key, db=db) - return user if user else None - except Exception: - return False - - def authenticate_user_by_email( - self, email: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - log.info(f"authenticate_user_by_email: {email}") - try: - with get_db_context(db) as db: - auth = db.query(Auth).filter_by(email=email, active=True).first() - if auth: - user = Users.get_user_by_id(auth.id, db=db) - return user - except Exception: - return None - - def update_user_password_by_id( - self, id: str, new_password: str, db: Optional[Session] = None + async def authenticate_user_by_email( + self, + email: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Single-query auth via JOIN on Auth ↔ User, filtered by active flag.""" + log.info('authenticate_user_by_email: %s', email) + # single JOIN avoids N+1 — returns (Auth, User) tuple or None + async with get_async_db_context(db) as session: + joined_query = ( + select(Auth, User) + .join(User, Auth.id == User.id) + .where(Auth.email == email, Auth.active.is_(True)) + ) + match = (await session.execute(joined_query)).first() + if not match: + return + _, found_user = match + return UserModel.model_validate(found_user) + async def update_email_by_id( + self, user_id: str, email: str, db: AsyncSession | None = None, ) -> bool: - try: - with get_db_context(db) as db: - result = ( - db.query(Auth).filter_by(id=id).update({"password": new_password}) - ) - db.commit() - return True if result == 1 else False - except Exception: - return False - - def update_email_by_id( - self, id: str, email: str, db: Optional[Session] = None + """Set a new email on the auth record and propagate to the user row.""" + async with get_async_db_context(db) as session: + auth_row = await session.get(Auth, user_id) + if auth_row is None: + return False + auth_row.email = email + await session.commit() + await Users.update_user_by_id(user_id, {'email': email}, db=session) + return True + # --- password modification --- + async def update_user_password_by_id( + self, user_id: str, new_password: str, db: AsyncSession | None = None, ) -> bool: - try: - with get_db_context(db) as db: - result = db.query(Auth).filter_by(id=id).update({"email": email}) - db.commit() - return True if result == 1 else False - except Exception: - return False + """Set a new password hash for an existing user.""" + async with get_async_db_context(db) as session: + auth_row = await session.get(Auth, user_id) + if auth_row is None: + return False + auth_row.password = new_password + await session.commit() + return True - def delete_auth_by_id(self, id: str, db: Optional[Session] = None) -> bool: - try: - with get_db_context(db) as db: - # Delete User - result = Users.delete_user_by_id(id, db=db) - - if result: - db.query(Auth).filter_by(id=id).delete() - db.commit() - - return True - else: - return False - except Exception: - return False + async def delete_auth_by_id( + self, id: str, db: AsyncSession | None = None, + ) -> bool: + """Remove a user and their auth credential in one transaction.""" + async with get_async_db_context(db) as session: + if not await Users.delete_user_by_id(id, db=session): + return False + await session.execute(delete(Auth).where(Auth.id == id)) + await session.commit() + return True -Auths = AuthsTable() +Auths = AuthsTable() # singleton — module-level instance diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py new file mode 100644 index 0000000000..4038a3bdbe --- /dev/null +++ b/backend/open_webui/models/automations.py @@ -0,0 +1,420 @@ +import logging +import time +from typing import Optional +from uuid import uuid4 + +from open_webui.internal.db import Base, get_async_db_context +from pydantic import BaseModel, ConfigDict +from sqlalchemy import JSON, BigInteger, Boolean, Column, Index, String, Text, cast, delete, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +#################### +# Automation DB Schema +#################### + + +class Automation(Base): + __tablename__ = 'automation' + + id = Column(Text, primary_key=True) + user_id = Column(Text, nullable=False) + name = Column(Text, nullable=False) + data = Column(JSON, nullable=False) # {prompt, model_id, rrule} + meta = Column(JSON, nullable=True) + is_active = Column(Boolean, nullable=False, default=True) + last_run_at = Column(BigInteger, nullable=True) + next_run_at = Column(BigInteger, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (Index('ix_automation_next_run', 'next_run_at'),) + + +class AutomationRun(Base): + __tablename__ = 'automation_run' + + id = Column(Text, primary_key=True) + automation_id = Column(Text, nullable=False) + chat_id = Column(Text, nullable=True) + status = Column(Text, nullable=False) # success | error + error = Column(Text, nullable=True) + created_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + Index('ix_automation_run_automation_id', 'automation_id'), + Index('ix_automation_run_aid_created', 'automation_id', 'created_at'), + ) + + +#################### +# Pydantic Models +#################### + + +class AutomationTerminalConfig(BaseModel): + server_id: str + cwd: Optional[str] = None + + +class AutomationData(BaseModel): + prompt: str + model_id: str + rrule: str + terminal: Optional[AutomationTerminalConfig] = None + + +class AutomationModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + name: str + data: dict + meta: Optional[dict] = None + is_active: bool + last_run_at: Optional[int] = None + next_run_at: Optional[int] = None + + created_at: int + updated_at: int + + +class AutomationRunModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + automation_id: str + chat_id: Optional[str] = None + status: str + error: Optional[str] = None + created_at: int + + +class AutomationForm(BaseModel): + name: str + data: AutomationData + meta: Optional[dict] = None + is_active: Optional[bool] = True + + +class AutomationResponse(AutomationModel): + last_run: Optional[AutomationRunModel] = None + next_runs: Optional[list[int]] = None + + +class AutomationListResponse(BaseModel): + items: list[AutomationModel] + total: int + + +#################### +# AutomationTable +#################### + + +class AutomationTable: + async def insert( + self, + user_id: str, + form: AutomationForm, + next_run_at: int, + db: Optional[AsyncSession] = None, + ) -> AutomationModel: + async with get_async_db_context(db) as db: + now = int(time.time_ns()) + row = Automation( + id=str(uuid4()), + user_id=user_id, + name=form.name, + data=form.data.model_dump(), + meta=form.meta, + is_active=form.is_active, + next_run_at=next_run_at, + created_at=now, + updated_at=now, + ) + db.add(row) + await db.commit() + await db.refresh(row) + return AutomationModel.model_validate(row) + + async def count_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(select(func.count()).select_from(Automation).filter_by(user_id=user_id)) + return result.scalar() + + async def get_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[AutomationModel]: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + return AutomationModel.model_validate(row) if row else None + + async def get_active_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[AutomationModel]: + """Get active automations for a user (for calendar RRULE expansion).""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Automation).filter_by(user_id=user_id, is_active=True).order_by(Automation.created_at.desc()) + ) + return [AutomationModel.model_validate(r) for r in result.scalars().all()] + + async def search_automations( + self, + user_id: str, + query: Optional[str] = None, + status: Optional[str] = None, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> 'AutomationListResponse': + async with get_async_db_context(db) as db: + stmt = select(Automation).filter_by(user_id=user_id) + + if query: + search = f'%{query}%' + # Search in name and prompt inside JSON data + stmt = stmt.filter( + or_( + Automation.name.ilike(search), + cast(Automation.data, String).ilike(search), + ) + ) + + if status == 'active': + stmt = stmt.filter(Automation.is_active == True) + elif status == 'paused': + stmt = stmt.filter(Automation.is_active == False) + + stmt = stmt.order_by(Automation.created_at.desc()) + + # Get total count + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + rows = result.scalars().all() + return AutomationListResponse( + items=[AutomationModel.model_validate(r) for r in rows], + total=total, + ) + + async def update_by_id( + self, + id: str, + form: AutomationForm, + next_run_at: int, + db: Optional[AsyncSession] = None, + ) -> Optional[AutomationModel]: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + if not row: + return None + row.name = form.name + row.data = form.data.model_dump() + row.meta = form.meta + if form.is_active is not None: + row.is_active = form.is_active + row.next_run_at = next_run_at + row.updated_at = int(time.time_ns()) + await db.commit() + await db.refresh(row) + return AutomationModel.model_validate(row) + + async def toggle( + self, + id: str, + next_run_at: Optional[int], + db: Optional[AsyncSession] = None, + ) -> Optional[AutomationModel]: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + if not row: + return None + row.is_active = not row.is_active + row.next_run_at = next_run_at if row.is_active else None + row.updated_at = int(time.time_ns()) + await db.commit() + await db.refresh(row) + return AutomationModel.model_validate(row) + + async def delete(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + row = await db.get(Automation, id) + if not row: + return False + await db.delete(row) + await db.commit() + return True + + async def claim_due(self, now_ns: int, limit: int = 10, db: Optional[AsyncSession] = None) -> list[AutomationModel]: + """ + Atomically claim due automations for execution. + + Advances next_run_at immediately so the row can never be + double-claimed. On PostgreSQL, uses FOR UPDATE SKIP LOCKED + for zero-contention distributed work claiming. + """ + async with get_async_db_context(db) as db: + stmt = ( + select(Automation) + .where( + Automation.is_active == True, + Automation.next_run_at <= now_ns, + ) + .order_by(Automation.next_run_at) + .limit(limit) + ) + + if db.bind.dialect.name == 'postgresql': + stmt = stmt.with_for_update(skip_locked=True) + + result = await db.execute(stmt) + rows = result.scalars().all() + + from open_webui.utils.automations import next_run_ns + + # Batch-fetch user timezones so rescheduling respects each + # user's local timezone instead of falling back to server time. + user_ids = list({row.user_id for row in rows}) + timezone_by_user_id: dict[str, Optional[str]] = {} + if user_ids: + from open_webui.models.users import User + + tz_result = await db.execute(select(User.id, User.timezone).where(User.id.in_(user_ids))) + timezone_by_user_id = {uid: tz for uid, tz in tz_result.all()} + + for row in rows: + row.last_run_at = now_ns + row.next_run_at = next_run_ns(row.data.get('rrule', ''), tz=timezone_by_user_id.get(row.user_id)) + + await db.commit() + + return [AutomationModel.model_validate(r) for r in rows] + + +#################### +# AutomationRunTable +#################### + + +class AutomationRunTable: + async def insert( + self, + automation_id: str, + status: str, + chat_id: Optional[str] = None, + error: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> AutomationRunModel: + async with get_async_db_context(db) as db: + row = AutomationRun( + id=str(uuid4()), + automation_id=automation_id, + chat_id=chat_id, + status=status, + error=error, + created_at=int(time.time_ns()), + ) + db.add(row) + await db.commit() + await db.refresh(row) + return AutomationRunModel.model_validate(row) + + async def get_latest(self, automation_id: str, db: Optional[AsyncSession] = None) -> Optional[AutomationRunModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(AutomationRun) + .filter_by(automation_id=automation_id) + .order_by(AutomationRun.created_at.desc()) + .limit(1) + ) + row = result.scalars().first() + return AutomationRunModel.model_validate(row) if row else None + + async def get_latest_batch( + self, automation_ids: list[str], db: Optional[AsyncSession] = None + ) -> dict[str, AutomationRunModel]: + """Fetch the latest run for each automation in a single query.""" + if not automation_ids: + return {} + async with get_async_db_context(db) as db: + # Subquery: max created_at per automation_id + subq = ( + select( + AutomationRun.automation_id, + func.max(AutomationRun.created_at).label('max_created'), + ) + .filter(AutomationRun.automation_id.in_(automation_ids)) + .group_by(AutomationRun.automation_id) + .subquery() + ) + result = await db.execute( + select(AutomationRun).join( + subq, + (AutomationRun.automation_id == subq.c.automation_id) + & (AutomationRun.created_at == subq.c.max_created), + ) + ) + rows = result.scalars().all() + return {row.automation_id: AutomationRunModel.model_validate(row) for row in rows} + + async def get_by_automation( + self, + automation_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[AutomationRunModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(AutomationRun) + .filter_by(automation_id=automation_id) + .order_by(AutomationRun.created_at.desc()) + .offset(skip) + .limit(limit) + ) + rows = result.scalars().all() + return [AutomationRunModel.model_validate(r) for r in rows] + + async def delete_by_automation(self, automation_id: str, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(delete(AutomationRun).filter_by(automation_id=automation_id)) + await db.commit() + return result.rowcount + + async def get_runs_by_user_range( + self, + user_id: str, + start_ns: int, + end_ns: int, + limit: int = 500, + db: Optional[AsyncSession] = None, + ) -> list[tuple['AutomationRunModel', 'AutomationModel']]: + """Get runs within a date range for a user, joined with parent automation.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(AutomationRun, Automation) + .join(Automation, Automation.id == AutomationRun.automation_id) + .filter( + Automation.user_id == user_id, + AutomationRun.created_at >= start_ns, + AutomationRun.created_at < end_ns, + ) + .order_by(AutomationRun.created_at.desc()) + .limit(limit) + ) + return [ + (AutomationRunModel.model_validate(run), AutomationModel.model_validate(auto)) + for run, auto in result.all() + ] + + +Automations = AutomationTable() +AutomationRuns = AutomationRunTable() diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py new file mode 100644 index 0000000000..f2f9ed464c --- /dev/null +++ b/backend/open_webui/models/calendar.py @@ -0,0 +1,823 @@ +import logging +import time +from typing import Optional +from uuid import uuid4 + +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, UserResponse +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + Index, + Text, + UniqueConstraint, + delete, + exists, + func, + or_, + select, + update, +) +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +#################### +# Calendar DB Schema +#################### + + +class Calendar(Base): + __tablename__ = 'calendar' + + id = Column(Text, primary_key=True) + user_id = Column(Text, nullable=False) + name = Column(Text, nullable=False) + color = Column(Text, nullable=True) + is_default = Column(Boolean, nullable=False, default=False) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = (Index('ix_calendar_user', 'user_id'),) + + +class CalendarEvent(Base): + __tablename__ = 'calendar_event' + + id = Column(Text, primary_key=True) + calendar_id = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + title = Column(Text, nullable=False) + description = Column(Text, nullable=True) + start_at = Column(BigInteger, nullable=False) + end_at = Column(BigInteger, nullable=True) + all_day = Column(Boolean, nullable=False, default=False) + rrule = Column(Text, nullable=True) + color = Column(Text, nullable=True) + location = Column(Text, nullable=True) + data = Column(JSON, nullable=True) + meta = Column(JSON, nullable=True) + is_cancelled = Column(Boolean, nullable=False, default=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + Index('ix_calendar_event_calendar', 'calendar_id', 'start_at'), + Index('ix_calendar_event_user_date', 'user_id', 'start_at'), + ) + + +class CalendarEventAttendee(Base): + __tablename__ = 'calendar_event_attendee' + + id = Column(Text, primary_key=True) + event_id = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + status = Column(Text, nullable=False, default='pending') + meta = Column(JSON, nullable=True) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'), + Index('ix_calendar_event_attendee_user', 'user_id', 'status'), + ) + + +#################### +# Pydantic Models +#################### + + +class CalendarModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + name: str + color: Optional[str] = None + is_default: bool = False + is_system: bool = False + + data: Optional[dict] = None + meta: Optional[dict] = None + + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + created_at: int + updated_at: int + + +class CalendarEventModel(BaseModel): + model_config = ConfigDict(from_attributes=True, extra='allow') + + id: str + calendar_id: str + user_id: str + title: str + description: Optional[str] = None + start_at: int + end_at: Optional[int] = None + all_day: bool = False + rrule: Optional[str] = None + color: Optional[str] = None + location: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + is_cancelled: bool = False + + attendees: list['CalendarEventAttendeeModel'] = Field(default_factory=list) + + created_at: int + updated_at: int + + +class CalendarEventAttendeeModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + event_id: str + user_id: str + status: str = 'pending' + meta: Optional[dict] = None + + created_at: int + updated_at: int + + +#################### +# Forms +#################### + + +class CalendarForm(BaseModel): + name: str + color: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + + +class CalendarUpdateForm(BaseModel): + name: Optional[str] = None + color: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + access_grants: Optional[list[dict]] = None + + +class CalendarEventForm(BaseModel): + calendar_id: str + title: str + description: Optional[str] = None + start_at: int + end_at: Optional[int] = None + all_day: bool = False + rrule: Optional[str] = None + color: Optional[str] = None + location: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + attendees: Optional[list[dict]] = None + + +class CalendarEventUpdateForm(BaseModel): + calendar_id: Optional[str] = None + title: Optional[str] = None + description: Optional[str] = None + start_at: Optional[int] = None + end_at: Optional[int] = None + all_day: Optional[bool] = None + rrule: Optional[str] = None + color: Optional[str] = None + location: Optional[str] = None + data: Optional[dict] = None + meta: Optional[dict] = None + is_cancelled: Optional[bool] = None + attendees: Optional[list[dict]] = None + + +class RSVPForm(BaseModel): + status: str # 'accepted' | 'declined' | 'tentative' | 'pending' + + +#################### +# Response Models +#################### + + +class CalendarEventUserResponse(CalendarEventModel): + user: Optional[UserResponse] = None + + +class CalendarEventListResponse(BaseModel): + items: list[CalendarEventUserResponse] + total: int + + +#################### +# Table Operations +#################### + + +class CalendarTable: + async def _get_access_grants(self, calendar_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('calendar', calendar_id, db=db) + + async def _to_calendar_model( + self, + cal: Calendar, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> CalendarModel: + cal_data = CalendarModel.model_validate(cal).model_dump(exclude={'access_grants'}) + cal_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(cal_data['id'], db=db) + ) + return CalendarModel.model_validate(cal_data) + + async def get_or_create_defaults(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]: + """Return user's calendars, creating 'Personal' default if none exist.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Calendar).filter(Calendar.user_id == user_id).order_by(Calendar.created_at.asc()) + ) + calendars = result.scalars().all() + + if calendars: + return [CalendarModel.model_validate(c) for c in calendars] + + now = int(time.time_ns()) + cal = Calendar( + id=str(uuid4()), + user_id=user_id, + name='Personal', + color='#3b82f6', + is_default=True, + created_at=now, + updated_at=now, + ) + db.add(cal) + await db.commit() + return [CalendarModel.model_validate(cal)] + + async def get_calendars_by_user(self, user_id: str, db: Optional[AsyncSession] = None) -> list[CalendarModel]: + """Owned + shared calendars.""" + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [g.id for g in user_groups] + + stmt = select(Calendar) + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Calendar, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='calendar', + permission='read', + ) + stmt = stmt.order_by(Calendar.created_at.asc()) + + result = await db.execute(stmt) + calendars = result.scalars().all() + + if not calendars: + return await self.get_or_create_defaults(user_id, db=db) + + cal_ids = [c.id for c in calendars] + grants_map = await AccessGrants.get_grants_by_resources('calendar', cal_ids, db=db) + + return [await self._to_calendar_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in calendars] + + async def get_calendar_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Calendar).filter(Calendar.id == id)) + cal = result.scalars().first() + return await self._to_calendar_model(cal, db=db) if cal else None + + async def insert_new_calendar( + self, user_id: str, form_data: CalendarForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarModel]: + async with get_async_db_context(db) as db: + now = int(time.time_ns()) + cal = Calendar( + id=str(uuid4()), + user_id=user_id, + name=form_data.name, + color=form_data.color, + is_default=False, + data=form_data.data, + meta=form_data.meta, + created_at=now, + updated_at=now, + ) + db.add(cal) + await db.commit() + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('calendar', cal.id, form_data.access_grants, db=db) + return await self._to_calendar_model(cal, db=db) + + async def update_calendar_by_id( + self, id: str, form_data: CalendarUpdateForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Calendar).filter(Calendar.id == id)) + cal = result.scalars().first() + if not cal: + return None + + update_data = form_data.model_dump(exclude_unset=True) + if 'name' in update_data: + cal.name = update_data['name'] + if 'color' in update_data: + cal.color = update_data['color'] + if 'data' in update_data: + cal.data = {**(cal.data or {}), **update_data['data']} + if 'meta' in update_data: + cal.meta = {**(cal.meta or {}), **update_data['meta']} + if 'access_grants' in update_data: + await AccessGrants.set_access_grants('calendar', id, update_data['access_grants'], db=db) + + cal.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_calendar_model(cal, db=db) + + async def set_default_calendar( + self, user_id: str, calendar_id: str, db: Optional[AsyncSession] = None + ) -> Optional[CalendarModel]: + """Set a calendar as the user's default, clearing all others.""" + async with get_async_db_context(db) as db: + # Clear all defaults for this user + await db.execute( + update(Calendar) + .where(Calendar.user_id == user_id, Calendar.is_default == True) + .values(is_default=False) + ) + # Set the new default + result = await db.execute(select(Calendar).filter(Calendar.id == calendar_id, Calendar.user_id == user_id)) + cal = result.scalars().first() + if not cal: + return None + cal.is_default = True + cal.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_calendar_model(cal, db=db) + + async def delete_calendar_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete a non-default calendar. Cascades to events, attendees, and grants.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Calendar).filter(Calendar.id == id)) + cal = result.scalars().first() + if not cal or cal.is_default: + return False + + # Delete attendees for all events in this calendar + event_ids_result = await db.execute(select(CalendarEvent.id).filter(CalendarEvent.calendar_id == id)) + event_ids = [r[0] for r in event_ids_result.all()] + if event_ids: + await db.execute( + delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) + ) + + # Delete events + await db.execute(delete(CalendarEvent).filter(CalendarEvent.calendar_id == id)) + + # Delete calendar + await db.execute(delete(Calendar).filter(Calendar.id == id)) + await db.commit() + + # Revoke access grants in a separate transaction to avoid + # write-lock contention on SQLite when session sharing is off. + await AccessGrants.revoke_all_access('calendar', id) + return True + except Exception as e: + log.exception(f'Failed to delete calendar {id}: {e}') + return False + + +class CalendarEventTable: + async def _get_attendees( + self, event_id: str, db: Optional[AsyncSession] = None + ) -> list[CalendarEventAttendeeModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) + rows = result.scalars().all() + return [CalendarEventAttendeeModel.model_validate(r) for r in rows] + + async def _to_event_model( + self, + event: CalendarEvent, + attendees: Optional[list[CalendarEventAttendeeModel]] = None, + db: Optional[AsyncSession] = None, + ) -> CalendarEventModel: + event_data = CalendarEventModel.model_validate(event).model_dump(exclude={'attendees'}) + event_data['attendees'] = ( + attendees if attendees is not None else await self._get_attendees(event_data['id'], db=db) + ) + return CalendarEventModel.model_validate(event_data) + + async def insert_new_event( + self, user_id: str, form_data: CalendarEventForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarEventModel]: + async with get_async_db_context(db) as db: + now = int(time.time_ns()) + event = CalendarEvent( + id=str(uuid4()), + calendar_id=form_data.calendar_id, + user_id=user_id, + title=form_data.title, + description=form_data.description, + start_at=form_data.start_at, + end_at=form_data.end_at, + all_day=form_data.all_day, + rrule=form_data.rrule, + color=form_data.color, + location=form_data.location, + data=form_data.data, + meta=form_data.meta, + is_cancelled=False, + created_at=now, + updated_at=now, + ) + db.add(event) + await db.commit() + + # Add attendees + if form_data.attendees: + await CalendarEventAttendees.set_attendees(event.id, form_data.attendees, db=db) + + return await self._to_event_model(event, db=db) + + async def get_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[CalendarEventModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEvent).filter(CalendarEvent.id == id)) + event = result.scalars().first() + return await self._to_event_model(event, db=db) if event else None + + async def get_events_by_range( + self, + user_id: str, + start: int, + end: int, + calendar_ids: Optional[list[str]] = None, + db: Optional[AsyncSession] = None, + ) -> list[CalendarEventUserResponse]: + """Fetch events visible to user within a date range. + + Visible events = events in owned/shared calendars + events user attends. + Recurring events are fetched if they have any rrule (expansion in Python). + """ + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [g.id for g in user_groups] + + # Get calendar IDs accessible to user + cal_stmt = select(Calendar.id) + cal_stmt = AccessGrants.has_permission_filter( + db=db, + query=cal_stmt, + DocumentModel=Calendar, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='calendar', + permission='read', + ) + cal_result = await db.execute(cal_stmt) + accessible_cal_ids = [r[0] for r in cal_result.all()] + + if calendar_ids: + # Filter to requested calendars only + accessible_cal_ids = [c for c in accessible_cal_ids if c in calendar_ids] + + # Also get event IDs where user is an attendee + attendee_event_ids_result = await db.execute( + select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) + ) + attendee_event_ids = [r[0] for r in attendee_event_ids_result.all()] + + # Build conditions for accessible events + conditions = [] + if accessible_cal_ids: + conditions.append(CalendarEvent.calendar_id.in_(accessible_cal_ids)) + if attendee_event_ids: + conditions.append(CalendarEvent.id.in_(attendee_event_ids)) + + if not conditions: + return [] + + # Build event query + stmt = ( + select(CalendarEvent, User) + .outerjoin(User, User.id == CalendarEvent.user_id) + .filter( + CalendarEvent.is_cancelled == False, + or_(*conditions), + or_( + # Non-recurring: overlaps the range + ( + CalendarEvent.rrule.is_(None) + & (CalendarEvent.start_at < end) + & or_( + CalendarEvent.end_at.is_(None) & (CalendarEvent.start_at >= start), + CalendarEvent.end_at.isnot(None) & (CalendarEvent.end_at > start), + ) + ), + # Recurring: fetch all (expansion in Python) + CalendarEvent.rrule.isnot(None), + ), + ) + .order_by(CalendarEvent.start_at.asc()) + ) + + result = await db.execute(stmt) + items = result.all() + + if not items: + return [] + + # Batch-load attendees for all events in one query (avoid N+1) + event_ids = [event.id for event, _user in items] + att_result = await db.execute( + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) + ) + att_rows = att_result.scalars().all() + att_map: dict[str, list[CalendarEventAttendeeModel]] = {} + for a in att_rows: + att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a)) + + events = [] + for event, user in items: + event_data = CalendarEventModel.model_validate(event).model_dump(exclude={'attendees'}) + event_data['attendees'] = att_map.get(event.id, []) + events.append( + CalendarEventUserResponse( + **event_data, + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return events + + async def search_events( + self, + user_id: str, + query: Optional[str] = None, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> CalendarEventListResponse: + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [g.id for g in user_groups] + + # Get accessible calendar IDs + cal_stmt = select(Calendar.id) + cal_stmt = AccessGrants.has_permission_filter( + db=db, + query=cal_stmt, + DocumentModel=Calendar, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='calendar', + permission='read', + ) + cal_result = await db.execute(cal_stmt) + accessible_cal_ids = [r[0] for r in cal_result.all()] + if not accessible_cal_ids: + return CalendarEventListResponse(items=[], total=0) + + stmt = ( + select(CalendarEvent, User) + .outerjoin(User, User.id == CalendarEvent.user_id) + .filter( + CalendarEvent.is_cancelled == False, + CalendarEvent.calendar_id.in_(accessible_cal_ids), + ) + ) + + if query: + search = f'%{query}%' + stmt = stmt.filter( + or_( + CalendarEvent.title.ilike(search), + CalendarEvent.description.ilike(search), + CalendarEvent.location.ilike(search), + ) + ) + + stmt = stmt.order_by(CalendarEvent.start_at.desc()) + + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + if not items: + return CalendarEventListResponse(items=[], total=total) + + # Batch-load attendees + event_ids = [event.id for event, _user in items] + att_result = await db.execute( + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id.in_(event_ids)) + ) + att_rows = att_result.scalars().all() + att_map: dict[str, list[CalendarEventAttendeeModel]] = {} + for a in att_rows: + att_map.setdefault(a.event_id, []).append(CalendarEventAttendeeModel.model_validate(a)) + + events = [] + for event, user in items: + event_data = CalendarEventModel.model_validate(event).model_dump(exclude={'attendees'}) + event_data['attendees'] = att_map.get(event.id, []) + events.append( + CalendarEventUserResponse( + **event_data, + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return CalendarEventListResponse(items=events, total=total) + + async def update_event_by_id( + self, id: str, form_data: CalendarEventUpdateForm, db: Optional[AsyncSession] = None + ) -> Optional[CalendarEventModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEvent).filter(CalendarEvent.id == id)) + event = result.scalars().first() + if not event: + return None + + update_data = form_data.model_dump(exclude_unset=True) + for field in [ + 'calendar_id', + 'title', + 'description', + 'start_at', + 'end_at', + 'all_day', + 'rrule', + 'color', + 'location', + 'is_cancelled', + ]: + if field in update_data: + setattr(event, field, update_data[field]) + + if 'data' in update_data and update_data['data'] is not None: + event.data = {**(event.data or {}), **update_data['data']} + if 'meta' in update_data and update_data['meta'] is not None: + event.meta = {**(event.meta or {}), **update_data['meta']} + + if 'attendees' in update_data and update_data['attendees'] is not None: + await CalendarEventAttendees.set_attendees(id, update_data['attendees'], db=db) + + event.updated_at = int(time.time_ns()) + await db.commit() + return await self._to_event_model(event, db=db) + + async def get_upcoming_events( + self, + now_ns: int, + default_lookahead_ns: int, + db: Optional[AsyncSession] = None, + ) -> list[tuple[CalendarEventModel, Optional[str]]]: + """Events starting between now and now + lookahead, for alert processing. + + Per-event lookahead is read from meta.alert_minutes (falls back to + default_lookahead_ns). Returns (event, user_timezone) pairs. + """ + from open_webui.models.users import User as UserRow + + # Use the maximum possible lookahead (60 min) to cast a wide net; + # per-event filtering happens in Python after fetching. + max_lookahead_ns = max(default_lookahead_ns, 60 * 60 * 1_000_000_000) + upper = now_ns + max_lookahead_ns + + async with get_async_db_context(db) as db: + result = await db.execute( + select(CalendarEvent, UserRow.timezone) + .outerjoin(UserRow, UserRow.id == CalendarEvent.user_id) + .filter( + CalendarEvent.is_cancelled == False, + CalendarEvent.start_at >= now_ns, + CalendarEvent.start_at <= upper, + ) + ) + rows = result.all() + + events = [] + for event, tz in rows: + model = CalendarEventModel.model_validate(event) + # Determine per-event alert window + alert_minutes = None + if model.meta and 'alert_minutes' in model.meta: + alert_minutes = model.meta['alert_minutes'] + + if alert_minutes is not None: + if alert_minutes < 0: + # alert_minutes < 0 means "no alert" + continue + event_lookahead_ns = alert_minutes * 60 * 1_000_000_000 + else: + event_lookahead_ns = default_lookahead_ns + + if model.start_at <= now_ns + event_lookahead_ns: + events.append((model, tz)) + + return events + + async def delete_event_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == id)) + await db.execute(delete(CalendarEvent).filter(CalendarEvent.id == id)) + await db.commit() + return True + except Exception: + return False + + +class CalendarEventAttendeeTable: + async def set_attendees( + self, event_id: str, attendees: list[dict], db: Optional[AsyncSession] = None + ) -> list[CalendarEventAttendeeModel]: + """Replace all attendees for an event. + + Each dict in attendees: {user_id: str, status?: str, meta?: dict} + """ + async with get_async_db_context(db) as db: + # Remove existing + await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) + + now = int(time.time_ns()) + models = [] + for att in attendees: + row = CalendarEventAttendee( + id=str(uuid4()), + event_id=event_id, + user_id=att['user_id'], + status=att.get('status', 'pending'), + meta=att.get('meta'), + created_at=now, + updated_at=now, + ) + db.add(row) + models.append(CalendarEventAttendeeModel.model_validate(row)) + + await db.commit() + return models + + async def update_rsvp( + self, event_id: str, user_id: str, status: str, db: Optional[AsyncSession] = None + ) -> Optional[CalendarEventAttendeeModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(CalendarEventAttendee).filter( + CalendarEventAttendee.event_id == event_id, + CalendarEventAttendee.user_id == user_id, + ) + ) + att = result.scalars().first() + if not att: + return None + + att.status = status + att.updated_at = int(time.time_ns()) + await db.commit() + return CalendarEventAttendeeModel.model_validate(att) + + async def get_attendees_by_event( + self, event_id: str, db: Optional[AsyncSession] = None + ) -> list[CalendarEventAttendeeModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) + return [CalendarEventAttendeeModel.model_validate(r) for r in result.scalars().all()] + + async def get_events_by_attendee(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]: + """Return event IDs where user is an attendee.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) + ) + return [r[0] for r in result.all()] + + +Calendars = CalendarTable() +CalendarEvents = CalendarEventTable() +CalendarEventAttendees = CalendarEventAttendeeTable() diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index 8e70918e1a..9d5f130355 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -4,28 +4,33 @@ import time import uuid from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.access_grants import ( + AccessGrantModel, + AccessGrants, +) from open_webui.models.groups import Groups - -from pydantic import BaseModel, ConfigDict -from sqlalchemy.dialects.postgresql import JSONB - - +from open_webui.utils.validate import validate_profile_image_url +from pydantic import BaseModel, ConfigDict, Field, field_validator from sqlalchemy import ( + JSON, BigInteger, Boolean, Column, ForeignKey, String, Text, - JSON, UniqueConstraint, + and_, case, - cast, + delete, + func, + or_, + select, + update, ) -from sqlalchemy import or_, func, select, and_, text -from sqlalchemy.sql import exists +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession #################### # Channel DB Schema @@ -33,7 +38,7 @@ from sqlalchemy.sql import exists class Channel(Base): - __tablename__ = "channel" + __tablename__ = 'channel' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text) @@ -47,7 +52,6 @@ class Channel(Base): data = Column(JSON, nullable=True) meta = Column(JSON, nullable=True) - access_control = Column(JSON, nullable=True) created_at = Column(BigInteger) @@ -76,7 +80,7 @@ class ChannelModel(BaseModel): data: Optional[dict] = None meta: Optional[dict] = None - access_control: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) created_at: int # timestamp in epoch (time_ns) @@ -91,7 +95,7 @@ class ChannelModel(BaseModel): class ChannelMember(Base): - __tablename__ = "channel_member" + __tablename__ = 'channel_member' id = Column(Text, primary_key=True, unique=True) channel_id = Column(Text, nullable=False) @@ -151,25 +155,19 @@ class ChannelMemberModel(BaseModel): class ChannelFile(Base): - __tablename__ = "channel_file" + __tablename__ = 'channel_file' id = Column(Text, unique=True, primary_key=True) user_id = Column(Text, nullable=False) - channel_id = Column( - Text, ForeignKey("channel.id", ondelete="CASCADE"), nullable=False - ) - message_id = Column( - Text, ForeignKey("message.id", ondelete="CASCADE"), nullable=True - ) - file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False) + channel_id = Column(Text, ForeignKey('channel.id', ondelete='CASCADE'), nullable=False) + message_id = Column(Text, ForeignKey('message.id', ondelete='CASCADE'), nullable=True) + file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) created_at = Column(BigInteger, nullable=False) updated_at = Column(BigInteger, nullable=False) - __table_args__ = ( - UniqueConstraint("channel_id", "file_id", name="uq_channel_file_channel_file"), - ) + __table_args__ = (UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'),) class ChannelFileModel(BaseModel): @@ -186,7 +184,7 @@ class ChannelFileModel(BaseModel): class ChannelWebhook(Base): - __tablename__ = "channel_webhook" + __tablename__ = 'channel_webhook' id = Column(Text, primary_key=True, unique=True) channel_id = Column(Text, nullable=False) @@ -232,12 +230,12 @@ class ChannelResponse(ChannelModel): class ChannelForm(BaseModel): - name: str = "" + name: str = '' description: Optional[str] = None is_private: Optional[bool] = None data: Optional[dict] = None meta: Optional[dict] = None - access_control: Optional[dict] = None + access_grants: Optional[list[dict]] = None group_ids: Optional[list[str]] = None user_ids: Optional[list[str]] = None @@ -250,10 +248,31 @@ class ChannelWebhookForm(BaseModel): name: str profile_image_url: Optional[str] = None + @field_validator('profile_image_url', mode='before') + @classmethod + def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]: + if v is None: + return v + return validate_profile_image_url(v) + class ChannelTable: + async def _get_access_grants(self, channel_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('channel', channel_id, db=db) - def _collect_unique_user_ids( + async def _to_channel_model( + self, + channel: Channel, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> ChannelModel: + channel_data = ChannelModel.model_validate(channel).model_dump(exclude={'access_grants'}) + channel_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(channel_data['id'], db=db) + ) + return ChannelModel.model_validate(channel_data) + + async def _collect_unique_user_ids( self, invited_by: str, user_ids: Optional[list[str]] = None, @@ -270,7 +289,8 @@ class ChannelTable: users.add(invited_by) for group_id in group_ids or []: - users.update(Groups.get_group_user_ids_by_id(group_id)) + group_user_ids = await Groups.get_group_user_ids_by_id(group_id) + users.update(group_user_ids) return users @@ -290,45 +310,56 @@ class ChannelTable: for uid in user_ids: model = ChannelMemberModel( **{ - "id": str(uuid.uuid4()), - "channel_id": channel_id, - "user_id": uid, - "status": "joined", - "is_active": True, - "is_channel_muted": False, - "is_channel_pinned": False, - "invited_at": now, - "invited_by": invited_by, - "joined_at": now, - "left_at": None, - "last_read_at": now, - "created_at": now, - "updated_at": now, + 'id': str(uuid.uuid4()), + 'channel_id': channel_id, + 'user_id': uid, + 'status': 'joined', + 'is_active': True, + 'is_channel_muted': False, + 'is_channel_pinned': False, + 'invited_at': now, + 'invited_by': invited_by, + 'joined_at': now, + 'left_at': None, + 'last_read_at': now, + 'created_at': now, + 'updated_at': now, } ) memberships.append(ChannelMember(**model.model_dump())) return memberships - def insert_new_channel( - self, form_data: CreateChannelForm, user_id: str, db: Optional[Session] = None + def _has_permission(self, db, query, filter: dict, permission: str = 'read'): + return AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Channel, + filter=filter, + resource_type='channel', + permission=permission, + ) + + async def insert_new_channel( + self, form_data: CreateChannelForm, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: channel = ChannelModel( **{ - **form_data.model_dump(), - "type": form_data.type if form_data.type else None, - "name": form_data.name.lower(), - "id": str(uuid.uuid4()), - "user_id": user_id, - "created_at": int(time.time_ns()), - "updated_at": int(time.time_ns()), + **form_data.model_dump(exclude={'access_grants'}), + 'type': form_data.type if form_data.type else None, + 'name': form_data.name.lower(), + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'created_at': int(time.time_ns()), + 'updated_at': int(time.time_ns()), + 'access_grants': [], } ) - new_channel = Channel(**channel.model_dump()) + new_channel = Channel(**channel.model_dump(exclude={'access_grants'})) - if form_data.type in ["group", "dm"]: - users = self._collect_unique_user_ids( + if form_data.type in ['group', 'dm']: + users = await self._collect_unique_user_ids( invited_by=user_id, user_ids=form_data.user_ids, group_ids=form_data.group_ids, @@ -341,99 +372,67 @@ class ChannelTable: db.add_all(memberships) db.add(new_channel) - db.commit() - return channel + await db.commit() + await AccessGrants.set_access_grants('channel', new_channel.id, form_data.access_grants, db=db) + return await self._to_channel_model(new_channel, db=db) - def get_channels(self, db: Optional[Session] = None) -> list[ChannelModel]: - with get_db_context(db) as db: - channels = db.query(Channel).all() - return [ChannelModel.model_validate(channel) for channel in channels] - - def _has_permission(self, db, query, filter: dict, permission: str = "read"): - group_ids = filter.get("group_ids", []) - user_id = filter.get("user_id") - - dialect_name = db.bind.dialect.name - - # Public access - conditions = [] - if group_ids or user_id: - conditions.extend( - [ - Channel.access_control.is_(None), - cast(Channel.access_control, String) == "null", - ] - ) - - # User-level permission - if user_id: - conditions.append(Channel.user_id == user_id) - - # Group-level permission - if group_ids: - group_conditions = [] - for gid in group_ids: - if dialect_name == "sqlite": - group_conditions.append( - Channel.access_control[permission]["group_ids"].contains([gid]) - ) - elif dialect_name == "postgresql": - group_conditions.append( - cast( - Channel.access_control[permission]["group_ids"], - JSONB, - ).contains([gid]) - ) - conditions.append(or_(*group_conditions)) - - if conditions: - query = query.filter(or_(*conditions)) - - return query - - def get_channels_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[ChannelModel]: - with get_db_context(db) as db: - user_group_ids = [ - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) + async def get_channels(self, db: Optional[AsyncSession] = None) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel)) + channels = result.scalars().all() + channel_ids = [channel.id for channel in channels] + grants_map = await AccessGrants.get_grants_by_resources('channel', channel_ids, db=db) + return [ + await self._to_channel_model( + channel, + access_grants=grants_map.get(channel.id, []), + db=db, + ) + for channel in channels ] - membership_channels = ( - db.query(Channel) + async def get_channels_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id, db=db)] + + result = await db.execute( + select(Channel) .join(ChannelMember, Channel.id == ChannelMember.channel_id) .filter( Channel.deleted_at.is_(None), Channel.archived_at.is_(None), - Channel.type.in_(["group", "dm"]), + Channel.type.in_(['group', 'dm']), ChannelMember.user_id == user_id, ChannelMember.is_active.is_(True), ) - .all() ) + membership_channels = result.scalars().all() - query = db.query(Channel).filter( + stmt = select(Channel).filter( Channel.deleted_at.is_(None), Channel.archived_at.is_(None), or_( Channel.type.is_(None), # True NULL/None - Channel.type == "", # Empty string - and_(Channel.type != "group", Channel.type != "dm"), + Channel.type == '', # Empty string + and_(Channel.type != 'group', Channel.type != 'dm'), ), ) - query = self._has_permission( - db, query, {"user_id": user_id, "group_ids": user_group_ids} - ) + stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}) - standard_channels = query.all() + result = await db.execute(stmt) + standard_channels = result.scalars().all() - all_channels = membership_channels + standard_channels - return [ChannelModel.model_validate(c) for c in all_channels] + all_channels = list(membership_channels) + list(standard_channels) + channel_ids = [c.id for c in all_channels] + grants_map = await AccessGrants.get_grants_by_resources('channel', channel_ids, db=db) + return [ + await self._to_channel_model(c, access_grants=grants_map.get(c.id, []), db=db) for c in all_channels + ] - def get_dm_channel_by_user_ids( - self, user_ids: list[str], db: Optional[Session] = None + async def get_dm_channel_by_user_ids( + self, user_ids: list[str], db: Optional[AsyncSession] = None ) -> Optional[ChannelModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Ensure uniqueness in case a list with duplicates is passed unique_user_ids = list(set(user_ids)) @@ -445,7 +444,7 @@ class ChannelTable: ) subquery = ( - db.query(ChannelMember.channel_id) + select(ChannelMember.channel_id) .group_by(ChannelMember.channel_id) # 1. Channel must have exactly len(user_ids) members .having(func.count(ChannelMember.user_id) == len(unique_user_ids)) @@ -454,416 +453,386 @@ class ChannelTable: .subquery() ) - channel = ( - db.query(Channel) + result = await db.execute( + select(Channel) .filter( - Channel.id.in_(subquery), - Channel.type == "dm", + Channel.id.in_(select(subquery.c.channel_id)), + Channel.type == 'dm', ) - .first() + .limit(1) ) + channel = result.scalars().first() - return ChannelModel.model_validate(channel) if channel else None + return await self._to_channel_model(channel, db=db) if channel else None - def add_members_to_channel( + async def add_members_to_channel( self, channel_id: str, invited_by: str, user_ids: Optional[list[str]] = None, group_ids: Optional[list[str]] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> list[ChannelMemberModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # 1. Collect all user_ids including groups + inviter - requested_users = self._collect_unique_user_ids( - invited_by, user_ids, group_ids - ) + requested_users = await self._collect_unique_user_ids(invited_by, user_ids, group_ids) - existing_users = { - row.user_id - for row in db.query(ChannelMember.user_id) - .filter(ChannelMember.channel_id == channel_id) - .all() - } + result = await db.execute(select(ChannelMember.user_id).filter(ChannelMember.channel_id == channel_id)) + existing_users = {row[0] for row in result.all()} new_user_ids = requested_users - existing_users if not new_user_ids: return [] # Nothing to add - new_memberships = self._create_membership_models( - channel_id, invited_by, new_user_ids - ) + new_memberships = self._create_membership_models(channel_id, invited_by, new_user_ids) db.add_all(new_memberships) - db.commit() + await db.commit() - return [ - ChannelMemberModel.model_validate(membership) - for membership in new_memberships - ] + return [ChannelMemberModel.model_validate(membership) for membership in new_memberships] - def remove_members_from_channel( + async def remove_members_from_channel( self, channel_id: str, user_ids: list[str], - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> int: - with get_db_context(db) as db: - result = ( - db.query(ChannelMember) - .filter( + async with get_async_db_context(db) as db: + result = await db.execute( + delete(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id.in_(user_ids), ) - .delete(synchronize_session=False) ) - db.commit() - return result # number of rows deleted + await db.commit() + return result.rowcount # number of rows deleted - def is_user_channel_manager( - self, channel_id: str, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - # Check if the user is the creator of the channel - # or has a 'manager' role in ChannelMember - channel = db.query(Channel).filter(Channel.id == channel_id).first() + async def is_user_channel_manager(self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel).filter(Channel.id == channel_id)) + channel = result.scalars().first() if channel and channel.user_id == user_id: return True - membership = ( - db.query(ChannelMember) - .filter( + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, - ChannelMember.role == "manager", + ChannelMember.is_active.is_(True), + ChannelMember.role == 'manager', ) - .first() ) + membership = result.scalars().first() return membership is not None - def join_channel( - self, channel_id: str, user_id: str, db: Optional[Session] = None + async def join_channel( + self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelMemberModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Check if the membership already exists - existing_membership = ( - db.query(ChannelMember) - .filter( + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ) - .first() ) + existing_membership = result.scalars().first() if existing_membership: return ChannelMemberModel.model_validate(existing_membership) # Create new membership channel_member = ChannelMemberModel( **{ - "id": str(uuid.uuid4()), - "channel_id": channel_id, - "user_id": user_id, - "status": "joined", - "is_active": True, - "is_channel_muted": False, - "is_channel_pinned": False, - "joined_at": int(time.time_ns()), - "left_at": None, - "last_read_at": int(time.time_ns()), - "created_at": int(time.time_ns()), - "updated_at": int(time.time_ns()), + 'id': str(uuid.uuid4()), + 'channel_id': channel_id, + 'user_id': user_id, + 'status': 'joined', + 'is_active': True, + 'is_channel_muted': False, + 'is_channel_pinned': False, + 'joined_at': int(time.time_ns()), + 'left_at': None, + 'last_read_at': int(time.time_ns()), + 'created_at': int(time.time_ns()), + 'updated_at': int(time.time_ns()), } ) new_membership = ChannelMember(**channel_member.model_dump()) db.add(new_membership) - db.commit() + await db.commit() return channel_member - def leave_channel( - self, channel_id: str, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - membership = ( - db.query(ChannelMember) - .filter( + async def leave_channel(self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ) - .first() ) + membership = result.scalars().first() if not membership: return False - membership.status = "left" + membership.status = 'left' membership.is_active = False membership.left_at = int(time.time_ns()) membership.updated_at = int(time.time_ns()) - db.commit() + await db.commit() return True - def get_member_by_channel_and_user_id( - self, channel_id: str, user_id: str, db: Optional[Session] = None + async def get_member_by_channel_and_user_id( + self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelMemberModel]: - with get_db_context(db) as db: - membership = ( - db.query(ChannelMember) - .filter( + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ) - .first() ) + membership = result.scalars().first() return ChannelMemberModel.model_validate(membership) if membership else None - def get_members_by_channel_id( - self, channel_id: str, db: Optional[Session] = None + async def get_members_by_channel_id( + self, channel_id: str, db: Optional[AsyncSession] = None ) -> list[ChannelMemberModel]: - with get_db_context(db) as db: - memberships = ( - db.query(ChannelMember) - .filter(ChannelMember.channel_id == channel_id) - .all() - ) - return [ - ChannelMemberModel.model_validate(membership) - for membership in memberships - ] + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelMember).filter(ChannelMember.channel_id == channel_id)) + memberships = result.scalars().all() + return [ChannelMemberModel.model_validate(membership) for membership in memberships] - def pin_channel( + async def pin_channel( self, channel_id: str, user_id: str, is_pinned: bool, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> bool: - with get_db_context(db) as db: - membership = ( - db.query(ChannelMember) - .filter( + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ) - .first() ) + membership = result.scalars().first() if not membership: return False membership.is_channel_pinned = is_pinned membership.updated_at = int(time.time_ns()) - db.commit() + await db.commit() return True - def update_member_last_read_at( - self, channel_id: str, user_id: str, db: Optional[Session] = None + async def update_member_last_read_at( + self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None ) -> bool: - with get_db_context(db) as db: - membership = ( - db.query(ChannelMember) - .filter( + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ) - .first() ) + membership = result.scalars().first() if not membership: return False membership.last_read_at = int(time.time_ns()) membership.updated_at = int(time.time_ns()) - db.commit() + await db.commit() return True - def update_member_active_status( + async def update_member_active_status( self, channel_id: str, user_id: str, is_active: bool, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> bool: - with get_db_context(db) as db: - membership = ( - db.query(ChannelMember) - .filter( + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember).filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, ) - .first() ) + membership = result.scalars().first() if not membership: return False membership.is_active = is_active membership.updated_at = int(time.time_ns()) - db.commit() + await db.commit() return True - def is_user_channel_member( - self, channel_id: str, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - membership = ( - db.query(ChannelMember) + async def is_user_channel_member(self, channel_id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelMember) .filter( ChannelMember.channel_id == channel_id, ChannelMember.user_id == user_id, + ChannelMember.is_active.is_(True), ) - .first() + .limit(1) ) + membership = result.scalars().first() return membership is not None - def get_channel_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ChannelModel]: + async def get_channel_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChannelModel]: try: - with get_db_context(db) as db: - channel = db.query(Channel).filter(Channel.id == id).first() - return ChannelModel.model_validate(channel) if channel else None + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel).filter(Channel.id == id)) + channel = result.scalars().first() + return await self._to_channel_model(channel, db=db) if channel else None except Exception: return None - def get_channels_by_file_id( - self, file_id: str, db: Optional[Session] = None - ) -> list[ChannelModel]: - with get_db_context(db) as db: - channel_files = ( - db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all() - ) + async def get_channels_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[ChannelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelFile).filter(ChannelFile.file_id == file_id)) + channel_files = result.scalars().all() channel_ids = [cf.channel_id for cf in channel_files] - channels = db.query(Channel).filter(Channel.id.in_(channel_ids)).all() - return [ChannelModel.model_validate(channel) for channel in channels] + result = await db.execute(select(Channel).filter(Channel.id.in_(channel_ids))) + channels = result.scalars().all() + grants_map = await AccessGrants.get_grants_by_resources('channel', channel_ids, db=db) + return [ + await self._to_channel_model( + channel, + access_grants=grants_map.get(channel.id, []), + db=db, + ) + for channel in channels + ] - def get_channels_by_file_id_and_user_id( - self, file_id: str, user_id: str, db: Optional[Session] = None + async def get_channels_by_file_id_and_user_id( + self, file_id: str, user_id: str, db: Optional[AsyncSession] = None ) -> list[ChannelModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # 1. Determine which channels have this file - channel_file_rows = ( - db.query(ChannelFile).filter(ChannelFile.file_id == file_id).all() - ) + result = await db.execute(select(ChannelFile).filter(ChannelFile.file_id == file_id)) + channel_file_rows = result.scalars().all() channel_ids = [row.channel_id for row in channel_file_rows] if not channel_ids: return [] # 2. Load all channel rows that still exist - channels = ( - db.query(Channel) - .filter( + result = await db.execute( + select(Channel).filter( Channel.id.in_(channel_ids), Channel.deleted_at.is_(None), Channel.archived_at.is_(None), ) - .all() ) + channels = result.scalars().all() if not channels: return [] # Preload user's group membership - user_group_ids = [ - g.id for g in Groups.get_groups_by_member_id(user_id, db=db) - ] + user_group_ids = [g.id for g in await Groups.get_groups_by_member_id(user_id, db=db)] allowed_channels = [] for channel in channels: # --- Case A: group or dm => user must be an active member --- - if channel.type in ["group", "dm"]: - membership = ( - db.query(ChannelMember) + if channel.type in ['group', 'dm']: + result = await db.execute( + select(ChannelMember) .filter( ChannelMember.channel_id == channel.id, ChannelMember.user_id == user_id, ChannelMember.is_active.is_(True), ) - .first() + .limit(1) ) + membership = result.scalars().first() if membership: - allowed_channels.append(ChannelModel.model_validate(channel)) + allowed_channels.append(await self._to_channel_model(channel, db=db)) continue # --- Case B: standard channel => rely on ACL permissions --- - query = db.query(Channel).filter(Channel.id == channel.id) + stmt = select(Channel).filter(Channel.id == channel.id) - query = self._has_permission( + stmt = self._has_permission( db, - query, - {"user_id": user_id, "group_ids": user_group_ids}, - permission="read", + stmt, + {'user_id': user_id, 'group_ids': user_group_ids}, + permission='read', ) - allowed = query.first() + result = await db.execute(stmt) + allowed = result.scalars().first() if allowed: - allowed_channels.append(ChannelModel.model_validate(allowed)) + allowed_channels.append(await self._to_channel_model(allowed, db=db)) return allowed_channels - def get_channel_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def get_channel_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Fetch the channel - channel: Channel = ( - db.query(Channel) - .filter( + result = await db.execute( + select(Channel).filter( Channel.id == id, Channel.deleted_at.is_(None), Channel.archived_at.is_(None), ) - .first() ) + channel = result.scalars().first() if not channel: return None # If the channel is a group or dm, read access requires membership (active) - if channel.type in ["group", "dm"]: - membership = ( - db.query(ChannelMember) + if channel.type in ['group', 'dm']: + result = await db.execute( + select(ChannelMember) .filter( ChannelMember.channel_id == id, ChannelMember.user_id == user_id, ChannelMember.is_active.is_(True), ) - .first() + .limit(1) ) + membership = result.scalars().first() if membership: - return ChannelModel.model_validate(channel) + return await self._to_channel_model(channel, db=db) else: return None # For channels that are NOT group/dm, fall back to ACL-based read access - query = db.query(Channel).filter(Channel.id == id) + stmt = select(Channel).filter(Channel.id == id) # Determine user groups - user_group_ids = [ - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - ] + user_group_ids = [group.id for group in await Groups.get_groups_by_member_id(user_id, db=db)] # Apply ACL rules - query = self._has_permission( + stmt = self._has_permission( db, - query, - {"user_id": user_id, "group_ids": user_group_ids}, - permission="read", + stmt, + {'user_id': user_id, 'group_ids': user_group_ids}, + permission='read', ) - channel_allowed = query.first() - return ( - ChannelModel.model_validate(channel_allowed) - if channel_allowed - else None - ) + result = await db.execute(stmt) + channel_allowed = result.scalars().first() + return await self._to_channel_model(channel_allowed, db=db) if channel_allowed else None - def update_channel_by_id( - self, id: str, form_data: ChannelForm, db: Optional[Session] = None + async def update_channel_by_id( + self, id: str, form_data: ChannelForm, db: Optional[AsyncSession] = None ) -> Optional[ChannelModel]: - with get_db_context(db) as db: - channel = db.query(Channel).filter(Channel.id == id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Channel).filter(Channel.id == id)) + channel = result.scalars().first() if not channel: return None @@ -874,32 +843,33 @@ class ChannelTable: channel.data = form_data.data channel.meta = form_data.meta - channel.access_control = form_data.access_control + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('channel', id, form_data.access_grants, db=db) channel.updated_at = int(time.time_ns()) - db.commit() - return ChannelModel.model_validate(channel) if channel else None + await db.commit() + return await self._to_channel_model(channel, db=db) if channel else None - def add_file_to_channel_by_id( - self, channel_id: str, file_id: str, user_id: str, db: Optional[Session] = None + async def add_file_to_channel_by_id( + self, channel_id: str, file_id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelFileModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: channel_file = ChannelFileModel( **{ - "id": str(uuid.uuid4()), - "channel_id": channel_id, - "file_id": file_id, - "user_id": user_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + 'id': str(uuid.uuid4()), + 'channel_id': channel_id, + 'file_id': file_id, + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) try: result = ChannelFile(**channel_file.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return ChannelFileModel.model_validate(result) else: @@ -907,62 +877,58 @@ class ChannelTable: except Exception: return None - def set_file_message_id_in_channel_by_id( + async def set_file_message_id_in_channel_by_id( self, channel_id: str, file_id: str, message_id: str, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> bool: try: - with get_db_context(db) as db: - channel_file = ( - db.query(ChannelFile) - .filter_by(channel_id=channel_id, file_id=file_id) - .first() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelFile).filter_by(channel_id=channel_id, file_id=file_id)) + channel_file = result.scalars().first() if not channel_file: return False channel_file.message_id = message_id channel_file.updated_at = int(time.time()) - db.commit() + await db.commit() return True except Exception: return False - def remove_file_from_channel_by_id( - self, channel_id: str, file_id: str, db: Optional[Session] = None + async def remove_file_from_channel_by_id( + self, channel_id: str, file_id: str, db: Optional[AsyncSession] = None ) -> bool: try: - with get_db_context(db) as db: - db.query(ChannelFile).filter_by( - channel_id=channel_id, file_id=file_id - ).delete() - db.commit() + async with get_async_db_context(db) as db: + await db.execute(delete(ChannelFile).filter_by(channel_id=channel_id, file_id=file_id)) + await db.commit() return True except Exception: return False - def delete_channel_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - db.query(Channel).filter(Channel.id == id).delete() - db.commit() + async def delete_channel_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('channel', id, db=db) + await db.execute(delete(Channel).filter(Channel.id == id)) + await db.commit() return True #################### # Webhook Methods #################### - def insert_webhook( + async def insert_webhook( self, channel_id: str, user_id: str, form_data: ChannelWebhookForm, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[ChannelWebhookModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: webhook = ChannelWebhookModel( id=str(uuid.uuid4()), channel_id=channel_id, @@ -975,85 +941,70 @@ class ChannelTable: updated_at=int(time.time_ns()), ) db.add(ChannelWebhook(**webhook.model_dump())) - db.commit() + await db.commit() return webhook - def get_webhooks_by_channel_id( - self, channel_id: str, db: Optional[Session] = None + async def get_webhooks_by_channel_id( + self, channel_id: str, db: Optional[AsyncSession] = None ) -> list[ChannelWebhookModel]: - with get_db_context(db) as db: - webhooks = ( - db.query(ChannelWebhook) - .filter(ChannelWebhook.channel_id == channel_id) - .all() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.channel_id == channel_id)) + webhooks = result.scalars().all() return [ChannelWebhookModel.model_validate(w) for w in webhooks] - def get_webhook_by_id( - self, webhook_id: str, db: Optional[Session] = None + async def get_webhook_by_id( + self, webhook_id: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelWebhookModel]: - with get_db_context(db) as db: - webhook = ( - db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + webhook = result.scalars().first() return ChannelWebhookModel.model_validate(webhook) if webhook else None - def get_webhook_by_id_and_token( - self, webhook_id: str, token: str, db: Optional[Session] = None + async def get_webhook_by_id_and_token( + self, webhook_id: str, token: str, db: Optional[AsyncSession] = None ) -> Optional[ChannelWebhookModel]: - with get_db_context(db) as db: - webhook = ( - db.query(ChannelWebhook) - .filter( + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChannelWebhook).filter( ChannelWebhook.id == webhook_id, ChannelWebhook.token == token, ) - .first() ) + webhook = result.scalars().first() return ChannelWebhookModel.model_validate(webhook) if webhook else None - def update_webhook_by_id( + async def update_webhook_by_id( self, webhook_id: str, form_data: ChannelWebhookForm, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[ChannelWebhookModel]: - with get_db_context(db) as db: - webhook = ( - db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + webhook = result.scalars().first() if not webhook: return None webhook.name = form_data.name webhook.profile_image_url = form_data.profile_image_url webhook.updated_at = int(time.time_ns()) - db.commit() + await db.commit() return ChannelWebhookModel.model_validate(webhook) - def update_webhook_last_used_at( - self, webhook_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - webhook = ( - db.query(ChannelWebhook).filter(ChannelWebhook.id == webhook_id).first() - ) + async def update_webhook_last_used_at(self, webhook_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + webhook = result.scalars().first() if not webhook: return False webhook.last_used_at = int(time.time_ns()) - db.commit() + await db.commit() return True - def delete_webhook_by_id( - self, webhook_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - result = ( - db.query(ChannelWebhook) - .filter(ChannelWebhook.id == webhook_id) - .delete() - ) - db.commit() - return result > 0 + async def delete_webhook_by_id(self, webhook_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(delete(ChannelWebhook).filter(ChannelWebhook.id == webhook_id)) + await db.commit() + return result.rowcount > 0 Channels = ChannelTable() diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py new file mode 100644 index 0000000000..8660e1e987 --- /dev/null +++ b/backend/open_webui/models/chat_messages.py @@ -0,0 +1,688 @@ +import json +import time +import uuid +from typing import Any, Optional + +from open_webui.internal.db import Base, get_async_db_context +from open_webui.utils.response import normalize_usage +from pydantic import BaseModel, ConfigDict +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + ForeignKey, + Index, + Integer, + Text, + cast, + delete, + func, + select, +) +from sqlalchemy.ext.asyncio import AsyncSession + +#################### +# Helpers +#################### + + +def _normalize_timestamp(timestamp: int) -> float: + """Normalize and validate timestamp. Returns current time if invalid.""" + now = time.time() + + # Convert milliseconds to seconds if needed + if timestamp > 10_000_000_000: + timestamp = timestamp / 1000 + + # Validate: must be after 2020 and not in the future (with 1 day tolerance) + min_valid = 1577836800 # 2020-01-01 00:00:00 UTC + max_valid = now + 86400 # 1 day in the future (clock skew tolerance) + + if timestamp < min_valid or timestamp > max_valid: + return now + + return timestamp + + +def get_usage(data: dict) -> Optional[dict]: + """Extract and normalize usage from message data.""" + usage = data.get('usage') or (data.get('info') or {}).get('usage') + return normalize_usage(usage) if usage else None + + +def _token_columns(dialect: str): + """Return (input_tokens, output_tokens) SQL column expressions. + + Falls back to OpenAI-style keys (prompt_tokens / completion_tokens) + when the normalized keys are absent. + """ + if dialect == 'sqlite': + extract = lambda key: cast(func.json_extract(ChatMessage.usage, f'$.{key}'), Integer) + elif dialect == 'postgresql': + extract = lambda key: cast(func.json_extract_path_text(ChatMessage.usage, key), Integer) + else: + raise NotImplementedError(f'Unsupported dialect: {dialect}') + + return ( + func.coalesce(extract('input_tokens'), extract('prompt_tokens')), + func.coalesce(extract('output_tokens'), extract('completion_tokens')), + ) + + +#################### +# ChatMessage DB Schema +#################### + + +class ChatMessage(Base): + __tablename__ = 'chat_message' + + # Identity + id = Column(Text, primary_key=True) + chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False, index=True) + user_id = Column(Text, index=True) + + # Structure + role = Column(Text, nullable=False) # user, assistant, system + parent_id = Column(Text, nullable=True) + + # Content + content = Column(JSON, nullable=True) # Can be str or list of blocks + output = Column(JSON, nullable=True) + + # Model (for assistant messages) + model_id = Column(Text, nullable=True, index=True) + + # Attachments + files = Column(JSON, nullable=True) + sources = Column(JSON, nullable=True) + embeds = Column(JSON, nullable=True) + + # Status + done = Column(Boolean, default=True) + status_history = Column(JSON, nullable=True) + error = Column(JSON, nullable=True) + + # Usage (tokens, timing, etc.) + usage = Column(JSON, nullable=True) + + # Timestamps + created_at = Column(BigInteger, index=True) + updated_at = Column(BigInteger) + + __table_args__ = ( + Index('chat_message_chat_parent_idx', 'chat_id', 'parent_id'), + Index('chat_message_model_created_idx', 'model_id', 'created_at'), + Index('chat_message_user_created_idx', 'user_id', 'created_at'), + ) + + +#################### +# Pydantic Models +#################### + + +class ChatMessageModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + chat_id: str + user_id: str + role: str + parent_id: Optional[str] = None + content: Optional[Any] = None # str or list of blocks + output: Optional[list] = None + model_id: Optional[str] = None + files: Optional[list] = None + sources: Optional[list] = None + embeds: Optional[list] = None + done: bool = True + status_history: Optional[list] = None + error: Optional[dict | str] = None + usage: Optional[dict] = None + created_at: int + updated_at: int + + +#################### +# Table Operations +#################### + + +class ChatMessageTable: + async def upsert_message( + self, + message_id: str, + chat_id: str, + user_id: str, + data: dict, + db: Optional[AsyncSession] = None, + ) -> Optional[ChatMessageModel]: + """Insert or update a chat message.""" + async with get_async_db_context(db) as db: + now = int(time.time()) + timestamp = data.get('timestamp', now) + + # Use composite ID: {chat_id}-{message_id} + composite_id = f'{chat_id}-{message_id}' + + existing = await db.get(ChatMessage, composite_id) + if existing: + # Update existing + if 'role' in data: + existing.role = data['role'] + if 'parent_id' in data or 'parentId' in data: + existing.parent_id = data.get('parent_id') or data.get('parentId') + if 'content' in data: + existing.content = data.get('content') + if 'output' in data: + existing.output = data.get('output') + if 'model_id' in data or 'model' in data: + existing.model_id = data.get('model_id') or data.get('model') + if 'files' in data: + existing.files = data.get('files') + if 'sources' in data: + existing.sources = data.get('sources') + if 'embeds' in data: + existing.embeds = data.get('embeds') + if 'done' in data: + existing.done = data.get('done', True) + if 'status_history' in data or 'statusHistory' in data: + existing.status_history = data.get('status_history') or data.get('statusHistory') + if 'error' in data: + existing.error = data.get('error') + # Extract and normalize usage + usage = get_usage(data) + if usage: + # Deep-merge: preserve existing keys not present in new data + # This prevents background tasks (follow-ups, title, tags) + # from accidentally clearing the primary response's token counts + existing.usage = {**(existing.usage or {}), **usage} + existing.updated_at = now + await db.commit() + await db.refresh(existing) + return ChatMessageModel.model_validate(existing) + else: + # Insert new + # Extract and normalize usage + usage = get_usage(data) + message = ChatMessage( + id=composite_id, + chat_id=chat_id, + user_id=user_id, + role=data.get('role', 'user'), + parent_id=data.get('parent_id') or data.get('parentId'), + content=data.get('content'), + output=data.get('output'), + model_id=data.get('model_id') or data.get('model'), + files=data.get('files'), + sources=data.get('sources'), + embeds=data.get('embeds'), + done=data.get('done', True), + status_history=data.get('status_history') or data.get('statusHistory'), + error=data.get('error'), + usage=usage, + created_at=timestamp, + updated_at=now, + ) + db.add(message) + await db.commit() + await db.refresh(message) + return ChatMessageModel.model_validate(message) + + async def get_message_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ChatMessageModel]: + async with get_async_db_context(db) as db: + message = await db.get(ChatMessage, id) + return ChatMessageModel.model_validate(message) if message else None + + async def get_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> list[ChatMessageModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatMessage).filter_by(chat_id=chat_id).order_by(ChatMessage.created_at.asc()) + ) + messages = result.scalars().all() + return [ChatMessageModel.model_validate(message) for message in messages] + + # DB column names that differ from the JSON message keys. + DB_TO_JSON_KEY_MAP = { + 'parent_id': 'parentId', + 'model_id': 'model', + 'status_history': 'statusHistory', + 'created_at': 'timestamp', + } + # DB-internal columns excluded from the reconstructed message dict. + EXCLUDED_COLUMNS = frozenset({'id', 'chat_id', 'user_id', 'updated_at'}) + + async def get_messages_map_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> Optional[dict]: + """Build a {message_id: message_dict} map from chat_message rows. + + Returns the same shape as chat.history.messages so callers + (get_message_list, middleware) work unchanged. Returns None if + no rows exist for the chat (caller should fall back to the + embedded JSON blob for legacy chats). + """ + async with get_async_db_context(db) as db: + result = await db.execute(select(ChatMessage).filter_by(chat_id=chat_id)) + rows = result.scalars().all() + + if not rows: + return None + + # Strip the composite-id prefix ("{chat_id}-") to recover the + # original message_id used as map key. + prefix = f'{chat_id}-' + prefix_len = len(prefix) + col_keys = [c.key for c in ChatMessage.__table__.columns] + + messages_map: dict[str, dict] = {} + for row in rows: + msg_id = row.id[prefix_len:] if row.id.startswith(prefix) else row.id + + msg: dict = {'id': msg_id} + for key in col_keys: + if key in self.EXCLUDED_COLUMNS: + continue + val = getattr(row, key) + if val is None: + continue + json_key = self.DB_TO_JSON_KEY_MAP.get(key, key) + msg[json_key] = val + + # Ensure content always has a value + msg.setdefault('content', '') + + # Mirror usage into info.usage for callers that read it there + if 'usage' in msg: + msg['info'] = {'usage': msg['usage']} + + messages_map[msg_id] = msg + + # Reconstruct childrenIds from parentId links so that the map + # is fully navigable (callers like the frontend rely on this). + for msg_id, msg in messages_map.items(): + parent_id = msg.get('parentId') + if parent_id and parent_id in messages_map: + parent = messages_map[parent_id] + children = parent.get('childrenIds') + if children is None: + parent['childrenIds'] = [msg_id] + elif msg_id not in children: + children.append(msg_id) + + # Ensure every message has a childrenIds list (leaf nodes get []) + for msg in messages_map.values(): + if 'childrenIds' not in msg: + msg['childrenIds'] = [] + + return messages_map + + async def get_messages_by_user_id( + self, + user_id: str, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[ChatMessageModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatMessage) + .filter_by(user_id=user_id) + .order_by(ChatMessage.created_at.desc()) + .offset(skip) + .limit(limit) + ) + messages = result.scalars().all() + return [ChatMessageModel.model_validate(message) for message in messages] + + async def get_messages_by_model_id( + self, + model_id: str, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + skip: int = 0, + limit: int = 100, + db: Optional[AsyncSession] = None, + ) -> list[ChatMessageModel]: + async with get_async_db_context(db) as db: + stmt = select(ChatMessage).filter_by(model_id=model_id) + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + stmt = stmt.order_by(ChatMessage.created_at.desc()).offset(skip).limit(limit) + result = await db.execute(stmt) + messages = result.scalars().all() + return [ChatMessageModel.model_validate(message) for message in messages] + + async def get_chat_ids_by_model_id( + self, + model_id: str, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[str]: + """Get distinct chat_ids that used a specific model.""" + + async with get_async_db_context(db) as db: + stmt = select( + ChatMessage.chat_id, + func.max(ChatMessage.created_at).label('last_message_at'), + ).filter(ChatMessage.model_id == model_id) + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + + # Group by chat_id and order by most recent message in each chat + # Secondary sort on chat_id ensures deterministic pagination + stmt = ( + stmt.group_by(ChatMessage.chat_id) + .order_by(func.max(ChatMessage.created_at).desc(), ChatMessage.chat_id) + .offset(skip) + .limit(limit) + ) + result = await db.execute(stmt) + chat_ids = result.all() + return [chat_id for chat_id, _ in chat_ids] + + async def delete_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(ChatMessage).filter_by(chat_id=chat_id)) + await db.commit() + return True + + async def delete_message_ids_by_chat_id( + self, + chat_id: str, + message_ids: set[str], + db: Optional[AsyncSession] = None, + ) -> bool: + """Delete specific ``chat_message`` rows by their original message IDs.""" + if not message_ids: + return True + async with get_async_db_context(db) as db: + await db.execute( + delete(ChatMessage) + .where(ChatMessage.chat_id == chat_id) + .where(ChatMessage.id.in_({f'{chat_id}-{mid}' for mid in message_ids})) + ) + await db.commit() + return True + + # Analytics methods + async def get_message_count_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, int]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.model_id, func.count(ChatMessage.id).label('count')).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.model_id) + result = await db.execute(stmt) + return {row.model_id: row.count for row in result.all()} + + async def get_token_usage_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict]: + """Aggregate token usage by model using database-level aggregation.""" + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + # We need the dialect to determine JSON extraction syntax + # For async sessions, access via get_bind() + bind = await db.connection() + dialect = bind.dialect.name + + input_tokens, output_tokens = _token_columns(dialect) + + stmt = select( + ChatMessage.model_id, + func.coalesce(func.sum(input_tokens), 0).label('input_tokens'), + func.coalesce(func.sum(output_tokens), 0).label('output_tokens'), + func.count(ChatMessage.id).label('message_count'), + ).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ChatMessage.usage.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.model_id) + result = await db.execute(stmt) + + return { + row.model_id: { + 'input_tokens': row.input_tokens, + 'output_tokens': row.output_tokens, + 'total_tokens': row.input_tokens + row.output_tokens, + 'message_count': row.message_count, + } + for row in result.all() + } + + async def get_token_usage_by_user( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict]: + """Aggregate token usage by user using database-level aggregation.""" + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + bind = await db.connection() + dialect = bind.dialect.name + + input_tokens, output_tokens = _token_columns(dialect) + + stmt = select( + ChatMessage.user_id, + func.coalesce(func.sum(input_tokens), 0).label('input_tokens'), + func.coalesce(func.sum(output_tokens), 0).label('output_tokens'), + func.count(ChatMessage.id).label('message_count'), + ).filter( + ChatMessage.role == 'assistant', + ChatMessage.user_id.isnot(None), + ChatMessage.usage.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.user_id) + result = await db.execute(stmt) + + return { + row.user_id: { + 'input_tokens': row.input_tokens, + 'output_tokens': row.output_tokens, + 'total_tokens': row.input_tokens + row.output_tokens, + 'message_count': row.message_count, + } + for row in result.all() + } + + async def get_message_count_by_user( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, int]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.user_id, func.count(ChatMessage.id).label('count')).filter( + ChatMessage.role == 'assistant', + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.user_id) + result = await db.execute(stmt) + return {row.user_id: row.count for row in result.all()} + + async def get_message_count_by_chat( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, int]: + async with get_async_db_context(db) as db: + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.chat_id, func.count(ChatMessage.id).label('count')).filter( + ChatMessage.role == 'assistant', + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + stmt = stmt.group_by(ChatMessage.chat_id) + result = await db.execute(stmt) + return {row.chat_id: row.count for row in result.all()} + + async def get_daily_message_counts_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + group_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict[str, int]]: + """Get message counts grouped by day and model.""" + async with get_async_db_context(db) as db: + from datetime import datetime, timedelta + + from open_webui.models.groups import GroupMember + + stmt = select(ChatMessage.created_at, ChatMessage.model_id).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + if group_id: + group_users = select(GroupMember.user_id).filter(GroupMember.group_id == group_id).scalar_subquery() + stmt = stmt.filter(ChatMessage.user_id.in_(group_users)) + + result = await db.execute(stmt) + results = result.all() + + # Group by date -> model -> count + daily_counts: dict[str, dict[str, int]] = {} + for timestamp, model_id in results: + date_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d') + if date_str not in daily_counts: + daily_counts[date_str] = {} + daily_counts[date_str][model_id] = daily_counts[date_str].get(model_id, 0) + 1 + + # Fill in missing days + if start_date and end_date: + current = datetime.fromtimestamp(_normalize_timestamp(start_date)) + end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date)) + while current <= end_dt: + date_str = current.strftime('%Y-%m-%d') + if date_str not in daily_counts: + daily_counts[date_str] = {} + current += timedelta(days=1) + + return daily_counts + + async def get_hourly_message_counts_by_model( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + db: Optional[AsyncSession] = None, + ) -> dict[str, dict[str, int]]: + """Get message counts grouped by hour and model.""" + async with get_async_db_context(db) as db: + from datetime import datetime, timedelta + + stmt = select(ChatMessage.created_at, ChatMessage.model_id).filter( + ChatMessage.role == 'assistant', + ChatMessage.model_id.isnot(None), + ) + + if start_date: + stmt = stmt.filter(ChatMessage.created_at >= start_date) + if end_date: + stmt = stmt.filter(ChatMessage.created_at <= end_date) + + result = await db.execute(stmt) + results = result.all() + + # Group by hour -> model -> count + hourly_counts: dict[str, dict[str, int]] = {} + for timestamp, model_id in results: + hour_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d %H:00') + if hour_str not in hourly_counts: + hourly_counts[hour_str] = {} + hourly_counts[hour_str][model_id] = hourly_counts[hour_str].get(model_id, 0) + 1 + + # Fill in missing hours + if start_date and end_date: + current = datetime.fromtimestamp(_normalize_timestamp(start_date)).replace( + minute=0, second=0, microsecond=0 + ) + end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date)) + while current <= end_dt: + hour_str = current.strftime('%Y-%m-%d %H:00') + if hour_str not in hourly_counts: + hourly_counts[hour_str] = {} + current += timedelta(hours=1) + + return hourly_counts + + +ChatMessages = ChatMessageTable() diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 12359eec9f..1ad6959e5c 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -1,74 +1,79 @@ -import logging +"""Chat models, forms, and database operations.""" + +from __future__ import annotations + import json +import logging import time import uuid -from typing import Optional - -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from open_webui.models.tags import TagModel, Tag, Tags +# local imports +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.automations import AutomationRun +from open_webui.models.chat_messages import ChatMessage, ChatMessages from open_webui.models.folders import Folders +from open_webui.models.tags import Tag, TagModel, Tags from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db - from pydantic import BaseModel, ConfigDict from sqlalchemy import ( + JSON, BigInteger, Boolean, Column, ForeignKey, + Index, String, Text, - JSON, - Index, UniqueConstraint, + and_, + delete, + func, + or_, + select, + text, + update, ) -from sqlalchemy import or_, func, select, and_, text +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql import exists from sqlalchemy.sql.expression import bindparam -#################### -# Chat DB Schema -#################### - log = logging.getLogger(__name__) -class Chat(Base): - __tablename__ = "chat" +class Chat(Base): # database table mapping for chat entity + __tablename__ = 'chat' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) - title = Column(Text) + user_id = Column(String, index=True) # owner user id + title = Column(Text) # user-visible conversation title chat = Column(JSON) - created_at = Column(BigInteger) - updated_at = Column(BigInteger) + created_at = Column(BigInteger, index=True) # conversation creation timestamp + updated_at = Column(BigInteger, index=True) # conversation modification timestamp - share_id = Column(Text, unique=True, nullable=True) - archived = Column(Boolean, default=False) + share_id = Column(Text, unique=True, nullable=True) # public share link token + archived = Column(Boolean, default=False) # hidden from main chat list pinned = Column(Boolean, default=False, nullable=True) - meta = Column(JSON, server_default="{}") + meta = Column(JSON, server_default='{}') folder_id = Column(Text, nullable=True) + tasks = Column(JSON, nullable=True) + summary = Column(Text, nullable=True) + + last_read_at = Column(BigInteger, nullable=True) + __table_args__ = ( # Performance indexes for common queries - # WHERE folder_id = ... - Index("folder_id_idx", "folder_id"), - # WHERE user_id = ... AND pinned = ... - Index("user_id_pinned_idx", "user_id", "pinned"), - # WHERE user_id = ... AND archived = ... - Index("user_id_archived_idx", "user_id", "archived"), - # WHERE user_id = ... ORDER BY updated_at DESC - Index("updated_at_user_id_idx", "updated_at", "user_id"), - # WHERE folder_id = ... AND user_id = ... - Index("folder_id_user_id_idx", "folder_id", "user_id"), + Index('folder_id_idx', 'folder_id'), + Index('user_id_pinned_idx', 'user_id', 'pinned'), + Index('user_id_archived_idx', 'user_id', 'archived'), + Index('updated_at_user_id_idx', 'updated_at', 'user_id'), + Index('folder_id_user_id_idx', 'folder_id', 'user_id'), ) class ChatModel(BaseModel): - model_config = ConfigDict(from_attributes=True) - + model_config = ConfigDict(from_attributes=True) # allows ORM model binding id: str user_id: str title: str @@ -77,30 +82,33 @@ class ChatModel(BaseModel): created_at: int # timestamp in epoch updated_at: int # timestamp in epoch - share_id: Optional[str] = None + share_id: str | None = None archived: bool = False - pinned: Optional[bool] = False + pinned: bool | None = False meta: dict = {} - folder_id: Optional[str] = None + folder_id: str | None = None + + tasks: list | None = None + summary: str | None = None + + last_read_at: int | None = None class ChatFile(Base): - __tablename__ = "chat_file" + __tablename__ = 'chat_file' id = Column(Text, unique=True, primary_key=True) user_id = Column(Text, nullable=False) - chat_id = Column(Text, ForeignKey("chat.id", ondelete="CASCADE"), nullable=False) + chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False) message_id = Column(Text, nullable=True) - file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False) + file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) created_at = Column(BigInteger, nullable=False) updated_at = Column(BigInteger, nullable=False) - __table_args__ = ( - UniqueConstraint("chat_id", "file_id", name="uq_chat_file_chat_file"), - ) + __table_args__ = (UniqueConstraint('chat_id', 'file_id', name='uq_chat_file_chat_file'),) class ChatFileModel(BaseModel): @@ -108,7 +116,7 @@ class ChatFileModel(BaseModel): user_id: str chat_id: str - message_id: Optional[str] = None + message_id: str | None = None file_id: str created_at: int @@ -124,14 +132,14 @@ class ChatFileModel(BaseModel): class ChatForm(BaseModel): chat: dict - folder_id: Optional[str] = None + folder_id: str | None = None class ChatImportForm(ChatForm): - meta: Optional[dict] = {} - pinned: Optional[bool] = False - created_at: Optional[int] = None - updated_at: Optional[int] = None + meta: dict | None = {} + pinned: bool | None = False + created_at: int | None = None + updated_at: int | None = None class ChatsImportForm(BaseModel): @@ -154,11 +162,14 @@ class ChatResponse(BaseModel): chat: dict updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - share_id: Optional[str] = None # id of the chat to be shared + share_id: str | None = None # id of the chat to be shared archived: bool - pinned: Optional[bool] = False + pinned: bool | None = False meta: dict = {} - folder_id: Optional[str] = None + folder_id: str | None = None + + tasks: list | None = None + summary: str | None = None class ChatTitleIdResponse(BaseModel): @@ -166,6 +177,15 @@ class ChatTitleIdResponse(BaseModel): title: str updated_at: int created_at: int + last_read_at: int | None = None + + +class SharedChatResponse(BaseModel): + id: str + title: str + share_id: str | None = None + updated_at: int + created_at: int class ChatListResponse(BaseModel): @@ -182,19 +202,11 @@ class ChatUsageStatsResponse(BaseModel): history_models: dict = {} # models used in the chat history with their usage counts history_message_count: int # number of messages in the chat history history_user_message_count: int # number of user messages in the chat history - history_assistant_message_count: ( - int # number of assistant messages in the chat history - ) + history_assistant_message_count: int # number of assistant messages in the chat history - average_response_time: ( - float # average response time of assistant messages in seconds - ) - average_user_message_content_length: ( - float # average length of user message contents - ) - average_assistant_message_content_length: ( - float # average length of assistant message contents - ) + average_response_time: float # average response time of assistant messages in seconds + average_user_message_content_length: float # average length of user message contents + average_assistant_message_content_length: float # average length of assistant message contents tags: list[str] = [] # tags associated with the chat @@ -202,29 +214,29 @@ class ChatUsageStatsResponse(BaseModel): updated_at: int created_at: int - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class ChatUsageStatsListResponse(BaseModel): items: list[ChatUsageStatsResponse] total: int - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class MessageStats(BaseModel): id: str role: str - model: Optional[str] = None + model: str | None = None content_length: int - token_count: Optional[int] = None - timestamp: Optional[int] = None - rating: Optional[int] = None # Derived from message.annotation.rating - tags: Optional[list[str]] = None # Derived from message.annotation.tags + token_count: int | None = None + timestamp: int | None = None + rating: int | None = None # Derived from message.annotation.rating + tags: list[str | None] = None # Derived from message.annotation.tags class ChatHistoryStats(BaseModel): messages: dict[str, MessageStats] - currentId: Optional[str] = None + currentId: str | None = None class ChatBody(BaseModel): @@ -281,633 +293,817 @@ class ChatTable: return changed - def insert_new_chat( - self, user_id: str, form_data: ChatForm, db: Optional[Session] = None - ) -> Optional[ChatModel]: - with get_db_context(db) as db: - id = str(uuid.uuid4()) + async def insert_new_chat( + self, id: str, user_id: str, form_data: ChatForm, db: AsyncSession | None = None + ) -> ChatModel | None: + async with get_async_db_context(db) as session: chat = ChatModel( **{ - "id": id, - "user_id": user_id, - "title": self._clean_null_bytes( - form_data.chat["title"] - if "title" in form_data.chat - else "New Chat" + 'id': id, + 'user_id': user_id, + 'title': self._clean_null_bytes( + form_data.chat['title'] if 'title' in form_data.chat else 'New Chat' ), - "chat": self._clean_null_bytes(form_data.chat), - "folder_id": form_data.folder_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + 'chat': self._clean_null_bytes(form_data.chat), + 'folder_id': form_data.folder_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) chat_item = Chat(**chat.model_dump()) - db.add(chat_item) - db.commit() - db.refresh(chat_item) + session.add(chat_item) + await session.commit() + await session.refresh(chat_item) + + # Dual-write initial messages to chat_message table + try: + history = form_data.chat.get('history', {}) + messages = history.get('messages', {}) + for message_id, message in messages.items(): + if isinstance(message, dict) and message.get('role'): + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=id, + user_id=user_id, + data=message, + ) + except Exception as e: + log.warning(f'Failed to write initial messages to chat_message table: {e}') + return ChatModel.model_validate(chat_item) if chat_item else None - def _chat_import_form_to_chat_model( - self, user_id: str, form_data: ChatImportForm - ) -> ChatModel: + def _chat_import_form_to_chat_model(self, user_id: str, form_data: ChatImportForm) -> ChatModel: id = str(uuid.uuid4()) chat = ChatModel( **{ - "id": id, - "user_id": user_id, - "title": self._clean_null_bytes( - form_data.chat["title"] if "title" in form_data.chat else "New Chat" - ), - "chat": self._clean_null_bytes(form_data.chat), - "meta": form_data.meta, - "pinned": form_data.pinned, - "folder_id": form_data.folder_id, - "created_at": ( - form_data.created_at if form_data.created_at else int(time.time()) - ), - "updated_at": ( - form_data.updated_at if form_data.updated_at else int(time.time()) - ), + 'id': id, + 'user_id': user_id, + 'title': self._clean_null_bytes(form_data.chat['title'] if 'title' in form_data.chat else 'New Chat'), + 'chat': self._clean_null_bytes(form_data.chat), + 'meta': form_data.meta, + 'pinned': form_data.pinned, + 'folder_id': form_data.folder_id, + 'created_at': (form_data.created_at if form_data.created_at else int(time.time())), + 'updated_at': (form_data.updated_at if form_data.updated_at else int(time.time())), } ) return chat - def import_chats( + async def import_chats( self, user_id: str, chat_import_forms: list[ChatImportForm], - db: Optional[Session] = None, + db: AsyncSession | None = None, ) -> list[ChatModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as session: chats = [] for form_data in chat_import_forms: chat = self._chat_import_form_to_chat_model(user_id, form_data) chats.append(Chat(**chat.model_dump())) - db.add_all(chats) - db.commit() + session.add_all(chats) + await session.commit() + + # Dual-write messages to chat_message table + for form_data, chat_obj in zip(chat_import_forms, chats): + history = form_data.chat.get('history', {}) + messages = history.get('messages', {}) + for message_id, message in messages.items(): + if isinstance(message, dict) and message.get('role'): + try: + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=chat_obj.id, + user_id=user_id, + data=message, + ) + except Exception as e: + log.warning(f'Failed to write imported message {message_id} for chat {chat_obj.id}: {e}') + return [ChatModel.model_validate(chat) for chat in chats] - def update_chat_by_id( - self, id: str, chat: dict, db: Optional[Session] = None - ) -> Optional[ChatModel]: - try: - with get_db_context(db) as db: - chat_item = db.get(Chat, id) + async def update_chat_by_id( + self, id: str, chat: dict, db: AsyncSession | None = None, + ) -> ChatModel | None: + """Persist updated chat content, sanitizing null bytes.""" + try: # load the chat record for in-place mutation + async with get_async_db_context(db) as session: + chat_item = await session.get(Chat, id) + if chat_item is None: + return None + chat_item.chat = self._clean_null_bytes(chat) - chat_item.title = ( - self._clean_null_bytes(chat["title"]) - if "title" in chat - else "New Chat" - ) + chat_item.title = self._clean_null_bytes(chat['title']) if 'title' in chat else 'New Chat' chat_item.updated_at = int(time.time()) - db.commit() - db.refresh(chat_item) + await session.commit() return ChatModel.model_validate(chat_item) except Exception: + return + + async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: + try: + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) + if chat and chat.user_id == user_id: + chat.last_read_at = int(time.time()) + await session.commit() + return True + return False + except Exception: + return False + + async def update_chat_title_by_id(self, id: str, title: str) -> ChatModel | None: + try: + async with get_async_db_context() as session: + chat_item = await session.get(Chat, id) + if chat_item is None: + return None + clean_title = self._clean_null_bytes(title) + chat_item.title = clean_title + chat_item.chat = {**(chat_item.chat or {}), 'title': clean_title} + chat_item.updated_at = int(time.time()) + await session.commit() + await session.refresh(chat_item) + return ChatModel.model_validate(chat_item) + except Exception: return None - def update_chat_title_by_id(self, id: str, title: str) -> Optional[ChatModel]: - chat = self.get_chat_by_id(id) - if chat is None: - return None + async def update_chat_tags_by_id(self, id: str, tags: list[str], user) -> ChatModel | None: + async with get_async_db_context() as session: + chat = await session.get(Chat, id) + if chat is None: + return None - chat = chat.chat - chat["title"] = title + old_tags = chat.meta.get('tags', []) + new_tags = [t for t in tags if t.replace(' ', '_').lower() != 'none'] + new_tag_ids = [t.replace(' ', '_').lower() for t in new_tags] - return self.update_chat_by_id(id, chat) + # Single meta update + chat.meta = {**chat.meta, 'tags': new_tag_ids} + await session.commit() + await session.refresh(chat) - def update_chat_tags_by_id( - self, id: str, tags: list[str], user - ) -> Optional[ChatModel]: - chat = self.get_chat_by_id(id) - if chat is None: - return None + # Batch-create any missing tag rows + await Tags.ensure_tags_exist(new_tags, user.id, db=session) - self.delete_all_tags_by_id_and_user_id(id, user.id) + # Clean up orphaned old tags in one query + removed = set(old_tags) - set(new_tag_ids) + if removed: + await self.delete_orphan_tags_for_user(list(removed), user.id, db=session) - for tag in chat.meta.get("tags", []): - if self.count_chats_by_tag_name_and_user_id(tag, user.id) == 0: - Tags.delete_tag_by_name_and_user_id(tag, user.id) + return ChatModel.model_validate(chat) - for tag_name in tags: - if tag_name.lower() == "none": + async def get_chat_title_by_id(self, id: str) -> str | None: + async with get_async_db_context() as session: + result = await session.execute(select(Chat.title).filter_by(id=id)) + row = result.first() + if row is None: + return None + return row[0] or 'New Chat' + + @staticmethod + def get_unresolved_parent_ids(messages_map: dict) -> set[str]: + """Return parent IDs referenced by messages but absent from the map. + + An empty set means the message graph is fully connected. + """ + return { + msg['parentId'] + for msg in messages_map.values() + if msg.get('parentId') and msg['parentId'] not in messages_map + } + + async def backfill_messages_by_chat_id(self, chat_id: str, user_id: str, messages: dict[str, dict]) -> None: + """Write messages to the ``chat_message`` table so future lookups + use the fast path. Errors are logged but never raised. + """ + for message_id, message in messages.items(): + if not isinstance(message, dict) or not message.get('role'): continue + try: + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=chat_id, + user_id=user_id, + data=message, + ) + except Exception as e: + log.warning('Backfill failed for message %s in chat %s: %s', message_id, chat_id, e) - self.add_chat_tag_by_id_and_user_id_and_tag_name(id, user.id, tag_name) - return self.get_chat_by_id(id) + async def reconcile_messages_by_chat_id( + self, chat_id: str, user_id: str, messages: dict[str, dict] + ) -> None: + """Sync ``chat_message`` rows with the committed JSON blob. - def get_chat_title_by_id(self, id: str) -> Optional[str]: - chat = self.get_chat_by_id(id) + Upserts current messages via ``backfill_messages_by_chat_id`` + and deletes orphaned rows whose message_id no longer appears + in the blob. Best-effort: errors are logged but never raised. + """ + try: + await self.backfill_messages_by_chat_id(chat_id, user_id, messages) + + existing_map = await ChatMessages.get_messages_map_by_chat_id(chat_id) + if existing_map is not None: + orphaned_ids = set(existing_map.keys()) - set(messages.keys()) + if orphaned_ids: + await ChatMessages.delete_message_ids_by_chat_id(chat_id, orphaned_ids) + except Exception as e: + log.warning('Failed to reconcile chat_message rows for chat %s: %s', chat_id, e) + + async def get_messages_map_by_chat_id(self, id: str) -> dict | None: + """Message map for walking history (see ``get_message_list``). + + Prefer ``chat_message`` rows to avoid loading the large embedded + history; fall back to the legacy JSON when no rows exist. + When rows exist but the parent-link graph has gaps (e.g. migration + failures), missing messages are merged from the legacy history + and backfilled so future requests self-heal. + """ + # Fast path: build from normalized chat_message rows. + messages_map = await ChatMessages.get_messages_map_by_chat_id(id) + + if messages_map is not None: + unresolved_ids = self.get_unresolved_parent_ids(messages_map) + if not unresolved_ids: + return messages_map + + # Graph has gaps — enrich from the legacy embedded history. + log.info( + 'Chat %s: %d unresolved parent reference(s) in chat_message — enriching from legacy history', + id, + len(unresolved_ids), + ) + chat = await self.get_chat_by_id(id) + if chat: + history_messages = chat.chat.get('history', {}).get('messages', {}) or {} + missing_messages = { + message_id: history_messages[message_id] + for message_id in unresolved_ids + if message_id in history_messages + } + + if missing_messages: + messages_map.update(missing_messages) + + # Backfill so future requests use the fast path. + await self.backfill_messages_by_chat_id(id, chat.user_id, missing_messages) + + return messages_map + + # No rows — fall back to the legacy embedded history. + chat = await self.get_chat_by_id(id) if chat is None: return None - return chat.chat.get("title", "New Chat") + history_messages = chat.chat.get('history', {}).get('messages', {}) or {} - def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: - chat = self.get_chat_by_id(id) + # Backfill so future requests use the fast path. + if history_messages: + await self.backfill_messages_by_chat_id(id, chat.user_id, history_messages) + + return history_messages + + async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> dict | None: + chat = await self.get_chat_by_id(id) if chat is None: return None - return chat.chat.get("history", {}).get("messages", {}) or {} + return chat.chat.get('history', {}).get('messages', {}).get(message_id, {}) - def get_message_by_id_and_message_id( - self, id: str, message_id: str - ) -> Optional[dict]: - chat = self.get_chat_by_id(id) - if chat is None: - return None - - return chat.chat.get("history", {}).get("messages", {}).get(message_id, {}) - - def upsert_message_to_chat_by_id_and_message_id( + async def upsert_message_to_chat_by_id_and_message_id( self, id: str, message_id: str, message: dict - ) -> Optional[ChatModel]: - chat = self.get_chat_by_id(id) + ) -> ChatModel | None: + chat = await self.get_chat_by_id(id) if chat is None: return None # Sanitize message content for null characters before upserting - if isinstance(message.get("content"), str): - message["content"] = sanitize_text_for_db(message["content"]) + if isinstance(message.get('content'), str): + message['content'] = sanitize_text_for_db(message['content']) + user_id = chat.user_id chat = chat.chat - history = chat.get("history", {}) + history = chat.get('history', {}) - if message_id in history.get("messages", {}): - history["messages"][message_id] = { - **history["messages"][message_id], + if message_id in history.get('messages', {}): + history['messages'][message_id] = { + **history['messages'][message_id], **message, } else: - history["messages"][message_id] = message + history['messages'][message_id] = message - history["currentId"] = message_id + history['currentId'] = message_id - chat["history"] = history - return self.update_chat_by_id(id, chat) + chat['history'] = history - def add_message_status_to_chat_by_id_and_message_id( + # Dual-write to chat_message table + try: + await ChatMessages.upsert_message( + message_id=message_id, + chat_id=id, + user_id=user_id, + data=history['messages'][message_id], + ) + except Exception as e: + log.warning(f'Failed to write to chat_message table: {e}') + + return await self.update_chat_by_id(id, chat) + + async def add_message_status_to_chat_by_id_and_message_id( self, id: str, message_id: str, status: dict - ) -> Optional[ChatModel]: - chat = self.get_chat_by_id(id) + ) -> ChatModel | None: + chat = await self.get_chat_by_id(id) if chat is None: return None chat = chat.chat - history = chat.get("history", {}) + history = chat.get('history', {}) - if message_id in history.get("messages", {}): - status_history = history["messages"][message_id].get("statusHistory", []) + if message_id in history.get('messages', {}): + status_history = history['messages'][message_id].get('statusHistory', []) status_history.append(status) - history["messages"][message_id]["statusHistory"] = status_history + history['messages'][message_id]['statusHistory'] = status_history - chat["history"] = history - return self.update_chat_by_id(id, chat) + chat['history'] = history + return await self.update_chat_by_id(id, chat) - def add_message_files_by_id_and_message_id( - self, id: str, message_id: str, files: list[dict] - ) -> list[dict]: - with get_db_context() as db: - chat = self.get_chat_by_id(id, db=db) + async def add_message_files_by_id_and_message_id(self, id: str, message_id: str, files: list[dict]) -> list[dict]: + async with get_async_db_context() as session: + chat = await self.get_chat_by_id(id, db=session) if chat is None: return None chat = chat.chat - history = chat.get("history", {}) + history = chat.get('history', {}) message_files = [] - if message_id in history.get("messages", {}): - message_files = history["messages"][message_id].get("files", []) + if message_id in history.get('messages', {}): + message_files = history['messages'][message_id].get('files', []) message_files = message_files + files - history["messages"][message_id]["files"] = message_files + history['messages'][message_id]['files'] = message_files - chat["history"] = history - self.update_chat_by_id(id, chat, db=db) + chat['history'] = history + await self.update_chat_by_id(id, chat, db=session) return message_files - def insert_shared_chat_by_chat_id( - self, chat_id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: - with get_db_context(db) as db: - # Get the existing chat to share - chat = db.get(Chat, chat_id) - # Check if chat exists + async def insert_shared_chat_by_chat_id(self, chat_id: str, db: AsyncSession | None = None) -> ChatModel | None: + """Create a shared snapshot for a chat. Returns the original chat with share_id set.""" + from open_webui.models.shared_chats import SharedChats + + async with get_async_db_context(db) as session: + chat = await session.get(Chat, chat_id) if not chat: return None - # Check if the chat is already shared + + # If already shared, just update the existing snapshot if chat.share_id: - return self.get_chat_by_id_and_user_id(chat.share_id, "shared", db=db) - # Create a new chat with the same data, but with a new ID - shared_chat = ChatModel( - **{ - "id": str(uuid.uuid4()), - "user_id": f"shared-{chat_id}", - "title": chat.title, - "chat": chat.chat, - "meta": chat.meta, - "pinned": chat.pinned, - "folder_id": chat.folder_id, - "created_at": chat.created_at, - "updated_at": int(time.time()), - } - ) - shared_result = Chat(**shared_chat.model_dump()) - db.add(shared_result) - db.commit() - db.refresh(shared_result) + return await self.update_shared_chat_by_chat_id(chat_id, db=session) - # Update the original chat with the share_id - result = ( - db.query(Chat) - .filter_by(id=chat_id) - .update({"share_id": shared_chat.id}) - ) - db.commit() - return shared_chat if (shared_result and result) else None + shared = await SharedChats.create(chat_id, chat.user_id, db=session) + if not shared: + return None + + # Set share_id on the original chat + chat.share_id = shared.id + await session.commit() + await session.refresh(chat) + return ChatModel.model_validate(chat) # return the updated original + # refresh helper + async def update_shared_chat_by_chat_id( + self, chat_id: str, db: AsyncSession | None = None, + ) -> ChatModel | None: + """Refresh the shared snapshot with current chat content.""" + from open_webui.models.shared_chats import SharedChats + + async with get_async_db_context(db) as session: + record = await session.get(Chat, chat_id) + if not record or not record.share_id: + return await self.insert_shared_chat_by_chat_id(chat_id, db=session) + await SharedChats.update(record.share_id, db=session) + return ChatModel.model_validate(record) + # unreachable — context manager above always returns + return + + async def delete_shared_chat_by_chat_id(self, chat_id: str, db: AsyncSession | None = None) -> bool: + """Delete shared snapshot for a chat.""" + from open_webui.models.shared_chats import SharedChats - def update_shared_chat_by_chat_id( - self, chat_id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: try: - with get_db_context(db) as db: - chat = db.get(Chat, chat_id) - shared_chat = ( - db.query(Chat).filter_by(user_id=f"shared-{chat_id}").first() - ) - - if shared_chat is None: - return self.insert_shared_chat_by_chat_id(chat_id, db=db) - - shared_chat.title = chat.title - shared_chat.chat = chat.chat - shared_chat.meta = chat.meta - shared_chat.pinned = chat.pinned - shared_chat.folder_id = chat.folder_id - shared_chat.updated_at = int(time.time()) - db.commit() - db.refresh(shared_chat) - - return ChatModel.model_validate(shared_chat) + return await SharedChats.delete_by_chat_id(chat_id, db=db) except Exception: - return None + return False - def delete_shared_chat_by_chat_id( - self, chat_id: str, db: Optional[Session] = None - ) -> bool: + async def unarchive_all_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete() - db.commit() - + async with get_async_db_context(db) as session: + await session.execute(update(Chat).filter_by(user_id=user_id).values(archived=False)) + await session.commit() return True except Exception: return False - def unarchive_all_chats_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: + async def update_chat_share_id_by_id( + self, id: str, share_id: str | None, db: AsyncSession | None = None + ) -> ChatModel | None: try: - with get_db_context(db) as db: - db.query(Chat).filter_by(user_id=user_id).update({"archived": False}) - db.commit() - return True - except Exception: - return False - - def update_chat_share_id_by_id( - self, id: str, share_id: Optional[str], db: Optional[Session] = None - ) -> Optional[ChatModel]: - try: - with get_db_context(db) as db: - chat = db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.share_id = share_id - db.commit() - db.refresh(chat) + await session.commit() + await session.refresh(chat) return ChatModel.model_validate(chat) except Exception: return None - def toggle_chat_pinned_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: + async def toggle_chat_pinned_by_id(self, id: str, db: AsyncSession | None = None) -> ChatModel | None: try: - with get_db_context(db) as db: - chat = db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.pinned = not chat.pinned chat.updated_at = int(time.time()) - db.commit() - db.refresh(chat) + await session.commit() + await session.refresh(chat) return ChatModel.model_validate(chat) except Exception: return None - def toggle_chat_archive_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: + async def toggle_chat_archive_by_id(self, id: str, db: AsyncSession | None = None) -> ChatModel | None: try: - with get_db_context(db) as db: - chat = db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.archived = not chat.archived chat.folder_id = None chat.updated_at = int(time.time()) - db.commit() - db.refresh(chat) + await session.commit() + await session.refresh(chat) return ChatModel.model_validate(chat) except Exception: return None - def archive_all_chats_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: + async def archive_all_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - db.query(Chat).filter_by(user_id=user_id).update({"archived": True}) - db.commit() + async with get_async_db_context(db) as session: + await session.execute(update(Chat).filter_by(user_id=user_id).values(archived=True)) + await session.commit() return True except Exception: return False - def get_archived_chat_list_by_user_id( + async def get_archived_chat_list_by_user_id( self, user_id: str, - filter: Optional[dict] = None, + filter: dict | None = None, skip: int = 0, limit: int = 50, - db: Optional[Session] = None, - ) -> list[ChatModel]: - - with get_db_context(db) as db: - query = db.query(Chat).filter_by(user_id=user_id, archived=True) + db: AsyncSession | None = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as session: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at).filter_by( + user_id=user_id, archived=True + ) if filter: - query_key = filter.get("query") + query_key = filter.get('query') if query_key: - query = query.filter(Chat.title.ilike(f"%{query_key}%")) + stmt = stmt.filter(Chat.title.ilike(f'%{query_key}%')) - order_by = filter.get("order_by") - direction = filter.get("direction") + order_by = filter.get('order_by') + direction = filter.get('direction') if order_by and direction: if not getattr(Chat, order_by, None): - raise ValueError("Invalid order_by field") + raise ValueError('Invalid order_by field') - if direction.lower() == "asc": - query = query.order_by(getattr(Chat, order_by).asc()) - elif direction.lower() == "desc": - query = query.order_by(getattr(Chat, order_by).desc()) + if direction.lower() == 'asc': + stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id) + elif direction.lower() == 'desc': + stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id) else: - raise ValueError("Invalid direction for ordering") + raise ValueError('Invalid direction for ordering') else: - query = query.order_by(Chat.updated_at.desc()) + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - all_chats = query.all() - return [ChatModel.model_validate(chat) for chat in all_chats] - - def get_chat_list_by_user_id( - self, - user_id: str, - include_archived: bool = False, - filter: Optional[dict] = None, - skip: int = 0, - limit: int = 50, - db: Optional[Session] = None, - ) -> list[ChatModel]: - with get_db_context(db) as db: - query = db.query(Chat).filter_by(user_id=user_id) - if not include_archived: - query = query.filter_by(archived=False) - - if filter: - query_key = filter.get("query") - if query_key: - query = query.filter(Chat.title.ilike(f"%{query_key}%")) - - order_by = filter.get("order_by") - direction = filter.get("direction") - - if order_by and direction and getattr(Chat, order_by): - if direction.lower() == "asc": - query = query.order_by(getattr(Chat, order_by).asc()) - elif direction.lower() == "desc": - query = query.order_by(getattr(Chat, order_by).desc()) - else: - raise ValueError("Invalid direction for ordering") - else: - query = query.order_by(Chat.updated_at.desc()) - - if skip: - query = query.offset(skip) - if limit: - query = query.limit(limit) - - all_chats = query.all() - return [ChatModel.model_validate(chat) for chat in all_chats] - - def get_chat_title_id_list_by_user_id( - self, - user_id: str, - include_archived: bool = False, - include_folders: bool = False, - include_pinned: bool = False, - skip: Optional[int] = None, - limit: Optional[int] = None, - db: Optional[Session] = None, - ) -> list[ChatTitleIdResponse]: - with get_db_context(db) as db: - query = db.query(Chat).filter_by(user_id=user_id) - - if not include_folders: - query = query.filter_by(folder_id=None) - - if not include_pinned: - query = query.filter(or_(Chat.pinned == False, Chat.pinned == None)) - - if not include_archived: - query = query.filter_by(archived=False) - - query = query.order_by(Chat.updated_at.desc()).with_entities( - Chat.id, Chat.title, Chat.updated_at, Chat.created_at - ) - - if skip: - query = query.offset(skip) - if limit: - query = query.limit(limit) - - all_chats = query.all() - - # result has to be destructured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass. + result = await session.execute(stmt) + all_chats = result.all() return [ ChatTitleIdResponse.model_validate( { - "id": chat[0], - "title": chat[1], - "updated_at": chat[2], - "created_at": chat[3], + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], } ) for chat in all_chats ] - def get_chat_list_by_chat_ids( + async def get_shared_chat_list_by_user_id( + self, + user_id: str, + filter: dict | None = None, + skip: int = 0, + limit: int = 50, + db: AsyncSession | None = None, + ) -> list[SharedChatResponse]: + """Delegate to SharedChats for listing shared chats by user.""" + from open_webui.models.shared_chats import SharedChats + + return await SharedChats.get_by_user_id(user_id, filter=filter, skip=skip, limit=limit, db=db) + + async def get_chat_list_by_user_id( + self, + user_id: str, + include_archived: bool = False, + filter: dict | None = None, + skip: int = 0, + limit: int = 50, + db: AsyncSession | None = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as session: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( + user_id=user_id + ) + if not include_archived: + stmt = stmt.filter_by(archived=False) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter(Chat.title.ilike(f'%{query_key}%')) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and direction and getattr(Chat, order_by): + if direction.lower() == 'asc': + stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id) + elif direction.lower() == 'desc': + stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id) + else: + raise ValueError('Invalid direction for ordering') + else: + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await session.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def get_chat_title_id_list_by_user_id( + self, + user_id: str, + include_archived: bool = False, + include_folders: bool = False, + include_pinned: bool = False, + skip: int | None = None, + limit: int | None = None, + db: AsyncSession | None = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as session: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( + user_id=user_id + ) + + if not include_folders: + stmt = stmt.filter_by(folder_id=None) + + if not include_pinned: + stmt = stmt.filter(or_(Chat.pinned == False, Chat.pinned == None)) + + if not include_archived: + stmt = stmt.filter_by(archived=False) + + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await session.execute(stmt) + all_chats = result.all() + + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] + + async def get_chat_list_by_chat_ids( self, chat_ids: list[str], skip: int = 0, limit: int = 50, - db: Optional[Session] = None, + db: AsyncSession | None = None, ) -> list[ChatModel]: - with get_db_context(db) as db: - all_chats = ( - db.query(Chat) - .filter(Chat.id.in_(chat_ids)) - .filter_by(archived=False) - .order_by(Chat.updated_at.desc()) - .all() + async with get_async_db_context(db) as session: + result = await session.execute( + select(Chat).filter(Chat.id.in_(chat_ids)).filter_by(archived=False).order_by(Chat.updated_at.desc()) ) + all_chats = result.scalars().all() return [ChatModel.model_validate(chat) for chat in all_chats] - - def get_chat_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: + # retrieve conversation + async def get_chat_by_id( + self, id: str, db: AsyncSession | None = None, + ) -> ChatModel | None: + """Fetch a chat by PK, auto-sanitizing null bytes on read.""" try: - with get_db_context(db) as db: - chat_item = db.get(Chat, id) + async with get_async_db_context(db) as session: + chat_item = await session.get(Chat, id) if chat_item is None: return None if self._sanitize_chat_row(chat_item): - db.commit() - db.refresh(chat_item) + await session.commit() + await session.refresh(chat_item) return ChatModel.model_validate(chat_item) except Exception: return None - def get_chat_by_share_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: - try: - with get_db_context(db) as db: - # it is possible that the shared link was deleted. hence, - # we check if the chat is still shared by checking if a chat with the share_id exists - chat = db.query(Chat).filter_by(share_id=id).first() + async def get_chat_by_share_id(self, id: str, db: AsyncSession | None = None) -> ChatModel | None: + """Look up a shared chat snapshot by its share token.""" + from open_webui.models.shared_chats import SharedChats - if chat: - return self.get_chat_by_id(id, db=db) - else: - return None + try: + shared = await SharedChats.get_by_id(id, db=db) + if shared: + # Return a ChatModel-compatible view of the snapshot + return ChatModel( + id=shared.id, + user_id=shared.user_id, + title=shared.title, + chat=shared.chat, + created_at=shared.created_at, + updated_at=shared.updated_at, + share_id=shared.id, + ) + return None except Exception: return None - def get_chat_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: + async def get_chat_by_id_and_user_id( + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> ChatModel | None: try: - with get_db_context(db) as db: - chat = db.query(Chat).filter_by(id=id, user_id=user_id).first() - return ChatModel.model_validate(chat) + async with get_async_db_context(db) as session: + result = await session.execute(select(Chat).filter_by(id=id, user_id=user_id)) + chat = result.scalars().first() + return ChatModel.model_validate(chat) if chat else None except Exception: return None - def get_chats( - self, skip: int = 0, limit: int = 50, db: Optional[Session] = None - ) -> list[ChatModel]: - with get_db_context(db) as db: - all_chats = ( - db.query(Chat) - # .limit(limit).offset(skip) - .order_by(Chat.updated_at.desc()) - ) + async def is_chat_owner(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: + """ + Lightweight ownership check — uses EXISTS subquery instead of loading + the full Chat row (which includes the potentially large JSON blob). + """ + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(exists().where(and_(Chat.id == id, Chat.user_id == user_id)))) + return result.scalar() + except Exception: + return False + + async def get_chat_folder_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> str | None: + """ + Fetch only the folder_id column for a chat, without loading the full + JSON blob. Returns None if chat doesn't exist or doesn't belong to user. + """ + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(Chat.folder_id).filter_by(id=id, user_id=user_id)) + row = result.first() + return row[0] if row else None + except Exception: + return None + + async def get_chats(self, skip: int = 0, limit: int = 50, db: AsyncSession | None = None) -> list[ChatModel]: + async with get_async_db_context(db) as session: + result = await session.execute(select(Chat).order_by(Chat.updated_at.desc())) + all_chats = result.scalars().all() return [ChatModel.model_validate(chat) for chat in all_chats] - - def get_chats_by_user_id( + # list user conversations + async def get_chats_by_user_id( self, user_id: str, - filter: Optional[dict] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - db: Optional[Session] = None, + filter: dict | None = None, + skip: int | None = None, + limit: int | None = None, + db: AsyncSession | None = None, ) -> ChatListResponse: - with get_db_context(db) as db: - query = db.query(Chat).filter_by(user_id=user_id) + async with get_async_db_context(db) as session: + stmt = select(Chat).filter_by(user_id=user_id) if filter: - if filter.get("updated_at"): - query = query.filter(Chat.updated_at > filter.get("updated_at")) + if filter.get('updated_at'): + stmt = stmt.filter(Chat.updated_at > filter.get('updated_at')) - order_by = filter.get("order_by") - direction = filter.get("direction") + order_by = filter.get('order_by') + direction = filter.get('direction') if order_by and direction: if hasattr(Chat, order_by): - if direction.lower() == "asc": - query = query.order_by(getattr(Chat, order_by).asc()) - elif direction.lower() == "desc": - query = query.order_by(getattr(Chat, order_by).desc()) + if direction.lower() == 'asc': + stmt = stmt.order_by(getattr(Chat, order_by).asc(), Chat.id) + elif direction.lower() == 'desc': + stmt = stmt.order_by(getattr(Chat, order_by).desc(), Chat.id) else: - query = query.order_by(Chat.updated_at.desc()) + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) else: - query = query.order_by(Chat.updated_at.desc()) + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) - total = query.count() + count_result = await session.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() if skip is not None: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit is not None: - query = query.limit(limit) + stmt = stmt.limit(limit) - all_chats = query.all() + result = await session.execute(stmt) + all_chats = result.scalars().all() return ChatListResponse( **{ - "items": [ChatModel.model_validate(chat) for chat in all_chats], - "total": total, + 'items': [ChatModel.model_validate(chat) for chat in all_chats], + 'total': total, } ) - - def get_pinned_chats_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[ChatModel]: - with get_db_context(db) as db: - all_chats = ( - db.query(Chat) + # list pinned chats + async def get_pinned_chats_by_user_id( + self, user_id: str, db: AsyncSession | None = None + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as session: + result = await session.execute( + select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at) .filter_by(user_id=user_id, pinned=True, archived=False) .order_by(Chat.updated_at.desc()) ) - return [ChatModel.model_validate(chat) for chat in all_chats] + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] - def get_archived_chats_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[ChatModel]: - with get_db_context(db) as db: - all_chats = ( - db.query(Chat) - .filter_by(user_id=user_id, archived=True) - .order_by(Chat.updated_at.desc()) + async def get_archived_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[ChatModel]: + async with get_async_db_context(db) as session: + result = await session.execute( + select(Chat).filter_by(user_id=user_id, archived=True).order_by(Chat.updated_at.desc()) ) - return [ChatModel.model_validate(chat) for chat in all_chats] - - def get_chats_by_user_id_and_search_text( + return [ChatModel.model_validate(chat) for chat in result.scalars().all()] + # search user conversations + async def get_chats_by_user_id_and_search_text( self, user_id: str, search_text: str, include_archived: bool = False, skip: int = 0, limit: int = 60, - db: Optional[Session] = None, + db: AsyncSession | None = None, ) -> list[ChatModel]: """ Filters chats based on a search query using Python, allowing pagination using skip and limit. @@ -915,141 +1111,129 @@ class ChatTable: search_text = sanitize_text_for_db(search_text).lower().strip() if not search_text: - return self.get_chat_list_by_user_id( + return await self.get_chat_list_by_user_id( user_id, include_archived, filter={}, skip=skip, limit=limit, db=db ) - search_text_words = search_text.split(" ") + search_text_words = search_text.split(' ') - # search_text might contain 'tag:tag_name' format so we need to extract the tag_name, split the search_text and remove the tags + # search_text might contain 'tag:tag_name' format so we need to extract the tag_name tag_ids = [ - word.replace("tag:", "").replace(" ", "_").lower() - for word in search_text_words - if word.startswith("tag:") + word.replace('tag:', '').replace(' ', '_').lower() for word in search_text_words if word.startswith('tag:') ] - # Extract folder names - handle spaces and case insensitivity - folders = Folders.search_folders_by_names( + # Extract folder names + folders = await Folders.search_folders_by_names( user_id, - [ - word.replace("folder:", "") - for word in search_text_words - if word.startswith("folder:") - ], + [word.replace('folder:', '') for word in search_text_words if word.startswith('folder:')], ) folder_ids = [folder.id for folder in folders] is_pinned = None - if "pinned:true" in search_text_words: + if 'pinned:true' in search_text_words: is_pinned = True - elif "pinned:false" in search_text_words: + elif 'pinned:false' in search_text_words: is_pinned = False is_archived = None - if "archived:true" in search_text_words: + if 'archived:true' in search_text_words: is_archived = True - elif "archived:false" in search_text_words: + elif 'archived:false' in search_text_words: is_archived = False is_shared = None - if "shared:true" in search_text_words: + if 'shared:true' in search_text_words: is_shared = True - elif "shared:false" in search_text_words: + elif 'shared:false' in search_text_words: is_shared = False search_text_words = [ word for word in search_text_words if ( - not word.startswith("tag:") - and not word.startswith("folder:") - and not word.startswith("pinned:") - and not word.startswith("archived:") - and not word.startswith("shared:") + not word.startswith('tag:') + and not word.startswith('folder:') + and not word.startswith('pinned:') + and not word.startswith('archived:') + and not word.startswith('shared:') ) ] - search_text = " ".join(search_text_words) + search_text = ' '.join(search_text_words) - with get_db_context(db) as db: - query = db.query(Chat).filter(Chat.user_id == user_id) + async with get_async_db_context(db) as session: + stmt = select(Chat).filter(Chat.user_id == user_id) if is_archived is not None: - query = query.filter(Chat.archived == is_archived) + stmt = stmt.filter(Chat.archived == is_archived) elif not include_archived: - query = query.filter(Chat.archived == False) + stmt = stmt.filter(Chat.archived == False) if is_pinned is not None: - query = query.filter(Chat.pinned == is_pinned) + stmt = stmt.filter(Chat.pinned == is_pinned) if is_shared is not None: if is_shared: - query = query.filter(Chat.share_id.isnot(None)) + stmt = stmt.filter(Chat.share_id.isnot(None)) else: - query = query.filter(Chat.share_id.is_(None)) + stmt = stmt.filter(Chat.share_id.is_(None)) if folder_ids: - query = query.filter(Chat.folder_id.in_(folder_ids)) + stmt = stmt.filter(Chat.folder_id.in_(folder_ids)) - query = query.order_by(Chat.updated_at.desc()) + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) # Check if the database dialect is either 'sqlite' or 'postgresql' - dialect_name = db.bind.dialect.name - if dialect_name == "sqlite": + bind = await session.connection() + dialect_name = bind.dialect.name + if dialect_name == 'sqlite': # SQLite case: using JSON1 extension for JSON searching sqlite_content_sql = ( - "EXISTS (" - " SELECT 1 " + 'EXISTS (' + ' SELECT 1 ' " FROM json_each(Chat.chat, '$.messages') AS message " " WHERE LOWER(message.value->>'content') LIKE '%' || :content_key || '%'" - ")" + ')' ) sqlite_content_clause = text(sqlite_content_sql) - query = query.filter( - or_( - Chat.title.ilike(bindparam("title_key")), sqlite_content_clause - ).params(title_key=f"%{search_text}%", content_key=search_text) + stmt = stmt.filter( + or_(Chat.title.ilike(bindparam('title_key')), sqlite_content_clause).params( + title_key=f'%{search_text}%', content_key=search_text + ) ) - # Check if there are any tags to filter, it should have all the tags - if "none" in tag_ids: - query = query.filter( - text( - """ + # Check if there are any tags to filter + if 'none' in tag_ids: + stmt = stmt.filter( + text(""" NOT EXISTS ( SELECT 1 FROM json_each(Chat.meta, '$.tags') AS tag ) - """ - ) + """) ) elif tag_ids: - query = query.filter( + stmt = stmt.filter( and_( *[ - text( - f""" + text(f""" EXISTS ( SELECT 1 FROM json_each(Chat.meta, '$.tags') AS tag WHERE tag.value = :tag_id_{tag_idx} ) - """ - ).params(**{f"tag_id_{tag_idx}": tag_id}) + """).params(**{f'tag_id_{tag_idx}': tag_id}) for tag_idx, tag_id in enumerate(tag_ids) ] ) ) - elif dialect_name == "postgresql": - # PostgreSQL doesn't allow null bytes in text. We filter those out by checking - # the JSON representation for \u0000 before attempting text extraction - + elif dialect_name == 'postgresql': # Safety filter: JSON field must not contain \u0000 - query = query.filter(text("Chat.chat::text NOT LIKE '%\\\\u0000%'")) + stmt = stmt.filter(text("Chat.chat::text NOT LIKE '%\\\\u0000%'")) # Safety filter: title must not contain actual null bytes - query = query.filter(text("Chat.title::text NOT LIKE '%\\x00%'")) + stmt = stmt.filter(text("Chat.title::text NOT LIKE '%\\x00%'")) postgres_content_sql = """ EXISTS ( @@ -1062,373 +1246,414 @@ class ChatTable: postgres_content_clause = text(postgres_content_sql) - query = query.filter( + stmt = stmt.filter( or_( - Chat.title.ilike(bindparam("title_key")), + Chat.title.ilike(bindparam('title_key')), postgres_content_clause, ) - ).params(title_key=f"%{search_text}%", content_key=search_text.lower()) + ).params(title_key=f'%{search_text}%', content_key=search_text.lower()) - # Check if there are any tags to filter, it should have all the tags - if "none" in tag_ids: - query = query.filter( - text( - """ + if 'none' in tag_ids: + stmt = stmt.filter( + text(""" NOT EXISTS ( SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') AS tag ) - """ - ) + """) ) elif tag_ids: - query = query.filter( + stmt = stmt.filter( and_( *[ - text( - f""" + text(f""" EXISTS ( SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') AS tag WHERE tag = :tag_id_{tag_idx} ) - """ - ).params(**{f"tag_id_{tag_idx}": tag_id}) + """).params(**{f'tag_id_{tag_idx}': tag_id}) for tag_idx, tag_id in enumerate(tag_ids) ] ) ) else: - raise NotImplementedError( - f"Unsupported dialect: {db.bind.dialect.name}" - ) + raise NotImplementedError(f'Unsupported dialect: {dialect_name}') # Perform pagination at the SQL level - all_chats = query.offset(skip).limit(limit).all() + stmt = stmt.offset(skip).limit(limit) + result = await session.execute(stmt) + all_chats = result.scalars().all() - log.info(f"The number of chats: {len(all_chats)}") + log.info(f'The number of chats: {len(all_chats)}') # Validate and return chats return [ChatModel.model_validate(chat) for chat in all_chats] - def get_chats_by_folder_id_and_user_id( + async def get_chats_by_folder_id_and_user_id( self, folder_id: str, user_id: str, skip: int = 0, limit: int = 60, - db: Optional[Session] = None, - ) -> list[ChatModel]: - with get_db_context(db) as db: - query = db.query(Chat).filter_by(folder_id=folder_id, user_id=user_id) - query = query.filter(or_(Chat.pinned == False, Chat.pinned == None)) - query = query.filter_by(archived=False) - - query = query.order_by(Chat.updated_at.desc()) + db: AsyncSession | None = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as session: + stmt = ( + select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at) + .filter_by(folder_id=folder_id, user_id=user_id) + .filter(or_(Chat.pinned == False, Chat.pinned == None)) + .filter_by(archived=False) + .order_by(Chat.updated_at.desc(), Chat.id) + ) if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - all_chats = query.all() - return [ChatModel.model_validate(chat) for chat in all_chats] + result = await session.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } + ) + for chat in all_chats + ] - def get_chats_by_folder_ids_and_user_id( - self, folder_ids: list[str], user_id: str, db: Optional[Session] = None + async def get_chats_by_folder_ids_and_user_id( + self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None ) -> list[ChatModel]: - with get_db_context(db) as db: - query = db.query(Chat).filter( - Chat.folder_id.in_(folder_ids), Chat.user_id == user_id + async with get_async_db_context(db) as session: + stmt = ( + select(Chat) + .filter(Chat.folder_id.in_(folder_ids), Chat.user_id == user_id) + .filter(or_(Chat.pinned == False, Chat.pinned == None)) + .filter_by(archived=False) + .order_by(Chat.updated_at.desc()) ) - query = query.filter(or_(Chat.pinned == False, Chat.pinned == None)) - query = query.filter_by(archived=False) - query = query.order_by(Chat.updated_at.desc()) - - all_chats = query.all() + result = await session.execute(stmt) + all_chats = result.scalars().all() return [ChatModel.model_validate(chat) for chat in all_chats] - def update_chat_folder_id_by_id_and_user_id( - self, id: str, user_id: str, folder_id: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: + async def update_chat_folder_id_by_id_and_user_id( + self, id: str, user_id: str, folder_id: str, db: AsyncSession | None = None + ) -> ChatModel | None: try: - with get_db_context(db) as db: - chat = db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.folder_id = folder_id chat.updated_at = int(time.time()) chat.pinned = False - db.commit() - db.refresh(chat) + await session.commit() + await session.refresh(chat) return ChatModel.model_validate(chat) except Exception: return None - def get_chat_tags_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def get_chat_tags_by_id_and_user_id( + self, id: str, user_id: str, db: AsyncSession | None = None ) -> list[TagModel]: - with get_db_context(db) as db: - chat = db.get(Chat, id) - tags = chat.meta.get("tags", []) - return [Tags.get_tag_by_name_and_user_id(tag, user_id) for tag in tags] + async with get_async_db_context(db) as session: + stmt = select(Chat.meta).where(Chat.id == id) + result = await session.execute(stmt) + meta = result.scalar_one_or_none() + tag_ids = (meta or {}).get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tag_ids, user_id, db=session) - def get_chat_list_by_user_id_and_tag_name( + async def get_chat_list_by_user_id_and_tag_name( self, user_id: str, tag_name: str, skip: int = 0, limit: int = 50, - db: Optional[Session] = None, - ) -> list[ChatModel]: - with get_db_context(db) as db: - query = db.query(Chat).filter_by(user_id=user_id) - tag_id = tag_name.replace(" ", "_").lower() + db: AsyncSession | None = None, + ) -> list[ChatTitleIdResponse]: + async with get_async_db_context(db) as session: + stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( + user_id=user_id + ) + tag_id = tag_name.replace(' ', '_').lower() - log.info(f"DB dialect name: {db.bind.dialect.name}") - if db.bind.dialect.name == "sqlite": - # SQLite JSON1 querying for tags within the meta JSON field - query = query.filter( - text( - f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)" - ) + bind = await session.connection() + dialect_name = bind.dialect.name + log.info(f'DB dialect name: {dialect_name}') + if dialect_name == 'sqlite': + stmt = stmt.filter( + text(f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)") ).params(tag_id=tag_id) - elif db.bind.dialect.name == "postgresql": - # PostgreSQL JSON query for tags within the meta JSON field (for `json` type) - query = query.filter( - text( - "EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)" - ) + elif dialect_name == 'postgresql': + stmt = stmt.filter( + text("EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)") ).params(tag_id=tag_id) else: - raise NotImplementedError( - f"Unsupported dialect: {db.bind.dialect.name}" + raise NotImplementedError(f'Unsupported dialect: {dialect_name}') + + stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await session.execute(stmt) + all_chats = result.all() + return [ + ChatTitleIdResponse.model_validate( + { + 'id': chat[0], + 'title': chat[1], + 'updated_at': chat[2], + 'created_at': chat[3], + 'last_read_at': chat[4], + } ) + for chat in all_chats + ] - all_chats = query.all() - log.debug(f"all_chats: {all_chats}") - return [ChatModel.model_validate(chat) for chat in all_chats] - - def add_chat_tag_by_id_and_user_id_and_tag_name( - self, id: str, user_id: str, tag_name: str, db: Optional[Session] = None - ) -> Optional[ChatModel]: - tag = Tags.get_tag_by_name_and_user_id(tag_name, user_id) - if tag is None: - tag = Tags.insert_new_tag(tag_name, user_id) + async def add_chat_tag_by_id_and_user_id_and_tag_name( + self, id: str, user_id: str, tag_name: str, db: AsyncSession | None = None + ) -> ChatModel | None: + tag_id = tag_name.replace(' ', '_').lower() + await Tags.ensure_tags_exist([tag_name], user_id, db=db) try: - with get_db_context(db) as db: - chat = db.get(Chat, id) - - tag_id = tag.id - if tag_id not in chat.meta.get("tags", []): + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) + if tag_id not in chat.meta.get('tags', []): chat.meta = { **chat.meta, - "tags": list(set(chat.meta.get("tags", []) + [tag_id])), + 'tags': list(set(chat.meta.get('tags', []) + [tag_id])), } - - db.commit() - db.refresh(chat) + await session.commit() + await session.refresh(chat) return ChatModel.model_validate(chat) except Exception: return None - def count_chats_by_tag_name_and_user_id( - self, tag_name: str, user_id: str, db: Optional[Session] = None + async def count_chats_by_tag_name_and_user_id( + self, tag_name: str, user_id: str, db: AsyncSession | None = None ) -> int: - with get_db_context(db) as db: # Assuming `get_db()` returns a session object - query = db.query(Chat).filter_by(user_id=user_id, archived=False) + async with get_async_db_context(db) as session: + stmt = select(func.count(Chat.id)).filter_by(user_id=user_id, archived=False) + tag_id = tag_name.replace(' ', '_').lower() - # Normalize the tag_name for consistency - tag_id = tag_name.replace(" ", "_").lower() - - if db.bind.dialect.name == "sqlite": - # SQLite JSON1 support for querying the tags inside the `meta` JSON field - query = query.filter( - text( - f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)" - ) + bind = await session.connection() + dialect_name = bind.dialect.name + if dialect_name == 'sqlite': + stmt = stmt.filter( + text("EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)") ).params(tag_id=tag_id) - - elif db.bind.dialect.name == "postgresql": - # PostgreSQL JSONB support for querying the tags inside the `meta` JSON field - query = query.filter( - text( - "EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)" - ) + elif dialect_name == 'postgresql': + stmt = stmt.filter( + text("EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)") ).params(tag_id=tag_id) - else: - raise NotImplementedError( - f"Unsupported dialect: {db.bind.dialect.name}" - ) + raise NotImplementedError(f'Unsupported dialect: {dialect_name}') - # Get the count of matching records - count = query.count() + result = await session.execute(stmt) + return result.scalar() - # Debugging output for inspection - log.info(f"Count of chats for tag '{tag_name}': {count}") + async def delete_orphan_tags_for_user( + self, + tag_ids: list[str], + user_id: str, + threshold: int = 0, + db: AsyncSession | None = None, + ) -> None: + """Delete tag rows from *tag_ids* that appear in at most *threshold* + non-archived chats for *user_id*. One query to find orphans, one to + delete them. - return count + Use threshold=0 after a tag is already removed from a chat's meta. + Use threshold=1 when the chat itself is about to be deleted (the + referencing chat still exists at query time). + """ + if not tag_ids: + return + async with get_async_db_context(db) as session: + orphans = [] + for tag_id in tag_ids: + count = await self.count_chats_by_tag_name_and_user_id(tag_id, user_id, db=session) + if count <= threshold: + orphans.append(tag_id) + await Tags.delete_tags_by_ids_and_user_id(orphans, user_id, db=session) - def count_chats_by_folder_id_and_user_id( - self, folder_id: str, user_id: str, db: Optional[Session] = None + async def count_chats_by_folder_id_and_user_id( + self, folder_id: str, user_id: str, db: AsyncSession | None = None ) -> int: - with get_db_context(db) as db: - query = db.query(Chat).filter_by(user_id=user_id) - - query = query.filter_by(folder_id=folder_id) - count = query.count() + async with get_async_db_context(db) as session: + result = await session.execute(select(func.count(Chat.id)).filter_by(user_id=user_id, folder_id=folder_id)) + count = result.scalar() log.info(f"Count of chats for folder '{folder_id}': {count}") return count - def delete_tag_by_id_and_user_id_and_tag_name( - self, id: str, user_id: str, tag_name: str, db: Optional[Session] = None + async def delete_tag_by_id_and_user_id_and_tag_name( + self, id: str, user_id: str, tag_name: str, db: AsyncSession | None = None ) -> bool: try: - with get_db_context(db) as db: - chat = db.get(Chat, id) - tags = chat.meta.get("tags", []) - tag_id = tag_name.replace(" ", "_").lower() + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) + tags = chat.meta.get('tags', []) + tag_id = tag_name.replace(' ', '_').lower() tags = [tag for tag in tags if tag != tag_id] chat.meta = { **chat.meta, - "tags": list(set(tags)), + 'tags': list(set(tags)), } - db.commit() + await session.commit() return True except Exception: return False - def delete_all_tags_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> bool: + async def delete_chat_by_id(self, id: str, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - chat = db.get(Chat, id) - chat.meta = { - **chat.meta, - "tags": [], - } - db.commit() + async with get_async_db_context(db) as session: + await session.execute(update(AutomationRun).filter_by(chat_id=id).values(chat_id=None)) + await session.execute(delete(ChatMessage).filter_by(chat_id=id)) + await session.execute(delete(Chat).filter_by(id=id)) + await session.commit() + + return True and await self.delete_shared_chat_by_chat_id(id, db=session) + except Exception: + return False + + async def delete_chat_by_id_and_user_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: + try: + async with get_async_db_context(db) as session: + await session.execute(update(AutomationRun).filter_by(chat_id=id).values(chat_id=None)) + await session.execute(delete(ChatMessage).filter_by(chat_id=id)) + await session.execute(delete(Chat).filter_by(id=id, user_id=user_id)) + await session.commit() + + return True and await self.delete_shared_chat_by_chat_id(id, db=session) + except Exception: + return False + + async def delete_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: + try: + async with get_async_db_context(db) as session: + await self.delete_shared_chats_by_user_id(user_id, db=session) + + chat_id_subquery = select(Chat.id).filter_by(user_id=user_id).scalar_subquery() + await session.execute( + update(AutomationRun) + .filter(AutomationRun.chat_id.in_(select(Chat.id).filter_by(user_id=user_id))) + .values(chat_id=None) + ) + await session.execute( + delete(ChatMessage).filter(ChatMessage.chat_id.in_(select(Chat.id).filter_by(user_id=user_id))) + ) + await session.execute(delete(Chat).filter_by(user_id=user_id)) + await session.commit() return True except Exception: return False - def delete_chat_by_id(self, id: str, db: Optional[Session] = None) -> bool: - try: - with get_db_context(db) as db: - db.query(Chat).filter_by(id=id).delete() - db.commit() - - return True and self.delete_shared_chat_by_chat_id(id, db=db) - except Exception: - return False - - def delete_chat_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def delete_chats_by_user_id_and_folder_id( + self, user_id: str, folder_id: str, db: AsyncSession | None = None ) -> bool: try: - with get_db_context(db) as db: - db.query(Chat).filter_by(id=id, user_id=user_id).delete() - db.commit() - - return True and self.delete_shared_chat_by_chat_id(id, db=db) - except Exception: - return False - - def delete_chats_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: - try: - with get_db_context(db) as db: - self.delete_shared_chats_by_user_id(user_id, db=db) - - db.query(Chat).filter_by(user_id=user_id).delete() - db.commit() + async with get_async_db_context(db) as session: + chat_ids_stmt = select(Chat.id).filter_by(user_id=user_id, folder_id=folder_id) + await session.execute( + update(AutomationRun).filter(AutomationRun.chat_id.in_(chat_ids_stmt)).values(chat_id=None) + ) + await session.execute(delete(ChatMessage).filter(ChatMessage.chat_id.in_(chat_ids_stmt))) + await session.execute(delete(Chat).filter_by(user_id=user_id, folder_id=folder_id)) + await session.commit() return True except Exception: return False - def delete_chats_by_user_id_and_folder_id( - self, user_id: str, folder_id: str, db: Optional[Session] = None - ) -> bool: - try: - with get_db_context(db) as db: - db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete() - db.commit() - - return True - except Exception: - return False - - def move_chats_by_user_id_and_folder_id( + async def move_chats_by_user_id_and_folder_id( self, user_id: str, folder_id: str, - new_folder_id: Optional[str], - db: Optional[Session] = None, + new_folder_id: str | None, + db: AsyncSession | None = None, ) -> bool: try: - with get_db_context(db) as db: - db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).update( - {"folder_id": new_folder_id} + async with get_async_db_context(db) as session: + await session.execute( + update(Chat).filter_by(user_id=user_id, folder_id=folder_id).values(folder_id=new_folder_id) ) - db.commit() + await session.commit() return True except Exception: return False - def delete_shared_chats_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: + async def delete_shared_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: + """Delete all shared chat snapshots created by a user.""" + from open_webui.models.shared_chats import SharedChat as SharedChatTable + from open_webui.models.shared_chats import SharedChats + try: - with get_db_context(db) as db: - chats_by_user = db.query(Chat).filter_by(user_id=user_id).all() - shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user] + async with get_async_db_context(db) as session: + # Delete shared_chat rows for this user's chats + await session.execute(delete(SharedChatTable).filter_by(user_id=user_id)) - db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete() - db.commit() + # Clear share_id on all of this user's chats + await session.execute(update(Chat).filter_by(user_id=user_id).values(share_id=None)) + await session.commit() return True except Exception: return False - def insert_chat_files( + async def insert_chat_files( self, chat_id: str, message_id: str, file_ids: list[str], user_id: str, - db: Optional[Session] = None, - ) -> Optional[list[ChatFileModel]]: + db: AsyncSession | None = None, + ) -> list[ChatFileModel | None]: if not file_ids: return None - chat_message_file_ids = [ - item.id - for item in self.get_chat_files_by_chat_id_and_message_id( - chat_id, message_id, db=db - ) - ] + chat_message_file_ids = { + item.id for item in await self.get_chat_files_by_chat_id_and_message_id(chat_id, message_id, db=db) + } # Remove duplicates and existing file_ids - file_ids = list( - set( - [ - file_id - for file_id in file_ids - if file_id and file_id not in chat_message_file_ids - ] - ) - ) + file_ids = list({file_id for file_id in file_ids if file_id and file_id not in chat_message_file_ids}) + if not file_ids: + return None + + # Only link files the caller can read; blocks forging a chat_file row to another user's file. + from open_webui.models.files import Files + from open_webui.models.users import Users + from open_webui.utils.access_control.files import has_access_to_file + + user = await Users.get_user_by_id(user_id, db=db) + accessible_file_ids = [] + for file_id in file_ids: + file = await Files.get_file_by_id(file_id, db=db) + if not file: + continue + if ( + file.user_id == user_id + or (user and user.role == 'admin') + or (user and await has_access_to_file(file_id, 'read', user, db=db)) + ): + accessible_file_ids.append(file_id) + file_ids = accessible_file_ids if not file_ids: return None try: - with get_db_context(db) as db: + async with get_async_db_context(db) as session: now = int(time.time()) chat_files = [ @@ -1444,55 +1669,66 @@ class ChatTable: for file_id in file_ids ] - results = [ - ChatFile(**chat_file.model_dump()) for chat_file in chat_files - ] + results = [ChatFile(**chat_file.model_dump()) for chat_file in chat_files] - db.add_all(results) - db.commit() + session.add_all(results) + await session.commit() return chat_files except Exception: return None - def get_chat_files_by_chat_id_and_message_id( - self, chat_id: str, message_id: str, db: Optional[Session] = None + async def get_chat_files_by_chat_id_and_message_id( + self, chat_id: str, message_id: str, db: AsyncSession | None = None ) -> list[ChatFileModel]: - with get_db_context(db) as db: - all_chat_files = ( - db.query(ChatFile) - .filter_by(chat_id=chat_id, message_id=message_id) - .order_by(ChatFile.created_at.asc()) - .all() + async with get_async_db_context(db) as session: + result = await session.execute( + select(ChatFile).filter_by(chat_id=chat_id, message_id=message_id).order_by(ChatFile.created_at.asc()) ) - return [ - ChatFileModel.model_validate(chat_file) for chat_file in all_chat_files - ] + all_chat_files = result.scalars().all() + return [ChatFileModel.model_validate(chat_file) for chat_file in all_chat_files] - def delete_chat_file( - self, chat_id: str, file_id: str, db: Optional[Session] = None - ) -> bool: + async def delete_chat_file(self, chat_id: str, file_id: str, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - db.query(ChatFile).filter_by(chat_id=chat_id, file_id=file_id).delete() - db.commit() + async with get_async_db_context(db) as session: + await session.execute(delete(ChatFile).filter_by(chat_id=chat_id, file_id=file_id)) + await session.commit() return True except Exception: return False - def get_shared_chats_by_file_id( - self, file_id: str, db: Optional[Session] = None - ) -> list[ChatModel]: - with get_db_context(db) as db: - # Join Chat and ChatFile tables to get shared chats associated with the file_id - all_chats = ( - db.query(Chat) + async def get_shared_chat_ids_by_file_id(self, file_id: str, db: AsyncSession | None = None) -> list[str]: + """Return IDs of chats that contain this file and have an active share link.""" + async with get_async_db_context(db) as session: + result = await session.execute( + select(Chat.id) .join(ChatFile, Chat.id == ChatFile.chat_id) .filter(ChatFile.file_id == file_id, Chat.share_id.isnot(None)) - .all() ) + return [row[0] for row in result.all()] - return [ChatModel.model_validate(chat) for chat in all_chats] + async def update_chat_tasks_by_id(self, id: str, tasks: list[dict]) -> ChatModel | None: + """Update the tasks list on a chat.""" + try: + async with get_async_db_context() as session: + chat = await session.get(Chat, id) + if chat is None: + return None + chat.tasks = tasks + await session.commit() + await session.refresh(chat) + return ChatModel.model_validate(chat) + except Exception: + return None + + async def get_chat_tasks_by_id(self, id: str) -> list[dict]: + """Read the tasks list from a chat (lightweight column query).""" + async with get_async_db_context() as session: + result = await session.execute(select(Chat.tasks).filter_by(id=id)) + row = result.first() + if row is None or row[0] is None: + return [] + return row[0] -Chats = ChatTable() +Chats = ChatTable() # singleton chats repository diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 048c10f85c..00bb6f1588 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -3,12 +3,11 @@ import time import uuid from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from open_webui.models.users import User - +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import User, UserModel from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, Text, JSON, Boolean +from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -19,7 +18,7 @@ log = logging.getLogger(__name__) class Feedback(Base): - __tablename__ = "feedback" + __tablename__ = 'feedback' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text) version = Column(BigInteger, default=0) @@ -81,7 +80,7 @@ class RatingData(BaseModel): sibling_model_ids: Optional[list[str]] = None reason: Optional[str] = None comment: Optional[str] = None - model_config = ConfigDict(extra="allow", protected_namespaces=()) + model_config = ConfigDict(extra='allow', protected_namespaces=()) class MetaData(BaseModel): @@ -89,12 +88,12 @@ class MetaData(BaseModel): chat_id: Optional[str] = None message_id: Optional[str] = None tags: Optional[list[str]] = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class SnapshotData(BaseModel): chat: Optional[dict] = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class FeedbackForm(BaseModel): @@ -102,14 +101,15 @@ class FeedbackForm(BaseModel): data: Optional[RatingData] = None meta: Optional[dict] = None snapshot: Optional[SnapshotData] = None - model_config = ConfigDict(extra="allow") + # ignore: drop client-supplied id/user_id/version/timestamps at parse time. + model_config = ConfigDict(extra='ignore') class UserResponse(BaseModel): id: str name: str email: str - role: str = "pending" + role: str = 'pending' last_active_at: int # timestamp in epoch updated_at: int # timestamp in epoch @@ -139,139 +139,149 @@ class ModelHistoryResponse(BaseModel): class FeedbackTable: - def insert_new_feedback( - self, user_id: str, form_data: FeedbackForm, db: Optional[Session] = None + async def insert_new_feedback( + self, user_id: str, form_data: FeedbackForm, db: Optional[AsyncSession] = None ) -> Optional[FeedbackModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: id = str(uuid.uuid4()) + # Spread form_data first so server-controlled fields win on duplicate keys. feedback = FeedbackModel( **{ - "id": id, - "user_id": user_id, - "version": 0, **form_data.model_dump(), - "created_at": int(time.time()), - "updated_at": int(time.time()), + 'id': id, + 'user_id': user_id, + 'version': 0, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) try: result = Feedback(**feedback.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return FeedbackModel.model_validate(result) else: return None except Exception as e: - log.exception(f"Error creating a new feedback: {e}") + log.exception(f'Error creating a new feedback: {e}') return None - def get_feedback_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[FeedbackModel]: + async def get_feedback_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[FeedbackModel]: try: - with get_db_context(db) as db: - feedback = db.query(Feedback).filter_by(id=id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id)) + feedback = result.scalars().first() if not feedback: return None return FeedbackModel.model_validate(feedback) except Exception: return None - def get_feedback_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def get_feedback_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[FeedbackModel]: try: - with get_db_context(db) as db: - feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id, user_id=user_id)) + feedback = result.scalars().first() if not feedback: return None return FeedbackModel.model_validate(feedback) except Exception: return None - def get_feedback_items( + async def get_feedbacks_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + """Get all feedbacks for a specific chat.""" + try: + async with get_async_db_context(db) as db: + # meta.chat_id stores the chat reference + result = await db.execute( + select(Feedback) + .filter(Feedback.meta['chat_id'].as_string() == chat_id) + .order_by(Feedback.created_at.desc()) + ) + feedbacks = result.scalars().all() + return [FeedbackModel.model_validate(fb) for fb in feedbacks] + except Exception: + return [] + + async def get_feedback_items( self, filter: dict = {}, skip: int = 0, limit: int = 30, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> FeedbackListResponse: - with get_db_context(db) as db: - query = db.query(Feedback, User).join(User, Feedback.user_id == User.id) + async with get_async_db_context(db) as db: + stmt = select(Feedback, User).join(User, Feedback.user_id == User.id) if filter: - order_by = filter.get("order_by") - direction = filter.get("direction") + # Apply model_id filter (exact match) + model_id = filter.get('model_id') + if model_id: + stmt = stmt.filter(Feedback.data['model_id'].as_string() == model_id) - if order_by == "username": - if direction == "asc": - query = query.order_by(User.name.asc()) + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'username': + if direction == 'asc': + stmt = stmt.order_by(User.name.asc()) else: - query = query.order_by(User.name.desc()) - elif order_by == "model_id": - # it's stored in feedback.data['model_id'] - if direction == "asc": - query = query.order_by( - Feedback.data["model_id"].as_string().asc() - ) + stmt = stmt.order_by(User.name.desc()) + elif order_by == 'model_id': + if direction == 'asc': + stmt = stmt.order_by(Feedback.data['model_id'].as_string().asc()) else: - query = query.order_by( - Feedback.data["model_id"].as_string().desc() - ) - elif order_by == "rating": - # it's stored in feedback.data['rating'] - if direction == "asc": - query = query.order_by( - Feedback.data["rating"].as_string().asc() - ) + stmt = stmt.order_by(Feedback.data['model_id'].as_string().desc()) + elif order_by == 'rating': + if direction == 'asc': + stmt = stmt.order_by(Feedback.data['rating'].as_string().asc()) else: - query = query.order_by( - Feedback.data["rating"].as_string().desc() - ) - elif order_by == "updated_at": - if direction == "asc": - query = query.order_by(Feedback.updated_at.asc()) + stmt = stmt.order_by(Feedback.data['rating'].as_string().desc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(Feedback.updated_at.asc()) else: - query = query.order_by(Feedback.updated_at.desc()) + stmt = stmt.order_by(Feedback.updated_at.desc()) else: - query = query.order_by(Feedback.created_at.desc()) + stmt = stmt.order_by(Feedback.created_at.desc()) # Count BEFORE pagination - total = query.count() + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - items = query.all() + result = await db.execute(stmt) + items = result.all() feedbacks = [] for feedback, user in items: feedback_model = FeedbackModel.model_validate(feedback) user_model = UserResponse.model_validate(user) - feedbacks.append( - FeedbackUserResponse(**feedback_model.model_dump(), user=user_model) - ) + feedbacks.append(FeedbackUserResponse(**feedback_model.model_dump(), user=user_model)) return FeedbackListResponse(items=feedbacks, total=total) - def get_all_feedbacks(self, db: Optional[Session] = None) -> list[FeedbackModel]: - with get_db_context(db) as db: - return [ - FeedbackModel.model_validate(feedback) - for feedback in db.query(Feedback) - .order_by(Feedback.updated_at.desc()) - .all() - ] + async def get_all_feedbacks(self, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).order_by(Feedback.updated_at.desc())) + return [FeedbackModel.model_validate(feedback) for feedback in result.scalars().all()] - def get_all_feedback_ids( - self, db: Optional[Session] = None - ) -> list[FeedbackIdResponse]: - with get_db_context(db) as db: + async def get_all_feedback_ids(self, db: Optional[AsyncSession] = None) -> list[FeedbackIdResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Feedback.id, Feedback.user_id, Feedback.created_at, Feedback.updated_at).order_by( + Feedback.updated_at.desc() + ) + ) return [ FeedbackIdResponse( id=row.id, @@ -279,67 +289,66 @@ class FeedbackTable: created_at=row.created_at, updated_at=row.updated_at, ) - for row in db.query( - Feedback.id, - Feedback.user_id, - Feedback.created_at, - Feedback.updated_at, - ) - .order_by(Feedback.updated_at.desc()) - .all() + for row in result.all() ] - def get_feedbacks_for_leaderboard( - self, db: Optional[Session] = None - ) -> list[LeaderboardFeedbackData]: + async def get_distinct_model_ids(self, db: Optional[AsyncSession] = None) -> list[str]: + """Get distinct model_ids from feedback data for filter dropdowns.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(Feedback.data['model_id'].as_string()) + .filter(Feedback.data['model_id'].as_string().isnot(None)) + .distinct() + ) + rows = result.all() + return sorted([row[0] for row in rows if row[0]]) + + async def get_feedbacks_for_leaderboard(self, db: Optional[AsyncSession] = None) -> list[LeaderboardFeedbackData]: """Fetch only id and data for leaderboard computation (excludes snapshot/meta).""" - with get_db_context(db) as db: - return [ - LeaderboardFeedbackData(id=row.id, data=row.data) - for row in db.query(Feedback.id, Feedback.data).all() - ] + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback.id, Feedback.data)) + return [LeaderboardFeedbackData(id=row.id, data=row.data) for row in result.all()] - def get_model_evaluation_history( - self, model_id: str, days: int = 30, db: Optional[Session] = None + async def get_model_evaluation_history( + self, model_id: str, days: int = 30, db: Optional[AsyncSession] = None ) -> list[ModelHistoryEntry]: """ Get daily wins/losses for a specific model over the past N days. If days=0, returns all time data starting from first feedback. Returns: [{"date": "2026-01-08", "won": 5, "lost": 2}, ...] """ - from datetime import datetime, timedelta from collections import defaultdict + from datetime import datetime, timedelta - with get_db_context(db) as db: + async with get_async_db_context(db) as db: if days == 0: # All time - no cutoff - rows = db.query(Feedback.created_at, Feedback.data).all() + result = await db.execute(select(Feedback.created_at, Feedback.data)) else: cutoff = int(time.time()) - (days * 86400) - rows = ( - db.query(Feedback.created_at, Feedback.data) - .filter(Feedback.created_at >= cutoff) - .all() + result = await db.execute( + select(Feedback.created_at, Feedback.data).filter(Feedback.created_at >= cutoff) ) + rows = result.all() - daily_counts = defaultdict(lambda: {"won": 0, "lost": 0}) + daily_counts = defaultdict(lambda: {'won': 0, 'lost': 0}) first_date = None for created_at, data in rows: if not data: continue - if data.get("model_id") != model_id: + if data.get('model_id') != model_id: continue - rating_str = str(data.get("rating", "")) - if rating_str not in ("1", "-1"): + rating_str = str(data.get('rating', '')) + if rating_str not in ('1', '-1'): continue - date_str = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d") - if rating_str == "1": - daily_counts[date_str]["won"] += 1 + date_str = datetime.fromtimestamp(created_at).strftime('%Y-%m-%d') + if rating_str == '1': + daily_counts[date_str]['won'] += 1 else: - daily_counts[date_str]["lost"] += 1 + daily_counts[date_str]['lost'] += 1 # Track first date for this model if first_date is None or date_str < first_date: @@ -351,7 +360,7 @@ class FeedbackTable: if days == 0 and first_date: # All time: start from first feedback date - start_date = datetime.strptime(first_date, "%Y-%m-%d").date() + start_date = datetime.strptime(first_date, '%Y-%m-%d').date() num_days = (today - start_date).days + 1 else: # Fixed range @@ -360,43 +369,28 @@ class FeedbackTable: for i in range(num_days): d = start_date + timedelta(days=i) - date_str = d.strftime("%Y-%m-%d") - counts = daily_counts.get(date_str, {"won": 0, "lost": 0}) - result.append( - ModelHistoryEntry(date=date_str, won=counts["won"], lost=counts["lost"]) - ) + date_str = d.strftime('%Y-%m-%d') + counts = daily_counts.get(date_str, {'won': 0, 'lost': 0}) + result.append(ModelHistoryEntry(date=date_str, won=counts['won'], lost=counts['lost'])) return result - def get_feedbacks_by_type( - self, type: str, db: Optional[Session] = None - ) -> list[FeedbackModel]: - with get_db_context(db) as db: - return [ - FeedbackModel.model_validate(feedback) - for feedback in db.query(Feedback) - .filter_by(type=type) - .order_by(Feedback.updated_at.desc()) - .all() - ] + async def get_feedbacks_by_type(self, type: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(type=type).order_by(Feedback.updated_at.desc())) + return [FeedbackModel.model_validate(feedback) for feedback in result.scalars().all()] - def get_feedbacks_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[FeedbackModel]: - with get_db_context(db) as db: - return [ - FeedbackModel.model_validate(feedback) - for feedback in db.query(Feedback) - .filter_by(user_id=user_id) - .order_by(Feedback.updated_at.desc()) - .all() - ] + async def get_feedbacks_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[FeedbackModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(user_id=user_id).order_by(Feedback.updated_at.desc())) + return [FeedbackModel.model_validate(feedback) for feedback in result.scalars().all()] - def update_feedback_by_id( - self, id: str, form_data: FeedbackForm, db: Optional[Session] = None + async def update_feedback_by_id( + self, id: str, form_data: FeedbackForm, db: Optional[AsyncSession] = None ) -> Optional[FeedbackModel]: - with get_db_context(db) as db: - feedback = db.query(Feedback).filter_by(id=id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id)) + feedback = result.scalars().first() if not feedback: return None @@ -409,18 +403,19 @@ class FeedbackTable: feedback.updated_at = int(time.time()) - db.commit() + await db.commit() return FeedbackModel.model_validate(feedback) - def update_feedback_by_id_and_user_id( + async def update_feedback_by_id_and_user_id( self, id: str, user_id: str, form_data: FeedbackForm, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[FeedbackModel]: - with get_db_context(db) as db: - feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id, user_id=user_id)) + feedback = result.scalars().first() if not feedback: return None @@ -433,50 +428,40 @@ class FeedbackTable: feedback.updated_at = int(time.time()) - db.commit() + await db.commit() return FeedbackModel.model_validate(feedback) - def delete_feedback_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - feedback = db.query(Feedback).filter_by(id=id).first() + async def delete_feedback_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id)) + feedback = result.scalars().first() if not feedback: return False - db.delete(feedback) - db.commit() + await db.delete(feedback) + await db.commit() return True - def delete_feedback_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - feedback = db.query(Feedback).filter_by(id=id, user_id=user_id).first() + async def delete_feedback_by_id_and_user_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(select(Feedback).filter_by(id=id, user_id=user_id)) + feedback = result.scalars().first() if not feedback: return False - db.delete(feedback) - db.commit() + await db.delete(feedback) + await db.commit() return True - def delete_feedbacks_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: - feedbacks = db.query(Feedback).filter_by(user_id=user_id).all() - if not feedbacks: - return False - for feedback in feedbacks: - db.delete(feedback) - db.commit() - return True + async def delete_feedbacks_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(delete(Feedback).filter_by(user_id=user_id)) + await db.commit() + return result.rowcount > 0 - def delete_all_feedbacks(self, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - feedbacks = db.query(Feedback).all() - if not feedbacks: - return False - for feedback in feedbacks: - db.delete(feedback) - db.commit() - return True + async def delete_all_feedbacks(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute(delete(Feedback)) + await db.commit() + return result.rowcount > 0 Feedbacks = FeedbackTable() diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index 4097ae08e1..8d2ff54510 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -1,34 +1,32 @@ +"""File upload models, forms, and database operations.""" + +from __future__ import annotations + import logging import time -from typing import Optional - -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, JSON +# local imports +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.utils.misc import sanitize_metadata +from pydantic import BaseModel, ConfigDict, model_validator +from sqlalchemy import JSON, BigInteger, Column, String, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# Files DB Schema -#################### - -class File(Base): - __tablename__ = "file" +class File(Base): # uploaded file record + __tablename__ = 'file' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) + user_id = Column(String, index=True) # owner user id hash = Column(Text, nullable=True) - filename = Column(Text) + filename = Column(Text) # original upload filename path = Column(Text, nullable=True) data = Column(JSON, nullable=True) meta = Column(JSON, nullable=True) - access_control = Column(JSON, nullable=True) - - created_at = Column(BigInteger) + created_at = Column(BigInteger, index=True) # upload timestamp updated_at = Column(BigInteger) @@ -37,123 +35,140 @@ class FileModel(BaseModel): id: str user_id: str - hash: Optional[str] = None + hash: str | None = None filename: str - path: Optional[str] = None + path: str | None = None - data: Optional[dict] = None - meta: Optional[dict] = None - - access_control: Optional[dict] = None - - created_at: Optional[int] # timestamp in epoch - updated_at: Optional[int] # timestamp in epoch - - -#################### -# Forms -#################### + data: dict | None = None + meta: dict | None = None + created_at: int | None # timestamp in epoch + updated_at: int | None # timestamp in epoch +# --- metadata structures --- class FileMeta(BaseModel): - name: Optional[str] = None - content_type: Optional[str] = None - size: Optional[int] = None + name: str | None = None + content_type: str | None = None + size: int | None = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') + + @model_validator(mode='before') + @classmethod + def sanitize_meta(cls, data): + """Sanitize metadata fields to handle malformed legacy data.""" + if not isinstance(data, dict): + return data + + # Handle content_type that may be a list like ['application/pdf', None] + content_type = data.get('content_type') + if isinstance(content_type, list): + # Extract first non-None string value + data['content_type'] = next((item for item in content_type if isinstance(item, str)), None) + elif content_type is not None and not isinstance(content_type, str): + data['content_type'] = None + + return data class FileModelResponse(BaseModel): id: str user_id: str - hash: Optional[str] = None + hash: str | None = None filename: str - data: Optional[dict] = None - meta: FileMeta + data: dict | None = None + meta: FileMeta | None = None created_at: int # timestamp in epoch - updated_at: int # timestamp in epoch + updated_at: int | None = None # timestamp in epoch, optional for legacy files - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class FileMetadataResponse(BaseModel): id: str - hash: Optional[str] = None - meta: Optional[dict] = None + hash: str | None = None + meta: dict | None = None created_at: int # timestamp in epoch updated_at: int # timestamp in epoch +class FileListResponse(BaseModel): + items: list[FileModelResponse] + total: int + + class FileForm(BaseModel): id: str - hash: Optional[str] = None + hash: str | None = None filename: str path: str data: dict = {} meta: dict = {} - access_control: Optional[dict] = None class FileUpdateForm(BaseModel): - hash: Optional[str] = None - data: Optional[dict] = None - meta: Optional[dict] = None - - -class FileListResponse(BaseModel): - items: list[FileModel] - total: int + hash: str | None = None + data: dict | None = None + meta: dict | None = None class FilesTable: - def insert_new_file( - self, user_id: str, form_data: FileForm, db: Optional[Session] = None - ) -> Optional[FileModel]: - with get_db_context(db) as db: + async def insert_new_file( + self, user_id: str, form_data: FileForm, db: AsyncSession | None = None + ) -> FileModel | None: + async with get_async_db_context(db) as db: + file_data = form_data.model_dump() + + # Sanitize meta to remove non-JSON-serializable objects + # (e.g. callable tool functions, MCP client instances from middleware) + if file_data.get('meta'): + file_data['meta'] = sanitize_metadata(file_data['meta']) + file = FileModel( **{ - **form_data.model_dump(), - "user_id": user_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + **file_data, + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) try: result = File(**file.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return FileModel.model_validate(result) else: return None except Exception as e: - log.exception(f"Error inserting a new file: {e}") - return None + log.exception(f'Error inserting a new file: {e}') + return None # insertion failed - def get_file_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[FileModel]: + async def get_file_by_id( + self, id: str, db: AsyncSession | None = None, + ) -> FileModel | None: + """Look up a file by its primary key.""" try: - with get_db_context(db) as db: - try: - file = db.get(File, id) - return FileModel.model_validate(file) - except Exception: + async with get_async_db_context(db) as db: + file = await db.get(File, id) + if not file: return None + return FileModel.model_validate(file) except Exception: return None - def get_file_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> Optional[FileModel]: - with get_db_context(db) as db: + async def get_file_by_id_and_user_id( + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> FileModel | None: + async with get_async_db_context(db) as db: try: - file = db.query(File).filter_by(id=id, user_id=user_id).first() + result = await db.execute(select(File).filter_by(id=id, user_id=user_id)) + file = result.scalars().first() if file: return FileModel.model_validate(file) else: @@ -161,12 +176,12 @@ class FilesTable: except Exception: return None - def get_file_metadata_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[FileMetadataResponse]: - with get_db_context(db) as db: + async def get_file_metadata_by_id(self, id: str, db: AsyncSession | None = None) -> FileMetadataResponse | None: + async with get_async_db_context(db) as db: try: - file = db.get(File, id) + file = await db.get(File, id) + if not file: + return None return FileMetadataResponse( id=file.id, hash=file.hash, @@ -177,14 +192,13 @@ class FilesTable: except Exception: return None - def get_files(self, db: Optional[Session] = None) -> list[FileModel]: - with get_db_context(db) as db: - return [FileModel.model_validate(file) for file in db.query(File).all()] + async def get_files(self, db: AsyncSession | None = None) -> list[FileModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(File)) + return [FileModel.model_validate(file) for file in result.scalars().all()] - def check_access_by_user_id( - self, id, user_id, permission="write", db: Optional[Session] = None - ) -> bool: - file = self.get_file_by_id(id, db=db) + async def check_access_by_user_id(self, id, user_id, permission='write', db: AsyncSession | None = None) -> bool: + file = await self.get_file_by_id(id, db=db) if not file: return False if file.user_id == user_id: @@ -192,46 +206,55 @@ class FilesTable: # Implement additional access control logic here as needed return False - def get_files_by_ids( - self, ids: list[str], db: Optional[Session] = None - ) -> list[FileModel]: - with get_db_context(db) as db: - return [ - FileModel.model_validate(file) - for file in db.query(File) + async def get_files_by_ids(self, ids: list[str], db: AsyncSession | None = None) -> list[FileModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(File).filter(File.id.in_(ids)).order_by(File.updated_at.desc())) + return [FileModel.model_validate(file) for file in result.scalars().all()] + + async def get_file_metadatas_by_ids( + self, ids: list[str], db: AsyncSession | None = None + ) -> list[FileMetadataResponse]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(File.id, File.hash, File.meta, File.created_at, File.updated_at) .filter(File.id.in_(ids)) .order_by(File.updated_at.desc()) - .all() - ] - - def get_file_metadatas_by_ids( - self, ids: list[str], db: Optional[Session] = None - ) -> list[FileMetadataResponse]: - with get_db_context(db) as db: + ) return [ FileMetadataResponse( - id=file.id, - hash=file.hash, - meta=file.meta, - created_at=file.created_at, - updated_at=file.updated_at, + id=row.id, + hash=row.hash, + meta=row.meta, + created_at=row.created_at, + updated_at=row.updated_at, ) - for file in db.query( - File.id, File.hash, File.meta, File.created_at, File.updated_at - ) - .filter(File.id.in_(ids)) - .order_by(File.updated_at.desc()) - .all() + for row in result.all() ] - def get_files_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[FileModel]: - with get_db_context(db) as db: - return [ - FileModel.model_validate(file) - for file in db.query(File).filter_by(user_id=user_id).all() - ] + async def get_files_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[FileModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(File).filter_by(user_id=user_id)) + return [FileModel.model_validate(file) for file in result.scalars().all()] + + async def get_file_list( + self, + user_id: str | None = None, + skip: int = 0, + limit: int = 50, + db: AsyncSession | None = None, + ) -> 'FileListResponse': + async with get_async_db_context(db) as db: + stmt = select(File) + if user_id: + stmt = stmt.filter_by(user_id=user_id) + + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + result = await db.execute(stmt.order_by(File.updated_at.desc(), File.id.desc()).offset(skip).limit(limit)) + items = [FileModelResponse.model_validate(file, from_attributes=True) for file in result.scalars().all()] + + return FileListResponse(items=items, total=total) @staticmethod def _glob_to_like_pattern(glob: str) -> str: @@ -249,20 +272,20 @@ class FilesTable: A SQL LIKE compatible pattern with proper escaping. """ # Escape SQL special characters first, then convert glob wildcards - pattern = glob.replace("\\", "\\\\") - pattern = pattern.replace("%", "\\%") - pattern = pattern.replace("_", "\\_") - pattern = pattern.replace("*", "%") - pattern = pattern.replace("?", "_") + pattern = glob.replace('\\', '\\\\') + pattern = pattern.replace('%', '\\%') + pattern = pattern.replace('_', '\\_') + pattern = pattern.replace('*', '%') + pattern = pattern.replace('?', '_') return pattern - def search_files( + async def search_files( self, - user_id: Optional[str] = None, - filename: str = "*", + user_id: str | None = None, + filename: str = '*', skip: int = 0, limit: int = 100, - db: Optional[Session] = None, + db: AsyncSession | None = None, ) -> list[FileModel]: """ Search files with glob pattern matching, optional user filter, and pagination. @@ -275,32 +298,28 @@ class FilesTable: db: Optional database session. Returns: - List of matching FileModel objects, ordered by updated_at descending. + List of matching FileModel objects, ordered by created_at descending. """ - with get_db_context(db) as db: - query = db.query(File) + async with get_async_db_context(db) as db: + stmt = select(File) if user_id: - query = query.filter_by(user_id=user_id) + stmt = stmt.filter_by(user_id=user_id) pattern = self._glob_to_like_pattern(filename) - if pattern != "%": - query = query.filter(File.filename.ilike(pattern, escape="\\")) + if pattern != '%': + stmt = stmt.filter(File.filename.ilike(pattern, escape='\\')) - return [ - FileModel.model_validate(file) - for file in query.order_by(File.updated_at.desc()) - .offset(skip) - .limit(limit) - .all() - ] + result = await db.execute(stmt.order_by(File.created_at.desc(), File.id.desc()).offset(skip).limit(limit)) + return [FileModel.model_validate(file) for file in result.scalars().all()] - def update_file_by_id( - self, id: str, form_data: FileUpdateForm, db: Optional[Session] = None - ) -> Optional[FileModel]: - with get_db_context(db) as db: + async def update_file_by_id( + self, id: str, form_data: FileUpdateForm, db: AsyncSession | None = None + ) -> FileModel | None: + async with get_async_db_context(db) as db: try: - file = db.query(File).filter_by(id=id).first() + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() if form_data.hash is not None: file.hash = form_data.hash @@ -312,74 +331,126 @@ class FilesTable: file.meta = {**(file.meta if file.meta else {}), **form_data.meta} file.updated_at = int(time.time()) - db.commit() + await db.commit() return FileModel.model_validate(file) except Exception as e: - log.exception(f"Error updating file completely by id: {e}") + log.exception(f'Error updating file completely by id: {e}') return None - def update_file_hash_by_id( - self, id: str, hash: Optional[str], db: Optional[Session] = None - ) -> Optional[FileModel]: - with get_db_context(db) as db: + async def update_file_hash_by_id( + self, id: str, hash: str | None, db: AsyncSession | None = None + ) -> FileModel | None: + async with get_async_db_context(db) as db: try: - file = db.query(File).filter_by(id=id).first() + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() file.hash = hash file.updated_at = int(time.time()) - db.commit() + await db.commit() return FileModel.model_validate(file) except Exception: return None - def update_file_data_by_id( - self, id: str, data: dict, db: Optional[Session] = None - ) -> Optional[FileModel]: - with get_db_context(db) as db: + async def update_file_data_by_id(self, id: str, data: dict, db: AsyncSession | None = None) -> FileModel | None: + async with get_async_db_context(db) as db: try: - file = db.query(File).filter_by(id=id).first() + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() file.data = {**(file.data if file.data else {}), **data} file.updated_at = int(time.time()) - db.commit() + await db.commit() return FileModel.model_validate(file) except Exception as e: - return None - def update_file_metadata_by_id( - self, id: str, meta: dict, db: Optional[Session] = None - ) -> Optional[FileModel]: - with get_db_context(db) as db: + async def update_file_metadata_by_id(self, id: str, meta: dict, db: AsyncSession | None = None) -> FileModel | None: + async with get_async_db_context(db) as db: try: - file = db.query(File).filter_by(id=id).first() + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() file.meta = {**(file.meta if file.meta else {}), **meta} file.updated_at = int(time.time()) - db.commit() + await db.commit() return FileModel.model_validate(file) except Exception: return None - return False - - def delete_file_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: + async def update_file_name_by_id(self, id: str, name: str, db: AsyncSession | None = None) -> FileModel | None: + async with get_async_db_context(db) as db: try: - db.query(File).filter_by(id=id).delete() - db.commit() + result = await db.execute(select(File).filter_by(id=id)) + file = result.scalars().first() + file.filename = name + file.meta = {**(file.meta if file.meta else {}), 'name': name} + file.updated_at = int(time.time()) + await db.commit() + return FileModel.model_validate(file) + except Exception: + return None + + async def get_pending_files_for_knowledge( + self, knowledge_id: str, db: AsyncSession | None = None + ) -> list[FileModelResponse]: + """Return files still being processed for this knowledge base. + + These are files uploaded with ``meta.data.knowledge_id`` set, whose + ``data.status`` is still ``pending`` or ``processing``, and which + have not yet been added to the ``knowledge_file`` join table. + + The JSON subscript syntax (``Column['key']['subkey'].as_string()``) + is supported by both SQLite (``json_extract``) and PostgreSQL + (``->>``/``->``). + """ + async with get_async_db_context(db) as db: + try: + # Lazy import to avoid circular dependency + from open_webui.models.knowledge import KnowledgeFile + + # Subquery: file IDs already linked to this knowledge base + linked_ids = ( + select(KnowledgeFile.file_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + .correlate(None) + ) + + stmt = ( + select(File) + .filter( + File.meta['data']['knowledge_id'].as_string() == knowledge_id, + File.data['status'].as_string().in_(['pending', 'processing']), + File.id.notin_(linked_ids), + ) + .order_by(File.created_at.desc()) + ) + result = await db.execute(stmt) + return [ + FileModelResponse.model_validate(f, from_attributes=True) + for f in result.scalars().all() + ] + except Exception as e: + log.warning(f'Error fetching pending files for knowledge {knowledge_id}: {e}') + return [] + + async def delete_file_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(File).filter_by(id=id)) + await db.commit() return True except Exception: return False - def delete_all_files(self, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: + async def delete_all_files(self, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as db: try: - db.query(File).delete() - db.commit() + await db.execute(delete(File)) + await db.commit() return True except Exception: return False -Files = FilesTable() +Files = FilesTable() # singleton files repository diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index 3455208944..1688b8bd46 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -1,27 +1,26 @@ import logging +import re import time import uuid from typing import Optional -import re - +from open_webui.internal.db import Base, JSONField, get_async_db_context from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, Text, JSON, Boolean, func -from sqlalchemy.orm import Session - -from open_webui.internal.db import Base, JSONField, get_db, get_db_context - +from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) #################### # Folder DB Schema +# Let every room in this house shelter someone who needs it, +# and let no chamber stand empty while there is want. #################### class Folder(Base): - __tablename__ = "folder" + __tablename__ = 'folder' id = Column(Text, primary_key=True, unique=True) parent_id = Column(Text, nullable=True) user_id = Column(Text) @@ -72,55 +71,57 @@ class FolderForm(BaseModel): name: str data: Optional[dict] = None meta: Optional[dict] = None - model_config = ConfigDict(extra="allow") + parent_id: Optional[str] = None + model_config = ConfigDict(extra='forbid') class FolderUpdateForm(BaseModel): name: Optional[str] = None data: Optional[dict] = None meta: Optional[dict] = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='forbid') class FolderTable: - def insert_new_folder( + async def insert_new_folder( self, user_id: str, form_data: FolderForm, parent_id: Optional[str] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[FolderModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: id = str(uuid.uuid4()) folder = FolderModel( **{ - "id": id, - "user_id": user_id, + 'id': id, + 'user_id': user_id, **(form_data.model_dump(exclude_unset=True) or {}), - "parent_id": parent_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + 'parent_id': parent_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) try: result = Folder(**folder.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return FolderModel.model_validate(result) else: return None except Exception as e: - log.exception(f"Error inserting a new folder: {e}") + log.exception(f'Error inserting a new folder: {e}') return None - def get_folder_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def get_folder_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[FolderModel]: try: - with get_db_context(db) as db: - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() if not folder: return None @@ -129,85 +130,75 @@ class FolderTable: except Exception: return None - def get_children_folders_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def get_children_folders_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[list[FolderModel]]: try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: folders = [] - def get_children(folder): - children = self.get_folders_by_parent_id_and_user_id( - folder.id, user_id, db=db - ) + async def get_children(folder): + children = await self.get_folders_by_parent_id_and_user_id(folder.id, user_id, db=db) for child in children: - get_children(child) + await get_children(child) folders.append(child) - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() if not folder: return None - get_children(folder) + await get_children(folder) return folders except Exception: return None - def get_folders_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[FolderModel]: - with get_db_context(db) as db: - return [ - FolderModel.model_validate(folder) - for folder in db.query(Folder).filter_by(user_id=user_id).all() - ] + async def get_folders_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[FolderModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(user_id=user_id)) + return [FolderModel.model_validate(folder) for folder in result.scalars().all()] - def get_folder_by_parent_id_and_user_id_and_name( + async def get_folder_by_parent_id_and_user_id_and_name( self, parent_id: Optional[str], user_id: str, name: str, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[FolderModel]: try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Check if folder exists - folder = ( - db.query(Folder) - .filter_by(parent_id=parent_id, user_id=user_id) - .filter(Folder.name.ilike(name)) - .first() + result = await db.execute( + select(Folder).filter_by(parent_id=parent_id, user_id=user_id).filter(Folder.name.ilike(name)) ) + folder = result.scalars().first() if not folder: return None return FolderModel.model_validate(folder) except Exception as e: - log.error(f"get_folder_by_parent_id_and_user_id_and_name: {e}") + log.error(f'get_folder_by_parent_id_and_user_id_and_name: {e}') return None - def get_folders_by_parent_id_and_user_id( - self, parent_id: Optional[str], user_id: str, db: Optional[Session] = None + async def get_folders_by_parent_id_and_user_id( + self, parent_id: Optional[str], user_id: str, db: Optional[AsyncSession] = None ) -> list[FolderModel]: - with get_db_context(db) as db: - return [ - FolderModel.model_validate(folder) - for folder in db.query(Folder) - .filter_by(parent_id=parent_id, user_id=user_id) - .all() - ] + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(parent_id=parent_id, user_id=user_id)) + return [FolderModel.model_validate(folder) for folder in result.scalars().all()] - def update_folder_parent_id_by_id_and_user_id( + async def update_folder_parent_id_by_id_and_user_id( self, id: str, user_id: str, parent_id: str, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[FolderModel]: try: - with get_db_context(db) as db: - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() if not folder: return None @@ -215,69 +206,70 @@ class FolderTable: folder.parent_id = parent_id folder.updated_at = int(time.time()) - db.commit() + await db.commit() return FolderModel.model_validate(folder) except Exception as e: - log.error(f"update_folder: {e}") + log.error(f'update_folder: {e}') return - def update_folder_by_id_and_user_id( + async def update_folder_by_id_and_user_id( self, id: str, user_id: str, form_data: FolderUpdateForm, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[FolderModel]: try: - with get_db_context(db) as db: - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() if not folder: return None form_data = form_data.model_dump(exclude_unset=True) - existing_folder = ( - db.query(Folder) - .filter_by( - name=form_data.get("name"), + existing_result = await db.execute( + select(Folder).filter_by( + name=form_data.get('name'), parent_id=folder.parent_id, user_id=user_id, ) - .first() ) + existing_folder = existing_result.scalars().first() if existing_folder and existing_folder.id != id: return None - folder.name = form_data.get("name", folder.name) - if "data" in form_data: + folder.name = form_data.get('name', folder.name) + if 'data' in form_data: folder.data = { **(folder.data or {}), - **form_data["data"], + **form_data['data'], } - if "meta" in form_data: + if 'meta' in form_data: folder.meta = { **(folder.meta or {}), - **form_data["meta"], + **form_data['meta'], } folder.updated_at = int(time.time()) - db.commit() + await db.commit() return FolderModel.model_validate(folder) except Exception as e: - log.error(f"update_folder: {e}") + log.error(f'update_folder: {e}') return - def update_folder_is_expanded_by_id_and_user_id( - self, id: str, user_id: str, is_expanded: bool, db: Optional[Session] = None + async def update_folder_is_expanded_by_id_and_user_id( + self, id: str, user_id: str, is_expanded: bool, db: Optional[AsyncSession] = None ) -> Optional[FolderModel]: try: - with get_db_context(db) as db: - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() if not folder: return None @@ -285,54 +277,53 @@ class FolderTable: folder.is_expanded = is_expanded folder.updated_at = int(time.time()) - db.commit() + await db.commit() return FolderModel.model_validate(folder) except Exception as e: - log.error(f"update_folder: {e}") + log.error(f'update_folder: {e}') return - def delete_folder_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def delete_folder_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None ) -> list[str]: try: folder_ids = [] - with get_db_context(db) as db: - folder = db.query(Folder).filter_by(id=id, user_id=user_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(id=id, user_id=user_id)) + folder = result.scalars().first() if not folder: return folder_ids folder_ids.append(folder.id) # Delete all children folders - def delete_children(folder): - folder_children = self.get_folders_by_parent_id_and_user_id( - folder.id, user_id, db=db - ) + async def delete_children(folder): + folder_children = await self.get_folders_by_parent_id_and_user_id(folder.id, user_id, db=db) for folder_child in folder_children: - - delete_children(folder_child) + await delete_children(folder_child) folder_ids.append(folder_child.id) - folder = db.query(Folder).filter_by(id=folder_child.id).first() - db.delete(folder) - db.commit() + child_result = await db.execute(select(Folder).filter_by(id=folder_child.id)) + child_folder = child_result.scalars().first() + await db.delete(child_folder) + await db.commit() - delete_children(folder) - db.delete(folder) - db.commit() + await delete_children(folder) + await db.delete(folder) + await db.commit() return folder_ids except Exception as e: - log.error(f"delete_folder: {e}") + log.error(f'delete_folder: {e}') return [] def normalize_folder_name(self, name: str) -> str: # Replace _ and space with a single space, lower case, collapse multiple spaces - name = re.sub(r"[\s_]+", " ", name) + name = re.sub(r'[\s_]+', ' ', name) return name.strip().lower() - def search_folders_by_names( - self, user_id: str, queries: list[str], db: Optional[Session] = None + async def search_folders_by_names( + self, user_id: str, queries: list[str], db: Optional[AsyncSession] = None ) -> list[FolderModel]: """ Search for folders for a user where the name matches any of the queries, treating _ and space as equivalent, case-insensitive. @@ -342,18 +333,18 @@ class FolderTable: return [] results = {} - with get_db_context(db) as db: - folders = db.query(Folder).filter_by(user_id=user_id).all() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(user_id=user_id)) + folders = result.scalars().all() for folder in folders: if self.normalize_folder_name(folder.name) in normalized_queries: results[folder.id] = FolderModel.model_validate(folder) # get children folders - children = self.get_children_folders_by_id_and_user_id( - folder.id, user_id, db=db - ) - for child in children: - results[child.id] = child + children = await self.get_children_folders_by_id_and_user_id(folder.id, user_id, db=db) + if children: + for child in children: + results[child.id] = child # Return the results as a list if not results: @@ -362,16 +353,17 @@ class FolderTable: results = list(results.values()) return results - def search_folders_by_name_contains( - self, user_id: str, query: str, db: Optional[Session] = None + async def search_folders_by_name_contains( + self, user_id: str, query: str, db: Optional[AsyncSession] = None ) -> list[FolderModel]: """ Partial match: normalized name contains (as substring) the normalized query. """ normalized_query = self.normalize_folder_name(query) results = [] - with get_db_context(db) as db: - folders = db.query(Folder).filter_by(user_id=user_id).all() + async with get_async_db_context(db) as db: + result = await db.execute(select(Folder).filter_by(user_id=user_id)) + folders = result.scalars().all() for folder in folders: norm_name = self.normalize_folder_name(folder.name) if normalized_query in norm_name: diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index 8e23bac093..99677a62e6 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -1,42 +1,41 @@ +"""Function (filter/action/pipe) models, forms, and database operations.""" + +from __future__ import annotations + import logging import time -from typing import Optional - -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from open_webui.models.users import Users, UserModel +# local imports +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.users import UserModel, UserResponse, Users from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index +from sqlalchemy import BigInteger, Boolean, Column, Index, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# Functions DB Schema -#################### - -class Function(Base): - __tablename__ = "function" +class Function(Base): # database table mapping + __tablename__ = 'function' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) - name = Column(Text) - type = Column(Text) - content = Column(Text) - meta = Column(JSONField) - valves = Column(JSONField) - is_active = Column(Boolean) - is_global = Column(Boolean) - updated_at = Column(BigInteger) - created_at = Column(BigInteger) + user_id = Column(String, index=True) # creator user id + name = Column(Text, nullable=False) # function identifier + type = Column(Text, nullable=False) # function type (pipe, filter, etc.) + content = Column(Text, nullable=True) # Python source code + meta = Column(JSONField, nullable=True) # function metadata + valves = Column(JSONField, nullable=True) # function configuration valves + is_active = Column(Boolean, default=False) # function activation status + is_global = Column(Boolean) # if True, applied to every chat automatically + updated_at = Column(BigInteger) # epoch seconds + created_at = Column(BigInteger) # epoch seconds - __table_args__ = (Index("is_global_idx", "is_global"),) + __table_args__ = (Index('is_global_idx', 'is_global'),) # speed up global-function lookups class FunctionMeta(BaseModel): - description: Optional[str] = None - manifest: Optional[dict] = {} - model_config = ConfigDict(extra="allow") + description: str | None = None + manifest: dict | None = {} + model_config = ConfigDict(extra='allow') class FunctionModel(BaseModel): @@ -51,9 +50,9 @@ class FunctionModel(BaseModel): updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) - + model_config = ConfigDict(from_attributes=True) # allows ORM model binding +# --- form / schema definitions --- class FunctionWithValvesModel(BaseModel): id: str user_id: str @@ -61,7 +60,7 @@ class FunctionWithValvesModel(BaseModel): type: str content: str meta: FunctionMeta - valves: Optional[dict] = None + valves: dict | None = None is_active: bool = False is_global: bool = False updated_at: int # timestamp in epoch @@ -75,10 +74,6 @@ class FunctionWithValvesModel(BaseModel): #################### -class FunctionUserResponse(FunctionModel): - user: Optional[UserModel] = None - - class FunctionResponse(BaseModel): id: str user_id: str @@ -90,6 +85,12 @@ class FunctionResponse(BaseModel): updated_at: int # timestamp in epoch created_at: int # timestamp in epoch + model_config = ConfigDict(from_attributes=True) + + +class FunctionUserResponse(FunctionResponse): + user: UserResponse | None = None + class FunctionForm(BaseModel): id: str @@ -99,52 +100,53 @@ class FunctionForm(BaseModel): class FunctionValves(BaseModel): - valves: Optional[dict] = None + valves: dict | None = None class FunctionsTable: - def insert_new_function( + async def insert_new_function( self, user_id: str, type: str, form_data: FunctionForm, - db: Optional[Session] = None, - ) -> Optional[FunctionModel]: + db: AsyncSession | None = None, + ) -> FunctionModel | None: function = FunctionModel( **{ **form_data.model_dump(), - "user_id": user_id, - "type": type, - "updated_at": int(time.time()), - "created_at": int(time.time()), + 'user_id': user_id, + 'type': type, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), } ) try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: result = Function(**function.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return FunctionModel.model_validate(result) else: return None except Exception as e: - log.exception(f"Error creating a new function: {e}") + log.exception(f'Error creating a new function: {e}') return None - def sync_functions( + async def sync_functions( self, user_id: str, functions: list[FunctionWithValvesModel], - db: Optional[Session] = None, + db: AsyncSession | None = None, ) -> list[FunctionWithValvesModel]: # Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present. try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Get existing functions - existing_functions = db.query(Function).all() + result = await db.execute(select(Function)) + existing_functions = result.scalars().all() existing_ids = {func.id for func in existing_functions} # Prepare a set of new function IDs @@ -153,19 +155,21 @@ class FunctionsTable: # Update or insert functions for func in functions: if func.id in existing_ids: - db.query(Function).filter_by(id=func.id).update( - { + await db.execute( + update(Function) + .filter_by(id=func.id) + .values( **func.model_dump(), - "user_id": user_id, - "updated_at": int(time.time()), - } + user_id=user_id, + updated_at=int(time.time()), + ) ) else: new_func = Function( **{ **func.model_dump(), - "user_id": user_id, - "updated_at": int(time.time()), + 'user_id': user_id, + 'updated_at': int(time.time()), } ) db.add(new_func) @@ -173,64 +177,78 @@ class FunctionsTable: # Remove functions that are no longer present for func in existing_functions: if func.id not in new_function_ids: - db.delete(func) + await db.delete(func) - db.commit() + await db.commit() - return [ - FunctionModel.model_validate(func) - for func in db.query(Function).all() - ] + result = await db.execute(select(Function)) + return [FunctionModel.model_validate(func) for func in result.scalars().all()] except Exception as e: - log.exception(f"Error syncing functions for user {user_id}: {e}") + log.exception(f'Error syncing functions for user {user_id}: {e}') return [] - def get_function_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[FunctionModel]: + async def get_function_by_id(self, id: str, db: AsyncSession | None = None) -> FunctionModel | None: try: - with get_db_context(db) as db: - function = db.get(Function, id) - return FunctionModel.model_validate(function) + async with get_async_db_context(db) as db: + function = await db.get(Function, id) + return FunctionModel.model_validate(function) if function else None except Exception: return None - def get_functions( - self, active_only=False, include_valves=False, db: Optional[Session] = None - ) -> list[FunctionModel | FunctionWithValvesModel]: - with get_db_context(db) as db: - if active_only: - functions = db.query(Function).filter_by(is_active=True).all() + async def get_functions_by_ids(self, ids: list[str], db: AsyncSession | None = None) -> list[FunctionModel]: + """ + Batch fetch multiple functions by their IDs in a single query. + Returns functions in the same order as the input IDs (None entries filtered out). + """ + if not ids: + return [] + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).filter(Function.id.in_(ids))) + functions = result.scalars().all() + # Create a dict for O(1) lookup + func_dict = {f.id: FunctionModel.model_validate(f) for f in functions} + # Return in original order, filtering out any not found + return [func_dict[id] for id in ids if id in func_dict] + except Exception: + return [] + async def get_functions( + self, active_only=False, include_valves=False, db: AsyncSession | None = None + ) -> list[FunctionModel | FunctionWithValvesModel]: + async with get_async_db_context(db) as db: + if active_only: + result = await db.execute(select(Function).filter_by(is_active=True)) else: - functions = db.query(Function).all() + result = await db.execute(select(Function)) + + functions = result.scalars().all() if include_valves: - return [ - FunctionWithValvesModel.model_validate(function) - for function in functions - ] + return [FunctionWithValvesModel.model_validate(function) for function in functions] else: - return [ - FunctionModel.model_validate(function) for function in functions - ] + return [FunctionModel.model_validate(function) for function in functions] - def get_function_list( - self, db: Optional[Session] = None - ) -> list[FunctionUserResponse]: - with get_db_context(db) as db: - functions = db.query(Function).order_by(Function.updated_at.desc()).all() + async def get_function_list(self, db: AsyncSession | None = None) -> list[FunctionUserResponse]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).order_by(Function.updated_at.desc())) + functions = result.scalars().all() user_ids = list(set(func.user_id for func in functions)) - users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} return [ FunctionUserResponse.model_validate( { - **FunctionModel.model_validate(func).model_dump(), - "user": ( - users_dict.get(func.user_id).model_dump() + **FunctionResponse.model_validate(func).model_dump(), + 'user': ( + UserResponse( + id=users_dict[func.user_id].id, + name=users_dict[func.user_id].name, + role=users_dict[func.user_id].role, + email=users_dict[func.user_id].email, + ).model_dump() if func.user_id in users_dict else None ), @@ -239,76 +257,72 @@ class FunctionsTable: for func in functions ] - def get_functions_by_type( - self, type: str, active_only=False, db: Optional[Session] = None + async def get_functions_by_type( + self, type: str, active_only=False, db: AsyncSession | None = None ) -> list[FunctionModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: if active_only: - return [ - FunctionModel.model_validate(function) - for function in db.query(Function) - .filter_by(type=type, is_active=True) - .all() - ] + result = await db.execute(select(Function).filter_by(type=type, is_active=True)) else: - return [ - FunctionModel.model_validate(function) - for function in db.query(Function).filter_by(type=type).all() - ] + result = await db.execute(select(Function).filter_by(type=type)) + return [FunctionModel.model_validate(function) for function in result.scalars().all()] - def get_global_filter_functions( - self, db: Optional[Session] = None - ) -> list[FunctionModel]: - with get_db_context(db) as db: - return [ - FunctionModel.model_validate(function) - for function in db.query(Function) - .filter_by(type="filter", is_active=True, is_global=True) - .all() - ] + async def get_global_filter_functions(self, db: AsyncSession | None = None) -> list[FunctionModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).filter_by(type='filter', is_active=True, is_global=True)) + return [FunctionModel.model_validate(function) for function in result.scalars().all()] - def get_global_action_functions( - self, db: Optional[Session] = None - ) -> list[FunctionModel]: - with get_db_context(db) as db: - return [ - FunctionModel.model_validate(function) - for function in db.query(Function) - .filter_by(type="action", is_active=True, is_global=True) - .all() - ] + async def get_global_action_functions(self, db: AsyncSession | None = None) -> list[FunctionModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function).filter_by(type='action', is_active=True, is_global=True)) + return [FunctionModel.model_validate(function) for function in result.scalars().all()] - def get_function_valves_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[dict]: - with get_db_context(db) as db: + async def get_function_valves_by_id(self, id: str, db: AsyncSession | None = None) -> dict | None: + async with get_async_db_context(db) as db: try: - function = db.get(Function, id) + function = await db.get(Function, id) return function.valves if function.valves else {} except Exception as e: - log.exception(f"Error getting function valves by id {id}: {e}") + log.exception(f'Error getting function valves by id {id}: {e}') return None - def update_function_valves_by_id( - self, id: str, valves: dict, db: Optional[Session] = None - ) -> Optional[FunctionValves]: - with get_db_context(db) as db: + async def get_function_valves_by_ids(self, ids: list[str], db: AsyncSession | None = None) -> dict[str, dict]: + """ + Batch fetch valves for multiple functions in a single query. + Returns a dict mapping function_id -> valves dict. + Functions without valves are mapped to {}. + """ + if not ids: + return {} + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Function.id, Function.valves).filter(Function.id.in_(ids))) + functions = result.all() + return {f.id: (f.valves if f.valves else {}) for f in functions} + except Exception as e: + log.exception(f'Error batch-fetching function valves: {e}') + return {} + + async def update_function_valves_by_id( + self, id: str, valves: dict, db: AsyncSession | None = None + ) -> FunctionValves | None: + async with get_async_db_context(db) as db: try: - function = db.get(Function, id) + function = await db.get(Function, id) function.valves = valves function.updated_at = int(time.time()) - db.commit() - db.refresh(function) - return self.get_function_by_id(id, db=db) + await db.commit() + await db.refresh(function) + return FunctionModel.model_validate(function) except Exception: return None - def update_function_metadata_by_id( - self, id: str, metadata: dict, db: Optional[Session] = None - ) -> Optional[FunctionModel]: - with get_db_context(db) as db: + async def update_function_metadata_by_id( + self, id: str, metadata: dict, db: AsyncSession | None = None + ) -> FunctionModel | None: + async with get_async_db_context(db) as db: try: - function = db.get(Function, id) + function = await db.get(Function, id) if function: if function.meta: @@ -317,97 +331,98 @@ class FunctionsTable: function.meta = metadata function.updated_at = int(time.time()) - db.commit() - db.refresh(function) - return self.get_function_by_id(id, db=db) + await db.commit() + await db.refresh(function) + return FunctionModel.model_validate(function) else: return None except Exception as e: - log.exception(f"Error updating function metadata by id {id}: {e}") + log.exception(f'Error updating function metadata by id {id}: {e}') return None - def get_user_valves_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> Optional[dict]: + async def get_user_valves_by_id_and_user_id( + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> dict | None: try: - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} # Check if user has "functions" and "valves" settings - if "functions" not in user_settings: - user_settings["functions"] = {} - if "valves" not in user_settings["functions"]: - user_settings["functions"]["valves"] = {} + if 'functions' not in user_settings: + user_settings['functions'] = {} + if 'valves' not in user_settings['functions']: + user_settings['functions']['valves'] = {} - return user_settings["functions"]["valves"].get(id, {}) + return user_settings['functions']['valves'].get(id, {}) except Exception as e: - log.exception(f"Error getting user values by id {id} and user id {user_id}") + log.exception(f'Error getting user values by id {id} and user id {user_id}') return None - def update_user_valves_by_id_and_user_id( - self, id: str, user_id: str, valves: dict, db: Optional[Session] = None - ) -> Optional[dict]: + async def update_user_valves_by_id_and_user_id( + self, id: str, user_id: str, valves: dict, db: AsyncSession | None = None + ) -> dict | None: try: - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} # Check if user has "functions" and "valves" settings - if "functions" not in user_settings: - user_settings["functions"] = {} - if "valves" not in user_settings["functions"]: - user_settings["functions"]["valves"] = {} + if 'functions' not in user_settings: + user_settings['functions'] = {} + if 'valves' not in user_settings['functions']: + user_settings['functions']['valves'] = {} - user_settings["functions"]["valves"][id] = valves + user_settings['functions']['valves'][id] = valves # Update the user settings in the database - Users.update_user_by_id(user_id, {"settings": user_settings}, db=db) + await Users.update_user_by_id(user_id, {'settings': user_settings}, db=db) - return user_settings["functions"]["valves"][id] + return user_settings['functions']['valves'][id] except Exception as e: - log.exception( - f"Error updating user valves by id {id} and user_id {user_id}: {e}" - ) + log.exception(f'Error updating user valves by id {id} and user_id {user_id}: {e}') return None - def update_function_by_id( - self, id: str, updated: dict, db: Optional[Session] = None - ) -> Optional[FunctionModel]: - with get_db_context(db) as db: + async def update_function_by_id( + self, id: str, updated: dict, db: AsyncSession | None = None + ) -> FunctionModel | None: + async with get_async_db_context(db) as db: try: - db.query(Function).filter_by(id=id).update( - { + await db.execute( + update(Function) + .filter_by(id=id) + .values( **updated, - "updated_at": int(time.time()), - } + updated_at=int(time.time()), + ) ) - db.commit() - return self.get_function_by_id(id, db=db) + await db.commit() + function = await db.get(Function, id) + return FunctionModel.model_validate(function) if function else None except Exception: return None - def deactivate_all_functions(self, db: Optional[Session] = None) -> Optional[bool]: - with get_db_context(db) as db: + async def deactivate_all_functions(self, db: AsyncSession | None = None) -> bool | None: + async with get_async_db_context(db) as db: try: - db.query(Function).update( - { - "is_active": False, - "updated_at": int(time.time()), - } + await db.execute( + update(Function).values( + is_active=False, + updated_at=int(time.time()), + ) ) - db.commit() + await db.commit() return True except Exception: return None - def delete_function_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: + async def delete_function_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as db: try: - db.query(Function).filter_by(id=id).delete() - db.commit() + await db.execute(delete(Function).filter_by(id=id)) + await db.commit() return True except Exception: return False -Functions = FunctionsTable() +Functions = FunctionsTable() # singleton functions engine diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index ae557f4daf..66785794a7 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -1,39 +1,41 @@ import json import logging import time -from typing import Optional import uuid +from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context - +from open_webui.env import DEFAULT_GROUP_SHARE_PERMISSION +from open_webui.internal.db import Base, JSONField, get_async_db_context from open_webui.models.files import FileMetadataResponse - - from pydantic import BaseModel, ConfigDict from sqlalchemy import ( + JSON, BigInteger, Column, + ForeignKey, String, Text, - JSON, and_, - func, - ForeignKey, cast, + delete, + func, or_, + select, + update, ) - +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) #################### # UserGroup DB Schema +# Let none who belong to this house be turned away, +# and let the covenant hold for every member. #################### class Group(Base): - __tablename__ = "group" + __tablename__ = 'group' id = Column(Text, unique=True, primary_key=True) user_id = Column(Text) @@ -69,12 +71,12 @@ class GroupModel(BaseModel): class GroupMember(Base): - __tablename__ = "group_member" + __tablename__ = 'group_member' id = Column(Text, unique=True, primary_key=True) group_id = Column( Text, - ForeignKey("group.id", ondelete="CASCADE"), + ForeignKey('group.id', ondelete='CASCADE'), nullable=False, ) user_id = Column(Text, nullable=False) @@ -99,6 +101,16 @@ class GroupResponse(GroupModel): member_count: Optional[int] = None +class GroupInfoResponse(BaseModel): + id: str + user_id: str + name: str + description: str + member_count: Optional[int] = None + created_at: int + updated_at: int + + class GroupForm(BaseModel): name: str description: str @@ -120,25 +132,36 @@ class GroupListResponse(BaseModel): class GroupTable: - def insert_new_group( - self, user_id: str, form_data: GroupForm, db: Optional[Session] = None + def _ensure_default_share_config(self, group_data: dict) -> dict: + """Ensure the group data dict has a default share config if not already set.""" + if 'data' not in group_data or group_data['data'] is None: + group_data['data'] = {} + if 'config' not in group_data['data']: + group_data['data']['config'] = {} + if 'share' not in group_data['data']['config']: + group_data['data']['config']['share'] = DEFAULT_GROUP_SHARE_PERMISSION + return group_data + + async def insert_new_group( + self, user_id: str, form_data: GroupForm, db: Optional[AsyncSession] = None ) -> Optional[GroupModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: + group_data = self._ensure_default_share_config(form_data.model_dump(exclude_none=True)) group = GroupModel( **{ - **form_data.model_dump(exclude_none=True), - "id": str(uuid.uuid4()), - "user_id": user_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + **group_data, + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) try: result = Group(**group.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return GroupModel.model_validate(result) else: @@ -147,199 +170,208 @@ class GroupTable: except Exception: return None - def get_all_groups(self, db: Optional[Session] = None) -> list[GroupModel]: - with get_db_context(db) as db: - groups = db.query(Group).order_by(Group.updated_at.desc()).all() + async def get_all_groups(self, db: Optional[AsyncSession] = None) -> list[GroupModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).order_by(Group.updated_at.desc())) + groups = result.scalars().all() return [GroupModel.model_validate(group) for group in groups] - def get_groups(self, filter, db: Optional[Session] = None) -> list[GroupResponse]: - with get_db_context(db) as db: - query = db.query(Group) + async def get_group_by_name(self, name: str, db: Optional[AsyncSession] = None) -> Optional[GroupModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter(Group.name == name)) + group = result.scalars().first() + return GroupModel.model_validate(group) if group else None + + async def get_groups(self, filter, db: Optional[AsyncSession] = None) -> list[GroupResponse]: + async with get_async_db_context(db) as db: + member_count = ( + select(func.count(GroupMember.user_id)) + .where(GroupMember.group_id == Group.id) + .correlate(Group) + .scalar_subquery() + .label('member_count') + ) + stmt = select(Group, member_count) if filter: - if "query" in filter: - query = query.filter(Group.name.ilike(f"%{filter['query']}%")) + if 'query' in filter: + stmt = stmt.filter(Group.name.ilike(f'%{filter["query"]}%')) # When share filter is present, member check is handled in the share logic - if "share" in filter: - share_value = filter["share"] - member_id = filter.get("member_id") - json_share = Group.data["config"]["share"] - json_share_bool = json_share.as_boolean() + if 'share' in filter: + share_value = filter['share'] + member_id = filter.get('member_id') + json_share = Group.data['config']['share'] json_share_str = json_share.as_string() + json_share_lower = func.lower(json_share_str) if share_value: - # Groups open to anyone: data is null, share is null, or share is true anyone_can_share = or_( Group.data.is_(None), - json_share_bool.is_(None), - json_share_bool == True, + json_share_str.is_(None), + json_share_lower == 'true', + json_share_lower == '1', # Handle SQLite boolean true ) if member_id: - # Also include member-only groups where user is a member - member_groups_subq = ( - db.query(GroupMember.group_id) - .filter(GroupMember.user_id == member_id) - .subquery() - ) + member_groups_select = select(GroupMember.group_id).where(GroupMember.user_id == member_id) members_only_and_is_member = and_( - json_share_str == "members", - Group.id.in_(member_groups_subq), - ) - query = query.filter( - or_(anyone_can_share, members_only_and_is_member) + json_share_lower == 'members', + Group.id.in_(member_groups_select), ) + stmt = stmt.filter(or_(anyone_can_share, members_only_and_is_member)) else: - query = query.filter(anyone_can_share) + stmt = stmt.filter(anyone_can_share) else: - query = query.filter( - and_(Group.data.isnot(None), json_share_bool == False) - ) + stmt = stmt.filter(and_(Group.data.isnot(None), json_share_lower == 'false')) else: # Only apply member_id filter when share filter is NOT present - if "member_id" in filter: - query = query.join( - GroupMember, GroupMember.group_id == Group.id - ).filter(GroupMember.user_id == filter["member_id"]) + if 'member_id' in filter: + stmt = stmt.filter( + Group.id.in_(select(GroupMember.group_id).where(GroupMember.user_id == filter['member_id'])) + ) + + result = await db.execute(stmt.order_by(Group.updated_at.desc())) + rows = result.all() - groups = query.order_by(Group.updated_at.desc()).all() return [ GroupResponse.model_validate( { **GroupModel.model_validate(group).model_dump(), - "member_count": self.get_group_member_count_by_id( - group.id, db=db - ), + 'member_count': count or 0, } ) - for group in groups + for group, count in rows ] - def search_groups( + async def search_groups( self, filter: Optional[dict] = None, skip: int = 0, limit: int = 30, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> GroupListResponse: - with get_db_context(db) as db: - query = db.query(Group) + async with get_async_db_context(db) as db: + stmt = select(Group) if filter: - if "query" in filter: - query = query.filter(Group.name.ilike(f"%{filter['query']}%")) - if "member_id" in filter: - query = query.join( - GroupMember, GroupMember.group_id == Group.id - ).filter(GroupMember.user_id == filter["member_id"]) - - if "share" in filter: - # 'share' is stored in data JSON, support both sqlite and postgres - share_value = filter["share"] - print("Filtering by share:", share_value) - query = query.filter( - Group.data.op("->>")("share") == str(share_value) + if 'query' in filter: + stmt = stmt.filter(Group.name.ilike(f'%{filter["query"]}%')) + if 'member_id' in filter: + stmt = stmt.filter( + Group.id.in_(select(GroupMember.group_id).where(GroupMember.user_id == filter['member_id'])) ) - total = query.count() - query = query.order_by(Group.updated_at.desc()) - groups = query.offset(skip).limit(limit).all() + if 'share' in filter: + share_value = filter['share'] + stmt = stmt.filter(Group.data.op('->>')('share') == str(share_value)) + + # Get total count + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + member_count = ( + select(func.count(GroupMember.user_id)) + .where(GroupMember.group_id == Group.id) + .correlate(Group) + .scalar_subquery() + .label('member_count') + ) + result = await db.execute( + select(Group, member_count) + .where(Group.id.in_(select(stmt.subquery().c.id))) + .order_by(Group.updated_at.desc()) + .offset(skip) + .limit(limit) + ) + rows = result.all() return { - "items": [ + 'items': [ GroupResponse.model_validate( - **GroupModel.model_validate(group).model_dump(), - member_count=self.get_group_member_count_by_id(group.id, db=db), + { + **GroupModel.model_validate(group).model_dump(), + 'member_count': count or 0, + } ) - for group in groups + for group, count in rows ], - "total": total, + 'total': total, } - def get_groups_by_member_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[GroupModel]: - with get_db_context(db) as db: - return [ - GroupModel.model_validate(group) - for group in db.query(Group) + async def get_groups_by_member_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[GroupModel]: + async with get_async_db_context(db) as db: + result = await db.execute( + select(Group) .join(GroupMember, GroupMember.group_id == Group.id) .filter(GroupMember.user_id == user_id) .order_by(Group.updated_at.desc()) - .all() - ] + ) + return [GroupModel.model_validate(group) for group in result.scalars().all()] - def get_groups_by_member_ids( - self, user_ids: list[str], db: Optional[Session] = None + async def get_groups_by_member_ids( + self, user_ids: list[str], db: Optional[AsyncSession] = None ) -> dict[str, list[GroupModel]]: """Fetch groups for multiple users in a single query to avoid N+1.""" - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Query GroupMember joined with Group, filtering by user_ids - results = ( - db.query(GroupMember.user_id, Group) + result = await db.execute( + select(GroupMember.user_id, Group) .join(Group, Group.id == GroupMember.group_id) .filter(GroupMember.user_id.in_(user_ids)) .order_by(Group.updated_at.desc()) - .all() ) + rows = result.all() # Group groups by user_id user_groups: dict[str, list[GroupModel]] = {uid: [] for uid in user_ids} - for user_id, group in results: + for user_id, group in rows: user_groups[user_id].append(GroupModel.model_validate(group)) return user_groups - def get_group_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[GroupModel]: + async def get_group_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[GroupModel]: try: - with get_db_context(db) as db: - group = db.query(Group).filter_by(id=id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter_by(id=id)) + group = result.scalars().first() return GroupModel.model_validate(group) if group else None except Exception: return None - def get_group_user_ids_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[list[str]]: - with get_db_context(db) as db: - members = ( - db.query(GroupMember.user_id).filter(GroupMember.group_id == id).all() - ) + async def get_group_user_ids_by_id(self, id: str, db: Optional[AsyncSession] = None) -> list[str]: + async with get_async_db_context(db) as db: + result = await db.execute(select(GroupMember.user_id).filter(GroupMember.group_id == id)) + members = result.all() if not members: - return None + return [] return [m[0] for m in members] - def get_group_user_ids_by_ids( - self, group_ids: list[str], db: Optional[Session] = None + async def get_group_user_ids_by_ids( + self, group_ids: list[str], db: Optional[AsyncSession] = None ) -> dict[str, list[str]]: - with get_db_context(db) as db: - members = ( - db.query(GroupMember.group_id, GroupMember.user_id) - .filter(GroupMember.group_id.in_(group_ids)) - .all() + async with get_async_db_context(db) as db: + result = await db.execute( + select(GroupMember.group_id, GroupMember.user_id).filter(GroupMember.group_id.in_(group_ids)) ) + members = result.all() - group_user_ids: dict[str, list[str]] = { - group_id: [] for group_id in group_ids - } + group_user_ids: dict[str, list[str]] = {group_id: [] for group_id in group_ids} for group_id, user_id in members: group_user_ids[group_id].append(user_id) return group_user_ids - def set_group_user_ids_by_id( - self, group_id: str, user_ids: list[str], db: Optional[Session] = None + async def set_group_user_ids_by_id( + self, group_id: str, user_ids: list[str], db: Optional[AsyncSession] = None ) -> None: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Delete existing members - db.query(GroupMember).filter(GroupMember.group_id == group_id).delete() + await db.execute(delete(GroupMember).filter(GroupMember.group_id == group_id)) # Insert new members now = int(time.time()) @@ -355,142 +387,149 @@ class GroupTable: ] db.add_all(new_members) - db.commit() + await db.commit() - def get_group_member_count_by_id( - self, id: str, db: Optional[Session] = None - ) -> int: - with get_db_context(db) as db: - count = ( - db.query(func.count(GroupMember.user_id)) - .filter(GroupMember.group_id == id) - .scalar() - ) + async def get_group_member_count_by_id(self, id: str, db: Optional[AsyncSession] = None) -> int: + async with get_async_db_context(db) as db: + result = await db.execute(select(func.count(GroupMember.user_id)).filter(GroupMember.group_id == id)) + count = result.scalar() return count if count else 0 - def update_group_by_id( + async def get_group_member_counts_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> dict[str, int]: + if not ids: + return {} + async with get_async_db_context(db) as db: + result = await db.execute( + select(GroupMember.group_id, func.count(GroupMember.user_id)) + .filter(GroupMember.group_id.in_(ids)) + .group_by(GroupMember.group_id) + ) + rows = result.all() + return {group_id: count for group_id, count in rows} + + async def update_group_by_id( self, id: str, form_data: GroupUpdateForm, overwrite: bool = False, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[GroupModel]: try: - with get_db_context(db) as db: - db.query(Group).filter_by(id=id).update( - { + async with get_async_db_context(db) as db: + await db.execute( + update(Group) + .filter_by(id=id) + .values( **form_data.model_dump(exclude_none=True), - "updated_at": int(time.time()), - } + updated_at=int(time.time()), + ) ) - db.commit() - return self.get_group_by_id(id=id, db=db) + await db.commit() + return await self.get_group_by_id(id=id, db=db) except Exception as e: log.exception(e) return None - def delete_group_by_id(self, id: str, db: Optional[Session] = None) -> bool: + async def delete_group_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: try: - with get_db_context(db) as db: - db.query(Group).filter_by(id=id).delete() - db.commit() + async with get_async_db_context(db) as db: + await db.execute(delete(Group).filter_by(id=id)) + await db.commit() return True except Exception: return False - def delete_all_groups(self, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: + async def delete_all_groups(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: try: - db.query(Group).delete() - db.commit() + await db.execute(delete(Group)) + await db.commit() return True except Exception: return False - def remove_user_from_all_groups( - self, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: + async def remove_user_from_all_groups(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: try: # Find all groups the user belongs to - groups = ( - db.query(Group) + result = await db.execute( + select(Group) .join(GroupMember, GroupMember.group_id == Group.id) .filter(GroupMember.user_id == user_id) - .all() ) + groups = result.scalars().all() # Remove the user from each group for group in groups: - db.query(GroupMember).filter( - GroupMember.group_id == group.id, GroupMember.user_id == user_id - ).delete() - - db.query(Group).filter_by(id=group.id).update( - {"updated_at": int(time.time())} + await db.execute( + delete(GroupMember).filter(GroupMember.group_id == group.id, GroupMember.user_id == user_id) ) - db.commit() + await db.execute(update(Group).filter_by(id=group.id).values(updated_at=int(time.time()))) + + await db.commit() return True except Exception: - db.rollback() + await db.rollback() return False - def create_groups_by_group_names( - self, user_id: str, group_names: list[str], db: Optional[Session] = None + async def create_groups_by_group_names( + self, user_id: str, group_names: list[str], db: Optional[AsyncSession] = None ) -> list[GroupModel]: - # check for existing groups - existing_groups = self.get_all_groups(db=db) + existing_groups = await self.get_all_groups(db=db) existing_group_names = {group.name for group in existing_groups} new_groups = [] - with get_db_context(db) as db: + async with get_async_db_context(db) as db: for group_name in group_names: if group_name not in existing_group_names: new_group = GroupModel( id=str(uuid.uuid4()), user_id=user_id, name=group_name, - description="", + description='', + data={ + 'config': { + 'share': DEFAULT_GROUP_SHARE_PERMISSION, + } + }, created_at=int(time.time()), updated_at=int(time.time()), ) try: result = Group(**new_group.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) new_groups.append(GroupModel.model_validate(result)) except Exception as e: log.exception(e) continue return new_groups - def sync_groups_by_group_names( - self, user_id: str, group_names: list[str], db: Optional[Session] = None + async def sync_groups_by_group_names( + self, user_id: str, group_names: list[str], db: Optional[AsyncSession] = None ) -> bool: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: try: now = int(time.time()) # 1. Groups that SHOULD contain the user - target_groups = ( - db.query(Group).filter(Group.name.in_(group_names)).all() - ) + result = await db.execute(select(Group).filter(Group.name.in_(group_names))) + target_groups = result.scalars().all() target_group_ids = {g.id for g in target_groups} # 2. Groups the user is CURRENTLY in - existing_group_ids = { - g.id - for g in db.query(Group) + result = await db.execute( + select(Group) .join(GroupMember, GroupMember.group_id == Group.id) .filter(GroupMember.user_id == user_id) - .all() - } + ) + existing_group_ids = {g.id for g in result.scalars().all()} # 3. Determine adds + removals groups_to_add = target_group_ids - existing_group_ids @@ -498,15 +537,15 @@ class GroupTable: # 4. Remove in one bulk delete if groups_to_remove: - db.query(GroupMember).filter( - GroupMember.user_id == user_id, - GroupMember.group_id.in_(groups_to_remove), - ).delete(synchronize_session=False) - - db.query(Group).filter(Group.id.in_(groups_to_remove)).update( - {"updated_at": now}, synchronize_session=False + await db.execute( + delete(GroupMember).filter( + GroupMember.user_id == user_id, + GroupMember.group_id.in_(groups_to_remove), + ) ) + await db.execute(update(Group).filter(Group.id.in_(groups_to_remove)).values(updated_at=now)) + # 5. Bulk insert missing memberships for group_id in groups_to_add: db.add( @@ -520,27 +559,26 @@ class GroupTable: ) if groups_to_add: - db.query(Group).filter(Group.id.in_(groups_to_add)).update( - {"updated_at": now}, synchronize_session=False - ) + await db.execute(update(Group).filter(Group.id.in_(groups_to_add)).values(updated_at=now)) - db.commit() + await db.commit() return True except Exception as e: log.exception(e) - db.rollback() + await db.rollback() return False - def add_users_to_group( + async def add_users_to_group( self, id: str, user_ids: Optional[list[str]] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[GroupModel]: try: - with get_db_context(db) as db: - group = db.query(Group).filter_by(id=id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter_by(id=id)) + group = result.scalars().first() if not group: return None @@ -557,15 +595,14 @@ class GroupTable: updated_at=now, ) ) - db.flush() # Detect unique constraint violation early + await db.flush() # Detect unique constraint violation early except Exception: - db.rollback() # Clear failed INSERT - db.begin() # Start a new transaction + await db.rollback() # Clear failed INSERT continue # Duplicate → ignore group.updated_at = now - db.commit() - db.refresh(group) + await db.commit() + await db.refresh(group) return GroupModel.model_validate(group) @@ -573,32 +610,32 @@ class GroupTable: log.exception(e) return None - def remove_users_from_group( + async def remove_users_from_group( self, id: str, user_ids: Optional[list[str]] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[GroupModel]: try: - with get_db_context(db) as db: - group = db.query(Group).filter_by(id=id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Group).filter_by(id=id)) + group = result.scalars().first() if not group: return None if not user_ids: return GroupModel.model_validate(group) - # Remove each user from group_member - for user_id in user_ids: - db.query(GroupMember).filter( - GroupMember.group_id == id, GroupMember.user_id == user_id - ).delete() + # Remove users from group_member in batch + await db.execute( + delete(GroupMember).filter(GroupMember.group_id == id, GroupMember.user_id.in_(user_ids)) + ) # Update group timestamp group.updated_at = int(time.time()) - db.commit() - db.refresh(group) + await db.commit() + await db.refresh(group) return GroupModel.model_validate(group) except Exception as e: diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 7f99f828c7..84cf4b7ae8 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -1,47 +1,48 @@ import json import logging import time -from typing import Optional import uuid +from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context - +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.files import ( File, - FileModel, FileMetadataResponse, + FileModel, FileModelResponse, ) from open_webui.models.groups import Groups -from open_webui.models.users import User, UserModel, Users, UserResponse - - -from pydantic import BaseModel, ConfigDict +from open_webui.models.users import User, UserModel, UserResponse, Users +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import ( + JSON, BigInteger, Column, ForeignKey, + Index, String, Text, - JSON, UniqueConstraint, + delete, + func, or_, + select, + update, ) - -from open_webui.utils.access_control import has_access -from open_webui.utils.db.access_control import has_permission - +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) #################### # Knowledge DB Schema +# Let what was gathered here outlast the one who gathered it, +# and still teach when the builder is gone. #################### class Knowledge(Base): - __tablename__ = "knowledge" + __tablename__ = 'knowledge' id = Column(Text, unique=True, primary_key=True) user_id = Column(Text) @@ -50,27 +51,30 @@ class Knowledge(Base): description = Column(Text) meta = Column(JSON, nullable=True) - access_control = Column(JSON, nullable=True) # Controls data access levels. - # Defines access control rules for this entry. - # - `None`: Public access, available to all users with the "user" role. - # - `{}`: Private access, restricted exclusively to the owner. - # - Custom permissions: Specific access control for reading and writing; - # Can specify group or user-level restrictions: - # { - # "read": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # }, - # "write": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # } - # } created_at = Column(BigInteger) updated_at = Column(BigInteger) +class KnowledgeDirectory(Base): + __tablename__ = 'knowledge_directory' + + id = Column(Text, unique=True, primary_key=True) + knowledge_id = Column(Text, ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False) + parent_id = Column(Text, ForeignKey('knowledge_directory.id', ondelete='CASCADE'), nullable=True) + name = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + + created_at = Column(BigInteger, nullable=False) + updated_at = Column(BigInteger, nullable=False) + + __table_args__ = ( + UniqueConstraint('knowledge_id', 'parent_id', 'name', name='uq_knowledge_directory_knowledge_parent_name'), + Index('ix_knowledge_directory_knowledge_id', 'knowledge_id'), + Index('ix_knowledge_directory_parent_id', 'parent_id'), + ) + + class KnowledgeModel(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -82,30 +86,28 @@ class KnowledgeModel(BaseModel): meta: Optional[dict] = None - access_control: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) created_at: int # timestamp in epoch updated_at: int # timestamp in epoch class KnowledgeFile(Base): - __tablename__ = "knowledge_file" + __tablename__ = 'knowledge_file' id = Column(Text, unique=True, primary_key=True) - knowledge_id = Column( - Text, ForeignKey("knowledge.id", ondelete="CASCADE"), nullable=False - ) - file_id = Column(Text, ForeignKey("file.id", ondelete="CASCADE"), nullable=False) + knowledge_id = Column(Text, ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False) + file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + directory_id = Column(Text, ForeignKey('knowledge_directory.id', ondelete='SET NULL'), nullable=True) user_id = Column(Text, nullable=False) created_at = Column(BigInteger, nullable=False) updated_at = Column(BigInteger, nullable=False) __table_args__ = ( - UniqueConstraint( - "knowledge_id", "file_id", name="uq_knowledge_file_knowledge_file" - ), + UniqueConstraint('knowledge_id', 'file_id', name='uq_knowledge_file_knowledge_file'), + Index('ix_knowledge_file_directory_id', 'directory_id'), ) @@ -113,6 +115,7 @@ class KnowledgeFileModel(BaseModel): id: str knowledge_id: str file_id: str + directory_id: Optional[str] = None user_id: str created_at: int # timestamp in epoch @@ -121,6 +124,24 @@ class KnowledgeFileModel(BaseModel): model_config = ConfigDict(from_attributes=True) +class KnowledgeDirectoryModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + knowledge_id: str + parent_id: Optional[str] = None + name: str + user_id: str + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +class KnowledgeDirectoryForm(BaseModel): + name: str + parent_id: Optional[str] = None + + #################### # Forms #################### @@ -139,7 +160,7 @@ class KnowledgeUserResponse(KnowledgeUserModel): class KnowledgeForm(BaseModel): name: str description: str - access_control: Optional[dict] = None + access_grants: Optional[list[dict]] = None class FileUserResponse(FileModelResponse): @@ -153,47 +174,67 @@ class KnowledgeListResponse(BaseModel): class KnowledgeFileListResponse(BaseModel): items: list[FileUserResponse] + directories: list[KnowledgeDirectoryModel] = Field(default_factory=list) + breadcrumbs: list[KnowledgeDirectoryModel] = Field(default_factory=list) total: int class KnowledgeTable: - def insert_new_knowledge( - self, user_id: str, form_data: KnowledgeForm, db: Optional[Session] = None + async def _get_access_grants(self, knowledge_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('knowledge', knowledge_id, db=db) + + async def _to_knowledge_model( + self, + knowledge: Knowledge, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> KnowledgeModel: + knowledge_data = KnowledgeModel.model_validate(knowledge).model_dump(exclude={'access_grants'}) + knowledge_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(knowledge_data['id'], db=db) + ) + return KnowledgeModel.model_validate(knowledge_data) + + async def insert_new_knowledge( + self, user_id: str, form_data: KnowledgeForm, db: Optional[AsyncSession] = None ) -> Optional[KnowledgeModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: knowledge = KnowledgeModel( **{ - **form_data.model_dump(), - "id": str(uuid.uuid4()), - "user_id": user_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + **form_data.model_dump(exclude={'access_grants'}), + 'id': str(uuid.uuid4()), + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + 'access_grants': [], } ) try: - result = Knowledge(**knowledge.model_dump()) + result = Knowledge(**knowledge.model_dump(exclude={'access_grants'})) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('knowledge', result.id, form_data.access_grants, db=db) if result: - return KnowledgeModel.model_validate(result) + return await self._to_knowledge_model(result, db=db) else: return None except Exception: return None - def get_knowledge_bases( - self, skip: int = 0, limit: int = 30, db: Optional[Session] = None + async def get_knowledge_bases( + self, skip: int = 0, limit: int = 30, db: Optional[AsyncSession] = None ) -> list[KnowledgeUserModel]: - with get_db_context(db) as db: - all_knowledge = ( - db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(Knowledge).order_by(Knowledge.updated_at.desc())) + all_knowledge = result.scalars().all() user_ids = list(set(knowledge.user_id for knowledge in all_knowledge)) + knowledge_ids = [knowledge.id for knowledge in all_knowledge] - users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db) knowledge_bases = [] for knowledge in all_knowledge: @@ -201,68 +242,87 @@ class KnowledgeTable: knowledge_bases.append( KnowledgeUserModel.model_validate( { - **KnowledgeModel.model_validate(knowledge).model_dump(), - "user": user.model_dump() if user else None, + **( + await self._to_knowledge_model( + knowledge, + access_grants=grants_map.get(knowledge.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, } ) ) return knowledge_bases - def search_knowledge_bases( + async def search_knowledge_bases( self, user_id: str, filter: dict, skip: int = 0, limit: int = 30, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> KnowledgeListResponse: try: - with get_db_context(db) as db: - query = db.query(Knowledge, User).outerjoin( - User, User.id == Knowledge.user_id - ) + async with get_async_db_context(db) as db: + stmt = select(Knowledge, User).outerjoin(User, User.id == Knowledge.user_id) if filter: - query_key = filter.get("query") + query_key = filter.get('query') if query_key: - query = query.filter( + stmt = stmt.filter( or_( - Knowledge.name.ilike(f"%{query_key}%"), - Knowledge.description.ilike(f"%{query_key}%"), + Knowledge.name.ilike(f'%{query_key}%'), + Knowledge.description.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + User.username.ilike(f'%{query_key}%'), ) ) - view_option = filter.get("view_option") - if view_option == "created": - query = query.filter(Knowledge.user_id == user_id) - elif view_option == "shared": - query = query.filter(Knowledge.user_id != user_id) + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Knowledge.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Knowledge.user_id != user_id) - query = has_permission(db, Knowledge, query, filter) + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Knowledge, + filter=filter, + resource_type='knowledge', + permission='read', + ) - query = query.order_by(Knowledge.updated_at.desc()) + stmt = stmt.order_by(Knowledge.updated_at.desc(), Knowledge.id.asc()) - total = query.count() + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - items = query.all() + result = await db.execute(stmt) + items = result.all() + + knowledge_ids = [kb.id for kb, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db) knowledge_bases = [] for knowledge_base, user in items: knowledge_bases.append( KnowledgeUserModel.model_validate( { - **KnowledgeModel.model_validate( - knowledge_base + **( + await self._to_knowledge_model( + knowledge_base, + access_grants=grants_map.get(knowledge_base.id, []), + db=db, + ) ).model_dump(), - "user": ( - UserModel.model_validate(user).model_dump() - if user - else None - ), + 'user': (UserModel.model_validate(user).model_dump() if user else None), } ) ) @@ -272,275 +332,343 @@ class KnowledgeTable: print(e) return KnowledgeListResponse(items=[], total=0) - def search_knowledge_files( - self, filter: dict, skip: int = 0, limit: int = 30, db: Optional[Session] = None + async def search_knowledge_files( + self, filter: dict, skip: int = 0, limit: int = 30, db: Optional[AsyncSession] = None ) -> KnowledgeFileListResponse: """ Scalable version: search files across all knowledge bases the user has READ access to, without loading all KBs or using large IN() lists. """ try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Base query: join Knowledge → KnowledgeFile → File - query = ( - db.query(File, User, Knowledge) + stmt = ( + select(File, User, Knowledge) .join(KnowledgeFile, File.id == KnowledgeFile.file_id) .join(Knowledge, KnowledgeFile.knowledge_id == Knowledge.id) .outerjoin(User, User.id == KnowledgeFile.user_id) ) # Apply access-control directly to the joined query - # This makes the database handle filtering, even with 10k+ KBs - query = has_permission(db, Knowledge, query, filter) + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Knowledge, + filter=filter, + resource_type='knowledge', + permission='read', + ) - # Apply filename search + # Apply filename / content search + search_filter = None if filter: - q = filter.get("query") + q = filter.get('query') if q: - query = query.filter(File.filename.ilike(f"%{q}%")) + if filter.get('include_content'): + # Use ->> (as_string) instead of CAST(-> AS TEXT) + # to avoid PostgreSQL "invalid memory alloc request + # size" on large extracted-content rows (#24670). + content_text = File.data['content'].as_string() + search_filter = or_( + File.filename.ilike(f'%{q}%'), + content_text.ilike(f'%{q}%'), + ) + else: + search_filter = File.filename.ilike(f'%{q}%') + stmt = stmt.filter(search_filter) # Order by file changes - query = query.order_by(File.updated_at.desc()) + stmt = stmt.order_by(File.updated_at.desc(), File.id.asc()) - # Count before pagination - total = query.count() + # Lightweight count: avoid selecting File.data and ORDER BY + count_stmt = ( + select(func.count(File.id)) + .select_from(File) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .join(Knowledge, KnowledgeFile.knowledge_id == Knowledge.id) + ) + count_stmt = AccessGrants.has_permission_filter( + db=db, + query=count_stmt, + DocumentModel=Knowledge, + filter=filter, + resource_type='knowledge', + permission='read', + ) + if search_filter is not None: + count_stmt = count_stmt.filter(search_filter) + count_result = await db.execute(count_stmt) + total = count_result.scalar() if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - rows = query.all() + result = await db.execute(stmt) + rows = result.all() items = [] for file, user, knowledge in rows: items.append( FileUserResponse( **FileModel.model_validate(file).model_dump(), - user=( - UserResponse( - **UserModel.model_validate(user).model_dump() - ) - if user - else None - ), - collection=KnowledgeModel.model_validate( - knowledge - ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + collection=(await self._to_knowledge_model(knowledge, db=db)).model_dump(), ) ) return KnowledgeFileListResponse(items=items, total=total) except Exception as e: - print("search_knowledge_files error:", e) + print('search_knowledge_files error:', e) return KnowledgeFileListResponse(items=[], total=0) - def check_access_by_user_id( - self, id, user_id, permission="write", db: Optional[Session] = None - ) -> bool: - knowledge = self.get_knowledge_by_id(id, db=db) + async def check_access_by_user_id(self, id, user_id, permission='write', db: Optional[AsyncSession] = None) -> bool: + knowledge = await self.get_knowledge_by_id(id, db=db) if not knowledge: return False if knowledge.user_id == user_id: return True - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - } - return has_access(user_id, permission, knowledge.access_control, user_group_ids) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + return await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ) - def get_knowledge_bases_by_user_id( - self, user_id: str, permission: str = "write", db: Optional[Session] = None + async def get_knowledge_bases_by_user_id( + self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None ) -> list[KnowledgeUserModel]: - knowledge_bases = self.get_knowledge_bases(db=db) - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - } - return [ - knowledge_base - for knowledge_base in knowledge_bases - if knowledge_base.user_id == user_id - or has_access( - user_id, permission, knowledge_base.access_control, user_group_ids - ) - ] + knowledge_bases = await self.get_knowledge_bases(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} - def get_knowledge_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[KnowledgeModel]: + result = [] + for knowledge_base in knowledge_bases: + if knowledge_base.user_id == user_id: + result.append(knowledge_base) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(knowledge_base) + return result + + async def get_knowledge_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[KnowledgeModel]: try: - with get_db_context(db) as db: - knowledge = db.query(Knowledge).filter_by(id=id).first() - return KnowledgeModel.model_validate(knowledge) if knowledge else None + async with get_async_db_context(db) as db: + result = await db.execute(select(Knowledge).filter_by(id=id)) + knowledge = result.scalars().first() + return await self._to_knowledge_model(knowledge, db=db) if knowledge else None except Exception: return None - def get_knowledge_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None + async def get_knowledge_by_id_and_user_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[KnowledgeModel]: - knowledge = self.get_knowledge_by_id(id, db=db) + knowledge = await self.get_knowledge_by_id(id, db=db) if not knowledge: return None if knowledge.user_id == user_id: return knowledge - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - } - if has_access(user_id, "write", knowledge.access_control, user_group_ids): + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + if await AccessGrants.has_access( + user_id=user_id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + user_group_ids=user_group_ids, + db=db, + ): return knowledge return None - def get_knowledges_by_file_id( - self, file_id: str, db: Optional[Session] = None - ) -> list[KnowledgeModel]: + async def get_knowledges_by_file_id(self, file_id: str, db: Optional[AsyncSession] = None) -> list[KnowledgeModel]: try: - with get_db_context(db) as db: - knowledges = ( - db.query(Knowledge) + async with get_async_db_context(db) as db: + result = await db.execute( + select(Knowledge) .join(KnowledgeFile, Knowledge.id == KnowledgeFile.knowledge_id) .filter(KnowledgeFile.file_id == file_id) - .all() ) + knowledges = result.scalars().all() + knowledge_ids = [k.id for k in knowledges] + grants_map = await AccessGrants.get_grants_by_resources('knowledge', knowledge_ids, db=db) return [ - KnowledgeModel.model_validate(knowledge) for knowledge in knowledges + await self._to_knowledge_model( + knowledge, + access_grants=grants_map.get(knowledge.id, []), + db=db, + ) + for knowledge in knowledges ] except Exception: return [] - def search_files_by_id( + async def search_files_by_id( self, knowledge_id: str, user_id: str, filter: dict, skip: int = 0, limit: int = 30, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> KnowledgeFileListResponse: try: - with get_db_context(db) as db: - query = ( - db.query(File, User) + async with get_async_db_context(db) as db: + stmt = ( + select(File, User) .join(KnowledgeFile, File.id == KnowledgeFile.file_id) .outerjoin(User, User.id == KnowledgeFile.user_id) .filter(KnowledgeFile.knowledge_id == knowledge_id) ) + # Filter by directory_id (None = root level) + directory_id = filter.get('directory_id') if filter else None + if directory_id: + stmt = stmt.filter(KnowledgeFile.directory_id == directory_id) + elif filter and 'directory_id' in filter: + # Explicit None = root level only + stmt = stmt.filter(KnowledgeFile.directory_id.is_(None)) + + # Default sort: updated_at descending + primary_sort = File.updated_at.desc() + if filter: - query_key = filter.get("query") + query_key = filter.get('query') if query_key: - query = query.filter(or_(File.filename.ilike(f"%{query_key}%"))) - - view_option = filter.get("view_option") - if view_option == "created": - query = query.filter(KnowledgeFile.user_id == user_id) - elif view_option == "shared": - query = query.filter(KnowledgeFile.user_id != user_id) - - order_by = filter.get("order_by") - direction = filter.get("direction") - - if order_by == "name": - if direction == "asc": - query = query.order_by(File.filename.asc()) + if filter.get('include_content'): + # Use ->> (as_string) instead of CAST(-> AS TEXT) + # to avoid PostgreSQL memory allocation failures on + # large content (#24670). + content_text = File.data['content'].as_string() + stmt = stmt.filter( + or_( + File.filename.ilike(f'%{query_key}%'), + content_text.ilike(f'%{query_key}%'), + ) + ) else: - query = query.order_by(File.filename.desc()) - elif order_by == "created_at": - if direction == "asc": - query = query.order_by(File.created_at.asc()) - else: - query = query.order_by(File.created_at.desc()) - elif order_by == "updated_at": - if direction == "asc": - query = query.order_by(File.updated_at.asc()) - else: - query = query.order_by(File.updated_at.desc()) - else: - query = query.order_by(File.updated_at.desc()) + stmt = stmt.filter(File.filename.ilike(f'%{query_key}%')) - else: - query = query.order_by(File.updated_at.desc()) + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(KnowledgeFile.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(KnowledgeFile.user_id != user_id) + + order_by = filter.get('order_by') + direction = filter.get('direction') + is_asc = direction == 'asc' + + if order_by == 'name': + primary_sort = File.filename.asc() if is_asc else File.filename.desc() + elif order_by == 'created_at': + primary_sort = File.created_at.asc() if is_asc else File.created_at.desc() + elif order_by == 'updated_at': + primary_sort = File.updated_at.asc() if is_asc else File.updated_at.desc() + + # Apply sort with secondary key for deterministic pagination + stmt = stmt.order_by(primary_sort, File.id.asc()) # Count BEFORE pagination - total = query.count() + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - items = query.all() + result = await db.execute(stmt) + items = result.all() files = [] for file, user in items: files.append( FileUserResponse( **FileModel.model_validate(file).model_dump(), - user=( - UserResponse( - **UserModel.model_validate(user).model_dump() - ) - if user - else None - ), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), ) ) - return KnowledgeFileListResponse(items=files, total=total) + return KnowledgeFileListResponse( + items=files, + directories=await self.get_directories( + knowledge_id, + parent_id=filter.get('directory_id') if filter else None, + db=db, + ), + breadcrumbs=await self.get_directory_breadcrumbs( + filter.get('directory_id') if filter else None, + db=db, + ), + total=total, + ) except Exception as e: print(e) return KnowledgeFileListResponse(items=[], total=0) - def get_files_by_id( - self, knowledge_id: str, db: Optional[Session] = None - ) -> list[FileModel]: + async def get_files_by_id(self, knowledge_id: str, db: Optional[AsyncSession] = None) -> list[FileModel]: try: - with get_db_context(db) as db: - files = ( - db.query(File) + async with get_async_db_context(db) as db: + result = await db.execute( + select(File) .join(KnowledgeFile, File.id == KnowledgeFile.file_id) .filter(KnowledgeFile.knowledge_id == knowledge_id) - .all() ) + files = result.scalars().all() return [FileModel.model_validate(file) for file in files] except Exception: return [] - def get_file_metadatas_by_id( - self, knowledge_id: str, db: Optional[Session] = None + async def get_file_metadatas_by_id( + self, knowledge_id: str, db: Optional[AsyncSession] = None ) -> list[FileMetadataResponse]: try: - with get_db_context(db) as db: - files = self.get_files_by_id(knowledge_id, db=db) - return [FileMetadataResponse(**file.model_dump()) for file in files] + files = await self.get_files_by_id(knowledge_id, db=db) + return [FileMetadataResponse(**file.model_dump()) for file in files] except Exception: return [] - def add_file_to_knowledge_by_id( + async def add_file_to_knowledge_by_id( self, knowledge_id: str, file_id: str, user_id: str, - db: Optional[Session] = None, + directory_id: Optional[str] = None, + db: Optional[AsyncSession] = None, ) -> Optional[KnowledgeFileModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: knowledge_file = KnowledgeFileModel( **{ - "id": str(uuid.uuid4()), - "knowledge_id": knowledge_id, - "file_id": file_id, - "user_id": user_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), + 'id': str(uuid.uuid4()), + 'knowledge_id': knowledge_id, + 'file_id': file_id, + 'directory_id': directory_id, + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), } ) try: result = KnowledgeFile(**knowledge_file.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: return KnowledgeFileModel.model_validate(result) else: @@ -548,99 +676,390 @@ class KnowledgeTable: except Exception: return None - def remove_file_from_knowledge_by_id( - self, knowledge_id: str, file_id: str, db: Optional[Session] = None + async def has_file(self, knowledge_id: str, file_id: str, db: Optional[AsyncSession] = None) -> bool: + """Check whether a file belongs to a knowledge base.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(KnowledgeFile).filter_by(knowledge_id=knowledge_id, file_id=file_id).limit(1) + ) + return result.scalars().first() is not None + except Exception: + return False + + async def remove_file_from_knowledge_by_id( + self, knowledge_id: str, file_id: str, db: Optional[AsyncSession] = None ) -> bool: try: - with get_db_context(db) as db: - db.query(KnowledgeFile).filter_by( - knowledge_id=knowledge_id, file_id=file_id - ).delete() - db.commit() + async with get_async_db_context(db) as db: + await db.execute(delete(KnowledgeFile).filter_by(knowledge_id=knowledge_id, file_id=file_id)) + await db.commit() return True except Exception: return False - def reset_knowledge_by_id( - self, id: str, db: Optional[Session] = None + async def reset_knowledge_by_id( + self, id: str, include_directories: bool = True, db: Optional[AsyncSession] = None ) -> Optional[KnowledgeModel]: try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Delete all knowledge_file entries for this knowledge_id - db.query(KnowledgeFile).filter_by(knowledge_id=id).delete() - db.commit() + await db.execute(delete(KnowledgeFile).filter_by(knowledge_id=id)) + + # Delete all directories if requested + if include_directories: + await db.execute(delete(KnowledgeDirectory).filter_by(knowledge_id=id)) + + await db.commit() # Update the knowledge entry's updated_at timestamp - db.query(Knowledge).filter_by(id=id).update( - { - "updated_at": int(time.time()), - } - ) - db.commit() + await db.execute(update(Knowledge).filter_by(id=id).values(updated_at=int(time.time()))) + await db.commit() - return self.get_knowledge_by_id(id=id, db=db) + return await self.get_knowledge_by_id(id=id, db=db) except Exception as e: log.exception(e) return None - def update_knowledge_by_id( + async def update_knowledge_by_id( self, id: str, form_data: KnowledgeForm, overwrite: bool = False, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[KnowledgeModel]: try: - with get_db_context(db) as db: - knowledge = self.get_knowledge_by_id(id=id, db=db) - db.query(Knowledge).filter_by(id=id).update( - { - **form_data.model_dump(), - "updated_at": int(time.time()), - } + async with get_async_db_context(db) as db: + await db.execute( + update(Knowledge) + .filter_by(id=id) + .values( + **form_data.model_dump(exclude={'access_grants'}), + updated_at=int(time.time()), + ) ) - db.commit() - return self.get_knowledge_by_id(id=id, db=db) + await db.commit() + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) + return await self.get_knowledge_by_id(id=id, db=db) except Exception as e: log.exception(e) return None - def update_knowledge_data_by_id( - self, id: str, data: dict, db: Optional[Session] = None + async def update_knowledge_data_by_id( + self, id: str, data: dict, db: Optional[AsyncSession] = None ) -> Optional[KnowledgeModel]: try: - with get_db_context(db) as db: - knowledge = self.get_knowledge_by_id(id=id, db=db) - db.query(Knowledge).filter_by(id=id).update( - { - "data": data, - "updated_at": int(time.time()), - } + async with get_async_db_context(db) as db: + await db.execute( + update(Knowledge) + .filter_by(id=id) + .values( + data=data, + updated_at=int(time.time()), + ) ) - db.commit() - return self.get_knowledge_by_id(id=id, db=db) + await db.commit() + return await self.get_knowledge_by_id(id=id, db=db) except Exception as e: log.exception(e) return None - def delete_knowledge_by_id(self, id: str, db: Optional[Session] = None) -> bool: + async def delete_knowledge_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: try: - with get_db_context(db) as db: - db.query(Knowledge).filter_by(id=id).delete() - db.commit() + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('knowledge', id, db=db) + await db.execute(delete(Knowledge).filter_by(id=id)) + await db.commit() return True except Exception: return False - def delete_all_knowledge(self, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: + async def delete_all_knowledge(self, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: try: - db.query(Knowledge).delete() - db.commit() + result = await db.execute(select(Knowledge.id)) + knowledge_ids = [row[0] for row in result.all()] + for knowledge_id in knowledge_ids: + await AccessGrants.revoke_all_access('knowledge', knowledge_id, db=db) + await db.execute(delete(Knowledge)) + await db.commit() return True except Exception: return False + # ── Directory CRUD ──────────────────────────────────────────────── + + async def create_directory( + self, + knowledge_id: str, + name: str, + user_id: str, + parent_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + async with get_async_db_context(db) as db: + try: + now = int(time.time()) + directory = KnowledgeDirectory( + id=str(uuid.uuid4()), + knowledge_id=knowledge_id, + parent_id=parent_id, + name=name, + user_id=user_id, + created_at=now, + updated_at=now, + ) + db.add(directory) + await db.commit() + await db.refresh(directory) + return KnowledgeDirectoryModel.model_validate(directory) + except Exception as e: + log.exception(e) + return None + + async def get_directories( + self, + knowledge_id: str, + parent_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> list[KnowledgeDirectoryModel]: + """List directories at a given level (parent_id=None for root).""" + async with get_async_db_context(db) as db: + stmt = select(KnowledgeDirectory).filter(KnowledgeDirectory.knowledge_id == knowledge_id) + if parent_id: + stmt = stmt.filter(KnowledgeDirectory.parent_id == parent_id) + else: + stmt = stmt.filter(KnowledgeDirectory.parent_id.is_(None)) + + stmt = stmt.order_by(KnowledgeDirectory.name.asc()) + result = await db.execute(stmt) + return [KnowledgeDirectoryModel.model_validate(d) for d in result.scalars().all()] + + async def get_all_directories( + self, + knowledge_id: str, + db: Optional[AsyncSession] = None, + ) -> list[KnowledgeDirectoryModel]: + """Get ALL directories for a KB (no parent filter). Used for tree building.""" + async with get_async_db_context(db) as db: + stmt = ( + select(KnowledgeDirectory) + .filter(KnowledgeDirectory.knowledge_id == knowledge_id) + .order_by(KnowledgeDirectory.name.asc()) + ) + result = await db.execute(stmt) + return [KnowledgeDirectoryModel.model_validate(d) for d in result.scalars().all()] + + async def get_files_with_directory_ids( + self, + knowledge_id: str, + db: Optional[AsyncSession] = None, + ) -> list[tuple[FileModel, Optional[str]]]: + """Get all files in a KB with their directory_id from KnowledgeFile.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(File, KnowledgeFile.directory_id) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + ) + return [(FileModel.model_validate(file), dir_id) for file, dir_id in result.all()] + except Exception: + return [] + + async def get_directory_by_id( + self, directory_id: str, db: Optional[AsyncSession] = None + ) -> Optional[KnowledgeDirectoryModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(KnowledgeDirectory).filter_by(id=directory_id)) + directory = result.scalars().first() + return KnowledgeDirectoryModel.model_validate(directory) if directory else None + + async def get_directory_breadcrumbs( + self, + directory_id: Optional[str], + db: Optional[AsyncSession] = None, + ) -> list[KnowledgeDirectoryModel]: + """Walk up the parent chain to build breadcrumbs (root first).""" + if not directory_id: + return [] + + async with get_async_db_context(db) as db: + breadcrumbs = [] + current_id = directory_id + seen = set() + + while current_id and current_id not in seen: + seen.add(current_id) + result = await db.execute(select(KnowledgeDirectory).filter_by(id=current_id)) + directory = result.scalars().first() + if not directory: + break + breadcrumbs.append(KnowledgeDirectoryModel.model_validate(directory)) + current_id = directory.parent_id + + breadcrumbs.reverse() # root first + return breadcrumbs + + async def rename_directory( + self, + directory_id: str, + name: str, + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + async with get_async_db_context(db) as db: + try: + await db.execute( + update(KnowledgeDirectory).filter_by(id=directory_id).values(name=name, updated_at=int(time.time())) + ) + await db.commit() + return await self.get_directory_by_id(directory_id, db=db) + except Exception as e: + log.exception(e) + return None + + async def move_directory( + self, + directory_id: str, + new_parent_id: Optional[str], + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + """Move a directory to a new parent, with cycle detection.""" + async with get_async_db_context(db) as db: + try: + # Cycle detection: walk up from new_parent_id to ensure + # we don't encounter directory_id + if new_parent_id: + current = new_parent_id + seen = set() + while current and current not in seen: + if current == directory_id: + return None # Would create a cycle + seen.add(current) + result = await db.execute(select(KnowledgeDirectory.parent_id).filter_by(id=current)) + row = result.first() + current = row[0] if row else None + + await db.execute( + update(KnowledgeDirectory) + .filter_by(id=directory_id) + .values(parent_id=new_parent_id, updated_at=int(time.time())) + ) + await db.commit() + return await self.get_directory_by_id(directory_id, db=db) + except Exception as e: + log.exception(e) + return None + + async def update_directory( + self, + directory_id: str, + name: Optional[str] = None, + parent_id: Optional[str] = '__unset__', + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + """Update directory name and/or parent. Pass parent_id=None to move to root.""" + # Handle move if parent_id is being changed + if parent_id != '__unset__': + result = await self.move_directory(directory_id, parent_id, db=db) + if result is None: + return None # Cycle detected or error + + if name is not None: + return await self.rename_directory(directory_id, name, db=db) + + return await self.get_directory_by_id(directory_id, db=db) + + async def delete_directory( + self, + directory_id: str, + move_files_to_parent: bool = True, + db: Optional[AsyncSession] = None, + ) -> bool: + """ + Delete a directory. + - If move_files_to_parent=True: files move to parent dir (or root) + - If move_files_to_parent=False: files are also deleted + """ + async with get_async_db_context(db) as db: + try: + # Get the directory to find its parent + result = await db.execute(select(KnowledgeDirectory).filter_by(id=directory_id)) + directory = result.scalars().first() + if not directory: + return False + + parent_id = directory.parent_id + + if move_files_to_parent: + # Move files in this directory to its parent (or root) + await db.execute( + update(KnowledgeFile).filter_by(directory_id=directory_id).values(directory_id=parent_id) + ) + # Recursively move files from all subdirectories too + await self._move_files_from_subtree(directory_id, parent_id, db=db) + else: + # Delete files in this directory and all subdirectories + await self._delete_files_in_subtree(directory_id, db=db) + + # CASCADE on parent_id will handle deleting subdirectories + await db.execute(delete(KnowledgeDirectory).filter_by(id=directory_id)) + await db.commit() + return True + except Exception as e: + log.exception(e) + return False + + async def _move_files_from_subtree( + self, + directory_id: str, + target_directory_id: Optional[str], + db: AsyncSession, + ) -> None: + """Recursively move all files from a directory subtree to the target.""" + result = await db.execute(select(KnowledgeDirectory.id).filter_by(parent_id=directory_id)) + child_ids = [row[0] for row in result.all()] + + for child_id in child_ids: + await db.execute( + update(KnowledgeFile).filter_by(directory_id=child_id).values(directory_id=target_directory_id) + ) + await self._move_files_from_subtree(child_id, target_directory_id, db=db) + + async def _delete_files_in_subtree( + self, + directory_id: str, + db: AsyncSession, + ) -> None: + """Recursively delete all files from a directory subtree.""" + await db.execute(delete(KnowledgeFile).filter_by(directory_id=directory_id)) + result = await db.execute(select(KnowledgeDirectory.id).filter_by(parent_id=directory_id)) + child_ids = [row[0] for row in result.all()] + for child_id in child_ids: + await self._delete_files_in_subtree(child_id, db=db) + + async def move_file_to_directory( + self, + knowledge_id: str, + file_id: str, + directory_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> bool: + """Move a file to a different directory within the same KB.""" + async with get_async_db_context(db) as db: + try: + await db.execute( + update(KnowledgeFile) + .filter_by(knowledge_id=knowledge_id, file_id=file_id) + .values(directory_id=directory_id, updated_at=int(time.time())) + ) + await db.commit() + return True + except Exception as e: + log.exception(e) + return False + Knowledges = KnowledgeTable() diff --git a/backend/open_webui/models/memories.py b/backend/open_webui/models/memories.py index 2dc9656856..668d8c005f 100644 --- a/backend/open_webui/models/memories.py +++ b/backend/open_webui/models/memories.py @@ -1,158 +1,142 @@ +"""Long-term memory storage for per-user context recall.""" + +from __future__ import annotations + import time import uuid from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, get_db, get_db_context +from open_webui.internal.db import Base, get_async_db_context from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text - -#################### -# Memory DB Schema -#################### +from sqlalchemy import BigInteger, Column, String, Text, delete, select +from sqlalchemy.ext.asyncio import AsyncSession -class Memory(Base): - __tablename__ = "memory" +class Memory(Base): # user memory store + """Stores user-created memory entries linked to a vector collection.""" + + __tablename__ = 'memory' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) - content = Column(Text) - updated_at = Column(BigInteger) - created_at = Column(BigInteger) + user_id = Column(String, index=True) + content = Column(Text) # free-form text learned from conversation + updated_at = Column(BigInteger) # epoch seconds + created_at = Column(BigInteger) # epoch seconds class MemoryModel(BaseModel): + """Pydantic mirror of the Memory table row.""" + id: str user_id: str content: str updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - - model_config = ConfigDict(from_attributes=True) - - -#################### -# Forms -#################### - + model_config = ConfigDict(from_attributes=True) # allows ORM mapping class MemoriesTable: - def insert_new_memory( + async def insert_new_memory( self, user_id: str, content: str, - db: Optional[Session] = None, - ) -> Optional[MemoryModel]: - with get_db_context(db) as db: - id = str(uuid.uuid4()) - - memory = MemoryModel( - **{ - "id": id, - "user_id": user_id, - "content": content, - "created_at": int(time.time()), - "updated_at": int(time.time()), - } + db: AsyncSession | None = None, + ) -> MemoryModel | None: + """Persist a new memory entry and return the created model.""" + async with get_async_db_context(db) as db: + now = int(time.time()) + record = Memory( + id=str(uuid.uuid4()), + user_id=user_id, + content=content, + created_at=now, + updated_at=now, ) - result = Memory(**memory.model_dump()) - db.add(result) - db.commit() - db.refresh(result) - if result: - return MemoryModel.model_validate(result) - else: - return None + db.add(record) + await db.commit() + await db.refresh(record) + return MemoryModel.model_validate(record) if record else None - def update_memory_by_id_and_user_id( + async def update_memory_by_id_and_user_id( self, id: str, user_id: str, content: str, - db: Optional[Session] = None, - ) -> Optional[MemoryModel]: - with get_db_context(db) as db: + db: AsyncSession | None = None, + ) -> MemoryModel | None: + async with get_async_db_context(db) as db: try: - memory = db.get(Memory, id) + memory = await db.get(Memory, id) if not memory or memory.user_id != user_id: return None memory.content = content memory.updated_at = int(time.time()) - db.commit() - return self.get_memory_by_id(id) - except Exception: - return None - - def get_memories(self, db: Optional[Session] = None) -> list[MemoryModel]: - with get_db_context(db) as db: - try: - memories = db.query(Memory).all() - return [MemoryModel.model_validate(memory) for memory in memories] - except Exception: - return None - - def get_memories_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[MemoryModel]: - with get_db_context(db) as db: - try: - memories = db.query(Memory).filter_by(user_id=user_id).all() - return [MemoryModel.model_validate(memory) for memory in memories] - except Exception: - return None - - def get_memory_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[MemoryModel]: - with get_db_context(db) as db: - try: - memory = db.get(Memory, id) + await db.commit() + await db.refresh(memory) return MemoryModel.model_validate(memory) except Exception: return None - def delete_memory_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: + async def get_memories(self, db: AsyncSession | None = None) -> list[MemoryModel]: + async with get_async_db_context(db) as db: try: - db.query(Memory).filter_by(id=id).delete() - db.commit() + result = await db.execute(select(Memory)) + memories = result.scalars().all() + return [MemoryModel.model_validate(memory) for memory in memories] + except Exception: + return None + + async def get_memories_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[MemoryModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Memory).filter_by(user_id=user_id)) + memories = result.scalars().all() + return [MemoryModel.model_validate(memory) for memory in memories] + except Exception: + return None + + async def get_memory_by_id(self, id: str, db: AsyncSession | None = None) -> MemoryModel | None: + async with get_async_db_context(db) as db: + try: + memory = await db.get(Memory, id) + return MemoryModel.model_validate(memory) if memory else None + except Exception: + return None + + async def delete_memory_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as db: + try: + await db.execute(delete(Memory).filter_by(id=id)) + await db.commit() return True except Exception: return False - def delete_memories_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: + async def delete_memories_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as db: try: - db.query(Memory).filter_by(user_id=user_id).delete() - db.commit() + await db.execute(delete(Memory).filter_by(user_id=user_id)) + await db.commit() return True except Exception: return False - def delete_memory_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> bool: - with get_db_context(db) as db: + async def delete_memory_by_id_and_user_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as db: try: - memory = db.get(Memory, id) + memory = await db.get(Memory, id) if not memory or memory.user_id != user_id: - return None - - # Delete the memory - db.delete(memory) - db.commit() + return False + await db.delete(memory) + await db.commit() return True except Exception: return False -Memories = MemoriesTable() +Memories = MemoriesTable() # user memory registry diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index 0851107b0b..342abed2f8 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -3,16 +3,13 @@ import time import uuid from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from open_webui.models.tags import TagModel, Tag, Tags -from open_webui.models.users import Users, User, UserNameResponse -from open_webui.models.channels import Channels, ChannelMember - - +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.channels import ChannelMember, Channels +from open_webui.models.tags import Tag, TagModel, Tags +from open_webui.models.users import User, UserNameResponse, Users from pydantic import BaseModel, ConfigDict, field_validator -from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON -from sqlalchemy import or_, func, select, and_, text +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, and_, delete, func, or_, select, text +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql import exists #################### @@ -21,7 +18,7 @@ from sqlalchemy.sql import exists class MessageReaction(Base): - __tablename__ = "message_reaction" + __tablename__ = 'message_reaction' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text) message_id = Column(Text) @@ -40,7 +37,7 @@ class MessageReactionModel(BaseModel): class Message(Base): - __tablename__ = "message" + __tablename__ = 'message' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text) @@ -112,7 +109,7 @@ class MessageUserResponse(MessageModel): class MessageUserSlimResponse(MessageUserResponse): data: bool | None = None - @field_validator("data", mode="before") + @field_validator('data', mode='before') def convert_data_to_bool(cls, v): # No data or not a dict → False if not isinstance(v, dict): @@ -137,248 +134,211 @@ class MessageResponse(MessageReplyToResponse): class MessageTable: - def insert_new_message( + async def insert_new_message( self, form_data: MessageForm, channel_id: str, user_id: str, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[MessageModel]: - with get_db_context(db) as db: - channel_member = Channels.join_channel(channel_id, user_id) + async with get_async_db_context(db) as db: + channel_member = await Channels.join_channel(channel_id, user_id) id = str(uuid.uuid4()) ts = int(time.time_ns()) message = MessageModel( **{ - "id": id, - "user_id": user_id, - "channel_id": channel_id, - "reply_to_id": form_data.reply_to_id, - "parent_id": form_data.parent_id, - "is_pinned": False, - "pinned_at": None, - "pinned_by": None, - "content": form_data.content, - "data": form_data.data, - "meta": form_data.meta, - "created_at": ts, - "updated_at": ts, + 'id': id, + 'user_id': user_id, + 'channel_id': channel_id, + 'reply_to_id': form_data.reply_to_id, + 'parent_id': form_data.parent_id, + 'is_pinned': False, + 'pinned_at': None, + 'pinned_by': None, + 'content': form_data.content, + 'data': form_data.data, + 'meta': form_data.meta, + 'created_at': ts, + 'updated_at': ts, } ) result = Message(**message.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) return MessageModel.model_validate(result) if result else None - def get_message_by_id( + async def get_message_by_id( self, id: str, include_thread_replies: Optional[bool] = True, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[MessageResponse]: - with get_db_context(db) as db: - message = db.get(Message, id) + async with get_async_db_context(db) as db: + message = await db.get(Message, id) if not message: return None reply_to_message = ( - self.get_message_by_id( - message.reply_to_id, include_thread_replies=False, db=db - ) + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) if message.reply_to_id else None ) - reactions = self.get_reactions_by_message_id(id, db=db) + reactions = await self.get_reactions_by_message_id(id, db=db) thread_replies = [] if include_thread_replies: - thread_replies = self.get_thread_replies_by_message_id(id, db=db) + thread_replies = await self.get_thread_replies_by_message_id(id, db=db) # Check if message was sent by webhook (webhook info in meta takes precedence) - webhook_info = message.meta.get("webhook") if message.meta else None - if webhook_info and webhook_info.get("id"): + webhook_info = message.meta.get('webhook') if message.meta else None + if webhook_info and webhook_info.get('id'): # Look up webhook by ID to get current name - webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) + webhook = await Channels.get_webhook_by_id(webhook_info.get('id'), db=db) if webhook: user_info = { - "id": webhook.id, - "name": webhook.name, - "role": "webhook", + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', } else: # Webhook was deleted, use placeholder user_info = { - "id": webhook_info.get("id"), - "name": "Deleted Webhook", - "role": "webhook", + 'id': webhook_info.get('id'), + 'name': 'Deleted Webhook', + 'role': 'webhook', } else: - user = Users.get_user_by_id(message.user_id, db=db) + user = await Users.get_user_by_id(message.user_id, db=db) user_info = user.model_dump() if user else None return MessageResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), - "user": user_info, - "reply_to_message": ( - reply_to_message.model_dump() if reply_to_message else None - ), - "latest_reply_at": ( - thread_replies[0].created_at if thread_replies else None - ), - "reply_count": len(thread_replies), - "reactions": reactions, + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), + 'latest_reply_at': (thread_replies[0].created_at if thread_replies else None), + 'reply_count': len(thread_replies), + 'reactions': reactions, } ) - def get_thread_replies_by_message_id( - self, id: str, db: Optional[Session] = None + async def _resolve_user_info(self, message: Message, db: AsyncSession) -> Optional[dict]: + """Resolve user info from message, handling webhook messages.""" + webhook_info = message.meta.get('webhook') if message.meta else None + if webhook_info and webhook_info.get('id'): + webhook = await Channels.get_webhook_by_id(webhook_info.get('id'), db=db) + if webhook: + return { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', + } + else: + return { + 'id': webhook_info.get('id'), + 'name': 'Deleted Webhook', + 'role': 'webhook', + } + return None + + async def get_thread_replies_by_message_id( + self, id: str, db: Optional[AsyncSession] = None ) -> list[MessageReplyToResponse]: - with get_db_context(db) as db: - all_messages = ( - db.query(Message) - .filter_by(parent_id=id) - .order_by(Message.created_at.desc()) - .all() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(Message).filter_by(parent_id=id).order_by(Message.created_at.desc())) + all_messages = result.scalars().all() messages = [] for message in all_messages: reply_to_message = ( - self.get_message_by_id( - message.reply_to_id, include_thread_replies=False, db=db - ) + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) if message.reply_to_id else None ) - webhook_info = message.meta.get("webhook") if message.meta else None - user_info = None - if webhook_info and webhook_info.get("id"): - webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) - if webhook: - user_info = { - "id": webhook.id, - "name": webhook.name, - "role": "webhook", - } - else: - user_info = { - "id": webhook_info.get("id"), - "name": "Deleted Webhook", - "role": "webhook", - } + user_info = await self._resolve_user_info(message, db) messages.append( MessageReplyToResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), - "user": user_info, - "reply_to_message": ( - reply_to_message.model_dump() - if reply_to_message - else None - ), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), } ) ) return messages - def get_reply_user_ids_by_message_id( - self, id: str, db: Optional[Session] = None - ) -> list[str]: - with get_db_context(db) as db: - return [ - message.user_id - for message in db.query(Message).filter_by(parent_id=id).all() - ] + async def get_reply_user_ids_by_message_id(self, id: str, db: Optional[AsyncSession] = None) -> list[str]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Message.user_id).filter_by(parent_id=id)) + return [row[0] for row in result.all()] - def get_messages_by_channel_id( + async def get_messages_by_channel_id( self, channel_id: str, skip: int = 0, limit: int = 50, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> list[MessageReplyToResponse]: - with get_db_context(db) as db: - all_messages = ( - db.query(Message) + async with get_async_db_context(db) as db: + result = await db.execute( + select(Message) .filter_by(channel_id=channel_id, parent_id=None) .order_by(Message.created_at.desc()) .offset(skip) .limit(limit) - .all() ) + all_messages = result.scalars().all() messages = [] for message in all_messages: reply_to_message = ( - self.get_message_by_id( - message.reply_to_id, include_thread_replies=False, db=db - ) + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) if message.reply_to_id else None ) - webhook_info = message.meta.get("webhook") if message.meta else None - user_info = None - if webhook_info and webhook_info.get("id"): - webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) - if webhook: - user_info = { - "id": webhook.id, - "name": webhook.name, - "role": "webhook", - } - else: - user_info = { - "id": webhook_info.get("id"), - "name": "Deleted Webhook", - "role": "webhook", - } + user_info = await self._resolve_user_info(message, db) messages.append( MessageReplyToResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), - "user": user_info, - "reply_to_message": ( - reply_to_message.model_dump() - if reply_to_message - else None - ), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), } ) ) return messages - def get_messages_by_parent_id( + async def get_messages_by_parent_id( self, channel_id: str, parent_id: str, skip: int = 0, limit: int = 50, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> list[MessageReplyToResponse]: - with get_db_context(db) as db: - message = db.get(Message, parent_id) + async with get_async_db_context(db) as db: + message = await db.get(Message, parent_id) if not message: return [] - all_messages = ( - db.query(Message) + result = await db.execute( + select(Message) .filter_by(channel_id=channel_id, parent_id=parent_id) .order_by(Message.created_at.desc()) .offset(skip) .limit(limit) - .all() ) + all_messages = list(result.scalars().all()) # If length of all_messages is less than limit, then add the parent message if len(all_messages) < limit: @@ -387,80 +347,57 @@ class MessageTable: messages = [] for message in all_messages: reply_to_message = ( - self.get_message_by_id( - message.reply_to_id, include_thread_replies=False, db=db - ) + await self.get_message_by_id(message.reply_to_id, include_thread_replies=False, db=db) if message.reply_to_id else None ) - webhook_info = message.meta.get("webhook") if message.meta else None - user_info = None - if webhook_info and webhook_info.get("id"): - webhook = Channels.get_webhook_by_id(webhook_info.get("id"), db=db) - if webhook: - user_info = { - "id": webhook.id, - "name": webhook.name, - "role": "webhook", - } - else: - user_info = { - "id": webhook_info.get("id"), - "name": "Deleted Webhook", - "role": "webhook", - } + user_info = await self._resolve_user_info(message, db) messages.append( MessageReplyToResponse.model_validate( { **MessageModel.model_validate(message).model_dump(), - "user": user_info, - "reply_to_message": ( - reply_to_message.model_dump() - if reply_to_message - else None - ), + 'user': user_info, + 'reply_to_message': (reply_to_message.model_dump() if reply_to_message else None), } ) ) return messages - def get_last_message_by_channel_id( - self, channel_id: str, db: Optional[Session] = None + async def get_last_message_by_channel_id( + self, channel_id: str, db: Optional[AsyncSession] = None ) -> Optional[MessageModel]: - with get_db_context(db) as db: - message = ( - db.query(Message) - .filter_by(channel_id=channel_id) - .order_by(Message.created_at.desc()) - .first() + async with get_async_db_context(db) as db: + result = await db.execute( + select(Message).filter_by(channel_id=channel_id).order_by(Message.created_at.desc()).limit(1) ) + message = result.scalars().first() return MessageModel.model_validate(message) if message else None - def get_pinned_messages_by_channel_id( + async def get_pinned_messages_by_channel_id( self, channel_id: str, skip: int = 0, limit: int = 50, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> list[MessageModel]: - with get_db_context(db) as db: - all_messages = ( - db.query(Message) + async with get_async_db_context(db) as db: + result = await db.execute( + select(Message) .filter_by(channel_id=channel_id, is_pinned=True) .order_by(Message.pinned_at.desc()) .offset(skip) .limit(limit) - .all() ) + all_messages = result.scalars().all() return [MessageModel.model_validate(message) for message in all_messages] - def update_message_by_id( - self, id: str, form_data: MessageForm, db: Optional[Session] = None + async def update_message_by_id( + self, id: str, form_data: MessageForm, db: Optional[AsyncSession] = None ) -> Optional[MessageModel]: - with get_db_context(db) as db: - message = db.get(Message, id) + async with get_async_db_context(db) as db: + message = await db.get(Message, id) message.content = form_data.content message.data = { **(message.data if message.data else {}), @@ -471,53 +408,51 @@ class MessageTable: **(form_data.meta if form_data.meta else {}), } message.updated_at = int(time.time_ns()) - db.commit() - db.refresh(message) + await db.commit() + await db.refresh(message) return MessageModel.model_validate(message) if message else None - def update_is_pinned_by_id( + async def update_is_pinned_by_id( self, id: str, is_pinned: bool, pinned_by: Optional[str] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[MessageModel]: - with get_db_context(db) as db: - message = db.get(Message, id) + async with get_async_db_context(db) as db: + message = await db.get(Message, id) message.is_pinned = is_pinned message.pinned_at = int(time.time_ns()) if is_pinned else None message.pinned_by = pinned_by if is_pinned else None - db.commit() - db.refresh(message) + await db.commit() + await db.refresh(message) return MessageModel.model_validate(message) if message else None - def get_unread_message_count( + async def get_unread_message_count( self, channel_id: str, user_id: str, last_read_at: Optional[int] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> int: - with get_db_context(db) as db: - query = db.query(Message).filter( + async with get_async_db_context(db) as db: + stmt = select(func.count(Message.id)).filter( Message.channel_id == channel_id, Message.parent_id == None, # only count top-level messages Message.created_at > (last_read_at if last_read_at else 0), ) if user_id: - query = query.filter(Message.user_id != user_id) - return query.count() + stmt = stmt.filter(Message.user_id != user_id) + result = await db.execute(stmt) + return result.scalar() - def add_reaction_to_message( - self, id: str, user_id: str, name: str, db: Optional[Session] = None + async def add_reaction_to_message( + self, id: str, user_id: str, name: str, db: Optional[AsyncSession] = None ) -> Optional[MessageReactionModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # check for existing reaction - existing_reaction = ( - db.query(MessageReaction) - .filter_by(message_id=id, user_id=user_id, name=name) - .first() - ) + result = await db.execute(select(MessageReaction).filter_by(message_id=id, user_id=user_id, name=name)) + existing_reaction = result.scalars().first() if existing_reaction: return MessageReactionModel.model_validate(existing_reaction) @@ -531,102 +466,94 @@ class MessageTable: ) result = MessageReaction(**reaction.model_dump()) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) return MessageReactionModel.model_validate(result) if result else None - def get_reactions_by_message_id( - self, id: str, db: Optional[Session] = None - ) -> list[Reactions]: - with get_db_context(db) as db: + async def get_reactions_by_message_id(self, id: str, db: Optional[AsyncSession] = None) -> list[Reactions]: + async with get_async_db_context(db) as db: # JOIN User so all user info is fetched in one query - results = ( - db.query(MessageReaction, User) + result = await db.execute( + select(MessageReaction, User) .join(User, MessageReaction.user_id == User.id) .filter(MessageReaction.message_id == id) - .all() ) + results = result.all() reactions = {} for reaction, user in results: if reaction.name not in reactions: reactions[reaction.name] = { - "name": reaction.name, - "users": [], - "count": 0, + 'name': reaction.name, + 'users': [], + 'count': 0, } - reactions[reaction.name]["users"].append( + reactions[reaction.name]['users'].append( { - "id": user.id, - "name": user.name, + 'id': user.id, + 'name': user.name, } ) - reactions[reaction.name]["count"] += 1 + reactions[reaction.name]['count'] += 1 return [Reactions(**reaction) for reaction in reactions.values()] - def remove_reaction_by_id_and_user_id_and_name( - self, id: str, user_id: str, name: str, db: Optional[Session] = None + async def remove_reaction_by_id_and_user_id_and_name( + self, id: str, user_id: str, name: str, db: Optional[AsyncSession] = None ) -> bool: - with get_db_context(db) as db: - db.query(MessageReaction).filter_by( - message_id=id, user_id=user_id, name=name - ).delete() - db.commit() + async with get_async_db_context(db) as db: + await db.execute(delete(MessageReaction).filter_by(message_id=id, user_id=user_id, name=name)) + await db.commit() return True - def delete_reactions_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - db.query(MessageReaction).filter_by(message_id=id).delete() - db.commit() + async def delete_reactions_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(MessageReaction).filter_by(message_id=id)) + await db.commit() return True - def delete_replies_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - db.query(Message).filter_by(parent_id=id).delete() - db.commit() + async def delete_replies_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(Message).filter_by(parent_id=id)) + await db.commit() return True - def delete_message_by_id(self, id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - db.query(Message).filter_by(id=id).delete() + async def delete_message_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async with get_async_db_context(db) as db: + await db.execute(delete(Message).filter_by(id=id)) # Delete all reactions to this message - db.query(MessageReaction).filter_by(message_id=id).delete() + await db.execute(delete(MessageReaction).filter_by(message_id=id)) - db.commit() + await db.commit() return True - def search_messages_by_channel_ids( + async def search_messages_by_channel_ids( self, channel_ids: list[str], query: str, start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None, limit: int = 10, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> list[MessageModel]: """Search messages in specified channels by content.""" - with get_db_context(db) as db: - query_builder = db.query(Message).filter( + async with get_async_db_context(db) as db: + stmt = select(Message).filter( Message.channel_id.in_(channel_ids), - Message.content.ilike(f"%{query}%"), + Message.content.ilike(f'%{query}%'), ) if start_timestamp: - query_builder = query_builder.filter( - Message.created_at >= start_timestamp - ) + stmt = stmt.filter(Message.created_at >= start_timestamp) if end_timestamp: - query_builder = query_builder.filter( - Message.created_at <= end_timestamp - ) + stmt = stmt.filter(Message.created_at <= end_timestamp) - messages = ( - query_builder.order_by(Message.created_at.desc()).limit(limit).all() - ) + stmt = stmt.order_by(Message.created_at.desc()).limit(limit) + result = await db.execute(stmt) + messages = result.scalars().all() return [MessageModel.model_validate(msg) for msg in messages] diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 5457413f0d..fa8f05a377 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -1,137 +1,121 @@ +from __future__ import annotations + +import json import logging import time from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context - +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.groups import Groups -from open_webui.models.users import User, UserModel, Users, UserResponse - - -from pydantic import BaseModel, ConfigDict - -from sqlalchemy import String, cast, or_, and_, func -from sqlalchemy.dialects import postgresql, sqlite - +from open_webui.models.users import User, UserModel, UserResponse, Users +from open_webui.utils.validate import validate_profile_image_url +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from sqlalchemy import BigInteger, Boolean, Column, String, Text, cast, delete, func, or_, select, update from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy import BigInteger, Column, Text, JSON, Boolean - - -from open_webui.utils.access_control import has_access - +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) - -#################### -# Models DB Schema -#################### +# Track invalid profile_image_url values we've already warned about so we +# don't flood the logs on every DB read (the validator fires per-row). +_warned_profile_urls: set[str] = set() + + +# --- Models DB Schema --- -# ModelParams is a model for the data stored in the params field of the Model table class ModelParams(BaseModel): - model_config = ConfigDict(extra="allow") - pass + """Parameters for model inference (temperature, top_p, etc.).""" + + model_config = ConfigDict(extra='allow') -# ModelMeta is a model for the data stored in the meta field of the Model table class ModelMeta(BaseModel): - profile_image_url: Optional[str] = "/static/favicon.png" + """Metadata for a workspace model entry (profile, description, tags, capabilities).""" - description: Optional[str] = None - """ - User-facing description of the model. - """ + profile_image_url: str | None = None + description: str | None = Field(default=None, description='User-facing description of the model.') + capabilities: dict | None = None - capabilities: Optional[dict] = None + model_config = ConfigDict(extra='allow') - model_config = ConfigDict(extra="allow") + @field_validator('profile_image_url', mode='before') + @classmethod + def check_profile_image_url(cls, v: str | None) -> str | None: + if v is None: + return v + try: + return validate_profile_image_url(v) + except ValueError: + if v not in _warned_profile_urls: + _warned_profile_urls.add(v) + log.warning( + 'Clearing invalid profile_image_url stored in DB ' + '(likely a legacy SVG data-URI): %.80s…', + v, + ) + return None - pass + @model_validator(mode='before') + @classmethod + def normalize_tags(cls, data): + if isinstance(data, dict) and 'tags' in data: + raw_tags = data['tags'] + if isinstance(raw_tags, list): + normalized = [] + for tag in raw_tags: + if isinstance(tag, str): + normalized.append({'name': tag}) + elif isinstance(tag, dict) and 'name' in tag: + normalized.append(tag) + data['tags'] = normalized + return data class Model(Base): - __tablename__ = "model" - id = Column(Text, primary_key=True, unique=True) - """ - The model's id as used in the API. If set to an existing model, it will override the model. - """ - user_id = Column(Text) + """Workspace model entry — wraps an upstream LLM with custom params and metadata.""" - base_model_id = Column(Text, nullable=True) - """ - An optional pointer to the actual model that should be used when proxying requests. - """ + __tablename__ = 'model' - name = Column(Text) - """ - The human-readable display name of the model. - """ - - params = Column(JSONField) - """ - Holds a JSON encoded blob of parameters, see `ModelParams`. - """ - - meta = Column(JSONField) - """ - Holds a JSON encoded blob of metadata, see `ModelMeta`. - """ - - access_control = Column(JSON, nullable=True) # Controls data access levels. - # Defines access control rules for this entry. - # - `None`: Public access, available to all users with the "user" role. - # - `{}`: Private access, restricted exclusively to the owner. - # - Custom permissions: Specific access control for reading and writing; - # Can specify group or user-level restrictions: - # { - # "read": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # }, - # "write": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # } - # } - - is_active = Column(Boolean, default=True) - - updated_at = Column(BigInteger) - created_at = Column(BigInteger) + id = Column(Text, primary_key=True, unique=True) # API model identifier; overrides built-in when matching + user_id = Column(Text) # owner + base_model_id = Column(Text, nullable=True) # actual upstream model for proxied requests + name = Column(Text) # human-readable display name + params = Column(JSONField) # see ModelParams + meta = Column(JSONField) # see ModelMeta + is_active = Column(Boolean, default=True) # soft-disable toggle + updated_at = Column(BigInteger) # epoch seconds + created_at = Column(BigInteger) # epoch seconds class ModelModel(BaseModel): id: str user_id: str - base_model_id: Optional[str] = None + base_model_id: str | None = None name: str params: ModelParams meta: ModelMeta - access_control: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) is_active: bool updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) - - -#################### -# Forms -#################### + model_config = ConfigDict( + from_attributes=True, + ) class ModelUserResponse(ModelModel): - user: Optional[UserResponse] = None + user: UserResponse | None = None class ModelAccessResponse(ModelUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class ModelResponse(ModelModel): @@ -149,54 +133,86 @@ class ModelAccessListResponse(BaseModel): class ModelForm(BaseModel): + model_config = ConfigDict(extra='ignore') + id: str - base_model_id: Optional[str] = None + base_model_id: str | None = None name: str meta: ModelMeta params: ModelParams - access_control: Optional[dict] = None + access_grants: list[dict | None] = None is_active: bool = True class ModelsTable: - def insert_new_model( - self, form_data: ModelForm, user_id: str, db: Optional[Session] = None - ) -> Optional[ModelModel]: - model = ModelModel( - **{ - **form_data.model_dump(), - "user_id": user_id, - "created_at": int(time.time()), - "updated_at": int(time.time()), - } + async def _get_access_grants(self, model_id: str, db: AsyncSession | None = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('model', model_id, db=db) + + async def _to_model_model( + self, + model: Model, + access_grants: list[AccessGrantModel | None] = None, + db: AsyncSession | None = None, + ) -> ModelModel: + model_data = ModelModel.model_validate(model).model_dump(exclude={'access_grants'}) + model_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(model_data['id'], db=db) ) + return ModelModel.model_validate(model_data) + + async def insert_new_model( + self, form_data: ModelForm, user_id: str, db: AsyncSession | None = None + ) -> ModelModel | None: try: - with get_db_context(db) as db: - result = Model(**model.model_dump()) + async with get_async_db_context(db) as db: + result = Model( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'user_id': user_id, + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + } + ) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('model', result.id, form_data.access_grants, db=db) if result: - return ModelModel.model_validate(result) + return await self._to_model_model(result, db=db) else: return None except Exception as e: - log.exception(f"Failed to insert a new model: {e}") + log.exception(f'Failed to insert a new model: {e}') return None - def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]: - with get_db_context(db) as db: - return [ModelModel.model_validate(model) for model in db.query(Model).all()] + async def get_all_models(self, db: AsyncSession | None = None) -> list[ModelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model)) + all_models = result.scalars().all() + model_ids = [model.id for model in all_models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + models: list[ModelModel] = [] + for model in all_models: + try: + models.append( + await self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db) + ) + except Exception as exc: + log.error('Skipping model %r during get_all_models due to error: %s', model.id, exc) + return models - def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]: - with get_db_context(db) as db: - all_models = db.query(Model).filter(Model.base_model_id != None).all() + async def get_models(self, db: AsyncSession | None = None) -> list[ModelUserResponse]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter(Model.base_model_id != None)) + all_models = result.scalars().all() user_ids = list(set(model.user_id for model in all_models)) + model_ids = [model.id for model in all_models] - users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) models = [] for model in all_models: @@ -204,252 +220,326 @@ class ModelsTable: models.append( ModelUserResponse.model_validate( { - **ModelModel.model_validate(model).model_dump(), - "user": user.model_dump() if user else None, + **( + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, } ) ) return models - def get_base_models(self, db: Optional[Session] = None) -> list[ModelModel]: - with get_db_context(db) as db: + async def get_base_models(self, db: AsyncSession | None = None) -> list[ModelModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter(Model.base_model_id == None)) + all_models = result.scalars().all() + model_ids = [model.id for model in all_models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) return [ - ModelModel.model_validate(model) - for model in db.query(Model).filter(Model.base_model_id == None).all() + await self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db) + for model in all_models ] - def get_models_by_user_id( - self, user_id: str, permission: str = "write", db: Optional[Session] = None + async def get_models_by_user_id( + self, user_id: str, permission: str = 'write', db: AsyncSession | None = None ) -> list[ModelUserResponse]: - models = self.get_models(db=db) - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - } - return [ - model - for model in models - if model.user_id == user_id - or has_access(user_id, permission, model.access_control, user_group_ids) - ] + models = await self.get_models(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} - def _has_permission(self, db, query, filter: dict, permission: str = "read"): - group_ids = filter.get("group_ids", []) - user_id = filter.get("user_id") + result = [] + for model in models: + if model.user_id == user_id: + result.append(model) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='model', + resource_id=model.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(model) + return result - dialect_name = db.bind.dialect.name + def _has_permission(self, db, query, filter: dict, permission: str = 'read'): + return AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Model, + filter=filter, + resource_type='model', + permission=permission, + ) - # Public access - conditions = [] - if group_ids or user_id: - conditions.extend( - [ - Model.access_control.is_(None), - cast(Model.access_control, String) == "null", - ] - ) - - # User-level permission - if user_id: - conditions.append(Model.user_id == user_id) - - # Group-level permission - if group_ids: - group_conditions = [] - for gid in group_ids: - if dialect_name == "sqlite": - group_conditions.append( - Model.access_control[permission]["group_ids"].contains([gid]) - ) - elif dialect_name == "postgresql": - group_conditions.append( - cast( - Model.access_control[permission]["group_ids"], - JSONB, - ).contains([gid]) - ) - conditions.append(or_(*group_conditions)) - - if conditions: - query = query.filter(or_(*conditions)) - - return query - - def search_models( + async def search_models( self, user_id: str, filter: dict = {}, skip: int = 0, limit: int = 30, - db: Optional[Session] = None, + db: AsyncSession | None = None, ) -> ModelListResponse: - with get_db_context(db) as db: - # Join GroupMember so we can order by group_id when requested - query = db.query(Model, User).outerjoin(User, User.id == Model.user_id) - query = query.filter(Model.base_model_id != None) + async with get_async_db_context(db) as db: + stmt = select(Model, User).outerjoin(User, User.id == Model.user_id) + stmt = stmt.filter(Model.base_model_id != None) if filter: - query_key = filter.get("query") + query_key = filter.get('query') if query_key: - query = query.filter( + stmt = stmt.filter( or_( - Model.name.ilike(f"%{query_key}%"), - Model.base_model_id.ilike(f"%{query_key}%"), + Model.name.ilike(f'%{query_key}%'), + Model.base_model_id.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + User.username.ilike(f'%{query_key}%'), ) ) - view_option = filter.get("view_option") - if view_option == "created": - query = query.filter(Model.user_id == user_id) - elif view_option == "shared": - query = query.filter(Model.user_id != user_id) + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Model.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Model.user_id != user_id) # Apply access control filtering - query = self._has_permission( + stmt = self._has_permission( db, - query, + stmt, filter, - permission="read", + permission='read', ) - tag = filter.get("tag") + tag = filter.get('tag') if tag: - # TODO: This is a simple implementation and should be improved for performance - like_pattern = f'%"{tag.lower()}"%' # `"tag"` inside JSON array - meta_text = func.lower(cast(Model.meta, String)) - - query = query.filter(meta_text.like(like_pattern)) - - order_by = filter.get("order_by") - direction = filter.get("direction") - - if order_by == "name": - if direction == "asc": - query = query.order_by(Model.name.asc()) + # SQLite stores JSON text via json.dumps(ensure_ascii=True), + # so non-ASCII chars are \uXXXX-escaped. PostgreSQL native JSONB + # stores literal Unicode. Use the right pattern for each. + if db.bind.dialect.name == 'sqlite': + if tag.isascii(): + meta_text = func.lower(cast(Model.meta, String)) + pattern = f'%{json.dumps(tag.lower())}%' + else: + meta_text = cast(Model.meta, String) + pattern = f'%{json.dumps(tag)}%' else: - query = query.order_by(Model.name.desc()) - elif order_by == "created_at": - if direction == "asc": - query = query.order_by(Model.created_at.asc()) + meta_text = func.lower(cast(Model.meta, String)) + pattern = f'%{json.dumps(tag.lower(), ensure_ascii=False)}%' + stmt = stmt.filter(meta_text.like(pattern)) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'name': + if direction == 'asc': + stmt = stmt.order_by(Model.name.asc()) else: - query = query.order_by(Model.created_at.desc()) - elif order_by == "updated_at": - if direction == "asc": - query = query.order_by(Model.updated_at.asc()) + stmt = stmt.order_by(Model.name.desc()) + elif order_by == 'created_at': + if direction == 'asc': + stmt = stmt.order_by(Model.created_at.asc()) else: - query = query.order_by(Model.updated_at.desc()) + stmt = stmt.order_by(Model.created_at.desc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(Model.updated_at.asc()) + else: + stmt = stmt.order_by(Model.updated_at.desc()) else: - query = query.order_by(Model.created_at.desc()) + stmt = stmt.order_by(Model.created_at.desc()) # Count BEFORE pagination - total = query.count() + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - items = query.all() + result = await db.execute(stmt) + items = result.all() + + model_ids = [model.id for model, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) models = [] for model, user in items: models.append( ModelUserResponse( - **ModelModel.model_validate(model).model_dump(), - user=( - UserResponse(**UserModel.model_validate(user).model_dump()) - if user - else None - ), + **( + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), ) ) return ModelListResponse(items=models, total=total) - def get_model_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ModelModel]: + async def get_model_meta_by_id(self, id: str, db: AsyncSession | None = None) -> tuple[dict, int | None]: + """Return (meta, updated_at) for a model, skipping access grant resolution.""" try: - with get_db_context(db) as db: - model = db.get(Model, id) - return ModelModel.model_validate(model) + async with get_async_db_context(db) as db: + result = await db.execute(select(Model.meta, Model.updated_at).filter_by(id=id)) + return result.first() except Exception: return None - def get_models_by_ids( - self, ids: list[str], db: Optional[Session] = None - ) -> list[ModelModel]: + async def get_all_tags( + self, + user_id: str, + is_admin: bool = False, + db: AsyncSession | None = None, + ) -> set[str]: + """Extract unique tag names from model meta, querying only the meta column.""" + async with get_async_db_context(db) as db: + stmt = select(Model.meta).filter(Model.base_model_id != None) + + if not is_admin: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [group.id for group in user_groups] + + filter_dict = {'user_id': user_id} + if user_group_ids: + filter_dict['group_ids'] = user_group_ids + + stmt = self._has_permission(db, stmt, filter_dict, permission='read') + + result = await db.execute(stmt) + rows = result.scalars().all() + + tags_set: set[str] = set() + for meta in rows: + if not meta: + continue + for tag in meta.get('tags', []): + try: + name = tag.get('name') if isinstance(tag, dict) else str(tag) + if name: + tags_set.add(name) + except Exception: + continue + + return tags_set + + async def get_model_by_id(self, id: str, db: AsyncSession | None = None) -> ModelModel | None: try: - with get_db_context(db) as db: - models = db.query(Model).filter(Model.id.in_(ids)).all() - return [ModelModel.model_validate(model) for model in models] + async with get_async_db_context(db) as db: + model = await db.get(Model, id) + return await self._to_model_model(model, db=db) if model else None + except Exception: + return None + + async def get_models_by_ids(self, ids: list[str], db: AsyncSession | None = None) -> list[ModelModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter(Model.id.in_(ids))) + models = result.scalars().all() + model_ids = [model.id for model in models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) + return [ + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + for model in models + ] except Exception: return [] - def toggle_model_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ModelModel]: - with get_db_context(db) as db: + async def toggle_model_by_id(self, id: str, db: AsyncSession | None = None) -> ModelModel | None: + async with get_async_db_context(db) as db: try: - is_active = db.query(Model).filter_by(id=id).first().is_active + result = await db.execute(select(Model).filter_by(id=id)) + model = result.scalars().first() + if not model: + return None - db.query(Model).filter_by(id=id).update( - { - "is_active": not is_active, - "updated_at": int(time.time()), - } - ) - db.commit() + model.is_active = not model.is_active + model.updated_at = int(time.time()) + await db.commit() + await db.refresh(model) - return self.get_model_by_id(id, db=db) + return await self._to_model_model(model, db=db) except Exception: return None - def update_model_by_id( - self, id: str, model: ModelForm, db: Optional[Session] = None - ) -> Optional[ModelModel]: + async def update_model_by_id(self, id: str, model: ModelForm, db: AsyncSession | None = None) -> ModelModel | None: try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # update only the fields that are present in the model - data = model.model_dump(exclude={"id"}) - result = db.query(Model).filter_by(id=id).update(data) + data = model.model_dump(exclude={'id', 'access_grants'}) + data['updated_at'] = int(time.time()) + await db.execute(update(Model).filter_by(id=id).values(**data)) - db.commit() + await db.commit() + if model.access_grants is not None: + await AccessGrants.set_access_grants('model', id, model.access_grants, db=db) - model = db.get(Model, id) - db.refresh(model) - return ModelModel.model_validate(model) + return await self.get_model_by_id(id, db=db) except Exception as e: - log.exception(f"Failed to update the model by id {id}: {e}") + log.exception(f'Failed to update the model by id {id}: {e}') return None - def delete_model_by_id(self, id: str, db: Optional[Session] = None) -> bool: + async def update_model_updated_at_by_id(self, id: str, db: AsyncSession | None = None) -> ModelModel | None: try: - with get_db_context(db) as db: - db.query(Model).filter_by(id=id).delete() - db.commit() + async with get_async_db_context(db) as db: + result = await db.execute(select(Model).filter_by(id=id)) + model_obj = result.scalars().first() + if not model_obj: + return None + model_obj.updated_at = int(time.time()) + await db.commit() + await db.refresh(model_obj) + return await self._to_model_model(model_obj, db=db) + except Exception as e: + log.exception(f'Failed to update the model updated_at by id {id}: {e}') + return None + + async def delete_model_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('model', id, db=db) + await db.execute(delete(Model).filter_by(id=id)) + await db.commit() return True except Exception: return False - def delete_all_models(self, db: Optional[Session] = None) -> bool: + async def delete_all_models(self, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - db.query(Model).delete() - db.commit() + async with get_async_db_context(db) as db: + result = await db.execute(select(Model.id)) + model_ids = [row[0] for row in result.all()] + for model_id in model_ids: + await AccessGrants.revoke_all_access('model', model_id, db=db) + await db.execute(delete(Model)) + await db.commit() return True except Exception: return False - def sync_models( - self, user_id: str, models: list[ModelModel], db: Optional[Session] = None + async def sync_models( + self, user_id: str, models: list[ModelModel], db: AsyncSession | None = None ) -> list[ModelModel]: try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: # Get existing models - existing_models = db.query(Model).all() + result = await db.execute(select(Model)) + existing_models = result.scalars().all() existing_ids = {model.id for model in existing_models} # Prepare a set of new model IDs @@ -458,36 +548,49 @@ class ModelsTable: # Update or insert models for model in models: if model.id in existing_ids: - db.query(Model).filter_by(id=model.id).update( - { - **model.model_dump(), - "user_id": user_id, - "updated_at": int(time.time()), - } + await db.execute( + update(Model) + .filter_by(id=model.id) + .values( + **model.model_dump(exclude={'access_grants'}), + user_id=user_id, + updated_at=int(time.time()), + ) ) else: new_model = Model( **{ - **model.model_dump(), - "user_id": user_id, - "updated_at": int(time.time()), + **model.model_dump(exclude={'access_grants'}), + 'user_id': user_id, + 'updated_at': int(time.time()), } ) db.add(new_model) + await AccessGrants.set_access_grants('model', model.id, model.access_grants, db=db) # Remove models that are no longer present for model in existing_models: if model.id not in new_model_ids: - db.delete(model) + await AccessGrants.revoke_all_access('model', model.id, db=db) + await db.delete(model) - db.commit() + await db.commit() + result = await db.execute(select(Model)) + all_models = result.scalars().all() + model_ids = [model.id for model in all_models] + grants_map = await AccessGrants.get_grants_by_resources('model', model_ids, db=db) return [ - ModelModel.model_validate(model) for model in db.query(Model).all() + await self._to_model_model( + model, + access_grants=grants_map.get(model.id, []), + db=db, + ) + for model in all_models ] except Exception as e: - log.exception(f"Error syncing models for user {user_id}: {e}") + log.exception(f'Error syncing models for user {user_id}: {e}') return [] -Models = ModelsTable() +Models = ModelsTable() # singleton model registry diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index bd23530785..1cddd2f8fa 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -1,23 +1,16 @@ import json import time import uuid -from typing import Optional from functools import lru_cache +from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, get_db, get_db_context +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.groups import Groups -from open_webui.utils.access_control import has_access -from open_webui.models.users import User, UserModel, Users, UserResponse - - -from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON -from sqlalchemy.dialects.postgresql import JSONB - - -from sqlalchemy import or_, func, select, and_, text, cast, or_, and_, func -from sqlalchemy.sql import exists +from open_webui.models.users import User, UserModel, UserResponse, Users +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import JSON, BigInteger, Boolean, Column, ForeignKey, Text, delete, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession #################### # Note DB Schema @@ -25,7 +18,7 @@ from sqlalchemy.sql import exists class Note(Base): - __tablename__ = "note" + __tablename__ = 'note' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text) @@ -34,8 +27,6 @@ class Note(Base): data = Column(JSON, nullable=True) meta = Column(JSON, nullable=True) - access_control = Column(JSON, nullable=True) - created_at = Column(BigInteger) updated_at = Column(BigInteger) @@ -49,13 +40,23 @@ class NoteModel(BaseModel): title: str data: Optional[dict] = None meta: Optional[dict] = None + is_pinned: Optional[bool] = False - access_control: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) created_at: int # timestamp in epoch updated_at: int # timestamp in epoch +class PinnedNote(Base): + __tablename__ = 'pinned_note' + + id = Column(Text, primary_key=True) + user_id = Column(Text, nullable=False) + note_id = Column(Text, ForeignKey('note.id', ondelete='CASCADE'), nullable=False) + created_at = Column(BigInteger, nullable=False) + + #################### # Forms #################### @@ -65,14 +66,14 @@ class NoteForm(BaseModel): title: str data: Optional[dict] = None meta: Optional[dict] = None - access_control: Optional[dict] = None + access_grants: Optional[list[dict]] = None class NoteUpdateForm(BaseModel): title: Optional[str] = None data: Optional[dict] = None meta: Optional[dict] = None - access_control: Optional[dict] = None + access_grants: Optional[list[dict]] = None class NoteUserResponse(NoteModel): @@ -83,6 +84,7 @@ class NoteItemResponse(BaseModel): id: str title: str data: Optional[dict] + is_pinned: Optional[bool] = False updated_at: int created_at: int user: Optional[UserResponse] = None @@ -94,319 +96,296 @@ class NoteListResponse(BaseModel): class NoteTable: - def _has_permission(self, db, query, filter: dict, permission: str = "read"): - group_ids = filter.get("group_ids", []) - user_id = filter.get("user_id") - dialect_name = db.bind.dialect.name + async def _get_access_grants(self, note_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('note', note_id, db=db) - conditions = [] + async def _to_note_model( + self, + note: Note, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> NoteModel: + # We exclude access_grants to inject them + note_data = NoteModel.model_validate(note).model_dump(exclude={'access_grants'}) + note_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(note_data['id'], db=db) + ) + return NoteModel.model_validate(note_data) - # Handle read_only permission separately - if permission == "read_only": - # For read_only, we want items where: - # 1. User has explicit read permission (via groups or user-level) - # 2. BUT does NOT have write permission - # 3. Public items are NOT considered read_only + def _has_permission(self, db, query, filter: dict, permission: str = 'read'): + return AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Note, + filter=filter, + resource_type='note', + permission=permission, + ) - read_conditions = [] - - # Group-level read permission - if group_ids: - group_read_conditions = [] - for gid in group_ids: - if dialect_name == "sqlite": - group_read_conditions.append( - Note.access_control["read"]["group_ids"].contains([gid]) - ) - elif dialect_name == "postgresql": - group_read_conditions.append( - cast( - Note.access_control["read"]["group_ids"], - JSONB, - ).contains([gid]) - ) - - if group_read_conditions: - read_conditions.append(or_(*group_read_conditions)) - - # Combine read conditions - if read_conditions: - has_read = or_(*read_conditions) - else: - # If no read conditions, return empty result - return query.filter(False) - - # Now exclude items where user has write permission - write_exclusions = [] - - # Exclude items owned by user (they have implicit write) - if user_id: - write_exclusions.append(Note.user_id != user_id) - - # Exclude items where user has explicit write permission via groups - if group_ids: - group_write_conditions = [] - for gid in group_ids: - if dialect_name == "sqlite": - group_write_conditions.append( - Note.access_control["write"]["group_ids"].contains([gid]) - ) - elif dialect_name == "postgresql": - group_write_conditions.append( - cast( - Note.access_control["write"]["group_ids"], - JSONB, - ).contains([gid]) - ) - - if group_write_conditions: - # User should NOT have write permission - write_exclusions.append(~or_(*group_write_conditions)) - - # Exclude public items (items without access_control) - write_exclusions.append(Note.access_control.isnot(None)) - write_exclusions.append(cast(Note.access_control, String) != "null") - - # Combine: has read AND does not have write AND not public - if write_exclusions: - query = query.filter(and_(has_read, *write_exclusions)) - else: - query = query.filter(has_read) - - return query - - # Original logic for other permissions (read, write, etc.) - # Public access conditions - if group_ids or user_id: - conditions.extend( - [ - Note.access_control.is_(None), - cast(Note.access_control, String) == "null", - ] - ) - - # User-level permission (owner has all permissions) - if user_id: - conditions.append(Note.user_id == user_id) - - # Group-level permission - if group_ids: - group_conditions = [] - for gid in group_ids: - if dialect_name == "sqlite": - group_conditions.append( - Note.access_control[permission]["group_ids"].contains([gid]) - ) - elif dialect_name == "postgresql": - group_conditions.append( - cast( - Note.access_control[permission]["group_ids"], - JSONB, - ).contains([gid]) - ) - conditions.append(or_(*group_conditions)) - - if conditions: - query = query.filter(or_(*conditions)) - - return query - - def insert_new_note( - self, user_id: str, form_data: NoteForm, db: Optional[Session] = None + async def insert_new_note( + self, user_id: str, form_data: NoteForm, db: Optional[AsyncSession] = None ) -> Optional[NoteModel]: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: note = NoteModel( **{ - "id": str(uuid.uuid4()), - "user_id": user_id, - **form_data.model_dump(), - "created_at": int(time.time_ns()), - "updated_at": int(time.time_ns()), + 'id': str(uuid.uuid4()), + 'user_id': user_id, + **form_data.model_dump(exclude={'access_grants'}), + 'created_at': int(time.time_ns()), + 'updated_at': int(time.time_ns()), + 'access_grants': [], } ) - new_note = Note(**note.model_dump()) + new_note = Note(**note.model_dump(exclude={'access_grants', 'is_pinned'})) db.add(new_note) - db.commit() - return note + await db.commit() + await AccessGrants.set_access_grants('note', note.id, form_data.access_grants, db=db) + return await self._to_note_model(new_note, db=db) - def get_notes( - self, skip: int = 0, limit: int = 50, db: Optional[Session] = None - ) -> list[NoteModel]: - with get_db_context(db) as db: - query = db.query(Note).order_by(Note.updated_at.desc()) + async def get_notes(self, skip: int = 0, limit: int = 50, db: Optional[AsyncSession] = None) -> list[NoteModel]: + async with get_async_db_context(db) as db: + stmt = select(Note).order_by(Note.updated_at.desc()) if skip is not None: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit is not None: - query = query.limit(limit) - notes = query.all() - return [NoteModel.model_validate(note) for note in notes] + stmt = stmt.limit(limit) + result = await db.execute(stmt) + notes = result.scalars().all() + note_ids = [note.id for note in notes] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + return [await self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes] - def search_notes( + async def search_notes( self, user_id: str, filter: dict = {}, skip: int = 0, limit: int = 30, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> NoteListResponse: - with get_db_context(db) as db: - query = db.query(Note, User).outerjoin(User, User.id == Note.user_id) + async with get_async_db_context(db) as db: + stmt = select(Note, User).outerjoin(User, User.id == Note.user_id) if filter: - query_key = filter.get("query") + query_key = filter.get('query') if query_key: - # Normalize search by removing hyphens and spaces (e.g., "todo" matches "to-do" and "to do") - normalized_query = query_key.replace("-", "").replace(" ", "") - query = query.filter( - or_( - func.replace( - func.replace(Note.title, "-", ""), " ", "" - ).ilike(f"%{normalized_query}%"), - func.replace( + # Split query into individual words and normalize each + # (strip hyphens so "todo" matches "to-do"). + # All words must match somewhere in title OR content (AND semantics). + search_words = query_key.split() + normalized_words = [w.replace('-', '') for w in search_words if w.replace('-', '')] + for word in normalized_words: + stmt = stmt.filter( + or_( + func.replace(func.replace(Note.title, '-', ''), ' ', '').ilike(f'%{word}%'), func.replace( - cast(Note.data["content"]["md"], Text), "-", "" - ), - " ", - "", - ).ilike(f"%{normalized_query}%"), + func.replace(Note.data['content']['md'].as_string(), '-', ''), + ' ', + '', + ).ilike(f'%{word}%'), + ) ) - ) - view_option = filter.get("view_option") - if view_option == "created": - query = query.filter(Note.user_id == user_id) - elif view_option == "shared": - query = query.filter(Note.user_id != user_id) + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Note.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Note.user_id != user_id) # Apply access control filtering - if "permission" in filter: - permission = filter["permission"] + if 'permission' in filter: + permission = filter['permission'] else: - permission = "write" + permission = 'write' - query = self._has_permission( + stmt = self._has_permission( db, - query, + stmt, filter, permission=permission, ) - order_by = filter.get("order_by") - direction = filter.get("direction") + order_by = filter.get('order_by') + direction = filter.get('direction') - if order_by == "name": - if direction == "asc": - query = query.order_by(Note.title.asc()) + if order_by == 'name': + if direction == 'asc': + stmt = stmt.order_by(Note.title.asc()) else: - query = query.order_by(Note.title.desc()) - elif order_by == "created_at": - if direction == "asc": - query = query.order_by(Note.created_at.asc()) + stmt = stmt.order_by(Note.title.desc()) + elif order_by == 'created_at': + if direction == 'asc': + stmt = stmt.order_by(Note.created_at.asc()) else: - query = query.order_by(Note.created_at.desc()) - elif order_by == "updated_at": - if direction == "asc": - query = query.order_by(Note.updated_at.asc()) + stmt = stmt.order_by(Note.created_at.desc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(Note.updated_at.asc()) else: - query = query.order_by(Note.updated_at.desc()) + stmt = stmt.order_by(Note.updated_at.desc()) else: - query = query.order_by(Note.updated_at.desc()) + stmt = stmt.order_by(Note.updated_at.desc()) else: - query = query.order_by(Note.updated_at.desc()) + stmt = stmt.order_by(Note.updated_at.desc()) # Count BEFORE pagination - total = query.count() + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() if skip: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit: - query = query.limit(limit) + stmt = stmt.limit(limit) - items = query.all() + result = await db.execute(stmt) + items = result.all() + + note_ids = [note.id for note, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) notes = [] for note, user in items: notes.append( NoteUserResponse( - **NoteModel.model_validate(note).model_dump(), - user=( - UserResponse(**UserModel.model_validate(user).model_dump()) - if user - else None - ), + **( + await self._to_note_model( + note, + access_grants=grants_map.get(note.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), ) ) return NoteListResponse(items=notes, total=total) - def get_notes_by_user_id( + async def get_notes_by_user_id( self, user_id: str, - permission: str = "read", + permission: str = 'read', skip: int = 0, limit: int = 50, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> list[NoteModel]: - with get_db_context(db) as db: - user_group_ids = [ - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - ] + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [group.id for group in user_groups] - query = db.query(Note).order_by(Note.updated_at.desc()) - query = self._has_permission( - db, query, {"user_id": user_id, "group_ids": user_group_ids}, permission - ) + stmt = select(Note).order_by(Note.updated_at.desc()) + stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}, permission) if skip is not None: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit is not None: - query = query.limit(limit) + stmt = stmt.limit(limit) - notes = query.all() - return [NoteModel.model_validate(note) for note in notes] + result = await db.execute(stmt) + notes = result.scalars().all() + note_ids = [note.id for note in notes] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + return [await self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes] - def get_note_by_id( - self, id: str, db: Optional[Session] = None + async def get_note_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[NoteModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Note).filter(Note.id == id)) + note = result.scalars().first() + return await self._to_note_model(note, db=db) if note else None + + async def update_note_by_id( + self, id: str, form_data: NoteUpdateForm, db: Optional[AsyncSession] = None ) -> Optional[NoteModel]: - with get_db_context(db) as db: - note = db.query(Note).filter(Note.id == id).first() - return NoteModel.model_validate(note) if note else None - - def update_note_by_id( - self, id: str, form_data: NoteUpdateForm, db: Optional[Session] = None - ) -> Optional[NoteModel]: - with get_db_context(db) as db: - note = db.query(Note).filter(Note.id == id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(Note).filter(Note.id == id)) + note = result.scalars().first() if not note: return None form_data = form_data.model_dump(exclude_unset=True) - if "title" in form_data: - note.title = form_data["title"] - if "data" in form_data: - note.data = {**note.data, **form_data["data"]} - if "meta" in form_data: - note.meta = {**note.meta, **form_data["meta"]} + if 'title' in form_data: + note.title = form_data['title'] + if 'data' in form_data: + note.data = {**note.data, **form_data['data']} + if 'meta' in form_data: + note.meta = {**note.meta, **form_data['meta']} - if "access_control" in form_data: - note.access_control = form_data["access_control"] + if 'access_grants' in form_data: + await AccessGrants.set_access_grants('note', id, form_data['access_grants'], db=db) note.updated_at = int(time.time_ns()) - db.commit() - return NoteModel.model_validate(note) if note else None + await db.commit() + return await self._to_note_model(note, db=db) if note else None - def delete_note_by_id(self, id: str, db: Optional[Session] = None) -> bool: + async def toggle_note_pinned_by_id( + self, id: str, user_id: str, db: Optional[AsyncSession] = None + ) -> Optional[NoteModel]: try: - with get_db_context(db) as db: - db.query(Note).filter(Note.id == id).delete() - db.commit() + async with get_async_db_context(db) as db: + result = await db.execute(select(Note).filter(Note.id == id)) + note = result.scalars().first() + if not note: + return None + + # Check if already pinned + pin_result = await db.execute(select(PinnedNote).filter_by(user_id=user_id, note_id=id)) + pinned_note = pin_result.scalars().first() + + if pinned_note: + await db.execute(delete(PinnedNote).filter_by(user_id=user_id, note_id=id)) + else: + new_pin = PinnedNote( + id=str(uuid.uuid4()), user_id=user_id, note_id=id, created_at=int(time.time_ns()) + ) + db.add(new_pin) + + await db.commit() + return await self._to_note_model(note, db=db) + except Exception: + return None + + async def get_pinned_notes_by_user_id( + self, + user_id: str, + permission: str = 'read', + db: Optional[AsyncSession] = None, + ) -> list[NoteModel]: + async with get_async_db_context(db) as db: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = [group.id for group in user_groups] + + stmt = ( + select(Note) + .join(PinnedNote, PinnedNote.note_id == Note.id) + .filter(PinnedNote.user_id == user_id) + .order_by(PinnedNote.created_at.desc()) + ) + stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}, permission) + + result = await db.execute(stmt) + notes = result.scalars().all() + note_ids = [note.id for note in notes] + grants_map = await AccessGrants.get_grants_by_resources('note', note_ids, db=db) + return [await self._to_note_model(note, access_grants=grants_map.get(note.id, []), db=db) for note in notes] + + async def delete_note_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('note', id, db=db) + await db.execute(delete(PinnedNote).filter(PinnedNote.note_id == id)) + await db.execute(delete(Note).filter(Note.id == id)) + await db.commit() return True except Exception: return False + async def get_pinned_note_ids(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]: + async with get_async_db_context(db) as db: + result = await db.execute(select(PinnedNote.note_id).filter_by(user_id=user_id)) + return result.scalars().all() + Notes = NoteTable() diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index f7ee5cceb8..0619bd574a 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -1,19 +1,17 @@ -import time -import logging -import uuid -from typing import Optional, List import base64 import hashlib import json +import logging +import time +import uuid +from typing import List, Optional from cryptography.fernet import Fernet - -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, get_db, get_db_context from open_webui.env import OAUTH_SESSION_TOKEN_ENCRYPTION_KEY - +from open_webui.internal.db import Base, get_async_db_context from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, Index +from sqlalchemy import BigInteger, Column, Index, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -23,23 +21,21 @@ log = logging.getLogger(__name__) class OAuthSession(Base): - __tablename__ = "oauth_session" + __tablename__ = 'oauth_session' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text, nullable=False) provider = Column(Text, nullable=False) - token = Column( - Text, nullable=False - ) # JSON with access_token, id_token, refresh_token + token = Column(Text, nullable=False) # JSON with access_token, id_token, refresh_token expires_at = Column(BigInteger, nullable=False) created_at = Column(BigInteger, nullable=False) updated_at = Column(BigInteger, nullable=False) # Add indexes for better performance __table_args__ = ( - Index("idx_oauth_session_user_id", "user_id"), - Index("idx_oauth_session_expires_at", "expires_at"), - Index("idx_oauth_session_user_provider", "user_id", "provider"), + Index('idx_oauth_session_user_id', 'user_id'), + Index('idx_oauth_session_expires_at', 'expires_at'), + Index('idx_oauth_session_user_provider', 'user_id', 'provider'), ) @@ -71,7 +67,7 @@ class OAuthSessionTable: def __init__(self): self.encryption_key = OAUTH_SESSION_TOKEN_ENCRYPTION_KEY if not self.encryption_key: - raise Exception("OAUTH_SESSION_TOKEN_ENCRYPTION_KEY is not set") + raise Exception('OAUTH_SESSION_TOKEN_ENCRYPTION_KEY is not set') # check if encryption key is in the right format for Fernet (32 url-safe base64-encoded bytes) if len(self.encryption_key) != 44: @@ -83,7 +79,7 @@ class OAuthSessionTable: try: self.fernet = Fernet(self.encryption_key) except Exception as e: - log.error(f"Error initializing Fernet with provided key: {e}") + log.error(f'Error initializing Fernet with provided key: {e}') raise def _encrypt_token(self, token) -> str: @@ -93,7 +89,7 @@ class OAuthSessionTable: encrypted = self.fernet.encrypt(token_json.encode()).decode() return encrypted except Exception as e: - log.error(f"Error encrypting tokens: {e}") + log.error(f'Error encrypting tokens: {e}') raise def _decrypt_token(self, token: str): @@ -102,186 +98,247 @@ class OAuthSessionTable: decrypted = self.fernet.decrypt(token.encode()).decode() return json.loads(decrypted) except Exception as e: - log.error(f"Error decrypting tokens: {e}") + log.error(f'Error decrypting tokens: {type(e).__name__}: {e}') raise - def create_session( + async def create_session( self, user_id: str, provider: str, token: dict, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ) -> Optional[OAuthSessionModel]: """Create a new OAuth session""" try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: current_time = int(time.time()) id = str(uuid.uuid4()) result = OAuthSession( **{ - "id": id, - "user_id": user_id, - "provider": provider, - "token": self._encrypt_token(token), - "expires_at": token.get("expires_at"), - "created_at": current_time, - "updated_at": current_time, + 'id': id, + 'user_id': user_id, + 'provider': provider, + 'token': self._encrypt_token(token), + 'expires_at': token.get('expires_at') or int(time.time() + 3600), + 'created_at': current_time, + 'updated_at': current_time, } ) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) if result: - result.token = token # Return decrypted token - return OAuthSessionModel.model_validate(result) + # Make a copy of the model data before closing session + model = OAuthSessionModel( + id=result.id, + user_id=result.user_id, + provider=result.provider, + token=token, # Return decrypted token + expires_at=result.expires_at, + created_at=result.created_at, + updated_at=result.updated_at, + ) + return model else: return None except Exception as e: - log.error(f"Error creating OAuth session: {e}") + log.error(f'Error creating OAuth session: {e}') return None - def get_session_by_id( - self, session_id: str, db: Optional[Session] = None + async def get_session_by_id( + self, session_id: str, db: Optional[AsyncSession] = None ) -> Optional[OAuthSessionModel]: """Get OAuth session by ID""" try: - with get_db_context(db) as db: - session = db.query(OAuthSession).filter_by(id=session_id).first() + async with get_async_db_context(db) as db: + result = await db.execute(select(OAuthSession).filter_by(id=session_id)) + session = result.scalars().first() if session: - session.token = self._decrypt_token(session.token) - return OAuthSessionModel.model_validate(session) + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) return None except Exception as e: - log.error(f"Error getting OAuth session by ID: {e}") + log.error(f'Error getting OAuth session by ID: {e}') return None - def get_session_by_id_and_user_id( - self, session_id: str, user_id: str, db: Optional[Session] = None + async def get_session_by_id_and_user_id( + self, session_id: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[OAuthSessionModel]: """Get OAuth session by ID and user ID""" try: - with get_db_context(db) as db: - session = ( - db.query(OAuthSession) - .filter_by(id=session_id, user_id=user_id) - .first() - ) + async with get_async_db_context(db) as db: + result = await db.execute(select(OAuthSession).filter_by(id=session_id, user_id=user_id)) + session = result.scalars().first() if session: - session.token = self._decrypt_token(session.token) - return OAuthSessionModel.model_validate(session) + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) return None except Exception as e: - log.error(f"Error getting OAuth session by ID: {e}") + log.error(f'Error getting OAuth session by ID: {e}') return None - def get_session_by_provider_and_user_id( - self, provider: str, user_id: str, db: Optional[Session] = None + async def get_session_by_provider_and_user_id( + self, provider: str, user_id: str, db: Optional[AsyncSession] = None ) -> Optional[OAuthSessionModel]: """Get OAuth session by provider and user ID""" try: - with get_db_context(db) as db: - session = ( - db.query(OAuthSession) + async with get_async_db_context(db) as db: + result = await db.execute( + select(OAuthSession) .filter_by(provider=provider, user_id=user_id) - .first() + .order_by(OAuthSession.created_at.desc()) ) + session = result.scalars().first() if session: - session.token = self._decrypt_token(session.token) - return OAuthSessionModel.model_validate(session) + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) return None except Exception as e: - log.error(f"Error getting OAuth session by provider and user ID: {e}") + log.error(f'Error getting OAuth session by provider and user ID: {e}') return None - def get_sessions_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> List[OAuthSessionModel]: + async def get_sessions_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> List[OAuthSessionModel]: """Get all OAuth sessions for a user""" try: - with get_db_context(db) as db: - sessions = db.query(OAuthSession).filter_by(user_id=user_id).all() + async with get_async_db_context(db) as db: + result = await db.execute(select(OAuthSession).filter_by(user_id=user_id)) + sessions = result.scalars().all() results = [] for session in sessions: - session.token = self._decrypt_token(session.token) - results.append(OAuthSessionModel.model_validate(session)) + try: + results.append( + OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) + ) + except Exception as e: + log.warning( + f'Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}' + ) + await db.execute(delete(OAuthSession).filter_by(id=session.id)) + await db.commit() return results except Exception as e: - log.error(f"Error getting OAuth sessions by user ID: {e}") + log.error(f'Error getting OAuth sessions by user ID: {e}') return [] - def update_session_by_id( - self, session_id: str, token: dict, db: Optional[Session] = None + async def update_session_by_id( + self, session_id: str, token: dict, db: Optional[AsyncSession] = None ) -> Optional[OAuthSessionModel]: """Update OAuth session tokens""" try: - with get_db_context(db) as db: + async with get_async_db_context(db) as db: current_time = int(time.time()) - db.query(OAuthSession).filter_by(id=session_id).update( - { - "token": self._encrypt_token(token), - "expires_at": token.get("expires_at"), - "updated_at": current_time, - } + await db.execute( + update(OAuthSession) + .filter_by(id=session_id) + .values( + token=self._encrypt_token(token), + expires_at=token.get('expires_at') or int(time.time() + 3600), + updated_at=current_time, + ) ) - db.commit() - session = db.query(OAuthSession).filter_by(id=session_id).first() + await db.commit() + result = await db.execute(select(OAuthSession).filter_by(id=session_id)) + session = result.scalars().first() if session: - session.token = self._decrypt_token(session.token) - return OAuthSessionModel.model_validate(session) + return OAuthSessionModel( + id=session.id, + user_id=session.user_id, + provider=session.provider, + token=self._decrypt_token(session.token), + expires_at=session.expires_at, + created_at=session.created_at, + updated_at=session.updated_at, + ) return None except Exception as e: - log.error(f"Error updating OAuth session tokens: {e}") + log.error(f'Error updating OAuth session tokens: {e}') return None - def delete_session_by_id( - self, session_id: str, db: Optional[Session] = None - ) -> bool: + async def delete_session_by_id(self, session_id: str, db: Optional[AsyncSession] = None) -> bool: """Delete an OAuth session""" try: - with get_db_context(db) as db: - result = db.query(OAuthSession).filter_by(id=session_id).delete() - db.commit() - return result > 0 + async with get_async_db_context(db) as db: + result = await db.execute(delete(OAuthSession).filter_by(id=session_id)) + await db.commit() + return result.rowcount > 0 except Exception as e: - log.error(f"Error deleting OAuth session: {e}") + log.error(f'Error deleting OAuth session: {e}') return False - def delete_sessions_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> bool: + async def delete_sessions_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: """Delete all OAuth sessions for a user""" try: - with get_db_context(db) as db: - result = db.query(OAuthSession).filter_by(user_id=user_id).delete() - db.commit() + async with get_async_db_context(db) as db: + await db.execute(delete(OAuthSession).filter_by(user_id=user_id)) + await db.commit() return True except Exception as e: - log.error(f"Error deleting OAuth sessions by user ID: {e}") + log.error(f'Error deleting OAuth sessions by user ID: {e}') return False - def delete_sessions_by_provider( - self, provider: str, db: Optional[Session] = None + async def delete_sessions_by_user_id_and_provider( + self, user_id: str, provider: str, db: Optional[AsyncSession] = None ) -> bool: + """Delete all OAuth sessions for a specific user and provider""" + try: + async with get_async_db_context(db) as db: + result = await db.execute(delete(OAuthSession).filter_by(user_id=user_id, provider=provider)) + await db.commit() + return result.rowcount > 0 + except Exception as e: + log.error(f'Error deleting OAuth sessions for user {user_id} and provider {provider}: {e}') + return False + + async def delete_sessions_by_provider(self, provider: str, db: Optional[AsyncSession] = None) -> bool: """Delete all OAuth sessions for a provider""" try: - with get_db_context(db) as db: - db.query(OAuthSession).filter_by(provider=provider).delete() - db.commit() + async with get_async_db_context(db) as db: + await db.execute(delete(OAuthSession).filter_by(provider=provider)) + await db.commit() return True except Exception as e: - log.error(f"Error deleting OAuth sessions by provider {provider}: {e}") + log.error(f'Error deleting OAuth sessions by provider {provider}: {e}') return False diff --git a/backend/open_webui/models/prompt_history.py b/backend/open_webui/models/prompt_history.py new file mode 100644 index 0000000000..bb27657032 --- /dev/null +++ b/backend/open_webui/models/prompt_history.py @@ -0,0 +1,236 @@ +"""Prompt history model for version tracking.""" + +import difflib +import json +import time +import uuid +from typing import Optional + +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.users import UserResponse, Users +from pydantic import BaseModel, ConfigDict +from sqlalchemy import JSON, BigInteger, Column, Index, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession + +#################### +# PromptHistory DB Schema +#################### + + +class PromptHistory(Base): + __tablename__ = 'prompt_history' + + id = Column(Text, primary_key=True) + prompt_id = Column(Text, nullable=False, index=True) + parent_id = Column(Text, nullable=True) # Reference to parent commit + snapshot = Column(JSON, nullable=False) + user_id = Column(Text, nullable=False) + commit_message = Column(Text, nullable=True) + created_at = Column(BigInteger, nullable=False) + + +class PromptHistoryModel(BaseModel): + id: str + prompt_id: str + parent_id: Optional[str] = None + snapshot: dict + user_id: str + commit_message: Optional[str] = None + created_at: int + + model_config = ConfigDict(from_attributes=True) + + +class PromptHistoryResponse(PromptHistoryModel): + """Response model with user info.""" + + user: Optional[UserResponse] = None + + +class PromptHistoryTable: + async def create_history_entry( + self, + prompt_id: str, + snapshot: dict, + user_id: str, + parent_id: Optional[str] = None, + commit_message: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptHistoryModel]: + """Create a new history entry (commit) for a prompt.""" + async with get_async_db_context(db) as db: + history = PromptHistory( + id=str(uuid.uuid4()), + prompt_id=prompt_id, + parent_id=parent_id, + snapshot=snapshot, + user_id=user_id, + commit_message=commit_message, + created_at=int(time.time()), + ) + db.add(history) + await db.commit() + await db.refresh(history) + return PromptHistoryModel.model_validate(history) + + async def get_history_by_prompt_id( + self, + prompt_id: str, + limit: int = 50, + offset: int = 0, + db: Optional[AsyncSession] = None, + ) -> list[PromptHistoryResponse]: + """Get all history entries for a prompt, ordered by created_at desc.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(PromptHistory) + .filter(PromptHistory.prompt_id == prompt_id) + .order_by(PromptHistory.created_at.desc()) + .offset(offset) + .limit(limit) + ) + entries = result.scalars().all() + + # Get user info for each entry + user_ids = list(set(e.user_id for e in entries)) + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + + return [ + PromptHistoryResponse( + **PromptHistoryModel.model_validate(entry).model_dump(), + user=(users_dict.get(entry.user_id).model_dump() if users_dict.get(entry.user_id) else None), + ) + for entry in entries + ] + + async def get_history_entry_by_id( + self, + history_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptHistoryModel]: + """Get a specific history entry by ID.""" + async with get_async_db_context(db) as db: + result = await db.execute(select(PromptHistory).filter(PromptHistory.id == history_id)) + entry = result.scalars().first() + if entry: + return PromptHistoryModel.model_validate(entry) + return None + + async def get_latest_history_entry( + self, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[PromptHistoryModel]: + """Get the most recent history entry for a prompt.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(PromptHistory) + .filter(PromptHistory.prompt_id == prompt_id) + .order_by(PromptHistory.created_at.desc()) + .limit(1) + ) + entry = result.scalars().first() + if entry: + return PromptHistoryModel.model_validate(entry) + return None + + async def get_history_count( + self, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> int: + """Get the number of history entries for a prompt.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(func.count()).select_from(PromptHistory).filter(PromptHistory.prompt_id == prompt_id) + ) + return result.scalar() + + async def compute_diff( + self, + from_id: str, + to_id: str, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> Optional[dict]: + """Compute diff between two history entries.""" + async with get_async_db_context(db) as db: + # Bind both entries to the authorized prompt; an unbound id reads another prompt's snapshot. + result_from = await db.execute( + select(PromptHistory).filter(PromptHistory.id == from_id, PromptHistory.prompt_id == prompt_id) + ) + from_entry = result_from.scalars().first() + result_to = await db.execute( + select(PromptHistory).filter(PromptHistory.id == to_id, PromptHistory.prompt_id == prompt_id) + ) + to_entry = result_to.scalars().first() + + if not from_entry or not to_entry: + return None + + from_snapshot = from_entry.snapshot + to_snapshot = to_entry.snapshot + + # Compute diff for content field + from_content = from_snapshot.get('content', '') + to_content = to_snapshot.get('content', '') + + diff_lines = list( + difflib.unified_diff( + from_content.splitlines(keepends=True), + to_content.splitlines(keepends=True), + fromfile=f'v{from_id[:8]}', + tofile=f'v{to_id[:8]}', + lineterm='', + ) + ) + + return { + 'from_id': from_id, + 'to_id': to_id, + 'from_snapshot': from_snapshot, + 'to_snapshot': to_snapshot, + 'content_diff': diff_lines, + 'name_changed': from_snapshot.get('name') != to_snapshot.get('name'), + } + + async def delete_history_by_prompt_id( + self, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> bool: + """Delete all history entries for a prompt.""" + async with get_async_db_context(db) as db: + await db.execute(delete(PromptHistory).filter(PromptHistory.prompt_id == prompt_id)) + await db.commit() + return True + + async def delete_history_entry( + self, + history_id: str, + prompt_id: str, + db: Optional[AsyncSession] = None, + ) -> bool: + """Delete a history entry and reparent its children to grandparent.""" + async with get_async_db_context(db) as db: + # Bind to the authorized prompt; an unbound id deletes another prompt's history. + result = await db.execute(select(PromptHistory).filter_by(id=history_id, prompt_id=prompt_id)) + entry = result.scalars().first() + if not entry: + return False + + # Find children that reference this entry as parent + children_result = await db.execute(select(PromptHistory).filter_by(parent_id=history_id)) + children = children_result.scalars().all() + + # Reparent children to grandparent + for child in children: + child.parent_id = entry.parent_id + + await db.delete(entry) + await db.commit() + return True + + +PromptHistories = PromptHistoryTable() diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 847597bc65..6155c5f95b 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -1,178 +1,702 @@ +"""Prompt template models, forms, and database operations.""" + +from __future__ import annotations + +import json +import logging import time +import uuid from typing import Optional -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context +log = logging.getLogger(__name__) + +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.groups import Groups -from open_webui.models.users import Users, UserResponse - -from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, JSON - -from open_webui.utils.access_control import has_access - -#################### -# Prompts DB Schema -#################### +from open_webui.models.prompt_history import PromptHistories +from open_webui.models.users import User, UserModel, UserResponse, Users +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, cast, delete, func, or_, select, text, update +from sqlalchemy.ext.asyncio import AsyncSession -class Prompt(Base): - __tablename__ = "prompt" +class Prompt(Base): # versioned template + """Slash-command prompt with history tracking and access control.""" - command = Column(String, primary_key=True) - user_id = Column(String) - title = Column(Text) - content = Column(Text) - timestamp = Column(BigInteger) + __tablename__ = 'prompt' - access_control = Column(JSON, nullable=True) # Controls data access levels. - # Defines access control rules for this entry. - # - `None`: Public access, available to all users with the "user" role. - # - `{}`: Private access, restricted exclusively to the owner. - # - Custom permissions: Specific access control for reading and writing; - # Can specify group or user-level restrictions: - # { - # "read": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # }, - # "write": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # } - # } + id = Column(Text, primary_key=True) + command = Column(String, unique=True, index=True) + user_id = Column(String, index=True) # owner user id + name = Column(Text) + content = Column(Text) # the prompt template body + data = Column(JSON, nullable=True) # structured prompt parameters + meta = Column(JSON, nullable=True) # freeform metadata (description, etc.) + tags = Column(JSON, nullable=True) + is_active = Column(Boolean, default=True) + version_id = Column(Text, nullable=True) # Points to active history entry + created_at = Column(BigInteger, nullable=True) + updated_at = Column(BigInteger, nullable=True) class PromptModel(BaseModel): + id: str | None = None command: str user_id: str - title: str + name: str content: str - timestamp: int # timestamp in epoch + data: dict | None = None + meta: dict | None = None + tags: list[str | None] = None + is_active: bool | None = True + version_id: str | None = None + created_at: int | None = None + updated_at: int | None = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) - access_control: Optional[dict] = None - model_config = ConfigDict(from_attributes=True) - - -#################### + model_config = ConfigDict(from_attributes=True) # allows ORM model binding +# --- form / schema definitions --- # Forms #################### class PromptUserResponse(PromptModel): - user: Optional[UserResponse] = None + user: UserResponse | None = None class PromptAccessResponse(PromptUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False + + +class PromptListResponse(BaseModel): + items: list[PromptUserResponse] + total: int + + +class PromptAccessListResponse(BaseModel): + items: list[PromptAccessResponse] + total: int class PromptForm(BaseModel): command: str - title: str + name: str # Changed from title content: str - access_control: Optional[dict] = None + data: dict | None = None + meta: dict | None = None + tags: list[str | None] = None + access_grants: list[dict | None] = None + version_id: str | None = None # Active version + commit_message: str | None = None # For history tracking + is_production: bool | None = True # Whether to set new version as production class PromptsTable: - def insert_new_prompt( - self, user_id: str, form_data: PromptForm, db: Optional[Session] = None - ) -> Optional[PromptModel]: - prompt = PromptModel( - **{ - "user_id": user_id, - **form_data.model_dump(), - "timestamp": int(time.time()), - } + async def _get_access_grants(self, prompt_id: str, db: AsyncSession | None = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('prompt', prompt_id, db=db) + + async def _to_prompt_model( + self, + prompt: Prompt, + access_grants: list[AccessGrantModel | None] = None, + db: AsyncSession | None = None, + ) -> PromptModel: + prompt_data = PromptModel.model_validate(prompt).model_dump(exclude={'access_grants'}) + prompt_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(prompt_data['id'], db=db) ) + return PromptModel.model_validate(prompt_data) - try: - with get_db_context(db) as db: - result = Prompt(**prompt.model_dump()) - db.add(result) - db.commit() - db.refresh(result) - if result: - return PromptModel.model_validate(result) - else: + async def insert_new_prompt(self, user_id: str, form_data: PromptForm, db: AsyncSession | None = None) -> PromptModel | None: + now = int(time.time()) + prompt_id = str(uuid.uuid4()) + + async with get_async_db_context(db) as session: + try: + record = Prompt( + id=prompt_id, user_id=user_id, + command=form_data.command, name=form_data.name, + content=form_data.content, + data=form_data.data or {}, meta=form_data.meta or {}, + tags=form_data.tags or [], is_active=True, + created_at=now, updated_at=now, + ) + session.add(record) + await session.commit() + await session.refresh(record) # populate generated defaults + + await AccessGrants.set_access_grants( + 'prompt', prompt_id, form_data.access_grants, db=session, + ) # persist sharing rules + + if not record: # shouldn't happen, but guard anyway return None - except Exception: - return None - def get_prompt_by_command( - self, command: str, db: Optional[Session] = None - ) -> Optional[PromptModel]: + # Build the initial version snapshot. + grants = await self._get_access_grants(prompt_id, db=session) + snapshot = { + 'name': form_data.name, + 'content': form_data.content, + 'command': form_data.command, + 'data': form_data.data or {}, + 'meta': form_data.meta or {}, + 'tags': form_data.tags or [], + 'access_grants': [g.model_dump() for g in grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt_id, snapshot=snapshot, + user_id=user_id, parent_id=None, + commit_message=form_data.commit_message or 'Initial version', + db=session, + ) # creates the first version entry + + # Pin the initial history entry as the production version. + if history_entry: + record.version_id = history_entry.id + await session.commit() + await session.refresh(record) # re-read version_id + + return await self._to_prompt_model(record, db=session) + except Exception as e: + log.exception('Error creating prompt: %s', e) + return None + + async def get_prompt_by_id(self, prompt_id: str, db: AsyncSession | None = None) -> PromptModel | None: try: - with get_db_context(db) as db: - prompt = db.query(Prompt).filter_by(command=command).first() - return PromptModel.model_validate(prompt) - except Exception: - return None + async with get_async_db_context(db) as session: + result = await session.execute( + select(Prompt).filter_by(id=prompt_id), + ) + prompt = result.scalars().first() # None when not found + if not prompt: + return None + return await self._to_prompt_model(prompt, db=session) + except Exception: # connection / integrity error + return - def get_prompts(self, db: Optional[Session] = None) -> list[PromptUserResponse]: - with get_db_context(db) as db: - all_prompts = db.query(Prompt).order_by(Prompt.timestamp.desc()).all() + async def get_prompt_by_command(self, command: str, db: AsyncSession | None = None) -> PromptModel | None: + """Look up a prompt by its unique slash-command string.""" + async with get_async_db_context(db) as session: + match = (await session.execute( + select(Prompt).where(Prompt.command == command) + )).scalars().first() + if match is None: + return + return await self._to_prompt_model(match, db=session) + # --- context manager always returns above --- + return - user_ids = list(set(prompt.user_id for prompt in all_prompts)) + async def get_prompts(self, db: AsyncSession | None = None) -> list[PromptUserResponse]: + """Return all active prompts ordered by most recently updated.""" + async with get_async_db_context(db) as session: + active = (await session.execute( + select(Prompt).where(Prompt.is_active.is_(True)).order_by(Prompt.updated_at.desc()) + )).scalars().all() - users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] - users_dict = {user.id: user for user in users} + user_ids = list(set(p.user_id for p in active)) + prompt_ids = [p.id for p in active] + + users = await Users.get_users_by_user_ids(user_ids, db=session) if user_ids else [] + users_dict = {u.id: u for u in users} + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=session) prompts = [] - for prompt in all_prompts: + for prompt in active: user = users_dict.get(prompt.user_id) prompts.append( PromptUserResponse.model_validate( { - **PromptModel.model_validate(prompt).model_dump(), - "user": user.model_dump() if user else None, + **( + await self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=session, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, } ) ) return prompts - def get_prompts_by_user_id( - self, user_id: str, permission: str = "write", db: Optional[Session] = None + async def get_prompts_by_user_id( + self, user_id: str, permission: str = 'write', db: AsyncSession | None = None ) -> list[PromptUserResponse]: - prompts = self.get_prompts(db=db) - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - } + async with get_async_db_context(db) as session: + user_groups = await Groups.get_groups_by_member_id(user_id, db=session) + user_group_ids = [group.id for group in user_groups] - return [ - prompt - for prompt in prompts - if prompt.user_id == user_id - or has_access(user_id, permission, prompt.access_control, user_group_ids) - ] + query = select(Prompt).filter(Prompt.is_active == True).order_by(Prompt.updated_at.desc()) + query = AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Prompt, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='prompt', + permission=permission, + ) - def update_prompt_by_command( - self, command: str, form_data: PromptForm, db: Optional[Session] = None - ) -> Optional[PromptModel]: - try: - with get_db_context(db) as db: - prompt = db.query(Prompt).filter_by(command=command).first() - prompt.title = form_data.title + result = await session.execute(query) + accessible_prompts = result.scalars().all() + + if not accessible_prompts: + return [] + + prompt_ids = [p.id for p in accessible_prompts] + owner_ids = list({p.user_id for p in accessible_prompts}) + + users = await Users.get_users_by_user_ids(owner_ids, db=session) + users_dict = {u.id: u for u in users} + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=session) + + results = [] + for prompt in accessible_prompts: + user = users_dict.get(prompt.user_id) + results.append( + PromptUserResponse.model_validate( + { + **( + await self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + return results + + async def search_prompts( + self, + user_id: str, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: AsyncSession | None = None, + ) -> PromptListResponse: + async with get_async_db_context(db) as session: + # Join with User table for user filtering and sorting + query = select(Prompt, User).outerjoin(User, User.id == Prompt.user_id) + + if filter: + query_key = filter.get('query') + if query_key: + query = query.filter( + or_( + Prompt.name.ilike(f'%{query_key}%'), + Prompt.command.ilike(f'%{query_key}%'), + Prompt.content.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + query = query.filter(Prompt.user_id == user_id) + elif view_option == 'shared': + query = query.filter(Prompt.user_id != user_id) + + # Apply access grant filtering + query = AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Prompt, + filter=filter, + resource_type='prompt', + permission='read', + ) + + tag = filter.get('tag') + if tag: + bind = await session.connection() + dialect_name = bind.dialect.name + tag_lower = tag.lower() + + if dialect_name == 'sqlite': + tag_clause = text( + 'EXISTS (SELECT 1 FROM json_each(prompt.tags) t WHERE LOWER(t.value) = :tag_val)' + ) + elif dialect_name == 'postgresql': + tag_clause = text( + 'EXISTS (SELECT 1 FROM json_array_elements_text(prompt.tags) t WHERE LOWER(t) = :tag_val)' + ) + else: + # Fallback: LIKE on serialised JSON text (ASCII-safe only) + tag_clause = func.lower(cast(Prompt.tags, String)).like( + f'%{json.dumps(tag_lower, ensure_ascii=False)}%' + ) + tag_lower = None + + if tag_lower is not None: + query = query.filter(tag_clause.params(tag_val=tag_lower)) + else: + query = query.filter(tag_clause) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by == 'name': + if direction == 'asc': + query = query.order_by(Prompt.name.asc()) + else: + query = query.order_by(Prompt.name.desc()) + elif order_by == 'created_at': + if direction == 'asc': + query = query.order_by(Prompt.created_at.asc()) + else: + query = query.order_by(Prompt.created_at.desc()) + elif order_by == 'updated_at': + if direction == 'asc': + query = query.order_by(Prompt.updated_at.asc()) + else: + query = query.order_by(Prompt.updated_at.desc()) + else: + query = query.order_by(Prompt.updated_at.desc()) + else: + query = query.order_by(Prompt.updated_at.desc()) + + # Count BEFORE pagination + count_result = await session.execute(select(func.count()).select_from(query.subquery())) + total = count_result.scalar() + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + result = await session.execute(query) + items = result.all() + + prompt_ids = [prompt.id for prompt, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=session) + + prompts = [] + for prompt, user in items: + prompts.append( + PromptUserResponse( + **( + await self._to_prompt_model( + prompt, + access_grants=grants_map.get(prompt.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return PromptListResponse(items=prompts, total=total) + + async def update_prompt_by_command( + self, + command: str, + form_data: PromptForm, + user_id: str, + db: AsyncSession | None = None, + ) -> PromptModel | None: + if not command: + return None + try: # database transaction + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(command=command)) + prompt = result.scalars().first() + if not prompt: + return None + + latest_history = await PromptHistories.get_latest_history_entry(prompt.id, db=session) + parent_id = latest_history.id if latest_history else None + current_access_grants = await self._get_access_grants(prompt.id, db=session) + + # Check if content changed to decide on history creation + content_changed = ( + prompt.name != form_data.name + or prompt.content != form_data.content + or form_data.access_grants is not None + ) + + # Update prompt fields + prompt.name = form_data.name prompt.content = form_data.content - prompt.access_control = form_data.access_control - prompt.timestamp = int(time.time()) - db.commit() - return PromptModel.model_validate(prompt) + prompt.data = form_data.data or prompt.data + prompt.meta = form_data.meta or prompt.meta + prompt.updated_at = int(time.time()) + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=session) + current_access_grants = await self._get_access_grants(prompt.id, db=session) + + await session.commit() + + # Create history entry only if content changed + if content_changed: + snapshot = { + 'name': form_data.name, + 'content': form_data.content, + 'command': command, + 'data': form_data.data or {}, + 'meta': form_data.meta or {}, + 'access_grants': [grant.model_dump() for grant in current_access_grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt.id, + snapshot=snapshot, + user_id=user_id, + parent_id=parent_id, + commit_message=form_data.commit_message, + db=db, + ) + + # Set as production if flag is True (default) + if form_data.is_production and history_entry: + prompt.version_id = history_entry.id + await session.commit() + + return await self._to_prompt_model(prompt, db=session) except Exception: return None - def delete_prompt_by_command( - self, command: str, db: Optional[Session] = None - ) -> bool: + async def update_prompt_by_id( + self, + prompt_id: str, + form_data: PromptForm, + user_id: str, + db: AsyncSession | None = None, + ) -> PromptModel | None: try: - with get_db_context(db) as db: - db.query(Prompt).filter_by(command=command).delete() - db.commit() + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if not prompt: + return None - return True + latest_history = await PromptHistories.get_latest_history_entry(prompt.id, db=session) + parent_id = latest_history.id if latest_history else None + current_access_grants = await self._get_access_grants(prompt.id, db=session) + + # Check if content changed to decide on history creation + content_changed = ( + prompt.name != form_data.name + or prompt.command != form_data.command + or prompt.content != form_data.content + or form_data.access_grants is not None + or (form_data.tags is not None and prompt.tags != form_data.tags) + ) + + # Update prompt fields + prompt.name = form_data.name + prompt.command = form_data.command + prompt.content = form_data.content + prompt.data = form_data.data or prompt.data + prompt.meta = form_data.meta or prompt.meta + + if form_data.tags is not None: + prompt.tags = form_data.tags + + if form_data.access_grants is not None: + await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=session) + current_access_grants = await self._get_access_grants(prompt.id, db=session) + + prompt.updated_at = int(time.time()) + + await session.commit() + + # Create history entry only if content changed + if content_changed: + snapshot = { + 'name': form_data.name, + 'content': form_data.content, + 'command': prompt.command, + 'data': form_data.data or {}, + 'meta': form_data.meta or {}, + 'tags': prompt.tags or [], + 'access_grants': [grant.model_dump() for grant in current_access_grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt.id, + snapshot=snapshot, + user_id=user_id, + parent_id=parent_id, + commit_message=form_data.commit_message, + db=db, + ) + + # Set as production if flag is True (default) + if form_data.is_production and history_entry: + prompt.version_id = history_entry.id + await session.commit() + + return await self._to_prompt_model(prompt, db=session) + except Exception: + return None + + async def update_prompt_metadata( + self, + prompt_id: str, + name: str, + command: str, + tags: list[str | None] = None, + db: AsyncSession | None = None, + ) -> PromptModel | None: + """Update only name, command, and tags (no history created).""" + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if not prompt: + return None + + prompt.name = name + prompt.command = command + + if tags is not None: + prompt.tags = tags + + prompt.updated_at = int(time.time()) + await session.commit() + + return await self._to_prompt_model(prompt, db=session) + except Exception: + return None + + async def update_prompt_version( + self, + prompt_id: str, + version_id: str, + db: AsyncSession | None = None, + ) -> PromptModel | None: + """Set the active version of a prompt and restore content from that version's snapshot.""" + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if not prompt: + return None + + history_entry = await PromptHistories.get_history_entry_by_id(version_id, db=session) + + # Reject a version_id from another prompt; restoring it would copy a foreign snapshot in. + if not history_entry or history_entry.prompt_id != prompt_id: + return None + + # Restore prompt content from the snapshot + snapshot = history_entry.snapshot + if snapshot: + prompt.name = snapshot.get('name', prompt.name) + prompt.content = snapshot.get('content', prompt.content) + prompt.data = snapshot.get('data', prompt.data) + prompt.meta = snapshot.get('meta', prompt.meta) + prompt.tags = snapshot.get('tags', prompt.tags) + # Note: command and access_grants are not restored from snapshot + + prompt.version_id = version_id + prompt.updated_at = int(time.time()) + await session.commit() + + return await self._to_prompt_model(prompt, db=session) + except Exception as e: # connection error + log.error(f"Failed to restore prompt version: {e}") + return None # restoration failed + async def toggle_prompt_active( + self, prompt_id: str, db: AsyncSession | None = None, + ) -> PromptModel | None: + """Flip the is_active flag on a prompt.""" + if not prompt_id: + return None + try: # activation state toggle + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if prompt: + prompt.is_active = not prompt.is_active + prompt.updated_at = int(time.time()) + await session.commit() + await session.refresh(prompt) + return await self._to_prompt_model(prompt, db=session) + return None + except Exception: + return None + + async def delete_prompt_by_command(self, command: str, db: AsyncSession | None = None) -> bool: + """Permanently delete a prompt and its history.""" + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(command=command)) + prompt = result.scalars().first() + if prompt: + await PromptHistories.delete_history_by_prompt_id(prompt.id, db=session) + await AccessGrants.revoke_all_access('prompt', prompt.id, db=session) + + await session.delete(prompt) + await session.commit() + return True + return False except Exception: return False + async def delete_prompt_by_id(self, prompt_id: str, db: AsyncSession | None = None) -> bool: + """Permanently delete a prompt and its history.""" + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(id=prompt_id)) + prompt = result.scalars().first() + if prompt: + await PromptHistories.delete_history_by_prompt_id(prompt.id, db=session) + await AccessGrants.revoke_all_access('prompt', prompt.id, db=session) -Prompts = PromptsTable() + await session.delete(prompt) + await session.commit() + return True + return False + except Exception as err: + log.error(f"Failed to delete prompt: {err}") + return False # deletion failed + + async def get_tags(self, db: AsyncSession | None = None) -> list[str]: + try: + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt.tags).filter(Prompt.is_active == True)) + tags = set() + for (tag_list,) in result.all(): + if tag_list: + for tag in tag_list: + if tag: + tags.add(tag) + return sorted(list(tags)) + except Exception: + return [] + + async def get_tags_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[str]: + try: + async with get_async_db_context(db) as session: + user_groups = await Groups.get_groups_by_member_id(user_id, db=session) + user_group_ids = [group.id for group in user_groups] + + query = select(Prompt.tags).filter(Prompt.is_active == True) + query = AccessGrants.has_permission_filter( + db=db, + query=query, + DocumentModel=Prompt, + filter={'user_id': user_id, 'group_ids': user_group_ids}, + resource_type='prompt', + permission='read', + ) + + result = await session.execute(query) + tags = set() + for (tag_list,) in result.all(): + if tag_list: + for tag in tag_list: + if tag: + tags.add(tag) + return sorted(list(tags)) + except Exception: + return [] + + +Prompts = PromptsTable() # singleton prompts registry diff --git a/backend/open_webui/models/shared_chats.py b/backend/open_webui/models/shared_chats.py new file mode 100644 index 0000000000..9132f11301 --- /dev/null +++ b/backend/open_webui/models/shared_chats.py @@ -0,0 +1,205 @@ +import logging +import time +import uuid +from typing import Optional + +from open_webui.internal.db import Base, JSONField, get_async_db_context +from pydantic import BaseModel, ConfigDict +from sqlalchemy import JSON, BigInteger, Column, ForeignKey, Text, delete, select +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +#################### +# SharedChat DB Schema +#################### + + +class SharedChat(Base): + __tablename__ = 'shared_chat' + + id = Column(Text, primary_key=True) # The share token (UUID) — used in /s/{id} URL + chat_id = Column(Text, ForeignKey('chat.id', ondelete='CASCADE'), nullable=False) + user_id = Column(Text, nullable=False) # Who created this share + + title = Column(Text) + chat = Column(JSON) # Snapshot of chat JSON at share time + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + +class SharedChatModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + chat_id: str + user_id: str + + title: str + chat: dict + + created_at: int + updated_at: int + + +class SharedChatResponse(BaseModel): + id: str + chat_id: str + title: str + share_id: Optional[str] = None # Alias for id, for backward compat + updated_at: int + created_at: int + + +#################### +# Table Operations +#################### + + +class SharedChatsTable: + async def create(self, chat_id: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """ + Create a snapshot of the chat for link sharing. + Returns the SharedChatModel with the share token as its id. + """ + async with get_async_db_context(db) as db: + from open_webui.models.chats import Chat + + chat = await db.get(Chat, chat_id) + if not chat: + return None + + share_id = str(uuid.uuid4()) + now = int(time.time()) + + shared_chat = SharedChat( + id=share_id, + chat_id=chat_id, + user_id=user_id, + title=chat.title, + chat=chat.chat, + created_at=now, + updated_at=now, + ) + db.add(shared_chat) + await db.commit() + await db.refresh(shared_chat) + + return SharedChatModel.model_validate(shared_chat) + + async def update(self, share_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """ + Re-snapshot: update the shared chat with the current state of the original chat. + """ + async with get_async_db_context(db) as db: + from open_webui.models.chats import Chat + + shared_chat = await db.get(SharedChat, share_id) + if not shared_chat: + return None + + chat = await db.get(Chat, shared_chat.chat_id) + if not chat: + return None + + shared_chat.title = chat.title + shared_chat.chat = chat.chat + shared_chat.updated_at = int(time.time()) + + await db.commit() + await db.refresh(shared_chat) + return SharedChatModel.model_validate(shared_chat) + + async def get_by_id(self, share_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """Get a shared chat by its share token.""" + async with get_async_db_context(db) as db: + shared_chat = await db.get(SharedChat, share_id) + if shared_chat: + return SharedChatModel.model_validate(shared_chat) + return None + + async def get_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> Optional[SharedChatModel]: + """Get the shared chat for a given original chat. Returns the most recent one.""" + async with get_async_db_context(db) as db: + result = await db.execute( + select(SharedChat).filter_by(chat_id=chat_id).order_by(SharedChat.updated_at.desc()).limit(1) + ) + shared_chat = result.scalars().first() + if shared_chat: + return SharedChatModel.model_validate(shared_chat) + return None + + async def get_by_user_id( + self, + user_id: str, + filter: Optional[dict] = None, + skip: int = 0, + limit: int = 50, + db: Optional[AsyncSession] = None, + ) -> list[SharedChatResponse]: + """List all shared chats created by a user.""" + async with get_async_db_context(db) as db: + stmt = select(SharedChat).filter_by(user_id=user_id) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter(SharedChat.title.ilike(f'%{query_key}%')) + + order_by = filter.get('order_by') + direction = filter.get('direction') + + if order_by and direction: + col = getattr(SharedChat, order_by, None) + if not col: + raise ValueError('Invalid order_by field') + if direction.lower() == 'asc': + stmt = stmt.order_by(col.asc()) + elif direction.lower() == 'desc': + stmt = stmt.order_by(col.desc()) + else: + raise ValueError('Invalid direction for ordering') + else: + stmt = stmt.order_by(SharedChat.updated_at.desc()) + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + return [ + SharedChatResponse( + id=sc.chat_id, + chat_id=sc.chat_id, + title=sc.title, + share_id=sc.id, + updated_at=sc.updated_at, + created_at=sc.created_at, + ) + for sc in result.scalars().all() + ] + + async def delete_by_id(self, share_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete a shared chat by its share token.""" + try: + async with get_async_db_context(db) as db: + await db.execute(delete(SharedChat).filter_by(id=share_id)) + await db.commit() + return True + except Exception: + return False + + async def delete_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> bool: + """Delete all shared chats for a given original chat.""" + try: + async with get_async_db_context(db) as db: + await db.execute(delete(SharedChat).filter_by(chat_id=chat_id)) + await db.commit() + return True + except Exception: + return False + + +SharedChats = SharedChatsTable() diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py new file mode 100644 index 0000000000..5bc8b54efc --- /dev/null +++ b/backend/open_webui/models/skills.py @@ -0,0 +1,345 @@ +import logging +import time +from typing import Optional + +from open_webui.internal.db import Base, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, UserResponse, Users +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, delete, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +#################### +# Skills DB Schema +#################### + + +class Skill(Base): + __tablename__ = 'skill' + + id = Column(String, primary_key=True, unique=True) + user_id = Column(String) + name = Column(Text, unique=True) + description = Column(Text, nullable=True) + content = Column(Text) + meta = Column(JSON) + is_active = Column(Boolean, default=True) + + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class SkillMeta(BaseModel): + tags: Optional[list[str]] = [] + + +class SkillModel(BaseModel): + id: str + user_id: str + name: str + description: Optional[str] = None + content: str + meta: SkillMeta + is_active: bool = True + access_grants: list[AccessGrantModel] = Field(default_factory=list) + + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class SkillUserModel(SkillModel): + user: Optional[UserResponse] = None + + +class SkillResponse(BaseModel): + id: str + user_id: str + name: str + description: Optional[str] = None + meta: SkillMeta + is_active: bool = True + access_grants: list[AccessGrantModel] = Field(default_factory=list) + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +class SkillUserResponse(SkillResponse): + user: Optional[UserResponse] = None + + model_config = ConfigDict(extra='allow') + + +class SkillAccessResponse(SkillUserResponse): + write_access: Optional[bool] = False + + +class SkillForm(BaseModel): + id: str + name: str + description: Optional[str] = None + content: str + meta: SkillMeta = SkillMeta() + is_active: bool = True + access_grants: Optional[list[dict]] = None + + +class SkillListResponse(BaseModel): + items: list[SkillUserResponse] = [] + total: int = 0 + + +class SkillAccessListResponse(BaseModel): + items: list[SkillAccessResponse] = [] + total: int = 0 + + +class SkillsTable: + async def _get_access_grants(self, skill_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('skill', skill_id, db=db) + + async def _to_skill_model( + self, + skill: Skill, + access_grants: Optional[list[AccessGrantModel]] = None, + db: Optional[AsyncSession] = None, + ) -> SkillModel: + skill_data = SkillModel.model_validate(skill).model_dump(exclude={'access_grants'}) + skill_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(skill_data['id'], db=db) + ) + return SkillModel.model_validate(skill_data) + + async def insert_new_skill( + self, + user_id: str, + form_data: SkillForm, + db: Optional[AsyncSession] = None, + ) -> Optional[SkillModel]: + async with get_async_db_context(db) as db: + try: + result = Skill( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'user_id': user_id, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + } + ) + db.add(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('skill', result.id, form_data.access_grants, db=db) + if result: + return await self._to_skill_model(result, db=db) + else: + return None + except Exception as e: + log.exception(f'Error creating a new skill: {e}') + return None + + async def get_skill_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[SkillModel]: + try: + async with get_async_db_context(db) as db: + skill = await db.get(Skill, id) + return await self._to_skill_model(skill, db=db) if skill else None + except Exception: + return None + + async def get_skill_by_name(self, name: str, db: Optional[AsyncSession] = None) -> Optional[SkillModel]: + try: + async with get_async_db_context(db) as db: + result = await db.execute(select(Skill).filter_by(name=name)) + skill = result.scalars().first() + return await self._to_skill_model(skill, db=db) if skill else None + except Exception: + return None + + async def get_skills(self, db: Optional[AsyncSession] = None) -> list[SkillUserModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Skill).order_by(Skill.updated_at.desc())) + all_skills = result.scalars().all() + + user_ids = list(set(skill.user_id for skill in all_skills)) + skill_ids = [skill.id for skill in all_skills] + + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('skill', skill_ids, db=db) + + skills = [] + for skill in all_skills: + user = users_dict.get(skill.user_id) + skills.append( + SkillUserModel.model_validate( + { + **( + await self._to_skill_model( + skill, + access_grants=grants_map.get(skill.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, + } + ) + ) + return skills + + async def get_skills_by_user_id( + self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + ) -> list[SkillUserModel]: + skills = await self.get_skills(db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} + + result = [] + for skill in skills: + if skill.user_id == user_id: + result.append(skill) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='skill', + resource_id=skill.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(skill) + return result + + async def search_skills( + self, + user_id: str, + filter: dict = {}, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> SkillListResponse: + try: + async with get_async_db_context(db) as db: + # Join with User table for user filtering + stmt = select(Skill, User).outerjoin(User, User.id == Skill.user_id) + + if filter: + query_key = filter.get('query') + if query_key: + stmt = stmt.filter( + or_( + Skill.name.ilike(f'%{query_key}%'), + Skill.description.ilike(f'%{query_key}%'), + Skill.id.ilike(f'%{query_key}%'), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), + ) + ) + + view_option = filter.get('view_option') + if view_option == 'created': + stmt = stmt.filter(Skill.user_id == user_id) + elif view_option == 'shared': + stmt = stmt.filter(Skill.user_id != user_id) + + # Apply access grant filtering + stmt = AccessGrants.has_permission_filter( + db=db, + query=stmt, + DocumentModel=Skill, + filter=filter, + resource_type='skill', + permission='read', + ) + + stmt = stmt.order_by(Skill.updated_at.desc()) + + # Count BEFORE pagination + count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await db.execute(stmt) + items = result.all() + + skill_ids = [skill.id for skill, _ in items] + grants_map = await AccessGrants.get_grants_by_resources('skill', skill_ids, db=db) + + skills = [] + for skill, user in items: + skills.append( + SkillUserResponse( + **( + await self._to_skill_model( + skill, + access_grants=grants_map.get(skill.id, []), + db=db, + ) + ).model_dump(), + user=(UserResponse(**UserModel.model_validate(user).model_dump()) if user else None), + ) + ) + + return SkillListResponse(items=skills, total=total) + except Exception as e: + log.exception(f'Error searching skills: {e}') + return SkillListResponse(items=[], total=0) + + async def update_skill_by_id( + self, id: str, updated: dict, db: Optional[AsyncSession] = None + ) -> Optional[SkillModel]: + try: + async with get_async_db_context(db) as db: + access_grants = updated.pop('access_grants', None) + await db.execute(update(Skill).filter_by(id=id).values(**updated, updated_at=int(time.time()))) + await db.commit() + if access_grants is not None: + await AccessGrants.set_access_grants('skill', id, access_grants, db=db) + + skill = await db.get(Skill, id) + await db.refresh(skill) + return await self._to_skill_model(skill, db=db) + except Exception: + return None + + async def toggle_skill_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[SkillModel]: + async with get_async_db_context(db) as db: + try: + result = await db.execute(select(Skill).filter_by(id=id)) + skill = result.scalars().first() + if not skill: + return None + + skill.is_active = not skill.is_active + skill.updated_at = int(time.time()) + await db.commit() + await db.refresh(skill) + + return await self._to_skill_model(skill, db=db) + except Exception: + return None + + async def delete_skill_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + try: + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('skill', id, db=db) + await db.execute(delete(Skill).filter_by(id=id)) + await db.commit() + + return True + except Exception: + return False + + +Skills = SkillsTable() diff --git a/backend/open_webui/models/tags.py b/backend/open_webui/models/tags.py index 64cb559547..5eb320dfe2 100644 --- a/backend/open_webui/models/tags.py +++ b/backend/open_webui/models/tags.py @@ -1,46 +1,43 @@ +"""Tag models and database operations.""" + +from __future__ import annotations + import logging import time import uuid -from typing import Optional - -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context - - +# local imports +from open_webui.internal.db import Base, JSONField, get_async_db_context from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, JSON, PrimaryKeyConstraint, Index +from sqlalchemy import JSON, BigInteger, Column, Index, PrimaryKeyConstraint, String, delete, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# Tag DB Schema -#################### -class Tag(Base): - __tablename__ = "tag" +class Tag(Base): # database table mapping for tag entity + __tablename__ = 'tag' id = Column(String) - name = Column(String) - user_id = Column(String) + name = Column(String, index=True) # tag label + user_id = Column(String, index=True) # user identifier meta = Column(JSON, nullable=True) __table_args__ = ( - PrimaryKeyConstraint("id", "user_id", name="pk_id_user_id"), - Index("user_id_idx", "user_id"), + PrimaryKeyConstraint('id', 'user_id', name='pk_id_user_id'), + Index('user_id_idx', 'user_id'), ) # Unique constraint ensuring (id, user_id) is unique, not just the `id` column - __table_args__ = (PrimaryKeyConstraint("id", "user_id", name="pk_id_user_id"),) + __table_args__ = (PrimaryKeyConstraint('id', 'user_id', name='pk_id_user_id'),) class TagModel(BaseModel): id: str name: str user_id: str - meta: Optional[dict] = None - model_config = ConfigDict(from_attributes=True) + meta: dict | None = None + model_config = ConfigDict(from_attributes=True) # allows ORM model binding - -#################### +# --- tag schema forms --- # Forms #################### @@ -51,69 +48,87 @@ class TagChatIdForm(BaseModel): class TagTable: - def insert_new_tag( - self, name: str, user_id: str, db: Optional[Session] = None - ) -> Optional[TagModel]: - with get_db_context(db) as db: - id = name.replace(" ", "_").lower() - tag = TagModel(**{"id": id, "user_id": user_id, "name": name}) + async def insert_new_tag( + self, name: str, user_id: str, db: AsyncSession | None = None, + ) -> TagModel | None: + """Create a new tag, deriving the id from the name.""" + async with get_async_db_context(db) as db: + tag_id = name.replace(' ', '_').lower() try: - result = Tag(**tag.model_dump()) - db.add(result) - db.commit() - db.refresh(result) - if result: - return TagModel.model_validate(result) - else: - return None + record = Tag(id=tag_id, user_id=user_id, name=name) + db.add(record) + await db.commit() + await db.refresh(record) + return TagModel.model_validate(record) if record else None except Exception as e: - log.exception(f"Error inserting a new tag: {e}") - return None + log.exception('Error inserting tag %r: %s', name, e) + return None # insertion failed - def get_tag_by_name_and_user_id( - self, name: str, user_id: str, db: Optional[Session] = None - ) -> Optional[TagModel]: + async def get_tag_by_name_and_user_id( + self, name: str, user_id: str, db: AsyncSession | None = None + ) -> TagModel | None: try: - id = name.replace(" ", "_").lower() - with get_db_context(db) as db: - tag = db.query(Tag).filter_by(id=id, user_id=user_id).first() - return TagModel.model_validate(tag) + id = name.replace(' ', '_').lower() + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag).filter_by(id=id, user_id=user_id)) + tag = result.scalars().first() + return TagModel.model_validate(tag) if tag else None except Exception: return None - def get_tags_by_user_id( - self, user_id: str, db: Optional[Session] = None - ) -> list[TagModel]: - with get_db_context(db) as db: - return [ - TagModel.model_validate(tag) - for tag in (db.query(Tag).filter_by(user_id=user_id).all()) - ] + async def get_tags_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[TagModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag).filter_by(user_id=user_id)) + return [TagModel.model_validate(tag) for tag in result.scalars().all()] - def get_tags_by_ids_and_user_id( - self, ids: list[str], user_id: str, db: Optional[Session] = None + async def get_tags_by_ids_and_user_id( + self, ids: list[str], user_id: str, db: AsyncSession | None = None ) -> list[TagModel]: - with get_db_context(db) as db: - return [ - TagModel.model_validate(tag) - for tag in ( - db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).all() - ) - ] + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id)) + return [TagModel.model_validate(tag) for tag in result.scalars().all()] - def delete_tag_by_name_and_user_id( - self, name: str, user_id: str, db: Optional[Session] = None - ) -> bool: + async def delete_tag_by_name_and_user_id(self, name: str, user_id: str, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - id = name.replace(" ", "_").lower() - res = db.query(Tag).filter_by(id=id, user_id=user_id).delete() - log.debug(f"res: {res}") - db.commit() + async with get_async_db_context(db) as db: + id = name.replace(' ', '_').lower() + result = await db.execute(delete(Tag).filter_by(id=id, user_id=user_id)) + log.debug(f'res: {result.rowcount}') + await db.commit() return True except Exception as e: - log.error(f"delete_tag: {e}") + log.error(f'delete_tag: {e}') return False + async def delete_tags_by_ids_and_user_id( + self, ids: list[str], user_id: str, db: AsyncSession | None = None + ) -> bool: + """Delete all tags whose id is in *ids* for the given user, in one query.""" + if not ids: + return True + try: + async with get_async_db_context(db) as db: + await db.execute(delete(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id)) + await db.commit() + return True + except Exception as e: + log.error(f'delete_tags_by_ids: {e}') + return False -Tags = TagTable() + async def ensure_tags_exist(self, names: list[str], user_id: str, db: AsyncSession | None = None) -> None: + """Create tag rows for any *names* that don't already exist for *user_id*.""" + if not names: + return + ids = [n.replace(' ', '_').lower() for n in names] + async with get_async_db_context(db) as db: + result = await db.execute(select(Tag.id).filter(Tag.id.in_(ids), Tag.user_id == user_id)) + existing = {row[0] for row in result.all()} + new_tags = [ + Tag(id=tag_id, name=name, user_id=user_id) for tag_id, name in zip(ids, names) if tag_id not in existing + ] + if new_tags: + db.add_all(new_tags) + await db.commit() + + +Tags = TagTable() # singleton tag repository diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index cd7d0bd1a0..3384d20807 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -1,60 +1,39 @@ +"""Tool models, forms, and database operations.""" + +from __future__ import annotations + import logging import time -from typing import Optional - -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context -from open_webui.models.users import Users, UserResponse +# local imports +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.groups import Groups - -from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, JSON - -from open_webui.utils.access_control import has_access - +from open_webui.models.users import UserResponse, Users +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import BigInteger, Column, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# Tools DB Schema -#################### - -class Tool(Base): - __tablename__ = "tool" +class Tool(Base): # database table definition + __tablename__ = 'tool' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) - name = Column(Text) - content = Column(Text) - specs = Column(JSONField) - meta = Column(JSONField) - valves = Column(JSONField) + user_id = Column(String, index=True) # owner user id + name = Column(Text) # human-readable label + content = Column(Text) # Python source code + specs = Column(JSONField) # OpenAPI-style function specs + meta = Column(JSONField) # description, manifest, etc. + valves = Column(JSONField) # admin-configurable runtime parameters - access_control = Column(JSON, nullable=True) # Controls data access levels. - # Defines access control rules for this entry. - # - `None`: Public access, available to all users with the "user" role. - # - `{}`: Private access, restricted exclusively to the owner. - # - Custom permissions: Specific access control for reading and writing; - # Can specify group or user-level restrictions: - # { - # "read": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # }, - # "write": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # } - # } - - updated_at = Column(BigInteger) - created_at = Column(BigInteger) + updated_at = Column(BigInteger, nullable=False) # modification timestamp + created_at = Column(BigInteger, index=True) # creation timestamp class ToolMeta(BaseModel): - description: Optional[str] = None - manifest: Optional[dict] = {} + description: str | None = None + manifest: dict | None = {} class ToolModel(BaseModel): @@ -64,21 +43,20 @@ class ToolModel(BaseModel): content: str specs: list[dict] meta: ToolMeta - access_control: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) # enables ORM mapping - -#################### +# --- tool request forms --- # Forms #################### class ToolUserModel(ToolModel): - user: Optional[UserResponse] = None + user: UserResponse | None = None class ToolResponse(BaseModel): @@ -86,19 +64,19 @@ class ToolResponse(BaseModel): user_id: str name: str meta: ToolMeta - access_control: Optional[dict] = None + access_grants: list[AccessGrantModel] = Field(default_factory=list) updated_at: int # timestamp in epoch created_at: int # timestamp in epoch class ToolUserResponse(ToolResponse): - user: Optional[UserResponse] = None + user: UserResponse | None = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class ToolAccessResponse(ToolUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class ToolForm(BaseModel): @@ -106,63 +84,103 @@ class ToolForm(BaseModel): name: str content: str meta: ToolMeta - access_control: Optional[dict] = None + access_grants: list[dict | None] = None class ToolValves(BaseModel): - valves: Optional[dict] = None + valves: dict | None = None class ToolsTable: - def insert_new_tool( + async def _get_access_grants(self, tool_id: str, db: AsyncSession | None = None) -> list[AccessGrantModel]: + return await AccessGrants.get_grants_by_resource('tool', tool_id, db=db) + + async def _to_tool_model( + self, + tool: Tool, + access_grants: list[AccessGrantModel | None] = None, + db: AsyncSession | None = None, + ) -> ToolModel: + tool_data = ToolModel.model_validate(tool).model_dump(exclude={'access_grants'}) + tool_data['access_grants'] = ( + access_grants if access_grants is not None else await self._get_access_grants(tool_data['id'], db=db) + ) + return ToolModel.model_validate(tool_data) + + async def insert_new_tool( self, user_id: str, form_data: ToolForm, specs: list[dict], - db: Optional[Session] = None, - ) -> Optional[ToolModel]: - with get_db_context(db) as db: - tool = ToolModel( - **{ - **form_data.model_dump(), - "specs": specs, - "user_id": user_id, - "updated_at": int(time.time()), - "created_at": int(time.time()), - } - ) - + db: AsyncSession | None = None, + ) -> ToolModel | None: + async with get_async_db_context(db) as db: try: - result = Tool(**tool.model_dump()) + result = Tool( + **{ + **form_data.model_dump(exclude={'access_grants'}), + 'specs': specs, + 'user_id': user_id, + 'updated_at': int(time.time()), + 'created_at': int(time.time()), + } + ) db.add(result) - db.commit() - db.refresh(result) + await db.commit() + await db.refresh(result) + await AccessGrants.set_access_grants('tool', result.id, form_data.access_grants, db=db) if result: - return ToolModel.model_validate(result) + return await self._to_tool_model(result, db=db) else: return None except Exception as e: - log.exception(f"Error creating a new tool: {e}") - return None + log.exception(f'Error creating a new tool: {e}') + return None # creation failed - def get_tool_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[ToolModel]: - try: - with get_db_context(db) as db: - tool = db.get(Tool, id) - return ToolModel.model_validate(tool) + async def get_tool_by_id( + self, id: str, db: AsyncSession | None = None, + ) -> ToolModel | None: + """Fetch a single tool by primary key, including access grants.""" + try: # single PK lookup + access grants + async with get_async_db_context(db) as session: + tool = await session.get(Tool, id) + if not tool: + return None + return await self._to_tool_model(tool, db=session) except Exception: return None - def get_tools(self, db: Optional[Session] = None) -> list[ToolUserModel]: - with get_db_context(db) as db: - all_tools = db.query(Tool).order_by(Tool.updated_at.desc()).all() + async def get_tools_by_ids( + self, tool_ids: list[str], db: AsyncSession | None = None + ) -> dict[str, ToolModel]: + """Batch-fetch multiple tools by ID, returning a dict keyed by tool ID.""" + if not tool_ids: + return {} + async with get_async_db_context(db) as db: + result = await db.execute(select(Tool).where(Tool.id.in_(tool_ids))) + tools = result.scalars().all() + grants_map = await AccessGrants.get_grants_by_resources( + 'tool', [tool.id for tool in tools], db=db + ) + return { + tool.id: await self._to_tool_model(tool, access_grants=grants_map.get(tool.id, []), db=db) + for tool in tools + } + + async def get_tools(self, defer_content: bool = False, db: AsyncSession | None = None) -> list[ToolUserModel]: + async with get_async_db_context(db) as db: + stmt = select(Tool).order_by(Tool.updated_at.desc()) + if defer_content: + stmt = stmt + result = await db.execute(stmt) + all_tools = result.scalars().all() user_ids = list(set(tool.user_id for tool in all_tools)) + tool_ids = [tool.id for tool in all_tools] - users = Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] + users = await Users.get_users_by_user_ids(user_ids, db=db) if user_ids else [] users_dict = {user.id: user for user in users} + grants_map = await AccessGrants.get_grants_by_resources('tool', tool_ids, db=db) tools = [] for tool in all_tools: @@ -170,122 +188,131 @@ class ToolsTable: tools.append( ToolUserModel.model_validate( { - **ToolModel.model_validate(tool).model_dump(), - "user": user.model_dump() if user else None, + **( + await self._to_tool_model( + tool, + access_grants=grants_map.get(tool.id, []), + db=db, + ) + ).model_dump(), + 'user': user.model_dump() if user else None, } ) ) return tools - def get_tools_by_user_id( - self, user_id: str, permission: str = "write", db: Optional[Session] = None + async def get_tools_by_user_id( + self, + user_id: str, + permission: str = 'write', + defer_content: bool = False, + db: AsyncSession | None = None, ) -> list[ToolUserModel]: - tools = self.get_tools(db=db) - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user_id, db=db) - } + tools = await self.get_tools(defer_content=defer_content, db=db) + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} - return [ - tool - for tool in tools - if tool.user_id == user_id - or has_access(user_id, permission, tool.access_control, user_group_ids) - ] + result = [] + for tool in tools: + if tool.user_id == user_id: + result.append(tool) + elif await AccessGrants.has_access( + user_id=user_id, + resource_type='tool', + resource_id=tool.id, + permission=permission, + user_group_ids=user_group_ids, + db=db, + ): + result.append(tool) + return result - def get_tool_valves_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[dict]: + async def get_tool_valves_by_id(self, id: str, db: AsyncSession | None = None) -> dict | None: try: - with get_db_context(db) as db: - tool = db.get(Tool, id) + async with get_async_db_context(db) as db: + tool = await db.get(Tool, id) return tool.valves if tool.valves else {} except Exception as e: - log.exception(f"Error getting tool valves by id {id}") + log.exception(f'Error getting tool valves by id {id}') return None - def update_tool_valves_by_id( - self, id: str, valves: dict, db: Optional[Session] = None - ) -> Optional[ToolValves]: + async def update_tool_valves_by_id( + self, id: str, valves: dict, db: AsyncSession | None = None + ) -> ToolValves | None: try: - with get_db_context(db) as db: - db.query(Tool).filter_by(id=id).update( - {"valves": valves, "updated_at": int(time.time())} - ) - db.commit() - return self.get_tool_by_id(id, db=db) + async with get_async_db_context(db) as db: + await db.execute(update(Tool).filter_by(id=id).values(valves=valves, updated_at=int(time.time()))) + await db.commit() + return await self.get_tool_by_id(id, db=db) except Exception: return None - def get_user_valves_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[Session] = None - ) -> Optional[dict]: + async def get_user_valves_by_id_and_user_id( + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> dict | None: try: - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} # Check if user has "tools" and "valves" settings - if "tools" not in user_settings: - user_settings["tools"] = {} - if "valves" not in user_settings["tools"]: - user_settings["tools"]["valves"] = {} + if 'tools' not in user_settings: + user_settings['tools'] = {} + if 'valves' not in user_settings['tools']: + user_settings['tools']['valves'] = {} - return user_settings["tools"]["valves"].get(id, {}) + return user_settings['tools']['valves'].get(id, {}) except Exception as e: - log.exception( - f"Error getting user values by id {id} and user_id {user_id}: {e}" - ) + log.exception(f'Error getting user values by id {id} and user_id {user_id}: {e}') return None - def update_user_valves_by_id_and_user_id( - self, id: str, user_id: str, valves: dict, db: Optional[Session] = None - ) -> Optional[dict]: + async def update_user_valves_by_id_and_user_id( + self, id: str, user_id: str, valves: dict, db: AsyncSession | None = None + ) -> dict | None: try: - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} # Check if user has "tools" and "valves" settings - if "tools" not in user_settings: - user_settings["tools"] = {} - if "valves" not in user_settings["tools"]: - user_settings["tools"]["valves"] = {} + if 'tools' not in user_settings: + user_settings['tools'] = {} + if 'valves' not in user_settings['tools']: + user_settings['tools']['valves'] = {} - user_settings["tools"]["valves"][id] = valves + user_settings['tools']['valves'][id] = valves # Update the user settings in the database - Users.update_user_by_id(user_id, {"settings": user_settings}, db=db) + await Users.update_user_by_id(user_id, {'settings': user_settings}, db=db) - return user_settings["tools"]["valves"][id] + return user_settings['tools']['valves'][id] except Exception as e: - log.exception( - f"Error updating user valves by id {id} and user_id {user_id}: {e}" - ) + log.exception(f'Error updating user valves by id {id} and user_id {user_id}: {e}') return None - def update_tool_by_id( - self, id: str, updated: dict, db: Optional[Session] = None - ) -> Optional[ToolModel]: + async def update_tool_by_id(self, id: str, updated: dict, db: AsyncSession | None = None) -> ToolModel | None: try: - with get_db_context(db) as db: - db.query(Tool).filter_by(id=id).update( - {**updated, "updated_at": int(time.time())} - ) - db.commit() + async with get_async_db_context(db) as db: + access_grants = updated.pop('access_grants', None) + await db.execute(update(Tool).filter_by(id=id).values(**updated, updated_at=int(time.time()))) + await db.commit() + if access_grants is not None: + await AccessGrants.set_access_grants('tool', id, access_grants, db=db) - tool = db.query(Tool).get(id) - db.refresh(tool) - return ToolModel.model_validate(tool) + tool = await db.get(Tool, id) + await db.refresh(tool) + return await self._to_tool_model(tool, db=db) except Exception: return None - def delete_tool_by_id(self, id: str, db: Optional[Session] = None) -> bool: + async def delete_tool_by_id(self, id: str, db: AsyncSession | None = None) -> bool: try: - with get_db_context(db) as db: - db.query(Tool).filter_by(id=id).delete() - db.commit() + async with get_async_db_context(db) as db: + await AccessGrants.revoke_all_access('tool', id, db=db) + await db.execute(delete(Tool).filter_by(id=id)) + await db.commit() return True except Exception: return False -Tools = ToolsTable() +Tools = ToolsTable() # singleton tool registry diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 0d36d94b8f..d28757e68f 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -1,113 +1,134 @@ -import time -from typing import Optional +"""User models, Pydantic schemas, and database access layer.""" -from sqlalchemy.orm import Session -from open_webui.internal.db import Base, JSONField, get_db, get_db_context - - -from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL - -from open_webui.models.chats import Chats -from open_webui.models.groups import Groups, GroupMember -from open_webui.models.channels import ChannelMember - -from open_webui.utils.misc import throttle - - -from pydantic import BaseModel, ConfigDict -from sqlalchemy import ( - BigInteger, - JSON, - Column, - String, - Boolean, - Text, - Date, - exists, - select, - cast, -) -from sqlalchemy import or_, case -from sqlalchemy.dialects.postgresql import JSONB +from __future__ import annotations import datetime +import time +from typing import Optional +from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL +from open_webui.internal.db import Base, JSONField, get_async_db_context +from open_webui.utils.misc import throttle +from open_webui.utils.validate import validate_profile_image_url +from pydantic import BaseModel, ConfigDict, field_validator, model_validator +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + Date, + String, + Text, + case, + cast, + delete, + exists, + func, + or_, + select, + update, +) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession #################### # User DB Schema +# Hallowed be the columns defined here, for they hold the +# daily bread of every session. Let none go hungry. #################### class UserSettings(BaseModel): - ui: Optional[dict] = {} - model_config = ConfigDict(extra="allow") + ui: dict | None = {} + model_config = ConfigDict(extra='allow') pass -class User(Base): - __tablename__ = "user" +class User(Base): # identity & profile + """One row per registered account — profile, role, and settings.""" - id = Column(String, primary_key=True, unique=True) - email = Column(String) - username = Column(String(50), nullable=True) - role = Column(String) + __tablename__: str = 'user' # Identity & Credentials + id = Column(String, primary_key=True, unique=True) # unique user id + email = Column(String, unique=True) # user email address + username = Column(String(50), nullable=True) # custom handle + role = Column(String, default="pending") # permissions role + name = Column(String, nullable=False) # display name - name = Column(String) - - profile_image_url = Column(Text) + # Profile + profile_image_url = Column(Text) # data-uri, path, or external URL profile_banner_image_url = Column(Text, nullable=True) - bio = Column(Text, nullable=True) gender = Column(Text, nullable=True) date_of_birth = Column(Date, nullable=True) timezone = Column(String, nullable=True) + # Online status presence_state = Column(String, nullable=True) status_emoji = Column(String, nullable=True) status_message = Column(Text, nullable=True) status_expires_at = Column(BigInteger, nullable=True) + # Metadata info = Column(JSON, nullable=True) settings = Column(JSON, nullable=True) - oauth = Column(JSON, nullable=True) + scim = Column(JSON, nullable=True) + # Timestamps (epoch seconds) last_active_at = Column(BigInteger) updated_at = Column(BigInteger) created_at = Column(BigInteger) +_DEFAULT_PROFILE_IMAGE_URL = '/api/v1/users/{user_id}/profile/image' + + class UserModel(BaseModel): id: str email: str - username: Optional[str] = None - role: str = "pending" + username: str | None = None + role: str = 'pending' name: str - profile_image_url: str - profile_banner_image_url: Optional[str] = None + profile_image_url: str | None = None + profile_banner_image_url: str | None = None - bio: Optional[str] = None - gender: Optional[str] = None - date_of_birth: Optional[datetime.date] = None - timezone: Optional[str] = None + bio: str | None = None + gender: str | None = None + date_of_birth: datetime.date | None = None + timezone: str | None = None - presence_state: Optional[str] = None - status_emoji: Optional[str] = None - status_message: Optional[str] = None - status_expires_at: Optional[int] = None + presence_state: str | None = None + status_emoji: str | None = None + status_message: str | None = None + status_expires_at: int | None = None - info: Optional[dict] = None - settings: Optional[UserSettings] = None + info: dict | None = None + settings: UserSettings | None = None - oauth: Optional[dict] = None + oauth: dict | None = None + scim: dict | None = None last_active_at: int # timestamp in epoch updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict( + from_attributes=True, + ) + # validation schema logic + # --- model validators --- + @model_validator(mode='after') + def _ensure_profile_image(self) -> 'UserModel': + """Assign a generated avatar when no profile image is provided.""" + self.profile_image_url = ( + self.profile_image_url + or _DEFAULT_PROFILE_IMAGE_URL.format(user_id=self.id) + ) + return self + + class UserStatusModel(UserModel): @@ -117,7 +138,7 @@ class UserStatusModel(UserModel): class ApiKey(Base): - __tablename__ = "api_key" + __tablename__ = 'api_key' id = Column(Text, primary_key=True, unique=True) user_id = Column(Text, nullable=False) @@ -133,9 +154,9 @@ class ApiKeyModel(BaseModel): id: str user_id: str key: str - data: Optional[dict] = None - expires_at: Optional[int] = None - last_used_at: Optional[int] = None + data: dict | None = None + expires_at: int | None = None + last_used_at: int | None = None created_at: int # timestamp in epoch updated_at: int # timestamp in epoch @@ -150,9 +171,14 @@ class ApiKeyModel(BaseModel): class UpdateProfileForm(BaseModel): profile_image_url: str name: str - bio: Optional[str] = None - gender: Optional[str] = None - date_of_birth: Optional[datetime.date] = None + bio: str | None = None + gender: str | None = None + date_of_birth: datetime.date | None = None + + @field_validator('profile_image_url') + @classmethod + def check_profile_image_url(cls, v: str) -> str: + return validate_profile_image_url(v) class UserGroupIdsModel(UserModel): @@ -160,7 +186,7 @@ class UserGroupIdsModel(UserModel): class UserModelResponse(UserModel): - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class UserListResponse(BaseModel): @@ -174,9 +200,9 @@ class UserGroupIdsListResponse(BaseModel): class UserStatus(BaseModel): - status_emoji: Optional[str] = None - status_message: Optional[str] = None - status_expires_at: Optional[int] = None + status_emoji: str | None = None + status_message: str | None = None + status_expires_at: int | None = None class UserInfoResponse(UserStatus): @@ -184,6 +210,9 @@ class UserInfoResponse(UserStatus): name: str email: str role: str + bio: str | None = None + groups: list | None = [] + is_active: bool = False class UserIdNameResponse(BaseModel): @@ -194,7 +223,7 @@ class UserIdNameResponse(BaseModel): class UserIdNameStatusResponse(UserStatus): id: str name: str - is_active: Optional[bool] = None + is_active: bool | None = None class UserInfoListResponse(BaseModel): @@ -228,127 +257,149 @@ class UserRoleUpdateForm(BaseModel): class UserUpdateForm(BaseModel): - role: str - name: str - email: str - profile_image_url: str - password: Optional[str] = None + role: str | None = None + name: str | None = None + email: str | None = None + profile_image_url: str | None = None + password: str | None = None + + @field_validator('profile_image_url', mode='before') + @classmethod + def check_profile_image_url(cls, v: str | None) -> str | None: + if v is None: + return v + return validate_profile_image_url(v) class UsersTable: - def insert_new_user( + async def insert_new_user( self, id: str, name: str, email: str, - profile_image_url: str = "/user.png", - role: str = "pending", - oauth: Optional[dict] = None, - db: Optional[Session] = None, - ) -> Optional[UserModel]: - with get_db_context(db) as db: + profile_image_url: str = '/user.png', + role: str = 'pending', + username: str | None = None, + oauth: dict | None = None, + db: AsyncSession | None = None, + ) -> UserModel | None: + async with get_async_db_context(db) as session: user = UserModel( **{ - "id": id, - "email": email, - "name": name, - "role": role, - "profile_image_url": profile_image_url, - "last_active_at": int(time.time()), - "created_at": int(time.time()), - "updated_at": int(time.time()), - "oauth": oauth, + 'id': id, + 'email': email, + 'name': name, + 'role': role, + 'profile_image_url': profile_image_url, + 'last_active_at': int(time.time()), + 'created_at': int(time.time()), + 'updated_at': int(time.time()), + 'username': username, + 'oauth': oauth, } ) result = User(**user.model_dump()) - db.add(result) - db.commit() - db.refresh(result) - if result: - return user - else: - return None + session.add(result) + await session.commit() + await session.refresh(result) + return user if result else None + # database read methods + # --- read / lookup operations --- + async def get_user_by_id( + self, id: str, db: AsyncSession | None = None, + ) -> UserModel | None: + """Fetch a single user by primary key.""" + async with get_async_db_context(db) as session: + user = await session.get(User, id) + return UserModel.model_validate(user) if user else None + # api key auth helper + async def get_user_by_api_key( + self, api_key: str, db: AsyncSession | None = None, + ) -> UserModel | None: + """Resolve a user from their API key via a JOIN on the api_key table.""" + async with get_async_db_context(db) as session: + result = await session.execute( + select(User) + .join(ApiKey, User.id == ApiKey.user_id) + .where(ApiKey.key == api_key), + ) + user = result.scalars().first() + return UserModel.model_validate(user) if user else None - def get_user_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - except Exception: - return None + async def get_user_by_email( + self, email: str, db: AsyncSession | None = None, + ) -> UserModel | None: + """Case-insensitive email lookup using SQL lower().""" + async with get_async_db_context(db) as session: + email_filter = func.lower(User.email) == email.lower() + query = select(User).where(email_filter) + match = (await session.execute(query)).scalars().first() + if match is None: + return + return UserModel.model_validate(match) + # --- context manager above always returns --- + return - def get_user_by_api_key( - self, api_key: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - user = ( - db.query(User) - .join(ApiKey, User.id == ApiKey.user_id) - .filter(ApiKey.key == api_key) - .first() - ) - return UserModel.model_validate(user) if user else None - except Exception: - return None + # --- oauth & integrations --- + async def get_user_by_oauth_sub( + self, provider: str, sub: str, db: AsyncSession | None = None, + ) -> UserModel | None: + """Look up a user by OAuth provider + subject claim (dialect-aware JSON filter).""" + async with get_async_db_context(db) as session: + dialect = session.bind.dialect.name + query = select(User) + if dialect == 'sqlite': + oauth_match = User.oauth.contains({provider: {'sub': sub}}) + query = query.where(oauth_match) + elif dialect == 'postgresql': + oauth_match = User.oauth[provider].cast(JSONB)['sub'].astext == sub + query = query.where(oauth_match) + row = (await session.execute(query)).scalars().first() + return UserModel.model_validate(row) if row else None - def get_user_by_email( - self, email: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - user = db.query(User).filter_by(email=email).first() - return UserModel.model_validate(user) - except Exception: - return None + async def get_user_by_scim_external_id( + self, provider: str, external_id: str, db: AsyncSession | None = None, + ) -> UserModel | None: + """Look up a user by SCIM provider + external ID (dialect-aware JSON filter).""" + async with get_async_db_context(db) as session: + dialect = session.bind.dialect.name + query = select(User) + if dialect == 'sqlite': + scim_match = User.scim.contains({provider: {'external_id': external_id}}) + query = query.where(scim_match) + elif dialect == 'postgresql': + scim_match = User.scim[provider].cast(JSONB)['external_id'].astext == external_id + query = query.where(scim_match) + row = (await session.execute(query)).scalars().first() + return UserModel.model_validate(row) if row else None - def get_user_by_oauth_sub( - self, provider: str, sub: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: # type: Session - dialect_name = db.bind.dialect.name - query = db.query(User) - if dialect_name == "sqlite": - query = query.filter(User.oauth.contains({provider: {"sub": sub}})) - elif dialect_name == "postgresql": - query = query.filter( - User.oauth[provider].cast(JSONB)["sub"].astext == sub - ) - - user = query.first() - return UserModel.model_validate(user) if user else None - except Exception as e: - # You may want to log the exception here - return None - - def get_users( - self, - filter: Optional[dict] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - db: Optional[Session] = None, + async def get_users( + self, filter: dict | None = None, skip: int | None = None, + limit: int | None = None, db: AsyncSession | None = None, ) -> dict: - with get_db_context(db) as db: + """Paginated user listing with optional filters for role, group, and channel.""" + async with get_async_db_context(db) as session: + # Deferred imports to avoid circular dependencies + from open_webui.models.channels import ChannelMember + from open_webui.models.groups import GroupMember + # Join GroupMember so we can order by group_id when requested - query = db.query(User) + stmt = select(User) if filter: - query_key = filter.get("query") + query_key = filter.get('query') if query_key: - query = query.filter( + stmt = stmt.filter( or_( - User.name.ilike(f"%{query_key}%"), - User.email.ilike(f"%{query_key}%"), + User.name.ilike(f'%{query_key}%'), + User.email.ilike(f'%{query_key}%'), ) ) - channel_id = filter.get("channel_id") + channel_id = filter.get('channel_id') if channel_id: - query = query.filter( + stmt = stmt.filter( exists( select(ChannelMember.id).where( ChannelMember.user_id == User.id, @@ -357,19 +408,19 @@ class UsersTable: ) ) - user_ids = filter.get("user_ids") - group_ids = filter.get("group_ids") + user_ids = filter.get('user_ids') + group_ids = filter.get('group_ids') if isinstance(user_ids, list) and isinstance(group_ids, list): # If both are empty lists, return no users if not user_ids and not group_ids: - return {"users": [], "total": 0} + return {'users': [], 'total': 0} if user_ids: - query = query.filter(User.id.in_(user_ids)) + stmt = stmt.filter(User.id.in_(user_ids)) if group_ids: - query = query.filter( + stmt = stmt.filter( exists( select(GroupMember.id).where( GroupMember.user_id == User.id, @@ -378,21 +429,21 @@ class UsersTable: ) ) - roles = filter.get("roles") + roles = filter.get('roles') if roles: - include_roles = [role for role in roles if not role.startswith("!")] - exclude_roles = [role[1:] for role in roles if role.startswith("!")] + include_roles = [role for role in roles if not role.startswith('!')] + exclude_roles = [role[1:] for role in roles if role.startswith('!')] if include_roles: - query = query.filter(User.role.in_(include_roles)) + stmt = stmt.filter(User.role.in_(include_roles)) if exclude_roles: - query = query.filter(~User.role.in_(exclude_roles)) + stmt = stmt.filter(~User.role.in_(exclude_roles)) - order_by = filter.get("order_by") - direction = filter.get("direction") + order_by = filter.get('order_by') + direction = filter.get('direction') - if order_by and order_by.startswith("group_id:"): - group_id = order_by.split(":", 1)[1] + if order_by and order_by.startswith('group_id:'): + group_id = order_by.split(':', 1)[1] # Subquery that checks if the user belongs to the group membership_exists = exists( @@ -405,344 +456,287 @@ class UsersTable: # CASE: user in group → 1, user not in group → 0 group_sort = case((membership_exists, 1), else_=0) - if direction == "asc": - query = query.order_by(group_sort.asc(), User.name.asc()) + if direction == 'asc': + stmt = stmt.order_by(group_sort.asc(), User.name.asc()) else: - query = query.order_by(group_sort.desc(), User.name.asc()) + stmt = stmt.order_by(group_sort.desc(), User.name.asc()) - elif order_by == "name": - if direction == "asc": - query = query.order_by(User.name.asc()) + elif order_by == 'name': + if direction == 'asc': + stmt = stmt.order_by(User.name.asc()) else: - query = query.order_by(User.name.desc()) + stmt = stmt.order_by(User.name.desc()) - elif order_by == "email": - if direction == "asc": - query = query.order_by(User.email.asc()) + elif order_by == 'email': + if direction == 'asc': + stmt = stmt.order_by(User.email.asc()) else: - query = query.order_by(User.email.desc()) + stmt = stmt.order_by(User.email.desc()) - elif order_by == "created_at": - if direction == "asc": - query = query.order_by(User.created_at.asc()) + elif order_by == 'created_at': + if direction == 'asc': + stmt = stmt.order_by(User.created_at.asc()) else: - query = query.order_by(User.created_at.desc()) + stmt = stmt.order_by(User.created_at.desc()) - elif order_by == "last_active_at": - if direction == "asc": - query = query.order_by(User.last_active_at.asc()) + elif order_by == 'last_active_at': + if direction == 'asc': + stmt = stmt.order_by(User.last_active_at.asc()) else: - query = query.order_by(User.last_active_at.desc()) + stmt = stmt.order_by(User.last_active_at.desc()) - elif order_by == "updated_at": - if direction == "asc": - query = query.order_by(User.updated_at.asc()) + elif order_by == 'updated_at': + if direction == 'asc': + stmt = stmt.order_by(User.updated_at.asc()) else: - query = query.order_by(User.updated_at.desc()) - elif order_by == "role": - if direction == "asc": - query = query.order_by(User.role.asc()) + stmt = stmt.order_by(User.updated_at.desc()) + elif order_by == 'role': + if direction == 'asc': + stmt = stmt.order_by(User.role.asc()) else: - query = query.order_by(User.role.desc()) + stmt = stmt.order_by(User.role.desc()) else: - query = query.order_by(User.created_at.desc()) + stmt = stmt.order_by(User.created_at.desc()) # Count BEFORE pagination - total = query.count() + count_result = await session.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() # correct pagination logic if skip is not None: - query = query.offset(skip) + stmt = stmt.offset(skip) if limit is not None: - query = query.limit(limit) + stmt = stmt.limit(limit) - users = query.all() + result = await session.execute(stmt) + users = result.scalars().all() return { - "users": [UserModel.model_validate(user) for user in users], - "total": total, + 'users': [UserModel.model_validate(user) for user in users], + 'total': total, } - def get_users_by_group_id( - self, group_id: str, db: Optional[Session] = None - ) -> list[UserModel]: - with get_db_context(db) as db: - users = ( - db.query(User) - .join(GroupMember, User.id == GroupMember.user_id) - .filter(GroupMember.group_id == group_id) - .all() + async def get_users_by_group_id(self, group_id: str, db: AsyncSession | None = None) -> list[UserModel]: + async with get_async_db_context(db) as session: + from open_webui.models.groups import GroupMember + + result = await session.execute( + select(User).join(GroupMember, User.id == GroupMember.user_id).filter(GroupMember.group_id == group_id) ) + users = result.scalars().all() return [UserModel.model_validate(user) for user in users] - def get_users_by_user_ids( - self, user_ids: list[str], db: Optional[Session] = None - ) -> list[UserStatusModel]: - with get_db_context(db) as db: - users = db.query(User).filter(User.id.in_(user_ids)).all() + async def get_users_by_user_ids(self, user_ids: list[str], db: AsyncSession | None = None) -> list[UserStatusModel]: + async with get_async_db_context(db) as session: + result = await session.execute(select(User).filter(User.id.in_(user_ids))) + users = result.scalars().all() return [UserModel.model_validate(user) for user in users] + # count registered accounts + async def get_num_users(self, db: AsyncSession | None = None) -> int | None: + async with get_async_db_context(db) as session: + result = await session.execute(select(func.count()).select_from(User)) + return result.scalar() + # check user existence + async def has_users(self, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + result = await session.execute(select(exists(select(User)))) + return result.scalar() - def get_num_users(self, db: Optional[Session] = None) -> Optional[int]: - with get_db_context(db) as db: - return db.query(User).count() + async def get_first_user(self, db: AsyncSession | None = None) -> UserModel | None: + """Return the earliest-created user (bootstrap admin detection).""" + async with get_async_db_context(db) as session: + stmt = select(User).order_by(User.created_at).limit(1) + row = (await session.execute(stmt)).scalars().first() + return UserModel.model_validate(row) if row else None - def has_users(self, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - return db.query(db.query(User).exists()).scalar() - - def get_first_user(self, db: Optional[Session] = None) -> UserModel: - try: - with get_db_context(db) as db: - user = db.query(User).order_by(User.created_at).first() - return UserModel.model_validate(user) - except Exception: + async def get_user_webhook_url_by_id(self, id: str, db: AsyncSession | None = None) -> str | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if user and user.settings: + return user.settings.get('ui', {}).get('notifications', {}).get('webhook_url', None) return None - def get_user_webhook_url_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[str]: - try: - with get_db_context(db) as db: - user = db.query(User).filter_by(id=id).first() - - if user.settings is None: - return None - else: - return ( - user.settings.get("ui", {}) - .get("notifications", {}) - .get("webhook_url", None) - ) - except Exception: - return None - - def get_num_users_active_today(self, db: Optional[Session] = None) -> Optional[int]: - with get_db_context(db) as db: - current_timestamp = int(datetime.datetime.now().timestamp()) + async def get_num_users_active_today(self, db: AsyncSession | None = None) -> int | None: + async with get_async_db_context(db) as session: + current_timestamp = int(time.time()) today_midnight_timestamp = current_timestamp - (current_timestamp % 86400) - query = db.query(User).filter( - User.last_active_at > today_midnight_timestamp + result = await session.execute( + select(func.count()).select_from(User).where(User.last_active_at > today_midnight_timestamp) ) - return query.count() + return result.scalar() - def update_user_role_by_id( - self, id: str, role: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - db.query(User).filter_by(id=id).update({"role": role}) - db.commit() - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - except Exception: - return None + async def update_user_role_by_id(self, id: str, role: str, db: AsyncSession | None = None) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + user.role = role + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) - def update_user_status_by_id( - self, id: str, form_data: UserStatus, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - db.query(User).filter_by(id=id).update( - {**form_data.model_dump(exclude_none=True)} - ) - db.commit() + async def update_user_status_by_id( + self, id: str, form_data: UserStatus, db: AsyncSession | None = None + ) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + for key, value in form_data.model_dump(exclude_none=True).items(): + setattr(user, key, value) + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - except Exception: - return None - - def update_user_profile_image_url_by_id( - self, id: str, profile_image_url: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - db.query(User).filter_by(id=id).update( - {"profile_image_url": profile_image_url} - ) - db.commit() - - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - except Exception: - return None + async def update_user_profile_image_url_by_id( + self, id: str, profile_image_url: str, db: AsyncSession | None = None, + ) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if user is None: + return None + user.profile_image_url = profile_image_url + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) @throttle(DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL) - def update_last_active_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - db.query(User).filter_by(id=id).update( - {"last_active_at": int(time.time())} - ) - db.commit() + async def update_last_active_by_id(self, id: str, db: AsyncSession | None = None) -> None: + async with get_async_db_context(db) as session: + await session.execute(update(User).where(User.id == id).values(last_active_at=int(time.time()))) + await session.commit() - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - except Exception: - return None - - def update_user_oauth_by_id( - self, id: str, provider: str, sub: str, db: Optional[Session] = None - ) -> Optional[UserModel]: - """ - Update or insert an OAuth provider/sub pair into the user's oauth JSON field. - Example resulting structure: - { - "google": { "sub": "123" }, - "github": { "sub": "abc" } - } - """ - try: - with get_db_context(db) as db: - user = db.query(User).filter_by(id=id).first() - if not user: - return None - - # Load existing oauth JSON or create empty - oauth = user.oauth or {} - - # Update or insert provider entry - oauth[provider] = {"sub": sub} - - # Persist updated JSON - db.query(User).filter_by(id=id).update({"oauth": oauth}) - db.commit() - - return UserModel.model_validate(user) - - except Exception: - return None - - def update_user_by_id( - self, id: str, updated: dict, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - db.query(User).filter_by(id=id).update(updated) - db.commit() - - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - # return UserModel(**user.dict()) - except Exception as e: - print(e) - return None - - def update_user_settings_by_id( - self, id: str, updated: dict, db: Optional[Session] = None - ) -> Optional[UserModel]: - try: - with get_db_context(db) as db: - user = db.query(User).filter_by(id=id).first() - if not user: - return None - - user_settings = user.settings - - if user_settings is None: - user_settings = {} - - user_settings.update(updated) - - db.query(User).filter_by(id=id).update({"settings": user_settings}) - db.commit() - - user = db.query(User).filter_by(id=id).first() - return UserModel.model_validate(user) - except Exception: - return None - - def delete_user_by_id(self, id: str, db: Optional[Session] = None) -> bool: - try: - # Remove User from Groups - Groups.remove_user_from_all_groups(id) - - # Delete User Chats - result = Chats.delete_chats_by_user_id(id, db=db) - if result: - with get_db_context(db) as db: - # Delete User - db.query(User).filter_by(id=id).delete() - db.commit() - - return True - else: - return False - except Exception: - return False - - def get_user_api_key_by_id( - self, id: str, db: Optional[Session] = None - ) -> Optional[str]: - try: - with get_db_context(db) as db: - api_key = db.query(ApiKey).filter_by(user_id=id).first() - return api_key.key if api_key else None - except Exception: - return None - - def update_user_api_key_by_id( - self, id: str, api_key: str, db: Optional[Session] = None - ) -> bool: - try: - with get_db_context(db) as db: - db.query(ApiKey).filter_by(user_id=id).delete() - db.commit() - - now = int(time.time()) - new_api_key = ApiKey( - id=f"key_{id}", - user_id=id, - key=api_key, - created_at=now, - updated_at=now, - ) - db.add(new_api_key) - db.commit() - - return True - - except Exception: - return False - - def delete_user_api_key_by_id(self, id: str, db: Optional[Session] = None) -> bool: - try: - with get_db_context(db) as db: - db.query(ApiKey).filter_by(user_id=id).delete() - db.commit() - return True - except Exception: - return False - - def get_valid_user_ids( - self, user_ids: list[str], db: Optional[Session] = None - ) -> list[str]: - with get_db_context(db) as db: - users = db.query(User).filter(User.id.in_(user_ids)).all() - return [user.id for user in users] - - def get_super_admin_user(self, db: Optional[Session] = None) -> Optional[UserModel]: - with get_db_context(db) as db: - user = db.query(User).filter_by(role="admin").first() - if user: - return UserModel.model_validate(user) - else: + async def update_user_oauth_by_id( + self, id: str, provider: str, sub: str, db: AsyncSession | None = None + ) -> UserModel | None: + """Update or insert an OAuth provider/sub pair into the user's oauth JSON field.""" + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: return None + oauth = dict(user.oauth or {}) + oauth[provider] = {'sub': sub} + user.oauth = oauth + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) - def get_active_user_count(self, db: Optional[Session] = None) -> int: - with get_db_context(db) as db: + async def update_user_scim_by_id( + self, + id: str, + provider: str, + external_id: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Update or insert a SCIM provider/external_id pair into the user's scim JSON field.""" + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + scim = dict(user.scim or {}) + scim[provider] = {'external_id': external_id} + user.scim = scim + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) + + async def update_user_by_id(self, id: str, updated: dict, db: AsyncSession | None = None) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + for key, value in updated.items(): + setattr(user, key, value) + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) + # settings update helper + async def update_user_settings_by_id( + self, id: str, updated: dict, db: AsyncSession | None = None + ) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + user_settings = dict(user.settings or {}) + user_settings.update(updated) + user.settings = user_settings + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) + + async def delete_user_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + from open_webui.models.chats import Chats + from open_webui.models.groups import Groups + + # Remove User from Groups + await Groups.remove_user_from_all_groups(id) + + # Delete User Chats + async with get_async_db_context(db) as session: + deleted_chats = await Chats.delete_chats_by_user_id(id, db=session) + if not deleted_chats: + return False # chats deletion failed + await session.execute(delete(User).where(User.id == id)) + await session.commit() + return True + + async def get_user_api_key_by_id(self, id: str, db: AsyncSession | None = None) -> str | None: + async with get_async_db_context(db) as session: + api_key = (await session.execute(select(ApiKey).where(ApiKey.user_id == id))).scalars().first() + return api_key.key if api_key else None + + async def update_user_api_key_by_id(self, id: str, api_key: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + await session.execute(delete(ApiKey).where(ApiKey.user_id == id)) + now_ts = int(time.time()) + new_key = ApiKey( + id=f'key_{id}', + user_id=id, + key=api_key, + created_at=now_ts, + updated_at=now_ts, + ) + session.add(new_key) + await session.commit() + return True + + async def delete_user_api_key_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + await session.execute(delete(ApiKey).where(ApiKey.user_id == id)) + await session.commit() + return True + + async def get_valid_user_ids(self, user_ids: list[str], db: AsyncSession | None = None) -> list[str]: + async with get_async_db_context(db) as session: + result = await session.execute(select(User).where(User.id.in_(user_ids))) + return [u.id for u in result.scalars().all()] + + async def get_super_admin_user(self, db: AsyncSession | None = None) -> UserModel | None: + async with get_async_db_context(db) as session: + row = (await session.execute(select(User).where(User.role == 'admin').limit(1))).scalars().first() + return UserModel.model_validate(row) if row else None + + async def get_active_user_count(self, db: AsyncSession | None = None) -> int: + async with get_async_db_context(db) as session: # Consider user active if last_active_at within the last 3 minutes three_minutes_ago = int(time.time()) - 180 - count = ( - db.query(User).filter(User.last_active_at >= three_minutes_ago).count() + result = await session.execute( + select(func.count()).select_from(User).where(User.last_active_at >= three_minutes_ago) ) - return count + return result.scalar() - def is_user_active(self, user_id: str, db: Optional[Session] = None) -> bool: - with get_db_context(db) as db: - user = db.query(User).filter_by(id=user_id).first() + @staticmethod + def is_active(user: UserModel) -> bool: + """Compute active status from an already-loaded UserModel (no DB hit).""" + if user.last_active_at: + three_minutes_ago = int(time.time()) - 180 + return user.last_active_at >= three_minutes_ago + return False + + async def is_user_active(self, user_id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + user = await session.get(User, user_id) if user and user.last_active_at: # Consider user active if last_active_at within the last 3 minutes three_minutes_ago = int(time.time()) - 180 @@ -750,4 +744,5 @@ class UsersTable: return False -Users = UsersTable() +Users = UsersTable() # singleton user repository + diff --git a/backend/open_webui/retrieval/loaders/datalab_marker.py b/backend/open_webui/retrieval/loaders/datalab_marker.py index 8d14be0a40..be8cb9baaa 100644 --- a/backend/open_webui/retrieval/loaders/datalab_marker.py +++ b/backend/open_webui/retrieval/loaders/datalab_marker.py @@ -1,11 +1,12 @@ +import json +import logging import os import time -import requests -import logging -import json from typing import List, Optional -from langchain_core.documents import Document + +import requests from fastapi import HTTPException, status +from langchain_core.documents import Document log = logging.getLogger(__name__) @@ -40,78 +41,76 @@ class DatalabMarkerLoader: self.output_format = output_format def _get_mime_type(self, filename: str) -> str: - ext = filename.rsplit(".", 1)[-1].lower() + ext = filename.rsplit('.', 1)[-1].lower() mime_map = { - "pdf": "application/pdf", - "xls": "application/vnd.ms-excel", - "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "ods": "application/vnd.oasis.opendocument.spreadsheet", - "doc": "application/msword", - "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "odt": "application/vnd.oasis.opendocument.text", - "ppt": "application/vnd.ms-powerpoint", - "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "odp": "application/vnd.oasis.opendocument.presentation", - "html": "text/html", - "epub": "application/epub+zip", - "png": "image/png", - "jpeg": "image/jpeg", - "jpg": "image/jpeg", - "webp": "image/webp", - "gif": "image/gif", - "tiff": "image/tiff", + 'pdf': 'application/pdf', + 'xls': 'application/vnd.ms-excel', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'doc': 'application/msword', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'odt': 'application/vnd.oasis.opendocument.text', + 'ppt': 'application/vnd.ms-powerpoint', + 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'odp': 'application/vnd.oasis.opendocument.presentation', + 'html': 'text/html', + 'epub': 'application/epub+zip', + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'webp': 'image/webp', + 'gif': 'image/gif', + 'tiff': 'image/tiff', } - return mime_map.get(ext, "application/octet-stream") + return mime_map.get(ext, 'application/octet-stream') def check_marker_request_status(self, request_id: str) -> dict: - url = f"{self.api_base_url}/{request_id}" - headers = {"X-Api-Key": self.api_key} + url = f'{self.api_base_url}/{request_id}' + headers = {'X-Api-Key': self.api_key} try: response = requests.get(url, headers=headers) response.raise_for_status() result = response.json() - log.info(f"Marker API status check for request {request_id}: {result}") + log.info(f'Marker API status check for request {request_id}: {result}') return result except requests.HTTPError as e: - log.error(f"Error checking Marker request status: {e}") + log.error(f'Error checking Marker request status: {e}') raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail=f"Failed to check Marker request: {e}", + detail=f'Failed to check Marker request: {e}', ) except ValueError as e: - log.error(f"Invalid JSON checking Marker request: {e}") - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, detail=f"Invalid JSON: {e}" - ) + log.error(f'Invalid JSON checking Marker request: {e}') + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail=f'Invalid JSON: {e}') def load(self) -> List[Document]: filename = os.path.basename(self.file_path) mime_type = self._get_mime_type(filename) - headers = {"X-Api-Key": self.api_key} + headers = {'X-Api-Key': self.api_key} form_data = { - "use_llm": str(self.use_llm).lower(), - "skip_cache": str(self.skip_cache).lower(), - "force_ocr": str(self.force_ocr).lower(), - "paginate": str(self.paginate).lower(), - "strip_existing_ocr": str(self.strip_existing_ocr).lower(), - "disable_image_extraction": str(self.disable_image_extraction).lower(), - "format_lines": str(self.format_lines).lower(), - "output_format": self.output_format, + 'use_llm': str(self.use_llm).lower(), + 'skip_cache': str(self.skip_cache).lower(), + 'force_ocr': str(self.force_ocr).lower(), + 'paginate': str(self.paginate).lower(), + 'strip_existing_ocr': str(self.strip_existing_ocr).lower(), + 'disable_image_extraction': str(self.disable_image_extraction).lower(), + 'format_lines': str(self.format_lines).lower(), + 'output_format': self.output_format, } if self.additional_config and self.additional_config.strip(): - form_data["additional_config"] = self.additional_config + form_data['additional_config'] = self.additional_config log.info( f"Datalab Marker POST request parameters: {{'filename': '{filename}', 'mime_type': '{mime_type}', **{form_data}}}" ) try: - with open(self.file_path, "rb") as f: - files = {"file": (filename, f, mime_type)} + with open(self.file_path, 'rb') as f: + files = {'file': (filename, f, mime_type)} response = requests.post( - f"{self.api_base_url}", + f'{self.api_base_url}', data=form_data, files=files, headers=headers, @@ -119,29 +118,25 @@ class DatalabMarkerLoader: response.raise_for_status() result = response.json() except FileNotFoundError: - raise HTTPException( - status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}" - ) + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f'File not found: {self.file_path}') except requests.HTTPError as e: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Datalab Marker request failed: {e}", + detail=f'Datalab Marker request failed: {e}', ) except ValueError as e: - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, detail=f"Invalid JSON response: {e}" - ) + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail=f'Invalid JSON response: {e}') except Exception as e: raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) - if not result.get("success"): + if not result.get('success'): raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Datalab Marker request failed: {result.get('error', 'Unknown error')}", + detail=f'Datalab Marker request failed: {result.get("error", "Unknown error")}', ) - check_url = result.get("request_check_url") - request_id = result.get("request_id") + check_url = result.get('request_check_url') + request_id = result.get('request_id') # Check if this is a direct response (self-hosted) or polling response (DataLab) if check_url: @@ -154,54 +149,45 @@ class DatalabMarkerLoader: poll_result = poll_response.json() except (requests.HTTPError, ValueError) as e: raw_body = poll_response.text - log.error(f"Polling error: {e}, response body: {raw_body}") - raise HTTPException( - status.HTTP_502_BAD_GATEWAY, detail=f"Polling failed: {e}" - ) + log.error(f'Polling error: {e}, response body: {raw_body}') + raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail=f'Polling failed: {e}') - status_val = poll_result.get("status") - success_val = poll_result.get("success") + status_val = poll_result.get('status') + success_val = poll_result.get('success') - if status_val == "complete": + if status_val == 'complete': summary = { k: poll_result.get(k) for k in ( - "status", - "output_format", - "success", - "error", - "page_count", - "total_cost", + 'status', + 'output_format', + 'success', + 'error', + 'page_count', + 'total_cost', ) } - log.info( - f"Marker processing completed successfully: {json.dumps(summary, indent=2)}" - ) + log.info(f'Marker processing completed successfully: {json.dumps(summary, indent=2)}') break - if status_val == "failed" or success_val is False: - log.error( - f"Marker poll failed full response: {json.dumps(poll_result, indent=2)}" - ) - error_msg = ( - poll_result.get("error") - or "Marker returned failure without error message" - ) + if status_val == 'failed' or success_val is False: + log.error(f'Marker poll failed full response: {json.dumps(poll_result, indent=2)}') + error_msg = poll_result.get('error') or 'Marker returned failure without error message' raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Marker processing failed: {error_msg}", + detail=f'Marker processing failed: {error_msg}', ) else: raise HTTPException( status.HTTP_504_GATEWAY_TIMEOUT, - detail="Marker processing timed out", + detail='Marker processing timed out', ) - if not poll_result.get("success", False): - error_msg = poll_result.get("error") or "Unknown processing error" + if not poll_result.get('success', False): + error_msg = poll_result.get('error') or 'Unknown processing error' raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Final processing failed: {error_msg}", + detail=f'Final processing failed: {error_msg}', ) # DataLab format - content in format-specific fields @@ -210,69 +196,65 @@ class DatalabMarkerLoader: final_result = poll_result else: # Self-hosted direct response - content in "output" field - if "output" in result: - log.info("Self-hosted Marker returned direct response without polling") - raw_content = result.get("output") + if 'output' in result: + log.info('Self-hosted Marker returned direct response without polling') + raw_content = result.get('output') final_result = result else: - available_fields = ( - list(result.keys()) - if isinstance(result, dict) - else "non-dict response" - ) + available_fields = list(result.keys()) if isinstance(result, dict) else 'non-dict response' raise HTTPException( status.HTTP_502_BAD_GATEWAY, detail=f"Custom Marker endpoint returned success but no 'output' field found. Available fields: {available_fields}. Expected either 'request_check_url' for polling or 'output' field for direct response.", ) - if self.output_format.lower() == "json": + if self.output_format.lower() == 'json': full_text = json.dumps(raw_content, indent=2) - elif self.output_format.lower() in {"markdown", "html"}: + elif self.output_format.lower() in {'markdown', 'html'}: full_text = str(raw_content).strip() else: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Unsupported output format: {self.output_format}", + detail=f'Unsupported output format: {self.output_format}', ) if not full_text: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail="Marker returned empty content", + detail='Marker returned empty content', ) - marker_output_dir = os.path.join("/app/backend/data/uploads", "marker_output") + marker_output_dir = os.path.join('/app/backend/data/uploads', 'marker_output') os.makedirs(marker_output_dir, exist_ok=True) - file_ext_map = {"markdown": "md", "json": "json", "html": "html"} - file_ext = file_ext_map.get(self.output_format.lower(), "txt") - output_filename = f"{os.path.splitext(filename)[0]}.{file_ext}" + file_ext_map = {'markdown': 'md', 'json': 'json', 'html': 'html'} + file_ext = file_ext_map.get(self.output_format.lower(), 'txt') + output_filename = f'{os.path.splitext(filename)[0]}.{file_ext}' output_path = os.path.join(marker_output_dir, output_filename) try: - with open(output_path, "w", encoding="utf-8") as f: + with open(output_path, 'w', encoding='utf-8') as f: f.write(full_text) - log.info(f"Saved Marker output to: {output_path}") + log.info(f'Saved Marker output to: {output_path}') except Exception as e: - log.warning(f"Failed to write marker output to disk: {e}") + log.warning(f'Failed to write marker output to disk: {e}') metadata = { - "source": filename, - "output_format": final_result.get("output_format", self.output_format), - "page_count": final_result.get("page_count", 0), - "processed_with_llm": self.use_llm, - "request_id": request_id or "", + 'source': filename, + 'output_format': final_result.get('output_format', self.output_format), + 'page_count': final_result.get('page_count', 0), + 'processed_with_llm': self.use_llm, + 'request_id': request_id or '', } - images = final_result.get("images", {}) + images = final_result.get('images', {}) if images: - metadata["image_count"] = len(images) - metadata["images"] = json.dumps(list(images.keys())) + metadata['image_count'] = len(images) + metadata['images'] = json.dumps(list(images.keys())) for k, v in metadata.items(): if isinstance(v, (dict, list)): metadata[k] = json.dumps(v) elif v is None: - metadata[k] = "" + metadata[k] = '' return [Document(page_content=full_text, metadata=metadata)] diff --git a/backend/open_webui/retrieval/loaders/external_document.py b/backend/open_webui/retrieval/loaders/external_document.py index e1371be288..ddafc3124b 100644 --- a/backend/open_webui/retrieval/loaders/external_document.py +++ b/backend/open_webui/retrieval/loaders/external_document.py @@ -1,8 +1,9 @@ -import requests -import logging, os +import logging +import os from typing import Iterator, List, Union from urllib.parse import quote +import requests from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document from open_webui.utils.headers import include_user_info_headers @@ -29,43 +30,42 @@ class ExternalDocumentLoader(BaseLoader): self.user = user def load(self) -> List[Document]: - with open(self.file_path, "rb") as f: + with open(self.file_path, 'rb') as f: data = f.read() headers = {} if self.mime_type is not None: - headers["Content-Type"] = self.mime_type + headers['Content-Type'] = self.mime_type if self.api_key is not None: - headers["Authorization"] = f"Bearer {self.api_key}" + headers['Authorization'] = f'Bearer {self.api_key}' try: - headers["X-Filename"] = quote(os.path.basename(self.file_path)) - except: + headers['X-Filename'] = quote(os.path.basename(self.file_path)) + except Exception: pass if self.user is not None: headers = include_user_info_headers(headers, self.user) url = self.url - if url.endswith("/"): + if url.endswith('/'): url = url[:-1] try: - response = requests.put(f"{url}/process", data=data, headers=headers) + response = requests.put(f'{url}/process', data=data, headers=headers) except Exception as e: - log.error(f"Error connecting to endpoint: {e}") - raise Exception(f"Error connecting to endpoint: {e}") + log.error(f'Error connecting to endpoint: {e}') + raise Exception(f'Error connecting to endpoint: {e}') if response.ok: - response_data = response.json() if response_data: if isinstance(response_data, dict): return [ Document( - page_content=response_data.get("page_content"), - metadata=response_data.get("metadata"), + page_content=response_data.get('page_content'), + metadata=response_data.get('metadata'), ) ] elif isinstance(response_data, list): @@ -73,17 +73,15 @@ class ExternalDocumentLoader(BaseLoader): for document in response_data: documents.append( Document( - page_content=document.get("page_content"), - metadata=document.get("metadata"), + page_content=document.get('page_content'), + metadata=document.get('metadata'), ) ) return documents else: - raise Exception("Error loading document: Unable to parse content") + raise Exception('Error loading document: Unable to parse content') else: - raise Exception("Error loading document: No content returned") + raise Exception('Error loading document: No content returned') else: - raise Exception( - f"Error loading document: {response.status_code} {response.text}" - ) + raise Exception(f'Error loading document: {response.status_code} {response.text}') diff --git a/backend/open_webui/retrieval/loaders/external_web.py b/backend/open_webui/retrieval/loaders/external_web.py index 39644caddb..e3fd0b2614 100644 --- a/backend/open_webui/retrieval/loaders/external_web.py +++ b/backend/open_webui/retrieval/loaders/external_web.py @@ -1,7 +1,7 @@ -import requests import logging from typing import Iterator, List, Union +import requests from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document @@ -30,22 +30,22 @@ class ExternalWebLoader(BaseLoader): response = requests.post( self.external_url, headers={ - "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) External Web Loader", - "Authorization": f"Bearer {self.external_api_key}", + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) External Web Loader', + 'Authorization': f'Bearer {self.external_api_key}', }, json={ - "urls": urls, + 'urls': urls, }, ) response.raise_for_status() results = response.json() for result in results: yield Document( - page_content=result.get("page_content", ""), - metadata=result.get("metadata", {}), + page_content=result.get('page_content', ''), + metadata=result.get('metadata', {}), ) except Exception as e: if self.continue_on_failure: - log.error(f"Error extracting content from batch {urls}: {e}") + log.error(f'Error extracting content from batch {urls}: {e}') else: raise e diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 2b83e44283..68d6b79702 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -1,9 +1,10 @@ -import requests -import logging -import ftfy -import sys +import asyncio import json +import logging +import sys +import ftfy +import requests from azure.identity import DefaultAzureCredential from langchain_community.document_loaders import ( AzureAIDocumentIntelligenceLoader, @@ -13,82 +14,124 @@ from langchain_community.document_loaders import ( OutlookMessageLoader, PyPDFLoader, TextLoader, - UnstructuredEPubLoader, - UnstructuredExcelLoader, - UnstructuredODTLoader, - UnstructuredPowerPointLoader, - UnstructuredRSTLoader, - UnstructuredXMLLoader, YoutubeLoader, ) from langchain_core.documents import Document - -from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader - -from open_webui.retrieval.loaders.mistral import MistralLoader +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, GLOBAL_LOG_LEVEL, REQUESTS_VERIFY from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader +from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader from open_webui.retrieval.loaders.mineru import MinerULoader - - -from open_webui.env import GLOBAL_LOG_LEVEL, REQUESTS_VERIFY +from open_webui.retrieval.loaders.mistral import MistralLoader +from open_webui.retrieval.loaders.paddleocr_vl import PaddleOCRVLLoader logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) known_source_ext = [ - "go", - "py", - "java", - "sh", - "bat", - "ps1", - "cmd", - "js", - "ts", - "css", - "cpp", - "hpp", - "h", - "c", - "cs", - "sql", - "log", - "ini", - "pl", - "pm", - "r", - "dart", - "dockerfile", - "env", - "php", - "hs", - "hsc", - "lua", - "nginxconf", - "conf", - "m", - "mm", - "plsql", - "perl", - "rb", - "rs", - "db2", - "scala", - "bash", - "swift", - "vue", - "svelte", - "ex", - "exs", - "erl", - "tsx", - "jsx", - "hs", - "lhs", - "json", + 'go', + 'py', + 'java', + 'sh', + 'bat', + 'ps1', + 'cmd', + 'js', + 'ts', + 'css', + 'cpp', + 'hpp', + 'h', + 'c', + 'cs', + 'sql', + 'log', + 'ini', + 'pl', + 'pm', + 'r', + 'dart', + 'dockerfile', + 'env', + 'php', + 'hs', + 'hsc', + 'lua', + 'nginxconf', + 'conf', + 'm', + 'mm', + 'plsql', + 'perl', + 'rb', + 'rs', + 'db2', + 'scala', + 'bash', + 'swift', + 'vue', + 'svelte', + 'ex', + 'exs', + 'erl', + 'tsx', + 'jsx', + 'hs', + 'lhs', + 'json', + 'yaml', + 'yml', + 'toml', ] +class ExcelLoader: + """Fallback Excel loader using pandas when unstructured is not installed.""" + + def __init__(self, file_path): + self.file_path = file_path + + def load(self) -> list[Document]: + import pandas as pd + + text_parts = [] + xls = pd.ExcelFile(self.file_path) + for sheet_name in xls.sheet_names: + df = pd.read_excel(xls, sheet_name=sheet_name) + text_parts.append(f'Sheet: {sheet_name}\n{df.to_string(index=False)}') + return [ + Document( + page_content='\n\n'.join(text_parts), + metadata={'source': self.file_path}, + ) + ] + + +class PptxLoader: + """Fallback PowerPoint loader using python-pptx when unstructured is not installed.""" + + def __init__(self, file_path): + self.file_path = file_path + + def load(self) -> list[Document]: + from pptx import Presentation + + prs = Presentation(self.file_path) + text_parts = [] + for i, slide in enumerate(prs.slides, 1): + slide_texts = [] + for shape in slide.shapes: + if shape.has_text_frame: + slide_texts.append(shape.text_frame.text) + if slide_texts: + text_parts.append(f'Slide {i}:\n' + '\n'.join(slide_texts)) + return [ + Document( + page_content='\n\n'.join(text_parts), + metadata={'source': self.file_path}, + ) + ] + + class TikaLoader: def __init__(self, url, file_path, mime_type=None, extract_images=None): self.url = url @@ -98,41 +141,41 @@ class TikaLoader: self.extract_images = extract_images def load(self) -> list[Document]: - with open(self.file_path, "rb") as f: + with open(self.file_path, 'rb') as f: data = f.read() if self.mime_type is not None: - headers = {"Content-Type": self.mime_type} + headers = {'Content-Type': self.mime_type} else: headers = {} if self.extract_images == True: - headers["X-Tika-PDFextractInlineImages"] = "true" + headers['X-Tika-PDFextractInlineImages'] = 'true' endpoint = self.url - if not endpoint.endswith("/"): - endpoint += "/" - endpoint += "tika/text" + if not endpoint.endswith('/'): + endpoint += '/' + endpoint += 'tika/text' r = requests.put(endpoint, data=data, headers=headers, verify=REQUESTS_VERIFY) if r.ok: raw_metadata = r.json() - text = raw_metadata.get("X-TIKA:content", "").strip() + text = raw_metadata.get('X-TIKA:content', '').strip() - if "Content-Type" in raw_metadata: - headers["Content-Type"] = raw_metadata["Content-Type"] + if 'Content-Type' in raw_metadata: + headers['Content-Type'] = raw_metadata['Content-Type'] - log.debug("Tika extracted text: %s", text) + log.debug('Tika extracted text: %s', text) return [Document(page_content=text, metadata=headers)] else: - raise Exception(f"Error calling Tika: {r.reason}") + raise Exception(f'Error calling Tika: {r.reason}') class DoclingLoader: def __init__(self, url, api_key=None, file_path=None, mime_type=None, params=None): - self.url = url.rstrip("/") + self.url = url.rstrip('/') self.api_key = api_key self.file_path = file_path self.mime_type = mime_type @@ -140,199 +183,329 @@ class DoclingLoader: self.params = params or {} def load(self) -> list[Document]: - with open(self.file_path, "rb") as f: + with open(self.file_path, 'rb') as f: headers = {} if self.api_key: - headers["X-Api-Key"] = f"Bearer {self.api_key}" + headers['X-Api-Key'] = f'{self.api_key}' r = requests.post( - f"{self.url}/v1/convert/file", + f'{self.url}/v1/convert/file', files={ - "files": ( + 'files': ( self.file_path, f, - self.mime_type or "application/octet-stream", + self.mime_type or 'application/octet-stream', ) }, data={ - "image_export_mode": "placeholder", + 'image_export_mode': 'placeholder', **self.params, }, headers=headers, + verify=AIOHTTP_CLIENT_SESSION_SSL, ) if r.ok: result = r.json() - document_data = result.get("document", {}) - text = document_data.get("md_content", "") + document_data = result.get('document', {}) + text = document_data.get('md_content', '') - metadata = {"Content-Type": self.mime_type} if self.mime_type else {} + metadata = {'Content-Type': self.mime_type} if self.mime_type else {} - log.debug("Docling extracted text: %s", text) + log.debug('Docling extracted text: %s', text) return [Document(page_content=text, metadata=metadata)] else: - error_msg = f"Error calling Docling API: {r.reason}" + error_msg = f'Error calling Docling API: {r.reason}' if r.text: try: error_data = r.json() - if "detail" in error_data: - error_msg += f" - {error_data['detail']}" + if 'detail' in error_data: + error_msg += f' - {error_data["detail"]}' except Exception: - error_msg += f" - {r.text}" - raise Exception(f"Error calling Docling: {error_msg}") + error_msg += f' - {r.text}' + raise Exception(f'Error calling Docling: {error_msg}') class Loader: - def __init__(self, engine: str = "", **kwargs): + def __init__(self, engine: str = '', **kwargs): self.engine = engine - self.user = kwargs.get("user", None) + self.user = kwargs.get('user', None) self.kwargs = kwargs - def load( - self, filename: str, file_content_type: str, file_path: str - ) -> list[Document]: + def load(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: loader = self._get_loader(filename, file_content_type, file_path) docs = loader.load() + return [Document(page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata) for doc in docs] - return [ - Document( - page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata - ) - for doc in docs - ] + async def aload(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: + """ + Async wrapper around `load`. + + Document loaders dispatched by `_get_loader` (PyMuPDF, Unstructured, + python-docx, Tika, etc.) are uniformly synchronous and CPU/IO-bound. + Calling `load` directly from an async handler would block the event + loop for the entire parse — minutes for large PDFs. This offloads + the work to a worker thread so the loop stays responsive. + """ + return await asyncio.to_thread(self.load, filename, file_content_type, file_path) def _is_text_file(self, file_ext: str, file_content_type: str) -> bool: return file_ext in known_source_ext or ( file_content_type - and file_content_type.find("text/") >= 0 + and file_content_type.find('text/') >= 0 # Avoid text/html files being detected as text - and not file_content_type.find("html") >= 0 + and not file_content_type.find('html') >= 0 ) + def _detect_text_encoding(self, file_path: str) -> str: + """Detect the encoding of a text file with CJK-aware fallbacks. + + Langchain's ``TextLoader`` uses chardet internally when + ``autodetect_encoding=True``, but chardet frequently misidentifies + CJK encodings (e.g. GB18030 detected as GB2312 or even Cyrillic). + This method replaces that by: + + 1. Trying UTF-8 first (fast path for the vast majority of files). + 2. Using chardet as a *hint* to prioritise the right CJK codec + family, but mapping subset names to their superset + (e.g. GB2312 → gb18030). + 3. Validating that decoded text actually contains CJK characters, + guarding against codecs that "succeed" but produce garbage. + 4. Falling back to latin-1 (always valid, ftfy fixes mojibake later). + """ + try: + with open(file_path, "rb") as f: + raw = f.read() + except OSError: + return "utf-8" + + if not raw: + return "utf-8" + + # Fast path: most files are UTF-8 + try: + raw.decode("utf-8") + return "utf-8" + except UnicodeDecodeError: + pass + + # Use chardet as a hint, not as ground truth + import chardet + + detected = chardet.detect(raw) + detected_enc = (detected.get("encoding") or "").lower().replace("-", "").replace("_", "") + + # Map chardet's detected encoding to the correct superset codec. + # chardet often reports GB2312 for content that is actually GB18030; + # GB18030 is a strict superset of both GB2312 and GBK. + _ENC_FAMILY = { + "gb2312": "gb18030", + "gb18030": "gb18030", + "gbk": "gb18030", + "big5": "big5", + "euckr": "euc-kr", + "eucjp": "euc-jp", + "iso2022jp": "euc-jp", + "shiftjis": "shift_jis", + } + + # Build priority list: chardet-hinted codec first, then remaining CJK + base_order = ["gb18030", "big5", "euc-kr", "euc-jp"] + hinted = _ENC_FAMILY.get(detected_enc) + if hinted and hinted in base_order: + ordered = [hinted] + [e for e in base_order if e != hinted] + else: + ordered = base_order + + for enc in ordered: + try: + text = raw.decode(enc) + if text.strip() and self._has_cjk_characters(text): + log.info( + "Detected encoding %s for %s (chardet guessed %s)", + enc, + file_path, + detected.get("encoding"), + ) + return enc + except (UnicodeDecodeError, LookupError): + continue + + # If chardet gave a non-CJK answer that isn't in our family map, + # try it directly — it might be a valid Western encoding. + chardet_encoding = detected.get("encoding") + if chardet_encoding: + try: + raw.decode(chardet_encoding) + log.info( + "Using chardet-detected encoding %s for %s", + chardet_encoding, + file_path, + ) + return chardet_encoding + except (UnicodeDecodeError, LookupError): + pass + + # latin-1 is the ultimate fallback: every byte 0x00–0xFF is valid. + # ftfy.fix_text() (applied downstream) repairs most mojibake that + # results from treating Windows-1252 content as Latin-1. + log.info("Falling back to latin-1 encoding for %s", file_path) + return "latin-1" + + @staticmethod + def _has_cjk_characters(text: str, threshold: float = 0.05) -> bool: + """Check if decoded text contains a meaningful proportion of CJK characters. + + This guards against codecs that technically "succeed" but decode the + bytes into wrong Unicode codepoints (e.g. PUA chars, random symbols). + A genuine CJK document should have at least ``threshold`` fraction of + its non-whitespace characters in CJK Unicode blocks. + """ + if not text: + return False + + cjk_count = 0 + total = 0 + for ch in text: + if ch.isspace(): + continue + total += 1 + cp = ord(ch) + if ( + 0x4E00 <= cp <= 0x9FFF # CJK Unified Ideographs + or 0x3400 <= cp <= 0x4DBF # CJK Extension A + or 0x20000 <= cp <= 0x2A6DF # CJK Extension B + or 0x2A700 <= cp <= 0x2B73F # CJK Extension C + or 0x2B740 <= cp <= 0x2B81F # CJK Extension D + or 0xF900 <= cp <= 0xFAFF # CJK Compatibility Ideographs + or 0x3000 <= cp <= 0x303F # CJK Symbols and Punctuation + or 0x3040 <= cp <= 0x309F # Hiragana + or 0x30A0 <= cp <= 0x30FF # Katakana + or 0xAC00 <= cp <= 0xD7AF # Hangul Syllables + or 0xFF00 <= cp <= 0xFFEF # Halfwidth and Fullwidth Forms + ): + cjk_count += 1 + + if total == 0: + return False + + return (cjk_count / total) >= threshold + def _get_loader(self, filename: str, file_content_type: str, file_path: str): - file_ext = filename.split(".")[-1].lower() + file_ext = filename.split('.')[-1].lower() if ( - self.engine == "external" - and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL") - and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY") + self.engine == 'external' + and self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_URL') + and self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_API_KEY') ): loader = ExternalDocumentLoader( file_path=file_path, - url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"), - api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"), + url=self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_URL'), + api_key=self.kwargs.get('EXTERNAL_DOCUMENT_LOADER_API_KEY'), mime_type=file_content_type, user=self.user, ) - elif self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"): + elif self.engine == 'tika' and self.kwargs.get('TIKA_SERVER_URL'): if self._is_text_file(file_ext, file_content_type): - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) else: loader = TikaLoader( - url=self.kwargs.get("TIKA_SERVER_URL"), + url=self.kwargs.get('TIKA_SERVER_URL'), file_path=file_path, - extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES"), + extract_images=self.kwargs.get('PDF_EXTRACT_IMAGES'), ) elif ( - self.engine == "datalab_marker" - and self.kwargs.get("DATALAB_MARKER_API_KEY") + self.engine == 'datalab_marker' + and self.kwargs.get('DATALAB_MARKER_API_KEY') and file_ext in [ - "pdf", - "xls", - "xlsx", - "ods", - "doc", - "docx", - "odt", - "ppt", - "pptx", - "odp", - "html", - "epub", - "png", - "jpeg", - "jpg", - "webp", - "gif", - "tiff", + 'pdf', + 'xls', + 'xlsx', + 'ods', + 'doc', + 'docx', + 'odt', + 'ppt', + 'pptx', + 'odp', + 'html', + 'epub', + 'png', + 'jpeg', + 'jpg', + 'webp', + 'gif', + 'tiff', ] ): - api_base_url = self.kwargs.get("DATALAB_MARKER_API_BASE_URL", "") - if not api_base_url or api_base_url.strip() == "": - api_base_url = "https://www.datalab.to/api/v1/marker" # https://github.com/open-webui/open-webui/pull/16867#issuecomment-3218424349 + api_base_url = self.kwargs.get('DATALAB_MARKER_API_BASE_URL', '') + if not api_base_url or api_base_url.strip() == '': + api_base_url = 'https://www.datalab.to/api/v1/marker' # https://github.com/open-webui/open-webui/pull/16867#issuecomment-3218424349 loader = DatalabMarkerLoader( file_path=file_path, - api_key=self.kwargs["DATALAB_MARKER_API_KEY"], + api_key=self.kwargs['DATALAB_MARKER_API_KEY'], api_base_url=api_base_url, - additional_config=self.kwargs.get("DATALAB_MARKER_ADDITIONAL_CONFIG"), - use_llm=self.kwargs.get("DATALAB_MARKER_USE_LLM", False), - skip_cache=self.kwargs.get("DATALAB_MARKER_SKIP_CACHE", False), - force_ocr=self.kwargs.get("DATALAB_MARKER_FORCE_OCR", False), - paginate=self.kwargs.get("DATALAB_MARKER_PAGINATE", False), - strip_existing_ocr=self.kwargs.get( - "DATALAB_MARKER_STRIP_EXISTING_OCR", False - ), - disable_image_extraction=self.kwargs.get( - "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION", False - ), - format_lines=self.kwargs.get("DATALAB_MARKER_FORMAT_LINES", False), - output_format=self.kwargs.get( - "DATALAB_MARKER_OUTPUT_FORMAT", "markdown" - ), + additional_config=self.kwargs.get('DATALAB_MARKER_ADDITIONAL_CONFIG'), + use_llm=self.kwargs.get('DATALAB_MARKER_USE_LLM', False), + skip_cache=self.kwargs.get('DATALAB_MARKER_SKIP_CACHE', False), + force_ocr=self.kwargs.get('DATALAB_MARKER_FORCE_OCR', False), + paginate=self.kwargs.get('DATALAB_MARKER_PAGINATE', False), + strip_existing_ocr=self.kwargs.get('DATALAB_MARKER_STRIP_EXISTING_OCR', False), + disable_image_extraction=self.kwargs.get('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', False), + format_lines=self.kwargs.get('DATALAB_MARKER_FORMAT_LINES', False), + output_format=self.kwargs.get('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown'), ) - elif self.engine == "docling" and self.kwargs.get("DOCLING_SERVER_URL"): + elif self.engine == 'docling' and self.kwargs.get('DOCLING_SERVER_URL'): if self._is_text_file(file_ext, file_content_type): - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) else: # Build params for DoclingLoader - params = self.kwargs.get("DOCLING_PARAMS", {}) + params = self.kwargs.get('DOCLING_PARAMS', {}) if not isinstance(params, dict): try: params = json.loads(params) except json.JSONDecodeError: - log.error("Invalid DOCLING_PARAMS format, expected JSON object") + log.error('Invalid DOCLING_PARAMS format, expected JSON object') params = {} loader = DoclingLoader( - url=self.kwargs.get("DOCLING_SERVER_URL"), - api_key=self.kwargs.get("DOCLING_API_KEY", None), + url=self.kwargs.get('DOCLING_SERVER_URL'), + api_key=self.kwargs.get('DOCLING_API_KEY', None), file_path=file_path, mime_type=file_content_type, params=params, ) elif ( - self.engine == "document_intelligence" - and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != "" + self.engine == 'document_intelligence' + and self.kwargs.get('DOCUMENT_INTELLIGENCE_ENDPOINT') != '' and ( - file_ext in ["pdf", "docx", "ppt", "pptx"] + file_ext in ['pdf', 'docx', 'ppt', 'pptx'] or file_content_type in [ - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', ] ) ): - if self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != "": + if self.kwargs.get('DOCUMENT_INTELLIGENCE_KEY') != '': loader = AzureAIDocumentIntelligenceLoader( file_path=file_path, - api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"), - api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"), - api_model=self.kwargs.get("DOCUMENT_INTELLIGENCE_MODEL"), + api_endpoint=self.kwargs.get('DOCUMENT_INTELLIGENCE_ENDPOINT'), + api_key=self.kwargs.get('DOCUMENT_INTELLIGENCE_KEY'), + api_model=self.kwargs.get('DOCUMENT_INTELLIGENCE_MODEL'), ) else: loader = AzureAIDocumentIntelligenceLoader( file_path=file_path, - api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"), + api_endpoint=self.kwargs.get('DOCUMENT_INTELLIGENCE_ENDPOINT'), azure_credential=DefaultAzureCredential(), - api_model=self.kwargs.get("DOCUMENT_INTELLIGENCE_MODEL"), + api_model=self.kwargs.get('DOCUMENT_INTELLIGENCE_MODEL'), ) - elif self.engine == "mineru" and file_ext in [ - "pdf" - ]: # MinerU currently only supports PDF - - mineru_timeout = self.kwargs.get("MINERU_API_TIMEOUT", 300) + elif self.engine == 'mineru' and file_ext in self.kwargs.get('MINERU_FILE_EXTENSIONS', ['pdf']): + mineru_timeout = self.kwargs.get('MINERU_API_TIMEOUT', 300) if mineru_timeout: try: mineru_timeout = int(mineru_timeout) @@ -341,63 +514,136 @@ class Loader: loader = MinerULoader( file_path=file_path, - api_mode=self.kwargs.get("MINERU_API_MODE", "local"), - api_url=self.kwargs.get("MINERU_API_URL", "http://localhost:8000"), - api_key=self.kwargs.get("MINERU_API_KEY", ""), - params=self.kwargs.get("MINERU_PARAMS", {}), + api_mode=self.kwargs.get('MINERU_API_MODE', 'local'), + api_url=self.kwargs.get('MINERU_API_URL', 'http://localhost:8000'), + api_key=self.kwargs.get('MINERU_API_KEY', ''), + params=self.kwargs.get('MINERU_PARAMS', {}), timeout=mineru_timeout, ) elif ( - self.engine == "mistral_ocr" - and self.kwargs.get("MISTRAL_OCR_API_KEY") != "" - and file_ext - in ["pdf"] # Mistral OCR currently only supports PDF and images + self.engine == 'mistral_ocr' + and self.kwargs.get('MISTRAL_OCR_API_KEY') != '' + and file_ext in ['pdf'] # Mistral OCR currently only supports PDF and images ): loader = MistralLoader( - base_url=self.kwargs.get("MISTRAL_OCR_API_BASE_URL"), - api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), + base_url=self.kwargs.get('MISTRAL_OCR_API_BASE_URL'), + api_key=self.kwargs.get('MISTRAL_OCR_API_KEY'), + file_path=file_path, + ) + elif self.engine == 'paddleocr_vl' and self.kwargs.get('PADDLEOCR_VL_TOKEN') != '': + loader = PaddleOCRVLLoader( + api_url=self.kwargs.get('PADDLEOCR_VL_BASE_URL'), + token=self.kwargs.get('PADDLEOCR_VL_TOKEN'), file_path=file_path, ) else: - if file_ext == "pdf": + if file_ext == 'pdf': loader = PyPDFLoader( - file_path, extract_images=self.kwargs.get("PDF_EXTRACT_IMAGES") + file_path, + extract_images=self.kwargs.get('PDF_EXTRACT_IMAGES'), + mode=self.kwargs.get('PDF_LOADER_MODE', 'page'), ) - elif file_ext == "csv": - loader = CSVLoader(file_path, autodetect_encoding=True) - elif file_ext == "rst": - loader = UnstructuredRSTLoader(file_path, mode="elements") - elif file_ext == "xml": - loader = UnstructuredXMLLoader(file_path) - elif file_ext in ["htm", "html"]: - loader = BSHTMLLoader(file_path, open_encoding="unicode_escape") - elif file_ext == "md": - loader = TextLoader(file_path, autodetect_encoding=True) - elif file_content_type == "application/epub+zip": - loader = UnstructuredEPubLoader(file_path) + elif file_ext == 'csv': + loader = CSVLoader(file_path, encoding=self._detect_text_encoding(file_path)) + elif file_ext == 'rst': + try: + from langchain_community.document_loaders import UnstructuredRSTLoader + + loader = UnstructuredRSTLoader(file_path, mode='elements') + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to plain text loading for .rst file. ' + 'Install it with: pip install unstructured' + ) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) + elif file_ext == 'xml': + try: + from langchain_community.document_loaders import UnstructuredXMLLoader + + loader = UnstructuredXMLLoader(file_path) + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to plain text loading for .xml file. ' + 'Install it with: pip install unstructured' + ) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) + elif file_ext in ['htm', 'html']: + loader = BSHTMLLoader(file_path, open_encoding='unicode_escape') + elif file_ext == 'md': + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) + elif file_content_type == 'application/epub+zip': + try: + from langchain_community.document_loaders import UnstructuredEPubLoader + + loader = UnstructuredEPubLoader(file_path) + except ImportError: + raise ValueError( + "Processing .epub files requires the 'unstructured' package. " + 'Install it with: pip install unstructured' + ) elif ( - file_content_type - == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - or file_ext == "docx" + file_content_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + or file_ext == 'docx' ): loader = Docx2txtLoader(file_path) + elif file_ext == 'doc' or file_content_type == 'application/msword': + try: + from langchain_community.document_loaders import UnstructuredWordDocumentLoader + + loader = UnstructuredWordDocumentLoader(file_path) + except ImportError: + raise ValueError( + "Processing .doc files requires the 'unstructured' package. " + 'Install it with: pip install unstructured' + ) elif file_content_type in [ - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ] or file_ext in ["xls", "xlsx"]: - loader = UnstructuredExcelLoader(file_path) + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ] or file_ext in ['xls', 'xlsx']: + try: + from langchain_community.document_loaders import UnstructuredExcelLoader + + loader = UnstructuredExcelLoader(file_path) + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to pandas for Excel file loading. ' + 'Install unstructured for better results: pip install unstructured' + ) + loader = ExcelLoader(file_path) elif file_content_type in [ - "application/vnd.ms-powerpoint", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - ] or file_ext in ["ppt", "pptx"]: - loader = UnstructuredPowerPointLoader(file_path) - elif file_ext == "msg": + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ] or file_ext in ['ppt', 'pptx']: + try: + from langchain_community.document_loaders import UnstructuredPowerPointLoader + + loader = UnstructuredPowerPointLoader(file_path) + except ImportError: + log.warning( + "The 'unstructured' package is not installed. " + 'Falling back to python-pptx for PowerPoint file loading. ' + 'Install unstructured for better results: pip install unstructured' + ) + loader = PptxLoader(file_path) + elif file_ext == 'msg': loader = OutlookMessageLoader(file_path) - elif file_ext == "odt": - loader = UnstructuredODTLoader(file_path) + elif file_ext == 'odt': + try: + from langchain_community.document_loaders import UnstructuredODTLoader + + loader = UnstructuredODTLoader(file_path) + except ImportError: + raise ValueError( + "Processing .odt files requires the 'unstructured' package. " + 'Install it with: pip install unstructured' + ) elif self._is_text_file(file_ext, file_content_type): - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) else: - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) return loader + diff --git a/backend/open_webui/retrieval/loaders/mineru.py b/backend/open_webui/retrieval/loaders/mineru.py index 617be8e87a..63608f9bf9 100644 --- a/backend/open_webui/retrieval/loaders/mineru.py +++ b/backend/open_webui/retrieval/loaders/mineru.py @@ -1,12 +1,13 @@ -import os -import time -import requests import logging +import os import tempfile +import time import zipfile from typing import List, Optional -from langchain_core.documents import Document + +import requests from fastapi import HTTPException, status +from langchain_core.documents import Document log = logging.getLogger(__name__) @@ -22,37 +23,35 @@ class MinerULoader: def __init__( self, file_path: str, - api_mode: str = "local", - api_url: str = "http://localhost:8000", - api_key: str = "", + api_mode: str = 'local', + api_url: str = 'http://localhost:8000', + api_key: str = '', params: dict = None, timeout: Optional[int] = 300, ): self.file_path = file_path self.api_mode = api_mode.lower() - self.api_url = api_url.rstrip("/") + self.api_url = api_url.rstrip('/') self.api_key = api_key self.timeout = timeout # Parse params dict with defaults self.params = params or {} - self.enable_ocr = params.get("enable_ocr", False) - self.enable_formula = params.get("enable_formula", True) - self.enable_table = params.get("enable_table", True) - self.language = params.get("language", "en") - self.model_version = params.get("model_version", "pipeline") + self.enable_ocr = params.get('enable_ocr', False) + self.enable_formula = params.get('enable_formula', True) + self.enable_table = params.get('enable_table', True) + self.language = params.get('language', 'en') + self.model_version = params.get('model_version', 'pipeline') - self.page_ranges = self.params.pop("page_ranges", "") + self.page_ranges = self.params.pop('page_ranges', '') # Validate API mode - if self.api_mode not in ["local", "cloud"]: - raise ValueError( - f"Invalid API mode: {self.api_mode}. Must be 'local' or 'cloud'" - ) + if self.api_mode not in ['local', 'cloud']: + raise ValueError(f"Invalid API mode: {self.api_mode}. Must be 'local' or 'cloud'") # Validate Cloud API requirements - if self.api_mode == "cloud" and not self.api_key: - raise ValueError("API key is required for Cloud API mode") + if self.api_mode == 'cloud' and not self.api_key: + raise ValueError('API key is required for Cloud API mode') def load(self) -> List[Document]: """ @@ -60,12 +59,12 @@ class MinerULoader: Routes to Cloud or Local API based on api_mode. """ try: - if self.api_mode == "cloud": + if self.api_mode == 'cloud': return self._load_cloud_api() else: return self._load_local_api() except Exception as e: - log.error(f"Error loading document with MinerU: {e}") + log.error(f'Error loading document with MinerU: {e}') raise def _load_local_api(self) -> List[Document]: @@ -73,14 +72,14 @@ class MinerULoader: Load document using Local API (synchronous). Posts file to /file_parse endpoint and gets immediate response. """ - log.info(f"Using MinerU Local API at {self.api_url}") + log.info(f'Using MinerU Local API at {self.api_url}') filename = os.path.basename(self.file_path) # Build form data for Local API form_data = { **self.params, - "return_md": "true", + 'return_md': 'true', } # Page ranges (Local API uses start_page_id and end_page_id) @@ -89,18 +88,18 @@ class MinerULoader: # Full page range parsing would require parsing the string log.warning( f"Page ranges '{self.page_ranges}' specified but Local API uses different format. " - "Consider using start_page_id/end_page_id parameters if needed." + 'Consider using start_page_id/end_page_id parameters if needed.' ) try: - with open(self.file_path, "rb") as f: - files = {"files": (filename, f, "application/octet-stream")} + with open(self.file_path, 'rb') as f: + files = {'files': (filename, f, 'application/octet-stream')} - log.info(f"Sending file to MinerU Local API: {filename}") - log.debug(f"Local API parameters: {form_data}") + log.info(f'Sending file to MinerU Local API: {filename}') + log.debug(f'Local API parameters: {form_data}') response = requests.post( - f"{self.api_url}/file_parse", + f'{self.api_url}/file_parse', data=form_data, files=files, timeout=self.timeout, @@ -108,27 +107,25 @@ class MinerULoader: response.raise_for_status() except FileNotFoundError: - raise HTTPException( - status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}" - ) + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f'File not found: {self.file_path}') except requests.Timeout: raise HTTPException( status.HTTP_504_GATEWAY_TIMEOUT, - detail="MinerU Local API request timed out", + detail='MinerU Local API request timed out', ) except requests.HTTPError as e: - error_detail = f"MinerU Local API request failed: {e}" + error_detail = f'MinerU Local API request failed: {e}' if e.response is not None: try: error_data = e.response.json() - error_detail += f" - {error_data}" - except: - error_detail += f" - {e.response.text}" + error_detail += f' - {error_data}' + except Exception: + error_detail += f' - {e.response.text}' raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) except Exception as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error calling MinerU Local API: {str(e)}", + detail=f'Error calling MinerU Local API: {str(e)}', ) # Parse response @@ -137,41 +134,41 @@ class MinerULoader: except ValueError as e: raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail=f"Invalid JSON response from MinerU Local API: {e}", + detail=f'Invalid JSON response from MinerU Local API: {e}', ) # Extract markdown content from response - if "results" not in result: + if 'results' not in result: raise HTTPException( status.HTTP_502_BAD_GATEWAY, detail="MinerU Local API response missing 'results' field", ) - results = result["results"] + results = result['results'] if not results: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail="MinerU returned empty results", + detail='MinerU returned empty results', ) # Get the first (and typically only) result file_result = list(results.values())[0] - markdown_content = file_result.get("md_content", "") + markdown_content = file_result.get('md_content', '') if not markdown_content: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail="MinerU returned empty markdown content", + detail='MinerU returned empty markdown content', ) - log.info(f"Successfully parsed document with MinerU Local API: {filename}") + log.info(f'Successfully parsed document with MinerU Local API: {filename}') # Create metadata metadata = { - "source": filename, - "api_mode": "local", - "backend": result.get("backend", "unknown"), - "version": result.get("version", "unknown"), + 'source': filename, + 'api_mode': 'local', + 'backend': result.get('backend', 'unknown'), + 'version': result.get('version', 'unknown'), } return [Document(page_content=markdown_content, metadata=metadata)] @@ -181,7 +178,7 @@ class MinerULoader: Load document using Cloud API (asynchronous). Uses batch upload endpoint to avoid need for public file URLs. """ - log.info(f"Using MinerU Cloud API at {self.api_url}") + log.info(f'Using MinerU Cloud API at {self.api_url}') filename = os.path.basename(self.file_path) @@ -195,17 +192,15 @@ class MinerULoader: result = self._poll_batch_status(batch_id, filename) # Step 4: Download and extract markdown from ZIP - markdown_content = self._download_and_extract_zip( - result["full_zip_url"], filename - ) + markdown_content = self._download_and_extract_zip(result['full_zip_url'], filename) - log.info(f"Successfully parsed document with MinerU Cloud API: {filename}") + log.info(f'Successfully parsed document with MinerU Cloud API: {filename}') # Create metadata metadata = { - "source": filename, - "api_mode": "cloud", - "batch_id": batch_id, + 'source': filename, + 'api_mode': 'cloud', + 'batch_id': batch_id, } return [Document(page_content=markdown_content, metadata=metadata)] @@ -216,49 +211,49 @@ class MinerULoader: Returns (batch_id, upload_url). """ headers = { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json", + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json', } # Build request body request_body = { **self.params, - "files": [ + 'files': [ { - "name": filename, - "is_ocr": self.enable_ocr, + 'name': filename, + 'is_ocr': self.enable_ocr, } ], } # Add page ranges if specified if self.page_ranges: - request_body["files"][0]["page_ranges"] = self.page_ranges + request_body['files'][0]['page_ranges'] = self.page_ranges - log.info(f"Requesting upload URL for: {filename}") - log.debug(f"Cloud API request body: {request_body}") + log.info(f'Requesting upload URL for: {filename}') + log.debug(f'Cloud API request body: {request_body}') try: response = requests.post( - f"{self.api_url}/file-urls/batch", + f'{self.api_url}/file-urls/batch', headers=headers, json=request_body, timeout=30, ) response.raise_for_status() except requests.HTTPError as e: - error_detail = f"Failed to request upload URL: {e}" + error_detail = f'Failed to request upload URL: {e}' if e.response is not None: try: error_data = e.response.json() - error_detail += f" - {error_data.get('msg', error_data)}" - except: - error_detail += f" - {e.response.text}" + error_detail += f' - {error_data.get("msg", error_data)}' + except Exception: + error_detail += f' - {e.response.text}' raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) except Exception as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error requesting upload URL: {str(e)}", + detail=f'Error requesting upload URL: {str(e)}', ) try: @@ -266,28 +261,28 @@ class MinerULoader: except ValueError as e: raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail=f"Invalid JSON response: {e}", + detail=f'Invalid JSON response: {e}', ) # Check for API error response - if result.get("code") != 0: + if result.get('code') != 0: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}", + detail=f'MinerU Cloud API error: {result.get("msg", "Unknown error")}', ) - data = result.get("data", {}) - batch_id = data.get("batch_id") - file_urls = data.get("file_urls", []) + data = result.get('data', {}) + batch_id = data.get('batch_id') + file_urls = data.get('file_urls', []) if not batch_id or not file_urls: raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail="MinerU Cloud API response missing batch_id or file_urls", + detail='MinerU Cloud API response missing batch_id or file_urls', ) upload_url = file_urls[0] - log.info(f"Received upload URL for batch: {batch_id}") + log.info(f'Received upload URL for batch: {batch_id}') return batch_id, upload_url @@ -295,10 +290,10 @@ class MinerULoader: """ Upload file to presigned URL (no authentication needed). """ - log.info(f"Uploading file to presigned URL") + log.info(f'Uploading file to presigned URL') try: - with open(self.file_path, "rb") as f: + with open(self.file_path, 'rb') as f: response = requests.put( upload_url, data=f, @@ -306,26 +301,24 @@ class MinerULoader: ) response.raise_for_status() except FileNotFoundError: - raise HTTPException( - status.HTTP_404_NOT_FOUND, detail=f"File not found: {self.file_path}" - ) + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f'File not found: {self.file_path}') except requests.Timeout: raise HTTPException( status.HTTP_504_GATEWAY_TIMEOUT, - detail="File upload to presigned URL timed out", + detail='File upload to presigned URL timed out', ) except requests.HTTPError as e: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Failed to upload file to presigned URL: {e}", + detail=f'Failed to upload file to presigned URL: {e}', ) except Exception as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error uploading file: {str(e)}", + detail=f'Error uploading file: {str(e)}', ) - log.info("File uploaded successfully") + log.info('File uploaded successfully') def _poll_batch_status(self, batch_id: str, filename: str) -> dict: """ @@ -333,35 +326,35 @@ class MinerULoader: Returns the result dict for the file. """ headers = { - "Authorization": f"Bearer {self.api_key}", + 'Authorization': f'Bearer {self.api_key}', } max_iterations = 300 # 10 minutes max (2 seconds per iteration) poll_interval = 2 # seconds - log.info(f"Polling batch status: {batch_id}") + log.info(f'Polling batch status: {batch_id}') for iteration in range(max_iterations): try: response = requests.get( - f"{self.api_url}/extract-results/batch/{batch_id}", + f'{self.api_url}/extract-results/batch/{batch_id}', headers=headers, timeout=30, ) response.raise_for_status() except requests.HTTPError as e: - error_detail = f"Failed to poll batch status: {e}" + error_detail = f'Failed to poll batch status: {e}' if e.response is not None: try: error_data = e.response.json() - error_detail += f" - {error_data.get('msg', error_data)}" - except: - error_detail += f" - {e.response.text}" + error_detail += f' - {error_data.get("msg", error_data)}' + except Exception: + error_detail += f' - {e.response.text}' raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=error_detail) except Exception as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error polling batch status: {str(e)}", + detail=f'Error polling batch status: {str(e)}', ) try: @@ -369,58 +362,56 @@ class MinerULoader: except ValueError as e: raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail=f"Invalid JSON response while polling: {e}", + detail=f'Invalid JSON response while polling: {e}', ) # Check for API error response - if result.get("code") != 0: + if result.get('code') != 0: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"MinerU Cloud API error: {result.get('msg', 'Unknown error')}", + detail=f'MinerU Cloud API error: {result.get("msg", "Unknown error")}', ) - data = result.get("data", {}) - extract_result = data.get("extract_result", []) + data = result.get('data', {}) + extract_result = data.get('extract_result', []) # Find our file in the batch results file_result = None for item in extract_result: - if item.get("file_name") == filename: + if item.get('file_name') == filename: file_result = item break if not file_result: raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail=f"File {filename} not found in batch results", + detail=f'File {filename} not found in batch results', ) - state = file_result.get("state") + state = file_result.get('state') - if state == "done": - log.info(f"Processing complete for {filename}") + if state == 'done': + log.info(f'Processing complete for {filename}') return file_result - elif state == "failed": - error_msg = file_result.get("err_msg", "Unknown error") + elif state == 'failed': + error_msg = file_result.get('err_msg', 'Unknown error') raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"MinerU processing failed: {error_msg}", + detail=f'MinerU processing failed: {error_msg}', ) - elif state in ["waiting-file", "pending", "running", "converting"]: + elif state in ['waiting-file', 'pending', 'running', 'converting']: # Still processing if iteration % 10 == 0: # Log every 20 seconds - log.info( - f"Processing status: {state} (iteration {iteration + 1}/{max_iterations})" - ) + log.info(f'Processing status: {state} (iteration {iteration + 1}/{max_iterations})') time.sleep(poll_interval) else: - log.warning(f"Unknown state: {state}") + log.warning(f'Unknown state: {state}') time.sleep(poll_interval) # Timeout raise HTTPException( status.HTTP_504_GATEWAY_TIMEOUT, - detail="MinerU processing timed out after 10 minutes", + detail='MinerU processing timed out after 10 minutes', ) def _download_and_extract_zip(self, zip_url: str, filename: str) -> str: @@ -428,7 +419,7 @@ class MinerULoader: Download ZIP file from CDN and extract markdown content. Returns the markdown content as a string. """ - log.info(f"Downloading results from: {zip_url}") + log.info(f'Downloading results from: {zip_url}') try: response = requests.get(zip_url, timeout=60) @@ -436,23 +427,23 @@ class MinerULoader: except requests.HTTPError as e: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail=f"Failed to download results ZIP: {e}", + detail=f'Failed to download results ZIP: {e}', ) except Exception as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error downloading results: {str(e)}", + detail=f'Error downloading results: {str(e)}', ) # Save ZIP to temporary file and extract try: - with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_zip: + with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as tmp_zip: tmp_zip.write(response.content) tmp_zip_path = tmp_zip.name with tempfile.TemporaryDirectory() as tmp_dir: # Extract ZIP - with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref: + with zipfile.ZipFile(tmp_zip_path, 'r') as zip_ref: zip_ref.extractall(tmp_dir) # Find markdown file - search recursively for any .md file @@ -466,33 +457,27 @@ class MinerULoader: full_path = os.path.join(root, file) all_files.append(full_path) # Look for any .md file - if file.endswith(".md"): + if file.endswith('.md'): found_md_path = full_path - log.info(f"Found markdown file at: {full_path}") + log.info(f'Found markdown file at: {full_path}') try: - with open(full_path, "r", encoding="utf-8") as f: + with open(full_path, 'r', encoding='utf-8') as f: markdown_content = f.read() - if ( - markdown_content - ): # Use the first non-empty markdown file + if markdown_content: # Use the first non-empty markdown file break except Exception as e: - log.warning(f"Failed to read {full_path}: {e}") + log.warning(f'Failed to read {full_path}: {e}') if markdown_content: break if markdown_content is None: - log.error(f"Available files in ZIP: {all_files}") + log.error(f'Available files in ZIP: {all_files}') # Try to provide more helpful error message - md_files = [f for f in all_files if f.endswith(".md")] + md_files = [f for f in all_files if f.endswith('.md')] if md_files: - error_msg = ( - f"Found .md files but couldn't read them: {md_files}" - ) + error_msg = f"Found .md files but couldn't read them: {md_files}" else: - error_msg = ( - f"No .md files found in ZIP. Available files: {all_files}" - ) + error_msg = f'No .md files found in ZIP. Available files: {all_files}' raise HTTPException( status.HTTP_502_BAD_GATEWAY, detail=error_msg, @@ -504,21 +489,19 @@ class MinerULoader: except zipfile.BadZipFile as e: raise HTTPException( status.HTTP_502_BAD_GATEWAY, - detail=f"Invalid ZIP file received: {e}", + detail=f'Invalid ZIP file received: {e}', ) except Exception as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error extracting ZIP: {str(e)}", + detail=f'Error extracting ZIP: {str(e)}', ) if not markdown_content: raise HTTPException( status.HTTP_400_BAD_REQUEST, - detail="Extracted markdown content is empty", + detail='Extracted markdown content is empty', ) - log.info( - f"Successfully extracted markdown content ({len(markdown_content)} characters)" - ) + log.info(f'Successfully extracted markdown content ({len(markdown_content)} characters)') return markdown_content diff --git a/backend/open_webui/retrieval/loaders/mistral.py b/backend/open_webui/retrieval/loaders/mistral.py index 68570757c8..465ea5d91e 100644 --- a/backend/open_webui/retrieval/loaders/mistral.py +++ b/backend/open_webui/retrieval/loaders/mistral.py @@ -1,15 +1,15 @@ -import requests -import aiohttp import asyncio import logging import os import sys import time -from typing import List, Dict, Any from contextlib import asynccontextmanager +from typing import Any, Dict, List +import aiohttp +import requests from langchain_core.documents import Document -from open_webui.env import GLOBAL_LOG_LEVEL +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, GLOBAL_LOG_LEVEL logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) @@ -49,13 +49,11 @@ class MistralLoader: enable_debug_logging: Enable detailed debug logs. """ if not api_key: - raise ValueError("API key cannot be empty.") + raise ValueError('API key cannot be empty.') if not os.path.exists(file_path): - raise FileNotFoundError(f"File not found at {file_path}") + raise FileNotFoundError(f'File not found at {file_path}') - self.base_url = ( - base_url.rstrip("/") if base_url else "https://api.mistral.ai/v1" - ) + self.base_url = base_url.rstrip('/') if base_url else 'https://api.mistral.ai/v1' self.api_key = api_key self.file_path = file_path self.timeout = timeout @@ -65,18 +63,10 @@ class MistralLoader: # PERFORMANCE OPTIMIZATION: Differentiated timeouts for different operations # This prevents long-running OCR operations from affecting quick operations # and improves user experience by failing fast on operations that should be quick - self.upload_timeout = min( - timeout, 120 - ) # Cap upload at 2 minutes - prevents hanging on large files - self.url_timeout = ( - 30 # URL requests should be fast - fail quickly if API is slow - ) - self.ocr_timeout = ( - timeout # OCR can take the full timeout - this is the heavy operation - ) - self.cleanup_timeout = ( - 30 # Cleanup should be quick - don't hang on file deletion - ) + self.upload_timeout = min(timeout, 120) # Cap upload at 2 minutes - prevents hanging on large files + self.url_timeout = 30 # URL requests should be fast - fail quickly if API is slow + self.ocr_timeout = timeout # OCR can take the full timeout - this is the heavy operation + self.cleanup_timeout = 30 # Cleanup should be quick - don't hang on file deletion # PERFORMANCE OPTIMIZATION: Pre-compute file info to avoid repeated filesystem calls # This avoids multiple os.path.basename() and os.path.getsize() calls during processing @@ -85,8 +75,8 @@ class MistralLoader: # ENHANCEMENT: Added User-Agent for better API tracking and debugging self.headers = { - "Authorization": f"Bearer {self.api_key}", - "User-Agent": "OpenWebUI-MistralLoader/2.0", # Helps API provider track usage + 'Authorization': f'Bearer {self.api_key}', + 'User-Agent': 'OpenWebUI-MistralLoader/2.0', # Helps API provider track usage } def _debug_log(self, message: str, *args) -> None: @@ -108,43 +98,39 @@ class MistralLoader: return {} # Return empty dict if no content return response.json() except requests.exceptions.HTTPError as http_err: - log.error(f"HTTP error occurred: {http_err} - Response: {response.text}") + log.error(f'HTTP error occurred: {http_err} - Response: {response.text}') raise except requests.exceptions.RequestException as req_err: - log.error(f"Request exception occurred: {req_err}") + log.error(f'Request exception occurred: {req_err}') raise except ValueError as json_err: # Includes JSONDecodeError - log.error(f"JSON decode error: {json_err} - Response: {response.text}") + log.error(f'JSON decode error: {json_err} - Response: {response.text}') raise # Re-raise after logging - async def _handle_response_async( - self, response: aiohttp.ClientResponse - ) -> Dict[str, Any]: + async def _handle_response_async(self, response: aiohttp.ClientResponse) -> Dict[str, Any]: """Async version of response handling with better error info.""" try: response.raise_for_status() # Check content type - content_type = response.headers.get("content-type", "") - if "application/json" not in content_type: + content_type = response.headers.get('content-type', '') + if 'application/json' not in content_type: if response.status == 204: return {} text = await response.text() - raise ValueError( - f"Unexpected content type: {content_type}, body: {text[:200]}..." - ) + raise ValueError(f'Unexpected content type: {content_type}, body: {text[:200]}...') return await response.json() except aiohttp.ClientResponseError as e: - error_text = await response.text() if response else "No response" - log.error(f"HTTP {e.status}: {e.message} - Response: {error_text[:500]}") + error_text = await response.text() if response else 'No response' + log.error(f'HTTP {e.status}: {e.message} - Response: {error_text[:500]}') raise except aiohttp.ClientError as e: - log.error(f"Client error: {e}") + log.error(f'Client error: {e}') raise except Exception as e: - log.error(f"Unexpected error processing response: {e}") + log.error(f'Unexpected error processing response: {e}') raise def _is_retryable_error(self, error: Exception) -> bool: @@ -172,13 +158,11 @@ class MistralLoader: return True # Timeouts might resolve on retry if isinstance(error, requests.exceptions.HTTPError): # Only retry on server errors (5xx) or rate limits (429) - if hasattr(error, "response") and error.response is not None: + if hasattr(error, 'response') and error.response is not None: status_code = error.response.status_code return status_code >= 500 or status_code == 429 return False - if isinstance( - error, (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError) - ): + if isinstance(error, (aiohttp.ClientConnectionError, aiohttp.ServerTimeoutError)): return True # Async network/timeout errors are retryable if isinstance(error, aiohttp.ClientResponseError): return error.status >= 500 or error.status == 429 @@ -204,8 +188,7 @@ class MistralLoader: # Prevents overwhelming the server while ensuring reasonable retry delays wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds log.warning( - f"Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. " - f"Retrying in {wait_time}s..." + f'Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. Retrying in {wait_time}s...' ) time.sleep(wait_time) @@ -226,8 +209,7 @@ class MistralLoader: # PERFORMANCE OPTIMIZATION: Non-blocking exponential backoff wait_time = min((2**attempt) + 0.5, 30) # Cap at 30 seconds log.warning( - f"Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. " - f"Retrying in {wait_time}s..." + f'Retryable error (attempt {attempt + 1}/{self.max_retries}): {e}. Retrying in {wait_time}s...' ) await asyncio.sleep(wait_time) # Non-blocking wait @@ -240,15 +222,15 @@ class MistralLoader: Although streaming is not enabled for this endpoint, the file is opened in a context manager to minimize memory usage duration. """ - log.info("Uploading file to Mistral API") - url = f"{self.base_url}/files" + log.info('Uploading file to Mistral API') + url = f'{self.base_url}/files' def upload_request(): # MEMORY OPTIMIZATION: Use context manager to minimize file handle lifetime # This ensures the file is closed immediately after reading, reducing memory usage - with open(self.file_path, "rb") as f: - files = {"file": (self.file_name, f, "application/pdf")} - data = {"purpose": "ocr"} + with open(self.file_path, 'rb') as f: + files = {'file': (self.file_name, f, 'application/pdf')} + data = {'purpose': 'ocr'} # NOTE: stream=False is required for this endpoint # The Mistral API doesn't support chunked uploads for this endpoint @@ -265,168 +247,158 @@ class MistralLoader: try: response_data = self._retry_request_sync(upload_request) - file_id = response_data.get("id") + file_id = response_data.get('id') if not file_id: - raise ValueError("File ID not found in upload response.") - log.info(f"File uploaded successfully. File ID: {file_id}") + raise ValueError('File ID not found in upload response.') + log.info(f'File uploaded successfully. File ID: {file_id}') return file_id except Exception as e: - log.error(f"Failed to upload file: {e}") + log.error(f'Failed to upload file: {e}') raise async def _upload_file_async(self, session: aiohttp.ClientSession) -> str: """Async file upload with streaming for better memory efficiency.""" - url = f"{self.base_url}/files" + url = f'{self.base_url}/files' async def upload_request(): # Create multipart writer for streaming upload - writer = aiohttp.MultipartWriter("form-data") + writer = aiohttp.MultipartWriter('form-data') # Add purpose field - purpose_part = writer.append("ocr") - purpose_part.set_content_disposition("form-data", name="purpose") + purpose_part = writer.append('ocr') + purpose_part.set_content_disposition('form-data', name='purpose') # Add file part with streaming file_part = writer.append_payload( aiohttp.streams.FilePayload( self.file_path, filename=self.file_name, - content_type="application/pdf", + content_type='application/pdf', ) ) - file_part.set_content_disposition( - "form-data", name="file", filename=self.file_name - ) + file_part.set_content_disposition('form-data', name='file', filename=self.file_name) - self._debug_log( - f"Uploading file: {self.file_name} ({self.file_size:,} bytes)" - ) + self._debug_log(f'Uploading file: {self.file_name} ({self.file_size:,} bytes)') async with session.post( url, data=writer, headers=self.headers, timeout=aiohttp.ClientTimeout(total=self.upload_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: return await self._handle_response_async(response) response_data = await self._retry_request_async(upload_request) - file_id = response_data.get("id") + file_id = response_data.get('id') if not file_id: - raise ValueError("File ID not found in upload response.") + raise ValueError('File ID not found in upload response.') - log.info(f"File uploaded successfully. File ID: {file_id}") + log.info(f'File uploaded successfully. File ID: {file_id}') return file_id def _get_signed_url(self, file_id: str) -> str: """Retrieves a temporary signed URL for the uploaded file (sync version).""" - log.info(f"Getting signed URL for file ID: {file_id}") - url = f"{self.base_url}/files/{file_id}/url" - params = {"expiry": 1} - signed_url_headers = {**self.headers, "Accept": "application/json"} + log.info(f'Getting signed URL for file ID: {file_id}') + url = f'{self.base_url}/files/{file_id}/url' + params = {'expiry': 1} + signed_url_headers = {**self.headers, 'Accept': 'application/json'} def url_request(): - response = requests.get( - url, headers=signed_url_headers, params=params, timeout=self.url_timeout - ) + response = requests.get(url, headers=signed_url_headers, params=params, timeout=self.url_timeout) return self._handle_response(response) try: response_data = self._retry_request_sync(url_request) - signed_url = response_data.get("url") + signed_url = response_data.get('url') if not signed_url: - raise ValueError("Signed URL not found in response.") - log.info("Signed URL received.") + raise ValueError('Signed URL not found in response.') + log.info('Signed URL received.') return signed_url except Exception as e: - log.error(f"Failed to get signed URL: {e}") + log.error(f'Failed to get signed URL: {e}') raise - async def _get_signed_url_async( - self, session: aiohttp.ClientSession, file_id: str - ) -> str: + async def _get_signed_url_async(self, session: aiohttp.ClientSession, file_id: str) -> str: """Async signed URL retrieval.""" - url = f"{self.base_url}/files/{file_id}/url" - params = {"expiry": 1} + url = f'{self.base_url}/files/{file_id}/url' + params = {'expiry': 1} - headers = {**self.headers, "Accept": "application/json"} + headers = {**self.headers, 'Accept': 'application/json'} async def url_request(): - self._debug_log(f"Getting signed URL for file ID: {file_id}") + self._debug_log(f'Getting signed URL for file ID: {file_id}') async with session.get( url, headers=headers, params=params, timeout=aiohttp.ClientTimeout(total=self.url_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: return await self._handle_response_async(response) response_data = await self._retry_request_async(url_request) - signed_url = response_data.get("url") + signed_url = response_data.get('url') if not signed_url: - raise ValueError("Signed URL not found in response.") + raise ValueError('Signed URL not found in response.') - self._debug_log("Signed URL received successfully") + self._debug_log('Signed URL received successfully') return signed_url def _process_ocr(self, signed_url: str) -> Dict[str, Any]: """Sends the signed URL to the OCR endpoint for processing (sync version).""" - log.info("Processing OCR via Mistral API") - url = f"{self.base_url}/ocr" + log.info('Processing OCR via Mistral API') + url = f'{self.base_url}/ocr' ocr_headers = { **self.headers, - "Content-Type": "application/json", - "Accept": "application/json", + 'Content-Type': 'application/json', + 'Accept': 'application/json', } payload = { - "model": "mistral-ocr-latest", - "document": { - "type": "document_url", - "document_url": signed_url, + 'model': 'mistral-ocr-latest', + 'document': { + 'type': 'document_url', + 'document_url': signed_url, }, - "include_image_base64": False, + 'include_image_base64': False, } def ocr_request(): - response = requests.post( - url, headers=ocr_headers, json=payload, timeout=self.ocr_timeout - ) + response = requests.post(url, headers=ocr_headers, json=payload, timeout=self.ocr_timeout) return self._handle_response(response) try: ocr_response = self._retry_request_sync(ocr_request) - log.info("OCR processing done.") - self._debug_log("OCR response: %s", ocr_response) + log.info('OCR processing done.') + self._debug_log('OCR response: %s', ocr_response) return ocr_response except Exception as e: - log.error(f"Failed during OCR processing: {e}") + log.error(f'Failed during OCR processing: {e}') raise - async def _process_ocr_async( - self, session: aiohttp.ClientSession, signed_url: str - ) -> Dict[str, Any]: + async def _process_ocr_async(self, session: aiohttp.ClientSession, signed_url: str) -> Dict[str, Any]: """Async OCR processing with timing metrics.""" - url = f"{self.base_url}/ocr" + url = f'{self.base_url}/ocr' headers = { **self.headers, - "Content-Type": "application/json", - "Accept": "application/json", + 'Content-Type': 'application/json', + 'Accept': 'application/json', } payload = { - "model": "mistral-ocr-latest", - "document": { - "type": "document_url", - "document_url": signed_url, + 'model': 'mistral-ocr-latest', + 'document': { + 'type': 'document_url', + 'document_url': signed_url, }, - "include_image_base64": False, + 'include_image_base64': False, } async def ocr_request(): - log.info("Starting OCR processing via Mistral API") + log.info('Starting OCR processing via Mistral API') start_time = time.time() async with session.post( @@ -434,11 +406,12 @@ class MistralLoader: json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=self.ocr_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: ocr_response = await self._handle_response_async(response) processing_time = time.time() - start_time - log.info(f"OCR processing completed in {processing_time:.2f}s") + log.info(f'OCR processing completed in {processing_time:.2f}s') return ocr_response @@ -446,42 +419,37 @@ class MistralLoader: def _delete_file(self, file_id: str) -> None: """Deletes the file from Mistral storage (sync version).""" - log.info(f"Deleting uploaded file ID: {file_id}") - url = f"{self.base_url}/files/{file_id}" + log.info(f'Deleting uploaded file ID: {file_id}') + url = f'{self.base_url}/files/{file_id}' try: - response = requests.delete( - url, headers=self.headers, timeout=self.cleanup_timeout - ) + response = requests.delete(url, headers=self.headers, timeout=self.cleanup_timeout) delete_response = self._handle_response(response) - log.info(f"File deleted successfully: {delete_response}") + log.info(f'File deleted successfully: {delete_response}') except Exception as e: # Log error but don't necessarily halt execution if deletion fails - log.error(f"Failed to delete file ID {file_id}: {e}") + log.error(f'Failed to delete file ID {file_id}: {e}') - async def _delete_file_async( - self, session: aiohttp.ClientSession, file_id: str - ) -> None: + async def _delete_file_async(self, session: aiohttp.ClientSession, file_id: str) -> None: """Async file deletion with error tolerance.""" try: async def delete_request(): - self._debug_log(f"Deleting file ID: {file_id}") + self._debug_log(f'Deleting file ID: {file_id}') async with session.delete( - url=f"{self.base_url}/files/{file_id}", + url=f'{self.base_url}/files/{file_id}', headers=self.headers, - timeout=aiohttp.ClientTimeout( - total=self.cleanup_timeout - ), # Shorter timeout for cleanup + timeout=aiohttp.ClientTimeout(total=self.cleanup_timeout), + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: return await self._handle_response_async(response) await self._retry_request_async(delete_request) - self._debug_log(f"File {file_id} deleted successfully") + self._debug_log(f'File {file_id} deleted successfully') except Exception as e: # Don't fail the entire process if cleanup fails - log.warning(f"Failed to delete file ID {file_id}: {e}") + log.warning(f'Failed to delete file ID {file_id}: {e}') @asynccontextmanager async def _get_session(self): @@ -506,7 +474,7 @@ class MistralLoader: async with aiohttp.ClientSession( connector=connector, timeout=timeout, - headers={"User-Agent": "OpenWebUI-MistralLoader/2.0"}, + headers={'User-Agent': 'OpenWebUI-MistralLoader/2.0'}, raise_for_status=False, # We handle status codes manually trust_env=True, ) as session: @@ -514,13 +482,13 @@ class MistralLoader: def _process_results(self, ocr_response: Dict[str, Any]) -> List[Document]: """Process OCR results into Document objects with enhanced metadata and memory efficiency.""" - pages_data = ocr_response.get("pages") + pages_data = ocr_response.get('pages') if not pages_data: - log.warning("No pages found in OCR response.") + log.warning('No pages found in OCR response.') return [ Document( - page_content="No text content found", - metadata={"error": "no_pages", "file_name": self.file_name}, + page_content='No text content found', + metadata={'error': 'no_pages', 'file_name': self.file_name}, ) ] @@ -530,8 +498,8 @@ class MistralLoader: # Process pages in a memory-efficient way for page_data in pages_data: - page_content = page_data.get("markdown") - page_index = page_data.get("index") # API uses 0-based index + page_content = page_data.get('markdown') + page_index = page_data.get('index') # API uses 0-based index if page_content is None or page_index is None: skipped_pages += 1 @@ -548,7 +516,7 @@ class MistralLoader: if not cleaned_content: skipped_pages += 1 - self._debug_log(f"Skipping empty page {page_index}") + self._debug_log(f'Skipping empty page {page_index}') continue # Create document with optimized metadata @@ -556,34 +524,30 @@ class MistralLoader: Document( page_content=cleaned_content, metadata={ - "page": page_index, # 0-based index from API - "page_label": page_index + 1, # 1-based label for convenience - "total_pages": total_pages, - "file_name": self.file_name, - "file_size": self.file_size, - "processing_engine": "mistral-ocr", - "content_length": len(cleaned_content), + 'page': page_index, # 0-based index from API + 'page_label': page_index + 1, # 1-based label for convenience + 'total_pages': total_pages, + 'file_name': self.file_name, + 'file_size': self.file_size, + 'processing_engine': 'mistral-ocr', + 'content_length': len(cleaned_content), }, ) ) if skipped_pages > 0: - log.info( - f"Processed {len(documents)} pages, skipped {skipped_pages} empty/invalid pages" - ) + log.info(f'Processed {len(documents)} pages, skipped {skipped_pages} empty/invalid pages') if not documents: # Case where pages existed but none had valid markdown/index - log.warning( - "OCR response contained pages, but none had valid content/index." - ) + log.warning('OCR response contained pages, but none had valid content/index.') return [ Document( - page_content="No valid text content found in document", + page_content='No valid text content found in document', metadata={ - "error": "no_valid_pages", - "total_pages": total_pages, - "file_name": self.file_name, + 'error': 'no_valid_pages', + 'total_pages': total_pages, + 'file_name': self.file_name, }, ) ] @@ -615,24 +579,20 @@ class MistralLoader: documents = self._process_results(ocr_response) total_time = time.time() - start_time - log.info( - f"Sync OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents" - ) + log.info(f'Sync OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents') return documents except Exception as e: total_time = time.time() - start_time - log.error( - f"An error occurred during the loading process after {total_time:.2f}s: {e}" - ) + log.error(f'An error occurred during the loading process after {total_time:.2f}s: {e}') # Return an error document on failure return [ Document( - page_content=f"Error during processing: {e}", + page_content=f'Error during processing: {e}', metadata={ - "error": "processing_failed", - "file_name": self.file_name, + 'error': 'processing_failed', + 'file_name': self.file_name, }, ) ] @@ -643,9 +603,7 @@ class MistralLoader: self._delete_file(file_id) except Exception as del_e: # Log deletion error, but don't overwrite original error if one occurred - log.error( - f"Cleanup error: Could not delete file ID {file_id}. Reason: {del_e}" - ) + log.error(f'Cleanup error: Could not delete file ID {file_id}. Reason: {del_e}') async def load_async(self) -> List[Document]: """ @@ -672,21 +630,19 @@ class MistralLoader: documents = self._process_results(ocr_response) total_time = time.time() - start_time - log.info( - f"Async OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents" - ) + log.info(f'Async OCR workflow completed in {total_time:.2f}s, produced {len(documents)} documents') return documents except Exception as e: total_time = time.time() - start_time - log.error(f"Async OCR workflow failed after {total_time:.2f}s: {e}") + log.error(f'Async OCR workflow failed after {total_time:.2f}s: {e}') return [ Document( - page_content=f"Error during OCR processing: {e}", + page_content=f'Error during OCR processing: {e}', metadata={ - "error": "processing_failed", - "file_name": self.file_name, + 'error': 'processing_failed', + 'file_name': self.file_name, }, ) ] @@ -697,11 +653,11 @@ class MistralLoader: async with self._get_session() as session: await self._delete_file_async(session, file_id) except Exception as cleanup_error: - log.error(f"Cleanup failed for file ID {file_id}: {cleanup_error}") + log.error(f'Cleanup failed for file ID {file_id}: {cleanup_error}') @staticmethod async def load_multiple_async( - loaders: List["MistralLoader"], + loaders: List['MistralLoader'], max_concurrent: int = 5, # Limit concurrent requests ) -> List[List[Document]]: """ @@ -717,15 +673,13 @@ class MistralLoader: if not loaders: return [] - log.info( - f"Starting concurrent processing of {len(loaders)} files with max {max_concurrent} concurrent" - ) + log.info(f'Starting concurrent processing of {len(loaders)} files with max {max_concurrent} concurrent') start_time = time.time() # Use semaphore to control concurrency semaphore = asyncio.Semaphore(max_concurrent) - async def process_with_semaphore(loader: "MistralLoader") -> List[Document]: + async def process_with_semaphore(loader: 'MistralLoader') -> List[Document]: async with semaphore: return await loader.load_async() @@ -737,14 +691,14 @@ class MistralLoader: processed_results = [] for i, result in enumerate(results): if isinstance(result, Exception): - log.error(f"File {i} failed: {result}") + log.error(f'File {i} failed: {result}') processed_results.append( [ Document( - page_content=f"Error processing file: {result}", + page_content=f'Error processing file: {result}', metadata={ - "error": "batch_processing_failed", - "file_index": i, + 'error': 'batch_processing_failed', + 'file_index': i, }, ) ] @@ -755,15 +709,13 @@ class MistralLoader: # MONITORING: Log comprehensive batch processing statistics total_time = time.time() - start_time total_docs = sum(len(docs) for docs in processed_results) - success_count = sum( - 1 for result in results if not isinstance(result, Exception) - ) + success_count = sum(1 for result in results if not isinstance(result, Exception)) failure_count = len(results) - success_count log.info( - f"Batch processing completed in {total_time:.2f}s: " - f"{success_count} files succeeded, {failure_count} files failed, " - f"produced {total_docs} total documents" + f'Batch processing completed in {total_time:.2f}s: ' + f'{success_count} files succeeded, {failure_count} files failed, ' + f'produced {total_docs} total documents' ) return processed_results diff --git a/backend/open_webui/retrieval/loaders/paddleocr_vl.py b/backend/open_webui/retrieval/loaders/paddleocr_vl.py new file mode 100644 index 0000000000..40c185eab6 --- /dev/null +++ b/backend/open_webui/retrieval/loaders/paddleocr_vl.py @@ -0,0 +1,125 @@ +import base64 +import logging +import os +import sys +from typing import List + +import requests +from langchain_core.documents import Document +from open_webui.env import GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) + + +class PaddleOCRVLLoader: + """Loader that uses PaddleOCR-vl API to extract text from PDF/images.""" + + def __init__( + self, + api_url: str, + token: str, + file_path: str, + ): + if not api_url or not token: + raise ValueError('PaddleOCR-vl API URL and Token are required.') + if not os.path.exists(file_path): + raise FileNotFoundError(f'File not found at {file_path}') + + self.api_url = api_url.rstrip('/') + self.token = token + self.file_path = file_path + self.file_name = os.path.basename(file_path) + + def load(self) -> List[Document]: + log.info(f'Processing with PaddleOCR-vl: {self.file_path}') + + try: + with open(self.file_path, 'rb') as file: + file_bytes = file.read() + file_data = base64.b64encode(file_bytes).decode('ascii') + except Exception as e: + log.error(f'Failed to read file {self.file_path}: {e}') + raise + + headers = {'Authorization': f'token {self.token}', 'Content-Type': 'application/json'} + + # Detect fileType based on file extension + ext = self.file_path.lower().split('.')[-1] + image_extensions = ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'webp'] + file_type = 1 if ext in image_extensions else 0 + + payload = { + 'file': file_data, + 'fileType': file_type, + 'useDocOrientationClassify': False, + 'useDocUnwarping': False, + 'useChartRecognition': False, + } + + try: + response = requests.post(f'{self.api_url}/layout-parsing', json=payload, headers=headers) + response.raise_for_status() + + result = response.json().get('result', {}) + layout_results = result.get('layoutParsingResults', []) + + documents = [] + total_pages = len(layout_results) + skipped_pages = 0 + + for i, res in enumerate(layout_results): + markdown_text = res.get('markdown', {}).get('text', '') + + if isinstance(markdown_text, str): + cleaned_content = markdown_text.strip() + else: + cleaned_content = str(markdown_text).strip() + + if not cleaned_content: + skipped_pages += 1 + continue + + documents.append( + Document( + page_content=cleaned_content, + metadata={ + 'page': i, + 'page_label': i + 1, + 'total_pages': total_pages, + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, + ) + ) + + if skipped_pages > 0: + log.info(f'PaddleOCR-vl: Processed {len(documents)} pages, skipped {skipped_pages} empty pages.') + + if not documents: + log.warning('No valid text content found by PaddleOCR-vl.') + return [ + Document( + page_content='No valid text content found in document', + metadata={ + 'error': 'no_valid_pages', + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, + ) + ] + + return documents + + except Exception as e: + log.error(f'Error calling PaddleOCR-vl: {e}') + return [ + Document( + page_content=f'Error during OCR processing: {e}', + metadata={ + 'error': 'processing_failed', + 'file_name': self.file_name, + 'processing_engine': 'paddleocr-vl', + }, + ) + ] diff --git a/backend/open_webui/retrieval/loaders/tavily.py b/backend/open_webui/retrieval/loaders/tavily.py index f298de80b4..bdf70830e4 100644 --- a/backend/open_webui/retrieval/loaders/tavily.py +++ b/backend/open_webui/retrieval/loaders/tavily.py @@ -1,7 +1,7 @@ -import requests import logging from typing import Iterator, List, Literal, Union +import requests from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document @@ -25,7 +25,7 @@ class TavilyLoader(BaseLoader): self, urls: Union[str, List[str]], api_key: str, - extract_depth: Literal["basic", "advanced"] = "basic", + extract_depth: Literal['basic', 'advanced'] = 'basic', continue_on_failure: bool = True, ) -> None: """Initialize Tavily Extract client. @@ -42,13 +42,13 @@ class TavilyLoader(BaseLoader): continue_on_failure: Whether to continue if extraction of a URL fails. """ if not urls: - raise ValueError("At least one URL must be provided.") + raise ValueError('At least one URL must be provided.') self.api_key = api_key self.urls = urls if isinstance(urls, list) else [urls] self.extract_depth = extract_depth self.continue_on_failure = continue_on_failure - self.api_url = "https://api.tavily.com/extract" + self.api_url = 'https://api.tavily.com/extract' def lazy_load(self) -> Iterator[Document]: """Extract and yield documents from the URLs using Tavily Extract API.""" @@ -57,35 +57,35 @@ class TavilyLoader(BaseLoader): batch_urls = self.urls[i : i + batch_size] try: headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}", + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.api_key}', } # Use string for single URL, array for multiple URLs urls_param = batch_urls[0] if len(batch_urls) == 1 else batch_urls - payload = {"urls": urls_param, "extract_depth": self.extract_depth} + payload = {'urls': urls_param, 'extract_depth': self.extract_depth} # Make the API call response = requests.post(self.api_url, headers=headers, json=payload) response.raise_for_status() response_data = response.json() # Process successful results - for result in response_data.get("results", []): - url = result.get("url", "") - content = result.get("raw_content", "") + for result in response_data.get('results', []): + url = result.get('url', '') + content = result.get('raw_content', '') if not content: - log.warning(f"No content extracted from {url}") + log.warning(f'No content extracted from {url}') continue # Add URLs as metadata - metadata = {"source": url} + metadata = {'source': url} yield Document( page_content=content, metadata=metadata, ) - for failed in response_data.get("failed_results", []): - url = failed.get("url", "") - error = failed.get("error", "Unknown error") - log.error(f"Failed to extract content from {url}: {error}") + for failed in response_data.get('failed_results', []): + url = failed.get('url', '') + error = failed.get('error', 'Unknown error') + log.error(f'Failed to extract content from {url}: {error}') except Exception as e: if self.continue_on_failure: - log.error(f"Error extracting content from batch {batch_urls}: {e}") + log.error(f'Error extracting content from batch {batch_urls}: {e}') else: raise e diff --git a/backend/open_webui/retrieval/loaders/youtube.py b/backend/open_webui/retrieval/loaders/youtube.py index faf7b4452e..ad4c3524ac 100644 --- a/backend/open_webui/retrieval/loaders/youtube.py +++ b/backend/open_webui/retrieval/loaders/youtube.py @@ -1,20 +1,20 @@ import logging -from xml.etree.ElementTree import ParseError - from typing import Any, Dict, Generator, List, Optional, Sequence, Union from urllib.parse import parse_qs, urlparse +from xml.etree.ElementTree import ParseError + from langchain_core.documents import Document log = logging.getLogger(__name__) -ALLOWED_SCHEMES = {"http", "https"} +ALLOWED_SCHEMES = {'http', 'https'} ALLOWED_NETLOCS = { - "youtu.be", - "m.youtube.com", - "youtube.com", - "www.youtube.com", - "www.youtube-nocookie.com", - "vid.plus", + 'youtu.be', + 'm.youtube.com', + 'youtube.com', + 'www.youtube.com', + 'www.youtube-nocookie.com', + 'vid.plus', } @@ -30,17 +30,17 @@ def _parse_video_id(url: str) -> Optional[str]: path = parsed_url.path - if path.endswith("/watch"): + if path.endswith('/watch'): query = parsed_url.query parsed_query = parse_qs(query) - if "v" in parsed_query: - ids = parsed_query["v"] + if 'v' in parsed_query: + ids = parsed_query['v'] video_id = ids if isinstance(ids, str) else ids[0] else: return None else: - path = parsed_url.path.lstrip("/") - video_id = path.split("/")[-1] + path = parsed_url.path.lstrip('/') + video_id = path.split('/')[-1] if len(video_id) != 11: # Video IDs are 11 characters long return None @@ -54,13 +54,13 @@ class YoutubeLoader: def __init__( self, video_id: str, - language: Union[str, Sequence[str]] = "en", + language: Union[str, Sequence[str]] = 'en', proxy_url: Optional[str] = None, ): """Initialize with YouTube video ID.""" _video_id = _parse_video_id(video_id) self.video_id = _video_id if _video_id is not None else video_id - self._metadata = {"source": video_id} + self._metadata = {'source': video_id} self.proxy_url = proxy_url # Ensure language is a list @@ -70,8 +70,8 @@ class YoutubeLoader: self.language = list(language) # Add English as fallback if not already in the list - if "en" not in self.language: - self.language.append("en") + if 'en' not in self.language: + self.language.append('en') def load(self) -> List[Document]: """Load YouTube transcripts into `Document` objects.""" @@ -85,14 +85,12 @@ class YoutubeLoader: except ImportError: raise ImportError( 'Could not import "youtube_transcript_api" Python package. ' - "Please install it with `pip install youtube-transcript-api`." + 'Please install it with `pip install youtube-transcript-api`.' ) if self.proxy_url: - youtube_proxies = GenericProxyConfig( - http_url=self.proxy_url, https_url=self.proxy_url - ) - log.debug(f"Using proxy URL: {self.proxy_url[:14]}...") + youtube_proxies = GenericProxyConfig(http_url=self.proxy_url, https_url=self.proxy_url) + log.debug(f'Using proxy URL: {self.proxy_url[:14]}...') else: youtube_proxies = None @@ -100,7 +98,7 @@ class YoutubeLoader: try: transcript_list = transcript_api.list(self.video_id) except Exception as e: - log.exception("Loading YouTube transcript failed") + log.warning(f'Loading YouTube transcript failed: {e}') return [] # Try each language in order of priority @@ -110,14 +108,10 @@ class YoutubeLoader: if transcript.is_generated: log.debug(f"Found generated transcript for language '{lang}'") try: - transcript = transcript_list.find_manually_created_transcript( - [lang] - ) + transcript = transcript_list.find_manually_created_transcript([lang]) log.debug(f"Found manual transcript for language '{lang}'") except NoTranscriptFound: - log.debug( - f"No manual transcript found for language '{lang}', using generated" - ) + log.debug(f"No manual transcript found for language '{lang}', using generated") pass log.debug(f"Found transcript for language '{lang}'") @@ -131,12 +125,10 @@ class YoutubeLoader: log.debug(f"Empty transcript for language '{lang}'") continue - transcript_text = " ".join( + transcript_text = ' '.join( map( lambda transcript_piece: ( - transcript_piece.text.strip(" ") - if hasattr(transcript_piece, "text") - else "" + transcript_piece.text.strip(' ') if hasattr(transcript_piece, 'text') else '' ), transcript_pieces, ) @@ -150,9 +142,9 @@ class YoutubeLoader: raise e # If we get here, all languages failed - languages_tried = ", ".join(self.language) + languages_tried = ', '.join(self.language) log.warning( - f"No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed." + f'No transcript found for any of the specified languages: {languages_tried}. Verify if the video has transcripts, add more languages if needed.' ) raise NoTranscriptFound(self.video_id, self.language, list(transcript_list)) diff --git a/backend/open_webui/retrieval/models/base_reranker.py b/backend/open_webui/retrieval/models/base_reranker.py index 6be7a5649b..78002e087a 100644 --- a/backend/open_webui/retrieval/models/base_reranker.py +++ b/backend/open_webui/retrieval/models/base_reranker.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional, List, Tuple +from typing import List, Optional, Tuple class BaseReranker(ABC): diff --git a/backend/open_webui/retrieval/models/colbert.py b/backend/open_webui/retrieval/models/colbert.py index 2a8c0329d7..d11bde1b09 100644 --- a/backend/open_webui/retrieval/models/colbert.py +++ b/backend/open_webui/retrieval/models/colbert.py @@ -1,11 +1,10 @@ -import os import logging -import torch +import os + import numpy as np +import torch from colbert.infra import ColBERTConfig from colbert.modeling.checkpoint import Checkpoint - - from open_webui.retrieval.models.base_reranker import BaseReranker log = logging.getLogger(__name__) @@ -13,19 +12,17 @@ log = logging.getLogger(__name__) class ColBERT(BaseReranker): def __init__(self, name, **kwargs) -> None: - log.info("ColBERT: Loading model", name) - self.device = "cuda" if torch.cuda.is_available() else "cpu" + log.info('ColBERT: Loading model', name) + self.device = 'cuda' if torch.cuda.is_available() else 'cpu' - DOCKER = kwargs.get("env") == "docker" + DOCKER = kwargs.get('env') == 'docker' if DOCKER: # This is a workaround for the issue with the docker container # where the torch extension is not loaded properly # and the following error is thrown: # /root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory - lock_file = ( - "/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock" - ) + lock_file = '/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock' if os.path.exists(lock_file): os.remove(lock_file) @@ -36,23 +33,16 @@ class ColBERT(BaseReranker): pass def calculate_similarity_scores(self, query_embeddings, document_embeddings): - query_embeddings = query_embeddings.to(self.device) document_embeddings = document_embeddings.to(self.device) # Validate dimensions to ensure compatibility if query_embeddings.dim() != 3: - raise ValueError( - f"Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}." - ) + raise ValueError(f'Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}.') if document_embeddings.dim() != 3: - raise ValueError( - f"Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}." - ) + raise ValueError(f'Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}.') if query_embeddings.size(0) not in [1, document_embeddings.size(0)]: - raise ValueError( - "There should be either one query or queries equal to the number of documents." - ) + raise ValueError('There should be either one query or queries equal to the number of documents.') # Transpose the query embeddings to align for matrix multiplication transposed_query_embeddings = query_embeddings.permute(0, 2, 1) @@ -68,20 +58,17 @@ class ColBERT(BaseReranker): return normalized_scores.detach().cpu().numpy().astype(np.float32) - def predict(self, sentences): - + def predict(self, sentences, batch_size=32): query = sentences[0][0] docs = [i[1] for i in sentences] # Embedding the documents - embedded_docs = self.ckpt.docFromText(docs, bsize=32)[0] + embedded_docs = self.ckpt.docFromText(docs, bsize=batch_size)[0] # Embedding the queries - embedded_queries = self.ckpt.queryFromText([query], bsize=32) + embedded_queries = self.ckpt.queryFromText([query], bsize=batch_size) embedded_query = embedded_queries[0] # Calculate retrieval scores for the query against all documents - scores = self.calculate_similarity_scores( - embedded_query.unsqueeze(0), embedded_docs - ) + scores = self.calculate_similarity_scores(embedded_query.unsqueeze(0), embedded_docs) return scores diff --git a/backend/open_webui/retrieval/models/external.py b/backend/open_webui/retrieval/models/external.py index 095143d20d..6d2849eb88 100644 --- a/backend/open_webui/retrieval/models/external.py +++ b/backend/open_webui/retrieval/models/external.py @@ -1,14 +1,12 @@ import logging -import requests -from typing import Optional, List, Tuple +from typing import List, Optional, Tuple from urllib.parse import quote - +import requests from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, REQUESTS_VERIFY from open_webui.retrieval.models.base_reranker import BaseReranker from open_webui.utils.headers import include_user_info_headers - log = logging.getLogger(__name__) @@ -16,8 +14,8 @@ class ExternalReranker(BaseReranker): def __init__( self, api_key: str, - url: str = "http://localhost:8080/v1/rerank", - model: str = "reranker", + url: str = 'http://localhost:8080/v1/rerank', + model: str = 'reranker', timeout: Optional[int] = None, ): self.api_key = api_key @@ -25,33 +23,31 @@ class ExternalReranker(BaseReranker): self.model = model self.timeout = timeout - def predict( - self, sentences: List[Tuple[str, str]], user=None - ) -> Optional[List[float]]: + def predict(self, sentences: List[Tuple[str, str]], user=None) -> Optional[List[float]]: query = sentences[0][0] docs = [i[1] for i in sentences] payload = { - "model": self.model, - "query": query, - "documents": docs, - "top_n": len(docs), + 'model': self.model, + 'query': query, + 'documents': docs, + 'top_n': len(docs), } try: - log.info(f"ExternalReranker:predict:model {self.model}") - log.info(f"ExternalReranker:predict:query {query}") + log.info(f'ExternalReranker:predict:model {self.model}') + log.info(f'ExternalReranker:predict:query {query}') headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}", + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.api_key}', } if ENABLE_FORWARD_USER_INFO_HEADERS and user: headers = include_user_info_headers(headers, user) r = requests.post( - f"{self.url}", + f'{self.url}', headers=headers, json=payload, timeout=self.timeout, @@ -61,13 +57,13 @@ class ExternalReranker(BaseReranker): r.raise_for_status() data = r.json() - if "results" in data: - sorted_results = sorted(data["results"], key=lambda x: x["index"]) - return [result["relevance_score"] for result in sorted_results] + if 'results' in data: + sorted_results = sorted(data['results'], key=lambda x: x['index']) + return [result['relevance_score'] for result in sorted_results] else: - log.error("No results found in external reranking response") + log.error('No results found in external reranking response') return None except Exception as e: - log.exception(f"Error in external reranking: {e}") + log.exception(f'Error in external reranking: {e}') return None diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 56315c73fd..dc23fafb07 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -1,16 +1,17 @@ -import logging -import os -from typing import Awaitable, Optional, Union +from __future__ import annotations -import requests -import aiohttp import asyncio import hashlib -from concurrent.futures import ThreadPoolExecutor -import time +import logging +import os import re - +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Awaitable, Optional, Union from urllib.parse import quote + +import aiohttp +import requests from huggingface_hub import snapshot_download from langchain_classic.retrievers import ( ContextualCompressionRetriever, @@ -18,38 +19,35 @@ from langchain_classic.retrievers import ( ) from langchain_community.retrievers import BM25Retriever from langchain_core.documents import Document - -from open_webui.config import VECTOR_DB -from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT - - -from open_webui.models.users import UserModel -from open_webui.models.files import Files -from open_webui.models.knowledge import Knowledges - -from open_webui.models.chats import Chats -from open_webui.models.notes import Notes - -from open_webui.retrieval.vector.main import GetResult -from open_webui.utils.access_control import has_access -from open_webui.utils.headers import include_user_info_headers -from open_webui.utils.misc import get_message_list - -from open_webui.retrieval.web.utils import get_web_loader -from open_webui.retrieval.loaders.youtube import YoutubeLoader - - -from open_webui.env import ( - AIOHTTP_CLIENT_TIMEOUT, - OFFLINE_MODE, - ENABLE_FORWARD_USER_INFO_HEADERS, - AIOHTTP_CLIENT_SESSION_SSL, -) from open_webui.config import ( - RAG_EMBEDDING_QUERY_PREFIX, RAG_EMBEDDING_CONTENT_PREFIX, RAG_EMBEDDING_PREFIX_FIELD_NAME, + RAG_EMBEDDING_QUERY_PREFIX, + VECTOR_DB, ) +from open_webui.env import ( + AIOHTTP_CLIENT_ALLOW_REDIRECTS, + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + BYPASS_RETRIEVAL_ACCESS_CONTROL, + ENABLE_FORWARD_USER_INFO_HEADERS, + ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS, + OFFLINE_MODE, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.models.chats import Chats +from open_webui.models.files import Files +from open_webui.models.knowledge import Knowledges +from open_webui.models.notes import Notes +from open_webui.models.users import UserModel +from open_webui.retrieval.loaders.youtube import YoutubeLoader +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT +from open_webui.retrieval.vector.main import GetResult +from open_webui.retrieval.web.utils import get_web_loader +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.misc import get_message_list log = logging.getLogger(__name__) @@ -61,7 +59,7 @@ from langchain_core.retrievers import BaseRetriever def is_youtube_url(url: str) -> bool: - youtube_regex = r"^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+$" + youtube_regex = r'^(https?://)?(www\.)?(youtube\.com|youtu\.be)/.+$' return re.match(youtube_regex, url) is not None @@ -81,11 +79,150 @@ def get_loader(request, url: str): ) +def build_loader_from_config(request): + """Build a Loader instance with the admin's configured extraction engine settings.""" + from open_webui.retrieval.loaders.main import Loader + + config = request.app.state.config + return Loader( + engine=config.CONTENT_EXTRACTION_ENGINE, + DATALAB_MARKER_API_KEY=config.DATALAB_MARKER_API_KEY, + DATALAB_MARKER_API_BASE_URL=config.DATALAB_MARKER_API_BASE_URL, + DATALAB_MARKER_ADDITIONAL_CONFIG=config.DATALAB_MARKER_ADDITIONAL_CONFIG, + DATALAB_MARKER_SKIP_CACHE=config.DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_FORCE_OCR=config.DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_PAGINATE=config.DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_STRIP_EXISTING_OCR=config.DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION=config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_FORMAT_LINES=config.DATALAB_MARKER_FORMAT_LINES, + DATALAB_MARKER_USE_LLM=config.DATALAB_MARKER_USE_LLM, + DATALAB_MARKER_OUTPUT_FORMAT=config.DATALAB_MARKER_OUTPUT_FORMAT, + EXTERNAL_DOCUMENT_LOADER_URL=config.EXTERNAL_DOCUMENT_LOADER_URL, + EXTERNAL_DOCUMENT_LOADER_API_KEY=config.EXTERNAL_DOCUMENT_LOADER_API_KEY, + TIKA_SERVER_URL=config.TIKA_SERVER_URL, + DOCLING_SERVER_URL=config.DOCLING_SERVER_URL, + DOCLING_API_KEY=config.DOCLING_API_KEY, + DOCLING_PARAMS=config.DOCLING_PARAMS, + PDF_EXTRACT_IMAGES=config.PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE=config.PDF_LOADER_MODE, + DOCUMENT_INTELLIGENCE_ENDPOINT=config.DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY=config.DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL=config.DOCUMENT_INTELLIGENCE_MODEL, + MISTRAL_OCR_API_BASE_URL=config.MISTRAL_OCR_API_BASE_URL, + MISTRAL_OCR_API_KEY=config.MISTRAL_OCR_API_KEY, + PADDLEOCR_VL_BASE_URL=config.PADDLEOCR_VL_BASE_URL, + PADDLEOCR_VL_TOKEN=config.PADDLEOCR_VL_TOKEN, + MINERU_API_MODE=config.MINERU_API_MODE, + MINERU_API_URL=config.MINERU_API_URL, + MINERU_API_KEY=config.MINERU_API_KEY, + MINERU_API_TIMEOUT=config.MINERU_API_TIMEOUT, + MINERU_PARAMS=config.MINERU_PARAMS, + MINERU_FILE_EXTENSIONS=config.MINERU_FILE_EXTENSIONS, + ) + + +def _extract_text_from_binary_response(request, response: requests.Response, url: str) -> tuple[str, list]: + """Download response body to a temp file and extract text using the Loader pipeline.""" + import mimetypes + import tempfile + import urllib.parse + + content_type = response.headers.get('Content-Type', '').split(';')[0].strip() + + # Derive filename from URL path, falling back to Content-Disposition or mime guess + url_path = urllib.parse.urlparse(url).path + filename = os.path.basename(url_path) if url_path else '' + + if not filename or '.' not in filename: + # Try Content-Disposition header + cd = response.headers.get('Content-Disposition', '') + if 'filename=' in cd: + filename = cd.split('filename=')[-1].strip('"\'') + + if not filename or '.' not in filename: + ext = mimetypes.guess_extension(content_type) or '' + filename = f'download{ext}' + + suffix = '.' + filename.split('.')[-1].lower() if '.' in filename else '' + + with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: + tmp.write(response.content) + tmp_path = tmp.name + + try: + loader = build_loader_from_config(request) + docs = loader.load(filename, content_type, tmp_path) + for doc in docs: + doc.metadata['source'] = url + content = ' '.join([doc.page_content for doc in docs]) + return content, docs + finally: + os.remove(tmp_path) + + +def _is_text_content_type(content_type: str) -> bool: + """Return True if the content type should be handled by the web loader.""" + ct = content_type.split(';')[0].strip().lower() + if ct.startswith('text/'): + return True + if any(t in ct for t in ['xml', 'json', 'javascript']): + return True + return not ct # empty / missing → assume HTML + + def get_content_from_url(request, url: str) -> str: - loader = get_loader(request, url) - docs = loader.load() - content = " ".join([doc.page_content for doc in docs]) - return content, docs + from open_webui.retrieval.web.utils import validate_url + + # Validate URL before making any request (blocks private IPs, non-HTTP, filter list) + validate_url(url) + + # YouTube URLs (including youtu.be short links) should go straight to + # YoutubeLoader, which uses youtube-transcript-api and never needs the + # HTTP response body. Probing the URL first is harmful for short URLs: + # youtu.be returns a 303 redirect with Content-Type: application/binary + # when allow_redirects=False, causing the binary-content path to run + # and produce empty docs → HTTP 400. + if is_youtube_url(url): + loader = get_loader(request, url) + docs = loader.load() + content = ' '.join([doc.page_content for doc in docs]) + return content, docs + + # Streamed GET to check Content-Type without downloading the body. + # allow_redirects=False prevents redirect-based SSRF: validate_url() above is + # called on the originally-submitted URL only; following 3xx redirects without + # re-validation would let an attacker reach private IPs (RFC1918, loopback, + # cloud-metadata 169.254.169.254) via a public host that redirects internally. + try: + response = requests.get(url, stream=True, timeout=30, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS) + response.raise_for_status() + content_type = response.headers.get('Content-Type', '') + except Exception: + content_type = '' + response = None + + # Text / HTML / unknown — use the configured web loader + if response is None or _is_text_content_type(content_type): + if response is not None: + response.close() + loader = get_loader(request, url) + docs = loader.load() + content = ' '.join([doc.page_content for doc in docs]) + return content, docs + + # Binary content (PDF, DOCX, XLSX, PPTX, etc.) — download and extract + try: + return _extract_text_from_binary_response(request, response, url) + finally: + response.close() + + +CHUNK_HASH_KEY = '_chunk_hash' + + +def _content_hash(text: str) -> str: + """SHA-256 hash of text, used as a stable chunk identifier for RRF dedup.""" + return hashlib.sha256(text.encode()).hexdigest() class VectorSearchRetriever(BaseRetriever): @@ -93,9 +230,7 @@ class VectorSearchRetriever(BaseRetriever): embedding_function: Any top_k: int - def _get_relevant_documents( - self, query: str, *, run_manager: CallbackManagerForRetrieverRun - ) -> list[Document]: + def _get_relevant_documents(self, query: str, *, run_manager: CallbackManagerForRetrieverRun) -> list[Document]: """Get documents relevant to a query. Args: @@ -114,7 +249,7 @@ class VectorSearchRetriever(BaseRetriever): run_manager: CallbackManagerForRetrieverRun, ) -> list[Document]: embedding = await self.embedding_function(query, RAG_EMBEDDING_QUERY_PREFIX) - result = VECTOR_DB_CLIENT.search( + result = await ASYNC_VECTOR_DB_CLIENT.search( collection_name=self.collection_name, vectors=[embedding], limit=self.top_k, @@ -126,20 +261,20 @@ class VectorSearchRetriever(BaseRetriever): results = [] for idx in range(len(ids)): + metadata = metadatas[idx] + metadata[CHUNK_HASH_KEY] = _content_hash(documents[idx]) results.append( Document( - metadata=metadatas[idx], + metadata=metadata, page_content=documents[idx], ) ) return results -def query_doc( - collection_name: str, query_embedding: list[float], k: int, user: UserModel = None -): +def query_doc(collection_name: str, query_embedding: list[float], k: int, user: UserModel = None): try: - log.debug(f"query_doc:doc {collection_name}") + log.debug(f'query_doc:doc {collection_name}') result = VECTOR_DB_CLIENT.search( collection_name=collection_name, vectors=[query_embedding], @@ -147,25 +282,25 @@ def query_doc( ) if result: - log.info(f"query_doc:result {result.ids} {result.metadatas}") + log.info(f'query_doc:result {result.ids} {result.metadatas}') return result except Exception as e: - log.exception(f"Error querying doc {collection_name} with limit {k}: {e}") + log.exception(f'Error querying doc {collection_name} with limit {k}: {e}') raise e def get_doc(collection_name: str, user: UserModel = None): try: - log.debug(f"get_doc:doc {collection_name}") + log.debug(f'get_doc:doc {collection_name}') result = VECTOR_DB_CLIENT.get(collection_name=collection_name) if result: - log.info(f"query_doc:result {result.ids} {result.metadatas}") + log.info(f'query_doc:result {result.ids} {result.metadatas}') return result except Exception as e: - log.exception(f"Error getting doc {collection_name}: {e}") + log.exception(f'Error getting doc {collection_name}: {e}') raise e @@ -176,33 +311,29 @@ def get_enriched_texts(collection_result: GetResult) -> list[str]: metadata_parts = [text] # Add filename (repeat twice for extra weight in BM25 scoring) - if metadata.get("name"): - filename = metadata["name"] - filename_tokens = ( - filename.replace("_", " ").replace("-", " ").replace(".", " ") - ) - metadata_parts.append( - f"Filename: {filename} {filename_tokens} {filename_tokens}" - ) + if metadata.get('name'): + filename = metadata['name'] + filename_tokens = filename.replace('_', ' ').replace('-', ' ').replace('.', ' ') + metadata_parts.append(f'Filename: {filename} {filename_tokens} {filename_tokens}') # Add title if available - if metadata.get("title"): - metadata_parts.append(f"Title: {metadata['title']}") + if metadata.get('title'): + metadata_parts.append(f'Title: {metadata["title"]}') # Add document section headings if available (from markdown splitter) - if metadata.get("headings") and isinstance(metadata["headings"], list): - headings = " > ".join(str(h) for h in metadata["headings"]) - metadata_parts.append(f"Section: {headings}") + if metadata.get('headings') and isinstance(metadata['headings'], list): + headings = ' > '.join(str(h) for h in metadata['headings']) + metadata_parts.append(f'Section: {headings}') # Add source URL/path if available - if metadata.get("source"): - metadata_parts.append(f"Source: {metadata['source']}") + if metadata.get('source'): + metadata_parts.append(f'Source: {metadata["source"]}') # Add snippet for web search results - if metadata.get("snippet"): - metadata_parts.append(f"Snippet: {metadata['snippet']}") + if metadata.get('snippet'): + metadata_parts.append(f'Snippet: {metadata["snippet"]}') - enriched_texts.append(" ".join(metadata_parts)) + enriched_texts.append(' '.join(metadata_parts)) return enriched_texts @@ -223,11 +354,11 @@ async def query_doc_with_hybrid_search( # First check if collection_result has the required attributes if ( not collection_result - or not hasattr(collection_result, "documents") - or not hasattr(collection_result, "metadatas") + or not hasattr(collection_result, 'documents') + or not hasattr(collection_result, 'metadatas') ): - log.warning(f"query_doc_with_hybrid_search:no_docs {collection_name}") - return {"documents": [], "metadatas": [], "distances": []} + log.warning(f'query_doc_with_hybrid_search:no_docs {collection_name}') + return {'documents': [], 'metadatas': [], 'distances': []} # Now safely check the documents content after confirming attributes exist if ( @@ -235,20 +366,22 @@ async def query_doc_with_hybrid_search( or len(collection_result.documents) == 0 or not collection_result.documents[0] ): - log.warning(f"query_doc_with_hybrid_search:no_docs {collection_name}") - return {"documents": [], "metadatas": [], "distances": []} + log.warning(f'query_doc_with_hybrid_search:no_docs {collection_name}') + return {'documents': [], 'metadatas': [], 'distances': []} - log.debug(f"query_doc_with_hybrid_search:doc {collection_name}") + log.debug(f'query_doc_with_hybrid_search:doc {collection_name}') - bm25_texts = ( - get_enriched_texts(collection_result) - if enable_enriched_texts - else collection_result.documents[0] - ) + original_texts = collection_result.documents[0] + bm25_metadatas = [ + {**meta, CHUNK_HASH_KEY: _content_hash(original_texts[idx])} + for idx, meta in enumerate(collection_result.metadatas[0]) + ] + + bm25_texts = get_enriched_texts(collection_result) if enable_enriched_texts else original_texts bm25_retriever = BM25Retriever.from_texts( texts=bm25_texts, - metadatas=collection_result.metadatas[0], + metadatas=bm25_metadatas, ) bm25_retriever.k = k @@ -258,18 +391,24 @@ async def query_doc_with_hybrid_search( top_k=k, ) + # Use CHUNK_HASH_KEY for dedup so enriched BM25 texts don't defeat RRF if hybrid_bm25_weight <= 0: ensemble_retriever = EnsembleRetriever( - retrievers=[vector_search_retriever], weights=[1.0] + retrievers=[vector_search_retriever], + weights=[1.0], + id_key=CHUNK_HASH_KEY, ) elif hybrid_bm25_weight >= 1: ensemble_retriever = EnsembleRetriever( - retrievers=[bm25_retriever], weights=[1.0] + retrievers=[bm25_retriever], + weights=[1.0], + id_key=CHUNK_HASH_KEY, ) else: ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_search_retriever], weights=[hybrid_bm25_weight, 1.0 - hybrid_bm25_weight], + id_key=CHUNK_HASH_KEY, ) compressor = RerankCompressor( @@ -285,15 +424,13 @@ async def query_doc_with_hybrid_search( result = await compression_retriever.ainvoke(query) - distances = [d.metadata.get("score") for d in result] + distances = [d.metadata.get('score') for d in result] documents = [d.page_content for d in result] metadatas = [d.metadata for d in result] # retrieve only min(k, k_reranker) items, sort and cut by distance if k < k_reranker if k < k_reranker: - sorted_items = sorted( - zip(distances, metadatas, documents), key=lambda x: x[0], reverse=True - ) + sorted_items = sorted(zip(distances, documents, metadatas), key=lambda x: x[0], reverse=True) sorted_items = sorted_items[:k] if sorted_items: @@ -302,18 +439,15 @@ async def query_doc_with_hybrid_search( distances, documents, metadatas = [], [], [] result = { - "distances": [distances], - "documents": [documents], - "metadatas": [metadatas], + 'distances': [distances], + 'documents': [documents], + 'metadatas': [metadatas], } - log.info( - "query_doc_with_hybrid_search:result " - + f'{result["metadatas"]} {result["distances"]}' - ) + log.info('query_doc_with_hybrid_search:result ' + f'{result["metadatas"]} {result["distances"]}') return result except Exception as e: - log.exception(f"Error querying doc {collection_name} with hybrid search: {e}") + log.exception(f'Error querying doc {collection_name} with hybrid search: {e}') raise e @@ -324,15 +458,15 @@ def merge_get_results(get_results: list[dict]) -> dict: combined_ids = [] for data in get_results: - combined_documents.extend(data["documents"][0]) - combined_metadatas.extend(data["metadatas"][0]) - combined_ids.extend(data["ids"][0]) + combined_documents.extend(data['documents'][0]) + combined_metadatas.extend(data['metadatas'][0]) + combined_ids.extend(data['ids'][0]) # Create the output dictionary result = { - "documents": [combined_documents], - "metadatas": [combined_metadatas], - "ids": [combined_ids], + 'documents': [combined_documents], + 'metadatas': [combined_metadatas], + 'ids': [combined_ids], } return result @@ -344,21 +478,19 @@ def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict: for data in query_results: if ( - len(data.get("distances", [])) == 0 - or len(data.get("documents", [])) == 0 - or len(data.get("metadatas", [])) == 0 + len(data.get('distances', [])) == 0 + or len(data.get('documents', [])) == 0 + or len(data.get('metadatas', [])) == 0 ): continue - distances = data["distances"][0] - documents = data["documents"][0] - metadatas = data["metadatas"][0] + distances = data['distances'][0] + documents = data['documents'][0] + metadatas = data['metadatas'][0] for distance, document, metadata in zip(distances, documents, metadatas): if isinstance(document, str): - doc_hash = hashlib.sha256( - document.encode() - ).hexdigest() # Compute a hash for uniqueness + doc_hash = hashlib.sha256(document.encode()).hexdigest() # Compute a hash for uniqueness if doc_hash not in combined.keys(): combined[doc_hash] = (distance, document, metadata) @@ -373,15 +505,13 @@ def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict: combined.sort(key=lambda x: x[0], reverse=True) # Slice to keep only the top k elements - sorted_distances, sorted_documents, sorted_metadatas = ( - zip(*combined[:k]) if combined else ([], [], []) - ) + sorted_distances, sorted_documents, sorted_metadatas = zip(*combined[:k]) if combined else ([], [], []) # Create and return the output dictionary return { - "distances": [list(sorted_distances)], - "documents": [list(sorted_documents)], - "metadatas": [list(sorted_metadatas)], + 'distances': [list(sorted_distances)], + 'documents': [list(sorted_documents)], + 'metadatas': [list(sorted_metadatas)], } @@ -395,7 +525,7 @@ def get_all_items_from_collections(collection_names: list[str]) -> dict: if result is not None: results.append(result.model_dump()) except Exception as e: - log.exception(f"Error when querying the collection: {e}") + log.exception(f'Error when querying the collection: {e}') else: pass @@ -403,11 +533,34 @@ def get_all_items_from_collections(collection_names: list[str]) -> dict: async def query_collection( + request, collection_names: list[str], queries: list[str], embedding_function, k: int, ) -> dict: + # When request is provided, try hybrid search + reranking if enabled + if request and request.app.state.config.ENABLE_RAG_HYBRID_SEARCH: + try: + reranking_function = ( + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents)) + if request.app.state.RERANKING_FUNCTION + else None + ) + return await query_collection_with_hybrid_search( + collection_names=collection_names, + queries=queries, + embedding_function=embedding_function, + k=k, + reranking_function=reranking_function, + k_reranker=request.app.state.config.TOP_K_RERANKER, + r=request.app.state.config.RELEVANCE_THRESHOLD, + hybrid_bm25_weight=request.app.state.config.HYBRID_BM25_WEIGHT, + enable_enriched_texts=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + ) + except Exception as e: + log.debug(f'Hybrid search failed, falling back to vector search: {e}') + results = [] error = False @@ -423,24 +576,25 @@ async def query_collection( return result.model_dump(), None return None, None except Exception as e: - log.exception(f"Error when querying the collection: {e}") + log.exception(f'Error when querying the collection: {e}') return None, e + # Sanitize: filter out None/empty queries to prevent embedding crashes + # (e.g. when get_last_user_message returns None) + queries = [q for q in queries if q] + if not queries: + log.warning('query_collection: all queries were None or empty, returning empty results') + return {'distances': [[]], 'documents': [[]], 'metadatas': [[]]} + # Generate all query embeddings (in one call) - query_embeddings = await embedding_function( - queries, prefix=RAG_EMBEDDING_QUERY_PREFIX - ) - log.debug( - f"query_collection: processing {len(queries)} queries across {len(collection_names)} collections" - ) + query_embeddings = await embedding_function(queries, prefix=RAG_EMBEDDING_QUERY_PREFIX) + log.debug(f'query_collection: processing {len(queries)} queries across {len(collection_names)} collections') with ThreadPoolExecutor() as executor: future_results = [] for query_embedding in query_embeddings: for collection_name in collection_names: - result = executor.submit( - process_query_collection, collection_name, query_embedding - ) + result = executor.submit(process_query_collection, collection_name, query_embedding) future_results.append(result) task_results = [future.result() for future in future_results] @@ -451,7 +605,7 @@ async def query_collection( results.append(result) if error and not results: - log.warning("All collection queries failed. No results returned.") + log.warning('All collection queries failed. No results returned.') return merge_and_sort_query_results(results, k=k) @@ -469,25 +623,27 @@ async def query_collection_with_hybrid_search( ) -> dict: results = [] error = False - # Fetch collection data once per collection sequentially - # Avoid fetching the same data multiple times later - collection_results = {} - for collection_name in collection_names: - try: - log.debug( - f"query_collection_with_hybrid_search:VECTOR_DB_CLIENT.get:collection {collection_name}" - ) - collection_results[collection_name] = VECTOR_DB_CLIENT.get( - collection_name=collection_name - ) - except Exception as e: - log.exception(f"Failed to fetch collection {collection_name}: {e}") - collection_results[collection_name] = None - - log.info( - f"Starting hybrid search for {len(queries)} queries in {len(collection_names)} collections..." + # Fetch every collection's contents once up front so the + # per-query/per-document loop below can reuse them. Each fetch + # offloads to a worker thread, so run them concurrently with + # `asyncio.gather` instead of awaiting them serially — otherwise + # latency scales linearly with `len(collection_names)`. + log.debug( + 'query_collection_with_hybrid_search: prefetching %d collections', + len(collection_names), ) + async def _fetch_collection(name: str): + try: + return name, await ASYNC_VECTOR_DB_CLIENT.get(collection_name=name) + except Exception as e: + log.exception(f'Failed to fetch collection {name}: {e}') + return name, None + + collection_results = dict(await asyncio.gather(*(_fetch_collection(name) for name in collection_names))) + + log.info(f'Starting hybrid search for {len(queries)} queries in {len(collection_names)} collections...') + async def process_query(collection_name, query): try: result = await query_doc_with_hybrid_search( @@ -504,7 +660,7 @@ async def query_collection_with_hybrid_search( ) return result, None except Exception as e: - log.exception(f"Error when querying the collection with hybrid_search: {e}") + log.exception(f'Error when querying the collection with hybrid_search: {e}') return None, e # Prepare tasks for all collections and queries @@ -517,9 +673,7 @@ async def query_collection_with_hybrid_search( ] # Run all queries in parallel using asyncio.gather - task_results = await asyncio.gather( - *[process_query(collection_name, query) for collection_name, query in tasks] - ) + task_results = await asyncio.gather(*[process_query(collection_name, query) for collection_name, query in tasks]) for result, err in task_results: if err is not None: @@ -528,9 +682,7 @@ async def query_collection_with_hybrid_search( results.append(result) if error and not results: - raise Exception( - "Hybrid search failed for all collections. Using Non-hybrid search as fallback." - ) + raise Exception('Hybrid search failed for all collections. Using Non-hybrid search as fallback.') return merge_and_sort_query_results(results, k=k) @@ -538,252 +690,229 @@ async def query_collection_with_hybrid_search( def generate_openai_batch_embeddings( model: str, texts: list[str], - url: str = "https://api.openai.com/v1", - key: str = "", + url: str = 'https://api.openai.com/v1', + key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug( - f"generate_openai_batch_embeddings:model {model} batch size: {len(texts)}" - ) - json_data = {"input": texts, "model": model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'generate_openai_batch_embeddings:model {model} batch size: {len(texts)}') + json_data = {'input': texts, 'model': model} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {key}", - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - r = requests.post( - f"{url}/embeddings", - headers=headers, - json=json_data, - ) - r.raise_for_status() - data = r.json() - if "data" in data: - return [elem["embedding"] for elem in data["data"]] - else: - raise "Something went wrong :/" - except Exception as e: - log.exception(f"Error generating openai batch embeddings: {e}") - return None + r = requests.post( + f'{url}/embeddings', + headers=headers, + json=json_data, + ) + r.raise_for_status() + data = r.json() + if 'data' in data: + return [elem['embedding'] for elem in data['data']] + else: + raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") async def agenerate_openai_batch_embeddings( model: str, texts: list[str], - url: str = "https://api.openai.com/v1", - key: str = "", + url: str = 'https://api.openai.com/v1', + key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug( - f"agenerate_openai_batch_embeddings:model {model} batch size: {len(texts)}" - ) - form_data = {"input": texts, "model": model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'agenerate_openai_batch_embeddings:model {model} batch size: {len(texts)}') + form_data = {'input': texts, 'model': model} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {key}", - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) as session: - async with session.post( - f"{url}/embeddings", headers=headers, json=form_data - ) as r: - r.raise_for_status() - data = await r.json() - if "data" in data: - return [item["embedding"] for item in data["data"]] - else: - raise Exception("Something went wrong :/") - except Exception as e: - log.exception(f"Error generating openai batch embeddings: {e}") - return None + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + f'{url}/embeddings', + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + data = await r.json() + if 'data' in data: + return [item['embedding'] for item in data['data']] + else: + raise ValueError("Unexpected OpenAI embeddings response: missing 'data' key") def generate_azure_openai_batch_embeddings( model: str, texts: list[str], url: str, - key: str = "", - version: str = "", + key: str = '', + version: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug( - f"generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}" +) -> list[list[float]]: + log.debug(f'generate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') + json_data = {'input': texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix + + url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' + + for _ in range(5): + headers = { + 'Content-Type': 'application/json', + 'api-key': key, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) + + r = requests.post( + url, + headers=headers, + json=json_data, ) - json_data = {"input": texts} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - - url = f"{url}/openai/deployments/{model}/embeddings?api-version={version}" - - for _ in range(5): - headers = { - "Content-Type": "application/json", - "api-key": key, - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.post( - url, - headers=headers, - json=json_data, - ) - if r.status_code == 429: - retry = float(r.headers.get("Retry-After", "1")) - time.sleep(retry) - continue - r.raise_for_status() - data = r.json() - if "data" in data: - return [elem["embedding"] for elem in data["data"]] - else: - raise Exception("Something went wrong :/") - return None - except Exception as e: - log.exception(f"Error generating azure openai batch embeddings: {e}") - return None + if r.status_code == 429: + retry = float(r.headers.get('Retry-After', '1')) + time.sleep(retry) + continue + r.raise_for_status() + data = r.json() + if 'data' in data: + return [elem['embedding'] for elem in data['data']] + else: + raise ValueError("Unexpected Azure OpenAI embeddings response: missing 'data' key") + raise Exception('Azure OpenAI embedding request failed: max retries (429) exceeded') async def agenerate_azure_openai_batch_embeddings( model: str, texts: list[str], url: str, - key: str = "", - version: str = "", + key: str = '', + version: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug( - f"agenerate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}" - ) - form_data = {"input": texts} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'agenerate_azure_openai_batch_embeddings:deployment {model} batch size: {len(texts)}') + form_data = {'input': texts} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - full_url = f"{url}/openai/deployments/{model}/embeddings?api-version={version}" + full_url = f'{url}/openai/deployments/{model}/embeddings?api-version={version}' - headers = { - "Content-Type": "application/json", - "api-key": key, - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'api-key': key, + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) as session: - async with session.post(full_url, headers=headers, json=form_data) as r: - r.raise_for_status() - data = await r.json() - if "data" in data: - return [item["embedding"] for item in data["data"]] - else: - raise Exception("Something went wrong :/") - except Exception as e: - log.exception(f"Error generating azure openai batch embeddings: {e}") - return None + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + full_url, + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + data = await r.json() + if 'data' in data: + return [item['embedding'] for item in data['data']] + else: + raise ValueError("Unexpected Azure OpenAI embeddings response: missing 'data' key") def generate_ollama_batch_embeddings( model: str, texts: list[str], url: str, - key: str = "", + key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug( - f"generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}" - ) - json_data = {"input": texts, "model": model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'generate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') + json_data = {'input': texts, 'model': model, 'truncate': True} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + json_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {key}", - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - r = requests.post( - f"{url}/api/embed", - headers=headers, - json=json_data, - ) - r.raise_for_status() - data = r.json() + r = requests.post( + f'{url}/api/embed', + headers=headers, + json=json_data, + ) + if r.status_code != 200: + error_detail = r.json().get('error', r.text) + raise Exception(f'Ollama embed error ({r.status_code}): {error_detail}') + data = r.json() - if "embeddings" in data: - return data["embeddings"] - else: - raise "Something went wrong :/" - except Exception as e: - log.exception(f"Error generating ollama batch embeddings: {e}") - return None + if 'embeddings' in data: + return data['embeddings'] + else: + raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") async def agenerate_ollama_batch_embeddings( model: str, texts: list[str], url: str, - key: str = "", + key: str = '', prefix: str = None, user: UserModel = None, -) -> Optional[list[list[float]]]: - try: - log.debug( - f"agenerate_ollama_batch_embeddings:model {model} batch size: {len(texts)}" - ) - form_data = {"input": texts, "model": model} - if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): - form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix +) -> list[list[float]]: + log.debug(f'agenerate_ollama_batch_embeddings:model {model} batch size: {len(texts)}') + form_data = {'input': texts, 'model': model, 'truncate': True} + if isinstance(RAG_EMBEDDING_PREFIX_FIELD_NAME, str) and isinstance(prefix, str): + form_data[RAG_EMBEDDING_PREFIX_FIELD_NAME] = prefix - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {key}", - } - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {key}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) as session: - async with session.post( - f"{url}/api/embed", - headers=headers, - json=form_data, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - r.raise_for_status() - data = await r.json() - if "embeddings" in data: - return data["embeddings"] - else: - raise Exception("Something went wrong :/") - except Exception as e: - log.exception(f"Error generating ollama batch embeddings: {e}") - return None + async with aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) as session: + async with session.post( + f'{url}/api/embed', + headers=headers, + json=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + if r.status != 200: + error_data = await r.json() + error_detail = error_data.get('error', str(error_data)) + raise Exception(f'Ollama embed error ({r.status}): {error_detail}') + data = await r.json() + if 'embeddings' in data: + return data['embeddings'] + else: + raise ValueError("Unexpected Ollama embeddings response: missing 'embeddings' key") def get_embedding_function( @@ -795,8 +924,15 @@ def get_embedding_function( embedding_batch_size, azure_api_version=None, enable_async=True, + concurrent_requests=0, ) -> Awaitable: - if embedding_engine == "": + if embedding_engine == '': + if embedding_function is None: + raise ValueError( + 'No embedding model is loaded. Set RAG_EMBEDDING_MODEL to a valid ' + 'SentenceTransformer model name, or configure an external ' + 'RAG_EMBEDDING_ENGINE (ollama, openai, azure_openai).' + ) # Sentence transformers: CPU-bound sync operation async def async_embedding_function(query, prefix=None, user=None): return await asyncio.to_thread( @@ -804,7 +940,7 @@ def get_embedding_function( lambda query, prefix=None: embedding_function.encode( query, batch_size=int(embedding_batch_size), - **({"prompt": prefix} if prefix else {}), + **({'prompt': prefix} if prefix else {}), ).tolist() ), query, @@ -812,7 +948,7 @@ def get_embedding_function( ) return async_embedding_function - elif embedding_engine in ["ollama", "openai", "azure_openai"]: + elif embedding_engine in ['ollama', 'openai', 'azure_openai']: embedding_function = lambda query, prefix=None, user=None: generate_embeddings( engine=embedding_engine, model=embedding_model, @@ -827,39 +963,38 @@ def get_embedding_function( async def async_embedding_function(query, prefix=None, user=None): if isinstance(query, list): # Create batches - batches = [ - query[i : i + embedding_batch_size] - for i in range(0, len(query), embedding_batch_size) - ] + batches = [query[i : i + embedding_batch_size] for i in range(0, len(query), embedding_batch_size)] if enable_async: - log.debug( - f"generate_multiple_async: Processing {len(batches)} batches in parallel" - ) - # Execute all batches in parallel - tasks = [ - embedding_function(batch, prefix=prefix, user=user) - for batch in batches - ] + log.debug(f'generate_multiple_async: Processing {len(batches)} batches in parallel') + # Use semaphore to limit concurrent embedding API requests + # 0 = unlimited (no semaphore) + if concurrent_requests: + semaphore = asyncio.Semaphore(concurrent_requests) + + async def generate_batch_with_semaphore(batch): + async with semaphore: + return await embedding_function(batch, prefix=prefix, user=user) + + tasks = [generate_batch_with_semaphore(batch) for batch in batches] + else: + tasks = [embedding_function(batch, prefix=prefix, user=user) for batch in batches] batch_results = await asyncio.gather(*tasks) else: - log.debug( - f"generate_multiple_async: Processing {len(batches)} batches sequentially" - ) + log.debug(f'generate_multiple_async: Processing {len(batches)} batches sequentially') batch_results = [] for batch in batches: - batch_results.append( - await embedding_function(batch, prefix=prefix, user=user) - ) + batch_results.append(await embedding_function(batch, prefix=prefix, user=user)) - # Flatten results + # Flatten results — raise if any batch failed embeddings = [] - for batch_embeddings in batch_results: - if isinstance(batch_embeddings, list): - embeddings.extend(batch_embeddings) + for i, batch_embeddings in enumerate(batch_results): + if batch_embeddings is None: + raise Exception(f'Embedding generation failed for batch {i + 1}/{len(batches)}') + embeddings.extend(batch_embeddings) log.debug( - f"generate_multiple_async: Generated {len(embeddings)} embeddings from {len(batches)} parallel batches" + f'generate_multiple_async: Generated {len(embeddings)} embeddings from {len(batches)} parallel batches' ) return embeddings else: @@ -867,7 +1002,7 @@ def get_embedding_function( return async_embedding_function else: - raise ValueError(f"Unknown embedding engine: {embedding_engine}") + raise ValueError(f'Unknown embedding engine: {embedding_engine}') async def generate_embeddings( @@ -877,35 +1012,39 @@ async def generate_embeddings( prefix: Union[str, None] = None, **kwargs, ): - url = kwargs.get("url", "") - key = kwargs.get("key", "") - user = kwargs.get("user") + url = kwargs.get('url', '') + key = kwargs.get('key', '') + user = kwargs.get('user') if prefix is not None and RAG_EMBEDDING_PREFIX_FIELD_NAME is None: if isinstance(text, list): - text = [f"{prefix}{text_element}" for text_element in text] + text = [f'{prefix}{text_element}' for text_element in text] else: - text = f"{prefix}{text}" + text = f'{prefix}{text}' - if engine == "ollama": + if engine == 'ollama': embeddings = await agenerate_ollama_batch_embeddings( **{ - "model": model, - "texts": text if isinstance(text, list) else [text], - "url": url, - "key": key, - "prefix": prefix, - "user": user, + 'model': model, + 'texts': text if isinstance(text, list) else [text], + 'url': url, + 'key': key, + 'prefix': prefix, + 'user': user, } ) + if embeddings is None: + return None return embeddings[0] if isinstance(text, str) else embeddings - elif engine == "openai": + elif engine == 'openai': embeddings = await agenerate_openai_batch_embeddings( model, text if isinstance(text, list) else [text], url, key, prefix, user ) + if embeddings is None: + return None return embeddings[0] if isinstance(text, str) else embeddings - elif engine == "azure_openai": - azure_api_version = kwargs.get("azure_api_version", "") + elif engine == 'azure_openai': + azure_api_version = kwargs.get('azure_api_version', '') embeddings = await agenerate_azure_openai_batch_embeddings( model, text if isinstance(text, list) else [text], @@ -915,22 +1054,101 @@ async def generate_embeddings( prefix, user, ) + if embeddings is None: + return None return embeddings[0] if isinstance(text, str) else embeddings -def get_reranking_function(reranking_engine, reranking_model, reranking_function): +def get_reranking_function(reranking_engine, reranking_model, reranking_function, reranking_batch_size=32): if reranking_function is None: return None - if reranking_engine == "external": + if reranking_engine == 'external': return lambda query, documents, user=None: reranking_function.predict( [(query, doc.page_content) for doc in documents], user=user ) else: return lambda query, documents, user=None: reranking_function.predict( - [(query, doc.page_content) for doc in documents] + [(query, doc.page_content) for doc in documents], batch_size=int(reranking_batch_size) ) +# UUIDs, SHA-256 digests, and prefixed variants thereof all fit [A-Za-z0-9_-]. +# Anything else cannot be a real Open WebUI collection and could break out of +# a Milvus expression literal. +_SAFE_COLLECTION_NAME_RE = re.compile(r'^[A-Za-z0-9_-]{1,255}$') + + +def _is_safe_collection_name(name: str) -> bool: + return isinstance(name, str) and bool(_SAFE_COLLECTION_NAME_RE.match(name)) + + +async def filter_accessible_collections( + collection_names: set[str], + user: UserModel, + access_type: str = 'read', +) -> set[str]: + """ + Return only the collection names the user is allowed to access. + Admins bypass all checks. For non-admins the policy is: + + - any name with characters outside [A-Za-z0-9_-] → rejected + - file-* → validated via has_access_to_file + - user-memory-* → must match user's own memory collection + - web-search-* → ephemeral per-query collections, always allowed + - knowledge-bases → always denied (system meta-collection) + - everything else → if the name matches a knowledge base, validated + via Knowledges.check_access_by_user_id; if no + such KB exists, denied by default. When + ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS is True, + the name is treated as a legacy/ephemeral + collection and allowed. + """ + # Applied before the admin bypass — malformed names should never reach the vector store. + safe_names = {n for n in collection_names if _is_safe_collection_name(n)} + rejected = collection_names - safe_names + if rejected: + log.warning( + 'filter_accessible_collections: rejected %d collection name(s) ' + 'with unsafe characters (user_id=%s)', + len(rejected), + getattr(user, 'id', ''), + ) + + if user.role == 'admin': + return safe_names + + validated = set() + for name in safe_names: + if name == 'knowledge-bases': + # System meta-collection — never exposed to non-admins. + continue + elif name.startswith('file-'): + file_id = name[len('file-') :] + if await has_access_to_file(file_id=file_id, access_type=access_type, user=user): + validated.add(name) + elif name.startswith('user-memory-'): + if name == f'user-memory-{user.id}': + validated.add(name) + elif name.startswith('web-search-'): + # Ephemeral collections created by process_web_search — safe + # to allow because they contain only transient web-search + # results scoped to the requesting user's session. + validated.add(name) + else: + # May be a knowledge-base ID or a legacy/ephemeral collection. + # If it IS a KB, enforce access control. If no such KB + # exists, the behaviour depends on + # ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS: + # False (default) — deny (closes the unscoped namespace) + # True — allow (preserves legacy behaviour) + if await Knowledges.check_access_by_user_id(name, user.id, permission=access_type): + validated.add(name) + elif ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS and not await Knowledges.get_knowledge_by_id(name): + # Not a KB at all — legacy/ephemeral collection, allow + validated.add(name) + return validated + + async def get_sources_from_items( request, items, @@ -943,11 +1161,9 @@ async def get_sources_from_items( hybrid_bm25_weight, hybrid_search, full_context=False, - user: Optional[UserModel] = None, + user: UserModel | None = None, ): - log.debug( - f"items: {items} {queries} {embedding_function} {reranking_function} {full_context}" - ) + log.debug(f'items: {items} {queries} {embedding_function} {reranking_function} {full_context}') extracted_collections = [] query_results = [] @@ -956,251 +1172,279 @@ async def get_sources_from_items( query_result = None collection_names = [] - if item.get("type") == "text": + if item.get('type') == 'text': # Raw Text # Used during temporary chat file uploads or web page & youtube attachements - if item.get("context") == "full": - if item.get("file"): + if item.get('context') == 'full': + if item.get('file'): # if item has file data, use it query_result = { - "documents": [ - [item.get("file", {}).get("data", {}).get("content")] - ], - "metadatas": [[item.get("file", {}).get("meta", {})]], + 'documents': [[item.get('file', {}).get('data', {}).get('content')]], + 'metadatas': [[item.get('file', {}).get('meta', {})]], } if query_result is None: # Fallback - if item.get("collection_name"): + if item.get('collection_name'): # If item has a collection name, use it - collection_names.append(item.get("collection_name")) - elif item.get("file"): + collection_names.append(item.get('collection_name')) + elif item.get('file'): # If item has file data, use it query_result = { - "documents": [ - [item.get("file", {}).get("data", {}).get("content")] - ], - "metadatas": [[item.get("file", {}).get("meta", {})]], + 'documents': [[item.get('file', {}).get('data', {}).get('content')]], + 'metadatas': [[item.get('file', {}).get('meta', {})]], } else: # Fallback to item content query_result = { - "documents": [[item.get("content")]], - "metadatas": [ - [{"file_id": item.get("id"), "name": item.get("name")}] - ], + 'documents': [[item.get('content')]], + 'metadatas': [[{'file_id': item.get('id'), 'name': item.get('name')}]], } - elif item.get("type") == "note": + elif item.get('type') == 'note': # Note Attached - note = Notes.get_note_by_id(item.get("id")) + note = await Notes.get_note_by_id(item.get('id')) if note and ( - user.role == "admin" + user.role == 'admin' or note.user_id == user.id - or has_access(user.id, "read", note.access_control) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + ) ): # User has access to the note query_result = { - "documents": [[note.data.get("content", {}).get("md", "")]], - "metadatas": [[{"file_id": note.id, "name": note.title}]], + 'documents': [[note.data.get('content', {}).get('md', '')]], + 'metadatas': [[{'file_id': note.id, 'name': note.title}]], } - elif item.get("type") == "chat": + elif item.get('type') == 'chat': # Chat Attached - chat = Chats.get_chat_by_id(item.get("id")) + chat = await Chats.get_chat_by_id(item.get('id')) - if chat and (user.role == "admin" or chat.user_id == user.id): - messages_map = chat.chat.get("history", {}).get("messages", {}) - message_id = chat.chat.get("history", {}).get("currentId") + if chat and (user.role == 'admin' or chat.user_id == user.id): + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') if messages_map and message_id: # Reconstruct the message list in order message_list = get_message_list(messages_map, message_id) - message_history = "\n".join( - [ - f"#### {m.get('role', 'user').capitalize()}\n{m.get('content')}\n" - for m in message_list - ] + message_history = '\n'.join( + [f'#### {m.get("role", "user").capitalize()}\n{m.get("content")}\n' for m in message_list] ) # User has access to the chat query_result = { - "documents": [[message_history]], - "metadatas": [[{"file_id": chat.id, "name": chat.title}]], + 'documents': [[message_history]], + 'metadatas': [[{'file_id': chat.id, 'name': chat.title}]], } - elif item.get("type") == "url": - content, docs = get_content_from_url(request, item.get("url")) + elif item.get('type') == 'url': + content, docs = get_content_from_url(request, item.get('url')) if docs: query_result = { - "documents": [[content]], - "metadatas": [[{"url": item.get("url"), "name": item.get("url")}]], + 'documents': [[content]], + 'metadatas': [[{'url': item.get('url'), 'name': item.get('url')}]], } - elif item.get("type") == "file": - if ( - item.get("context") == "full" - or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL - ): - if item.get("file", {}).get("data", {}).get("content", ""): + elif item.get('type') == 'file': + if item.get('context') == 'full' or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + if item.get('file', {}).get('data', {}).get('content', ''): # Manual Full Mode Toggle # Used from chat file modal, we can assume that the file content will be available from item.get("file").get("data", {}).get("content") query_result = { - "documents": [ - [item.get("file", {}).get("data", {}).get("content", "")] - ], - "metadatas": [ + 'documents': [[item.get('file', {}).get('data', {}).get('content', '')]], + 'metadatas': [ [ { - "file_id": item.get("id"), - "name": item.get("name"), - **item.get("file") - .get("data", {}) - .get("metadata", {}), + 'file_id': item.get('id'), + 'name': item.get('name'), + **item.get('file').get('data', {}).get('metadata', {}), } ] ], } - elif item.get("id"): - file_object = Files.get_file_by_id(item.get("id")) - if file_object: + elif item.get('id'): + file_object = await Files.get_file_by_id(item.get('id')) + if file_object and ( + user.role == 'admin' + or file_object.user_id == user.id + or await has_access_to_file(item.get('id'), 'read', user) + ): query_result = { - "documents": [[file_object.data.get("content", "")]], - "metadatas": [ + 'documents': [[file_object.data.get('content', '')]], + 'metadatas': [ [ { - "file_id": item.get("id"), - "name": file_object.filename, - "source": file_object.filename, + 'file_id': item.get('id'), + 'name': file_object.filename, + 'source': file_object.filename, } ] ], } else: - # Fallback to collection names - if item.get("legacy"): - collection_names.append(f"{item['id']}") - else: - collection_names.append(f"file-{item['id']}") + # Chunked-retrieval fallback — verify read access before + # exposing the file's vector collection (same posture as the + # full-context branch above). + file_id = item.get('id') + if file_id: + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + if item.get('legacy'): + collection_names.append(f'{file_id}') + else: + collection_names.append(f'file-{file_id}') + else: + file_object = await Files.get_file_by_id(file_id) + if file_object and ( + user.role == 'admin' + or file_object.user_id == user.id + or await has_access_to_file(file_id, 'read', user) + ): + if item.get('legacy'): + collection_names.append(f'{file_id}') + else: + collection_names.append(f'file-{file_id}') - elif item.get("type") == "collection": + elif item.get('type') == 'collection': # Manual Full Mode Toggle for Collection - knowledge_base = Knowledges.get_knowledge_by_id(item.get("id")) + knowledge_base = await Knowledges.get_knowledge_by_id(item.get('id')) if knowledge_base and ( - user.role == "admin" + user.role == 'admin' or knowledge_base.user_id == user.id - or has_access(user.id, "read", knowledge_base.access_control) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission='read', + ) ): - if ( - item.get("context") == "full" - or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL - ): + if item.get('context') == 'full' or request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: if knowledge_base and ( - user.role == "admin" + user.role == 'admin' or knowledge_base.user_id == user.id - or has_access(user.id, "read", knowledge_base.access_control) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge_base.id, + permission='read', + ) ): - files = Knowledges.get_files_by_id(knowledge_base.id) + files = await Knowledges.get_files_by_id(knowledge_base.id) documents = [] metadatas = [] for file in files: - documents.append(file.data.get("content", "")) + documents.append(file.data.get('content', '')) metadatas.append( { - "file_id": file.id, - "name": file.filename, - "source": file.filename, + 'file_id': file.id, + 'name': file.filename, + 'source': file.filename, } ) query_result = { - "documents": [documents], - "metadatas": [metadatas], + 'documents': [documents], + 'metadatas': [metadatas], } else: - # Fallback to collection names - if item.get("legacy"): - collection_names = item.get("collection_names", []) + if item.get('legacy'): + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + collection_names = item.get('collection_names', []) + else: + # Legacy KB: item.collection_names is client-supplied. + # Validate against the KB's actual files to prevent + # cross-tenant collection name substitution. + files = await Knowledges.get_files_by_id(knowledge_base.id) + owned_names = {f'file-{f.id}' for f in files} + owned_names.add(knowledge_base.id) + valid_names = [ + n for n in (item.get('collection_names') or []) + if n in owned_names + ] + collection_names = valid_names if valid_names else [knowledge_base.id] else: - collection_names.append(item["id"]) + collection_names.append(item['id']) - elif item.get("docs"): + elif item.get('docs'): # BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL query_result = { - "documents": [[doc.get("content") for doc in item.get("docs")]], - "metadatas": [[doc.get("metadata") for doc in item.get("docs")]], + 'documents': [[doc.get('content') for doc in item.get('docs')]], + 'metadatas': [[doc.get('metadata') for doc in item.get('docs')]], } - elif item.get("collection_name"): - # Direct Collection Name - collection_names.append(item["collection_name"]) - elif item.get("collection_names"): - # Collection Names List - collection_names.extend(item["collection_names"]) + elif item.get('collection_name'): + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + collection_names.append(item['collection_name']) + else: + log.debug( + "get_sources_from_items: ignoring untrusted direct " + "collection_name '%s' on item without type", + item.get('collection_name'), + ) + elif item.get('collection_names'): + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + collection_names.extend(item['collection_names']) + else: + log.debug( + "get_sources_from_items: ignoring untrusted direct " + "collection_names on item without type", + ) # If query_result is None # Fallback to collection names and vector search the collections if query_result is None and collection_names: collection_names = set(collection_names).difference(extracted_collections) if not collection_names: - log.debug(f"skipping {item} as it has already been extracted") + log.debug(f'skipping {item} as it has already been extracted') continue + # Filter out collections the user cannot read + if user: + collection_names = await filter_accessible_collections(collection_names, user) + if not collection_names: + log.debug(f'access denied for all collections in item {item}') + continue + try: if full_context: - query_result = get_all_items_from_collections(collection_names) + # Sync helper makes blocking VECTOR_DB_CLIENT calls; + # offload so the async caller's event loop stays free. + query_result = await asyncio.to_thread(get_all_items_from_collections, collection_names) else: - query_result = None # Initialize to None - if hybrid_search: - try: - query_result = await query_collection_with_hybrid_search( - collection_names=collection_names, - queries=queries, - embedding_function=embedding_function, - k=k, - reranking_function=reranking_function, - k_reranker=k_reranker, - r=r, - hybrid_bm25_weight=hybrid_bm25_weight, - enable_enriched_texts=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, - ) - except Exception as e: - log.debug( - "Error when using hybrid search, using non hybrid search as fallback." - ) - - # fallback to non-hybrid search - if not hybrid_search and query_result is None: - query_result = await query_collection( - collection_names=collection_names, - queries=queries, - embedding_function=embedding_function, - k=k, - ) + query_result = await query_collection( + request, + collection_names=collection_names, + queries=queries, + embedding_function=embedding_function, + k=k, + ) except Exception as e: log.exception(e) extracted_collections.extend(collection_names) if query_result: - if "data" in item: - del item["data"] - query_results.append({**query_result, "file": item}) + if 'data' in item: + del item['data'] + query_results.append({**query_result, 'file': item}) sources = [] for query_result in query_results: try: - if "documents" in query_result: - if "metadatas" in query_result: + if 'documents' in query_result: + if 'metadatas' in query_result: source = { - "source": query_result["file"], - "document": query_result["documents"][0], - "metadata": query_result["metadatas"][0], + 'source': query_result['file'], + 'document': query_result['documents'][0], + 'metadata': query_result['metadatas'][0], } - if "distances" in query_result and query_result["distances"]: - source["distances"] = query_result["distances"][0] + if 'distances' in query_result and query_result['distances']: + source['distances'] = query_result['distances'][0] sources.append(source) except Exception as e: @@ -1210,7 +1454,7 @@ async def get_sources_from_items( def get_model_path(model: str, update_model: bool = False): # Construct huggingface_hub kwargs with local_files_only to return the snapshot path - cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME") + cache_dir = os.getenv('SENTENCE_TRANSFORMERS_HOME') local_files_only = not update_model @@ -1218,34 +1462,32 @@ def get_model_path(model: str, update_model: bool = False): local_files_only = True snapshot_kwargs = { - "cache_dir": cache_dir, - "local_files_only": local_files_only, + 'cache_dir': cache_dir, + 'local_files_only': local_files_only, } - log.debug(f"model: {model}") - log.debug(f"snapshot_kwargs: {snapshot_kwargs}") + log.debug(f'model: {model}') + log.debug(f'snapshot_kwargs: {snapshot_kwargs}') # Inspiration from upstream sentence_transformers - if ( - os.path.exists(model) - or ("\\" in model or model.count("/") > 1) - and local_files_only - ): + if os.path.exists(model) or ('\\' in model or model.count('/') > 1) and local_files_only: # If fully qualified path exists, return input, else set repo_id return model - elif "/" not in model: + elif '/' not in model: # Set valid repo_id for model short-name - model = "sentence-transformers" + "/" + model + model = 'sentence-transformers' + '/' + model - snapshot_kwargs["repo_id"] = model + snapshot_kwargs['repo_id'] = model # Attempt to query the huggingface_hub library to determine the local path and/or to update try: model_repo_path = snapshot_download(**snapshot_kwargs) - log.debug(f"model_repo_path: {model_repo_path}") + log.debug(f'model_repo_path: {model_repo_path}') return model_repo_path except Exception as e: - log.exception(f"Cannot determine model snapshot path: {e}") + log.exception(f'Cannot determine model snapshot path: {e}') + if OFFLINE_MODE: + raise return model @@ -1263,14 +1505,14 @@ class RerankCompressor(BaseDocumentCompressor): r_score: float class Config: - extra = "forbid" + extra = 'forbid' arbitrary_types_allowed = True def compress_documents( self, documents: Sequence[Document], query: str, - callbacks: Optional[Callbacks] = None, + callbacks: Callbacks | None = None, ) -> Sequence[Document]: """Compress retrieved documents given the query context. @@ -1289,7 +1531,7 @@ class RerankCompressor(BaseDocumentCompressor): self, documents: Sequence[Document], query: str, - callbacks: Optional[Callbacks] = None, + callbacks: Callbacks | None = None, ) -> Sequence[Document]: reranking = self.reranking_function is not None @@ -1297,15 +1539,12 @@ class RerankCompressor(BaseDocumentCompressor): if reranking: scores = await asyncio.to_thread(self.reranking_function, query, documents) else: - from sentence_transformers import util + from sentence_transformers import util as st_util - query_embedding = await self.embedding_function( - query, RAG_EMBEDDING_QUERY_PREFIX - ) - document_embedding = await self.embedding_function( - [doc.page_content for doc in documents], RAG_EMBEDDING_CONTENT_PREFIX - ) - scores = util.cos_sim(query_embedding, document_embedding)[0] + query_embedding = await self.embedding_function(query, RAG_EMBEDDING_QUERY_PREFIX) + doc_texts = [doc.page_content for doc in documents] + document_embedding = await self.embedding_function(doc_texts, RAG_EMBEDDING_CONTENT_PREFIX) + scores = st_util.cos_sim(query_embedding, document_embedding)[0] if scores is not None: docs_with_scores = list( @@ -1315,15 +1554,13 @@ class RerankCompressor(BaseDocumentCompressor): ) ) if self.r_score: - docs_with_scores = [ - (d, s) for d, s in docs_with_scores if s >= self.r_score - ] + docs_with_scores = [(d, s) for d, s in docs_with_scores if s >= self.r_score] result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True) final_results = [] for doc, doc_score in result[: self.top_n]: metadata = doc.metadata - metadata["score"] = doc_score + metadata['score'] = doc_score doc = Document( page_content=doc.page_content, metadata=metadata, @@ -1331,7 +1568,5 @@ class RerankCompressor(BaseDocumentCompressor): final_results.append(doc) return final_results else: - log.warning( - "No valid scores found, check your reranking function. Returning original documents." - ) + log.warning('No valid scores found, check your reranking function. Returning original documents.') return documents diff --git a/backend/open_webui/retrieval/vector/async_client.py b/backend/open_webui/retrieval/vector/async_client.py new file mode 100644 index 0000000000..0bea6696a9 --- /dev/null +++ b/backend/open_webui/retrieval/vector/async_client.py @@ -0,0 +1,129 @@ +""" +Async facade over the synchronous VECTOR_DB_CLIENT. + +The vector DB backends bundled with Open WebUI (Chroma, pgvector, Qdrant, +Milvus, OpenSearch, Pinecone, Weaviate, …) all expose a uniformly +synchronous API. Each method performs blocking network or disk I/O — and +some, like `insert`/`upsert`, can run for several seconds. + +When such a sync method is awaited from an async route handler, it blocks +the event loop for its entire duration, freezing every other in-flight +HTTP request, websocket message and background task. + +This module wraps the sync client in an `AsyncVectorDBClient` that +transparently dispatches each call to a worker thread via +`asyncio.to_thread`. Async callers can `await ASYNC_VECTOR_DB_CLIENT.x(...)` +in place of `VECTOR_DB_CLIENT.x(...)` and the loop stays responsive. + +The original `VECTOR_DB_CLIENT` is unchanged, so callers already running +inside `run_in_threadpool` (e.g. `save_docs_to_vector_db`) are not +affected. + +Thread-safety expectations +-------------------------- +Every async caller now invokes `VECTOR_DB_CLIENT` from a worker thread +rather than the event-loop thread, and many can run concurrently. The +sync client (and its underlying backend driver) is therefore expected +to be safe for concurrent use across threads, which is the standard +contract for the bundled drivers (chroma, pgvector via SQLAlchemy +pool, qdrant-client, opensearch-py, …). This is *not* a new exposure +introduced by this facade — `save_docs_to_vector_db` already called +the sync client from `run_in_threadpool`, so concurrent threaded +access has always been a requirement of the codebase. Adding a global +serialization lock here would defeat the responsiveness this facade +exists to provide; any backend that genuinely cannot tolerate +concurrent access should grow its own internal serialization. + +API surface +----------- +Method signatures mirror `VectorDBBase` exactly. This is deliberate: +permissive `*args/**kwargs` forwarding hides typos at the call site +(an earlier revision of this file shipped that, and a `metadata=` +typo silently broke an entire endpoint until explicit signatures +surfaced it). Callers that need a backend-specific parameter not on +`VectorDBBase` should reach for the `.sync` escape hatch and wrap +their own `asyncio.to_thread`, e.g. :: + + await asyncio.to_thread( + ASYNC_VECTOR_DB_CLIENT.sync.some_backend_specific_op, + collection_name, special_kwarg=value, + ) +""" + +from __future__ import annotations + +import asyncio +from typing import Dict, List, Optional, Union + +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) + + +class AsyncVectorDBClient: + """Awaitable mirror of `VectorDBBase` that off-loads each call to a thread. + + Method signatures mirror `VectorDBBase` exactly so static analysis + catches bad kwargs at the call site instead of letting them surface + deep inside the worker thread (where the resulting ``TypeError`` is + typically swallowed by surrounding ``try/except``). + """ + + def __init__(self, sync_client: VectorDBBase) -> None: + self._sync = sync_client + + @property + def sync(self) -> VectorDBBase: + """Escape hatch for code that must call the sync client directly + (e.g. already inside a worker thread).""" + return self._sync + + async def has_collection(self, collection_name: str) -> bool: + return await asyncio.to_thread(self._sync.has_collection, collection_name) + + async def delete_collection(self, collection_name: str) -> None: + return await asyncio.to_thread(self._sync.delete_collection, collection_name) + + async def insert(self, collection_name: str, items: List[VectorItem]) -> None: + return await asyncio.to_thread(self._sync.insert, collection_name, items) + + async def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + return await asyncio.to_thread(self._sync.upsert, collection_name, items) + + async def search( + self, + collection_name: str, + vectors: List[List[Union[float, int]]], + filter: Optional[Dict] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + return await asyncio.to_thread(self._sync.search, collection_name, vectors, filter, limit) + + async def query( + self, + collection_name: str, + filter: Dict, + limit: Optional[int] = None, + ) -> Optional[GetResult]: + return await asyncio.to_thread(self._sync.query, collection_name, filter, limit) + + async def get(self, collection_name: str) -> Optional[GetResult]: + return await asyncio.to_thread(self._sync.get, collection_name) + + async def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict] = None, + ) -> None: + return await asyncio.to_thread(self._sync.delete, collection_name, ids, filter) + + async def reset(self) -> None: + return await asyncio.to_thread(self._sync.reset) + + +ASYNC_VECTOR_DB_CLIENT = AsyncVectorDBClient(VECTOR_DB_CLIENT) diff --git a/backend/open_webui/retrieval/vector/dbs/chroma.py b/backend/open_webui/retrieval/vector/dbs/chroma.py index b7ea5244b4..cd0a59eeaf 100755 --- a/backend/open_webui/retrieval/vector/dbs/chroma.py +++ b/backend/open_webui/retrieval/vector/dbs/chroma.py @@ -1,29 +1,27 @@ -import chromadb import logging -from chromadb import Settings -from chromadb.utils.batch_utils import create_batches - from typing import Optional -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) -from open_webui.retrieval.vector.utils import process_metadata - +import chromadb +from chromadb import Settings +from chromadb.utils.batch_utils import create_batches from open_webui.config import ( + CHROMA_CLIENT_AUTH_CREDENTIALS, + CHROMA_CLIENT_AUTH_PROVIDER, CHROMA_DATA_PATH, + CHROMA_DATABASE, + CHROMA_HTTP_HEADERS, CHROMA_HTTP_HOST, CHROMA_HTTP_PORT, - CHROMA_HTTP_HEADERS, CHROMA_HTTP_SSL, CHROMA_TENANT, - CHROMA_DATABASE, - CHROMA_CLIENT_AUTH_PROVIDER, - CHROMA_CLIENT_AUTH_CREDENTIALS, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata log = logging.getLogger(__name__) @@ -31,17 +29,15 @@ log = logging.getLogger(__name__) class ChromaClient(VectorDBBase): def __init__(self): settings_dict = { - "allow_reset": True, - "anonymized_telemetry": False, + 'allow_reset': True, + 'anonymized_telemetry': False, } if CHROMA_CLIENT_AUTH_PROVIDER is not None: - settings_dict["chroma_client_auth_provider"] = CHROMA_CLIENT_AUTH_PROVIDER + settings_dict['chroma_client_auth_provider'] = CHROMA_CLIENT_AUTH_PROVIDER if CHROMA_CLIENT_AUTH_CREDENTIALS is not None: - settings_dict["chroma_client_auth_credentials"] = ( - CHROMA_CLIENT_AUTH_CREDENTIALS - ) + settings_dict['chroma_client_auth_credentials'] = CHROMA_CLIENT_AUTH_CREDENTIALS - if CHROMA_HTTP_HOST != "": + if CHROMA_HTTP_HOST != '': self.client = chromadb.HttpClient( host=CHROMA_HTTP_HOST, port=CHROMA_HTTP_PORT, @@ -87,25 +83,23 @@ class ChromaClient(VectorDBBase): # chromadb has cosine distance, 2 (worst) -> 0 (best). Re-odering to 0 -> 1 # https://docs.trychroma.com/docs/collections/configure cosine equation - distances: list = result["distances"][0] + distances: list = result['distances'][0] distances = [2 - dist for dist in distances] distances = [[dist / 2 for dist in distances]] return SearchResult( **{ - "ids": result["ids"], - "distances": distances, - "documents": result["documents"], - "metadatas": result["metadatas"], + 'ids': result['ids'], + 'distances': distances, + 'documents': result['documents'], + 'metadatas': result['metadatas'], } ) return None except Exception as e: return None - def query( - self, collection_name: str, filter: dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None) -> Optional[GetResult]: # Query the items from the collection based on the filter. try: collection = self.client.get_collection(name=collection_name) @@ -117,13 +111,13 @@ class ChromaClient(VectorDBBase): return GetResult( **{ - "ids": [result["ids"]], - "documents": [result["documents"]], - "metadatas": [result["metadatas"]], + 'ids': [result['ids']], + 'documents': [result['documents']], + 'metadatas': [result['metadatas']], } ) return None - except: + except Exception: return None def get(self, collection_name: str) -> Optional[GetResult]: @@ -133,23 +127,21 @@ class ChromaClient(VectorDBBase): result = collection.get() return GetResult( **{ - "ids": [result["ids"]], - "documents": [result["documents"]], - "metadatas": [result["metadatas"]], + 'ids': [result['ids']], + 'documents': [result['documents']], + 'metadatas': [result['metadatas']], } ) return None def insert(self, collection_name: str, items: list[VectorItem]): # Insert the items into the collection, if the collection does not exist, it will be created. - collection = self.client.get_or_create_collection( - name=collection_name, metadata={"hnsw:space": "cosine"} - ) + collection = self.client.get_or_create_collection(name=collection_name, metadata={'hnsw:space': 'cosine'}) - ids = [item["id"] for item in items] - documents = [item["text"] for item in items] - embeddings = [item["vector"] for item in items] - metadatas = [process_metadata(item["metadata"]) for item in items] + ids = [item['id'] for item in items] + documents = [item['text'] for item in items] + embeddings = [item['vector'] for item in items] + metadatas = [process_metadata(item['metadata']) for item in items] for batch in create_batches( api=self.client, @@ -162,18 +154,14 @@ class ChromaClient(VectorDBBase): def upsert(self, collection_name: str, items: list[VectorItem]): # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created. - collection = self.client.get_or_create_collection( - name=collection_name, metadata={"hnsw:space": "cosine"} - ) + collection = self.client.get_or_create_collection(name=collection_name, metadata={'hnsw:space': 'cosine'}) - ids = [item["id"] for item in items] - documents = [item["text"] for item in items] - embeddings = [item["vector"] for item in items] - metadatas = [process_metadata(item["metadata"]) for item in items] + ids = [item['id'] for item in items] + documents = [item['text'] for item in items] + embeddings = [item['vector'] for item in items] + metadatas = [process_metadata(item['metadata']) for item in items] - collection.upsert( - ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas - ) + collection.upsert(ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas) def delete( self, @@ -191,9 +179,7 @@ class ChromaClient(VectorDBBase): collection.delete(where=filter) except Exception as e: # If collection doesn't exist, that's fine - nothing to delete - log.debug( - f"Attempted to delete from non-existent collection {collection_name}. Ignoring." - ) + log.debug(f'Attempted to delete from non-existent collection {collection_name}. Ignoring.') pass def reset(self): diff --git a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py index e209453f5c..da3e6e93c8 100644 --- a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py +++ b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py @@ -1,25 +1,29 @@ -from elasticsearch import Elasticsearch, BadRequestError -from typing import Optional -import ssl -from elasticsearch.helpers import bulk, scan +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" -from open_webui.retrieval.vector.utils import process_metadata -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) +import ssl +from typing import Optional + +from elasticsearch import BadRequestError, Elasticsearch +from elasticsearch.helpers import bulk, scan from open_webui.config import ( - ELASTICSEARCH_URL, - ELASTICSEARCH_CA_CERTS, ELASTICSEARCH_API_KEY, - ELASTICSEARCH_USERNAME, - ELASTICSEARCH_PASSWORD, + ELASTICSEARCH_CA_CERTS, ELASTICSEARCH_CLOUD_ID, ELASTICSEARCH_INDEX_PREFIX, + ELASTICSEARCH_PASSWORD, + ELASTICSEARCH_URL, + ELASTICSEARCH_USERNAME, SSL_ASSERT_FINGERPRINT, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata class ElasticsearchClient(VectorDBBase): @@ -47,7 +51,7 @@ class ElasticsearchClient(VectorDBBase): # Status: works def _get_index_name(self, dimension: int) -> str: - return f"{self.index_prefix}_d{str(dimension)}" + return f'{self.index_prefix}_d{str(dimension)}' # Status: works def _scan_result_to_get_result(self, result) -> GetResult: @@ -58,24 +62,24 @@ class ElasticsearchClient(VectorDBBase): metadatas = [] for hit in result: - ids.append(hit["_id"]) - documents.append(hit["_source"].get("text")) - metadatas.append(hit["_source"].get("metadata")) + ids.append(hit['_id']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) # Status: works def _result_to_get_result(self, result) -> GetResult: - if not result["hits"]["hits"]: + if not result['hits']['hits']: return None ids = [] documents = [] metadatas = [] - for hit in result["hits"]["hits"]: - ids.append(hit["_id"]) - documents.append(hit["_source"].get("text")) - metadatas.append(hit["_source"].get("metadata")) + for hit in result['hits']['hits']: + ids.append(hit['_id']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) @@ -86,11 +90,11 @@ class ElasticsearchClient(VectorDBBase): documents = [] metadatas = [] - for hit in result["hits"]["hits"]: - ids.append(hit["_id"]) - distances.append(hit["_score"]) - documents.append(hit["_source"].get("text")) - metadatas.append(hit["_source"].get("metadata")) + for hit in result['hits']['hits']: + ids.append(hit['_id']) + distances.append(hit['_score']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) return SearchResult( ids=[ids], @@ -102,26 +106,26 @@ class ElasticsearchClient(VectorDBBase): # Status: works def _create_index(self, dimension: int): body = { - "mappings": { - "dynamic_templates": [ + 'mappings': { + 'dynamic_templates': [ { - "strings": { - "match_mapping_type": "string", - "mapping": {"type": "keyword"}, + 'strings': { + 'match_mapping_type': 'string', + 'mapping': {'type': 'keyword'}, } } ], - "properties": { - "collection": {"type": "keyword"}, - "id": {"type": "keyword"}, - "vector": { - "type": "dense_vector", - "dims": dimension, # Adjust based on your vector dimensions - "index": True, - "similarity": "cosine", + 'properties': { + 'collection': {'type': 'keyword'}, + 'id': {'type': 'keyword'}, + 'vector': { + 'type': 'dense_vector', + 'dims': dimension, # Adjust based on your vector dimensions + 'index': True, + 'similarity': 'cosine', }, - "text": {"type": "text"}, - "metadata": {"type": "object"}, + 'text': {'type': 'text'}, + 'metadata': {'type': 'object'}, }, } } @@ -135,21 +139,19 @@ class ElasticsearchClient(VectorDBBase): # Status: works def has_collection(self, collection_name) -> bool: - query_body = {"query": {"bool": {"filter": []}}} - query_body["query"]["bool"]["filter"].append( - {"term": {"collection": collection_name}} - ) + query_body = {'query': {'bool': {'filter': []}}} + query_body['query']['bool']['filter'].append({'term': {'collection': collection_name}}) try: - result = self.client.count(index=f"{self.index_prefix}*", body=query_body) + result = self.client.count(index=f'{self.index_prefix}*', body=query_body) - return result.body["count"] > 0 + return result.body['count'] > 0 except Exception as e: return None def delete_collection(self, collection_name: str): - query = {"query": {"term": {"collection": collection_name}}} - self.client.delete_by_query(index=f"{self.index_prefix}*", body=query) + query = {'query': {'term': {'collection': collection_name}}} + self.client.delete_by_query(index=f'{self.index_prefix}*', body=query) # Status: works def search( @@ -160,51 +162,41 @@ class ElasticsearchClient(VectorDBBase): limit: int = 10, ) -> Optional[SearchResult]: query = { - "size": limit, - "_source": ["text", "metadata"], - "query": { - "script_score": { - "query": { - "bool": {"filter": [{"term": {"collection": collection_name}}]} - }, - "script": { - "source": "cosineSimilarity(params.vector, 'vector') + 1.0", - "params": { - "vector": vectors[0] - }, # Assuming single query vector + 'size': limit, + '_source': ['text', 'metadata'], + 'query': { + 'script_score': { + 'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}}, + 'script': { + 'source': "cosineSimilarity(params.vector, 'vector') + 1.0", + 'params': {'vector': vectors[0]}, # Assuming single query vector }, } }, } - result = self.client.search( - index=self._get_index_name(len(vectors[0])), body=query - ) + result = self.client.search(index=self._get_index_name(len(vectors[0])), body=query) return self._result_to_search_result(result) # Status: only tested halfwat - def query( - self, collection_name: str, filter: dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None) -> Optional[GetResult]: if not self.has_collection(collection_name): return None query_body = { - "query": {"bool": {"filter": []}}, - "_source": ["text", "metadata"], + 'query': {'bool': {'filter': []}}, + '_source': ['text', 'metadata'], } for field, value in filter.items(): - query_body["query"]["bool"]["filter"].append({"term": {field: value}}) - query_body["query"]["bool"]["filter"].append( - {"term": {"collection": collection_name}} - ) + query_body['query']['bool']['filter'].append({'term': {field: value}}) + query_body['query']['bool']['filter'].append({'term': {'collection': collection_name}}) size = limit if limit else 10 try: result = self.client.search( - index=f"{self.index_prefix}*", + index=f'{self.index_prefix}*', body=query_body, size=size, ) @@ -216,9 +208,7 @@ class ElasticsearchClient(VectorDBBase): # Status: works def _has_index(self, dimension: int): - return self.client.indices.exists( - index=self._get_index_name(dimension=dimension) - ) + return self.client.indices.exists(index=self._get_index_name(dimension=dimension)) def get_or_create_index(self, dimension: int): if not self._has_index(dimension=dimension): @@ -228,28 +218,28 @@ class ElasticsearchClient(VectorDBBase): def get(self, collection_name: str) -> Optional[GetResult]: # Get all the items in the collection. query = { - "query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}}, - "_source": ["text", "metadata"], + 'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}}, + '_source': ['text', 'metadata'], } - results = list(scan(self.client, index=f"{self.index_prefix}*", query=query)) + results = list(scan(self.client, index=f'{self.index_prefix}*', query=query)) return self._scan_result_to_get_result(results) # Status: works def insert(self, collection_name: str, items: list[VectorItem]): - if not self._has_index(dimension=len(items[0]["vector"])): - self._create_index(dimension=len(items[0]["vector"])) + if not self._has_index(dimension=len(items[0]['vector'])): + self._create_index(dimension=len(items[0]['vector'])) for batch in self._create_batches(items): actions = [ { - "_index": self._get_index_name(dimension=len(items[0]["vector"])), - "_id": item["id"], - "_source": { - "collection": collection_name, - "vector": item["vector"], - "text": item["text"], - "metadata": process_metadata(item["metadata"]), + '_index': self._get_index_name(dimension=len(items[0]['vector'])), + '_id': item['id'], + '_source': { + 'collection': collection_name, + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), }, } for item in batch @@ -258,21 +248,21 @@ class ElasticsearchClient(VectorDBBase): # Upsert documents using the update API with doc_as_upsert=True. def upsert(self, collection_name: str, items: list[VectorItem]): - if not self._has_index(dimension=len(items[0]["vector"])): - self._create_index(dimension=len(items[0]["vector"])) + if not self._has_index(dimension=len(items[0]['vector'])): + self._create_index(dimension=len(items[0]['vector'])) for batch in self._create_batches(items): actions = [ { - "_op_type": "update", - "_index": self._get_index_name(dimension=len(item["vector"])), - "_id": item["id"], - "doc": { - "collection": collection_name, - "vector": item["vector"], - "text": item["text"], - "metadata": process_metadata(item["metadata"]), + '_op_type': 'update', + '_index': self._get_index_name(dimension=len(item['vector'])), + '_id': item['id'], + 'doc': { + 'collection': collection_name, + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), }, - "doc_as_upsert": True, + 'doc_as_upsert': True, } for item in batch ] @@ -285,22 +275,17 @@ class ElasticsearchClient(VectorDBBase): ids: Optional[list[str]] = None, filter: Optional[dict] = None, ): - - query = { - "query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}} - } + query = {'query': {'bool': {'filter': [{'term': {'collection': collection_name}}]}}} # logic based on chromaDB if ids: - query["query"]["bool"]["filter"].append({"terms": {"_id": ids}}) + query['query']['bool']['filter'].append({'terms': {'_id': ids}}) elif filter: for field, value in filter.items(): - query["query"]["bool"]["filter"].append( - {"term": {f"metadata.{field}": value}} - ) + query['query']['bool']['filter'].append({'term': {f'metadata.{field}': value}}) - self.client.delete_by_query(index=f"{self.index_prefix}*", body=query) + self.client.delete_by_query(index=f'{self.index_prefix}*', body=query) def reset(self): - indices = self.client.indices.get(index=f"{self.index_prefix}*") + indices = self.client.indices.get(index=f'{self.index_prefix}*') for index in indices: self.client.indices.delete(index=index) diff --git a/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py b/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py new file mode 100644 index 0000000000..a8cf62f7b1 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py @@ -0,0 +1,582 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +from __future__ import annotations + +import array +import json +import logging +import math +import re +import sys +from contextlib import contextmanager +from typing import Any, Dict, List, Optional, Tuple + +from open_webui.config import ( + MARIADB_VECTOR_DB_URL, + MARIADB_VECTOR_DISTANCE_STRATEGY, + MARIADB_VECTOR_INDEX_M, + MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH, + MARIADB_VECTOR_POOL_MAX_OVERFLOW, + MARIADB_VECTOR_POOL_RECYCLE, + MARIADB_VECTOR_POOL_SIZE, + MARIADB_VECTOR_POOL_TIMEOUT, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata +from sqlalchemy import create_engine +from sqlalchemy.pool import NullPool, QueuePool + +log = logging.getLogger(__name__) + +VECTOR_LENGTH = int(MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH) + + +def _embedding_to_f32_bytes(vec: List[float]) -> bytes: + """ + Convert a Python float vector into the binary payload expected by MariaDB VECTOR. + + MariaDB Vector expects the vector argument to be bound as a little-endian float32 + byte sequence. We use array('f') to avoid a numpy dependency and byteswap on + big-endian platforms for portability. + """ + a = array.array('f', [float(x) for x in vec]) # float32 + if sys.byteorder != 'little': + a.byteswap() + return a.tobytes() + + +def _safe_json(v: Any) -> Dict[str, Any]: + """ + Normalize a potentially JSON-like value into a Python dict. + + Accepts: + - dict: returned as-is + - str / bytes: parsed as JSON if possible + - None / other types: returns {} + """ + if v is None: + return {} + if isinstance(v, dict): + return v + if isinstance(v, (bytes, bytearray)): + try: + v = v.decode('utf-8') + except Exception: + return {} + if isinstance(v, str): + try: + j = json.loads(v) + return j if isinstance(j, dict) else {} + except Exception: + return {} + return {} + + +class MariaDBVectorClient(VectorDBBase): + """ + MariaDB + MariaDB Vector backend using DBAPI cursor parameter binding. + + IMPORTANT: + - Intended for: mariadb+mariadbconnector://... (official MariaDB driver). + - Uses qmark ("?") params and binds vectors as float32 bytes. + - Uses binary binding for BOTH inserts/updates and distance computations. + """ + + def __init__( + self, + db_url: Optional[str] = None, + vector_length: int = VECTOR_LENGTH, + distance_strategy: str = MARIADB_VECTOR_DISTANCE_STRATEGY, + index_m: int = MARIADB_VECTOR_INDEX_M, + ) -> None: + """ + Initialize a MariaDB Vector-backed VectorDBBase implementation. + + Validates URL scheme/driver requirements, ensures schema exists, and guards + against dimension mismatch with an existing VECTOR(n) column. + """ + self.db_url = (db_url or MARIADB_VECTOR_DB_URL).strip() + self.vector_length = int(vector_length) + self.distance_strategy = (distance_strategy or 'cosine').strip().lower() + self.index_m = int(index_m) + + if self.distance_strategy not in {'cosine', 'euclidean'}: + raise ValueError("distance_strategy must be 'cosine' or 'euclidean'") + + if not self.db_url.lower().startswith('mariadb+mariadbconnector://'): + raise ValueError( + 'MariaDBVectorClient requires mariadb+mariadbconnector:// (official MariaDB driver) ' + 'to ensure qmark paramstyle and correct VECTOR binding.' + ) + + if isinstance(MARIADB_VECTOR_POOL_SIZE, int): + if MARIADB_VECTOR_POOL_SIZE > 0: + self.engine = create_engine( + self.db_url, + pool_size=MARIADB_VECTOR_POOL_SIZE, + max_overflow=MARIADB_VECTOR_POOL_MAX_OVERFLOW, + pool_timeout=MARIADB_VECTOR_POOL_TIMEOUT, + pool_recycle=MARIADB_VECTOR_POOL_RECYCLE, + pool_pre_ping=True, + poolclass=QueuePool, + ) + else: + self.engine = create_engine(self.db_url, pool_pre_ping=True, poolclass=NullPool) + else: + self.engine = create_engine(self.db_url, pool_pre_ping=True) + self._init_schema() + self._check_vector_length() + + @contextmanager + def _connect(self): + """ + Yield a context-managed DBAPI connection (SQLAlchemy raw_connection()). + + Callers can use: + with self._connect() as conn: + with conn.cursor() as cur: + ... + """ + conn = self.engine.raw_connection() + try: + yield conn + finally: + try: + conn.close() + except Exception: + pass + + def _init_schema(self) -> None: + """ + Create the backing table and vector index if they do not exist. + + Uses a PK definition compatible with MariaDB Vector's VECTOR INDEX key-size constraints. + """ + with self._connect() as conn: + with conn.cursor() as cur: + try: + dist = self.distance_strategy + cur.execute(f""" + CREATE TABLE IF NOT EXISTS document_chunk ( + -- MariaDB Vector requires the table PRIMARY KEY used with a VECTOR INDEX to be <= 256 bytes. + -- VARCHAR has internal length/metadata overhead, so VARCHAR(255) can exceed the 256-byte limit. + -- We use VARCHAR(254) to stay safely under the limit, and force ASCII (1 byte/char) so the byte + -- size is predictable (avoid utf8mb4 where a "255 char" key could be up to 1020 bytes). + -- ascii_bin gives bytewise, case-sensitive comparisons for stable ID matching. + id VARCHAR(254) CHARACTER SET ascii COLLATE ascii_bin PRIMARY KEY, + embedding VECTOR({self.vector_length}) NOT NULL, + collection_name VARCHAR(255) NOT NULL, + text LONGTEXT NULL, + vmetadata JSON NULL, + VECTOR INDEX (embedding) M={self.index_m} DISTANCE={dist}, + INDEX idx_document_chunk_collection_name (collection_name) + ) ENGINE=InnoDB; + """) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during database initialization: {e}') + raise + + def _check_vector_length(self) -> None: + """ + Validate that the existing VECTOR column dimension matches this client's configured dimension. + + Dimension guard: if table already exists with + a different VECTOR(n), refuse to silently mismatch. + """ + with self._connect() as conn: + with conn.cursor() as cur: + cur.execute('SHOW CREATE TABLE document_chunk') + row = cur.fetchone() + if not row or len(row) < 2: + return + ddl = row[1] + m = re.search(r'vector\\((\\d+)\\)', ddl, flags=re.IGNORECASE) + if not m: + return + existing = int(m.group(1)) + if existing != int(self.vector_length): + raise Exception( + f'VECTOR_LENGTH {self.vector_length} does not match existing vector column dimension {existing}. ' + 'Cannot change vector size after initialization without migrating the data.' + ) + + def adjust_vector_length(self, vector: List[float]) -> List[float]: + """ + Pad or truncate a vector to match `self.vector_length`. + """ + n = len(vector) + if n < self.vector_length: + return vector + [0.0] * (self.vector_length - n) + if n > self.vector_length: + return vector[: self.vector_length] + return vector + + def _dist_fn(self) -> str: + """ + Return the MariaDB Vector distance function name for the configured strategy. + """ + return 'vec_distance_cosine' if self.distance_strategy == 'cosine' else 'vec_distance_euclidean' + + def _score_from_dist(self, dist: float) -> float: + """ + Convert a DB distance value into a normalized score in (0, 1]. + + - cosine: score ~= 1 - cosine_distance, clamped to [0, 1] + - euclidean: score = 1 / (1 + dist) + """ + if self.distance_strategy == 'cosine': + score = 1.0 - dist + if score < 0.0: + score = 0.0 + if score > 1.0: + score = 1.0 + return score + return 1.0 / (1.0 + max(0.0, dist)) + + def _build_filter_sql_qmark(self, expr: Any) -> Tuple[str, List[Any]]: + """ + Build a WHERE-clause fragment and qmark params from a minimal Mongo-like filter. + + Supported forms: + - {"field": "v"} + - {"field": {"$in": ["a","b"]}} + - {"$and": [ ... ]} + - {"$or": [ ... ]} + """ + if not expr or not isinstance(expr, dict): + return '', [] + + if '$and' in expr: + parts: List[str] = [] + params: List[Any] = [] + for e in expr.get('$and') or []: + s, p = self._build_filter_sql_qmark(e) + if s: + parts.append(s) + params.extend(p) + return ('(' + ' AND '.join(parts) + ')') if parts else '', params + + if '$or' in expr: + parts: List[str] = [] + params: List[Any] = [] + for e in expr.get('$or') or []: + s, p = self._build_filter_sql_qmark(e) + if s: + parts.append(s) + params.extend(p) + return ('(' + ' OR '.join(parts) + ')') if parts else '', params + + clauses: List[str] = [] + params: List[Any] = [] + for key, value in expr.items(): + if key.startswith('$'): + continue + json_expr = f"JSON_UNQUOTE(JSON_EXTRACT(vmetadata, '$.{key}'))" + if isinstance(value, dict) and '$in' in value: + vals = [str(v) for v in (value.get('$in') or [])] + if not vals: + clauses.append('0=1') + continue + ors = [] + for v in vals: + ors.append(f'{json_expr} = ?') + params.append(v) + clauses.append('(' + ' OR '.join(ors) + ')') + else: + clauses.append(f'{json_expr} = ?') + params.append(str(value)) + return ('(' + ' AND '.join(clauses) + ')') if clauses else '', params + + def insert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert items into the given collection (best-effort, ignores duplicates). + + Uses executemany() with binary VECTOR binding for high-throughput ingestion. + """ + if not items: + return + with self._connect() as conn: + with conn.cursor() as cur: + try: + sql = """ + INSERT IGNORE INTO document_chunk + (id, embedding, collection_name, text, vmetadata) + VALUES + (?, ?, ?, ?, ?) + """ + params: List[Tuple[Any, ...]] = [] + for item in items: + v = self.adjust_vector_length(item['vector']) + emb = _embedding_to_f32_bytes(v) + meta = process_metadata(item.get('metadata') or {}) + params.append( + ( + item['id'], + emb, + collection_name, + item.get('text'), + json.dumps(meta), + ) + ) + cur.executemany(sql, params) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during insert: {e}') + raise + + def upsert(self, collection_name: str, items: List[VectorItem]) -> None: + """ + Insert or update items in the given collection by primary key. + + Uses executemany() and updates embedding/text/metadata on conflicts. + """ + if not items: + return + with self._connect() as conn: + with conn.cursor() as cur: + try: + sql = """ + INSERT INTO document_chunk + (id, embedding, collection_name, text, vmetadata) + VALUES + (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + embedding = VALUES(embedding), + collection_name = VALUES(collection_name), + text = VALUES(text), + vmetadata = VALUES(vmetadata) + """ + params: List[Tuple[Any, ...]] = [] + for item in items: + v = self.adjust_vector_length(item['vector']) + emb = _embedding_to_f32_bytes(v) + meta = process_metadata(item.get('metadata') or {}) + params.append( + ( + item['id'], + emb, + collection_name, + item.get('text'), + json.dumps(meta), + ) + ) + cur.executemany(sql, params) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during upsert: {e}') + raise + + def search( + self, + collection_name: str, + vectors: List[List[float]], + filter: Optional[Dict[str, Any]] = None, + limit: int = 10, + ) -> Optional[SearchResult]: + """ + Perform a vector similarity search. + + Args: + collection_name: Logical collection partition key. + vectors: One or more query vectors. + filter: Optional metadata filter (Mongo-like subset). + limit: Top-k per query vector. + + Returns a SearchResult where distances are normalized scores (higher is better). + """ + if not vectors: + return None + + dist_fn = self._dist_fn() + ids: List[List[str]] = [[] for _ in vectors] + distances: List[List[float]] = [[] for _ in vectors] + documents: List[List[str]] = [[] for _ in vectors] + metadatas: List[List[Any]] = [[] for _ in vectors] + + try: + with self._connect() as conn: + with conn.cursor() as cur: + fsql, fparams = self._build_filter_sql_qmark(filter or {}) + where = 'collection_name = ?' + base_params: List[Any] = [collection_name] + if fsql: + where = where + ' AND ' + fsql + base_params.extend(fparams) + + sql = f""" + SELECT + id, + text, + vmetadata, + {dist_fn}(embedding, ?) AS dist + FROM document_chunk + WHERE {where} + ORDER BY dist ASC + LIMIT ? + """ + + for q_idx, q in enumerate(vectors): + qv = self.adjust_vector_length(q) + qbin = _embedding_to_f32_bytes(qv) + params = [qbin] + list(base_params) + [int(limit)] + cur.execute(sql, params) + rows = cur.fetchall() + + for r in rows: + rid, rtext, rmeta, rdist = r[0], r[1], r[2], r[3] + ids[q_idx].append(str(rid)) + try: + dist = float(rdist) if rdist is not None else 1.0 + except Exception: + dist = 1.0 + if math.isnan(dist) or math.isinf(dist): + dist = 1.0 + distances[q_idx].append(self._score_from_dist(dist)) + documents[q_idx].append(rtext) + metadatas[q_idx].append(_safe_json(rmeta)) + + return SearchResult( + ids=ids, + distances=distances, + documents=documents, + metadatas=metadatas, + ) + except Exception as e: + log.exception(f'[MARIADB_VECTOR] search() failed: {e}') + return None + + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: + """ + Retrieve documents by metadata filter (non-vector query). + """ + with self._connect() as conn: + with conn.cursor() as cur: + fsql, fparams = self._build_filter_sql_qmark(filter or {}) + where = 'collection_name = ?' + params: List[Any] = [collection_name] + if fsql: + where = where + ' AND ' + fsql + params.extend(fparams) + sql = f'SELECT id, text, vmetadata FROM document_chunk WHERE {where}' + if limit is not None: + sql += ' LIMIT ?' + params.append(int(limit)) + cur.execute(sql, params) + rows = cur.fetchall() + if not rows: + return None + ids = [[str(r[0]) for r in rows]] + documents = [[r[1] for r in rows]] + metadatas = [[_safe_json(r[2]) for r in rows]] + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + + def get(self, collection_name: str, limit: Optional[int] = None) -> Optional[GetResult]: + """ + Retrieve documents in a collection without filtering (optionally limited). + """ + with self._connect() as conn: + with conn.cursor() as cur: + sql = 'SELECT id, text, vmetadata FROM document_chunk WHERE collection_name = ?' + params: List[Any] = [collection_name] + if limit is not None: + sql += ' LIMIT ?' + params.append(int(limit)) + cur.execute(sql, params) + rows = cur.fetchall() + if not rows: + return None + ids = [[str(r[0]) for r in rows]] + documents = [[r[1] for r in rows]] + metadatas = [[_safe_json(r[2]) for r in rows]] + return GetResult(ids=ids, documents=documents, metadatas=metadatas) + + def delete( + self, + collection_name: str, + ids: Optional[List[str]] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Delete rows from a collection by id list and/or metadata filter. + + If both are provided, they are combined with AND semantics. + """ + with self._connect() as conn: + with conn.cursor() as cur: + try: + where = ['collection_name = ?'] + params: List[Any] = [collection_name] + + if ids: + ph = ', '.join(['?'] * len(ids)) + where.append(f'id IN ({ph})') + params.extend(ids) + + if filter: + fsql, fparams = self._build_filter_sql_qmark(filter) + if fsql: + where.append(fsql) + params.extend(fparams) + + sql = 'DELETE FROM document_chunk WHERE ' + ' AND '.join(where) + cur.execute(sql, params) + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during delete: {e}') + raise + + def reset(self) -> None: + """ + Truncate the vector table (drops all collections). + """ + with self._connect() as conn: + with conn.cursor() as cur: + try: + cur.execute('TRUNCATE TABLE document_chunk') + conn.commit() + except Exception as e: + conn.rollback() + log.exception(f'Error during reset: {e}') + raise + + def has_collection(self, collection_name: str) -> bool: + """ + Return True if the collection contains at least one row, else False. + """ + try: + with self._connect() as conn: + with conn.cursor() as cur: + cur.execute( + 'SELECT 1 FROM document_chunk WHERE collection_name = ? LIMIT 1', + (collection_name,), + ) + return cur.fetchone() is not None + except Exception: + return False + + def delete_collection(self, collection_name: str) -> None: + """ + Delete all rows in a collection. + """ + self.delete(collection_name) + + def close(self) -> None: + """ + Dispose the underlying SQLAlchemy engine. + """ + try: + self.engine.dispose() + except Exception as e: + log.exception(f'Error during dispose the underlying SQLAlchemy engine: {e}') diff --git a/backend/open_webui/retrieval/vector/dbs/milvus.py b/backend/open_webui/retrieval/vector/dbs/milvus.py index 35cf6b3829..9b356b1c69 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus.py @@ -1,37 +1,39 @@ -from pymilvus import MilvusClient as Client -from pymilvus import FieldSchema, DataType -from pymilvus import connections, Collection +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" import json import logging from typing import Optional -from open_webui.retrieval.vector.utils import process_metadata -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) from open_webui.config import ( - MILVUS_URI, MILVUS_DB, - MILVUS_TOKEN, - MILVUS_INDEX_TYPE, - MILVUS_METRIC_TYPE, - MILVUS_HNSW_M, - MILVUS_HNSW_EFCONSTRUCTION, - MILVUS_IVF_FLAT_NLIST, MILVUS_DISKANN_MAX_DEGREE, MILVUS_DISKANN_SEARCH_LIST_SIZE, + MILVUS_HNSW_EFCONSTRUCTION, + MILVUS_HNSW_M, + MILVUS_INDEX_TYPE, + MILVUS_IVF_FLAT_NLIST, + MILVUS_METRIC_TYPE, + MILVUS_TOKEN, + MILVUS_URI, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata +from pymilvus import Collection, DataType, FieldSchema, connections +from pymilvus import MilvusClient as Client log = logging.getLogger(__name__) class MilvusClient(VectorDBBase): def __init__(self): - self.collection_prefix = "open_webui" + self.collection_prefix = 'open_webui' if MILVUS_TOKEN is None: self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB) else: @@ -46,17 +48,17 @@ class MilvusClient(VectorDBBase): _documents = [] _metadatas = [] for item in match: - _ids.append(item.get("id")) - _documents.append(item.get("data", {}).get("text")) - _metadatas.append(item.get("metadata")) + _ids.append(item.get('id')) + _documents.append(item.get('data', {}).get('text')) + _metadatas.append(item.get('metadata')) ids.append(_ids) documents.append(_documents) metadatas.append(_metadatas) return GetResult( **{ - "ids": ids, - "documents": documents, - "metadatas": metadatas, + 'ids': ids, + 'documents': documents, + 'metadatas': metadatas, } ) @@ -71,23 +73,23 @@ class MilvusClient(VectorDBBase): _documents = [] _metadatas = [] for item in match: - _ids.append(item.get("id")) + _ids.append(item.get('id')) # normalize milvus score from [-1, 1] to [0, 1] range # https://milvus.io/docs/de/metric.md - _dist = (item.get("distance") + 1.0) / 2.0 + _dist = (item.get('distance') + 1.0) / 2.0 _distances.append(_dist) - _documents.append(item.get("entity", {}).get("data", {}).get("text")) - _metadatas.append(item.get("entity", {}).get("metadata")) + _documents.append(item.get('entity', {}).get('data', {}).get('text')) + _metadatas.append(item.get('entity', {}).get('metadata')) ids.append(_ids) distances.append(_distances) documents.append(_documents) metadatas.append(_metadatas) return SearchResult( **{ - "ids": ids, - "distances": distances, - "documents": documents, - "metadatas": metadatas, + 'ids': ids, + 'distances': distances, + 'documents': documents, + 'metadatas': metadatas, } ) @@ -97,21 +99,19 @@ class MilvusClient(VectorDBBase): enable_dynamic_field=True, ) schema.add_field( - field_name="id", + field_name='id', datatype=DataType.VARCHAR, is_primary=True, max_length=65535, ) schema.add_field( - field_name="vector", + field_name='vector', datatype=DataType.FLOAT_VECTOR, dim=dimension, - description="vector", - ) - schema.add_field(field_name="data", datatype=DataType.JSON, description="data") - schema.add_field( - field_name="metadata", datatype=DataType.JSON, description="metadata" + description='vector', ) + schema.add_field(field_name='data', datatype=DataType.JSON, description='data') + schema.add_field(field_name='metadata', datatype=DataType.JSON, description='metadata') index_params = self.client.prepare_index_params() @@ -119,44 +119,44 @@ class MilvusClient(VectorDBBase): index_type = MILVUS_INDEX_TYPE.upper() metric_type = MILVUS_METRIC_TYPE.upper() - log.info(f"Using Milvus index type: {index_type}, metric type: {metric_type}") + log.info(f'Using Milvus index type: {index_type}, metric type: {metric_type}') index_creation_params = {} - if index_type == "HNSW": + if index_type == 'HNSW': index_creation_params = { - "M": MILVUS_HNSW_M, - "efConstruction": MILVUS_HNSW_EFCONSTRUCTION, + 'M': MILVUS_HNSW_M, + 'efConstruction': MILVUS_HNSW_EFCONSTRUCTION, } - log.info(f"HNSW params: {index_creation_params}") - elif index_type == "IVF_FLAT": - index_creation_params = {"nlist": MILVUS_IVF_FLAT_NLIST} - log.info(f"IVF_FLAT params: {index_creation_params}") - elif index_type == "DISKANN": + log.info(f'HNSW params: {index_creation_params}') + elif index_type == 'IVF_FLAT': + index_creation_params = {'nlist': MILVUS_IVF_FLAT_NLIST} + log.info(f'IVF_FLAT params: {index_creation_params}') + elif index_type == 'DISKANN': index_creation_params = { - "max_degree": MILVUS_DISKANN_MAX_DEGREE, - "search_list_size": MILVUS_DISKANN_SEARCH_LIST_SIZE, + 'max_degree': MILVUS_DISKANN_MAX_DEGREE, + 'search_list_size': MILVUS_DISKANN_SEARCH_LIST_SIZE, } - log.info(f"DISKANN params: {index_creation_params}") - elif index_type in ["FLAT", "AUTOINDEX"]: - log.info(f"Using {index_type} index with no specific build-time params.") + log.info(f'DISKANN params: {index_creation_params}') + elif index_type in ['FLAT', 'AUTOINDEX']: + log.info(f'Using {index_type} index with no specific build-time params.') else: log.warning( f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. " - f"Supported types: HNSW, IVF_FLAT, DISKANN, FLAT, AUTOINDEX. " - f"Milvus will use its default for the collection if this type is not directly supported for index creation." + f'Supported types: HNSW, IVF_FLAT, DISKANN, FLAT, AUTOINDEX. ' + f'Milvus will use its default for the collection if this type is not directly supported for index creation.' ) # For unsupported types, pass the type directly to Milvus; it might handle it or use a default. # If Milvus errors out, the user needs to correct the MILVUS_INDEX_TYPE env var. index_params.add_index( - field_name="vector", + field_name='vector', index_type=index_type, metric_type=metric_type, params=index_creation_params, ) self.client.create_collection( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', schema=schema, index_params=index_params, ) @@ -166,17 +166,13 @@ class MilvusClient(VectorDBBase): def has_collection(self, collection_name: str) -> bool: # Check if the collection exists based on the collection name. - collection_name = collection_name.replace("-", "_") - return self.client.has_collection( - collection_name=f"{self.collection_prefix}_{collection_name}" - ) + collection_name = collection_name.replace('-', '_') + return self.client.has_collection(collection_name=f'{self.collection_prefix}_{collection_name}') def delete_collection(self, collection_name: str): # Delete the collection based on the collection name. - collection_name = collection_name.replace("-", "_") - return self.client.drop_collection( - collection_name=f"{self.collection_prefix}_{collection_name}" - ) + collection_name = collection_name.replace('-', '_') + return self.client.drop_collection(collection_name=f'{self.collection_prefix}_{collection_name}') def search( self, @@ -186,15 +182,15 @@ class MilvusClient(VectorDBBase): limit: int = 10, ) -> Optional[SearchResult]: # Search for the nearest neighbor items based on the vectors and return 'limit' number of results. - collection_name = collection_name.replace("-", "_") + collection_name = collection_name.replace('-', '_') # For some index types like IVF_FLAT, search params like nprobe can be set. # Example: search_params = {"nprobe": 10} if using IVF_FLAT # For simplicity, not adding configurable search_params here, but could be extended. result = self.client.search( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', data=vectors, limit=limit, - output_fields=["data", "metadata"], + output_fields=['data', 'metadata'], # search_params=search_params # Potentially add later if needed ) return self._result_to_search_result(result) @@ -202,11 +198,9 @@ class MilvusClient(VectorDBBase): def query(self, collection_name: str, filter: dict, limit: int = -1): connections.connect(uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB) - collection_name = collection_name.replace("-", "_") + collection_name = collection_name.replace('-', '_') if not self.has_collection(collection_name): - log.warning( - f"Query attempted on non-existent collection: {self.collection_prefix}_{collection_name}" - ) + log.warning(f'Query attempted on non-existent collection: {self.collection_prefix}_{collection_name}') return None filter_expressions = [] @@ -216,9 +210,9 @@ class MilvusClient(VectorDBBase): else: filter_expressions.append(f'metadata["{key}"] == {value}') - filter_string = " && ".join(filter_expressions) + filter_string = ' && '.join(filter_expressions) - collection = Collection(f"{self.collection_prefix}_{collection_name}") + collection = Collection(f'{self.collection_prefix}_{collection_name}') collection.load() try: @@ -229,9 +223,9 @@ class MilvusClient(VectorDBBase): iterator = collection.query_iterator( expr=filter_string, output_fields=[ - "id", - "data", - "metadata", + 'id', + 'data', + 'metadata', ], limit=limit if limit > 0 else -1, ) @@ -244,7 +238,7 @@ class MilvusClient(VectorDBBase): break all_results.extend(batch) - log.debug(f"Total results from query: {len(all_results)}") + log.debug(f'Total results from query: {len(all_results)}') return self._result_to_get_result([all_results] if all_results else [[]]) except Exception as e: @@ -255,7 +249,7 @@ class MilvusClient(VectorDBBase): def get(self, collection_name: str) -> Optional[GetResult]: # Get all the items in the collection. This can be very resource-intensive for large collections. - collection_name = collection_name.replace("-", "_") + collection_name = collection_name.replace('-', '_') log.warning( f"Fetching ALL items from collection '{self.collection_prefix}_{collection_name}'. This might be slow for large collections." ) @@ -265,35 +259,25 @@ class MilvusClient(VectorDBBase): def insert(self, collection_name: str, items: list[VectorItem]): # Insert the items into the collection, if the collection does not exist, it will be created. - collection_name = collection_name.replace("-", "_") - if not self.client.has_collection( - collection_name=f"{self.collection_prefix}_{collection_name}" - ): - log.info( - f"Collection {self.collection_prefix}_{collection_name} does not exist. Creating now." - ) + collection_name = collection_name.replace('-', '_') + if not self.client.has_collection(collection_name=f'{self.collection_prefix}_{collection_name}'): + log.info(f'Collection {self.collection_prefix}_{collection_name} does not exist. Creating now.') if not items: log.error( - f"Cannot create collection {self.collection_prefix}_{collection_name} without items to determine dimension." + f'Cannot create collection {self.collection_prefix}_{collection_name} without items to determine dimension.' ) - raise ValueError( - "Cannot create Milvus collection without items to determine vector dimension." - ) - self._create_collection( - collection_name=collection_name, dimension=len(items[0]["vector"]) - ) + raise ValueError('Cannot create Milvus collection without items to determine vector dimension.') + self._create_collection(collection_name=collection_name, dimension=len(items[0]['vector'])) - log.info( - f"Inserting {len(items)} items into collection {self.collection_prefix}_{collection_name}." - ) + log.info(f'Inserting {len(items)} items into collection {self.collection_prefix}_{collection_name}.') return self.client.insert( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', data=[ { - "id": item["id"], - "vector": item["vector"], - "data": {"text": item["text"]}, - "metadata": process_metadata(item["metadata"]), + 'id': item['id'], + 'vector': item['vector'], + 'data': {'text': item['text']}, + 'metadata': process_metadata(item['metadata']), } for item in items ], @@ -301,35 +285,27 @@ class MilvusClient(VectorDBBase): def upsert(self, collection_name: str, items: list[VectorItem]): # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created. - collection_name = collection_name.replace("-", "_") - if not self.client.has_collection( - collection_name=f"{self.collection_prefix}_{collection_name}" - ): - log.info( - f"Collection {self.collection_prefix}_{collection_name} does not exist for upsert. Creating now." - ) + collection_name = collection_name.replace('-', '_') + if not self.client.has_collection(collection_name=f'{self.collection_prefix}_{collection_name}'): + log.info(f'Collection {self.collection_prefix}_{collection_name} does not exist for upsert. Creating now.') if not items: log.error( - f"Cannot create collection {self.collection_prefix}_{collection_name} for upsert without items to determine dimension." + f'Cannot create collection {self.collection_prefix}_{collection_name} for upsert without items to determine dimension.' ) raise ValueError( - "Cannot create Milvus collection for upsert without items to determine vector dimension." + 'Cannot create Milvus collection for upsert without items to determine vector dimension.' ) - self._create_collection( - collection_name=collection_name, dimension=len(items[0]["vector"]) - ) + self._create_collection(collection_name=collection_name, dimension=len(items[0]['vector'])) - log.info( - f"Upserting {len(items)} items into collection {self.collection_prefix}_{collection_name}." - ) + log.info(f'Upserting {len(items)} items into collection {self.collection_prefix}_{collection_name}.') return self.client.upsert( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', data=[ { - "id": item["id"], - "vector": item["vector"], - "data": {"text": item["text"]}, - "metadata": process_metadata(item["metadata"]), + 'id': item['id'], + 'vector': item['vector'], + 'data': {'text': item['text']}, + 'metadata': process_metadata(item['metadata']), } for item in items ], @@ -342,46 +318,35 @@ class MilvusClient(VectorDBBase): filter: Optional[dict] = None, ): # Delete the items from the collection based on the ids or filter. - collection_name = collection_name.replace("-", "_") + collection_name = collection_name.replace('-', '_') if not self.has_collection(collection_name): - log.warning( - f"Delete attempted on non-existent collection: {self.collection_prefix}_{collection_name}" - ) + log.warning(f'Delete attempted on non-existent collection: {self.collection_prefix}_{collection_name}') return None if ids: - log.info( - f"Deleting items by IDs from {self.collection_prefix}_{collection_name}. IDs: {ids}" - ) + log.info(f'Deleting items by IDs from {self.collection_prefix}_{collection_name}. IDs: {ids}') return self.client.delete( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', ids=ids, ) elif filter: - filter_string = " && ".join( - [ - f'metadata["{key}"] == {json.dumps(value)}' - for key, value in filter.items() - ] - ) + filter_string = ' && '.join([f'metadata["{key}"] == {json.dumps(value)}' for key, value in filter.items()]) log.info( - f"Deleting items by filter from {self.collection_prefix}_{collection_name}. Filter: {filter_string}" + f'Deleting items by filter from {self.collection_prefix}_{collection_name}. Filter: {filter_string}' ) return self.client.delete( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', filter=filter_string, ) else: log.warning( - f"Delete operation on {self.collection_prefix}_{collection_name} called without IDs or filter. No action taken." + f'Delete operation on {self.collection_prefix}_{collection_name} called without IDs or filter. No action taken.' ) return None def reset(self): # Resets the database. This will delete all collections and item entries that match the prefix. - log.warning( - f"Resetting Milvus: Deleting all collections with prefix '{self.collection_prefix}'." - ) + log.warning(f"Resetting Milvus: Deleting all collections with prefix '{self.collection_prefix}'.") collection_names = self.client.list_collections() deleted_collections = [] for collection_name_full in collection_names: @@ -389,7 +354,7 @@ class MilvusClient(VectorDBBase): try: self.client.drop_collection(collection_name=collection_name_full) deleted_collections.append(collection_name_full) - log.info(f"Deleted collection: {collection_name_full}") + log.info(f'Deleted collection: {collection_name_full}') except Exception as e: - log.error(f"Error deleting collection {collection_name_full}: {e}") - log.info(f"Milvus reset complete. Deleted collections: {deleted_collections}") + log.error(f'Error deleting collection {collection_name_full}: {e}') + log.info(f'Milvus reset complete. Deleted collections: {deleted_collections}') diff --git a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py index c58189b2a3..91c5f58665 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py @@ -1,16 +1,21 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + import logging -from typing import Optional, Tuple, List, Dict, Any +import re +from typing import Any, Dict, List, Optional, Tuple from open_webui.config import ( - MILVUS_URI, - MILVUS_TOKEN, - MILVUS_DB, MILVUS_COLLECTION_PREFIX, - MILVUS_INDEX_TYPE, - MILVUS_METRIC_TYPE, - MILVUS_HNSW_M, + MILVUS_DB, MILVUS_HNSW_EFCONSTRUCTION, + MILVUS_HNSW_M, + MILVUS_INDEX_TYPE, MILVUS_IVF_FLAT_NLIST, + MILVUS_METRIC_TYPE, + MILVUS_TOKEN, + MILVUS_URI, ) from open_webui.retrieval.vector.main import ( GetResult, @@ -19,36 +24,60 @@ from open_webui.retrieval.vector.main import ( VectorItem, ) from pymilvus import ( - connections, - utility, Collection, CollectionSchema, - FieldSchema, DataType, + FieldSchema, + connections, + utility, ) log = logging.getLogger(__name__) -RESOURCE_ID_FIELD = "resource_id" +RESOURCE_ID_FIELD = 'resource_id' + +# Milvus expressions are SQL-like strings with no parameterized-query API; +# values get interpolated into single-quoted literals. Reject anything that +# can't be a legitimate Open WebUI collection name. +_SAFE_RESOURCE_ID_RE = re.compile(r'^[A-Za-z0-9_-]{1,255}$') +_SAFE_METADATA_KEY_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,63}$') + + +def _validate_resource_id(resource_id: str) -> str: + if not isinstance(resource_id, str) or not _SAFE_RESOURCE_ID_RE.match(resource_id): + raise ValueError(f'Invalid Milvus resource_id (collection name): {resource_id!r}') + return resource_id + + +def _validate_metadata_key(key: str) -> str: + if not isinstance(key, str) or not _SAFE_METADATA_KEY_RE.match(key): + raise ValueError(f'Invalid Milvus metadata filter key: {key!r}') + return key + + +def _escape_milvus_string(value: str) -> str: + if not isinstance(value, str): + raise TypeError(f'Expected str for Milvus expression value, got {type(value).__name__}') + return value.replace('\\', '\\\\').replace("'", "\\'") class MilvusClient(VectorDBBase): def __init__(self): # Milvus collection names can only contain numbers, letters, and underscores. - self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace("-", "_") + self.collection_prefix = MILVUS_COLLECTION_PREFIX.replace('-', '_') connections.connect( - alias="default", + alias='default', uri=MILVUS_URI, token=MILVUS_TOKEN, db_name=MILVUS_DB, ) # Main collection types for multi-tenancy - self.MEMORY_COLLECTION = f"{self.collection_prefix}_memories" - self.KNOWLEDGE_COLLECTION = f"{self.collection_prefix}_knowledge" - self.FILE_COLLECTION = f"{self.collection_prefix}_files" - self.WEB_SEARCH_COLLECTION = f"{self.collection_prefix}_web_search" - self.HASH_BASED_COLLECTION = f"{self.collection_prefix}_hash_based" + self.MEMORY_COLLECTION = f'{self.collection_prefix}_memories' + self.KNOWLEDGE_COLLECTION = f'{self.collection_prefix}_knowledge' + self.FILE_COLLECTION = f'{self.collection_prefix}_files' + self.WEB_SEARCH_COLLECTION = f'{self.collection_prefix}_web_search' + self.HASH_BASED_COLLECTION = f'{self.collection_prefix}_hash_based' self.shared_collections = [ self.MEMORY_COLLECTION, self.KNOWLEDGE_COLLECTION, @@ -70,15 +99,13 @@ class MilvusClient(VectorDBBase): """ resource_id = collection_name - if collection_name.startswith("user-memory-"): + if collection_name.startswith('user-memory-'): return self.MEMORY_COLLECTION, resource_id - elif collection_name.startswith("file-"): + elif collection_name.startswith('file-'): return self.FILE_COLLECTION, resource_id - elif collection_name.startswith("web-search-"): + elif collection_name.startswith('web-search-'): return self.WEB_SEARCH_COLLECTION, resource_id - elif len(collection_name) == 63 and all( - c in "0123456789abcdef" for c in collection_name - ): + elif len(collection_name) == 63 and all(c in '0123456789abcdef' for c in collection_name): return self.HASH_BASED_COLLECTION, resource_id else: return self.KNOWLEDGE_COLLECTION, resource_id @@ -86,36 +113,36 @@ class MilvusClient(VectorDBBase): def _create_shared_collection(self, mt_collection_name: str, dimension: int): fields = [ FieldSchema( - name="id", + name='id', dtype=DataType.VARCHAR, is_primary=True, auto_id=False, max_length=36, ), - FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dimension), - FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=65535), - FieldSchema(name="metadata", dtype=DataType.JSON), + FieldSchema(name='vector', dtype=DataType.FLOAT_VECTOR, dim=dimension), + FieldSchema(name='text', dtype=DataType.VARCHAR, max_length=65535), + FieldSchema(name='metadata', dtype=DataType.JSON), FieldSchema(name=RESOURCE_ID_FIELD, dtype=DataType.VARCHAR, max_length=255), ] - schema = CollectionSchema(fields, "Shared collection for multi-tenancy") + schema = CollectionSchema(fields, 'Shared collection for multi-tenancy') collection = Collection(mt_collection_name, schema) index_params = { - "metric_type": MILVUS_METRIC_TYPE, - "index_type": MILVUS_INDEX_TYPE, - "params": {}, + 'metric_type': MILVUS_METRIC_TYPE, + 'index_type': MILVUS_INDEX_TYPE, + 'params': {}, } - if MILVUS_INDEX_TYPE == "HNSW": - index_params["params"] = { - "M": MILVUS_HNSW_M, - "efConstruction": MILVUS_HNSW_EFCONSTRUCTION, + if MILVUS_INDEX_TYPE == 'HNSW': + index_params['params'] = { + 'M': MILVUS_HNSW_M, + 'efConstruction': MILVUS_HNSW_EFCONSTRUCTION, } - elif MILVUS_INDEX_TYPE == "IVF_FLAT": - index_params["params"] = {"nlist": MILVUS_IVF_FLAT_NLIST} + elif MILVUS_INDEX_TYPE == 'IVF_FLAT': + index_params['params'] = {'nlist': MILVUS_IVF_FLAT_NLIST} - collection.create_index("vector", index_params) + collection.create_index('vector', index_params) collection.create_index(RESOURCE_ID_FIELD) - log.info(f"Created shared collection: {mt_collection_name}") + log.info(f'Created shared collection: {mt_collection_name}') return collection def _ensure_collection(self, mt_collection_name: str, dimension: int): @@ -123,9 +150,8 @@ class MilvusClient(VectorDBBase): self._create_shared_collection(mt_collection_name, dimension) def has_collection(self, collection_name: str) -> bool: - mt_collection, resource_id = self._get_collection_and_resource_id( - collection_name - ) + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return False @@ -137,19 +163,18 @@ class MilvusClient(VectorDBBase): def upsert(self, collection_name: str, items: List[VectorItem]): if not items: return - mt_collection, resource_id = self._get_collection_and_resource_id( - collection_name - ) - dimension = len(items[0]["vector"]) + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) + dimension = len(items[0]['vector']) self._ensure_collection(mt_collection, dimension) collection = Collection(mt_collection) entities = [ { - "id": item["id"], - "vector": item["vector"], - "text": item["text"], - "metadata": item["metadata"], + 'id': item['id'], + 'vector': item['vector'], + 'text': item['text'], + 'metadata': item['metadata'], RESOURCE_ID_FIELD: resource_id, } for item in items @@ -166,41 +191,38 @@ class MilvusClient(VectorDBBase): if not vectors: return None - mt_collection, resource_id = self._get_collection_and_resource_id( - collection_name - ) + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return None collection = Collection(mt_collection) collection.load() - search_params = {"metric_type": MILVUS_METRIC_TYPE, "params": {}} + search_params = {'metric_type': MILVUS_METRIC_TYPE, 'params': {}} results = collection.search( data=vectors, - anns_field="vector", + anns_field='vector', param=search_params, limit=limit, expr=f"{RESOURCE_ID_FIELD} == '{resource_id}'", - output_fields=["id", "text", "metadata"], + output_fields=['id', 'text', 'metadata'], ) ids, documents, metadatas, distances = [], [], [], [] for hits in results: batch_ids, batch_docs, batch_metadatas, batch_dists = [], [], [], [] for hit in hits: - batch_ids.append(hit.entity.get("id")) - batch_docs.append(hit.entity.get("text")) - batch_metadatas.append(hit.entity.get("metadata")) + batch_ids.append(hit.entity.get('id')) + batch_docs.append(hit.entity.get('text')) + batch_metadatas.append(hit.entity.get('metadata')) batch_dists.append(hit.distance) ids.append(batch_ids) documents.append(batch_docs) metadatas.append(batch_metadatas) distances.append(batch_dists) - return SearchResult( - ids=ids, documents=documents, metadatas=metadatas, distances=distances - ) + return SearchResult(ids=ids, documents=documents, metadatas=metadatas, distances=distances) def delete( self, @@ -208,26 +230,25 @@ class MilvusClient(VectorDBBase): ids: Optional[List[str]] = None, filter: Optional[Dict[str, Any]] = None, ): - mt_collection, resource_id = self._get_collection_and_resource_id( - collection_name - ) + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return collection = Collection(mt_collection) - # Build expression expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"] if ids: # Milvus expects a string list for 'in' operator - id_list_str = ", ".join([f"'{id_val}'" for id_val in ids]) - expr.append(f"id in [{id_list_str}]") + id_list_str = ', '.join([f"'{_escape_milvus_string(str(id_val))}'" for id_val in ids]) + expr.append(f'id in [{id_list_str}]') if filter: for key, value in filter.items(): - expr.append(f"metadata['{key}'] == '{value}'") + _validate_metadata_key(key) + expr.append(f"metadata['{key}'] == '{_escape_milvus_string(str(value))}'") - collection.delete(" and ".join(expr)) + collection.delete(' and '.join(expr)) def reset(self): for collection_name in self.shared_collections: @@ -235,21 +256,17 @@ class MilvusClient(VectorDBBase): utility.drop_collection(collection_name) def delete_collection(self, collection_name: str): - mt_collection, resource_id = self._get_collection_and_resource_id( - collection_name - ) + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return collection = Collection(mt_collection) collection.delete(f"{RESOURCE_ID_FIELD} == '{resource_id}'") - def query( - self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None - ) -> Optional[GetResult]: - mt_collection, resource_id = self._get_collection_and_resource_id( - collection_name - ) + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: + mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return None @@ -259,14 +276,21 @@ class MilvusClient(VectorDBBase): expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"] if filter: for key, value in filter.items(): + _validate_metadata_key(key) if isinstance(value, str): - expr.append(f"metadata['{key}'] == '{value}'") - else: + expr.append(f"metadata['{key}'] == '{_escape_milvus_string(value)}'") + elif isinstance(value, bool): + expr.append(f"metadata['{key}'] == {str(value).lower()}") + elif isinstance(value, (int, float)): expr.append(f"metadata['{key}'] == {value}") + else: + raise TypeError( + f'Unsupported Milvus filter value type for key {key!r}: {type(value).__name__}' + ) iterator = collection.query_iterator( - expr=" and ".join(expr), - output_fields=["id", "text", "metadata"], + expr=' and '.join(expr), + output_fields=['id', 'text', 'metadata'], limit=limit if limit else -1, ) @@ -278,9 +302,9 @@ class MilvusClient(VectorDBBase): break all_results.extend(batch) - ids = [res["id"] for res in all_results] - documents = [res["text"] for res in all_results] - metadatas = [res["metadata"] for res in all_results] + ids = [res['id'] for res in all_results] + documents = [res['text'] for res in all_results] + metadatas = [res['metadata'] for res in all_results] return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) diff --git a/backend/open_webui/retrieval/vector/dbs/opengauss.py b/backend/open_webui/retrieval/vector/dbs/opengauss.py index 7d4f9ea092..1c9d35253c 100644 --- a/backend/open_webui/retrieval/vector/dbs/opengauss.py +++ b/backend/open_webui/retrieval/vector/dbs/opengauss.py @@ -1,48 +1,49 @@ -from typing import Optional, List, Dict, Any +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +import json import logging import re -import json +from typing import Any, Dict, List, Optional + +from pgvector.sqlalchemy import Vector from sqlalchemy import ( - func, - literal, + Column, + Integer, + LargeBinary, + MetaData, + Table, + Text, cast, column, create_engine, - Column, - Integer, - MetaData, - LargeBinary, + func, + literal, select, text, - Text, - Table, values, ) -from sqlalchemy.sql import true -from sqlalchemy.pool import NullPool, QueuePool - -from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker -from sqlalchemy.dialects.postgresql import JSONB, array -from pgvector.sqlalchemy import Vector -from sqlalchemy.ext.mutable import MutableDict -from sqlalchemy.exc import NoSuchTableError - -from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2 from sqlalchemy.dialects import registry +from sqlalchemy.dialects.postgresql import JSONB, array +from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2 +from sqlalchemy.exc import NoSuchTableError +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker +from sqlalchemy.pool import NullPool, QueuePool +from sqlalchemy.sql import true class OpenGaussDialect(PGDialect_psycopg2): - name = "opengauss" + name = 'opengauss' def _get_server_version_info(self, connection): try: - version = connection.exec_driver_sql("SELECT version()").scalar() + version = connection.exec_driver_sql('SELECT version()').scalar() if not version: return (9, 0, 0) - match = re.search( - r"openGauss\s+(\d+)\.(\d+)\.(\d+)(?:-\w+)?", version, re.IGNORECASE - ) + match = re.search(r'openGauss\s+(\d+)\.(\d+)\.(\d+)(?:-\w+)?', version, re.IGNORECASE) if match: return (int(match.group(1)), int(match.group(2)), int(match.group(3))) @@ -52,35 +53,34 @@ class OpenGaussDialect(PGDialect_psycopg2): # Register dialect -registry.register("opengauss", __name__, "OpenGaussDialect") +registry.register('opengauss', __name__, 'OpenGaussDialect') -from open_webui.retrieval.vector.utils import process_metadata -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) from open_webui.config import ( OPENGAUSS_DB_URL, OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH, - OPENGAUSS_POOL_SIZE, OPENGAUSS_POOL_MAX_OVERFLOW, - OPENGAUSS_POOL_TIMEOUT, OPENGAUSS_POOL_RECYCLE, + OPENGAUSS_POOL_SIZE, + OPENGAUSS_POOL_TIMEOUT, ) - from open_webui.env import SRC_LOG_LEVELS +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH Base = declarative_base() log = logging.getLogger(__name__) -log.setLevel(SRC_LOG_LEVELS["RAG"]) +log.setLevel(SRC_LOG_LEVELS['RAG']) class DocumentChunk(Base): - __tablename__ = "document_chunk" + __tablename__ = 'document_chunk' id = Column(Text, primary_key=True) vector = Column(Vector(dim=VECTOR_LENGTH), nullable=True) @@ -96,26 +96,24 @@ class OpenGaussClient(VectorDBBase): self.session = ScopedSession else: - engine_kwargs = {"pool_pre_ping": True, "dialect": OpenGaussDialect()} + engine_kwargs = {'pool_pre_ping': True, 'dialect': OpenGaussDialect()} if isinstance(OPENGAUSS_POOL_SIZE, int) and OPENGAUSS_POOL_SIZE > 0: engine_kwargs.update( { - "pool_size": OPENGAUSS_POOL_SIZE, - "max_overflow": OPENGAUSS_POOL_MAX_OVERFLOW, - "pool_timeout": OPENGAUSS_POOL_TIMEOUT, - "pool_recycle": OPENGAUSS_POOL_RECYCLE, - "poolclass": QueuePool, + 'pool_size': OPENGAUSS_POOL_SIZE, + 'max_overflow': OPENGAUSS_POOL_MAX_OVERFLOW, + 'pool_timeout': OPENGAUSS_POOL_TIMEOUT, + 'pool_recycle': OPENGAUSS_POOL_RECYCLE, + 'poolclass': QueuePool, } ) else: - engine_kwargs["poolclass"] = NullPool + engine_kwargs['poolclass'] = NullPool engine = create_engine(OPENGAUSS_DB_URL, **engine_kwargs) - SessionLocal = sessionmaker( - autocommit=False, autoflush=False, bind=engine, expire_on_commit=False - ) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) self.session = scoped_session(SessionLocal) try: @@ -124,47 +122,42 @@ class OpenGaussClient(VectorDBBase): self.session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_document_chunk_vector " - "ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);" + 'CREATE INDEX IF NOT EXISTS idx_document_chunk_vector ' + 'ON document_chunk USING ivfflat (vector vector_cosine_ops) WITH (lists = 100);' ) ) self.session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name " - "ON document_chunk (collection_name);" + 'CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name ON document_chunk (collection_name);' ) ) self.session.commit() - log.info("OpenGauss vector database initialization completed.") + log.info('OpenGauss vector database initialization completed.') except Exception as e: self.session.rollback() - log.exception(f"OpenGauss Initialization failed.: {e}") + log.exception(f'OpenGauss Initialization failed.: {e}') raise def check_vector_length(self) -> None: metadata = MetaData() try: - document_chunk_table = Table( - "document_chunk", metadata, autoload_with=self.session.bind - ) + document_chunk_table = Table('document_chunk', metadata, autoload_with=self.session.bind) except NoSuchTableError: return - if "vector" in document_chunk_table.columns: - vector_column = document_chunk_table.columns["vector"] + if 'vector' in document_chunk_table.columns: + vector_column = document_chunk_table.columns['vector'] vector_type = vector_column.type if isinstance(vector_type, Vector): db_vector_length = vector_type.dim if db_vector_length != VECTOR_LENGTH: raise Exception( - f"Vector dimension mismatch: configured {VECTOR_LENGTH} vs. {db_vector_length} in the database." + f'Vector dimension mismatch: configured {VECTOR_LENGTH} vs. {db_vector_length} in the database.' ) else: raise Exception("The 'vector' column type is not Vector.") else: - raise Exception( - "The 'vector' column does not exist in the 'document_chunk' table." - ) + raise Exception("The 'vector' column does not exist in the 'document_chunk' table.") def adjust_vector_length(self, vector: List[float]) -> List[float]: current_length = len(vector) @@ -178,55 +171,47 @@ class OpenGaussClient(VectorDBBase): try: new_items = [] for item in items: - vector = self.adjust_vector_length(item["vector"]) + vector = self.adjust_vector_length(item['vector']) new_chunk = DocumentChunk( - id=item["id"], + id=item['id'], vector=vector, collection_name=collection_name, - text=item["text"], - vmetadata=process_metadata(item["metadata"]), + text=item['text'], + vmetadata=process_metadata(item['metadata']), ) new_items.append(new_chunk) self.session.bulk_save_objects(new_items) self.session.commit() - log.info( - f"Inserting {len(new_items)} items into collection '{collection_name}'." - ) + log.info(f"Inserting {len(new_items)} items into collection '{collection_name}'.") except Exception as e: self.session.rollback() - log.exception(f"Failed to insert data: {e}") + log.exception(f'Failed to insert data: {e}') raise def upsert(self, collection_name: str, items: List[VectorItem]) -> None: try: for item in items: - vector = self.adjust_vector_length(item["vector"]) - existing = ( - self.session.query(DocumentChunk) - .filter(DocumentChunk.id == item["id"]) - .first() - ) + vector = self.adjust_vector_length(item['vector']) + existing = self.session.query(DocumentChunk).filter(DocumentChunk.id == item['id']).first() if existing: existing.vector = vector - existing.text = item["text"] - existing.vmetadata = process_metadata(item["metadata"]) + existing.text = item['text'] + existing.vmetadata = process_metadata(item['metadata']) existing.collection_name = collection_name else: new_chunk = DocumentChunk( - id=item["id"], + id=item['id'], vector=vector, collection_name=collection_name, - text=item["text"], - vmetadata=process_metadata(item["metadata"]), + text=item['text'], + vmetadata=process_metadata(item['metadata']), ) self.session.add(new_chunk) self.session.commit() - log.info( - f"Inserting/updating {len(items)} items in collection '{collection_name}'." - ) + log.info(f"Inserting/updating {len(items)} items in collection '{collection_name}'.") except Exception as e: self.session.rollback() - log.exception(f"Failed to insert or update data.: {e}") + log.exception(f'Failed to insert or update data.: {e}') raise def search( @@ -246,35 +231,29 @@ class OpenGaussClient(VectorDBBase): def vector_expr(vector): return cast(array(vector), Vector(VECTOR_LENGTH)) - qid_col = column("qid", Integer) - q_vector_col = column("q_vector", Vector(VECTOR_LENGTH)) + qid_col = column('qid', Integer) + q_vector_col = column('q_vector', Vector(VECTOR_LENGTH)) query_vectors = ( values(qid_col, q_vector_col) - .data( - [(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)] - ) - .alias("query_vectors") + .data([(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]) + .alias('query_vectors') ) result_fields = [ DocumentChunk.id, DocumentChunk.text, DocumentChunk.vmetadata, - (DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label( - "distance" - ), + (DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label('distance'), ] subq = ( select(*result_fields) .where(DocumentChunk.collection_name == collection_name) - .order_by( - DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector) - ) + .order_by(DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)) ) if limit is not None: subq = subq.limit(limit) - subq = subq.lateral("result") + subq = subq.lateral('result') stmt = ( select( @@ -305,21 +284,15 @@ class OpenGaussClient(VectorDBBase): metadatas[qid].append(row.vmetadata) self.session.rollback() - return SearchResult( - ids=ids, distances=distances, documents=documents, metadatas=metadatas - ) + return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas) except Exception as e: self.session.rollback() - log.exception(f"Vector search failed: {e}") + log.exception(f'Vector search failed: {e}') return None - def query( - self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: try: - query = self.session.query(DocumentChunk).filter( - DocumentChunk.collection_name == collection_name - ) + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) for key, value in filter.items(): query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) @@ -340,16 +313,12 @@ class OpenGaussClient(VectorDBBase): return GetResult(ids=ids, documents=documents, metadatas=metadatas) except Exception as e: self.session.rollback() - log.exception(f"Conditional query failed: {e}") + log.exception(f'Conditional query failed: {e}') return None - def get( - self, collection_name: str, limit: Optional[int] = None - ) -> Optional[GetResult]: + def get(self, collection_name: str, limit: Optional[int] = None) -> Optional[GetResult]: try: - query = self.session.query(DocumentChunk).filter( - DocumentChunk.collection_name == collection_name - ) + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) if limit is not None: query = query.limit(limit) @@ -366,7 +335,7 @@ class OpenGaussClient(VectorDBBase): return GetResult(ids=ids, documents=documents, metadatas=metadatas) except Exception as e: self.session.rollback() - log.exception(f"Failed to retrieve data: {e}") + log.exception(f'Failed to retrieve data: {e}') return None def delete( @@ -376,32 +345,28 @@ class OpenGaussClient(VectorDBBase): filter: Optional[Dict[str, Any]] = None, ) -> None: try: - query = self.session.query(DocumentChunk).filter( - DocumentChunk.collection_name == collection_name - ) + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) if ids: query = query.filter(DocumentChunk.id.in_(ids)) if filter: for key, value in filter.items(): - query = query.filter( - DocumentChunk.vmetadata[key].astext == str(value) - ) + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) deleted = query.delete(synchronize_session=False) self.session.commit() log.info(f"Deleted {deleted} items from collection '{collection_name}'") except Exception as e: self.session.rollback() - log.exception(f"Failed to delete data: {e}") + log.exception(f'Failed to delete data: {e}') raise def reset(self) -> None: try: deleted = self.session.query(DocumentChunk).delete() self.session.commit() - log.info(f"Reset completed. Deleted {deleted} items") + log.info(f'Reset completed. Deleted {deleted} items') except Exception as e: self.session.rollback() - log.exception(f"Reset failed: {e}") + log.exception(f'Reset failed: {e}') raise def close(self) -> None: @@ -410,16 +375,14 @@ class OpenGaussClient(VectorDBBase): def has_collection(self, collection_name: str) -> bool: try: exists = ( - self.session.query(DocumentChunk) - .filter(DocumentChunk.collection_name == collection_name) - .first() + self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name).first() is not None ) self.session.rollback() return exists except Exception as e: self.session.rollback() - log.exception(f"Failed to check collection existence: {e}") + log.exception(f'Failed to check collection existence: {e}') return False def delete_collection(self, collection_name: str) -> None: diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index dc9c35805e..798802cd54 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -1,26 +1,30 @@ -from opensearchpy import OpenSearch -from opensearchpy.helpers import bulk +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + from typing import Optional -from open_webui.retrieval.vector.utils import process_metadata +from open_webui.config import ( + OPENSEARCH_CERT_VERIFY, + OPENSEARCH_PASSWORD, + OPENSEARCH_SSL, + OPENSEARCH_URI, + OPENSEARCH_USERNAME, +) from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, VectorDBBase, VectorItem, - SearchResult, - GetResult, -) -from open_webui.config import ( - OPENSEARCH_URI, - OPENSEARCH_SSL, - OPENSEARCH_CERT_VERIFY, - OPENSEARCH_USERNAME, - OPENSEARCH_PASSWORD, ) +from open_webui.retrieval.vector.utils import process_metadata +from opensearchpy import OpenSearch +from opensearchpy.helpers import bulk class OpenSearchClient(VectorDBBase): def __init__(self): - self.index_prefix = "open_webui" + self.index_prefix = 'open_webui' self.client = OpenSearch( hosts=[OPENSEARCH_URI], use_ssl=OPENSEARCH_SSL, @@ -29,25 +33,25 @@ class OpenSearchClient(VectorDBBase): ) def _get_index_name(self, collection_name: str) -> str: - return f"{self.index_prefix}_{collection_name}" + return f'{self.index_prefix}_{collection_name}' def _result_to_get_result(self, result) -> GetResult: - if not result["hits"]["hits"]: + if not result['hits']['hits']: return None ids = [] documents = [] metadatas = [] - for hit in result["hits"]["hits"]: - ids.append(hit["_id"]) - documents.append(hit["_source"].get("text")) - metadatas.append(hit["_source"].get("metadata")) + for hit in result['hits']['hits']: + ids.append(hit['_id']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) def _result_to_search_result(self, result) -> SearchResult: - if not result["hits"]["hits"]: + if not result['hits']['hits']: return None ids = [] @@ -55,11 +59,11 @@ class OpenSearchClient(VectorDBBase): documents = [] metadatas = [] - for hit in result["hits"]["hits"]: - ids.append(hit["_id"]) - distances.append(hit["_score"]) - documents.append(hit["_source"].get("text")) - metadatas.append(hit["_source"].get("metadata")) + for hit in result['hits']['hits']: + ids.append(hit['_id']) + distances.append(hit['_score']) + documents.append(hit['_source'].get('text')) + metadatas.append(hit['_source'].get('metadata')) return SearchResult( ids=[ids], @@ -70,33 +74,31 @@ class OpenSearchClient(VectorDBBase): def _create_index(self, collection_name: str, dimension: int): body = { - "settings": {"index": {"knn": True}}, - "mappings": { - "properties": { - "id": {"type": "keyword"}, - "vector": { - "type": "knn_vector", - "dimension": dimension, # Adjust based on your vector dimensions - "index": True, - "similarity": "faiss", - "method": { - "name": "hnsw", - "space_type": "innerproduct", # Use inner product to approximate cosine similarity - "engine": "faiss", - "parameters": { - "ef_construction": 128, - "m": 16, + 'settings': {'index': {'knn': True}}, + 'mappings': { + 'properties': { + 'id': {'type': 'keyword'}, + 'vector': { + 'type': 'knn_vector', + 'dimension': dimension, # Adjust based on your vector dimensions + 'index': True, + 'similarity': 'faiss', + 'method': { + 'name': 'hnsw', + 'space_type': 'innerproduct', # Use inner product to approximate cosine similarity + 'engine': 'faiss', + 'parameters': { + 'ef_construction': 128, + 'm': 16, }, }, }, - "text": {"type": "text"}, - "metadata": {"type": "object"}, + 'text': {'type': 'text'}, + 'metadata': {'type': 'object'}, } }, } - self.client.indices.create( - index=self._get_index_name(collection_name), body=body - ) + self.client.indices.create(index=self._get_index_name(collection_name), body=body) def _create_batches(self, items: list[VectorItem], batch_size=100): for i in range(0, len(items), batch_size): @@ -124,46 +126,40 @@ class OpenSearchClient(VectorDBBase): return None query = { - "size": limit, - "_source": ["text", "metadata"], - "query": { - "script_score": { - "query": {"match_all": {}}, - "script": { - "source": "(cosineSimilarity(params.query_value, doc[params.field]) + 1.0) / 2.0", - "params": { - "field": "vector", - "query_value": vectors[0], + 'size': limit, + '_source': ['text', 'metadata'], + 'query': { + 'script_score': { + 'query': {'match_all': {}}, + 'script': { + 'source': '(cosineSimilarity(params.query_value, doc[params.field]) + 1.0) / 2.0', + 'params': { + 'field': 'vector', + 'query_value': vectors[0], }, # Assuming single query vector }, } }, } - result = self.client.search( - index=self._get_index_name(collection_name), body=query - ) + result = self.client.search(index=self._get_index_name(collection_name), body=query) return self._result_to_search_result(result) except Exception as e: return None - def query( - self, collection_name: str, filter: dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: dict, limit: Optional[int] = None) -> Optional[GetResult]: if not self.has_collection(collection_name): return None query_body = { - "query": {"bool": {"filter": []}}, - "_source": ["text", "metadata"], + 'query': {'bool': {'filter': []}}, + '_source': ['text', 'metadata'], } for field, value in filter.items(): - query_body["query"]["bool"]["filter"].append( - {"term": {"metadata." + str(field) + ".keyword": value}} - ) + query_body['query']['bool']['filter'].append({'term': {'metadata.' + str(field) + '.keyword': value}}) size = limit if limit else 10000 @@ -184,57 +180,51 @@ class OpenSearchClient(VectorDBBase): self._create_index(collection_name, dimension) def get(self, collection_name: str) -> Optional[GetResult]: - query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]} + query = {'query': {'match_all': {}}, '_source': ['text', 'metadata']} - result = self.client.search( - index=self._get_index_name(collection_name), body=query - ) + result = self.client.search(index=self._get_index_name(collection_name), body=query) return self._result_to_get_result(result) def insert(self, collection_name: str, items: list[VectorItem]): - self._create_index_if_not_exists( - collection_name=collection_name, dimension=len(items[0]["vector"]) - ) + self._create_index_if_not_exists(collection_name=collection_name, dimension=len(items[0]['vector'])) for batch in self._create_batches(items): actions = [ { - "_op_type": "index", - "_index": self._get_index_name(collection_name), - "_id": item["id"], - "_source": { - "vector": item["vector"], - "text": item["text"], - "metadata": process_metadata(item["metadata"]), + '_op_type': 'index', + '_index': self._get_index_name(collection_name), + '_id': item['id'], + '_source': { + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), }, } for item in batch ] bulk(self.client, actions) - self.client.indices.refresh(self._get_index_name(collection_name)) + self.client.indices.refresh(index=self._get_index_name(collection_name)) def upsert(self, collection_name: str, items: list[VectorItem]): - self._create_index_if_not_exists( - collection_name=collection_name, dimension=len(items[0]["vector"]) - ) + self._create_index_if_not_exists(collection_name=collection_name, dimension=len(items[0]['vector'])) for batch in self._create_batches(items): actions = [ { - "_op_type": "update", - "_index": self._get_index_name(collection_name), - "_id": item["id"], - "doc": { - "vector": item["vector"], - "text": item["text"], - "metadata": process_metadata(item["metadata"]), + '_op_type': 'update', + '_index': self._get_index_name(collection_name), + '_id': item['id'], + 'doc': { + 'vector': item['vector'], + 'text': item['text'], + 'metadata': process_metadata(item['metadata']), }, - "doc_as_upsert": True, + 'doc_as_upsert': True, } for item in batch ] bulk(self.client, actions) - self.client.indices.refresh(self._get_index_name(collection_name)) + self.client.indices.refresh(index=self._get_index_name(collection_name)) def delete( self, @@ -245,27 +235,23 @@ class OpenSearchClient(VectorDBBase): if ids: actions = [ { - "_op_type": "delete", - "_index": self._get_index_name(collection_name), - "_id": id, + '_op_type': 'delete', + '_index': self._get_index_name(collection_name), + '_id': id, } for id in ids ] bulk(self.client, actions) elif filter: query_body = { - "query": {"bool": {"filter": []}}, + 'query': {'bool': {'filter': []}}, } for field, value in filter.items(): - query_body["query"]["bool"]["filter"].append( - {"term": {"metadata." + str(field) + ".keyword": value}} - ) - self.client.delete_by_query( - index=self._get_index_name(collection_name), body=query_body - ) - self.client.indices.refresh(self._get_index_name(collection_name)) + query_body['query']['bool']['filter'].append({'term': {'metadata.' + str(field) + '.keyword': value}}) + self.client.delete_by_query(index=self._get_index_name(collection_name), body=query_body) + self.client.indices.refresh(index=self._get_index_name(collection_name)) def reset(self): - indices = self.client.indices.get(index=f"{self.index_prefix}_*") + indices = self.client.indices.get(index=f'{self.index_prefix}_*') for index in indices: self.client.indices.delete(index=index) diff --git a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py index 9f16f82bc9..b09eacb81d 100644 --- a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py +++ b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py @@ -1,4 +1,6 @@ """ +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. + Oracle 23ai Vector Database Client - Fixed Version # .env @@ -26,34 +28,33 @@ ORACLE_DB_POOL_MAX = 10 ORACLE_DB_POOL_INCREMENT = 1 """ -from typing import Optional, List, Dict, Any, Union -from decimal import Decimal +import array +import json import logging import os import threading import time -import json -import array +from decimal import Decimal +from typing import Any, Dict, List, Optional, Union + import oracledb - -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) - from open_webui.config import ( + ORACLE_DB_DSN, + ORACLE_DB_PASSWORD, + ORACLE_DB_POOL_INCREMENT, + ORACLE_DB_POOL_MAX, + ORACLE_DB_POOL_MIN, ORACLE_DB_USE_WALLET, ORACLE_DB_USER, - ORACLE_DB_PASSWORD, - ORACLE_DB_DSN, + ORACLE_VECTOR_LENGTH, ORACLE_WALLET_DIR, ORACLE_WALLET_PASSWORD, - ORACLE_VECTOR_LENGTH, - ORACLE_DB_POOL_MIN, - ORACLE_DB_POOL_MAX, - ORACLE_DB_POOL_INCREMENT, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, ) log = logging.getLogger(__name__) @@ -92,15 +93,15 @@ class Oracle23aiClient(VectorDBBase): self._create_dbcs_pool() dsn = ORACLE_DB_DSN - log.info(f"Creating Connection Pool [{ORACLE_DB_USER}:**@{dsn}]") + log.info(f'Creating Connection Pool [{ORACLE_DB_USER}:**@{dsn}]') with self.get_connection() as connection: - log.info(f"Connection version: {connection.version}") + log.info(f'Connection version: {connection.version}') self._initialize_database(connection) - log.info("Oracle Vector Search initialization complete.") + log.info('Oracle Vector Search initialization complete.') except Exception as e: - log.exception(f"Error during Oracle Vector Search initialization: {e}") + log.exception(f'Error during Oracle Vector Search initialization: {e}') raise def _create_adb_pool(self) -> None: @@ -120,7 +121,7 @@ class Oracle23aiClient(VectorDBBase): wallet_location=ORACLE_WALLET_DIR, wallet_password=ORACLE_WALLET_PASSWORD, ) - log.info("Created ADB connection pool with wallet authentication.") + log.info('Created ADB connection pool with wallet authentication.') def _create_dbcs_pool(self) -> None: """ @@ -136,7 +137,7 @@ class Oracle23aiClient(VectorDBBase): max=ORACLE_DB_POOL_MAX, increment=ORACLE_DB_POOL_INCREMENT, ) - log.info("Created DB connection pool with basic authentication.") + log.info('Created DB connection pool with basic authentication.') def get_connection(self): """ @@ -153,13 +154,11 @@ class Oracle23aiClient(VectorDBBase): return connection except oracledb.DatabaseError as e: (error_obj,) = e.args - log.exception( - f"Connection attempt {attempt + 1} failed: {error_obj.message}" - ) + log.exception(f'Connection attempt {attempt + 1} failed: {error_obj.message}') if attempt < max_retries - 1: wait_time = 2**attempt - log.info(f"Retrying in {wait_time} seconds...") + log.info(f'Retrying in {wait_time} seconds...') time.sleep(wait_time) else: raise @@ -175,30 +174,30 @@ class Oracle23aiClient(VectorDBBase): def _monitor(): while True: try: - log.info("[HealthCheck] Running periodic DB health check...") + log.info('[HealthCheck] Running periodic DB health check...') self.ensure_connection() - log.info("[HealthCheck] Connection is healthy.") + log.info('[HealthCheck] Connection is healthy.') except Exception as e: - log.exception(f"[HealthCheck] Connection health check failed: {e}") + log.exception(f'[HealthCheck] Connection health check failed: {e}') time.sleep(interval_seconds) thread = threading.Thread(target=_monitor, daemon=True) thread.start() - log.info(f"Started DB health monitor every {interval_seconds} seconds.") + log.info(f'Started DB health monitor every {interval_seconds} seconds.') def _reconnect_pool(self): """ Attempt to reinitialize the connection pool if it's been closed or broken. """ try: - log.info("Attempting to reinitialize the Oracle connection pool...") + log.info('Attempting to reinitialize the Oracle connection pool...') # Close existing pool if it exists if self.pool: try: self.pool.close() except Exception as close_error: - log.warning(f"Error closing existing pool: {close_error}") + log.warning(f'Error closing existing pool: {close_error}') # Re-create the appropriate connection pool based on DB type if ORACLE_DB_USE_WALLET: @@ -206,9 +205,9 @@ class Oracle23aiClient(VectorDBBase): else: # DBCS self._create_dbcs_pool() - log.info("Connection pool reinitialized.") + log.info('Connection pool reinitialized.') except Exception as e: - log.exception(f"Failed to reinitialize the connection pool: {e}") + log.exception(f'Failed to reinitialize the connection pool: {e}') raise def ensure_connection(self): @@ -218,11 +217,9 @@ class Oracle23aiClient(VectorDBBase): try: with self.get_connection() as connection: with connection.cursor() as cursor: - cursor.execute("SELECT 1 FROM dual") + cursor.execute('SELECT 1 FROM dual') except Exception as e: - log.exception( - f"Connection check failed: {e}, attempting to reconnect pool..." - ) + log.exception(f'Connection check failed: {e}, attempting to reconnect pool...') self._reconnect_pool() def _output_type_handler(self, cursor, metadata): @@ -237,9 +234,7 @@ class Oracle23aiClient(VectorDBBase): A variable with appropriate conversion for vector types """ if metadata.type_code is oracledb.DB_TYPE_VECTOR: - return cursor.var( - metadata.type_code, arraysize=cursor.arraysize, outconverter=list - ) + return cursor.var(metadata.type_code, arraysize=cursor.arraysize, outconverter=list) def _initialize_database(self, connection) -> None: """ @@ -255,7 +250,7 @@ class Oracle23aiClient(VectorDBBase): """ with connection.cursor() as cursor: try: - log.info("Creating Table document_chunk") + log.info('Creating Table document_chunk') cursor.execute( """ BEGIN @@ -277,7 +272,7 @@ class Oracle23aiClient(VectorDBBase): """ ) - log.info("Creating Index document_chunk_collection_name_idx") + log.info('Creating Index document_chunk_collection_name_idx') cursor.execute( """ BEGIN @@ -294,7 +289,7 @@ class Oracle23aiClient(VectorDBBase): """ ) - log.info("Creating VECTOR INDEX document_chunk_vector_ivf_idx") + log.info('Creating VECTOR INDEX document_chunk_vector_ivf_idx') cursor.execute( """ BEGIN @@ -316,11 +311,11 @@ class Oracle23aiClient(VectorDBBase): ) connection.commit() - log.info("Database initialization completed successfully.") + log.info('Database initialization completed successfully.') except Exception as e: connection.rollback() - log.exception(f"Error during database initialization: {e}") + log.exception(f'Error during database initialization: {e}') raise def check_vector_length(self) -> None: @@ -342,7 +337,7 @@ class Oracle23aiClient(VectorDBBase): Returns: bytes: The vector in Oracle BLOB format """ - return array.array("f", vector) + return array.array('f', vector) def adjust_vector_length(self, vector: List[float]) -> List[float]: """ @@ -371,7 +366,7 @@ class Oracle23aiClient(VectorDBBase): """ if isinstance(obj, Decimal): return float(obj) - raise TypeError(f"{obj} is not JSON serializable") + raise TypeError(f'{obj} is not JSON serializable') def _metadata_to_json(self, metadata: Dict) -> str: """ @@ -383,7 +378,7 @@ class Oracle23aiClient(VectorDBBase): Returns: str: JSON representation of metadata """ - return json.dumps(metadata, default=self._decimal_handler) if metadata else "{}" + return json.dumps(metadata, default=self._decimal_handler) if metadata else '{}' def _json_to_metadata(self, json_str: str) -> Dict: """ @@ -422,8 +417,8 @@ class Oracle23aiClient(VectorDBBase): try: with connection.cursor() as cursor: for item in items: - vector_blob = self._vector_to_blob(item["vector"]) - metadata_json = self._metadata_to_json(item["metadata"]) + vector_blob = self._vector_to_blob(item['vector']) + metadata_json = self._metadata_to_json(item['metadata']) cursor.execute( """ @@ -432,22 +427,20 @@ class Oracle23aiClient(VectorDBBase): VALUES (:id, :collection_name, :text, :metadata, :vector) """, { - "id": item["id"], - "collection_name": collection_name, - "text": item["text"], - "metadata": metadata_json, - "vector": vector_blob, + 'id': item['id'], + 'collection_name': collection_name, + 'text': item['text'], + 'metadata': metadata_json, + 'vector': vector_blob, }, ) connection.commit() - log.info( - f"Successfully inserted {len(items)} items into collection '{collection_name}'." - ) + log.info(f"Successfully inserted {len(items)} items into collection '{collection_name}'.") except Exception as e: connection.rollback() - log.exception(f"Error during insert: {e}") + log.exception(f'Error during insert: {e}') raise def upsert(self, collection_name: str, items: List[VectorItem]) -> None: @@ -478,8 +471,8 @@ class Oracle23aiClient(VectorDBBase): try: with connection.cursor() as cursor: for item in items: - vector_blob = self._vector_to_blob(item["vector"]) - metadata_json = self._metadata_to_json(item["metadata"]) + vector_blob = self._vector_to_blob(item['vector']) + metadata_json = self._metadata_to_json(item['metadata']) cursor.execute( """ @@ -497,27 +490,25 @@ class Oracle23aiClient(VectorDBBase): VALUES (:ins_id, :ins_collection_name, :ins_text, :ins_metadata, :ins_vector) """, { - "merge_id": item["id"], - "upd_collection_name": collection_name, - "upd_text": item["text"], - "upd_metadata": metadata_json, - "upd_vector": vector_blob, - "ins_id": item["id"], - "ins_collection_name": collection_name, - "ins_text": item["text"], - "ins_metadata": metadata_json, - "ins_vector": vector_blob, + 'merge_id': item['id'], + 'upd_collection_name': collection_name, + 'upd_text': item['text'], + 'upd_metadata': metadata_json, + 'upd_vector': vector_blob, + 'ins_id': item['id'], + 'ins_collection_name': collection_name, + 'ins_text': item['text'], + 'ins_metadata': metadata_json, + 'ins_vector': vector_blob, }, ) connection.commit() - log.info( - f"Successfully upserted {len(items)} items into collection '{collection_name}'." - ) + log.info(f"Successfully upserted {len(items)} items into collection '{collection_name}'.") except Exception as e: connection.rollback() - log.exception(f"Error during upsert: {e}") + log.exception(f'Error during upsert: {e}') raise def search( @@ -549,13 +540,11 @@ class Oracle23aiClient(VectorDBBase): ... for i, (id, dist) in enumerate(zip(results.ids[0], results.distances[0])): ... log.info(f"Match {i+1}: id={id}, distance={dist}") """ - log.info( - f"Searching items from collection '{collection_name}' with limit {limit}." - ) + log.info(f"Searching items from collection '{collection_name}' with limit {limit}.") try: if not vectors: - log.warning("No vectors provided for search.") + log.warning('No vectors provided for search.') return None num_queries = len(vectors) @@ -581,9 +570,9 @@ class Oracle23aiClient(VectorDBBase): FETCH APPROX FIRST :limit ROWS ONLY """, { - "query_vector": vector_blob, - "collection_name": collection_name, - "limit": limit, + 'query_vector': vector_blob, + 'collection_name': collection_name, + 'limit': limit, }, ) @@ -591,35 +580,21 @@ class Oracle23aiClient(VectorDBBase): for row in results: ids[qid].append(row[0]) - documents[qid].append( - row[1].read() - if isinstance(row[1], oracledb.LOB) - else str(row[1]) - ) + documents[qid].append(row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1])) # 🔧 FIXED: Parse JSON metadata properly - metadata_str = ( - row[2].read() - if isinstance(row[2], oracledb.LOB) - else row[2] - ) + metadata_str = row[2].read() if isinstance(row[2], oracledb.LOB) else row[2] metadatas[qid].append(self._json_to_metadata(metadata_str)) distances[qid].append(float(row[3])) - log.info( - f"Search completed. Found {sum(len(ids[i]) for i in range(num_queries))} total results." - ) + log.info(f'Search completed. Found {sum(len(ids[i]) for i in range(num_queries))} total results.') - return SearchResult( - ids=ids, distances=distances, documents=documents, metadatas=metadatas - ) + return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas) except Exception as e: - log.exception(f"Error during search: {e}") + log.exception(f'Error during search: {e}') return None - def query( - self, collection_name: str, filter: Dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: """ Query items based on metadata filters. @@ -651,15 +626,15 @@ class Oracle23aiClient(VectorDBBase): WHERE collection_name = :collection_name """ - params = {"collection_name": collection_name} + params = {'collection_name': collection_name} for i, (key, value) in enumerate(filter.items()): - param_name = f"value_{i}" + param_name = f'value_{i}' query += f" AND JSON_VALUE(vmetadata, '$.{key}' RETURNING VARCHAR2(4096)) = :{param_name}" params[param_name] = str(value) - query += " FETCH FIRST :limit ROWS ONLY" - params["limit"] = limit + query += ' FETCH FIRST :limit ROWS ONLY' + params['limit'] = limit with self.get_connection() as connection: with connection.cursor() as cursor: @@ -667,32 +642,25 @@ class Oracle23aiClient(VectorDBBase): results = cursor.fetchall() if not results: - log.info("No results found for query.") + log.info('No results found for query.') return None ids = [[row[0] for row in results]] - documents = [ - [ - row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) - for row in results - ] - ] + documents = [[row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) for row in results]] # 🔧 FIXED: Parse JSON metadata properly metadatas = [ [ - self._json_to_metadata( - row[2].read() if isinstance(row[2], oracledb.LOB) else row[2] - ) + self._json_to_metadata(row[2].read() if isinstance(row[2], oracledb.LOB) else row[2]) for row in results ] ] - log.info(f"Query completed. Found {len(results)} results.") + log.info(f'Query completed. Found {len(results)} results.') return GetResult(ids=ids, documents=documents, metadatas=metadatas) except Exception as e: - log.exception(f"Error during query: {e}") + log.exception(f'Error during query: {e}') return None def get(self, collection_name: str) -> Optional[GetResult]: @@ -714,9 +682,6 @@ class Oracle23aiClient(VectorDBBase): >>> if results: ... print(f"Retrieved {len(results.ids[0])} documents from collection") """ - log.info( - f"Getting items from collection '{collection_name}' with limit {limit}." - ) try: limit = 1000 # Hardcoded limit for get operation @@ -730,28 +695,21 @@ class Oracle23aiClient(VectorDBBase): WHERE collection_name = :collection_name FETCH FIRST :limit ROWS ONLY """, - {"collection_name": collection_name, "limit": limit}, + {'collection_name': collection_name, 'limit': limit}, ) results = cursor.fetchall() if not results: - log.info("No results found.") + log.info('No results found.') return None ids = [[row[0] for row in results]] - documents = [ - [ - row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) - for row in results - ] - ] + documents = [[row[1].read() if isinstance(row[1], oracledb.LOB) else str(row[1]) for row in results]] # 🔧 FIXED: Parse JSON metadata properly metadatas = [ [ - self._json_to_metadata( - row[2].read() if isinstance(row[2], oracledb.LOB) else row[2] - ) + self._json_to_metadata(row[2].read() if isinstance(row[2], oracledb.LOB) else row[2]) for row in results ] ] @@ -759,7 +717,7 @@ class Oracle23aiClient(VectorDBBase): return GetResult(ids=ids, documents=documents, metadatas=metadatas) except Exception as e: - log.exception(f"Error during get: {e}") + log.exception(f'Error during get: {e}') return None def delete( @@ -791,21 +749,19 @@ class Oracle23aiClient(VectorDBBase): log.info(f"Deleting items from collection '{collection_name}'.") try: - query = ( - "DELETE FROM document_chunk WHERE collection_name = :collection_name" - ) - params = {"collection_name": collection_name} + query = 'DELETE FROM document_chunk WHERE collection_name = :collection_name' + params = {'collection_name': collection_name} if ids: # 🔧 FIXED: Use proper parameterized query to prevent SQL injection - placeholders = ",".join([f":id_{i}" for i in range(len(ids))]) - query += f" AND id IN ({placeholders})" + placeholders = ','.join([f':id_{i}' for i in range(len(ids))]) + query += f' AND id IN ({placeholders})' for i, id_val in enumerate(ids): - params[f"id_{i}"] = id_val + params[f'id_{i}'] = id_val if filter: for i, (key, value) in enumerate(filter.items()): - param_name = f"value_{i}" + param_name = f'value_{i}' query += f" AND JSON_VALUE(vmetadata, '$.{key}' RETURNING VARCHAR2(4096)) = :{param_name}" params[param_name] = str(value) @@ -818,7 +774,7 @@ class Oracle23aiClient(VectorDBBase): log.info(f"Deleted {deleted} items from collection '{collection_name}'.") except Exception as e: - log.exception(f"Error during delete: {e}") + log.exception(f'Error during delete: {e}') raise def reset(self) -> None: @@ -834,21 +790,19 @@ class Oracle23aiClient(VectorDBBase): >>> client = Oracle23aiClient() >>> client.reset() # Warning: Removes all data! """ - log.info("Resetting database - deleting all items.") + log.info('Resetting database - deleting all items.') try: with self.get_connection() as connection: with connection.cursor() as cursor: - cursor.execute("DELETE FROM document_chunk") + cursor.execute('DELETE FROM document_chunk') deleted = cursor.rowcount connection.commit() - log.info( - f"Reset complete. Deleted {deleted} items from 'document_chunk' table." - ) + log.info(f"Reset complete. Deleted {deleted} items from 'document_chunk' table.") except Exception as e: - log.exception(f"Error during reset: {e}") + log.exception(f'Error during reset: {e}') raise def close(self) -> None: @@ -863,11 +817,11 @@ class Oracle23aiClient(VectorDBBase): >>> client.close() """ try: - if hasattr(self, "pool") and self.pool: + if hasattr(self, 'pool') and self.pool: self.pool.close() - log.info("Oracle Vector Search connection pool closed.") + log.info('Oracle Vector Search connection pool closed.') except Exception as e: - log.exception(f"Error closing connection pool: {e}") + log.exception(f'Error closing connection pool: {e}') def has_collection(self, collection_name: str) -> bool: """ @@ -896,7 +850,7 @@ class Oracle23aiClient(VectorDBBase): WHERE collection_name = :collection_name FETCH FIRST 1 ROWS ONLY """, - {"collection_name": collection_name}, + {'collection_name': collection_name}, ) count = cursor.fetchone()[0] @@ -904,7 +858,7 @@ class Oracle23aiClient(VectorDBBase): return count > 0 except Exception as e: - log.exception(f"Error checking collection existence: {e}") + log.exception(f'Error checking collection existence: {e}') return False def delete_collection(self, collection_name: str) -> None: @@ -930,15 +884,13 @@ class Oracle23aiClient(VectorDBBase): DELETE FROM document_chunk WHERE collection_name = :collection_name """, - {"collection_name": collection_name}, + {'collection_name': collection_name}, ) deleted = cursor.rowcount connection.commit() - log.info( - f"Collection '{collection_name}' deleted. Removed {deleted} items." - ) + log.info(f"Collection '{collection_name}' deleted. Removed {deleted} items.") except Exception as e: log.exception(f"Error deleting collection '{collection_name}': {e}") diff --git a/backend/open_webui/retrieval/vector/dbs/pgvector.py b/backend/open_webui/retrieval/vector/dbs/pgvector.py index 15430db114..861d49bc1b 100644 --- a/backend/open_webui/retrieval/vector/dbs/pgvector.py +++ b/backend/open_webui/retrieval/vector/dbs/pgvector.py @@ -1,62 +1,60 @@ -from typing import Optional, List, Dict, Any, Tuple -import logging import json +import logging +from typing import Any, Dict, List, Optional, Tuple + +from open_webui.config import ( + PGVECTOR_CREATE_EXTENSION, + PGVECTOR_DB_URL, + PGVECTOR_HNSW_EF_CONSTRUCTION, + PGVECTOR_HNSW_M, + PGVECTOR_INDEX_METHOD, + PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH, + PGVECTOR_IVFFLAT_LISTS, + PGVECTOR_PGCRYPTO, + PGVECTOR_PGCRYPTO_KEY, + PGVECTOR_POOL_MAX_OVERFLOW, + PGVECTOR_POOL_RECYCLE, + PGVECTOR_POOL_SIZE, + PGVECTOR_POOL_TIMEOUT, + PGVECTOR_USE_HALFVEC, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.utils.misc import sanitize_text_for_db +from pgvector.sqlalchemy import HALFVEC, Vector from sqlalchemy import ( - func, - literal, + Column, + Integer, + LargeBinary, + MetaData, + Table, + Text, cast, column, create_engine, - Column, - Integer, - MetaData, - LargeBinary, + func, + literal, select, text, - Text, - Table, values, ) -from sqlalchemy.sql import true -from sqlalchemy.pool import NullPool, QueuePool - -from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker from sqlalchemy.dialects.postgresql import JSONB, array -from pgvector.sqlalchemy import Vector, HALFVEC -from sqlalchemy.ext.mutable import MutableDict from sqlalchemy.exc import NoSuchTableError - - -from open_webui.retrieval.vector.utils import process_metadata -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) -from open_webui.config import ( - PGVECTOR_DB_URL, - PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH, - PGVECTOR_CREATE_EXTENSION, - PGVECTOR_PGCRYPTO, - PGVECTOR_PGCRYPTO_KEY, - PGVECTOR_POOL_SIZE, - PGVECTOR_POOL_MAX_OVERFLOW, - PGVECTOR_POOL_TIMEOUT, - PGVECTOR_POOL_RECYCLE, - PGVECTOR_INDEX_METHOD, - PGVECTOR_HNSW_M, - PGVECTOR_HNSW_EF_CONSTRUCTION, - PGVECTOR_IVFFLAT_LISTS, - PGVECTOR_USE_HALFVEC, -) - +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker +from sqlalchemy.pool import NullPool, QueuePool +from sqlalchemy.sql import true VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH USE_HALFVEC = PGVECTOR_USE_HALFVEC VECTOR_TYPE_FACTORY = HALFVEC if USE_HALFVEC else Vector -VECTOR_OPCLASS = "halfvec_cosine_ops" if USE_HALFVEC else "vector_cosine_ops" +VECTOR_OPCLASS = 'halfvec_cosine_ops' if USE_HALFVEC else 'vector_cosine_ops' Base = declarative_base() log = logging.getLogger(__name__) @@ -66,12 +64,12 @@ def pgcrypto_encrypt(val, key): return func.pgp_sym_encrypt(val, literal(key)) -def pgcrypto_decrypt(col, key, outtype="text"): +def pgcrypto_decrypt(col, key, outtype='text'): return func.cast(func.pgp_sym_decrypt(col, literal(key)), outtype) class DocumentChunk(Base): - __tablename__ = "document_chunk" + __tablename__ = 'document_chunk' id = Column(Text, primary_key=True) vector = Column(VECTOR_TYPE_FACTORY(dim=VECTOR_LENGTH), nullable=True) @@ -87,7 +85,6 @@ class DocumentChunk(Base): class PgvectorClient(VectorDBBase): def __init__(self) -> None: - # if no pgvector uri, use the existing database connection if not PGVECTOR_DB_URL: from open_webui.internal.db import ScopedSession @@ -106,15 +103,11 @@ class PgvectorClient(VectorDBBase): poolclass=QueuePool, ) else: - engine = create_engine( - PGVECTOR_DB_URL, pool_pre_ping=True, poolclass=NullPool - ) + engine = create_engine(PGVECTOR_DB_URL, pool_pre_ping=True, poolclass=NullPool) else: engine = create_engine(PGVECTOR_DB_URL, pool_pre_ping=True) - SessionLocal = sessionmaker( - autocommit=False, autoflush=False, bind=engine, expire_on_commit=False - ) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False) self.session = scoped_session(SessionLocal) try: @@ -122,38 +115,32 @@ class PgvectorClient(VectorDBBase): # Use a conditional check to avoid permission issues on Azure PostgreSQL if PGVECTOR_CREATE_EXTENSION: self.session.execute( - text( - """ + text(""" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN CREATE EXTENSION IF NOT EXISTS vector; END IF; END $$; - """ - ) + """) ) if PGVECTOR_PGCRYPTO: # Ensure the pgcrypto extension is available for encryption # Use a conditional check to avoid permission issues on Azure PostgreSQL self.session.execute( - text( - """ + text(""" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN CREATE EXTENSION IF NOT EXISTS pgcrypto; END IF; END $$; - """ - ) + """) ) if not PGVECTOR_PGCRYPTO_KEY: - raise ValueError( - "PGVECTOR_PGCRYPTO_KEY must be set when PGVECTOR_PGCRYPTO is enabled." - ) + raise ValueError('PGVECTOR_PGCRYPTO_KEY must be set when PGVECTOR_PGCRYPTO is enabled.') # Check vector length consistency self.check_vector_length() @@ -169,15 +156,14 @@ class PgvectorClient(VectorDBBase): self.session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name " - "ON document_chunk (collection_name);" + 'CREATE INDEX IF NOT EXISTS idx_document_chunk_collection_name ON document_chunk (collection_name);' ) ) self.session.commit() - log.info("Initialization complete.") + log.info('Initialization complete.') except Exception as e: self.session.rollback() - log.exception(f"Error during initialization: {e}") + log.exception(f'Error during initialization: {e}') raise @staticmethod @@ -185,7 +171,7 @@ class PgvectorClient(VectorDBBase): if not index_def: return None try: - after_using = index_def.lower().split("using ", 1)[1] + after_using = index_def.lower().split('using ', 1)[1] return after_using.split()[0] except (IndexError, AttributeError): return None @@ -198,34 +184,32 @@ class PgvectorClient(VectorDBBase): index_method, ) elif USE_HALFVEC: - index_method = "hnsw" + index_method = 'hnsw' log.info( - "VECTOR_LENGTH=%s exceeds 2000; using halfvec column type with hnsw index.", + 'VECTOR_LENGTH=%s exceeds 2000; using halfvec column type with hnsw index.', VECTOR_LENGTH, ) else: - index_method = "ivfflat" + index_method = 'ivfflat' - if index_method == "hnsw": - index_options = f"WITH (m = {PGVECTOR_HNSW_M}, ef_construction = {PGVECTOR_HNSW_EF_CONSTRUCTION})" + if index_method == 'hnsw': + index_options = f'WITH (m = {PGVECTOR_HNSW_M}, ef_construction = {PGVECTOR_HNSW_EF_CONSTRUCTION})' else: - index_options = f"WITH (lists = {PGVECTOR_IVFFLAT_LISTS})" + index_options = f'WITH (lists = {PGVECTOR_IVFFLAT_LISTS})' return index_method, index_options def _ensure_vector_index(self, index_method: str, index_options: str) -> None: - index_name = "idx_document_chunk_vector" + index_name = 'idx_document_chunk_vector' existing_index_def = self.session.execute( - text( - """ + text(""" SELECT indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = 'document_chunk' AND indexname = :index_name - """ - ), - {"index_name": index_name}, + """), + {'index_name': index_name}, ).scalar() existing_method = self._extract_index_method(existing_index_def) @@ -233,23 +217,23 @@ class PgvectorClient(VectorDBBase): raise RuntimeError( f"Existing pgvector index '{index_name}' uses method '{existing_method}' but configuration now " f"requires '{index_method}'. Automatic rebuild is disabled to prevent long-running maintenance. " - "Drop the index manually (optionally after tuning maintenance_work_mem/max_parallel_maintenance_workers) " - "and recreate it with the new method before restarting Open WebUI." + 'Drop the index manually (optionally after tuning maintenance_work_mem/max_parallel_maintenance_workers) ' + 'and recreate it with the new method before restarting Open WebUI.' ) if not existing_index_def: index_sql = ( - f"CREATE INDEX IF NOT EXISTS {index_name} " - f"ON document_chunk USING {index_method} (vector {VECTOR_OPCLASS})" + f'CREATE INDEX IF NOT EXISTS {index_name} ' + f'ON document_chunk USING {index_method} (vector {VECTOR_OPCLASS})' ) if index_options: - index_sql = f"{index_sql} {index_options}" + index_sql = f'{index_sql} {index_options}' self.session.execute(text(index_sql)) log.info( "Ensured vector index '%s' using %s%s.", index_name, index_method, - f" {index_options}" if index_options else "", + f' {index_options}' if index_options else '', ) def check_vector_length(self) -> None: @@ -260,16 +244,14 @@ class PgvectorClient(VectorDBBase): metadata = MetaData() try: # Attempt to reflect the 'document_chunk' table - document_chunk_table = Table( - "document_chunk", metadata, autoload_with=self.session.bind - ) + document_chunk_table = Table('document_chunk', metadata, autoload_with=self.session.bind) except NoSuchTableError: # Table does not exist; no action needed return # Proceed to check the vector column - if "vector" in document_chunk_table.columns: - vector_column = document_chunk_table.columns["vector"] + if 'vector' in document_chunk_table.columns: + vector_column = document_chunk_table.columns['vector'] vector_type = vector_column.type expected_type = HALFVEC if USE_HALFVEC else Vector @@ -279,16 +261,14 @@ class PgvectorClient(VectorDBBase): f"('{expected_type.__name__}') for VECTOR_LENGTH {VECTOR_LENGTH}." ) - db_vector_length = getattr(vector_type, "dim", None) + db_vector_length = getattr(vector_type, 'dim', None) if db_vector_length is not None and db_vector_length != VECTOR_LENGTH: raise Exception( - f"VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. " - "Cannot change vector size after initialization without migrating the data." + f'VECTOR_LENGTH {VECTOR_LENGTH} does not match existing vector column dimension {db_vector_length}. ' + 'Cannot change vector size after initialization without migrating the data.' ) else: - raise Exception( - "The 'vector' column does not exist in the 'document_chunk' table." - ) + raise Exception("The 'vector' column does not exist in the 'document_chunk' table.") def adjust_vector_length(self, vector: List[float]) -> List[float]: # Adjust vector to have length VECTOR_LENGTH @@ -305,13 +285,14 @@ class PgvectorClient(VectorDBBase): try: if PGVECTOR_PGCRYPTO: for item in items: - vector = self.adjust_vector_length(item["vector"]) + vector = self.adjust_vector_length(item['vector']) # Use raw SQL for BYTEA/pgcrypto # Ensure metadata is converted to its JSON text representation - json_metadata = json.dumps(item["metadata"]) + # Sanitize to strip null bytes / surrogates that PostgreSQL cannot store + json_metadata = sanitize_text_for_db(json.dumps(item['metadata'])) + item_text = sanitize_text_for_db(item['text']) self.session.execute( - text( - """ + text(""" INSERT INTO document_chunk (id, vector, collection_name, text, vmetadata) VALUES ( @@ -320,15 +301,14 @@ class PgvectorClient(VectorDBBase): pgp_sym_encrypt(:metadata_text, :key) ) ON CONFLICT (id) DO NOTHING - """ - ), + """), { - "id": item["id"], - "vector": vector, - "collection_name": collection_name, - "text": item["text"], - "metadata_text": json_metadata, - "key": PGVECTOR_PGCRYPTO_KEY, + 'id': item['id'], + 'vector': vector, + 'collection_name': collection_name, + 'text': item_text, + 'metadata_text': json_metadata, + 'key': PGVECTOR_PGCRYPTO_KEY, }, ) self.session.commit() @@ -337,34 +317,33 @@ class PgvectorClient(VectorDBBase): else: new_items = [] for item in items: - vector = self.adjust_vector_length(item["vector"]) + vector = self.adjust_vector_length(item['vector']) new_chunk = DocumentChunk( - id=item["id"], + id=item['id'], vector=vector, collection_name=collection_name, - text=item["text"], - vmetadata=process_metadata(item["metadata"]), + text=item['text'], + vmetadata=process_metadata(item['metadata']), ) new_items.append(new_chunk) self.session.bulk_save_objects(new_items) self.session.commit() - log.info( - f"Inserted {len(new_items)} items into collection '{collection_name}'." - ) + log.info(f"Inserted {len(new_items)} items into collection '{collection_name}'.") except Exception as e: self.session.rollback() - log.exception(f"Error during insert: {e}") + log.exception(f'Error during insert: {e}') raise def upsert(self, collection_name: str, items: List[VectorItem]) -> None: try: if PGVECTOR_PGCRYPTO: for item in items: - vector = self.adjust_vector_length(item["vector"]) - json_metadata = json.dumps(item["metadata"]) + vector = self.adjust_vector_length(item['vector']) + # Sanitize to strip null bytes / surrogates that PostgreSQL cannot store + json_metadata = sanitize_text_for_db(json.dumps(item['metadata'])) + item_text = sanitize_text_for_db(item['text']) self.session.execute( - text( - """ + text(""" INSERT INTO document_chunk (id, vector, collection_name, text, vmetadata) VALUES ( @@ -377,50 +356,41 @@ class PgvectorClient(VectorDBBase): collection_name = EXCLUDED.collection_name, text = EXCLUDED.text, vmetadata = EXCLUDED.vmetadata - """ - ), + """), { - "id": item["id"], - "vector": vector, - "collection_name": collection_name, - "text": item["text"], - "metadata_text": json_metadata, - "key": PGVECTOR_PGCRYPTO_KEY, + 'id': item['id'], + 'vector': vector, + 'collection_name': collection_name, + 'text': item_text, + 'metadata_text': json_metadata, + 'key': PGVECTOR_PGCRYPTO_KEY, }, ) self.session.commit() log.info(f"Encrypted & upserted {len(items)} into '{collection_name}'") else: for item in items: - vector = self.adjust_vector_length(item["vector"]) - existing = ( - self.session.query(DocumentChunk) - .filter(DocumentChunk.id == item["id"]) - .first() - ) + vector = self.adjust_vector_length(item['vector']) + existing = self.session.query(DocumentChunk).filter(DocumentChunk.id == item['id']).first() if existing: existing.vector = vector - existing.text = item["text"] - existing.vmetadata = process_metadata(item["metadata"]) - existing.collection_name = ( - collection_name # Update collection_name if necessary - ) + existing.text = item['text'] + existing.vmetadata = process_metadata(item['metadata']) + existing.collection_name = collection_name # Update collection_name if necessary else: new_chunk = DocumentChunk( - id=item["id"], + id=item['id'], vector=vector, collection_name=collection_name, - text=item["text"], - vmetadata=process_metadata(item["metadata"]), + text=item['text'], + vmetadata=process_metadata(item['metadata']), ) self.session.add(new_chunk) self.session.commit() - log.info( - f"Upserted {len(items)} items into collection '{collection_name}'." - ) + log.info(f"Upserted {len(items)} items into collection '{collection_name}'.") except Exception as e: self.session.rollback() - log.exception(f"Error during upsert: {e}") + log.exception(f'Error during upsert: {e}') raise def search( @@ -442,38 +412,26 @@ class PgvectorClient(VectorDBBase): return cast(array(vector), VECTOR_TYPE_FACTORY(VECTOR_LENGTH)) # Create the values for query vectors - qid_col = column("qid", Integer) - q_vector_col = column("q_vector", VECTOR_TYPE_FACTORY(VECTOR_LENGTH)) + qid_col = column('qid', Integer) + q_vector_col = column('q_vector', VECTOR_TYPE_FACTORY(VECTOR_LENGTH)) query_vectors = ( values(qid_col, q_vector_col) - .data( - [(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)] - ) - .alias("query_vectors") + .data([(idx, vector_expr(vector)) for idx, vector in enumerate(vectors)]) + .alias('query_vectors') ) result_fields = [ DocumentChunk.id, ] if PGVECTOR_PGCRYPTO: + result_fields.append(pgcrypto_decrypt(DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text).label('text')) result_fields.append( - pgcrypto_decrypt( - DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text - ).label("text") - ) - result_fields.append( - pgcrypto_decrypt( - DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB - ).label("vmetadata") + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB).label('vmetadata') ) else: result_fields.append(DocumentChunk.text) result_fields.append(DocumentChunk.vmetadata) - result_fields.append( - (DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label( - "distance" - ) - ) + result_fields.append((DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)).label('distance')) # Build the lateral subquery for each query vector where_clauses = [DocumentChunk.collection_name == collection_name] @@ -481,9 +439,9 @@ class PgvectorClient(VectorDBBase): # Apply metadata filter if provided if filter: for key, value in filter.items(): - if isinstance(value, dict) and "$in" in value: + if isinstance(value, dict) and '$in' in value: # Handle $in operator: {"field": {"$in": [values]}} - in_values = value["$in"] + in_values = value['$in'] if PGVECTOR_PGCRYPTO: where_clauses.append( pgcrypto_decrypt( @@ -493,11 +451,7 @@ class PgvectorClient(VectorDBBase): )[key].astext.in_([str(v) for v in in_values]) ) else: - where_clauses.append( - DocumentChunk.vmetadata[key].astext.in_( - [str(v) for v in in_values] - ) - ) + where_clauses.append(DocumentChunk.vmetadata[key].astext.in_([str(v) for v in in_values])) else: # Handle simple equality: {"field": "value"} if PGVECTOR_PGCRYPTO: @@ -510,20 +464,16 @@ class PgvectorClient(VectorDBBase): == str(value) ) else: - where_clauses.append( - DocumentChunk.vmetadata[key].astext == str(value) - ) + where_clauses.append(DocumentChunk.vmetadata[key].astext == str(value)) subq = ( select(*result_fields) .where(*where_clauses) - .order_by( - (DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector)) - ) + .order_by((DocumentChunk.vector.cosine_distance(query_vectors.c.q_vector))) ) if limit is not None: subq = subq.limit(limit) - subq = subq.lateral("result") + subq = subq.lateral('result') # Build the main query by joining query_vectors and the lateral subquery stmt = ( @@ -565,17 +515,13 @@ class PgvectorClient(VectorDBBase): metadatas[qid].append(row.vmetadata) self.session.rollback() # read-only transaction - return SearchResult( - ids=ids, distances=distances, documents=documents, metadatas=metadatas - ) + return SearchResult(ids=ids, distances=distances, documents=documents, metadatas=metadatas) except Exception as e: self.session.rollback() - log.exception(f"Error during search: {e}") + log.exception(f'Error during search: {e}') return None - def query( - self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None) -> Optional[GetResult]: try: if PGVECTOR_PGCRYPTO: # Build where clause for vmetadata filter @@ -583,32 +529,22 @@ class PgvectorClient(VectorDBBase): for key, value in filter.items(): # decrypt then check key: JSON filter after decryption where_clauses.append( - pgcrypto_decrypt( - DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB - )[key].astext + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB)[key].astext == str(value) ) stmt = select( DocumentChunk.id, - pgcrypto_decrypt( - DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text - ).label("text"), - pgcrypto_decrypt( - DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB - ).label("vmetadata"), + pgcrypto_decrypt(DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text).label('text'), + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB).label('vmetadata'), ).where(*where_clauses) if limit is not None: stmt = stmt.limit(limit) results = self.session.execute(stmt).all() else: - query = self.session.query(DocumentChunk).filter( - DocumentChunk.collection_name == collection_name - ) + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) for key, value in filter.items(): - query = query.filter( - DocumentChunk.vmetadata[key].astext == str(value) - ) + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) if limit is not None: query = query.limit(limit) @@ -630,22 +566,16 @@ class PgvectorClient(VectorDBBase): ) except Exception as e: self.session.rollback() - log.exception(f"Error during query: {e}") + log.exception(f'Error during query: {e}') return None - def get( - self, collection_name: str, limit: Optional[int] = None - ) -> Optional[GetResult]: + def get(self, collection_name: str, limit: Optional[int] = None) -> Optional[GetResult]: try: if PGVECTOR_PGCRYPTO: stmt = select( DocumentChunk.id, - pgcrypto_decrypt( - DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text - ).label("text"), - pgcrypto_decrypt( - DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB - ).label("vmetadata"), + pgcrypto_decrypt(DocumentChunk.text, PGVECTOR_PGCRYPTO_KEY, Text).label('text'), + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB).label('vmetadata'), ).where(DocumentChunk.collection_name == collection_name) if limit is not None: stmt = stmt.limit(limit) @@ -654,10 +584,7 @@ class PgvectorClient(VectorDBBase): documents = [[row.text for row in results]] metadatas = [[row.vmetadata for row in results]] else: - - query = self.session.query(DocumentChunk).filter( - DocumentChunk.collection_name == collection_name - ) + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) if limit is not None: query = query.limit(limit) @@ -674,7 +601,7 @@ class PgvectorClient(VectorDBBase): return GetResult(ids=ids, documents=documents, metadatas=metadatas) except Exception as e: self.session.rollback() - log.exception(f"Error during get: {e}") + log.exception(f'Error during get: {e}') return None def delete( @@ -691,43 +618,35 @@ class PgvectorClient(VectorDBBase): if filter: for key, value in filter.items(): wheres.append( - pgcrypto_decrypt( - DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB - )[key].astext + pgcrypto_decrypt(DocumentChunk.vmetadata, PGVECTOR_PGCRYPTO_KEY, JSONB)[key].astext == str(value) ) stmt = DocumentChunk.__table__.delete().where(*wheres) result = self.session.execute(stmt) deleted = result.rowcount else: - query = self.session.query(DocumentChunk).filter( - DocumentChunk.collection_name == collection_name - ) + query = self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name) if ids: query = query.filter(DocumentChunk.id.in_(ids)) if filter: for key, value in filter.items(): - query = query.filter( - DocumentChunk.vmetadata[key].astext == str(value) - ) + query = query.filter(DocumentChunk.vmetadata[key].astext == str(value)) deleted = query.delete(synchronize_session=False) self.session.commit() log.info(f"Deleted {deleted} items from collection '{collection_name}'.") except Exception as e: self.session.rollback() - log.exception(f"Error during delete: {e}") + log.exception(f'Error during delete: {e}') raise def reset(self) -> None: try: deleted = self.session.query(DocumentChunk).delete() self.session.commit() - log.info( - f"Reset complete. Deleted {deleted} items from 'document_chunk' table." - ) + log.info(f"Reset complete. Deleted {deleted} items from 'document_chunk' table.") except Exception as e: self.session.rollback() - log.exception(f"Error during reset: {e}") + log.exception(f'Error during reset: {e}') raise def close(self) -> None: @@ -736,16 +655,14 @@ class PgvectorClient(VectorDBBase): def has_collection(self, collection_name: str) -> bool: try: exists = ( - self.session.query(DocumentChunk) - .filter(DocumentChunk.collection_name == collection_name) - .first() + self.session.query(DocumentChunk).filter(DocumentChunk.collection_name == collection_name).first() is not None ) self.session.rollback() # read-only transaction return exists except Exception as e: self.session.rollback() - log.exception(f"Error checking collection existence: {e}") + log.exception(f'Error checking collection existence: {e}') return False def delete_collection(self, collection_name: str) -> None: diff --git a/backend/open_webui/retrieval/vector/dbs/pinecone.py b/backend/open_webui/retrieval/vector/dbs/pinecone.py index fc3c98f8cf..7e2b6e2dfa 100644 --- a/backend/open_webui/retrieval/vector/dbs/pinecone.py +++ b/backend/open_webui/retrieval/vector/dbs/pinecone.py @@ -1,6 +1,11 @@ -from typing import Optional, List, Dict, Any, Union +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + import logging import time # for measuring elapsed time +from typing import Any, Dict, List, Optional, Union + from pinecone import Pinecone, ServerlessSpec # Add gRPC support for better performance (Pinecone best practice) @@ -12,28 +17,26 @@ except ImportError: GRPC_AVAILABLE = False import asyncio # for async upserts -import functools # for partial binding in async tasks - import concurrent.futures # for parallel batch upserts +import functools # for partial binding in async tasks import random # for jitter in retry backoff -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) from open_webui.config import ( PINECONE_API_KEY, + PINECONE_CLOUD, + PINECONE_DIMENSION, PINECONE_ENVIRONMENT, PINECONE_INDEX_NAME, - PINECONE_DIMENSION, PINECONE_METRIC, - PINECONE_CLOUD, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, ) from open_webui.retrieval.vector.utils import process_metadata - NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system BATCH_SIZE = 100 # Recommended batch size for Pinecone operations @@ -42,7 +45,7 @@ log = logging.getLogger(__name__) class PineconeClient(VectorDBBase): def __init__(self): - self.collection_prefix = "open-webui" + self.collection_prefix = 'open-webui' # Validate required configuration self._validate_config() @@ -64,7 +67,7 @@ class PineconeClient(VectorDBBase): timeout=30, # Reasonable timeout for operations ) self.using_grpc = True - log.info("Using Pinecone gRPC client for optimal performance") + log.info('Using Pinecone gRPC client for optimal performance') else: # Fallback to HTTP client with enhanced connection pooling self.client = Pinecone( @@ -73,7 +76,7 @@ class PineconeClient(VectorDBBase): timeout=30, # Reasonable timeout for operations ) self.using_grpc = False - log.info("Using Pinecone HTTP client (gRPC not available)") + log.info('Using Pinecone HTTP client (gRPC not available)') # Persistent executor for batch operations self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) @@ -85,20 +88,18 @@ class PineconeClient(VectorDBBase): """Validate that all required configuration variables are set.""" missing_vars = [] if not PINECONE_API_KEY: - missing_vars.append("PINECONE_API_KEY") + missing_vars.append('PINECONE_API_KEY') if not PINECONE_ENVIRONMENT: - missing_vars.append("PINECONE_ENVIRONMENT") + missing_vars.append('PINECONE_ENVIRONMENT') if not PINECONE_INDEX_NAME: - missing_vars.append("PINECONE_INDEX_NAME") + missing_vars.append('PINECONE_INDEX_NAME') if not PINECONE_DIMENSION: - missing_vars.append("PINECONE_DIMENSION") + missing_vars.append('PINECONE_DIMENSION') if not PINECONE_CLOUD: - missing_vars.append("PINECONE_CLOUD") + missing_vars.append('PINECONE_CLOUD') if missing_vars: - raise ValueError( - f"Required configuration missing: {', '.join(missing_vars)}" - ) + raise ValueError(f'Required configuration missing: {", ".join(missing_vars)}') def _initialize_index(self) -> None: """Initialize the Pinecone index.""" @@ -123,8 +124,8 @@ class PineconeClient(VectorDBBase): ) except Exception as e: - log.error(f"Failed to initialize Pinecone index: {e}") - raise RuntimeError(f"Failed to initialize Pinecone index: {e}") + log.error(f'Failed to initialize Pinecone index: {e}') + raise RuntimeError(f'Failed to initialize Pinecone index: {e}') def _retry_pinecone_operation(self, operation_func, max_retries=3): """Retry Pinecone operations with exponential backoff for rate limits and network issues.""" @@ -137,18 +138,18 @@ class PineconeClient(VectorDBBase): is_retryable = any( keyword in error_str for keyword in [ - "rate limit", - "quota", - "timeout", - "network", - "connection", - "unavailable", - "internal error", - "429", - "500", - "502", - "503", - "504", + 'rate limit', + 'quota', + 'timeout', + 'network', + 'connection', + 'unavailable', + 'internal error', + '429', + '500', + '502', + '503', + '504', ] ) @@ -159,45 +160,42 @@ class PineconeClient(VectorDBBase): # Exponential backoff with jitter delay = (2**attempt) + random.uniform(0, 1) log.warning( - f"Pinecone operation failed (attempt {attempt + 1}/{max_retries}), " - f"retrying in {delay:.2f}s: {e}" + f'Pinecone operation failed (attempt {attempt + 1}/{max_retries}), retrying in {delay:.2f}s: {e}' ) time.sleep(delay) - def _create_points( - self, items: List[VectorItem], collection_name_with_prefix: str - ) -> List[Dict[str, Any]]: + def _create_points(self, items: List[VectorItem], collection_name_with_prefix: str) -> List[Dict[str, Any]]: """Convert VectorItem objects to Pinecone point format.""" points = [] for item in items: # Start with any existing metadata or an empty dict - metadata = item.get("metadata", {}).copy() if item.get("metadata") else {} + metadata = item.get('metadata', {}).copy() if item.get('metadata') else {} # Add text to metadata if available - if "text" in item: - metadata["text"] = item["text"] + if 'text' in item: + metadata['text'] = item['text'] # Always add collection_name to metadata for filtering - metadata["collection_name"] = collection_name_with_prefix + metadata['collection_name'] = collection_name_with_prefix point = { - "id": item["id"], - "values": item["vector"], - "metadata": process_metadata(metadata), + 'id': item['id'], + 'values': item['vector'], + 'metadata': process_metadata(metadata), } points.append(point) return points def _get_collection_name_with_prefix(self, collection_name: str) -> str: """Get the collection name with prefix.""" - return f"{self.collection_prefix}_{collection_name}" + return f'{self.collection_prefix}_{collection_name}' def _normalize_distance(self, score: float) -> float: """Normalize distance score based on the metric used.""" - if self.metric.lower() == "cosine": + if self.metric.lower() == 'cosine': # Cosine similarity ranges from -1 to 1, normalize to 0 to 1 return (score + 1.0) / 2.0 - elif self.metric.lower() in ["euclidean", "dotproduct"]: + elif self.metric.lower() in ['euclidean', 'dotproduct']: # These are already suitable for ranking (smaller is better for Euclidean) return score else: @@ -211,68 +209,56 @@ class PineconeClient(VectorDBBase): metadatas = [] for match in matches: - metadata = getattr(match, "metadata", {}) or {} - ids.append(match.id if hasattr(match, "id") else match["id"]) - documents.append(metadata.get("text", "")) + metadata = getattr(match, 'metadata', {}) or {} + ids.append(match.id if hasattr(match, 'id') else match['id']) + documents.append(metadata.get('text', '')) metadatas.append(metadata) return GetResult( **{ - "ids": [ids], - "documents": [documents], - "metadatas": [metadatas], + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], } ) def has_collection(self, collection_name: str) -> bool: """Check if a collection exists by searching for at least one item.""" - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) try: # Search for at least 1 item with this collection name in metadata response = self.index.query( vector=[0.0] * self.dimension, # dummy vector top_k=1, - filter={"collection_name": collection_name_with_prefix}, + filter={'collection_name': collection_name_with_prefix}, include_metadata=False, ) - matches = getattr(response, "matches", []) or [] + matches = getattr(response, 'matches', []) or [] return len(matches) > 0 except Exception as e: - log.exception( - f"Error checking collection '{collection_name_with_prefix}': {e}" - ) + log.exception(f"Error checking collection '{collection_name_with_prefix}': {e}") return False def delete_collection(self, collection_name: str) -> None: """Delete a collection by removing all vectors with the collection name in metadata.""" - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) try: - self.index.delete(filter={"collection_name": collection_name_with_prefix}) - log.info( - f"Collection '{collection_name_with_prefix}' deleted (all vectors removed)." - ) + self.index.delete(filter={'collection_name': collection_name_with_prefix}) + log.info(f"Collection '{collection_name_with_prefix}' deleted (all vectors removed).") except Exception as e: - log.warning( - f"Failed to delete collection '{collection_name_with_prefix}': {e}" - ) + log.warning(f"Failed to delete collection '{collection_name_with_prefix}': {e}") raise def insert(self, collection_name: str, items: List[VectorItem]) -> None: """Insert vectors into a collection.""" if not items: - log.warning("No items to insert") + log.warning('No items to insert') return start_time = time.time() - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) points = self._create_points(items, collection_name_with_prefix) # Parallelize batch inserts for performance @@ -285,26 +271,23 @@ class PineconeClient(VectorDBBase): try: future.result() except Exception as e: - log.error(f"Error inserting batch: {e}") + log.error(f'Error inserting batch: {e}') raise elapsed = time.time() - start_time - log.debug(f"Insert of {len(points)} vectors took {elapsed:.2f} seconds") + log.debug(f'Insert of {len(points)} vectors took {elapsed:.2f} seconds') log.info( - f"Successfully inserted {len(points)} vectors in parallel batches " - f"into '{collection_name_with_prefix}'" + f"Successfully inserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'" ) def upsert(self, collection_name: str, items: List[VectorItem]) -> None: """Upsert (insert or update) vectors into a collection.""" if not items: - log.warning("No items to upsert") + log.warning('No items to upsert') return start_time = time.time() - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) points = self._create_points(items, collection_name_with_prefix) # Parallelize batch upserts for performance @@ -317,78 +300,53 @@ class PineconeClient(VectorDBBase): try: future.result() except Exception as e: - log.error(f"Error upserting batch: {e}") + log.error(f'Error upserting batch: {e}') raise elapsed = time.time() - start_time - log.debug(f"Upsert of {len(points)} vectors took {elapsed:.2f} seconds") + log.debug(f'Upsert of {len(points)} vectors took {elapsed:.2f} seconds') log.info( - f"Successfully upserted {len(points)} vectors in parallel batches " - f"into '{collection_name_with_prefix}'" + f"Successfully upserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'" ) async def insert_async(self, collection_name: str, items: List[VectorItem]) -> None: """Async version of insert using asyncio and run_in_executor for improved performance.""" if not items: - log.warning("No items to insert") + log.warning('No items to insert') return - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) points = self._create_points(items, collection_name_with_prefix) # Create batches - batches = [ - points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE) - ] + batches = [points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)] loop = asyncio.get_event_loop() - tasks = [ - loop.run_in_executor( - None, functools.partial(self.index.upsert, vectors=batch) - ) - for batch in batches - ] + tasks = [loop.run_in_executor(None, functools.partial(self.index.upsert, vectors=batch)) for batch in batches] results = await asyncio.gather(*tasks, return_exceptions=True) for result in results: if isinstance(result, Exception): - log.error(f"Error in async insert batch: {result}") + log.error(f'Error in async insert batch: {result}') raise result - log.info( - f"Successfully async inserted {len(points)} vectors in batches " - f"into '{collection_name_with_prefix}'" - ) + log.info(f"Successfully async inserted {len(points)} vectors in batches into '{collection_name_with_prefix}'") async def upsert_async(self, collection_name: str, items: List[VectorItem]) -> None: """Async version of upsert using asyncio and run_in_executor for improved performance.""" if not items: - log.warning("No items to upsert") + log.warning('No items to upsert') return - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) points = self._create_points(items, collection_name_with_prefix) # Create batches - batches = [ - points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE) - ] + batches = [points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)] loop = asyncio.get_event_loop() - tasks = [ - loop.run_in_executor( - None, functools.partial(self.index.upsert, vectors=batch) - ) - for batch in batches - ] + tasks = [loop.run_in_executor(None, functools.partial(self.index.upsert, vectors=batch)) for batch in batches] results = await asyncio.gather(*tasks, return_exceptions=True) for result in results: if isinstance(result, Exception): - log.error(f"Error in async upsert batch: {result}") + log.error(f'Error in async upsert batch: {result}') raise result - log.info( - f"Successfully async upserted {len(points)} vectors in batches " - f"into '{collection_name_with_prefix}'" - ) + log.info(f"Successfully async upserted {len(points)} vectors in batches into '{collection_name_with_prefix}'") def search( self, @@ -399,12 +357,10 @@ class PineconeClient(VectorDBBase): ) -> Optional[SearchResult]: """Search for similar vectors in a collection.""" if not vectors or not vectors[0]: - log.warning("No vectors provided for search") + log.warning('No vectors provided for search') return None - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) if limit is None or limit <= 0: limit = NO_LIMIT @@ -418,10 +374,10 @@ class PineconeClient(VectorDBBase): vector=query_vector, top_k=limit, include_metadata=True, - filter={"collection_name": collection_name_with_prefix}, + filter={'collection_name': collection_name_with_prefix}, ) - matches = getattr(query_response, "matches", []) or [] + matches = getattr(query_response, 'matches', []) or [] if not matches: # Return empty result if no matches return SearchResult( @@ -435,12 +391,7 @@ class PineconeClient(VectorDBBase): get_result = self._result_to_get_result(matches) # Calculate normalized distances based on metric - distances = [ - [ - self._normalize_distance(getattr(match, "score", 0.0)) - for match in matches - ] - ] + distances = [[self._normalize_distance(getattr(match, 'score', 0.0)) for match in matches]] return SearchResult( ids=get_result.ids, @@ -452,13 +403,9 @@ class PineconeClient(VectorDBBase): log.error(f"Error searching in '{collection_name_with_prefix}': {e}") return None - def query( - self, collection_name: str, filter: Dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: """Query vectors by metadata filter.""" - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) if limit is None or limit <= 0: limit = NO_LIMIT @@ -468,7 +415,7 @@ class PineconeClient(VectorDBBase): zero_vector = [0.0] * self.dimension # Combine user filter with collection_name - pinecone_filter = {"collection_name": collection_name_with_prefix} + pinecone_filter = {'collection_name': collection_name_with_prefix} if filter: pinecone_filter.update(filter) @@ -480,7 +427,7 @@ class PineconeClient(VectorDBBase): include_metadata=True, ) - matches = getattr(query_response, "matches", []) or [] + matches = getattr(query_response, 'matches', []) or [] return self._result_to_get_result(matches) except Exception as e: @@ -489,9 +436,7 @@ class PineconeClient(VectorDBBase): def get(self, collection_name: str) -> Optional[GetResult]: """Get all vectors in a collection.""" - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) try: # Use a zero vector for fetching all entries @@ -502,10 +447,10 @@ class PineconeClient(VectorDBBase): vector=zero_vector, top_k=NO_LIMIT, include_metadata=True, - filter={"collection_name": collection_name_with_prefix}, + filter={'collection_name': collection_name_with_prefix}, ) - matches = getattr(query_response, "matches", []) or [] + matches = getattr(query_response, 'matches', []) or [] return self._result_to_get_result(matches) except Exception as e: @@ -519,9 +464,7 @@ class PineconeClient(VectorDBBase): filter: Optional[Dict] = None, ) -> None: """Delete vectors by IDs or filter.""" - collection_name_with_prefix = self._get_collection_name_with_prefix( - collection_name - ) + collection_name_with_prefix = self._get_collection_name_with_prefix(collection_name) try: if ids: @@ -531,28 +474,20 @@ class PineconeClient(VectorDBBase): # Note: When deleting by ID, we can't filter by collection_name # This is a limitation of Pinecone - be careful with ID uniqueness self.index.delete(ids=batch_ids) - log.debug( - f"Deleted batch of {len(batch_ids)} vectors by ID " - f"from '{collection_name_with_prefix}'" - ) - log.info( - f"Successfully deleted {len(ids)} vectors by ID " - f"from '{collection_name_with_prefix}'" - ) + log.debug(f"Deleted batch of {len(batch_ids)} vectors by ID from '{collection_name_with_prefix}'") + log.info(f"Successfully deleted {len(ids)} vectors by ID from '{collection_name_with_prefix}'") elif filter: # Combine user filter with collection_name - pinecone_filter = {"collection_name": collection_name_with_prefix} + pinecone_filter = {'collection_name': collection_name_with_prefix} if filter: pinecone_filter.update(filter) # Delete by metadata filter self.index.delete(filter=pinecone_filter) - log.info( - f"Successfully deleted vectors by filter from '{collection_name_with_prefix}'" - ) + log.info(f"Successfully deleted vectors by filter from '{collection_name_with_prefix}'") else: - log.warning("No ids or filter provided for delete operation") + log.warning('No ids or filter provided for delete operation') except Exception as e: log.error(f"Error deleting from collection '{collection_name}': {e}") @@ -562,9 +497,9 @@ class PineconeClient(VectorDBBase): """Reset the database by deleting all collections.""" try: self.index.delete(delete_all=True) - log.info("All vectors successfully deleted from the index.") + log.info('All vectors successfully deleted from the index.') except Exception as e: - log.error(f"Failed to reset Pinecone index: {e}") + log.error(f'Failed to reset Pinecone index: {e}') raise def close(self): @@ -573,7 +508,7 @@ class PineconeClient(VectorDBBase): # The new Pinecone client doesn't need explicit closing pass except Exception as e: - log.warning(f"Failed to clean up Pinecone resources: {e}") + log.warning(f'Failed to clean up Pinecone resources: {e}') self._executor.shutdown(wait=True) def __enter__(self): diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant.py b/backend/open_webui/retrieval/vector/dbs/qdrant.py index d42984e1d6..acf4e61993 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant.py @@ -1,28 +1,31 @@ -from typing import Optional +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + import logging +from typing import Optional from urllib.parse import urlparse +from open_webui.config import ( + QDRANT_API_KEY, + QDRANT_COLLECTION_PREFIX, + QDRANT_GRPC_PORT, + QDRANT_HNSW_M, + QDRANT_ON_DISK, + QDRANT_PREFER_GRPC, + QDRANT_TIMEOUT, + QDRANT_URI, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) from qdrant_client import QdrantClient as Qclient from qdrant_client.http.models import PointStruct from qdrant_client.models import models -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) -from open_webui.config import ( - QDRANT_URI, - QDRANT_API_KEY, - QDRANT_ON_DISK, - QDRANT_GRPC_PORT, - QDRANT_PREFER_GRPC, - QDRANT_COLLECTION_PREFIX, - QDRANT_TIMEOUT, - QDRANT_HNSW_M, -) - NO_LIMIT = 999999999 log = logging.getLogger(__name__) @@ -72,19 +75,19 @@ class QdrantClient(VectorDBBase): for point in points: payload = point.payload ids.append(point.id) - documents.append(payload["text"]) - metadatas.append(payload["metadata"]) + documents.append(payload['text']) + metadatas.append(payload['metadata']) return GetResult( **{ - "ids": [ids], - "documents": [documents], - "metadatas": [metadatas], + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], } ) def _create_collection(self, collection_name: str, dimension: int): - collection_name_with_prefix = f"{self.collection_prefix}_{collection_name}" + collection_name_with_prefix = f'{self.collection_prefix}_{collection_name}' self.client.create_collection( collection_name=collection_name_with_prefix, vectors_config=models.VectorParams( @@ -100,7 +103,7 @@ class QdrantClient(VectorDBBase): # Create payload indexes for efficient filtering self.client.create_payload_index( collection_name=collection_name_with_prefix, - field_name="metadata.hash", + field_name='metadata.hash', field_schema=models.KeywordIndexParams( type=models.KeywordIndexType.KEYWORD, is_tenant=False, @@ -109,40 +112,34 @@ class QdrantClient(VectorDBBase): ) self.client.create_payload_index( collection_name=collection_name_with_prefix, - field_name="metadata.file_id", + field_name='metadata.file_id', field_schema=models.KeywordIndexParams( type=models.KeywordIndexType.KEYWORD, is_tenant=False, on_disk=self.QDRANT_ON_DISK, ), ) - log.info(f"collection {collection_name_with_prefix} successfully created!") + log.info(f'collection {collection_name_with_prefix} successfully created!') def _create_collection_if_not_exists(self, collection_name, dimension): if not self.has_collection(collection_name=collection_name): - self._create_collection( - collection_name=collection_name, dimension=dimension - ) + self._create_collection(collection_name=collection_name, dimension=dimension) def _create_points(self, items: list[VectorItem]): return [ PointStruct( - id=item["id"], - vector=item["vector"], - payload={"text": item["text"], "metadata": item["metadata"]}, + id=item['id'], + vector=item['vector'], + payload={'text': item['text'], 'metadata': item['metadata']}, ) for item in items ] def has_collection(self, collection_name: str) -> bool: - return self.client.collection_exists( - f"{self.collection_prefix}_{collection_name}" - ) + return self.client.collection_exists(f'{self.collection_prefix}_{collection_name}') def delete_collection(self, collection_name: str): - return self.client.delete_collection( - collection_name=f"{self.collection_prefix}_{collection_name}" - ) + return self.client.delete_collection(collection_name=f'{self.collection_prefix}_{collection_name}') def search( self, @@ -156,7 +153,7 @@ class QdrantClient(VectorDBBase): limit = NO_LIMIT # otherwise qdrant would set limit to 10! query_response = self.client.query_points( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', query=vectors[0], limit=limit, ) @@ -180,13 +177,11 @@ class QdrantClient(VectorDBBase): field_conditions = [] for key, value in filter.items(): field_conditions.append( - models.FieldCondition( - key=f"metadata.{key}", match=models.MatchValue(value=value) - ) + models.FieldCondition(key=f'metadata.{key}', match=models.MatchValue(value=value)) ) points = self.client.scroll( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', scroll_filter=models.Filter(should=field_conditions), limit=limit, ) @@ -198,22 +193,22 @@ class QdrantClient(VectorDBBase): def get(self, collection_name: str) -> Optional[GetResult]: # Get all the items in the collection. points = self.client.scroll( - collection_name=f"{self.collection_prefix}_{collection_name}", + collection_name=f'{self.collection_prefix}_{collection_name}', limit=NO_LIMIT, # otherwise qdrant would set limit to 10! ) return self._result_to_get_result(points[0]) def insert(self, collection_name: str, items: list[VectorItem]): # Insert the items into the collection, if the collection does not exist, it will be created. - self._create_collection_if_not_exists(collection_name, len(items[0]["vector"])) + self._create_collection_if_not_exists(collection_name, len(items[0]['vector'])) points = self._create_points(items) - self.client.upload_points(f"{self.collection_prefix}_{collection_name}", points) + self.client.upload_points(f'{self.collection_prefix}_{collection_name}', points) def upsert(self, collection_name: str, items: list[VectorItem]): # Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created. - self._create_collection_if_not_exists(collection_name, len(items[0]["vector"])) + self._create_collection_if_not_exists(collection_name, len(items[0]['vector'])) points = self._create_points(items) - return self.client.upsert(f"{self.collection_prefix}_{collection_name}", points) + return self.client.upsert(f'{self.collection_prefix}_{collection_name}', points) def delete( self, @@ -226,26 +221,28 @@ class QdrantClient(VectorDBBase): if ids: for id_value in ids: - field_conditions.append( - models.FieldCondition( - key="metadata.id", - match=models.MatchValue(value=id_value), + ( + field_conditions.append( + models.FieldCondition( + key='metadata.id', + match=models.MatchValue(value=id_value), + ), ), - ), + ) elif filter: for key, value in filter.items(): - field_conditions.append( - models.FieldCondition( - key=f"metadata.{key}", - match=models.MatchValue(value=value), + ( + field_conditions.append( + models.FieldCondition( + key=f'metadata.{key}', + match=models.MatchValue(value=value), + ), ), - ), + ) return self.client.delete( - collection_name=f"{self.collection_prefix}_{collection_name}", - points_selector=models.FilterSelector( - filter=models.Filter(must=field_conditions) - ), + collection_name=f'{self.collection_prefix}_{collection_name}', + points_selector=models.FilterSelector(filter=models.Filter(must=field_conditions)), ) def reset(self): diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py index f87f85a23b..15e72f1d16 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py @@ -1,17 +1,21 @@ +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + import logging -from typing import Optional, Tuple, List, Dict, Any +from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse import grpc from open_webui.config import ( QDRANT_API_KEY, + QDRANT_COLLECTION_PREFIX, QDRANT_GRPC_PORT, + QDRANT_HNSW_M, QDRANT_ON_DISK, QDRANT_PREFER_GRPC, - QDRANT_URI, - QDRANT_COLLECTION_PREFIX, QDRANT_TIMEOUT, - QDRANT_HNSW_M, + QDRANT_URI, ) from open_webui.retrieval.vector.main import ( GetResult, @@ -25,22 +29,18 @@ from qdrant_client.http.models import PointStruct from qdrant_client.models import models NO_LIMIT = 999999999 -TENANT_ID_FIELD = "tenant_id" +TENANT_ID_FIELD = 'tenant_id' DEFAULT_DIMENSION = 384 log = logging.getLogger(__name__) def _tenant_filter(tenant_id: str) -> models.FieldCondition: - return models.FieldCondition( - key=TENANT_ID_FIELD, match=models.MatchValue(value=tenant_id) - ) + return models.FieldCondition(key=TENANT_ID_FIELD, match=models.MatchValue(value=tenant_id)) def _metadata_filter(key: str, value: Any) -> models.FieldCondition: - return models.FieldCondition( - key=f"metadata.{key}", match=models.MatchValue(value=value) - ) + return models.FieldCondition(key=f'metadata.{key}', match=models.MatchValue(value=value)) class QdrantClient(VectorDBBase): @@ -55,9 +55,7 @@ class QdrantClient(VectorDBBase): self.QDRANT_HNSW_M = QDRANT_HNSW_M if not self.QDRANT_URI: - raise ValueError( - "QDRANT_URI is not set. Please configure it in the environment variables." - ) + raise ValueError('QDRANT_URI is not set. Please configure it in the environment variables.') # Unified handling for either scheme parsed = urlparse(self.QDRANT_URI) @@ -82,19 +80,19 @@ class QdrantClient(VectorDBBase): ) # Main collection types for multi-tenancy - self.MEMORY_COLLECTION = f"{self.collection_prefix}_memories" - self.KNOWLEDGE_COLLECTION = f"{self.collection_prefix}_knowledge" - self.FILE_COLLECTION = f"{self.collection_prefix}_files" - self.WEB_SEARCH_COLLECTION = f"{self.collection_prefix}_web-search" - self.HASH_BASED_COLLECTION = f"{self.collection_prefix}_hash-based" + self.MEMORY_COLLECTION = f'{self.collection_prefix}_memories' + self.KNOWLEDGE_COLLECTION = f'{self.collection_prefix}_knowledge' + self.FILE_COLLECTION = f'{self.collection_prefix}_files' + self.WEB_SEARCH_COLLECTION = f'{self.collection_prefix}_web-search' + self.HASH_BASED_COLLECTION = f'{self.collection_prefix}_hash-based' def _result_to_get_result(self, points) -> GetResult: ids, documents, metadatas = [], [], [] for point in points: payload = point.payload ids.append(point.id) - documents.append(payload["text"]) - metadatas.append(payload["metadata"]) + documents.append(payload['text']) + metadatas.append(payload['metadata']) return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) def _get_collection_and_tenant_id(self, collection_name: str) -> Tuple[str, str]: @@ -114,29 +112,25 @@ class QdrantClient(VectorDBBase): # Check for user memory collections tenant_id = collection_name - if collection_name.startswith("user-memory-"): + if collection_name.startswith('user-memory-'): return self.MEMORY_COLLECTION, tenant_id # Check for file collections - elif collection_name.startswith("file-"): + elif collection_name.startswith('file-'): return self.FILE_COLLECTION, tenant_id # Check for web search collections - elif collection_name.startswith("web-search-"): + elif collection_name.startswith('web-search-'): return self.WEB_SEARCH_COLLECTION, tenant_id # Handle hash-based collections (YouTube and web URLs) - elif len(collection_name) == 63 and all( - c in "0123456789abcdef" for c in collection_name - ): + elif len(collection_name) == 63 and all(c in '0123456789abcdef' for c in collection_name): return self.HASH_BASED_COLLECTION, tenant_id else: return self.KNOWLEDGE_COLLECTION, tenant_id - def _create_multi_tenant_collection( - self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION - ): + def _create_multi_tenant_collection(self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION): """ Creates a collection with multi-tenancy configuration and payload indexes for tenant_id and metadata fields. """ @@ -154,9 +148,7 @@ class QdrantClient(VectorDBBase): m=0, ), ) - log.info( - f"Multi-tenant collection {mt_collection_name} created with dimension {dimension}!" - ) + log.info(f'Multi-tenant collection {mt_collection_name} created with dimension {dimension}!') self.client.create_payload_index( collection_name=mt_collection_name, @@ -168,7 +160,7 @@ class QdrantClient(VectorDBBase): ), ) - for field in ("metadata.hash", "metadata.file_id"): + for field in ('metadata.hash', 'metadata.file_id'): self.client.create_payload_index( collection_name=mt_collection_name, field_name=field, @@ -178,28 +170,24 @@ class QdrantClient(VectorDBBase): ), ) - def _create_points( - self, items: List[VectorItem], tenant_id: str - ) -> List[PointStruct]: + def _create_points(self, items: List[VectorItem], tenant_id: str) -> List[PointStruct]: """ Create point structs from vector items with tenant ID. """ return [ PointStruct( - id=item["id"], - vector=item["vector"], + id=item['id'], + vector=item['vector'], payload={ - "text": item["text"], - "metadata": item["metadata"], + 'text': item['text'], + 'metadata': item['metadata'], TENANT_ID_FIELD: tenant_id, }, ) for item in items ] - def _ensure_collection( - self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION - ): + def _ensure_collection(self, mt_collection_name: str, dimension: int = DEFAULT_DIMENSION): """ Ensure the collection exists and payload indexes are created for tenant_id and metadata fields. """ @@ -242,15 +230,13 @@ class QdrantClient(VectorDBBase): must_conditions = [_tenant_filter(tenant_id)] should_conditions = [] if ids: - should_conditions = [_metadata_filter("id", id_value) for id_value in ids] + should_conditions = [_metadata_filter('id', id_value) for id_value in ids] elif filter: must_conditions += [_metadata_filter(k, v) for k, v in filter.items()] return self.client.delete( collection_name=mt_collection, - points_selector=models.FilterSelector( - filter=models.Filter(must=must_conditions, should=should_conditions) - ), + points_selector=models.FilterSelector(filter=models.Filter(must=must_conditions, should=should_conditions)), ) def search( @@ -285,9 +271,7 @@ class QdrantClient(VectorDBBase): distances=[[(point.score + 1.0) / 2.0 for point in query_response.points]], ) - def query( - self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None - ): + def query(self, collection_name: str, filter: Dict[str, Any], limit: Optional[int] = None): """ Query points with filters and tenant isolation. """ @@ -334,7 +318,7 @@ class QdrantClient(VectorDBBase): if not self.client or not items: return None mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name) - dimension = len(items[0]["vector"]) + dimension = len(items[0]['vector']) self._ensure_collection(mt_collection, dimension) points = self._create_points(items, tenant_id) self.client.upload_points(mt_collection, points) @@ -368,7 +352,5 @@ class QdrantClient(VectorDBBase): return None self.client.delete( collection_name=mt_collection, - points_selector=models.FilterSelector( - filter=models.Filter(must=[_tenant_filter(tenant_id)]) - ), + points_selector=models.FilterSelector(filter=models.Filter(must=[_tenant_filter(tenant_id)])), ) diff --git a/backend/open_webui/retrieval/vector/dbs/s3vector.py b/backend/open_webui/retrieval/vector/dbs/s3vector.py index 96e487f111..e0b156931c 100644 --- a/backend/open_webui/retrieval/vector/dbs/s3vector.py +++ b/backend/open_webui/retrieval/vector/dbs/s3vector.py @@ -1,14 +1,19 @@ -from open_webui.retrieval.vector.utils import process_metadata +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + +import logging +from typing import Any, Dict, List, Optional, Union + +import boto3 +from open_webui.config import S3_VECTOR_BUCKET_NAME, S3_VECTOR_REGION from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, GetResult, SearchResult, + VectorDBBase, + VectorItem, ) -from open_webui.config import S3_VECTOR_BUCKET_NAME, S3_VECTOR_REGION -from typing import List, Optional, Dict, Any, Union -import logging -import boto3 +from open_webui.retrieval.vector.utils import process_metadata log = logging.getLogger(__name__) @@ -24,18 +29,16 @@ class S3VectorClient(VectorDBBase): # Simple validation - log warnings instead of raising exceptions if not self.bucket_name: - log.warning("S3_VECTOR_BUCKET_NAME not set - S3Vector will not work") + log.warning('S3_VECTOR_BUCKET_NAME not set - S3Vector will not work') if not self.region: - log.warning("S3_VECTOR_REGION not set - S3Vector will not work") + log.warning('S3_VECTOR_REGION not set - S3Vector will not work') if self.bucket_name and self.region: try: - self.client = boto3.client("s3vectors", region_name=self.region) - log.info( - f"S3Vector client initialized for bucket '{self.bucket_name}' in region '{self.region}'" - ) + self.client = boto3.client('s3vectors', region_name=self.region) + log.info(f"S3Vector client initialized for bucket '{self.bucket_name}' in region '{self.region}'") except Exception as e: - log.error(f"Failed to initialize S3Vector client: {e}") + log.error(f'Failed to initialize S3Vector client: {e}') self.client = None else: self.client = None @@ -44,8 +47,8 @@ class S3VectorClient(VectorDBBase): self, index_name: str, dimension: int, - data_type: str = "float32", - distance_metric: str = "cosine", + data_type: str = 'float32', + distance_metric: str = 'cosine', ) -> None: """ Create a new index in the S3 vector bucket for the given collection if it does not exist. @@ -61,17 +64,18 @@ class S3VectorClient(VectorDBBase): dataType=data_type, dimension=dimension, distanceMetric=distance_metric, + metadataConfiguration={ + 'nonFilterableMetadataKeys': [ + 'text', + ] + }, ) - log.info( - f"Created S3 index: {index_name} (dim={dimension}, type={data_type}, metric={distance_metric})" - ) + log.info(f'Created S3 index: {index_name} (dim={dimension}, type={data_type}, metric={distance_metric})') except Exception as e: log.error(f"Error creating S3 index '{index_name}': {e}") raise - def _filter_metadata( - self, metadata: Dict[str, Any], item_id: str - ) -> Dict[str, Any]: + def _filter_metadata(self, metadata: Dict[str, Any], item_id: str) -> Dict[str, Any]: """ Filter vector metadata keys to comply with S3 Vector API limit of 10 keys maximum. """ @@ -80,16 +84,16 @@ class S3VectorClient(VectorDBBase): # Keep only the first 10 keys, prioritizing important ones based on actual Open WebUI metadata important_keys = [ - "text", # The actual document content - "file_id", # File ID - "source", # Document source file - "title", # Document title - "page", # Page number - "total_pages", # Total pages in document - "embedding_config", # Embedding configuration - "created_by", # User who created it - "name", # Document name - "hash", # Content hash + 'text', # The actual document content + 'file_id', # File ID + 'source', # Document source file + 'title', # Document title + 'page', # Page number + 'total_pages', # Total pages in document + 'embedding_config', # Embedding configuration + 'created_by', # User who created it + 'name', # Document name + 'hash', # Content hash ] filtered_metadata = {} @@ -108,9 +112,7 @@ class S3VectorClient(VectorDBBase): if len(filtered_metadata) >= 10: break - log.warning( - f"Metadata for key '{item_id}' had {len(metadata)} keys, limited to 10 keys" - ) + log.warning(f"Metadata for key '{item_id}' had {len(metadata)} keys, limited to 10 keys") return filtered_metadata def has_collection(self, collection_name: str) -> bool: @@ -119,9 +121,7 @@ class S3VectorClient(VectorDBBase): This avoids pagination issues with list_indexes() and is significantly faster. """ try: - self.client.get_index( - vectorBucketName=self.bucket_name, indexName=collection_name - ) + self.client.get_index(vectorBucketName=self.bucket_name, indexName=collection_name) return True except Exception as e: log.error(f"Error checking if index '{collection_name}' exists: {e}") @@ -133,16 +133,12 @@ class S3VectorClient(VectorDBBase): """ if not self.has_collection(collection_name): - log.warning( - f"Collection '{collection_name}' does not exist, nothing to delete" - ) + log.warning(f"Collection '{collection_name}' does not exist, nothing to delete") return try: log.info(f"Deleting collection '{collection_name}'") - self.client.delete_index( - vectorBucketName=self.bucket_name, indexName=collection_name - ) + self.client.delete_index(vectorBucketName=self.bucket_name, indexName=collection_name) log.info(f"Successfully deleted collection '{collection_name}'") except Exception as e: log.error(f"Error deleting collection '{collection_name}': {e}") @@ -153,10 +149,10 @@ class S3VectorClient(VectorDBBase): Insert vector items into the S3 Vector index. Create index if it does not exist. """ if not items: - log.warning("No items to insert") + log.warning('No items to insert') return - dimension = len(items[0]["vector"]) + dimension = len(items[0]['vector']) try: if not self.has_collection(collection_name): @@ -164,36 +160,36 @@ class S3VectorClient(VectorDBBase): self._create_index( index_name=collection_name, dimension=dimension, - data_type="float32", - distance_metric="cosine", + data_type='float32', + distance_metric='cosine', ) # Prepare vectors for insertion vectors = [] for item in items: # Ensure vector data is in the correct format for S3 Vector API - vector_data = item["vector"] + vector_data = item['vector'] if isinstance(vector_data, list): # Convert list to float32 values as required by S3 Vector API vector_data = [float(x) for x in vector_data] # Prepare metadata, ensuring the text field is preserved - metadata = item.get("metadata", {}).copy() + metadata = item.get('metadata', {}).copy() # Add the text field to metadata so it's available for retrieval - metadata["text"] = item["text"] + metadata['text'] = item['text'] # Convert metadata to string format for consistency metadata = process_metadata(metadata) # Filter metadata to comply with S3 Vector API limit of 10 keys - metadata = self._filter_metadata(metadata, item["id"]) + metadata = self._filter_metadata(metadata, item['id']) vectors.append( { - "key": item["id"], - "data": {"float32": vector_data}, - "metadata": metadata, + 'key': item['id'], + 'data': {'float32': vector_data}, + 'metadata': metadata, } ) @@ -206,15 +202,11 @@ class S3VectorClient(VectorDBBase): indexName=collection_name, vectors=batch, ) - log.info( - f"Inserted batch {i//batch_size + 1}: {len(batch)} vectors into index '{collection_name}'." - ) + log.info(f"Inserted batch {i // batch_size + 1}: {len(batch)} vectors into index '{collection_name}'.") - log.info( - f"Completed insertion of {len(vectors)} vectors into index '{collection_name}'." - ) + log.info(f"Completed insertion of {len(vectors)} vectors into index '{collection_name}'.") except Exception as e: - log.error(f"Error inserting vectors: {e}") + log.error(f'Error inserting vectors: {e}') raise def upsert(self, collection_name: str, items: List[VectorItem]) -> None: @@ -222,49 +214,47 @@ class S3VectorClient(VectorDBBase): Insert or update vector items in the S3 Vector index. Create index if it does not exist. """ if not items: - log.warning("No items to upsert") + log.warning('No items to upsert') return - dimension = len(items[0]["vector"]) - log.info(f"Upsert dimension: {dimension}") + dimension = len(items[0]['vector']) + log.info(f'Upsert dimension: {dimension}') try: if not self.has_collection(collection_name): - log.info( - f"Index '{collection_name}' does not exist. Creating index for upsert." - ) + log.info(f"Index '{collection_name}' does not exist. Creating index for upsert.") self._create_index( index_name=collection_name, dimension=dimension, - data_type="float32", - distance_metric="cosine", + data_type='float32', + distance_metric='cosine', ) # Prepare vectors for upsert vectors = [] for item in items: # Ensure vector data is in the correct format for S3 Vector API - vector_data = item["vector"] + vector_data = item['vector'] if isinstance(vector_data, list): # Convert list to float32 values as required by S3 Vector API vector_data = [float(x) for x in vector_data] # Prepare metadata, ensuring the text field is preserved - metadata = item.get("metadata", {}).copy() + metadata = item.get('metadata', {}).copy() # Add the text field to metadata so it's available for retrieval - metadata["text"] = item["text"] + metadata['text'] = item['text'] # Convert metadata to string format for consistency metadata = process_metadata(metadata) # Filter metadata to comply with S3 Vector API limit of 10 keys - metadata = self._filter_metadata(metadata, item["id"]) + metadata = self._filter_metadata(metadata, item['id']) vectors.append( { - "key": item["id"], - "data": {"float32": vector_data}, - "metadata": metadata, + 'key': item['id'], + 'data': {'float32': vector_data}, + 'metadata': metadata, } ) @@ -274,12 +264,10 @@ class S3VectorClient(VectorDBBase): batch = vectors[i : i + batch_size] if i == 0: # Log sample info for first batch only log.info( - f"Upserting batch 1: {len(batch)} vectors. First vector sample: key={batch[0]['key']}, data_type={type(batch[0]['data']['float32'])}, data_len={len(batch[0]['data']['float32'])}" + f'Upserting batch 1: {len(batch)} vectors. First vector sample: key={batch[0]["key"]}, data_type={type(batch[0]["data"]["float32"])}, data_len={len(batch[0]["data"]["float32"])}' ) else: - log.info( - f"Upserting batch {i//batch_size + 1}: {len(batch)} vectors." - ) + log.info(f'Upserting batch {i // batch_size + 1}: {len(batch)} vectors.') self.client.put_vectors( vectorBucketName=self.bucket_name, @@ -287,11 +275,9 @@ class S3VectorClient(VectorDBBase): vectors=batch, ) - log.info( - f"Completed upsert of {len(vectors)} vectors into index '{collection_name}'." - ) + log.info(f"Completed upsert of {len(vectors)} vectors into index '{collection_name}'.") except Exception as e: - log.error(f"Error upserting vectors: {e}") + log.error(f'Error upserting vectors: {e}') raise def search( @@ -310,13 +296,11 @@ class S3VectorClient(VectorDBBase): return None if not vectors: - log.warning("No query vectors provided") + log.warning('No query vectors provided') return None try: - log.info( - f"Searching collection '{collection_name}' with {len(vectors)} query vectors, limit={limit}" - ) + log.info(f"Searching collection '{collection_name}' with {len(vectors)} query vectors, limit={limit}") # Initialize result lists all_ids = [] @@ -326,10 +310,10 @@ class S3VectorClient(VectorDBBase): # Process each query vector for i, query_vector in enumerate(vectors): - log.debug(f"Processing query vector {i+1}/{len(vectors)}") + log.debug(f'Processing query vector {i + 1}/{len(vectors)}') # Prepare the query vector in S3 Vector format - query_vector_dict = {"float32": [float(x) for x in query_vector]} + query_vector_dict = {'float32': [float(x) for x in query_vector]} # Call S3 Vector query API response = self.client.query_vectors( @@ -347,24 +331,22 @@ class S3VectorClient(VectorDBBase): query_metadatas = [] query_distances = [] - result_vectors = response.get("vectors", []) + result_vectors = response.get('vectors', []) for vector in result_vectors: - vector_id = vector.get("key") - vector_metadata = vector.get("metadata", {}) - vector_distance = vector.get("distance", 0.0) + vector_id = vector.get('key') + vector_metadata = vector.get('metadata', {}) + vector_distance = vector.get('distance', 0.0) # Extract document text from metadata - document_text = "" + document_text = '' if isinstance(vector_metadata, dict): # Get the text field first (highest priority) - document_text = vector_metadata.get("text") + document_text = vector_metadata.get('text') if not document_text: # Fallback to other possible text fields document_text = ( - vector_metadata.get("content") - or vector_metadata.get("document") - or vector_id + vector_metadata.get('content') or vector_metadata.get('document') or vector_id ) else: document_text = vector_id @@ -380,7 +362,7 @@ class S3VectorClient(VectorDBBase): all_metadatas.append(query_metadatas) all_distances.append(query_distances) - log.info(f"Search completed. Found results for {len(all_ids)} queries") + log.info(f'Search completed. Found results for {len(all_ids)} queries') # Return SearchResult format return SearchResult( @@ -393,24 +375,20 @@ class S3VectorClient(VectorDBBase): except Exception as e: log.error(f"Error searching collection '{collection_name}': {str(e)}") # Handle specific AWS exceptions - if hasattr(e, "response") and "Error" in e.response: - error_code = e.response["Error"]["Code"] - if error_code == "NotFoundException": + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error']['Code'] + if error_code == 'NotFoundException': log.warning(f"Collection '{collection_name}' not found") return None - elif error_code == "ValidationException": - log.error(f"Invalid query vector dimensions or parameters") + elif error_code == 'ValidationException': + log.error(f'Invalid query vector dimensions or parameters') return None - elif error_code == "AccessDeniedException": - log.error( - f"Access denied for collection '{collection_name}'. Check permissions." - ) + elif error_code == 'AccessDeniedException': + log.error(f"Access denied for collection '{collection_name}'. Check permissions.") return None raise - def query( - self, collection_name: str, filter: Dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: """ Query vectors from a collection using metadata filter. """ @@ -420,7 +398,7 @@ class S3VectorClient(VectorDBBase): return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) if not filter: - log.warning("No filter provided, returning all vectors") + log.warning('No filter provided, returning all vectors') return self.get(collection_name) try: @@ -434,17 +412,13 @@ class S3VectorClient(VectorDBBase): all_vectors_result = self.get(collection_name) if not all_vectors_result or not all_vectors_result.ids: - log.warning("No vectors found in collection") + log.warning('No vectors found in collection') return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) # Extract the lists from the result all_ids = all_vectors_result.ids[0] if all_vectors_result.ids else [] - all_documents = ( - all_vectors_result.documents[0] if all_vectors_result.documents else [] - ) - all_metadatas = ( - all_vectors_result.metadatas[0] if all_vectors_result.metadatas else [] - ) + all_documents = all_vectors_result.documents[0] if all_vectors_result.documents else [] + all_metadatas = all_vectors_result.metadatas[0] if all_vectors_result.metadatas else [] # Apply client-side filtering filtered_ids = [] @@ -463,9 +437,7 @@ class S3VectorClient(VectorDBBase): if limit and len(filtered_ids) >= limit: break - log.info( - f"Filter applied: {len(filtered_ids)} vectors match out of {len(all_ids)} total" - ) + log.info(f'Filter applied: {len(filtered_ids)} vectors match out of {len(all_ids)} total') # Return GetResult format if filtered_ids: @@ -480,15 +452,13 @@ class S3VectorClient(VectorDBBase): except Exception as e: log.error(f"Error querying collection '{collection_name}': {str(e)}") # Handle specific AWS exceptions - if hasattr(e, "response") and "Error" in e.response: - error_code = e.response["Error"]["Code"] - if error_code == "NotFoundException": + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error']['Code'] + if error_code == 'NotFoundException': log.warning(f"Collection '{collection_name}' not found") return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) - elif error_code == "AccessDeniedException": - log.error( - f"Access denied for collection '{collection_name}'. Check permissions." - ) + elif error_code == 'AccessDeniedException': + log.error(f"Access denied for collection '{collection_name}'. Check permissions.") return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) raise @@ -515,47 +485,43 @@ class S3VectorClient(VectorDBBase): while True: # Prepare request parameters request_params = { - "vectorBucketName": self.bucket_name, - "indexName": collection_name, - "returnData": False, # Don't include vector data (not needed for get) - "returnMetadata": True, # Include metadata - "maxResults": 500, # Use reasonable page size + 'vectorBucketName': self.bucket_name, + 'indexName': collection_name, + 'returnData': False, # Don't include vector data (not needed for get) + 'returnMetadata': True, # Include metadata + 'maxResults': 500, # Use reasonable page size } if next_token: - request_params["nextToken"] = next_token + request_params['nextToken'] = next_token # Call S3 Vector API response = self.client.list_vectors(**request_params) # Process vectors in this page - vectors = response.get("vectors", []) + vectors = response.get('vectors', []) for vector in vectors: - vector_id = vector.get("key") - vector_data = vector.get("data", {}) - vector_metadata = vector.get("metadata", {}) + vector_id = vector.get('key') + vector_data = vector.get('data', {}) + vector_metadata = vector.get('metadata', {}) # Extract the actual vector array - vector_array = vector_data.get("float32", []) + vector_array = vector_data.get('float32', []) # For documents, we try to extract text from metadata or use the vector ID - document_text = "" + document_text = '' if isinstance(vector_metadata, dict): # Get the text field first (highest priority) - document_text = vector_metadata.get("text") + document_text = vector_metadata.get('text') if not document_text: # Fallback to other possible text fields document_text = ( - vector_metadata.get("content") - or vector_metadata.get("document") - or vector_id + vector_metadata.get('content') or vector_metadata.get('document') or vector_id ) # Log the actual content for debugging - log.debug( - f"Document text preview (first 200 chars): {str(document_text)[:200]}" - ) + log.debug(f'Document text preview (first 200 chars): {str(document_text)[:200]}') else: document_text = vector_id @@ -564,37 +530,29 @@ class S3VectorClient(VectorDBBase): all_metadatas.append(vector_metadata) # Check if there are more pages - next_token = response.get("nextToken") + next_token = response.get('nextToken') if not next_token: break - log.info( - f"Retrieved {len(all_ids)} vectors from collection '{collection_name}'" - ) + log.info(f"Retrieved {len(all_ids)} vectors from collection '{collection_name}'") # Return in GetResult format # The Open WebUI GetResult expects lists of lists, so we wrap each list if all_ids: - return GetResult( - ids=[all_ids], documents=[all_documents], metadatas=[all_metadatas] - ) + return GetResult(ids=[all_ids], documents=[all_documents], metadatas=[all_metadatas]) else: return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) except Exception as e: - log.error( - f"Error retrieving vectors from collection '{collection_name}': {str(e)}" - ) + log.error(f"Error retrieving vectors from collection '{collection_name}': {str(e)}") # Handle specific AWS exceptions - if hasattr(e, "response") and "Error" in e.response: - error_code = e.response["Error"]["Code"] - if error_code == "NotFoundException": + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error']['Code'] + if error_code == 'NotFoundException': log.warning(f"Collection '{collection_name}' not found") return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) - elif error_code == "AccessDeniedException": - log.error( - f"Access denied for collection '{collection_name}'. Check permissions." - ) + elif error_code == 'AccessDeniedException': + log.error(f"Access denied for collection '{collection_name}'. Check permissions.") return GetResult(ids=[[]], documents=[[]], metadatas=[[]]) raise @@ -609,20 +567,16 @@ class S3VectorClient(VectorDBBase): """ if not self.has_collection(collection_name): - log.warning( - f"Collection '{collection_name}' does not exist, nothing to delete" - ) + log.warning(f"Collection '{collection_name}' does not exist, nothing to delete") return # Check if this is a knowledge collection (not file-specific) - is_knowledge_collection = not collection_name.startswith("file-") + is_knowledge_collection = not collection_name.startswith('file-') try: if ids: # Delete by specific vector IDs/keys - log.info( - f"Deleting {len(ids)} vectors by IDs from collection '{collection_name}'" - ) + log.info(f"Deleting {len(ids)} vectors by IDs from collection '{collection_name}'") self.client.delete_vectors( vectorBucketName=self.bucket_name, indexName=collection_name, @@ -632,15 +586,13 @@ class S3VectorClient(VectorDBBase): elif filter: # Handle filter-based deletion - log.info( - f"Deleting vectors by filter from collection '{collection_name}': {filter}" - ) + log.info(f"Deleting vectors by filter from collection '{collection_name}': {filter}") # If this is a knowledge collection and we have a file_id filter, # also clean up the corresponding file-specific collection - if is_knowledge_collection and "file_id" in filter: - file_id = filter["file_id"] - file_collection_name = f"file-{file_id}" + if is_knowledge_collection and 'file_id' in filter: + file_id = filter['file_id'] + file_collection_name = f'file-{file_id}' if self.has_collection(file_collection_name): log.info( f"Found related file-specific collection '{file_collection_name}', deleting it to prevent duplicates" @@ -652,9 +604,7 @@ class S3VectorClient(VectorDBBase): query_result = self.query(collection_name, filter) if query_result and query_result.ids and query_result.ids[0]: matching_ids = query_result.ids[0] - log.info( - f"Found {len(matching_ids)} vectors matching filter, deleting them" - ) + log.info(f'Found {len(matching_ids)} vectors matching filter, deleting them') # Delete the matching vectors by ID self.client.delete_vectors( @@ -662,17 +612,13 @@ class S3VectorClient(VectorDBBase): indexName=collection_name, keys=matching_ids, ) - log.info( - f"Deleted {len(matching_ids)} vectors from index '{collection_name}' using filter" - ) + log.info(f"Deleted {len(matching_ids)} vectors from index '{collection_name}' using filter") else: - log.warning("No vectors found matching the filter criteria") + log.warning('No vectors found matching the filter criteria') else: - log.warning("No IDs or filter provided for deletion") + log.warning('No IDs or filter provided for deletion') except Exception as e: - log.error( - f"Error deleting vectors from collection '{collection_name}': {e}" - ) + log.error(f"Error deleting vectors from collection '{collection_name}': {e}") raise def reset(self) -> None: @@ -681,36 +627,32 @@ class S3VectorClient(VectorDBBase): """ try: - log.warning( - "Reset called - this will delete all vector indexes in the S3 bucket" - ) + log.warning('Reset called - this will delete all vector indexes in the S3 bucket') # List all indexes response = self.client.list_indexes(vectorBucketName=self.bucket_name) - indexes = response.get("indexes", []) + indexes = response.get('indexes', []) if not indexes: - log.warning("No indexes found to delete") + log.warning('No indexes found to delete') return # Delete all indexes deleted_count = 0 for index in indexes: - index_name = index.get("indexName") + index_name = index.get('indexName') if index_name: try: - self.client.delete_index( - vectorBucketName=self.bucket_name, indexName=index_name - ) + self.client.delete_index(vectorBucketName=self.bucket_name, indexName=index_name) deleted_count += 1 - log.info(f"Deleted index: {index_name}") + log.info(f'Deleted index: {index_name}') except Exception as e: log.error(f"Error deleting index '{index_name}': {e}") - log.info(f"Reset completed: deleted {deleted_count} indexes") + log.info(f'Reset completed: deleted {deleted_count} indexes') except Exception as e: - log.error(f"Error during reset: {e}") + log.error(f'Error during reset: {e}') raise def _matches_filter(self, metadata: Dict[str, Any], filter: Dict[str, Any]) -> bool: @@ -723,15 +665,15 @@ class S3VectorClient(VectorDBBase): # Check each filter condition for key, expected_value in filter.items(): # Handle special operators - if key.startswith("$"): - if key == "$and": + if key.startswith('$'): + if key == '$and': # All conditions must match if not isinstance(expected_value, list): continue for condition in expected_value: if not self._matches_filter(metadata, condition): return False - elif key == "$or": + elif key == '$or': # At least one condition must match if not isinstance(expected_value, list): continue @@ -751,22 +693,19 @@ class S3VectorClient(VectorDBBase): if isinstance(expected_value, dict): # Handle comparison operators for op, op_value in expected_value.items(): - if op == "$eq": + if op == '$eq': if actual_value != op_value: return False - elif op == "$ne": + elif op == '$ne': if actual_value == op_value: return False - elif op == "$in": - if ( - not isinstance(op_value, list) - or actual_value not in op_value - ): + elif op == '$in': + if not isinstance(op_value, list) or actual_value not in op_value: return False - elif op == "$nin": + elif op == '$nin': if isinstance(op_value, list) and actual_value in op_value: return False - elif op == "$exists": + elif op == '$exists': if bool(op_value) != (key in metadata): return False # Add more operators as needed diff --git a/backend/open_webui/retrieval/vector/dbs/valkey.py b/backend/open_webui/retrieval/vector/dbs/valkey.py new file mode 100644 index 0000000000..987993dc2b --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/valkey.py @@ -0,0 +1,766 @@ +# NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +# Requires Valkey core >= 9.0.1 with the valkey-search module >= 1.2.0 loaded. + +import atexit +import json +import logging +import re +import struct +from urllib.parse import urlparse + +from open_webui.config import ( + VALKEY_COLLECTION_PREFIX, + VALKEY_DISTANCE_METRIC, + VALKEY_HNSW_EF_CONSTRUCTION, + VALKEY_HNSW_EF_RUNTIME, + VALKEY_HNSW_M, + VALKEY_INDEX_TYPE, + VALKEY_URL, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata + +log = logging.getLogger(__name__) + + +def _import_glide(): + """Lazily import glide_sync so the module can be loaded without valkey-glide-sync installed.""" + try: + from glide_sync import ( + Batch, + DataType, + DistanceMetricType, + FtCreateOptions, + FtSearchLimit, + FtSearchOptions, + GlideClient, + GlideClientConfiguration, + NodeAddress, + RequestError, + ReturnField, + TagField, + TextField, + VectorAlgorithm, + VectorField, + VectorFieldAttributesFlat, + VectorFieldAttributesHnsw, + VectorType, + ) + from glide_sync import ( + ft as glide_ft, + ) + except ImportError as e: + raise ImportError( + 'valkey-glide-sync is required when VECTOR_DB=valkey. ' + 'Install it with: pip install valkey-glide-sync==2.3.1' + ) from e + return { + 'Batch': Batch, + 'DataType': DataType, + 'DistanceMetricType': DistanceMetricType, + 'FtCreateOptions': FtCreateOptions, + 'FtSearchLimit': FtSearchLimit, + 'FtSearchOptions': FtSearchOptions, + 'GlideClient': GlideClient, + 'GlideClientConfiguration': GlideClientConfiguration, + 'NodeAddress': NodeAddress, + 'RequestError': RequestError, + 'ReturnField': ReturnField, + 'TagField': TagField, + 'TextField': TextField, + 'VectorAlgorithm': VectorAlgorithm, + 'VectorField': VectorField, + 'VectorFieldAttributesFlat': VectorFieldAttributesFlat, + 'VectorFieldAttributesHnsw': VectorFieldAttributesHnsw, + 'VectorType': VectorType, + 'glide_ft': glide_ft, + } + + +# valkey-search 1.2.0 requires Valkey core 9.0.1+ per upstream release notes. +# Unlike RediSearch (dialects 1-4), valkey-search only implements DIALECT 2 — GLIDE's +# FtSearchOptions doesn't expose a dialect parameter because it's always dialect 2. +MIN_VALKEY_VERSION = (9, 0, 1) +MIN_SEARCH_MODULE_VERSION = (1, 2, 0) + +_VALID_DISTANCE_METRICS = {'COSINE', 'L2', 'IP'} +_NEVER_MATCH_SENTINEL = '__open_webui_valkey_never_match__' + +# Compile once at module load — includes `?` which is a single-char wildcard in TAG queries. +_TAG_SPECIAL_RE = re.compile(r'([,.<>{}\[\]"\':;!@#$%^&*()\-+=~?\\/| \t\n\r])') + + +_SAFE_FIELD_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') + + +def _vector_to_bytes(vector: list[float | int]) -> bytes: + """Pack a list of floats as a float32 little-endian binary blob.""" + return struct.pack(f'<{len(vector)}f', *vector) + + +def _escape_tag_value(value: str) -> str: + """Escape special characters for RediSearch/Valkey-Search TAG field queries.""" + return _TAG_SPECIAL_RE.sub(r'\\\1', str(value)) + + +def _build_filter_expression(filter: dict) -> str: + """Translate a Chroma-style filter dict into a valkey-search filter expression. + + Supports simple equality, $in, $ne, and $eq. Multiple keys are ANDed together. + Raises ValueError on unsupported operators rather than silently matching nothing. + """ + parts = [] + for key, value in filter.items(): + if not _SAFE_FIELD_RE.match(key): + raise ValueError( + f'Invalid filter field name: {key!r}. ' + 'Field names must start with a letter or underscore and contain only alphanumerics/underscores.' + ) + if isinstance(value, dict): + for op, operand in value.items(): + if op == '$in' and isinstance(operand, list): + if not operand: + # Empty $in → match nothing, not "match all". + parts.append(f'@{key}:{{{_NEVER_MATCH_SENTINEL}}}') + continue + escaped = [_escape_tag_value(str(v)) for v in operand] + parts.append(f'@{key}:{{{"|".join(escaped)}}}') + elif op in ('$eq', '$ne'): + prefix = '-' if op == '$ne' else '' + parts.append(f'{prefix}@{key}:{{{_escape_tag_value(str(operand))}}}') + else: + raise ValueError( + f'Unsupported filter operator {op!r} for key {key!r}. Supported operators: $in, $ne, $eq.' + ) + else: + parts.append(f'@{key}:{{{_escape_tag_value(str(value))}}}') + return ' '.join(parts) + + +def _decode(value) -> str: + """Decode bytes to str; pass through str unchanged.""" + if isinstance(value, (bytes, bytearray)): + return value.decode() + return str(value) if value is not None else '' + + +class ValkeyClient(VectorDBBase): + def __init__(self): + if not VALKEY_URL: + raise ValueError( + 'VALKEY_URL is required when VECTOR_DB=valkey. ' + 'Set it to your Valkey server URL (e.g., valkey://localhost:6379).' + ) + + # Lazily import glide_sync — only needed when this backend is actually used. + self._g = _import_glide() + + # Validate distance metric at init — invalid values pass through to FT.CREATE + # and fail with a cryptic server error. + metric = VALKEY_DISTANCE_METRIC.upper() + if metric not in _VALID_DISTANCE_METRICS: + raise ValueError( + f'Invalid VALKEY_DISTANCE_METRIC={VALKEY_DISTANCE_METRIC!r}. ' + f'Must be one of: {", ".join(sorted(_VALID_DISTANCE_METRICS))}.' + ) + + DistanceMetricType = self._g['DistanceMetricType'] + self._distance_metric_map = { + 'COSINE': DistanceMetricType.COSINE, + 'L2': DistanceMetricType.L2, + 'IP': DistanceMetricType.IP, + } + + self.collection_prefix = VALKEY_COLLECTION_PREFIX + self.index_type = VALKEY_INDEX_TYPE + self.distance_metric = metric + + parsed = urlparse(VALKEY_URL) + host = parsed.hostname or 'localhost' + port = parsed.port or 6379 + db = int(parsed.path.lstrip('/') or 0) + + GlideClientConfiguration = self._g['GlideClientConfiguration'] + NodeAddress = self._g['NodeAddress'] + GlideClient = self._g['GlideClient'] + + config = GlideClientConfiguration( + addresses=[NodeAddress(host=host, port=port)], + database_id=db if db else None, + request_timeout=5000, + client_name="open_webui_vector_store_client", + ) + try: + self.client = GlideClient.create(config) + except Exception as e: + raise ConnectionError(f'Failed to connect to Valkey at {host}:{port}: {e}') from e + + # Separate client for batch writes — large HSET payloads on the multiplexed + # connection can starve concurrent reads. + batch_config = GlideClientConfiguration( + addresses=[NodeAddress(host=host, port=port)], + database_id=db if db else None, + request_timeout=10000, # 10s — HNSW indexing can take 1-4s per vector + client_name="open_webui_vector_store_batch_client", + ) + try: + self.batch_client = GlideClient.create(batch_config) + except Exception as e: + raise ConnectionError(f'Failed to create batch write client for Valkey at {host}:{port}: {e}') from e + + try: + self.client.ping() + except Exception as e: + raise ConnectionError(f'Failed to ping Valkey at {host}:{port}: {e}') from e + + # Catch misconfigured deployments at startup (e.g., valkey-bundle:9.0.1 ships + # valkey-search 1.0.0 which lacks TEXT fields and filter-only FT.SEARCH). + self._check_core_version() + self._check_search_module() + + atexit.register(self.close) + + def close(self) -> None: + """Close both GLIDE clients, flushing in-flight requests.""" + try: + self.client.close() + except Exception: + pass + try: + self.batch_client.close() + except Exception: + pass + + # ----- version checks ---------------------------------------------------- + + @staticmethod + def _parse_semver(version_str: str) -> tuple[int, int, int] | None: + if not version_str: + return None + m = re.match(r'^(\d+)\.(\d+)\.(\d+)', version_str) + return (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None + + @staticmethod + def _format_version(v: tuple[int, int, int]) -> str: + return f'{v[0]}.{v[1]}.{v[2]}' + + def _check_core_version(self) -> None: + try: + info_raw = self.client.info() + except Exception as e: + log.warning(f'Could not fetch Valkey INFO for version check, proceeding: {e}') + return + + raw = None + text = _decode(info_raw) if info_raw else '' + redis_fallback = None + for line in text.splitlines(): + if line.startswith('valkey_version:'): + raw = line.split(':', 1)[1].strip() + break + if line.startswith('redis_version:') and redis_fallback is None: + redis_fallback = line.split(':', 1)[1].strip() + if raw is None: + raw = redis_fallback + version = self._parse_semver(raw) if raw else None + + if version is None: + log.warning( + f'Could not determine Valkey core version (raw={raw!r}); proceeding but ' + f'minimum {self._format_version(MIN_VALKEY_VERSION)} is required.' + ) + elif version < MIN_VALKEY_VERSION: + raise RuntimeError( + f'Valkey core {self._format_version(version)} is below the minimum required version ' + f'{self._format_version(MIN_VALKEY_VERSION)}. valkey-search 1.2.0 requires Valkey core ' + '9.0.1 or later. Upgrade your server or use valkey-bundle:9.1.0-rc2+.' + ) + log.info(f'Valkey core version: {self._format_version(version) if version else "unknown"}') + + def _check_search_module(self) -> None: + try: + modules = self.client.custom_command(['MODULE', 'LIST']) + except Exception as e: + log.warning( + f'Could not list modules on the Valkey server ({e}); proceeding but ' + f'valkey-search >= {self._format_version(MIN_SEARCH_MODULE_VERSION)} is required.' + ) + return + + # MODULE LIST returns [{b'name': b'search', b'ver': 66048, ...}] + # ver encoding: major*10000 + minor*100 + patch + search_version: tuple[int, int, int] | None = None + module_present = False + raw_ver = None + for entry in modules or []: + if isinstance(entry, dict): + name = _decode(entry.get(b'name') or entry.get('name') or '') + raw_ver = entry.get(b'ver') or entry.get('ver', 0) + else: + parsed = self._decode_kv_pairs(entry) + name = parsed.get('name', '') + raw_ver = parsed.get('ver', 0) + if name.lower() == 'search': + module_present = True + try: + ver_int = int(raw_ver) + search_version = (ver_int // 10000, (ver_int % 10000) // 100, ver_int % 100) + except (TypeError, ValueError): + search_version = None + break + + if not module_present: + raise RuntimeError( + 'The valkey-search module is not loaded on the Valkey server. ' + f'This backend requires valkey-search >= {self._format_version(MIN_SEARCH_MODULE_VERSION)}. ' + 'Use valkey-bundle:9.1.0-rc2+ or load libsearch.so via --loadmodule on a Valkey 9.0.1+ server.' + ) + if search_version is None: + log.warning( + f'valkey-search module is loaded but version could not be parsed (raw={raw_ver!r}); ' + f'proceeding but minimum {self._format_version(MIN_SEARCH_MODULE_VERSION)} is required.' + ) + elif search_version < MIN_SEARCH_MODULE_VERSION: + raise RuntimeError( + f'valkey-search {self._format_version(search_version)} is below the minimum required ' + f'version {self._format_version(MIN_SEARCH_MODULE_VERSION)}. Earlier versions lack the ' + 'TEXT field type and filter-only FT.SEARCH support required by this backend. ' + 'Upgrade to valkey-bundle:9.1.0-rc2+ or load valkey-search 1.2.0+ as a module.' + ) + log.info(f'valkey-search version: {self._format_version(search_version) if search_version else "unknown"}') + + def _index_name(self, collection_name: str) -> str: + return f'idx:{self.collection_prefix}:{collection_name}' + + def _key_prefix(self, collection_name: str) -> str: + return f'{self.collection_prefix}:{collection_name}:' + + def _item_key(self, collection_name: str, item_id: str) -> str: + return f'{self.collection_prefix}:{collection_name}:{item_id}' + + def _create_index(self, collection_name: str, dimension: int) -> None: + """Create an FT index for a collection with the given vector dimension.""" + g = self._g + index_name = self._index_name(collection_name) + prefix = self._key_prefix(collection_name) + distance_metric = self._distance_metric_map[self.distance_metric] + + if self.index_type == 'HNSW': + vector_attrs = g['VectorFieldAttributesHnsw']( + dimensions=dimension, + distance_metric=distance_metric, + type=g['VectorType'].FLOAT32, + number_of_edges=VALKEY_HNSW_M, + vectors_examined_on_construction=VALKEY_HNSW_EF_CONSTRUCTION, + vectors_examined_on_runtime=VALKEY_HNSW_EF_RUNTIME, + ) + algo = g['VectorAlgorithm'].HNSW + else: + if self.index_type != 'FLAT': + log.warning(f'Unrecognized VALKEY_INDEX_TYPE={self.index_type!r}; falling back to FLAT.') + vector_attrs = g['VectorFieldAttributesFlat']( + dimensions=dimension, + distance_metric=distance_metric, + type=g['VectorType'].FLOAT32, + ) + algo = g['VectorAlgorithm'].FLAT + + schema = [ + g['VectorField'](name='vector', algorithm=algo, attributes=vector_attrs), + g['TextField'](name='text'), + g['TagField'](name='id'), + g['TextField'](name='metadata_json'), + g['TagField'](name='hash'), + g['TagField'](name='file_id'), + g['TagField'](name='source'), + g['TagField'](name='knowledge_base_id'), + ] + + options = g['FtCreateOptions'](data_type=g['DataType'].HASH, prefixes=[prefix]) + + try: + g['glide_ft'].create(self.client, index_name, schema, options) + log.info( + f'Created Valkey index {index_name} with dimension={dimension}, ' + f'type={self.index_type}, metric={self.distance_metric}' + ) + except g['RequestError'] as e: + if 'already exists' in str(e).lower(): + log.debug(f'Index {index_name} already exists, skipping creation.') + else: + raise + + def _verify_collection_dimension(self, collection_name: str, dimension: int) -> None: + index_name = self._index_name(collection_name) + try: + info = self._g['glide_ft'].info(self.client, index_name) + except Exception as e: + log.warning(f'Could not FT.INFO {index_name} for dimension check, skipping: {e}') + return + + # ft.info response has nested structure: b'attributes' → list of fields, + # each field is [k1, v1, ...] with a nested 'index' sub-list containing 'dimensions'. + existing = None + attrs = None + if isinstance(info, dict): + attrs = info.get(b'attributes') or info.get('attributes') + elif isinstance(info, (list, tuple)): + attrs = self._find_in_kv_pairs(info, 'attributes', case_insensitive=True) + + for attr in attrs or []: + if not isinstance(attr, (list, tuple)): + continue + field_type = self._find_in_kv_pairs(attr, 'type', case_insensitive=True) + if _decode(field_type).upper() != 'VECTOR': + continue + index_params = self._find_in_kv_pairs(attr, 'index', case_insensitive=True) + if index_params and isinstance(index_params, (list, tuple)): + dim_raw = self._find_in_kv_pairs(index_params, 'dimensions', case_insensitive=True) + if dim_raw is not None: + try: + existing = int(dim_raw) + except (ValueError, TypeError): + pass + break + + if existing is None: + log.warning( + f'Could not determine vector dimension for {index_name} from FT.INFO response, ' + 'skipping dimension check.' + ) + return + if existing != dimension: + raise ValueError( + f'Collection {collection_name!r} was created with dim={existing}, refusing to ' + f'insert vectors with dim={dimension}. Recreate the collection (e.g., via ' + 'VECTOR_DB_CLIENT.delete_collection) if you intend to switch embedding models.' + ) + + def has_collection(self, collection_name: str) -> bool: + index_name = self._index_name(collection_name) + try: + self._g['glide_ft'].info(self.client, index_name) + return True + except self._g['RequestError'] as e: + msg = str(e).lower() + if 'no such index' in msg or 'unknown index' in msg or 'not found in database' in msg: + return False + log.warning(f'Unexpected FT.INFO response for collection {collection_name}: {e}') + raise + + def delete_collection(self, collection_name: str): + index_name = self._index_name(collection_name) + try: + self._g['glide_ft'].dropindex(self.client, index_name) + log.info(f'Dropped index {index_name}') + except self._g['RequestError'] as e: + log.debug(f'Could not drop index {index_name}: {e}') + + self._delete_keys_by_prefix(self._key_prefix(collection_name)) + + def insert(self, collection_name: str, items: list[VectorItem]): + if not items: + return + + dimension = len(items[0]['vector']) + if not self.has_collection(collection_name): + self._create_index(collection_name, dimension) + else: + self._verify_collection_dimension(collection_name, dimension) + + # Individual HSET rather than Batch.exec() — each command gets its own timeout. + # HNSW indexing can take 1-4s per vector (ef_construction=200), and Batch.exec() + # applies a single timeout to ALL commands, causing all-or-nothing failures on + # large inserts. + for item in items: + metadata = process_metadata(item['metadata']) if item.get('metadata') else {} + mapping = { + 'id': item['id'], + 'vector': _vector_to_bytes(item['vector']), + 'text': item['text'], + 'metadata_json': json.dumps(metadata), + # `or ''` prevents indexing literal 'None' as a TAG value, which would + # poison $ne / equality queries. + 'hash': str(metadata.get('hash') or ''), + 'file_id': str(metadata.get('file_id') or ''), + 'source': str(metadata.get('source') or ''), + 'knowledge_base_id': str(metadata.get('knowledge_base_id') or ''), + } + self.batch_client.hset(self._item_key(collection_name, item['id']), mapping) + + log.debug(f'Inserted {len(items)} items into collection {collection_name}') + + def upsert(self, collection_name: str, items: list[VectorItem]): + self.insert(collection_name, items) + + def search( + self, + collection_name: str, + vectors: list[list[float | int]], + filter: dict | None = None, + limit: int = 10, + ) -> SearchResult | None: + if not vectors: + return None + if not self.has_collection(collection_name): + return None + + filter_expr = _build_filter_expression(filter) if filter else '' + query_str = ( + f'({filter_expr})=>[KNN {limit} @vector $query_vec]' + if filter_expr + else f'*=>[KNN {limit} @vector $query_vec]' + ) + + g = self._g + try: + opts = g['FtSearchOptions']( + params={'query_vec': _vector_to_bytes(vectors[0])}, + limit=g['FtSearchLimit'](offset=0, count=limit), + ) + result = g['glide_ft'].search(self.client, self._index_name(collection_name), query_str, opts) + except g['RequestError'] as e: + log.error(f'Valkey search error on collection {collection_name}: {e}') + return None + + return self._parse_glide_search_response(result, include_score=True) + + def query(self, collection_name: str, filter: dict, limit: int | None = None) -> GetResult | None: + if not self.has_collection(collection_name): + return None + if not filter: + return self.get(collection_name, limit=limit) + + query_str = _build_filter_expression(filter) + if not query_str: + return self.get(collection_name, limit=limit) + + # Hard cap when no limit provided — FT.SEARCH requires a finite count. + effective_limit = limit if limit and limit > 0 else 10000 + if not (limit and limit > 0): + log.warning( + f'query() called without a limit on collection {collection_name}; ' + f'capping at {effective_limit} results. Pass an explicit limit to avoid silent truncation.' + ) + + g = self._g + try: + opts = g['FtSearchOptions']( + return_fields=[ + g['ReturnField'](field_identifier='id'), + g['ReturnField'](field_identifier='text'), + g['ReturnField'](field_identifier='metadata_json'), + ], + limit=g['FtSearchLimit'](offset=0, count=effective_limit), + ) + result = g['glide_ft'].search(self.client, self._index_name(collection_name), query_str, opts) + except g['RequestError'] as e: + log.error(f'Valkey query error on collection {collection_name}: {e}') + return None + + return self._parse_glide_search_response(result, include_score=False) + + def get(self, collection_name: str, limit: int | None = None) -> GetResult | None: + if not self.has_collection(collection_name): + return None + + # FT.SEARCH "*" wildcard not yet in a tagged valkey-search release (tracked in #957). + # SCAN fallback is acceptable here — get() is not on the hot search path. + prefix = self._key_prefix(collection_name) + ids, documents, metadatas = [], [], [] + cursor = '0' + while True: + scan_result = self.client.scan(cursor=cursor, match=f'{prefix}*', count=500) + cursor = _decode(scan_result[0]) + keys = scan_result[1] + if keys: + batch = self._g['Batch'](is_atomic=False) + for key in keys: + batch.hgetall(key) + results = self.client.exec(batch, raise_on_error=False) or [] + for fields in results: + if not fields: + continue + ids.append(_decode(fields.get(b'id', b''))) + documents.append(_decode(fields.get(b'text', b''))) + try: + metadatas.append(json.loads(_decode(fields.get(b'metadata_json', b'{}')))) + except (json.JSONDecodeError, TypeError): + metadatas.append({}) + if limit is not None and limit > 0 and len(ids) >= limit: + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + if cursor == '0': + break + + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + def delete( + self, + collection_name: str, + ids: list[str] | None = None, + filter: dict | None = None, + ): + if ids: + keys = [self._item_key(collection_name, item_id) for item_id in ids] + try: + self.batch_client.delete(keys) + except self._g['RequestError'] as e: + log.error(f'Valkey delete error on collection {collection_name}: {e}') + return + + if not filter: + return + + filter_expr = _build_filter_expression(filter) + if not filter_expr: + return + + index_name = self._index_name(collection_name) + page_size = 10000 + g = self._g + while True: + try: + opts = g['FtSearchOptions']( + return_fields=[g['ReturnField'](field_identifier='id')], + limit=g['FtSearchLimit'](offset=0, count=page_size), + ) + result = g['glide_ft'].search(self.client, index_name, filter_expr, opts) + except g['RequestError'] as e: + log.error(f'Valkey delete-by-filter error on collection {collection_name}: {e}') + return + + if not result or result[0] == 0: + return + + keys_map = result[1] if len(result) > 1 else {} + keys = [_decode(k) for k in keys_map.keys()] if isinstance(keys_map, dict) else [] + if not keys: + return + self.batch_client.delete(keys) + if len(keys) < page_size: + return + + def reset(self): + glide_ft = self._g['glide_ft'] + collections: list[str] = [] + try: + indexes = glide_ft.list(self.client) or [] + idx_prefix = f'idx:{self.collection_prefix}:' + for idx in indexes: + name = _decode(idx) + if name.startswith(idx_prefix): + collections.append(name[len(idx_prefix) :]) + try: + glide_ft.dropindex(self.client, idx) + log.info(f'Dropped index: {name}') + except Exception as e: + log.error(f'Error dropping index {name}: {e}') + except Exception as e: + log.error(f'Error listing indexes during reset: {e}') + + for collection in collections: + self._delete_keys_by_prefix(self._key_prefix(collection)) + log.info(f'Valkey vector store reset complete (prefix: {self.collection_prefix})') + + def _delete_keys_by_prefix(self, prefix: str) -> None: + cursor = '0' + while True: + scan_result = self.client.scan(cursor=cursor, match=f'{prefix}*', count=500) + cursor = _decode(scan_result[0]) + keys = scan_result[1] + if keys: + self.batch_client.delete(keys) + if cursor == '0': + break + + @staticmethod + def _decode_kv_pairs(fields) -> dict: + """Decode a flat [k1, v1, k2, v2, ...] wire array into a dict.""" + if not fields: + return {} + if len(fields) % 2 != 0: + fields = fields[:-1] + out = {} + for k, v in zip(fields[::2], fields[1::2]): + key = _decode(k) + if isinstance(v, (bytes, bytearray)): + try: + val = v.decode() + except UnicodeDecodeError: + val = v + else: + val = v + out[key] = val + return out + + @staticmethod + def _find_in_kv_pairs(pairs, target: str, case_insensitive: bool = False): + """Look up a value in a flat [k1, v1, k2, v2, ...] array or dict.""" + if isinstance(pairs, dict): + needle = target.lower() if case_insensitive else target + for k, v in pairs.items(): + key = _decode(k) + if (key.lower() if case_insensitive else key) == needle: + return v + return None + if not isinstance(pairs, (list, tuple)) or len(pairs) < 2: + return None + needle = target.lower() if case_insensitive else target + for j in range(0, len(pairs) - 1, 2): + key = _decode(pairs[j]) + if (key.lower() if case_insensitive else key) == needle: + return pairs[j + 1] + return None + + def _parse_glide_search_response(self, result, include_score: bool) -> SearchResult | GetResult | None: + """Parse ft.search response: [total_count, {key: {field: value, ...}, ...}]""" + empty_search = SearchResult(ids=[[]], distances=[[]], documents=[[]], metadatas=[[]]) + empty_get = GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + if not result or result[0] == 0: + return empty_search if include_score else empty_get + + docs_map = result[1] if len(result) > 1 else {} + if not isinstance(docs_map, dict): + return empty_search if include_score else empty_get + + ids, documents, metadatas, distances = [], [], [], [] + for _key, fields in docs_map.items(): + if not isinstance(fields, dict): + continue + ids.append(_decode(fields.get(b'id', b''))) + documents.append(_decode(fields.get(b'text', b''))) + try: + metadatas.append(json.loads(_decode(fields.get(b'metadata_json', b'{}')))) + except (json.JSONDecodeError, TypeError): + metadatas.append({}) + + if include_score: + try: + raw_score = _decode(fields.get(b'__vector_score', b'0')) + distances.append(self._normalize_score(float(raw_score))) + except (ValueError, TypeError): + distances.append(0.0) + + if not include_score: + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + return SearchResult(ids=[ids], distances=[distances], documents=[documents], metadatas=[metadatas]) + + def _normalize_score(self, score: float) -> float: + """Convert valkey-search __vector_score (a distance, lower = more similar) to [0, 1] similarity. + + All metrics return distance: COSINE/IP in [0, 2] for unit vectors, L2 in [0, ∞). + """ + if self.distance_metric == 'COSINE': + # COSINE distance: 0 (identical) → 2 (opposite). Map to similarity [1, -1], clamp [0, 1]. + return max(0.0, min(1.0, 1.0 - score)) + if self.distance_metric == 'L2': + # L2 distance: 0 (identical) → ∞. + return 1.0 / (1.0 + score) + # IP: distance = 1 - inner_product + return max(0.0, min(1.0, 1.0 - score)) diff --git a/backend/open_webui/retrieval/vector/dbs/weaviate.py b/backend/open_webui/retrieval/vector/dbs/weaviate.py index d204e8293a..a896d8ed5e 100644 --- a/backend/open_webui/retrieval/vector/dbs/weaviate.py +++ b/backend/open_webui/retrieval/vector/dbs/weaviate.py @@ -1,21 +1,29 @@ -import weaviate +""" +NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +""" + import re import uuid from typing import Any, Dict, List, Optional, Union -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) -from open_webui.retrieval.vector.utils import process_metadata +import weaviate from open_webui.config import ( + WEAVIATE_API_KEY, + WEAVIATE_GRPC_HOST, + WEAVIATE_GRPC_PORT, + WEAVIATE_GRPC_SECURE, WEAVIATE_HTTP_HOST, WEAVIATE_HTTP_PORT, - WEAVIATE_GRPC_PORT, - WEAVIATE_API_KEY, + WEAVIATE_HTTP_SECURE, + WEAVIATE_SKIP_INIT_CHECKS, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata def _convert_uuids_to_strings(obj: Any) -> Any: @@ -52,43 +60,43 @@ class WeaviateClient(VectorDBBase): try: # Build connection parameters connection_params = { - "host": WEAVIATE_HTTP_HOST, - "port": WEAVIATE_HTTP_PORT, - "grpc_port": WEAVIATE_GRPC_PORT, + 'http_host': WEAVIATE_HTTP_HOST, + 'http_port': WEAVIATE_HTTP_PORT, + 'http_secure': WEAVIATE_HTTP_SECURE, + 'grpc_host': WEAVIATE_GRPC_HOST, + 'grpc_port': WEAVIATE_GRPC_PORT, + 'grpc_secure': WEAVIATE_GRPC_SECURE, + 'skip_init_checks': WEAVIATE_SKIP_INIT_CHECKS, } # Only add auth_credentials if WEAVIATE_API_KEY exists and is not empty if WEAVIATE_API_KEY: - connection_params["auth_credentials"] = ( - weaviate.classes.init.Auth.api_key(WEAVIATE_API_KEY) - ) + connection_params['auth_credentials'] = weaviate.classes.init.Auth.api_key(WEAVIATE_API_KEY) - self.client = weaviate.connect_to_local(**connection_params) + self.client = weaviate.connect_to_custom(**connection_params) self.client.connect() except Exception as e: - raise ConnectionError(f"Failed to connect to Weaviate: {e}") from e + raise ConnectionError(f'Failed to connect to Weaviate: {e}') from e def _sanitize_collection_name(self, collection_name: str) -> str: """Sanitize collection name to be a valid Weaviate class name.""" if not isinstance(collection_name, str) or not collection_name.strip(): - raise ValueError("Collection name must be a non-empty string") + raise ValueError('Collection name must be a non-empty string') # Requirements for a valid Weaviate class name: # The collection name must begin with a capital letter. # The name can only contain letters, numbers, and the underscore (_) character. Spaces are not allowed. # Replace hyphens with underscores and keep only alphanumeric characters - name = re.sub(r"[^a-zA-Z0-9_]", "", collection_name.replace("-", "_")) - name = name.strip("_") + name = re.sub(r'[^a-zA-Z0-9_]', '', collection_name.replace('-', '_')) + name = name.strip('_') if not name: - raise ValueError( - "Could not sanitize collection name to be a valid Weaviate class name" - ) + raise ValueError('Could not sanitize collection name to be a valid Weaviate class name') # Ensure it starts with a letter and is capitalized if not name[0].isalpha(): - name = "C" + name + name = 'C' + name return name[0].upper() + name[1:] @@ -106,9 +114,7 @@ class WeaviateClient(VectorDBBase): name=collection_name, vector_config=weaviate.classes.config.Configure.Vectors.self_provided(), properties=[ - weaviate.classes.config.Property( - name="text", data_type=weaviate.classes.config.DataType.TEXT - ), + weaviate.classes.config.Property(name='text', data_type=weaviate.classes.config.DataType.TEXT), ], ) @@ -121,19 +127,15 @@ class WeaviateClient(VectorDBBase): with collection.batch.fixed_size(batch_size=100) as batch: for item in items: - item_uuid = str(uuid.uuid4()) if not item["id"] else str(item["id"]) + item_uuid = str(uuid.uuid4()) if not item['id'] else str(item['id']) - properties = {"text": item["text"]} - if item["metadata"]: - clean_metadata = _convert_uuids_to_strings( - process_metadata(item["metadata"]) - ) - clean_metadata.pop("text", None) + properties = {'text': item['text']} + if item['metadata']: + clean_metadata = _convert_uuids_to_strings(process_metadata(item['metadata'])) + clean_metadata.pop('text', None) properties.update(clean_metadata) - batch.add_object( - properties=properties, uuid=item_uuid, vector=item["vector"] - ) + batch.add_object(properties=properties, uuid=item_uuid, vector=item['vector']) def upsert(self, collection_name: str, items: List[VectorItem]) -> None: sane_collection_name = self._sanitize_collection_name(collection_name) @@ -144,19 +146,15 @@ class WeaviateClient(VectorDBBase): with collection.batch.fixed_size(batch_size=100) as batch: for item in items: - item_uuid = str(item["id"]) if item["id"] else None + item_uuid = str(item['id']) if item['id'] else None - properties = {"text": item["text"]} - if item["metadata"]: - clean_metadata = _convert_uuids_to_strings( - process_metadata(item["metadata"]) - ) - clean_metadata.pop("text", None) + properties = {'text': item['text']} + if item['metadata']: + clean_metadata = _convert_uuids_to_strings(process_metadata(item['metadata'])) + clean_metadata.pop('text', None) properties.update(clean_metadata) - batch.add_object( - properties=properties, uuid=item_uuid, vector=item["vector"] - ) + batch.add_object(properties=properties, uuid=item_uuid, vector=item['vector']) def search( self, @@ -193,16 +191,12 @@ class WeaviateClient(VectorDBBase): for obj in response.objects: properties = dict(obj.properties) if obj.properties else {} - documents.append(properties.pop("text", "")) + documents.append(properties.pop('text', '')) metadatas.append(_convert_uuids_to_strings(properties)) # Weaviate has cosine distance, 2 (worst) -> 0 (best). Re-ordering to 0 -> 1 raw_distances = [ - ( - obj.metadata.distance - if obj.metadata and obj.metadata.distance - else 2.0 - ) + (obj.metadata.distance if obj.metadata and obj.metadata.distance else 2.0) for obj in response.objects ] distances = [(2 - dist) / 2 for dist in raw_distances] @@ -219,16 +213,14 @@ class WeaviateClient(VectorDBBase): return SearchResult( **{ - "ids": result_ids, - "documents": result_documents, - "metadatas": result_metadatas, - "distances": result_distances, + 'ids': result_ids, + 'documents': result_documents, + 'metadatas': result_metadatas, + 'distances': result_distances, } ) - def query( - self, collection_name: str, filter: Dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: sane_collection_name = self._sanitize_collection_name(collection_name) if not self.client.collections.exists(sane_collection_name): return None @@ -238,21 +230,15 @@ class WeaviateClient(VectorDBBase): weaviate_filter = None if filter: for key, value in filter.items(): - prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal( - value - ) + prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal(value) weaviate_filter = ( prop_filter if weaviate_filter is None - else weaviate.classes.query.Filter.all_of( - [weaviate_filter, prop_filter] - ) + else weaviate.classes.query.Filter.all_of([weaviate_filter, prop_filter]) ) try: - response = collection.query.fetch_objects( - filters=weaviate_filter, limit=limit - ) + response = collection.query.fetch_objects(filters=weaviate_filter, limit=limit) ids = [str(obj.uuid) for obj in response.objects] documents = [] @@ -260,14 +246,14 @@ class WeaviateClient(VectorDBBase): for obj in response.objects: properties = dict(obj.properties) if obj.properties else {} - documents.append(properties.pop("text", "")) + documents.append(properties.pop('text', '')) metadatas.append(_convert_uuids_to_strings(properties)) return GetResult( **{ - "ids": [ids], - "documents": [documents], - "metadatas": [metadatas], + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], } ) except Exception: @@ -285,7 +271,7 @@ class WeaviateClient(VectorDBBase): for item in collection.iterator(): ids.append(str(item.uuid)) properties = dict(item.properties) if item.properties else {} - documents.append(properties.pop("text", "")) + documents.append(properties.pop('text', '')) metadatas.append(_convert_uuids_to_strings(properties)) if not ids: @@ -293,9 +279,9 @@ class WeaviateClient(VectorDBBase): return GetResult( **{ - "ids": [ids], - "documents": [documents], - "metadatas": [metadatas], + 'ids': [ids], + 'documents': [documents], + 'metadatas': [metadatas], } ) except Exception: @@ -320,15 +306,11 @@ class WeaviateClient(VectorDBBase): elif filter: weaviate_filter = None for key, value in filter.items(): - prop_filter = weaviate.classes.query.Filter.by_property( - name=key - ).equal(value) + prop_filter = weaviate.classes.query.Filter.by_property(name=key).equal(value) weaviate_filter = ( prop_filter if weaviate_filter is None - else weaviate.classes.query.Filter.all_of( - [weaviate_filter, prop_filter] - ) + else weaviate.classes.query.Filter.all_of([weaviate_filter, prop_filter]) ) if weaviate_filter: diff --git a/backend/open_webui/retrieval/vector/factory.py b/backend/open_webui/retrieval/vector/factory.py index 68595fb595..3080956163 100644 --- a/backend/open_webui/retrieval/vector/factory.py +++ b/backend/open_webui/retrieval/vector/factory.py @@ -1,14 +1,13 @@ +from open_webui.config import ( + ENABLE_MILVUS_MULTITENANCY_MODE, + ENABLE_QDRANT_MULTITENANCY_MODE, + VECTOR_DB, +) from open_webui.retrieval.vector.main import VectorDBBase from open_webui.retrieval.vector.type import VectorType -from open_webui.config import ( - VECTOR_DB, - ENABLE_QDRANT_MULTITENANCY_MODE, - ENABLE_MILVUS_MULTITENANCY_MODE, -) class Vector: - @staticmethod def get_vector(vector_type: str) -> VectorDBBase: """ @@ -57,6 +56,12 @@ class Vector: from open_webui.retrieval.vector.dbs.opengauss import OpenGaussClient return OpenGaussClient() + case VectorType.MARIADB_VECTOR: + from open_webui.retrieval.vector.dbs.mariadb_vector import ( + MariaDBVectorClient, + ) + + return MariaDBVectorClient() case VectorType.ELASTICSEARCH: from open_webui.retrieval.vector.dbs.elasticsearch import ( ElasticsearchClient, @@ -75,8 +80,12 @@ class Vector: from open_webui.retrieval.vector.dbs.weaviate import WeaviateClient return WeaviateClient() + case VectorType.VALKEY: + from open_webui.retrieval.vector.dbs.valkey import ValkeyClient + + return ValkeyClient() case _: - raise ValueError(f"Unsupported vector type: {vector_type}") + raise ValueError(f'Unsupported vector type: {vector_type}') VECTOR_DB_CLIENT = Vector.get_vector(VECTOR_DB) diff --git a/backend/open_webui/retrieval/vector/main.py b/backend/open_webui/retrieval/vector/main.py index a76fec9956..38ea699514 100644 --- a/backend/open_webui/retrieval/vector/main.py +++ b/backend/open_webui/retrieval/vector/main.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Union +from pydantic import BaseModel + class VectorItem(BaseModel): id: str @@ -63,9 +64,7 @@ class VectorDBBase(ABC): pass @abstractmethod - def query( - self, collection_name: str, filter: Dict, limit: Optional[int] = None - ) -> Optional[GetResult]: + def query(self, collection_name: str, filter: Dict, limit: Optional[int] = None) -> Optional[GetResult]: """Query vectors from a collection using metadata filter.""" pass diff --git a/backend/open_webui/retrieval/vector/type.py b/backend/open_webui/retrieval/vector/type.py index de20133fce..15e6da0f9e 100644 --- a/backend/open_webui/retrieval/vector/type.py +++ b/backend/open_webui/retrieval/vector/type.py @@ -2,14 +2,16 @@ from enum import StrEnum class VectorType(StrEnum): - MILVUS = "milvus" - QDRANT = "qdrant" - CHROMA = "chroma" - PINECONE = "pinecone" - ELASTICSEARCH = "elasticsearch" - OPENSEARCH = "opensearch" - PGVECTOR = "pgvector" - ORACLE23AI = "oracle23ai" - S3VECTOR = "s3vector" - WEAVIATE = "weaviate" - OPENGAUSS = "opengauss" + MILVUS = 'milvus' + MARIADB_VECTOR = 'mariadb-vector' + QDRANT = 'qdrant' + CHROMA = 'chroma' + PINECONE = 'pinecone' + ELASTICSEARCH = 'elasticsearch' + OPENSEARCH = 'opensearch' + PGVECTOR = 'pgvector' + ORACLE23AI = 'oracle23ai' + S3VECTOR = 's3vector' + WEAVIATE = 'weaviate' + OPENGAUSS = 'opengauss' + VALKEY = 'valkey' diff --git a/backend/open_webui/retrieval/vector/utils.py b/backend/open_webui/retrieval/vector/utils.py index a597390b92..4915b024c3 100644 --- a/backend/open_webui/retrieval/vector/utils.py +++ b/backend/open_webui/retrieval/vector/utils.py @@ -1,28 +1,29 @@ from datetime import datetime -KEYS_TO_EXCLUDE = ["content", "pages", "tables", "paragraphs", "sections", "figures"] +from open_webui.utils.misc import sanitize_text_for_db + +KEYS_TO_EXCLUDE = ['content', 'pages', 'tables', 'paragraphs', 'sections', 'figures'] def filter_metadata(metadata: dict[str, any]) -> dict[str, any]: - metadata = { - key: value for key, value in metadata.items() if key not in KEYS_TO_EXCLUDE - } + # Removes large/redundant fields from metadata dict. + metadata = {key: value for key, value in metadata.items() if key not in KEYS_TO_EXCLUDE} return metadata def process_metadata( metadata: dict[str, any], ) -> dict[str, any]: + # Removes large fields, converts non-serializable types (datetime, list, dict) to strings, + # and sanitizes strings for database storage (strips null bytes and invalid surrogates). + result = {} for key, value in metadata.items(): - # Remove large fields + # Skip large fields if key in KEYS_TO_EXCLUDE: - del metadata[key] - + continue # Convert non-serializable fields to strings - if ( - isinstance(value, datetime) - or isinstance(value, list) - or isinstance(value, dict) - ): - metadata[key] = str(value) - return metadata + if isinstance(value, (datetime, list, dict)): + result[key] = sanitize_text_for_db(str(value)) + else: + result[key] = sanitize_text_for_db(value) + return result diff --git a/backend/open_webui/retrieval/web/azure.py b/backend/open_webui/retrieval/web/azure.py index 3859ccc9b7..5ba57f11c5 100644 --- a/backend/open_webui/retrieval/web/azure.py +++ b/backend/open_webui/retrieval/web/azure.py @@ -1,5 +1,6 @@ import logging from typing import Optional + from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) @@ -40,20 +41,17 @@ def search_azure( from azure.search.documents import SearchClient except ImportError: log.error( - "azure-search-documents package is not installed. " - "Install it with: pip install azure-search-documents" + 'azure-search-documents package is not installed. Install it with: pip install azure-search-documents' ) raise ImportError( - "azure-search-documents is required for Azure AI Search. " - "Install it with: pip install azure-search-documents" + 'azure-search-documents is required for Azure AI Search. ' + 'Install it with: pip install azure-search-documents' ) try: # Create search client with API key authentication credential = AzureKeyCredential(api_key) - search_client = SearchClient( - endpoint=endpoint, index_name=index_name, credential=credential - ) + search_client = SearchClient(endpoint=endpoint, index_name=index_name, credential=credential) # Perform the search results = search_client.search(search_text=query, top=count) @@ -68,42 +66,42 @@ def search_azure( # Try to find URL field (common names) link = ( - result_dict.get("url") - or result_dict.get("link") - or result_dict.get("uri") - or result_dict.get("metadata_storage_path") - or "" + result_dict.get('url') + or result_dict.get('link') + or result_dict.get('uri') + or result_dict.get('metadata_storage_path') + or '' ) # Try to find title field (common names) title = ( - result_dict.get("title") - or result_dict.get("name") - or result_dict.get("metadata_title") - or result_dict.get("metadata_storage_name") + result_dict.get('title') + or result_dict.get('name') + or result_dict.get('metadata_title') + or result_dict.get('metadata_storage_name') or None ) # Try to find content/snippet field (common names) snippet = ( - result_dict.get("content") - or result_dict.get("snippet") - or result_dict.get("description") - or result_dict.get("summary") - or result_dict.get("text") + result_dict.get('content') + or result_dict.get('snippet') + or result_dict.get('description') + or result_dict.get('summary') + or result_dict.get('text') or None ) # Truncate snippet if too long if snippet and len(snippet) > 500: - snippet = snippet[:497] + "..." + snippet = snippet[:497] + '...' if link: # Only add if we found a valid link search_results.append( { - "link": link, - "title": title, - "snippet": snippet, + 'link': link, + 'title': title, + 'snippet': snippet, } ) @@ -114,13 +112,13 @@ def search_azure( # Convert to SearchResult objects return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("snippet"), + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), ) for result in search_results ] except Exception as ex: - log.error(f"Azure AI Search error: {ex}") + log.error(f'Azure AI Search error: {ex}') raise ex diff --git a/backend/open_webui/retrieval/web/bing.py b/backend/open_webui/retrieval/web/bing.py index 4c9822b900..2a38454b9d 100644 --- a/backend/open_webui/retrieval/web/bing.py +++ b/backend/open_webui/retrieval/web/bing.py @@ -1,10 +1,11 @@ +import argparse import logging import os from pprint import pprint from typing import Optional + import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results -import argparse log = logging.getLogger(__name__) """ @@ -21,51 +22,56 @@ def search_bing( filter_list: Optional[list[str]] = None, ) -> list[SearchResult]: mkt = locale - params = {"q": query, "mkt": mkt, "count": count} - headers = {"Ocp-Apim-Subscription-Key": subscription_key} + params = {'q': query, 'mkt': mkt, 'count': count} + headers = {'Ocp-Apim-Subscription-Key': subscription_key} try: response = requests.get(endpoint, headers=headers, params=params) response.raise_for_status() json_response = response.json() - results = json_response.get("webPages", {}).get("value", []) + results = json_response.get('webPages', {}).get('value', []) if filter_list: results = get_filtered_results(results, filter_list) return [ SearchResult( - link=result["url"], - title=result.get("name"), - snippet=result.get("snippet"), + link=result['url'], + title=result.get('name'), + snippet=result.get('snippet'), ) for result in results ] except Exception as ex: - log.error(f"Error: {ex}") + log.error(f'Error: {ex}') raise ex def main(): - parser = argparse.ArgumentParser(description="Search Bing from the command line.") + parser = argparse.ArgumentParser(description='Search Bing from the command line.') parser.add_argument( - "query", + 'query', type=str, - default="Top 10 international news today", - help="The search query.", + default='Top 10 international news today', + help='The search query.', ) + parser.add_argument('--count', type=int, default=10, help='Number of search results to return.') + parser.add_argument('--filter', nargs='*', help='List of filters to apply to the search results.') parser.add_argument( - "--count", type=int, default=10, help="Number of search results to return." - ) - parser.add_argument( - "--filter", nargs="*", help="List of filters to apply to the search results." - ) - parser.add_argument( - "--locale", + '--locale', type=str, - default="en-US", - help="The locale to use for the search, maps to market in api", + default='en-US', + help='The locale to use for the search, maps to market in api', ) args = parser.parse_args() - results = search_bing(args.locale, args.query, args.count, args.filter) + results = search_bing( + os.environ.get('BING_SEARCH_V7_SUBSCRIPTION_KEY', ''), + os.environ.get( + 'BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search' + ), + args.locale, + args.query, + args.count, + args.filter, + ) pprint(results) diff --git a/backend/open_webui/retrieval/web/bocha.py b/backend/open_webui/retrieval/web/bocha.py index 7e3c9b0a40..cb94646310 100644 --- a/backend/open_webui/retrieval/web/bocha.py +++ b/backend/open_webui/retrieval/web/bocha.py @@ -1,8 +1,8 @@ +import json import logging from typing import Optional import requests -import json from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) @@ -10,43 +10,38 @@ log = logging.getLogger(__name__) def _parse_response(response): results = [] - if "data" in response: - data = response["data"] - if "webPages" in data: - webPages = data["webPages"] - if "value" in webPages: + if 'data' in response: + data = response['data'] + if 'webPages' in data: + webPages = data['webPages'] + if 'value' in webPages: results = [ { - "id": item.get("id", ""), - "name": item.get("name", ""), - "url": item.get("url", ""), - "snippet": item.get("snippet", ""), - "summary": item.get("summary", ""), - "siteName": item.get("siteName", ""), - "siteIcon": item.get("siteIcon", ""), - "datePublished": item.get("datePublished", "") - or item.get("dateLastCrawled", ""), + 'id': item.get('id', ''), + 'name': item.get('name', ''), + 'url': item.get('url', ''), + 'snippet': item.get('snippet', ''), + 'summary': item.get('summary', ''), + 'siteName': item.get('siteName', ''), + 'siteIcon': item.get('siteIcon', ''), + 'datePublished': item.get('datePublished', '') or item.get('dateLastCrawled', ''), } - for item in webPages["value"] + for item in webPages['value'] ] return results -def search_bocha( - api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None -) -> list[SearchResult]: +def search_bocha(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: """Search using Bocha's Search API and return the results as a list of SearchResult objects. Args: api_key (str): A Bocha Search API key query (str): The query to search for """ - url = "https://api.bochaai.com/v1/web-search?utm_source=ollama" - headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + url = 'https://api.bochaai.com/v1/web-search?utm_source=ollama' + headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} - payload = json.dumps( - {"query": query, "summary": True, "freshness": "noLimit", "count": count} - ) + payload = json.dumps({'query': query, 'summary': True, 'freshness': 'noLimit', 'count': count}) response = requests.post(url, headers=headers, data=payload, timeout=5) response.raise_for_status() @@ -56,8 +51,6 @@ def search_bocha( results = get_filtered_results(results, filter_list) return [ - SearchResult( - link=result["url"], title=result.get("name"), snippet=result.get("summary") - ) + SearchResult(link=result['url'], title=result.get('name'), snippet=result.get('summary')) for result in results[:count] ] diff --git a/backend/open_webui/retrieval/web/brave.py b/backend/open_webui/retrieval/web/brave.py index 49c8a88e81..f785cbe779 100644 --- a/backend/open_webui/retrieval/web/brave.py +++ b/backend/open_webui/retrieval/web/brave.py @@ -1,51 +1,56 @@ -import logging -import time -from typing import Optional +from __future__ import annotations + +import asyncio +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) +# Brave free-tier rate limit: 1 request per second. +_RATE_LIMIT_RETRY_DELAY = 1.0 -def search_brave( - api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None + +async def search_brave( + api_key: str, + query: str, + count: int, + filter_list: list[str | None] | None = None, ) -> list[SearchResult]: - """Search using Brave's Search API and return the results as a list of SearchResult objects. + """Query the Brave Web Search API and return normalised results. - Args: - api_key (str): A Brave Search API key - query (str): The query to search for + Retries once on HTTP 429 (rate-limit) after a short delay. """ - url = "https://api.search.brave.com/res/v1/web/search" + url = 'https://api.search.brave.com/res/v1/web/search' headers = { - "Accept": "application/json", - "Accept-Encoding": "gzip", - "X-Subscription-Token": api_key, + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip', + 'X-Subscription-Token': api_key, } - params = {"q": query, "count": count} + params = {'q': query, 'count': count} - response = requests.get(url, headers=headers, params=params) + session = await get_session() + async with session.get(url, headers=headers, params=params) as response: + if response.status == 429: + log.info('Brave Search rate-limited (429); retrying after %.1fs', _RATE_LIMIT_RETRY_DELAY) + await asyncio.sleep(_RATE_LIMIT_RETRY_DELAY) + async with session.get(url, headers=headers, params=params) as retry_resp: + retry_resp.raise_for_status() + payload = await retry_resp.json() + else: + response.raise_for_status() + payload = await response.json() - # Handle 429 rate limiting - Brave free tier allows 1 request/second - # If rate limited, wait 1 second and retry once before failing - if response.status_code == 429: - log.info("Brave Search API rate limited (429), retrying after 1 second...") - time.sleep(1) - response = requests.get(url, headers=headers, params=params) - - response.raise_for_status() - - json_response = response.json() - results = json_response.get("web", {}).get("results", []) + web_results = payload.get('web', {}).get('results', []) if filter_list: - results = get_filtered_results(results, filter_list) + web_results = get_filtered_results(web_results, filter_list) return [ SearchResult( - link=result["url"], - title=result.get("title"), - snippet=result.get("description"), + link=item.get('url', ''), + title=item.get('title'), + snippet=item.get('description'), ) - for result in results[:count] + for item in web_results[:count] ] diff --git a/backend/open_webui/retrieval/web/brave_llm_context.py b/backend/open_webui/retrieval/web/brave_llm_context.py new file mode 100644 index 0000000000..ead410e7d4 --- /dev/null +++ b/backend/open_webui/retrieval/web/brave_llm_context.py @@ -0,0 +1,66 @@ +import logging +import time +from typing import Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_brave_llm_context( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + context_tokens: int = 8192, +) -> list[SearchResult]: + """Search using Brave's LLM Context API and return pre-extracted, relevance-scored + page content ready for LLM consumption. + + Uses /res/v1/llm/context instead of /res/v1/web/search. Same API key, same pricing. + Returns full extracted passages per URL rather than short snippets, eliminating + the need for post-search scraping. + + Args: + api_key (str): A Brave Search API key (same key as web search) + query (str): The query to search for + count (int): Maximum number of results to return + filter_list (list[str], optional): Domain filter list + context_tokens (int): Maximum total tokens to retrieve (1024–32768, default 8192) + """ + url = 'https://api.search.brave.com/res/v1/llm/context' + headers = { + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip', + 'X-Subscription-Token': api_key, + } + params = { + 'q': query, + 'count': count, + 'maximum_number_of_tokens': context_tokens, + } + + response = requests.get(url, headers=headers, params=params) + + # Handle 429 rate limiting - same rate limits as web search + if response.status_code == 429: + log.info('Brave LLM Context API rate limited (429), retrying after 1 second...') + time.sleep(1) + response = requests.get(url, headers=headers, params=params) + + response.raise_for_status() + + json_response = response.json() + results = json_response.get('grounding', {}).get('generic', []) + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result['url'], + title=result.get('title'), + snippet='\n\n'.join(result.get('snippets', [])), + ) + for result in results[:count] + ] diff --git a/backend/open_webui/retrieval/web/duckduckgo.py b/backend/open_webui/retrieval/web/duckduckgo.py index 7528418cdb..27d56f6934 100644 --- a/backend/open_webui/retrieval/web/duckduckgo.py +++ b/backend/open_webui/retrieval/web/duckduckgo.py @@ -1,9 +1,11 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging +import urllib.request -from open_webui.retrieval.web.main import SearchResult, get_filtered_results from ddgs import DDGS from ddgs.exceptions import RatelimitException +from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) @@ -11,9 +13,9 @@ log = logging.getLogger(__name__) def search_duckduckgo( query: str, count: int, - filter_list: Optional[list[str]] = None, - concurrent_requests: Optional[int] = None, - backend: Optional[str] = "auto", + filter_list: list[str | None] = None, + concurrent_requests: int | None = None, + backend: str | None = 'auto', ) -> list[SearchResult]: """ Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects. @@ -25,28 +27,34 @@ def search_duckduckgo( Returns: list[SearchResult]: A list of search results """ - # Use the DDGS context manager to create a DDGS object + # The ddgs library (primp-based) does not auto-detect proxy env vars. + # Resolve via stdlib getproxies() — same pattern as the other loaders. + env_proxies = urllib.request.getproxies() + proxy = env_proxies.get('https') or env_proxies.get('http') search_results = [] - with DDGS() as ddgs: + with DDGS(proxy=proxy) as ddgs: if concurrent_requests: ddgs.threads = concurrent_requests # Use the ddgs.text() method to perform the search try: - search_results = ddgs.text( - query, safesearch="moderate", max_results=count, backend=backend - ) + kwargs = {'safesearch': 'moderate', 'max_results': count} + if backend and backend != 'auto': + kwargs['backend'] = backend + results = ddgs.text(query, **kwargs) + search_results = results if results is not None else [] except RatelimitException as e: - log.error(f"RatelimitException: {e}") + log.error(f'RatelimitException: {e}') + search_results = [] if filter_list: search_results = get_filtered_results(search_results, filter_list) # Return the list of search results return [ SearchResult( - link=result["href"], - title=result.get("title"), - snippet=result.get("body"), + link=result['href'], + title=result.get('title'), + snippet=result.get('body'), ) for result in search_results ] diff --git a/backend/open_webui/retrieval/web/exa.py b/backend/open_webui/retrieval/web/exa.py index df9554fab2..860917854e 100644 --- a/backend/open_webui/retrieval/web/exa.py +++ b/backend/open_webui/retrieval/web/exa.py @@ -7,7 +7,7 @@ from open_webui.retrieval.web.main import SearchResult log = logging.getLogger(__name__) -EXA_API_BASE = "https://api.exa.ai" +EXA_API_BASE = 'https://api.exa.ai' @dataclass @@ -31,36 +31,34 @@ def search_exa( count (int): Number of results to return filter_list (Optional[list[str]]): List of domains to filter results by """ - log.info(f"Searching with Exa for query: {query}") + log.info(f'Searching with Exa for query: {query}') - headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} payload = { - "query": query, - "numResults": count or 5, - "includeDomains": filter_list, - "contents": {"text": True, "highlights": True}, - "type": "auto", # Use the auto search type (keyword or neural) + 'query': query, + 'numResults': count or 5, + 'includeDomains': filter_list, + 'contents': {'text': True, 'highlights': True}, + 'type': 'auto', # Use the auto search type (keyword or neural) } try: - response = requests.post( - f"{EXA_API_BASE}/search", headers=headers, json=payload - ) + response = requests.post(f'{EXA_API_BASE}/search', headers=headers, json=payload) response.raise_for_status() data = response.json() results = [] - for result in data["results"]: + for result in data['results']: results.append( ExaResult( - url=result["url"], - title=result["title"], - text=result["text"], + url=result['url'], + title=result['title'], + text=result['text'], ) ) - log.info(f"Found {len(results)} results") + log.info(f'Found {len(results)} results') return [ SearchResult( link=result.url, @@ -70,5 +68,5 @@ def search_exa( for result in results ] except Exception as e: - log.error(f"Error searching Exa: {e}") + log.error(f'Error searching Exa: {e}') return [] diff --git a/backend/open_webui/retrieval/web/external.py b/backend/open_webui/retrieval/web/external.py index 527c918a47..4db37cb645 100644 --- a/backend/open_webui/retrieval/web/external.py +++ b/backend/open_webui/retrieval/web/external.py @@ -1,15 +1,12 @@ import logging -from typing import Optional, List +from typing import List, Optional import requests - from fastapi import Request - - +from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID from open_webui.retrieval.web.main import SearchResult, get_filtered_results from open_webui.utils.headers import include_user_info_headers - log = logging.getLogger(__name__) @@ -24,21 +21,21 @@ def search_external( ) -> List[SearchResult]: try: headers = { - "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot", - "Authorization": f"Bearer {external_api_key}", + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Authorization': f'Bearer {external_api_key}', } headers = include_user_info_headers(headers, user) - chat_id = getattr(request.state, "chat_id", None) + chat_id = getattr(request.state, 'chat_id', None) if chat_id: - headers["X-OpenWebUI-Chat-Id"] = str(chat_id) + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = str(chat_id) response = requests.post( external_url, headers=headers, json={ - "query": query, - "count": count, + 'query': query, + 'count': count, }, ) response.raise_for_status() @@ -47,14 +44,14 @@ def search_external( results = get_filtered_results(results, filter_list) results = [ SearchResult( - link=result.get("link"), - title=result.get("title"), - snippet=result.get("snippet"), + link=result.get('link'), + title=result.get('title'), + snippet=result.get('snippet'), ) for result in results[:count] ] - log.info(f"External search results: {results}") + log.info(f'External search results: {results}') return results except Exception as e: - log.error(f"Error in External search: {e}") + log.error(f'Error in External search: {e}') return [] diff --git a/backend/open_webui/retrieval/web/firecrawl.py b/backend/open_webui/retrieval/web/firecrawl.py index 82635aa8ca..baffb6207d 100644 --- a/backend/open_webui/retrieval/web/firecrawl.py +++ b/backend/open_webui/retrieval/web/firecrawl.py @@ -1,39 +1,232 @@ +from __future__ import annotations + import logging -from typing import Optional, List +import time +from typing import TYPE_CHECKING, Any -from open_webui.retrieval.web.main import SearchResult, get_filtered_results +import requests +from langchain_core.documents import Document +if TYPE_CHECKING: + from open_webui.retrieval.web.main import SearchResult log = logging.getLogger(__name__) +DEFAULT_FIRECRAWL_API_BASE_URL = 'https://api.firecrawl.dev' +FIRECRAWL_RETRY_STATUS_CODES = {429, 500, 502, 503, 504} +FIRECRAWL_MAX_RETRIES = 2 + + +def build_firecrawl_url(base_url: str | None, path: str) -> str: + base_url = (base_url or DEFAULT_FIRECRAWL_API_BASE_URL).rstrip('/') + path = path.lstrip('/') + + if base_url.endswith('/v2'): + return f'{base_url}/{path}' + + return f'{base_url}/v2/{path}' + + +def build_firecrawl_headers(api_key: str | None) -> dict[str, str]: + return { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key or ""}', + } + + +def get_firecrawl_timeout_seconds(timeout: Any) -> float | None: + if timeout in (None, ''): + return None + + try: + timeout = float(timeout) + except (TypeError, ValueError): + return None + + return timeout if timeout > 0 else None + + +def get_firecrawl_scrape_timeout_ms(timeout: Any) -> int | None: + timeout_seconds = get_firecrawl_timeout_seconds(timeout) + if timeout_seconds is None: + return None + + # Firecrawl v2 expects scrape timeouts in milliseconds. + return min(300000, max(1000, int(timeout_seconds * 1000))) + + +def get_firecrawl_client_timeout_seconds(timeout: Any, fallback: float = 60) -> float: + # Keep the local HTTP timeout slightly above Firecrawl's scrape timeout. + return (get_firecrawl_timeout_seconds(timeout) or fallback) + 10 + + +def get_firecrawl_retry_delay(headers: Any, attempt: int) -> float: + retry_after = headers.get('Retry-After') if headers else None + if retry_after: + try: + return min(10.0, max(0.0, float(retry_after))) + except (TypeError, ValueError): + pass + + return min(8.0, float(2**attempt)) + + +def request_firecrawl_json( + method: str, + url: str, + *, + headers: dict[str, str], + json: dict[str, Any] | None = None, + timeout: float | None = None, + verify: bool = True, +) -> dict[str, Any]: + last_error = None + + for attempt in range(FIRECRAWL_MAX_RETRIES + 1): + try: + response = requests.request( + method, + url, + headers=headers, + json=json, + timeout=timeout, + verify=verify, + ) + + if response.status_code in FIRECRAWL_RETRY_STATUS_CODES and attempt < FIRECRAWL_MAX_RETRIES: + delay = get_firecrawl_retry_delay(response.headers, attempt) + log.warning( + 'Firecrawl %s %s returned HTTP %s; retrying in %.1fs', + method, + url, + response.status_code, + delay, + ) + time.sleep(delay) + continue + + response.raise_for_status() + return response.json() + except (requests.ConnectionError, requests.Timeout) as e: + last_error = e + if attempt >= FIRECRAWL_MAX_RETRIES: + break + + delay = get_firecrawl_retry_delay(None, attempt) + log.warning('Firecrawl %s %s failed; retrying in %.1fs: %s', method, url, delay, e) + time.sleep(delay) + + if last_error: + raise last_error + + raise RuntimeError(f'Firecrawl {method} {url} failed without a response') + + +def get_firecrawl_result_url(result: dict[str, Any]) -> str: + metadata = result.get('metadata') or {} + return ( + result.get('url') + or result.get('link') + or metadata.get('url') + or metadata.get('sourceURL') + or metadata.get('source_url') + or '' + ) + + +def scrape_firecrawl_url( + firecrawl_url: str, + firecrawl_api_key: str, + url: str, + *, + verify_ssl: bool = True, + timeout: Any = None, + params: dict[str, Any] | None = None, +) -> Document | None: + payload = { + 'url': url, + 'formats': ['markdown'], + 'skipTlsVerification': not verify_ssl, + 'removeBase64Images': True, + **(params or {}), + } + scrape_timeout_ms = get_firecrawl_scrape_timeout_ms(timeout) + if scrape_timeout_ms is not None: + payload['timeout'] = scrape_timeout_ms + + response = request_firecrawl_json( + 'POST', + build_firecrawl_url(firecrawl_url, 'scrape'), + headers=build_firecrawl_headers(firecrawl_api_key), + json=payload, + timeout=get_firecrawl_client_timeout_seconds(timeout), + verify=verify_ssl, + ) + data = response.get('data') or {} + content = data.get('markdown') or '' + if not isinstance(content, str) or not content.strip(): + return None + + metadata = data.get('metadata') or {} + document_metadata = {'source': get_firecrawl_result_url(data) or url} + if metadata.get('title'): + document_metadata['title'] = metadata['title'] + if metadata.get('description'): + document_metadata['description'] = metadata['description'] + + return Document(page_content=content, metadata=document_metadata) + def search_firecrawl( firecrawl_url: str, firecrawl_api_key: str, query: str, count: int, - filter_list: Optional[List[str]] = None, -) -> List[SearchResult]: + filter_list: list[str] | None = None, +) -> list[SearchResult]: try: - from firecrawl import FirecrawlApp - - firecrawl = FirecrawlApp(api_key=firecrawl_api_key, api_url=firecrawl_url) - response = firecrawl.search( - query=query, limit=count, ignore_invalid_urls=True, timeout=count * 3 + response = request_firecrawl_json( + 'POST', + build_firecrawl_url(firecrawl_url, 'search'), + headers=build_firecrawl_headers(firecrawl_api_key), + json={ + 'query': query, + 'limit': count, + 'timeout': count * 3000, + 'ignoreInvalidURLs': True, + }, + timeout=count * 3 + 10, ) - results = response.web + # Firecrawl /search has historically returned both `{"data": [...]}` + # (flat list, what v1 did and what frost19k reported under #23966) + # and `{"data": {"web": [...]}}` (current v2). Accept either. + data = response.get('data') or {} + results = data if isinstance(data, list) else (data.get('web') or []) + if filter_list: + from open_webui.retrieval.web.main import get_filtered_results + results = get_filtered_results(results, filter_list) - results = [ - SearchResult( - link=result.url, - title=result.title, - snippet=result.description, + + from open_webui.retrieval.web.main import SearchResult + + search_results = [] + for result in results[:count]: + url = get_firecrawl_result_url(result) + if not url: + continue + + metadata = result.get('metadata') or {} + search_results.append( + SearchResult( + link=url, + title=result.get('title') or metadata.get('title'), + snippet=result.get('description') or result.get('snippet') or metadata.get('description'), + ) ) - for result in results[:count] - ] - log.info(f"External search results: {results}") - return results + + log.info(f'FireCrawl search results: {search_results}') + return search_results except Exception as e: - log.error(f"Error in External search: {e}") + log.error(f'Error in FireCrawl search: {e}') return [] diff --git a/backend/open_webui/retrieval/web/google_pse.py b/backend/open_webui/retrieval/web/google_pse.py index 96fa8c98cd..3b5fe0d685 100644 --- a/backend/open_webui/retrieval/web/google_pse.py +++ b/backend/open_webui/retrieval/web/google_pse.py @@ -1,72 +1,66 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) -def search_google_pse( +async def search_google_pse( api_key: str, search_engine_id: str, query: str, count: int, - filter_list: Optional[list[str]] = None, - referer: Optional[str] = None, + filter_list: list[str | None] | None = None, + referer: str | None = None, ) -> list[SearchResult]: - """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects. - Handles pagination for counts greater than 10. + """Query Google Programmable Search Engine with automatic pagination. - Args: - api_key (str): A Programmable Search Engine API key - search_engine_id (str): A Programmable Search Engine ID - query (str): The query to search for - count (int): The number of results to return (max 100, as PSE max results per query is 10 and max page is 10) - filter_list (Optional[list[str]], optional): A list of keywords to filter out from results. Defaults to None. - - Returns: - list[SearchResult]: A list of SearchResult objects. + The PSE API returns at most 10 results per request, so this function + issues multiple requests when ``count > 10``. """ - url = "https://www.googleapis.com/customsearch/v1" - - headers = {"Content-Type": "application/json"} + url = 'https://www.googleapis.com/customsearch/v1' + headers: dict[str, str] = {'Content-Type': 'application/json'} if referer: - headers["Referer"] = referer + headers['Referer'] = referer - all_results = [] - start_index = 1 # Google PSE start parameter is 1-based + all_items: list[dict] = [] + start_index = 1 # PSE uses 1-based pagination - while count > 0: - num_results_this_page = min(count, 10) # Google PSE max results per page is 10 + session = await get_session() + remaining = count + while remaining > 0: + page_size = min(remaining, 10) params = { - "cx": search_engine_id, - "q": query, - "key": api_key, - "num": num_results_this_page, - "start": start_index, + 'cx': search_engine_id, + 'q': query, + 'key': api_key, + 'num': str(page_size), + 'start': str(start_index), } - response = requests.request("GET", url, headers=headers, params=params) - response.raise_for_status() - json_response = response.json() - results = json_response.get("items", []) - if results: # check if results are returned. If not, no more pages to fetch. - all_results.extend(results) - count -= len( - results - ) # Decrement count by the number of results fetched in this page. - start_index += 10 # Increment start index for the next page - else: - break # No more results from Google PSE, break the loop + + async with session.get(url, headers=headers, params=params) as response: + response.raise_for_status() + payload = await response.json() + + items = payload.get('items', []) + if not items: + break + + all_items.extend(items) + remaining -= len(items) + start_index += 10 if filter_list: - all_results = get_filtered_results(all_results, filter_list) + all_items = get_filtered_results(all_items, filter_list) return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("snippet"), + link=item.get('link', ''), + title=item.get('title'), + snippet=item.get('snippet'), ) - for result in all_results + for item in all_items ] diff --git a/backend/open_webui/retrieval/web/jina_search.py b/backend/open_webui/retrieval/web/jina_search.py index d1168bb36f..830beec702 100644 --- a/backend/open_webui/retrieval/web/jina_search.py +++ b/backend/open_webui/retrieval/web/jina_search.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import requests @@ -7,9 +9,7 @@ from yarl import URL log = logging.getLogger(__name__) -def search_jina( - api_key: str, query: str, count: int, base_url: str = "" -) -> list[SearchResult]: +def search_jina(api_key: str, query: str, count: int, base_url: str = '') -> list[SearchResult]: """ Search using Jina's Search API and return the results as a list of SearchResult objects. Args: @@ -19,18 +19,18 @@ def search_jina( base_url (str): Optional custom base URL for the Jina API Returns: - list[SearchResult]: A list of search results + A list of SearchResult objects. """ - jina_search_endpoint = base_url if base_url else "https://s.jina.ai/" + jina_search_endpoint = base_url or 'https://s.jina.ai/' headers = { - "Accept": "application/json", - "Content-Type": "application/json", - "Authorization": api_key, - "X-Retain-Images": "none", + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': api_key, + 'X-Retain-Images': 'none', } - payload = {"q": query, "count": count if count <= 10 else 10} + payload = {'q': query, 'count': count if count <= 10 else 10} url = str(URL(jina_search_endpoint)) response = requests.post(url, headers=headers, json=payload) @@ -38,12 +38,12 @@ def search_jina( data = response.json() results = [] - for result in data["data"]: + for result in data['data']: results.append( SearchResult( - link=result["url"], - title=result.get("title"), - snippet=result.get("content"), + link=result['url'], + title=result.get('title'), + snippet=result.get('content'), ) ) diff --git a/backend/open_webui/retrieval/web/kagi.py b/backend/open_webui/retrieval/web/kagi.py index f0303acf69..cb35ccbb22 100644 --- a/backend/open_webui/retrieval/web/kagi.py +++ b/backend/open_webui/retrieval/web/kagi.py @@ -7,9 +7,7 @@ from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) -def search_kagi( - api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None -) -> list[SearchResult]: +def search_kagi(api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None) -> list[SearchResult]: """Search using Kagi's Search API and return the results as a list of SearchResult objects. The Search API will inherit the settings in your account, including results personalization and snippet length. @@ -19,23 +17,20 @@ def search_kagi( query (str): The query to search for count (int): The number of results to return """ - url = "https://kagi.com/api/v0/search" + url = 'https://kagi.com/api/v1/search' headers = { - "Authorization": f"Bot {api_key}", + 'Authorization': f'Bearer {api_key}', } - params = {"q": query, "limit": count} + params = {'query': query, 'limit': count} - response = requests.get(url, headers=headers, params=params) + response = requests.post(url, headers=headers, json=params) response.raise_for_status() json_response = response.json() - search_results = json_response.get("data", []) + search_results = json_response.get('data', {}).get('search', []) results = [ - SearchResult( - link=result["url"], title=result["title"], snippet=result.get("snippet") - ) + SearchResult(link=result['url'], title=result['title'], snippet=result.get('snippet')) for result in search_results - if result["t"] == 0 ] print(results) diff --git a/backend/open_webui/retrieval/web/linkup.py b/backend/open_webui/retrieval/web/linkup.py new file mode 100644 index 0000000000..7547fe887e --- /dev/null +++ b/backend/open_webui/retrieval/web/linkup.py @@ -0,0 +1,76 @@ +import logging +from typing import Optional + +import requests + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + +DEFAULT_LINKUP_PARAMS = { + 'url': 'https://api.linkup.so/v1/search', + 'depth': 'standard', + 'outputType': 'sourcedAnswer', +} + + +def search_linkup( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + params: Optional[dict] = None, +) -> list[SearchResult]: + """Search using the Linkup Search API. + + ``params`` is forwarded almost verbatim as the JSON body; only ``q`` + and ``maxResults`` are injected automatically. The special key + ``url`` (default ``https://api.linkup.so/v1/search``) is popped and + used as the endpoint. + """ + if hasattr(api_key, '__str__'): + api_key = str(api_key) + + merged = {**DEFAULT_LINKUP_PARAMS, **(params or {})} + api_url = str(merged.pop('url', DEFAULT_LINKUP_PARAMS['url'])) + + payload = {**merged, 'q': query, 'maxResults': count} + + try: + response = requests.post( + api_url, + headers={ + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + }, + json=payload, + timeout=30, + ) + response.raise_for_status() + json_response = response.json() + + output_type = merged.get('outputType', 'sourcedAnswer') + search_results = ( + json_response.get('sources', []) + if output_type == 'sourcedAnswer' + else json_response.get('results', []) + ) + + if filter_list: + search_results = get_filtered_results(search_results, filter_list) + + return [ + SearchResult( + link=r.get('url', ''), + title=r.get('name') or r.get('title'), + snippet=r.get('content') or r.get('text') or r.get('snippet'), + ) + for r in search_results + ][:count] + + except requests.exceptions.RequestException as e: + log.error(f'Linkup API request failed: {e}') + raise Exception(f'Linkup search failed: {str(e)}') + except Exception as e: + log.error(f'Error searching Linkup: {e}') + raise Exception(f'Linkup search error: {str(e)}') diff --git a/backend/open_webui/retrieval/web/main.py b/backend/open_webui/retrieval/web/main.py index 1b8df9f8ee..a55c62c8b5 100644 --- a/backend/open_webui/retrieval/web/main.py +++ b/backend/open_webui/retrieval/web/main.py @@ -1,12 +1,11 @@ -import validators +from __future__ import annotations -from typing import Optional from urllib.parse import urlparse -from pydantic import BaseModel - +import validators from open_webui.retrieval.web.utils import resolve_hostname from open_webui.utils.misc import is_string_allowed +from pydantic import BaseModel def get_filtered_results(results, filter_list): @@ -16,7 +15,7 @@ def get_filtered_results(results, filter_list): filtered_results = [] for result in results: - url = result.get("url") or result.get("link", "") or result.get("href", "") + url = result.get('url') or result.get('link', '') or result.get('href', '') if not validators.url(url): continue @@ -42,5 +41,5 @@ def get_filtered_results(results, filter_list): class SearchResult(BaseModel): link: str - title: Optional[str] - snippet: Optional[str] + title: str | None + snippet: str | None diff --git a/backend/open_webui/retrieval/web/mojeek.py b/backend/open_webui/retrieval/web/mojeek.py index d48f7aeef8..da24c2a87c 100644 --- a/backend/open_webui/retrieval/web/mojeek.py +++ b/backend/open_webui/retrieval/web/mojeek.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results @@ -7,32 +8,27 @@ from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) -def search_mojeek( - api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None -) -> list[SearchResult]: +def search_mojeek(api_key: str, query: str, count: int, filter_list: list[str | None] = None) -> list[SearchResult]: """Search using Mojeek's Search API and return the results as a list of SearchResult objects. Args: api_key (str): A Mojeek Search API key query (str): The query to search for """ - url = "https://api.mojeek.com/search" + url = 'https://api.mojeek.com/search' headers = { - "Accept": "application/json", + 'Accept': 'application/json', } - params = {"q": query, "api_key": api_key, "fmt": "json", "t": count} + params = {'q': query, 'api_key': api_key, 'fmt': 'json', 't': count} response = requests.get(url, headers=headers, params=params) response.raise_for_status() json_response = response.json() - results = json_response.get("response", {}).get("results", []) + results = json_response.get('response', {}).get('results', []) print(results) if filter_list: results = get_filtered_results(results, filter_list) return [ - SearchResult( - link=result["url"], title=result.get("title"), snippet=result.get("desc") - ) - for result in results + SearchResult(link=result['url'], title=result.get('title'), snippet=result.get('desc')) for result in results ] diff --git a/backend/open_webui/retrieval/web/ollama.py b/backend/open_webui/retrieval/web/ollama.py index 71bd9d5124..7ed19b91b1 100644 --- a/backend/open_webui/retrieval/web/ollama.py +++ b/backend/open_webui/retrieval/web/ollama.py @@ -23,30 +23,30 @@ def search_ollama_cloud( count (int): Number of results to return filter_list (Optional[list[str]]): List of domains to filter results by """ - log.info(f"Searching with Ollama for query: {query}") + log.info(f'Searching with Ollama for query: {query}') - headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} - payload = {"query": query, "max_results": count} + headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} + payload = {'query': query, 'max_results': count} try: - response = requests.post(f"{url}/api/web_search", headers=headers, json=payload) + response = requests.post(f'{url}/api/web_search', headers=headers, json=payload) response.raise_for_status() data = response.json() - results = data.get("results", []) - log.info(f"Found {len(results)} results") + results = data.get('results', []) + log.info(f'Found {len(results)} results') if filter_list: results = get_filtered_results(results, filter_list) return [ SearchResult( - link=result.get("url", ""), - title=result.get("title", ""), - snippet=result.get("content", ""), + link=result.get('url', ''), + title=result.get('title', ''), + snippet=result.get('content', ''), ) for result in results ] except Exception as e: - log.error(f"Error searching Ollama: {e}") + log.error(f'Error searching Ollama: {e}') return [] diff --git a/backend/open_webui/retrieval/web/perplexity.py b/backend/open_webui/retrieval/web/perplexity.py index aae802b432..3794057ebc 100644 --- a/backend/open_webui/retrieval/web/perplexity.py +++ b/backend/open_webui/retrieval/web/perplexity.py @@ -1,17 +1,17 @@ import logging -from typing import Optional, Literal -import requests +from typing import Literal, Optional +import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results MODELS = Literal[ - "sonar", - "sonar-pro", - "sonar-reasoning", - "sonar-reasoning-pro", - "sonar-deep-research", + 'sonar', + 'sonar-pro', + 'sonar-reasoning', + 'sonar-reasoning-pro', + 'sonar-deep-research', ] -SEARCH_CONTEXT_USAGE_LEVELS = Literal["low", "medium", "high"] +SEARCH_CONTEXT_USAGE_LEVELS = Literal['low', 'medium', 'high'] log = logging.getLogger(__name__) @@ -22,8 +22,8 @@ def search_perplexity( query: str, count: int, filter_list: Optional[list[str]] = None, - model: MODELS = "sonar", - search_context_usage: SEARCH_CONTEXT_USAGE_LEVELS = "medium", + model: MODELS = 'sonar', + search_context_usage: SEARCH_CONTEXT_USAGE_LEVELS = 'medium', ) -> list[SearchResult]: """Search using Perplexity API and return the results as a list of SearchResult objects. @@ -37,67 +37,64 @@ def search_perplexity( """ - # Handle PersistentConfig object - if hasattr(api_key, "__str__"): + # Handle ConfigVar object + if hasattr(api_key, '__str__'): api_key = str(api_key) try: - url = "https://api.perplexity.ai/chat/completions" + url = 'https://api.perplexity.ai/chat/completions' # Create payload for the API call payload = { - "model": model, - "messages": [ + 'model': model, + 'messages': [ { - "role": "system", - "content": "You are a search assistant. Provide factual information with citations.", + 'role': 'system', + 'content': 'You are a search assistant. Provide factual information with citations.', }, - {"role": "user", "content": query}, + {'role': 'user', 'content': query}, ], - "temperature": 0.2, # Lower temperature for more factual responses - "stream": False, - "web_search_options": { - "search_context_usage": search_context_usage, + 'temperature': 0.2, # Lower temperature for more factual responses + 'stream': False, + 'web_search_options': { + 'search_context_usage': search_context_usage, }, } headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', } # Make the API request - response = requests.request("POST", url, json=payload, headers=headers) + response = requests.request('POST', url, json=payload, headers=headers) # Parse the JSON response json_response = response.json() # Extract citations from the response - citations = json_response.get("citations", []) + citations = json_response.get('citations', []) # Create search results from citations results = [] for i, citation in enumerate(citations[:count]): # Extract content from the response to use as snippet - content = "" - if "choices" in json_response and json_response["choices"]: + content = '' + if 'choices' in json_response and json_response['choices']: if i == 0: - content = json_response["choices"][0]["message"]["content"] + content = json_response['choices'][0]['message']['content'] - result = {"link": citation, "title": f"Source {i+1}", "snippet": content} + result = {'link': citation, 'title': f'Source {i + 1}', 'snippet': content} results.append(result) if filter_list: - results = get_filtered_results(results, filter_list) return [ - SearchResult( - link=result["link"], title=result["title"], snippet=result["snippet"] - ) + SearchResult(link=result['link'], title=result['title'], snippet=result['snippet']) for result in results[:count] ] except Exception as e: - log.error(f"Error searching with Perplexity API: {e}") + log.error(f'Error searching with Perplexity API: {e}') return [] diff --git a/backend/open_webui/retrieval/web/perplexity_search.py b/backend/open_webui/retrieval/web/perplexity_search.py index 5c591ff64f..dede51cadd 100644 --- a/backend/open_webui/retrieval/web/perplexity_search.py +++ b/backend/open_webui/retrieval/web/perplexity_search.py @@ -1,11 +1,10 @@ import logging -from typing import Optional, Literal -import requests +from typing import Literal, Optional +import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results from open_webui.utils.headers import include_user_info_headers - log = logging.getLogger(__name__) @@ -14,7 +13,7 @@ def search_perplexity_search( query: str, count: int, filter_list: Optional[list[str]] = None, - api_url: str = "https://api.perplexity.ai/search", + api_url: str = 'https://api.perplexity.ai/search', user=None, ) -> list[SearchResult]: """Search using Perplexity API and return the results as a list of SearchResult objects. @@ -29,11 +28,11 @@ def search_perplexity_search( """ - # Handle PersistentConfig object - if hasattr(api_key, "__str__"): + # Handle ConfigVar object + if hasattr(api_key, '__str__'): api_key = str(api_key) - if hasattr(api_url, "__str__"): + if hasattr(api_url, '__str__'): api_url = str(api_url) try: @@ -41,13 +40,13 @@ def search_perplexity_search( # Create payload for the API call payload = { - "query": query, - "max_results": count, + 'query': query, + 'max_results': count, } headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', } # Forward user info headers if user is provided @@ -55,20 +54,17 @@ def search_perplexity_search( headers = include_user_info_headers(headers, user) # Make the API request - response = requests.request("POST", url, json=payload, headers=headers) + response = requests.request('POST', url, json=payload, headers=headers) # Parse the JSON response json_response = response.json() # Extract citations from the response - results = json_response.get("results", []) + results = json_response.get('results', []) return [ - SearchResult( - link=result["url"], title=result["title"], snippet=result["snippet"] - ) - for result in results + SearchResult(link=result['url'], title=result['title'], snippet=result['snippet']) for result in results ] except Exception as e: - log.error(f"Error searching with Perplexity Search API: {e}") + log.error(f'Error searching with Perplexity Search API: {e}') return [] diff --git a/backend/open_webui/retrieval/web/searchapi.py b/backend/open_webui/retrieval/web/searchapi.py index caf781c5df..855269ef02 100644 --- a/backend/open_webui/retrieval/web/searchapi.py +++ b/backend/open_webui/retrieval/web/searchapi.py @@ -21,28 +21,26 @@ def search_searchapi( api_key (str): A searchapi.io API key query (str): The query to search for """ - url = "https://www.searchapi.io/api/v1/search" + url = 'https://www.searchapi.io/api/v1/search' - engine = engine or "google" + engine = engine or 'google' - payload = {"engine": engine, "q": query, "api_key": api_key} + payload = {'engine': engine, 'q': query, 'api_key': api_key} - url = f"{url}?{urlencode(payload)}" - response = requests.request("GET", url) + url = f'{url}?{urlencode(payload)}' + response = requests.request('GET', url) json_response = response.json() - log.info(f"results from searchapi search: {json_response}") + log.info(f'results from searchapi search: {json_response}') - results = sorted( - json_response.get("organic_results", []), key=lambda x: x.get("position", 0) - ) + results = sorted(json_response.get('organic_results', []), key=lambda x: x.get('position', 0)) if filter_list: results = get_filtered_results(results, filter_list) return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("snippet"), + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), ) for result in results[:count] ] diff --git a/backend/open_webui/retrieval/web/searxng.py b/backend/open_webui/retrieval/web/searxng.py index b3d4eb8795..9c48b0f1b3 100644 --- a/backend/open_webui/retrieval/web/searxng.py +++ b/backend/open_webui/retrieval/web/searxng.py @@ -1,89 +1,66 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) +# SearXNG request headers — identifies the bot to instance operators. +_SEARXNG_HEADERS = { + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Accept': 'text/html', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', +} -def search_searxng( + +async def search_searxng( query_url: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] | None = None, **kwargs, ) -> list[SearchResult]: + """Query a SearXNG instance and return results sorted by relevance score. + + Optional keyword arguments (language, safesearch, time_range, categories) + are forwarded directly as SearXNG query parameters. """ - Search a SearXNG instance for a given query and return the results as a list of SearchResult objects. - - The function allows passing additional parameters such as language or time_range to tailor the search result. - - Args: - query_url (str): The base URL of the SearXNG server. - query (str): The search term or question to find in the SearXNG database. - count (int): The maximum number of results to retrieve from the search. - - Keyword Args: - language (str): Language filter for the search results; e.g., "all", "en-US", "es". Defaults to "all". - safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate). - time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''. - categories: (Optional[list[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided. - - Returns: - list[SearchResult]: A list of SearchResults sorted by relevance score in descending order. - - Raise: - requests.exceptions.RequestException: If a request error occurs during the search process. - """ - - # Default values for optional parameters are provided as empty strings or None when not specified. - language = kwargs.get("language", "all") - safesearch = kwargs.get("safesearch", "1") - time_range = kwargs.get("time_range", "") - categories = "".join(kwargs.get("categories", [])) + # Normalise legacy ````-style URLs by stripping any query string. + if '' in query_url: + query_url = query_url.split('?')[0] params = { - "q": query, - "format": "json", - "pageno": 1, - "safesearch": safesearch, - "language": language, - "time_range": time_range, - "categories": categories, - "theme": "simple", - "image_proxy": 0, + 'q': query, + 'format': 'json', + 'pageno': 1, + 'safesearch': kwargs.get('safesearch', '1'), + 'language': kwargs.get('language', 'all').strip().rstrip(','), + 'time_range': kwargs.get('time_range', ''), + 'categories': ''.join(kwargs.get('categories', [])), + 'theme': 'simple', + 'image_proxy': 0, } - # Legacy query format - if "" in query_url: - # Strip all query parameters from the URL - query_url = query_url.split("?")[0] + log.debug('searching %s', query_url) - log.debug(f"searching {query_url}") + session = await get_session() + async with session.get(query_url, headers=_SEARXNG_HEADERS, params=params) as response: + response.raise_for_status() + payload = await response.json() - response = requests.get( - query_url, - headers={ - "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot", - "Accept": "text/html", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "en-US,en;q=0.5", - "Connection": "keep-alive", - }, - params=params, - ) - - response.raise_for_status() # Raise an exception for HTTP errors. - - json_response = response.json() - results = json_response.get("results", []) - sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True) + results = sorted(payload.get('results', []), key=lambda x: x.get('score', 0), reverse=True) if filter_list: - sorted_results = get_filtered_results(sorted_results, filter_list) + results = get_filtered_results(results, filter_list) + return [ SearchResult( - link=result["url"], title=result.get("title"), snippet=result.get("content") + link=item.get('url', ''), + title=item.get('title'), + snippet=item.get('content'), ) - for result in sorted_results[:count] + for item in results[:count] ] diff --git a/backend/open_webui/retrieval/web/serpapi.py b/backend/open_webui/retrieval/web/serpapi.py index bb421b500f..602f60d7a7 100644 --- a/backend/open_webui/retrieval/web/serpapi.py +++ b/backend/open_webui/retrieval/web/serpapi.py @@ -21,28 +21,26 @@ def search_serpapi( api_key (str): A serpapi.com API key query (str): The query to search for """ - url = "https://serpapi.com/search" + url = 'https://serpapi.com/search' - engine = engine or "google" + engine = engine or 'google' - payload = {"engine": engine, "q": query, "api_key": api_key} + payload = {'engine': engine, 'q': query, 'api_key': api_key} - url = f"{url}?{urlencode(payload)}" - response = requests.request("GET", url) + url = f'{url}?{urlencode(payload)}' + response = requests.request('GET', url) json_response = response.json() - log.info(f"results from serpapi search: {json_response}") + log.info(f'results from serpapi search: {json_response}') - results = sorted( - json_response.get("organic_results", []), key=lambda x: x.get("position", 0) - ) + results = sorted(json_response.get('organic_results', []), key=lambda x: x.get('position', 0)) if filter_list: results = get_filtered_results(results, filter_list) return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("snippet"), + link=result['link'], + title=result.get('title'), + snippet=result.get('snippet'), ) for result in results[:count] ] diff --git a/backend/open_webui/retrieval/web/serper.py b/backend/open_webui/retrieval/web/serper.py index 5a745e304e..1304529404 100644 --- a/backend/open_webui/retrieval/web/serper.py +++ b/backend/open_webui/retrieval/web/serper.py @@ -1,41 +1,41 @@ +from __future__ import annotations + import json import logging -from typing import Optional -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) -def search_serper( - api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None +async def search_serper( + api_key: str, + query: str, + count: int, + filter_list: list[str | None] | None = None, ) -> list[SearchResult]: - """Search using serper.dev's API and return the results as a list of SearchResult objects. + """Query the serper.dev Google Search API and return normalised results. - Args: - api_key (str): A serper.dev API key - query (str): The query to search for + Results are sorted by their position field before truncation. """ - url = "https://google.serper.dev/search" + url = 'https://google.serper.dev/search' + headers = {'X-API-KEY': api_key, 'Content-Type': 'application/json'} - payload = json.dumps({"q": query}) - headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} + session = await get_session() + async with session.post(url, headers=headers, data=json.dumps({'q': query})) as response: + response.raise_for_status() + payload = await response.json() - response = requests.request("POST", url, headers=headers, data=payload) - response.raise_for_status() - - json_response = response.json() - results = sorted( - json_response.get("organic", []), key=lambda x: x.get("position", 0) - ) + organic = sorted(payload.get('organic', []), key=lambda item: item.get('position', 0)) if filter_list: - results = get_filtered_results(results, filter_list) + organic = get_filtered_results(organic, filter_list) + return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("description"), + link=item.get('link', ''), + title=item.get('title'), + snippet=item.get('snippet'), ) - for result in results[:count] + for item in organic[:count] ] diff --git a/backend/open_webui/retrieval/web/serply.py b/backend/open_webui/retrieval/web/serply.py index 68843eba85..c4909c14e0 100644 --- a/backend/open_webui/retrieval/web/serply.py +++ b/backend/open_webui/retrieval/web/serply.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional from urllib.parse import urlencode import requests @@ -12,11 +13,11 @@ def search_serply( api_key: str, query: str, count: int, - hl: str = "us", + hl: str = 'us', limit: int = 10, - device_type: str = "desktop", - proxy_location: str = "US", - filter_list: Optional[list[str]] = None, + device_type: str = 'desktop', + proxy_location: str = 'US', + filter_list: list[str | None] = None, ) -> list[SearchResult]: """Search using serper.dev's API and return the results as a list of SearchResult objects. @@ -26,42 +27,40 @@ def search_serply( hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages) limit (int): The maximum number of results to return [10-100, defaults to 10] """ - log.info("Searching with Serply") + log.info('Searching with Serply') - url = "https://api.serply.io/v1/search/" + url = 'https://api.serply.io/v1/search/' query_payload = { - "q": query, - "language": "en", - "num": limit, - "gl": proxy_location.upper(), - "hl": hl.lower(), + 'q': query, + 'language': 'en', + 'num': limit, + 'gl': proxy_location.upper(), + 'hl': hl.lower(), } - url = f"{url}{urlencode(query_payload)}" + url = f'{url}{urlencode(query_payload)}' headers = { - "X-API-KEY": api_key, - "X-User-Agent": device_type, - "User-Agent": "open-webui", - "X-Proxy-Location": proxy_location, + 'X-API-KEY': api_key, + 'X-User-Agent': device_type, + 'User-Agent': 'open-webui', + 'X-Proxy-Location': proxy_location, } - response = requests.request("GET", url, headers=headers) + response = requests.request('GET', url, headers=headers) response.raise_for_status() json_response = response.json() - log.info(f"results from serply search: {json_response}") + log.info(f'results from serply search: {json_response}') - results = sorted( - json_response.get("results", []), key=lambda x: x.get("realPosition", 0) - ) + results = sorted(json_response.get('results', []), key=lambda x: x.get('realPosition', 0)) if filter_list: results = get_filtered_results(results, filter_list) return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("description"), + link=result['link'], + title=result.get('title'), + snippet=result.get('description'), ) for result in results[:count] ] diff --git a/backend/open_webui/retrieval/web/serpstack.py b/backend/open_webui/retrieval/web/serpstack.py index 97db858724..532246c11a 100644 --- a/backend/open_webui/retrieval/web/serpstack.py +++ b/backend/open_webui/retrieval/web/serpstack.py @@ -1,46 +1,42 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) -def search_serpstack( +async def search_serpstack( api_key: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] | None = None, https_enabled: bool = True, ) -> list[SearchResult]: - """Search using serpstack.com's and return the results as a list of SearchResult objects. + """Query the serpstack.com API and return normalised results. - Args: - api_key (str): A serpstack.com API key - query (str): The query to search for - https_enabled (bool): Whether to use HTTPS or HTTP for the API request + Uses HTTPS by default; set ``https_enabled=False`` for free-tier HTTP access. """ - url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search" + scheme = 'https' if https_enabled else 'http' + url = f'{scheme}://api.serpstack.com/search' + params = {'access_key': api_key, 'query': query} - headers = {"Content-Type": "application/json"} - params = { - "access_key": api_key, - "query": query, - } + session = await get_session() + async with session.get(url, params=params) as response: + response.raise_for_status() + payload = await response.json() - response = requests.request("POST", url, headers=headers, params=params) - response.raise_for_status() - - json_response = response.json() - results = sorted( - json_response.get("organic_results", []), key=lambda x: x.get("position", 0) - ) + organic = sorted(payload.get('organic_results', []), key=lambda x: x.get('position', 0)) if filter_list: - results = get_filtered_results(results, filter_list) + organic = get_filtered_results(organic, filter_list) + return [ SearchResult( - link=result["url"], title=result.get("title"), snippet=result.get("snippet") + link=item.get('url', ''), + title=item.get('title'), + snippet=item.get('snippet'), ) - for result in results[:count] + for item in organic[:count] ] diff --git a/backend/open_webui/retrieval/web/sougou.py b/backend/open_webui/retrieval/web/sougou.py index d8747c3ade..3d12e2a57b 100644 --- a/backend/open_webui/retrieval/web/sougou.py +++ b/backend/open_webui/retrieval/web/sougou.py @@ -1,7 +1,6 @@ -import logging import json -from typing import Optional, List - +import logging +from typing import List, Optional from open_webui.retrieval.web.main import SearchResult, get_filtered_results @@ -15,8 +14,8 @@ def search_sougou( count: int, filter_list: Optional[List[str]] = None, ) -> List[SearchResult]: - from tencentcloud.common.common_client import CommonClient from tencentcloud.common import credential + from tencentcloud.common.common_client import CommonClient from tencentcloud.common.exception.tencent_cloud_sdk_exception import ( TencentCloudSDKException, ) @@ -26,33 +25,26 @@ def search_sougou( try: cred = credential.Credential(sougou_api_sid, sougou_api_sk) http_profile = HttpProfile() - http_profile.endpoint = "tms.tencentcloudapi.com" + http_profile.endpoint = 'tms.tencentcloudapi.com' client_profile = ClientProfile() client_profile.http_profile = http_profile - params = json.dumps({"Query": query, "Cnt": 20}) - common_client = CommonClient( - "tms", "2020-12-29", cred, "", profile=client_profile - ) + params = json.dumps({'Query': query, 'Cnt': 20}) + common_client = CommonClient('tms', '2020-12-29', cred, '', profile=client_profile) results = [ - json.loads(page) - for page in common_client.call_json("SearchPro", json.loads(params))[ - "Response" - ]["Pages"] + json.loads(page) for page in common_client.call_json('SearchPro', json.loads(params))['Response']['Pages'] ] - sorted_results = sorted( - results, key=lambda x: x.get("scour", 0.0), reverse=True - ) + sorted_results = sorted(results, key=lambda x: x.get('scour', 0.0), reverse=True) if filter_list: sorted_results = get_filtered_results(sorted_results, filter_list) return [ SearchResult( - link=result.get("url"), - title=result.get("title"), - snippet=result.get("passage"), + link=result.get('url'), + title=result.get('title'), + snippet=result.get('passage'), ) for result in sorted_results[:count] ] except TencentCloudSDKException as err: - log.error(f"Error in Sougou search: {err}") + log.error(f'Error in Sougou search: {err}') return [] diff --git a/backend/open_webui/retrieval/web/tavily.py b/backend/open_webui/retrieval/web/tavily.py index 6d9ff89a87..419bebb05e 100644 --- a/backend/open_webui/retrieval/web/tavily.py +++ b/backend/open_webui/retrieval/web/tavily.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results @@ -11,7 +12,7 @@ def search_tavily( api_key: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] = None, # **kwargs, ) -> list[SearchResult]: """Search using Tavily's Search API and return the results as a list of SearchResult objects. @@ -22,28 +23,28 @@ def search_tavily( count (int): The maximum number of results to return Returns: - list[SearchResult]: A list of search results + A list of SearchResult objects. """ - url = "https://api.tavily.com/search" + url = 'https://api.tavily.com/search' headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {api_key}", + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}', } - data = {"query": query, "max_results": count} + data = {'query': query, 'max_results': count} response = requests.post(url, headers=headers, json=data) response.raise_for_status() json_response = response.json() - results = json_response.get("results", []) + results = json_response.get('results', []) if filter_list: results = get_filtered_results(results, filter_list) return [ SearchResult( - link=result["url"], - title=result.get("title", ""), - snippet=result.get("content"), + link=result['url'], + title=result.get('title', ''), + snippet=result.get('content'), ) for result in results ] diff --git a/backend/open_webui/retrieval/web/testdata/bing.json b/backend/open_webui/retrieval/web/testdata/bing.json deleted file mode 100644 index 80324f3b40..0000000000 --- a/backend/open_webui/retrieval/web/testdata/bing.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "_type": "SearchResponse", - "queryContext": { - "originalQuery": "Top 10 international results" - }, - "webPages": { - "webSearchUrl": "https://www.bing.com/search?q=Top+10+international+results", - "totalEstimatedMatches": 687, - "value": [ - { - "id": "https://api.bing.microsoft.com/api/v7/#WebPages.0", - "name": "2024 Mexican Grand Prix - F1 results and latest standings ... - PlanetF1", - "url": "https://www.planetf1.com/news/f1-results-2024-mexican-grand-prix-race-standings", - "datePublished": "2024-10-27T00:00:00.0000000", - "datePublishedFreshnessText": "1 day ago", - "isFamilyFriendly": true, - "displayUrl": "https://www.planetf1.com/news/f1-results-2024-mexican-grand-prix-race-standings", - "snippet": "Nico Hulkenberg and Pierre Gasly completed the top 10. A full report of the Mexican Grand Prix is available at the bottom of this article. F1 results – 2024 Mexican Grand Prix", - "dateLastCrawled": "2024-10-28T07:15:00.0000000Z", - "cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=916492551782&mkt=en-US&setlang=en-US&w=zBsfaAPyF2tUrHFHr_vFFdUm8sng4g34", - "language": "en", - "isNavigational": false, - "noCache": false - }, - { - "id": "https://api.bing.microsoft.com/api/v7/#WebPages.1", - "name": "F1 Results Today: HUGE Verstappen penalties cause major title change", - "url": "https://www.gpfans.com/en/f1-news/1033512/f1-results-today-mexican-grand-prix-huge-max-verstappen-penalties-cause-major-title-change/", - "datePublished": "2024-10-27T00:00:00.0000000", - "datePublishedFreshnessText": "1 day ago", - "isFamilyFriendly": true, - "displayUrl": "https://www.gpfans.com/en/f1-news/1033512/f1-results-today-mexican-grand-prix-huge-max...", - "snippet": "Elsewhere, Mercedes duo Lewis Hamilton and George Russell came home in P4 and P5 respectively. Meanwhile, the surprise package of the day were Haas, with both Kevin Magnussen and Nico Hulkenberg finishing inside the points.. READ MORE: RB star issues apology after red flag CRASH at Mexican GP Mexican Grand Prix 2024 results. 1. Carlos Sainz [Ferrari] 2. Lando Norris [McLaren] - +4.705", - "dateLastCrawled": "2024-10-28T06:06:00.0000000Z", - "cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=2840656522642&mkt=en-US&setlang=en-US&w=-Tbkwxnq52jZCvG7l3CtgcwT1vwAjIUD", - "language": "en", - "isNavigational": false, - "noCache": false - }, - { - "id": "https://api.bing.microsoft.com/api/v7/#WebPages.2", - "name": "International Power Rankings: England flying, Kangaroos cruising, Fiji rise", - "url": "https://www.loverugbyleague.com/post/international-power-rankings-england-flying-kangaroos-cruising-fiji-rise", - "datePublished": "2024-10-28T00:00:00.0000000", - "datePublishedFreshnessText": "7 hours ago", - "isFamilyFriendly": true, - "displayUrl": "https://www.loverugbyleague.com/post/international-power-rankings-england-flying...", - "snippet": "LRL RECOMMENDS: England player ratings from first Test against Samoa as omnificent George Williams scores perfect 10. 2. Australia (Men) – SAME. The Kangaroos remain 2nd in our Power Rankings after their 22-10 win against New Zealand in Christchurch on Sunday. As was the case in their win against Tonga last week, Mal Meninga’s side weren ...", - "dateLastCrawled": "2024-10-28T07:09:00.0000000Z", - "cachedPageUrl": "https://cc.bingj.com/cache.aspx?q=Top+10+international+results&d=1535008462672&mkt=en-US&setlang=en-US&w=82ujhH4Kp0iuhCS7wh1xLUFYUeetaVVm", - "language": "en", - "isNavigational": false, - "noCache": false - } - ], - "someResultsRemoved": true - } -} diff --git a/backend/open_webui/retrieval/web/testdata/brave.json b/backend/open_webui/retrieval/web/testdata/brave.json deleted file mode 100644 index 0cc72109ef..0000000000 --- a/backend/open_webui/retrieval/web/testdata/brave.json +++ /dev/null @@ -1,998 +0,0 @@ -{ - "query": { - "original": "python", - "show_strict_warning": false, - "is_navigational": true, - "is_news_breaking": false, - "spellcheck_off": true, - "country": "us", - "bad_results": false, - "should_fallback": false, - "postal_code": "", - "city": "", - "header_country": "", - "more_results_available": true, - "state": "" - }, - "mixed": { - "type": "mixed", - "main": [ - { - "type": "web", - "index": 0, - "all": false - }, - { - "type": "web", - "index": 1, - "all": false - }, - { - "type": "news", - "all": true - }, - { - "type": "web", - "index": 2, - "all": false - }, - { - "type": "videos", - "all": true - }, - { - "type": "web", - "index": 3, - "all": false - }, - { - "type": "web", - "index": 4, - "all": false - }, - { - "type": "web", - "index": 5, - "all": false - }, - { - "type": "web", - "index": 6, - "all": false - }, - { - "type": "web", - "index": 7, - "all": false - }, - { - "type": "web", - "index": 8, - "all": false - }, - { - "type": "web", - "index": 9, - "all": false - }, - { - "type": "web", - "index": 10, - "all": false - }, - { - "type": "web", - "index": 11, - "all": false - }, - { - "type": "web", - "index": 12, - "all": false - }, - { - "type": "web", - "index": 13, - "all": false - }, - { - "type": "web", - "index": 14, - "all": false - }, - { - "type": "web", - "index": 15, - "all": false - }, - { - "type": "web", - "index": 16, - "all": false - }, - { - "type": "web", - "index": 17, - "all": false - }, - { - "type": "web", - "index": 18, - "all": false - }, - { - "type": "web", - "index": 19, - "all": false - } - ], - "top": [], - "side": [] - }, - "news": { - "type": "news", - "results": [ - { - "title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch", - "url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/", - "is_source_local": false, - "is_source_both": false, - "description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.", - "page_age": "2024-05-02T17:40:05", - "family_friendly": true, - "meta_url": { - "scheme": "https", - "netloc": "techcrunch.com", - "hostname": "techcrunch.com", - "favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8", - "path": "› 2024 › 05 › 01 › google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference" - }, - "breaking": false, - "thumbnail": { - "src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA" - }, - "age": "3 days ago", - "extra_snippets": [ - "Ahead of Google’s annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.", - "In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.", - "Meanwhile, others shared on Y Combinator’s Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.", - "CNBC reports that a total of 200 people were let go across Google’s “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents." - ] - } - ], - "mutated_by_goggles": false - }, - "type": "search", - "videos": { - "type": "videos", - "results": [ - { - "type": "video_result", - "url": "https://www.youtube.com/watch?v=b093aqAZiPU", - "title": "👩‍💻 Python for Beginners Tutorial - YouTube", - "description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...", - "age": "March 25, 2021", - "page_age": "2021-03-25T10:00:08", - "video": {}, - "meta_url": { - "scheme": "https", - "netloc": "youtube.com", - "hostname": "www.youtube.com", - "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", - "path": "› watch" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw" - } - }, - { - "type": "video_result", - "url": "https://www.youtube.com/watch?v=rfscVS0vtbw", - "title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube", - "description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...", - "age": "July 11, 2018", - "page_age": "2018-07-11T18:00:42", - "video": {}, - "meta_url": { - "scheme": "https", - "netloc": "youtube.com", - "hostname": "www.youtube.com", - "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", - "path": "› watch" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw" - } - }, - { - "type": "video_result", - "url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc", - "title": "Python Tutorial - Python Full Course for Beginners - YouTube", - "description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...", - "age": "February 18, 2019", - "page_age": "2019-02-18T15:00:08", - "video": {}, - "meta_url": { - "scheme": "https", - "netloc": "youtube.com", - "hostname": "www.youtube.com", - "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", - "path": "› watch" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw" - } - }, - { - "type": "video_result", - "url": "https://www.youtube.com/watch?v=wRKgzC-MhIc", - "title": "[] and {} vs list() and dict(), which is better?", - "description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.", - "video": {}, - "meta_url": { - "scheme": "https", - "netloc": "youtube.com", - "hostname": "www.youtube.com", - "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", - "path": "› watch" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn" - } - }, - { - "type": "video_result", - "url": "https://www.youtube.com/watch?v=LWdsF79H1Pg", - "title": "print() vs. return in Python Functions - YouTube", - "description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...", - "age": "June 11, 2022", - "page_age": "2022-06-11T21:33:26", - "video": {}, - "meta_url": { - "scheme": "https", - "netloc": "youtube.com", - "hostname": "www.youtube.com", - "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", - "path": "› watch" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw" - } - }, - { - "type": "video_result", - "url": "https://www.youtube.com/watch?v=AovxLr8jUH4", - "title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...", - "description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...", - "age": "August 28, 2018", - "page_age": "2018-08-28T20:11:09", - "video": {}, - "meta_url": { - "scheme": "https", - "netloc": "youtube.com", - "hostname": "www.youtube.com", - "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", - "path": "› watch" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn" - } - } - ], - "mutated_by_goggles": false - }, - "web": { - "type": "search", - "results": [ - { - "title": "Welcome to Python.org", - "url": "https://www.python.org", - "is_source_local": false, - "is_source_both": false, - "description": "The official home of the Python Programming Language", - "page_age": "2023-09-09T15:55:05", - "profile": { - "name": "Python", - "url": "https://www.python.org", - "long_name": "python.org", - "img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "python.org", - "hostname": "www.python.org", - "favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8", - "path": "" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc", - "original": "https://www.python.org/static/opengraph-icon-200x200.png", - "logo": false - }, - "age": "September 9, 2023", - "cluster_type": "generic", - "cluster": [ - { - "title": "Downloads", - "url": "https://www.python.org/downloads/", - "is_source_local": false, - "is_source_both": false, - "description": "The official home of the Python Programming Language", - "family_friendly": true - }, - { - "title": "Macos", - "url": "https://www.python.org/downloads/macos/", - "is_source_local": false, - "is_source_both": false, - "description": "The official home of the Python Programming Language", - "family_friendly": true - }, - { - "title": "Windows", - "url": "https://www.python.org/downloads/windows/", - "is_source_local": false, - "is_source_both": false, - "description": "The official home of the Python Programming Language", - "family_friendly": true - }, - { - "title": "Getting Started", - "url": "https://www.python.org/about/gettingstarted/", - "is_source_local": false, - "is_source_both": false, - "description": "The official home of the Python Programming Language", - "family_friendly": true - } - ], - "extra_snippets": [ - "Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.", - "The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3", - "Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3", - "# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python." - ] - }, - { - "title": "Python (programming language) - Wikipedia", - "url": "https://en.wikipedia.org/wiki/Python_(programming_language)", - "is_source_local": false, - "is_source_both": false, - "description": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...", - "page_age": "2024-05-01T12:54:03", - "profile": { - "name": "Wikipedia", - "url": "https://en.wikipedia.org/wiki/Python_(programming_language)", - "long_name": "en.wikipedia.org", - "img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "en.wikipedia.org", - "hostname": "en.wikipedia.org", - "favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw", - "path": "› wiki › Python_(programming_language)" - }, - "age": "4 days ago", - "extra_snippets": [ - "Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.", - "Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.", - "Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.", - "Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community." - ] - }, - { - "title": "Python Tutorial", - "url": "https://www.w3schools.com/python/", - "is_source_local": false, - "is_source_both": false, - "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.", - "page_age": "2017-12-07T00:00:00", - "profile": { - "name": "W3Schools", - "url": "https://www.w3schools.com/python/", - "long_name": "w3schools.com", - "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "w3schools.com", - "hostname": "www.w3schools.com", - "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8", - "path": "› python" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n", - "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png", - "logo": true - }, - "age": "December 7, 2017", - "extra_snippets": [ - "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.", - "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE", - "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings", - "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists" - ] - }, - { - "title": "Online Python - IDE, Editor, Compiler, Interpreter", - "url": "https://www.online-python.com/", - "is_source_local": false, - "is_source_both": false, - "description": "Build and Run your Python code instantly. Online-Python is a quick and easy tool that helps you to build, compile, test your python programs.", - "profile": { - "name": "Online-python", - "url": "https://www.online-python.com/", - "long_name": "online-python.com", - "img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "online-python.com", - "hostname": "www.online-python.com", - "favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v", - "path": "" - }, - "extra_snippets": [ - "Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.", - "It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.", - "It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!", - "Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access." - ] - }, - { - "title": "Python · GitHub", - "url": "https://github.com/python", - "is_source_local": false, - "is_source_both": false, - "description": "Repositories related to the Python Programming language - Python", - "page_age": "2023-03-06T00:00:00", - "profile": { - "name": "GitHub", - "url": "https://github.com/python", - "long_name": "github.com", - "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "github.com", - "hostname": "github.com", - "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", - "path": "› python" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA", - "original": "https://avatars.githubusercontent.com/u/1525981?s=200&v=4", - "logo": false - }, - "age": "March 6, 2023", - "extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"] - }, - { - "title": "Online Python Compiler (Interpreter)", - "url": "https://www.programiz.com/python-programming/online-compiler/", - "is_source_local": false, - "is_source_both": false, - "description": "Write and run Python code using our online compiler (interpreter). You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.", - "page_age": "2020-06-02T00:00:00", - "profile": { - "name": "Programiz", - "url": "https://www.programiz.com/python-programming/online-compiler/", - "long_name": "programiz.com", - "img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "programiz.com", - "hostname": "www.programiz.com", - "favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8", - "path": "› python-programming › online-compiler" - }, - "age": "June 2, 2020", - "extra_snippets": [ - "Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler", - "# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")" - ] - }, - { - "title": "Python Developer", - "url": "https://twitter.com/Python_Dv/status/1786763460992544791", - "is_source_local": false, - "is_source_both": false, - "description": "Python Developer", - "page_age": "2024-05-04T14:30:03", - "profile": { - "name": "X", - "url": "https://twitter.com/Python_Dv/status/1786763460992544791", - "long_name": "twitter.com", - "img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "twitter.com", - "hostname": "twitter.com", - "favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8", - "path": "› Python_Dv › status › 1786763460992544791" - }, - "age": "20 hours ago" - }, - { - "title": "input table name? - python script - KNIME Extensions - KNIME Community Forum", - "url": "https://forum.knime.com/t/input-table-name-python-script/78978", - "is_source_local": false, - "is_source_both": false, - "description": "Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario", - "page_age": "2024-05-04T09:20:44", - "profile": { - "name": "Knime", - "url": "https://forum.knime.com/t/input-table-name-python-script/78978", - "long_name": "forum.knime.com", - "img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "article", - "meta_url": { - "scheme": "https", - "netloc": "forum.knime.com", - "hostname": "forum.knime.com", - "favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v", - "path": " › knime extensions" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc", - "original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png", - "logo": false - }, - "age": "1 day ago", - "extra_snippets": [ - "Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …" - ] - }, - { - "title": "What does the Double Star operator mean in Python? - GeeksforGeeks", - "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/", - "is_source_local": false, - "is_source_both": false, - "description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.", - "page_age": "2023-03-14T17:15:04", - "profile": { - "name": "GeeksforGeeks", - "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/", - "long_name": "geeksforgeeks.org", - "img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "article", - "meta_url": { - "scheme": "https", - "netloc": "geeksforgeeks.org", - "hostname": "www.geeksforgeeks.org", - "favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv", - "path": "› what-does-the-double-star-operator-mean-in-python" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc", - "original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png", - "logo": false - }, - "age": "March 14, 2023", - "extra_snippets": [ - "Difference between / vs. // operator in Python", - "Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.", - "The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.", - "Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)" - ] - }, - { - "title": "r/Python", - "url": "https://www.reddit.com/r/Python/", - "is_source_local": false, - "is_source_both": false, - "description": "The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python programming language. --- If you have questions or are new to Python use r/LearnPython", - "page_age": "2022-12-30T16:25:02", - "profile": { - "name": "Reddit", - "url": "https://www.reddit.com/r/Python/", - "long_name": "reddit.com", - "img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "reddit.com", - "hostname": "www.reddit.com", - "favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8", - "path": "› r › Python" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n", - "original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png", - "logo": false - }, - "age": "December 30, 2022", - "extra_snippets": [ - "r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…", - "By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.", - "Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!", - "Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here." - ] - }, - { - "title": "GitHub - python/cpython: The Python programming language", - "url": "https://github.com/python/cpython", - "is_source_local": false, - "is_source_both": false, - "description": "The Python programming language. Contribute to python/cpython development by creating an account on GitHub.", - "page_age": "2022-10-29T00:00:00", - "profile": { - "name": "GitHub", - "url": "https://github.com/python/cpython", - "long_name": "github.com", - "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "software", - "meta_url": { - "scheme": "https", - "netloc": "github.com", - "hostname": "github.com", - "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", - "path": "› python › cpython" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u", - "original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython", - "logo": false - }, - "age": "October 29, 2022", - "extra_snippets": [ - "You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.", - "Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or usable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.", - "To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.", - "Copyright © 2001-2024 Python Software Foundation. All rights reserved." - ] - }, - { - "title": "5. Data Structures — Python 3.12.3 documentation", - "url": "https://docs.python.org/3/tutorial/datastructures.html", - "is_source_local": false, - "is_source_both": false, - "description": "This chapter describes some things you’ve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...", - "page_age": "2023-07-04T00:00:00", - "profile": { - "name": "Python documentation", - "url": "https://docs.python.org/3/tutorial/datastructures.html", - "long_name": "docs.python.org", - "img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "docs.python.org", - "hostname": "docs.python.org", - "favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv", - "path": "› 3 › tutorial › datastructures.html" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw", - "original": "https://docs.python.org/3/_static/og-image.png", - "logo": false - }, - "age": "July 4, 2023", - "extra_snippets": [ - "You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed – they return the default None. [1] This is a design principle for all mutable data structures in Python.", - "We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.", - "Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.", - "Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys." - ] - }, - { - "title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums", - "url": "https://bbs.archlinux.org/viewtopic.php?id=295466", - "is_source_local": false, - "is_source_both": false, - "description": "Big Python updates require Python packages to be rebuild. For some reason they didn't think a bump that made it necessary to rebuild half the official repo was a news post.", - "page_age": "2024-05-04T08:30:02", - "profile": { - "name": "Archlinux", - "url": "https://bbs.archlinux.org/viewtopic.php?id=295466", - "long_name": "bbs.archlinux.org", - "img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "bbs.archlinux.org", - "hostname": "bbs.archlinux.org", - "favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8", - "path": "› viewtopic.php" - }, - "age": "1 day ago", - "extra_snippets": [ - "Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis" - ] - }, - { - "title": "Introduction to Python", - "url": "https://www.w3schools.com/python/python_intro.asp", - "is_source_local": false, - "is_source_both": false, - "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.", - "profile": { - "name": "W3Schools", - "url": "https://www.w3schools.com/python/python_intro.asp", - "long_name": "w3schools.com", - "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "w3schools.com", - "hostname": "www.w3schools.com", - "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8", - "path": "› python › python_intro.asp" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n", - "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png", - "logo": true - }, - "extra_snippets": [ - "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.", - "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE", - "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings", - "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists" - ] - }, - { - "title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf", - "url": "https://github.com/asdf-vm/asdf/issues/1740", - "is_source_local": false, - "is_source_both": false, - "description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...", - "page_age": "2024-05-04T06:45:04", - "profile": { - "name": "GitHub", - "url": "https://github.com/asdf-vm/asdf/issues/1740", - "long_name": "github.com", - "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "software", - "meta_url": { - "scheme": "https", - "netloc": "github.com", - "hostname": "github.com", - "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", - "path": "› asdf-vm › asdf › issues › 1740" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA", - "original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740", - "logo": false - }, - "age": "1 day ago", - "extra_snippets": [ - "==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...", - "-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python", - "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..." - ] - }, - { - "title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum", - "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/", - "is_source_local": false, - "is_source_both": false, - "description": "I was looking at App execution aliases (Settings > Apps > Advanced app settings > App execution aliases) on my new computer -- my first Windows 11 computer. Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...", - "page_age": "2024-05-03T17:30:04", - "profile": { - "name": "Windows 11 Forum", - "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/", - "long_name": "elevenforum.com", - "img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "elevenforum.com", - "hostname": "www.elevenforum.com", - "favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw", - "path": " › windows support forums › apps and software" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw", - "original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png", - "logo": true - }, - "age": "2 days ago", - "extra_snippets": [ - "Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?", - "Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.", - "I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it." - ] - }, - { - "title": "How to Watermark Your Images Using Python OpenCV in ...", - "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1", - "is_source_local": false, - "is_source_both": false, - "description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.", - "page_age": "2024-05-03T14:05:06", - "profile": { - "name": "Medium", - "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1", - "long_name": "medium.com", - "img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "medium.com", - "hostname": "medium.com", - "favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw", - "path": "› @daily_data_prep › how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1" - }, - "age": "2 days ago" - }, - { - "title": "Increment and Decrement Operators in Python?", - "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python", - "is_source_local": false, - "is_source_both": false, - "description": "Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python ...", - "page_age": "2023-08-23T00:00:00", - "profile": { - "name": "Tutorialspoint", - "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python", - "long_name": "tutorialspoint.com", - "img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "tutorialspoint.com", - "hostname": "www.tutorialspoint.com", - "favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw", - "path": "› increment-and-decrement-operators-in-python" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc", - "original": "https://www.tutorialspoint.com/images/tp_logo_436.png", - "logo": true - }, - "age": "August 23, 2023", - "extra_snippets": [ - "Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing", - "So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.", - "Python does not provide multiple ways to do the same thing .", - "However, be careful if you are coming from a language like C, Python doesn’t have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (int’s) are immutable." - ] - }, - { - "title": "Gumroad – How not to suck at Python / SideFX Houdini | CG Persia", - "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html", - "is_source_local": false, - "is_source_both": false, - "description": "Info: This course is made for artists or TD (technical director) willing to learn Python to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.", - "page_age": "2024-05-03T08:35:03", - "profile": { - "name": "Cgpersia", - "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html", - "long_name": "cgpersia.com", - "img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "cgpersia.com", - "hostname": "cgpersia.com", - "favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v", - "path": "› 2024 › 05 › gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html" - }, - "age": "2 days ago", - "extra_snippets": [ - "Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment", - "01 – Python – Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools !", - "02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools ! 04 – Houdini – Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 – Houdini – Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools." - ] - }, - { - "title": "How to install Python: The complete Python programmer’s guide", - "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide", - "is_source_local": false, - "is_source_both": false, - "description": "An easy guide on how set up your operating system so you can program in Python, and how to update or uninstall it. For Linux, Windows, and macOS.", - "page_age": "2024-05-02T07:30:02", - "profile": { - "name": "Pluralsight", - "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide", - "long_name": "pluralsight.com", - "img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw" - }, - "language": "en", - "family_friendly": true, - "type": "search_result", - "subtype": "generic", - "meta_url": { - "scheme": "https", - "netloc": "pluralsight.com", - "hostname": "www.pluralsight.com", - "favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw", - "path": " › blog › blog" - }, - "thumbnail": { - "src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA", - "original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp", - "logo": false - }, - "age": "3 days ago", - "extra_snippets": [ - "Whether it’s your first time programming or you’re a seasoned programmer, you’ll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.", - "Some systems come with Python, so to start off, we’ll first check to see if it’s installed on your system before we proceed. To do that, we’ll need to open a terminal. Since you might be new to programming, let’s go over how to open a terminal for Linux, Windows, and macOS.", - "Before we dive into setting up your system so you can program in Python, let’s talk terminal basics and benefits.", - "However, let’s focus on why we need it for working with Python. We use a terminal, or command line, to:" - ] - } - ], - "family_friendly": true - } -} diff --git a/backend/open_webui/retrieval/web/testdata/google_pse.json b/backend/open_webui/retrieval/web/testdata/google_pse.json deleted file mode 100644 index 15da9729cd..0000000000 --- a/backend/open_webui/retrieval/web/testdata/google_pse.json +++ /dev/null @@ -1,442 +0,0 @@ -{ - "kind": "customsearch#search", - "url": { - "type": "application/json", - "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json" - }, - "queries": { - "request": [ - { - "title": "Google Custom Search - lectures", - "totalResults": "2450000000", - "searchTerms": "lectures", - "count": 10, - "startIndex": 1, - "inputEncoding": "utf8", - "outputEncoding": "utf8", - "safe": "off", - "cx": "0473ef98502d44e18" - } - ], - "nextPage": [ - { - "title": "Google Custom Search - lectures", - "totalResults": "2450000000", - "searchTerms": "lectures", - "count": 10, - "startIndex": 11, - "inputEncoding": "utf8", - "outputEncoding": "utf8", - "safe": "off", - "cx": "0473ef98502d44e18" - } - ] - }, - "context": { - "title": "LLM Search" - }, - "searchInformation": { - "searchTime": 0.445959, - "formattedSearchTime": "0.45", - "totalResults": "2450000000", - "formattedTotalResults": "2,450,000,000" - }, - "items": [ - { - "kind": "customsearch#result", - "title": "The Feynman Lectures on Physics", - "htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics", - "link": "https://www.feynmanlectures.caltech.edu/", - "displayLink": "www.feynmanlectures.caltech.edu", - "snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.", - "htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.", - "cacheId": "CyXMWYWs9UEJ", - "formattedUrl": "https://www.feynmanlectures.caltech.edu/", - "htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/", - "pagemap": { - "metatags": [ - { - "viewport": "width=device-width, initial-scale=1.0" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Video Lectures", - "htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e", - "link": "https://www.reddit.com/r/lectures/", - "displayLink": "www.reddit.com", - "snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…", - "htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…", - "formattedUrl": "https://www.reddit.com/r/lectures/", - "htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/", - "pagemap": { - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s", - "width": "192", - "height": "192" - } - ], - "metatags": [ - { - "og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", - "theme-color": "#000000", - "og:image:width": "256", - "og:type": "website", - "twitter:card": "summary", - "twitter:title": "r/lectures", - "og:site_name": "Reddit", - "og:title": "r/lectures", - "og:image:height": "256", - "bingbot": "noarchive", - "msapplication-navbutton-color": "#000000", - "og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!", - "twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", - "apple-mobile-web-app-status-bar-style": "black", - "twitter:site": "@reddit", - "viewport": "width=device-width, initial-scale=1, viewport-fit=cover", - "apple-mobile-web-app-capable": "yes", - "og:ttl": "600", - "og:url": "https://www.reddit.com/r/lectures/" - } - ], - "cse_image": [ - { - "src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Lectures & Discussions | Flint Institute of Arts", - "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e & Discussions | Flint Institute of Arts", - "link": "https://flintarts.org/events/lectures", - "displayLink": "flintarts.org", - "snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...", - "htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...", - "cacheId": "jvpb9DxrfxoJ", - "formattedUrl": "https://flintarts.org/events/lectures", - "htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e", - "pagemap": { - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s", - "width": "447", - "height": "113" - } - ], - "metatags": [ - { - "og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg", - "og:type": "website", - "viewport": "width=device-width, initial-scale=1", - "og:title": "Lectures & Discussions | Flint Institute of Arts", - "og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation." - } - ], - "cse_image": [ - { - "src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham", - "htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham", - "link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", - "displayLink": "www.brandeis.edu", - "snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...", - "htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e · \u003cb\u003eLecture\u003c/b\u003e 1: "Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction" · \u003cb\u003eLecture\u003c/b\u003e 2: "Solidarity in Sound: Grassroots ...", - "cacheId": "cQLOZr0kgEEJ", - "formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", - "htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html", - "pagemap": { - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s", - "width": "275", - "height": "183" - } - ], - "metatags": [ - { - "og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba", - "twitter:card": "summary_large_image", - "viewport": "width=device-width,initial-scale=1,minimum-scale=1", - "og:title": "Mandel Lectures in the Humanities", - "og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", - "og:description": "Annual Lecture Series", - "twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba" - } - ], - "cse_image": [ - { - "src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Brian Douglas - YouTube", - "htmlTitle": "Brian Douglas - YouTube", - "link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "displayLink": "www.youtube.com", - "snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.", - "htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.", - "cacheId": "NEROyBHolL0J", - "formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "pagemap": { - "hcard": [ - { - "fn": "Brian Douglas", - "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg" - } - ], - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s", - "width": "225", - "height": "225" - } - ], - "imageobject": [ - { - "width": "900", - "url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", - "height": "900" - } - ], - "person": [ - { - "name": "Brian Douglas", - "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg" - } - ], - "metatags": [ - { - "apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008", - "og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", - "twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "twitter:app:id:googleplay": "com.google.android.youtube", - "theme-color": "rgb(255, 255, 255)", - "og:image:width": "900", - "twitter:card": "summary", - "og:site_name": "YouTube", - "twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "al:android:package": "com.google.android.youtube", - "twitter:app:name:googleplay": "YouTube", - "al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "twitter:app:id:iphone": "544007664", - "og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian", - "al:ios:app_store_id": "544007664", - "twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", - "twitter:site": "@youtube", - "og:type": "profile", - "twitter:title": "Brian Douglas", - "al:ios:app_name": "YouTube", - "og:title": "Brian Douglas", - "og:image:height": "900", - "twitter:app:id:ipad": "544007664", - "al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks", - "al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks", - "fb:app_id": "87741124305", - "twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "twitter:app:name:ipad": "YouTube", - "viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,", - "twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian", - "og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", - "al:android:app_name": "YouTube", - "twitter:app:name:iphone": "YouTube" - } - ], - "cse_image": [ - { - "src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Lecture - Wikipedia", - "htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia", - "link": "https://en.wikipedia.org/wiki/Lecture", - "displayLink": "en.wikipedia.org", - "snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...", - "htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...", - "cacheId": "d9Pjta02fmgJ", - "formattedUrl": "https://en.wikipedia.org/wiki/Lecture", - "htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture", - "pagemap": { - "metatags": [ - { - "referrer": "origin", - "og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg", - "theme-color": "#eaecf0", - "og:image:width": "1200", - "og:type": "website", - "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0", - "og:title": "Lecture - Wikipedia", - "og:image:height": "799", - "format-detection": "telephone=no" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Mount Wilson Observatory | Lectures", - "htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e", - "link": "https://www.mtwilson.edu/lectures/", - "displayLink": "www.mtwilson.edu", - "snippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...", - "htmlSnippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...", - "cacheId": "wdXI0azqx5UJ", - "formattedUrl": "https://www.mtwilson.edu/lectures/", - "htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/", - "pagemap": { - "metatags": [ - { - "viewport": "width=device-width,initial-scale=1,user-scalable=no" - } - ], - "webpage": [ - { - "image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg", - "url": "https://www.facebook.com/WilsonObs" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Lectures | NBER", - "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER", - "link": "https://www.nber.org/research/lectures", - "displayLink": "www.nber.org", - "snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...", - "htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic ...", - "cacheId": "CvvP3U3nb44J", - "formattedUrl": "https://www.nber.org/research/lectures", - "htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e", - "pagemap": { - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s", - "width": "310", - "height": "163" - } - ], - "metatags": [ - { - "og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg", - "og:site_name": "NBER", - "handheldfriendly": "true", - "viewport": "width=device-width, initial-scale=1.0", - "og:title": "Lectures", - "mobileoptimized": "width", - "og:url": "https://www.nber.org/research/lectures" - } - ], - "cse_image": [ - { - "src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved", - "htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved", - "link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358", - "displayLink": "community.canvaslms.com", - "snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...", - "htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...", - "cacheId": "wqrynQXX61sJ", - "formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358", - "htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358", - "pagemap": { - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s", - "width": "310", - "height": "163" - } - ], - "metatags": [ - { - "og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png", - "og:type": "article", - "article:section": "Canvas Question Forum", - "article:published_time": "2020-03-19T15:50:03.409Z", - "og:site_name": "Instructure Community", - "article:modified_time": "2020-03-19T13:55:53-07:00", - "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes", - "og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES", - "og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667", - "og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.", - "article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287", - "twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png" - } - ], - "cse_image": [ - { - "src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png" - } - ] - } - }, - { - "kind": "customsearch#result", - "title": "Public Lecture Series - Sam Fox School of Design & Visual Arts", - "htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design & Visual Arts", - "link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", - "displayLink": "samfoxschool.wustl.edu", - "snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...", - "htmlSnippet": "The Sam Fox School's Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...", - "cacheId": "B-cgQG0j6tUJ", - "formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", - "htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", - "pagemap": { - "cse_thumbnail": [ - { - "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s", - "width": "307", - "height": "164" - } - ], - "website": [ - { - "name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis" - } - ], - "metatags": [ - { - "og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg", - "og:type": "website", - "og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis", - "viewport": "width=device-width, initial-scale=1.0", - "og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis", - "csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg", - "csrf-param": "authenticity_token", - "og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields." - } - ], - "cse_image": [ - { - "src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg" - } - ] - } - } - ] -} diff --git a/backend/open_webui/retrieval/web/testdata/searchapi.json b/backend/open_webui/retrieval/web/testdata/searchapi.json deleted file mode 100644 index fa3d1c3d74..0000000000 --- a/backend/open_webui/retrieval/web/testdata/searchapi.json +++ /dev/null @@ -1,357 +0,0 @@ -{ - "search_metadata": { - "id": "search_VW19X7MebbAtdMwoQe68NbDz", - "status": "Success", - "created_at": "2024-08-27T13:43:20Z", - "request_time_taken": 0.6, - "parsing_time_taken": 0.72, - "total_time_taken": 1.32, - "request_url": "https://www.google.com/search?q=chatgpt&oq=chatgpt&gl=us&hl=en&ie=UTF-8", - "html_url": "https://www.searchapi.io/api/v1/searches/search_VW19X7MebbAtdMwoQe68NbDz.html", - "json_url": "https://www.searchapi.io/api/v1/searches/search_VW19X7MebbAtdMwoQe68NbDz" - }, - "search_parameters": { - "engine": "google", - "q": "chatgpt", - "device": "desktop", - "google_domain": "google.com", - "hl": "en", - "gl": "us" - }, - "search_information": { - "query_displayed": "chatgpt", - "total_results": 1010000000, - "time_taken_displayed": 0.37, - "detected_location": "United States" - }, - "knowledge_graph": { - "kgmid": "/g/11khcfz0y2", - "knowledge_graph_type": "Kp3 verticals", - "title": "ChatGPT", - "type": "Software", - "description": "ChatGPT is a chatbot and virtual assistant developed by OpenAI and launched on November 30, 2022. Based on large language models, it enables users to refine and steer a conversation towards a desired length, format, style, level of detail, and language.", - "source": { - "name": "Wikipedia", - "link": "https://en.wikipedia.org/wiki/ChatGPT" - }, - "developer": "OpenAI, Microsoft", - "developer_links": [ - { - "text": "OpenAI", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=OpenAI&si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgItjbuK5dmA2L3ta2Ero3Ypd_sib6W4Pr5sCi7O_W3yzdqxwyrjzsYeYOtNg2ogL1xVq9TKwgD48tL7rygfkRfNyy4k-R5yQgywoFukoCUths6NdRX69gl50cvd6dpZcMzVelCxT7mxXlRchl6XkueG326znDiZL-ODNOysdnCc4XoeAQUFtbaVjja6Vc7WkQF4X8rUdbDKPVU9WyLOV765d8Y777kMI7-nXGGyD7xXJX5E3HA%3D%3D&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECD0QAg" - }, - { - "text": "Microsoft", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Microsoft&si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-SdjhIP74XAMBYys4zy1Z9yzXEom04F9Qy-tMOt2d-L6jIC5cXse6I528G870-4sF-DZYAPj0F1HoGTUOqpWuP7jbEPm3w_-mCH0wVgBHBGCgxRrCaUn8_k2-aga9V9JD6hkq2kM8zVmERCqCM8rqo3bNfbPdJ-baTq4w8Pkxdan3K--CfOtXX--lTjJtO6BnfG2RdpY_jBfy3uZZ7DeAE4-P4rvKuty6UL6le4dqqDt-kLQA%3D%3D&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAXoECD0QAw" - } - ], - "initial_release_date": "November 30, 2022", - "programming_language": "Python", - "programming_language_links": [ - { - "text": "Python", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Python&si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqmwbtPHPEcZi5JOYKaqe_iu1m4TVPotntrDVKbuXCkoFhx-K-Dp6PbewOILPFWjhDofHha-WRuSQCgY7LnBkzXtVH7pxiRdHONv3wpVsflGBg_EdTHCxOnyWt1nDgBmCjsfchXU7DKtJq159-V0-seE_cp7VV&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDYQAg" - } - ], - "engine": "GPT-4; GPT-4o; GPT-4o mini", - "engine_links": [ - { - "text": "GPT-4", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4&stick=H4sIAAAAAAAAAONgVuLVT9c3NMy2TI_PNUtOX8TK6h4QomsCAKiBOxkZAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDUQAg" - }, - { - "text": "GPT-4o", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4o&stick=H4sIAAAAAAAAAONgVuLVT9c3NCyryEg3rMooWMTK5h4QomuSDwC3NAfvGgAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAXoECDUQAw" - }, - { - "text": "GPT-4o", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=GPT-4o&stick=H4sIAAAAAAAAAONgVuLVT9c3NCyryEg3rMooWMTK5h4QomuSDwC3NAfvGgAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAnoECDUQBA" - } - ], - "license": "Proprietary", - "platform": "Cloud computing platforms", - "platform_links": [ - { - "text": "Cloud computing", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=Cloud+computing&stick=H4sIAAAAAAAAAONgVuLSz9U3MKqMt8w1XsTK75yTX5qikJyfW1BakpmXDgB-4JvxIAAAAA&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQmxMoAHoECDcQAg" - } - ], - "stable_release": "July 18, 2024; 40 days ago", - "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALoAAAC6CAMAAAAu0KfDAAAAaVBMVEX///8AAAD7+/uysrJ5eXmoqKiYmJhZWVn4+PiVlZXw8PDj4+P09PTr6+vNzc2vr6/V1dWMjIxmZmY7OzvExMSCgoIgICBMTEygoKBUVFQUFBQxMTG4uLhDQ0O+vr7b29snJydubm4LCwtts+PWAAAPGElEQVR4nM1d6YKiMAxWBKlcIocoooDv/5CrjjpNmrTlcNz82x2Oz5KmX46mi8VMIoI42ZbZ+VRfl9e6891108aRM9fjPyVOmHhZvVSl2hVp8G10Gom3uyMB+yldf0i+jZCRcOdfeeAPOfUr8W2YqoTZ3oD7IfXJ+zZSKE7Y2+B+SvMfKX24oWYmL8fL/2JvPM3cpKV2w2+DvkvgmiYnJefLt3EvFsl5BPC77L6tNNuRwG/Sf1VpxFqHrd53WoN5TL+HPNgxoPb9ZnspiiRJiqLx2CU2/9rqGpUkoNMhDQN50RRxeMlo7MV/hHxfxszlTUUY/y5ZOEIEaXJZeQfvkrRRJD4/fR0CuX/QUZTEPamfyCsr+D9VuUrC6KPQ1RlaHUxvTGgVUxWp9JLP8bQVft11Z2HsxKWioBJy6g8fMp6J8u0LOx0NXEvsd/SfAB/76DVH+3sLa+y3ydPOPmk38A373YB720HUoZx53Urg4+uD/diEh2EEedkdOHM7SpCia00iFM92mkriz7hwHeCj7bWlPY0hyMvrYS7kYQce7Frf14/B/ZBqJqWB60pmqS3hwcrvZqSbZbamwEKcW6ubxErnBea+65al2x9VpvDGPodbBTS9totNJBmr5P4hScM4iKIoiOMwLdbMb9xPxx4CG5HZ3OL0DPBrVqjBSEc0NPpmKvQLGAqL6RN4NO668thpEmwoIzrRSArgNJjtIjeEy2ylXceiA+YaN5nmVQXgWUZenTChjvxi/F7hocN3WdoEUkTRy49aGy4PSsZkrK04oULyltk4++6EG0ScrnoAYsvQlcyazB7wrSOiN6L1VMK3061GIs1p4Mchsy3BDxlqZsRlp+jdTXSWNmV8ObMXCKXFSjMsTHxxyS9f8ZMm2jFDbqfksmCFt6ZMNwl9hnyU7C0rxpuo4hFOTwCnem2tMoKLcN1oOn2Hw0VQzyOXlBg+z7ezMk7BfPibnOgFomWU/LweHZ5I4bhbsfdooyGqZ+rXxx41n+/e64TVZNEAGGeL+aIuCbJQQYAtt+on0zx7+CW3xutTveuukkbnzKxBxeRQFvyEpnFoDK67Qr1C+rpuMxX3TVLwSIOR2Zr8MUxgBKle+1Kv5CIpisTCZoCg2Vl7qRJTvEsuT3XsH12o35oZDGJR3u6qs8aoUS14um6iFoS25KtQ/u14sihU6WY/Ddnd+MUs697kNjtgfdH4CQrrufu1An62FbpHjVrvDHxDDoRdS8PcAwFaPsSphgXz9f3JzgDoe8OqJwq8Aug/EXg3sxzeVqIeI3db9XY99MzgwCdEsNq/6FTek/0tbkXFtGX/CipaQz+t9EMebshFt3Y13mcsKzFD/VL0wNN7AG2hl4almnBbXu/SzA/Z+NKenoOMnEQZ7KD7BrpC2ABJui2nNXJQhE65IuZ3lp5kBd3XmgonNGZkzikNPpYvohZUlCU6yl9mOvRwbRM03dFWXr6EoGARHJQKPGQy9ANill1PY89JlZdnyFp9C0xSofjqROgpZjl+G4XMjM0JjZBVuVS88wgm85H5nARdyQzsf/jNhQkznRWtkXlVr3wWmODq0V+10F9ZPRp6jIOm53fuLMKhqffIIlMla8RRsY7gk17xLxsNXQ2abmRYbUlHJrstANhKf8rx0gHsjwJutMIkuFg2Q2bZKRhf0pdVXvZlThg6sOmVok7jRl30yCB2BFcRDY29Pv5i1EF3ALNQF6wx0APM4zsuPlzSwYRl+RpCHXTgjqpzeAR0pRTj5PI0oaUDhMvTU+Vl6FjXZWRXRdNHQFcKYPQRMMEUWi2zh8rL5g9lUkHcRbU+g6EHO8Vkn3Z6Nhxvaa2p7x9Lng4Z1IlCfhPFiIdB90i6YkpXxkzgr96BehAXrqbyhNpTDGgAdKEW+7wfbSiOCo+0ytfyyrUBxgAEdU/UQ+2hc5mB55AZUnErbcTwIZA5xvIcwRxgEPRgg7yJHP3bqPIen3L/EcgL25z90xDomRo03cQxqltaVoaYZ6gvzkMhAUC9SBfRCnqHE83VwwSHyn8b/MBWVxDhQ7MOqDr5NCvoGOGbhTQYPFuJ+hRNNQeyf3JBdzUT9Fyu3IoPSOX3nh58wGXTsCMh20Y6TzYYOnYyUxzi8Q1WXrnhR5Cqg4AkHRMfCL1ScxlOgrTm6usDpXTmGMUchS6MOwI6lyVUUg6uQeWJSD9iMKKX/ka/dgD0nM/mC6zBV6PKK8QGUhgAnaCNg6AzkZSnqCqvDZTejJ/CKEF9wowKczSVcIsE2T1TbkBZ0sDrAXR64ttBr7cWFQwRrjjpDMn/Ffa8JZVx5B9GB4GtoB8tSy8ivOBc9dsicS5UJoiyXSfZlyX9skOOAic/t2oTrDjXIvm4cnApnwDdMjHtUMxWO71TuCacfq+dicNMgr7M1xp9Q+5L+f5GgDnSWYPPQ79nLPl74Fyt33QA6BJp2D8DPeuRsTnz2xxgru3toAIviax4/Qz0w6JBH+DKlqA4cG16aTvgX93fQV8TdPjMqTysoXgbQjmOXFOeL4COVXIKdMKh85nEK4wDvtYlMIHJxU1+Ps7TT4N+3w2BwNOVnAGw7i9yHsgqV1E3AiZxhgvIVOg3dotTBCQdATa8e/1vaboPjQzYNDQd+iJeo5GnWCCwJvUrhglKSciEMNqQfFr/XjQDdCX0pCQA7gJSO2+yBSg9GZRVUkLvgZkF+kIUQGuoWDwIi/gvSwRcADWr8bgRs/6unRP6AuUYqTvJ8hJYWM9UbihB8J+GBrNBl5WStHNAr99TsgeYmAILsUamIL/vDZ0NuqyTtP8h6/XbB4YFJRmHot0gn+W4fa8VfwAdxHVfhgKVCLA7GhwcQqyzF5I/gC57Fr/7NxLA4qiE0ku4BPkfQJfTu9c3YxHQsOq6FERY5f8MOigv+bXhqOK60nlcLRUR/AvoMs+UaCAq+tOWXFMJ8sFu9QjovXSJbMJRlEy/j0k0OD4yOJgxArqs1XJCDK5LN+Ohmat3wRHBoSGkEdDlS0BZqrLfb6MPZ6Y4QT4scDcCumwdXRB9VFJQuAgEI1G4wZBw6UToPRimQAms6kon7xLjEGLObxXDQeo5R/2GRN3uuTdkC2Ncv2idGhgBXWZoJRojKneWM6WTL2l9BMomITMOuuxoohwEk+A3dYHwsJU3psFGQpe/sCaZJwtdOvkrMa7G0CcfR0OXlQIpMp/lPhqMvFITokn5jocu3w790FBXGXE0JcgtE+3H10o2HHokPwdVC+jrIkwd6mzKG26+ycsCD4cu76rP4UgyhXtv6UzZQlNRSb4JpjBHeZaimIu5/ZtvUHl9Kc/DUk2ALisFtI2OzHuZuVUbVF5TQHX6YTjjoYPtVdA2Br30p/Vix2zGMyXI6bK196o8Hrr8RU/QwAAK09xLJ+lS//3WlCAnigXfC8No6LFsBXy40IAdwQ/bw5ZO6hPkBXZeS8mSjYa+krUAhcdC2R78GMJ4S6uuLkEe4sZwR/BDx0IH6ow5Xii/8mXDudLJPbexfY3LKVAHgbHQQaKxQwSPhM6XTi6JnUTigoDvXXzRSOgBMHl43BjoCyXT9hIfd4lL0NyoCTdrJHS45mA7AXatgEWfVXmwraLF+zd9aivsOOhwX4MSOAE+EhouMmp0k/PmpcnOQUkJ0anvUdB78GTlW4I5rKz43Eb35y7DAq8BXH/AUdChI5Gpm5NkekPsBW6Y6sNzGCmNRfiCujHQYS8EakuGzFCpYk2FGb5HGNmVo6ZJxAjo6IO7RKgKzGLyvUzpJJS9vqXKYOgx9APIXcqAr9PeqDD0dri/NtRv3xwKPUBvJNdCQFg5Zu6smM04P2LqH/w7Kyyh442Ge/Kx5vKShxDM8CVnQ62uXM1gB11JojD+gsxXzpr4RULvJOo2eu81Xsk4bKBHa/wizgDI5rPWRezEheAGpq0YiEJbQE975R0c24blJdrZFmAfvDO0VIlxbyUjdLFRFPPIf1bZDLGtEp7ilNLHrA3tfoju4QboolF9tFwTQQQv2JgSW+mr8Kwz7GEIGmJi4x8LtgCEK2L163SDCUqs9sY+Rk5zV999aWqpQvmJSn9M0AaCsmGGrpTgt/Idvt4S31vU6HNIbUl650rzU9NC3Zma8oCr52gOTbfJIJ5t6JHQmdqHCWBH66m4BdOJuj6rX0q/i01jW14C03hDmk6r4mCH742DYhnaE0ZseutCyjCpX2u7o0NQdNSVa4rwuINo2kAIdEjy0d3ShOLwPYXpTYVjCZJUls1LUa+148jzdpQI2FPOXAM2ZkPcTewbjqMsYTXi7AKHyGL+AGeD3IIxMB2xTZ0X9JDhPU9D5rSTfMcPA+3CdO6wjooxut++XepDxIEZcm0Ok8qonDaWJxn8Cm65pq3rwVIwRk6fOY5UffEbvadIiqp3hjzMryiVnE+p2VZHP9Ki6ysvGNdPUe3o2FspPKfkp9I010A82Z3SBjJV8xKG7N1N4gsdprma3Cc8vaadf0Zwj8zTBylW3Kpv7h8IjVpvsegPxH71efDCo07ouYtnMYYpKCSbfLIDnf/NCketGIkKrrv9tbfSWvC9LDiiETvTqivzkjaMgyASURCHbeIxinLvbm9HPiBtmuOcQh0h8nu3LMs+04XxjpYnPcJt1KY+A3aSakN0Btnbnh6EdnFOcxHeEms76mnF/uQgqG/dXEfHOLtxR6ic7L86WrlnPJGTa2+lk8r+/bhtN73hdaTESsTSIPWAI7IE/qozH5FH9RvlxVRoJYuDI8amAxgGi5MOOQ/U3hOPlO13sx7v9JSwN/XL+RXXjj4JpROzLho6Cfwa5+lYqWyOj01VNZx8WgwrUeGVloa+NE23eK0+abZDqWj0YbJ10eJflUTzpdzVffwIdwS9y0zLqE4cEUVtUjTbbXNJQiGEs0gIVaorjryGZGGW2trzb4Q5VqEvwhgU9wdhwkTztEekfFTY/sHHndc8Tmu9f6cNe2qVJkLzcUm1J93tO/0huQMOA/yAcGEMGzGf3fBZUXepWMqgCNWHpBl1mPW3TAuUdvgR4gMI8mdFXIYd3F4bqgr+VJQEvE76EQHRj8q6s1Kb/cDg95+I2Bopcu3bHF/8FUnWuDeyLP6u+YRTMZfE6aWkjGXXr5L/dcB/RURxu1q7WV5fl9f6VPW7bRIHs/Gsf2zY1viSH96vAAAAAElFTkSuQmCC" - }, - "organic_results": [ - { - "position": 1, - "title": "ChatGPT | OpenAI", - "link": "https://openai.com/chatgpt/", - "source": "OpenAI", - "domain": "openai.com", - "displayed_link": "https://openai.com › chatgpt", - "snippet": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, ...", - "snippet_highlighted_words": ["ChatGPT", "ChatGPT"], - "sitelinks": { - "expanded": [ - { - "title": "Introducing ChatGPT", - "link": "https://openai.com/index/chatgpt/", - "snippet": "We've trained a model called ChatGPT which interacts in a ..." - }, - { - "title": "Download ChatGPT", - "link": "https://openai.com/chatgpt/download/", - "snippet": "Download ChatGPT Use ChatGPT your way. Talk to type or have a ..." - }, - { - "title": "Pricing", - "link": "https://openai.com/chatgpt/pricing/", - "snippet": "Pricing · $25per user / month billed annually · $30per user / month ..." - }, - { - "title": "“What is ChatGPT?” article", - "link": "https://help.openai.com/en/articles/6783457-what-is-chatgpt", - "snippet": "How does ChatGPT work? ChatGPT is fine-tuned from ..." - }, - { - "title": "For Teams", - "link": "https://openai.com/chatgpt/team/", - "snippet": "ChatGPT simplifies lead qualification and forecasting ..." - } - ] - }, - "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABQElEQVR4Ac3PIYyDMBiG4VefPDtxEj0xM39qZl40mcPhMzONOjWNrqxA4UgmqklweBQKVfFdGhbSZZvfY5qmb35++DAbO4XQF7xjpN42s1oyXtlr2gN4SRpynnTaANtesy1tkOOR8aoAJ12J6ngmGkknCqn5gv0y8Jv03eYy+PEAu07jCQ66sDqqpohBCVb2PMtvSbeoxRJcLlIFVFKVBuOwBDdNxkzjEbKbVDwHvgZw8j+Qq2fVhhjkxB2g7JwqKJMRhUqo5Lol8OTxMbSsehXw45e9ao+J92EkGaFbBscxLqnbPRhYOVXr/53L+wTVaUDmNZ+tLNyDWgdWl3gxo7otHMYY5DYdwLc6gB18tVLBSVJD6qr6fsoBVt7wyCm4PxfiRyBTx5N8kCQP8DtrzysZrebG9ZLhnaILYbIbPss/4c/row+G/FAAAAAASUVORK5CYII=" - }, - { - "position": 2, - "title": "ChatGPT", - "link": "https://chatgpt.com/", - "source": "ChatGPT", - "domain": "chatgpt.com", - "displayed_link": "https://chatgpt.com", - "snippet": "ChatGPT helps you get answers, find inspiration and be more productive. It is free to use and easy to try. Just ask and ChatGPT can help with writing, learning,", - "snippet_highlighted_words": ["ChatGPT", "ChatGPT"], - "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABGElEQVR4Aa3SIWzCUBSF4d8rDA6LnMfiMPjU18xiJjHPCxzBVePqaqsrK6sqK5qgnmjybzShzQKb4tjv3mvuwX/yHhya9i8cDgCXlziwKm99TnIM5RN+rlQvkO5Z97+wP1FpAbkadwwzWgAOW4L2rcppxoZLjc2i1xMEzZYzblMrbBILzpaQV0wYqUfcbNNk3+kZPibsaEek1oqjxj3DA6W8Y5uobs7kuggTphvNOKWq6/HQlQl70sF4oNaS2NNaMzxQ4Krt9rBPliMW82akubKqDFSuR9x9TiiF8QsybfnBLtDNePhQm3ifSOyAyhlvpKoZy0pzsuiM2kKSwlWNhKd/FiHsFsXtVrB5XbAAEHyN2jTv7+1TvgE1rn+XcUk3JAAAAABJRU5ErkJggg==" - }, - { - "position": 3, - "title": "OpenAI", - "link": "https://openai.com/", - "source": "OpenAI", - "domain": "openai.com", - "displayed_link": "https://openai.com", - "snippet": "ChatGPT on your desktop. Chat about email, screenshots, files, and anything on your screen. Chat about email, screenshots, files ...", - "snippet_highlighted_words": ["ChatGPT"], - "sitelinks": { - "inline": [ - { - "title": "ChatGPT", - "link": "https://openai.com/chatgpt/" - }, - { - "title": "Introducing ChatGPT", - "link": "https://openai.com/index/chatgpt/" - }, - { - "title": "Download ChatGPT", - "link": "https://openai.com/chatgpt/download/" - }, - { - "title": "ChatGPT for teams", - "link": "https://openai.com/chatgpt/team/" - } - ] - }, - "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAABQElEQVR4Ac3PIYyDMBiG4VefPDtxEj0xM39qZl40mcPhMzONOjWNrqxA4UgmqklweBQKVfFdGhbSZZvfY5qmb35++DAbO4XQF7xjpN42s1oyXtlr2gN4SRpynnTaANtesy1tkOOR8aoAJ12J6ngmGkknCqn5gv0y8Jv03eYy+PEAu07jCQ66sDqqpohBCVb2PMtvSbeoxRJcLlIFVFKVBuOwBDdNxkzjEbKbVDwHvgZw8j+Qq2fVhhjkxB2g7JwqKJMRhUqo5Lol8OTxMbSsehXw45e9ao+J92EkGaFbBscxLqnbPRhYOVXr/53L+wTVaUDmNZ+tLNyDWgdWl3gxo7otHMYY5DYdwLc6gB18tVLBSVJD6qr6fsoBVt7wyCm4PxfiRyBTx5N8kCQP8DtrzysZrebG9ZLhnaILYbIbPss/4c/row+G/FAAAAAASUVORK5CYII=" - }, - { - "position": 4, - "title": "ChatGPT - Apps on Google Play", - "link": "https://play.google.com/store/apps/details?id=com.openai.chatgpt&hl=en_US", - "source": "Google Play", - "domain": "play.google.com", - "displayed_link": "https://play.google.com › store › apps › details › id=com...", - "snippet": "With the official ChatGPT app, get instant answers and inspiration wherever you are. This app is free and brings you the newest model improvements from ...", - "snippet_highlighted_words": ["ChatGPT"], - "rich_snippet": { - "detected_extensions": { - "rating": 4.8, - "reviews": 3113820 - }, - "extensions": ["Rating: 4.8", "3,113,820 votes", "Free", "Android", "Business/Productivity"] - }, - "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADNklEQVR4AcXUA4wdURiG4VPbtu32mmvUtm3bts2gnNq2bbtBbSzn3n79M8nZrvdcZZO8yUTzPUMGIFmLOlBJTXhtqcPUUaoDxRwp16aObORuHcNpxmwnUjM5/uICam0PyKza2miJSmoGOlH0HlIGjwPUm9tOqrXdD6qt9RANEb39VEGPAXym/5YMK9ehxu5ahKgbH4I3m0rjdoDfBEj+EwDDyrUiiO9UR7ffAd8pMvzHAyJ3Ivr74R7AtD8SIRATURO1ttWBakuCCN4BqrDLAApRiAmAcflm1NilJUSwCAJqqcm8nBs7pRu4y+gCIAoRqdwJ07LtdCfUSSLUlEZqjBQbevwctVvX1QUA7zd8pkYIIfh41o2d0Xh7ICJOpAFOsic0ZHYBIIZQKynjaLQtCPLJVKCrRyQhaAjUYaqIMCBxxA5CqAgRRIjmUMapzBu7oHG0cZmPx2welU4AkARi6T7U3KWHanuAgshCV952hy/sJ1MmNs77Q5mcAHBEuIKwLD6Mmrv1yCS1QMftfsApJjLO+0A1cxjAEb5TwxRE/uX30PmkFrjAgJPRn7lQklMAXwL4TAtB8UVA927PIA8sBNxikC+mhHzMgwA+7j0tFEWXAN1GXkGksSzkMpXxdWBZ/L3LYLvMRBHfqLbCAD7uNT0MJRYDfYedQ4S5FOymonjvbcD7Slb86Fsa9psM9itJIuxUsPhLGG282BJg8ODjgL4QbKbieO+nxyc/NT55a/CughXfu5dLCrGGyir2GcYzPoTGYSiESFNJGtfhk69aSUH4qPG+YoKIc1Q58R+R+Hj8iJ5lYbuUArazKd/QkL9Tv2Lfqb9hpfGiSYzHh3hXxjv05/Di/e23GB8TB/Bxy4xw5YUbMfAwjRdJajw6Yun7KpaM37uWY/YbBDjpIICP023HxH47AV0ehJtLi4wfp0pR7H01M6OvwnGA/9Rf0v/xHTSeF2GWMviQ+PhzykoxJVcA1eZBKh5jvGxi47+oHnzULYCacyFN7is0Po9KRzG3Am7XbzopxFoeoZZyCY0foIrwIbcDZFPxzN+9qy2JZ/whZeADngJEP0k76gB1kOooOuwKIFn7B3LHHIJtp64TAAAAAElFTkSuQmCC", - "thumbnail": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFwAXAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAAAgMEBQYBB//EADQQAAIBAwMDAgQEBAcAAAAAAAECAwAEEQUSIQYTMSJBFCMyUTNhgZFCUnHSFTRTVGJysf/EABkBAQADAQEAAAAAAAAAAAAAAAABAgMEBf/EACURAAICAAQFBQAAAAAAAAAAAAABAhEDEhMhIjFRUoFBYcHh8P/aAAwDAQACEQMRAD8A8sooorc8s0+l6ppbaRp1tqF3qNlNplxNNH8CvqnEgHh8jY4K43c8Grew6u0sx2UGoid4LWGwEfyEcxyRKwlbJ859PPuB7cVzo3pe21jpW9ke2jkvLh3W2nPcIhCBM5K8KctkL5cAiq/o/TkudMubmDT4b++F5FC0c8DzLbwMGJk7aEE+oBc+35VxT03m9vk64uaSLxuutNhuFltxMwWe0nkjW3C/EMgdJDksSDyjAknOwA481BseoOn9JtoILY3F40XxO6SSBkE3ciZQHXuHLZI3MNvHjxVje6Z05odvIur21msTXt1GQI5ZZWARCixSAjbgt5aqS+Syvun9FJttF0qW9inkmue1IvMcu0KuC2Mjzke3tVIxw5ck6+izc16ogw9RiebUZb+GKIS6K2m20VtGQkY3KVHJJxw3OTUrovqXT+m7aZ5rWe4uLi4jEoRwii3UHI5B3ZJOV4zgc1anpHSptJ0w28q3Dqly872MyyTXrJGj9uMcgEEsPGcAZGTWS6m0yPR9Xks4XkZBHG+2bHcjLIG2PjjcM4Nax0sS4L9RlJzjxMuX6pt7GwsbDR4I2WGK4tnuriI94QvMzAL6scoVzkHnPNUfUl7DqXUGo39tu7NzcvKm8YOCcjIqtoreOFGLtGcsSUlTCiiitDMKKKKAUruoIVmAJBIBxzXUZw2ULA4wSpxxXFXJp1Fz44pQboTtJ+piR/Wu7RgDJOPHNPrH9h+ppez/AJAUKOZFAIIKswKnKkHwfypDK2SSc5OSfvUwxk/Y006UolTsi0U4y5596boWCiiigCiilL5oBaj2qRGv7UzH71IHipM5sft0E1xFE0ixK7qpkbwgJxk/kK37QaN0wz20+LcveSJI17Zx3Us8KKgwFyBGjlnIb7Ac159BKYZ4pgquY3D7WGQ2DnB/Kt4uqaFrqqZjGiw3Bl7er3mwW0bnLrAEX5gGOFY/YBearKzTArfqUvV2i2mlJA9tHcW0jyvG1vcSrIXUYKyoygZRgft5BrNMM/1rQ9U6/barEttYxz9lby4ue5ORnMjcKij6UAA48kk5rO5qY3Rni1n4RiQc5phxzmpUnJqO3ihaLG6KKKFgpS+aTXV80A8nvV/0qmktqZm16VFsIYyzRszZlYkKFAX1H6t3A/h54rPKcGtBp+mG9jYW6WG+NYsLcSyK8pePecYYDjn9BRiMW5WWT2WiW+rW9vHcWVyqafKQ7XHyZrlWkCdxgRtBAU+QPHIzUqXTem7oRRteWlnc7i0ht70GH8SFSuXBPh3YHwNp+oc1XHprUBjNnpWSSAPjGOcDP+pjxSX6fu07bG303Y8ywlhJL6WL7PBbPk1XyaqL7S5TR+lBG8IvY3f6e82pQowy0ByM+n0q0wzyDtYecYhDSOmorSV/8UW5lNs5hBuo4w8mzcDjymGyu1/q8ik3fTEkd0sNqtjL3ACm8TKSTkY4c8cYz4JKjyRTMfTV60gR4NJjJGctcv8AoOH8nn9qjyS4vtMw58VHPipd2UKwSJGI+5HuKqSQDuYcZJPsPeoh8VcwSoRRRRQkKKKKAUDUxb3KqJbaCRlULuYNkgDA8EewA/SoVGaAnC6j/wBlbfs/91KF2i+LO3Hvxv8A7qghjXd1BbLCbUWnk7txDFLJ/PI0jN+5amjdRY/yVr+z/wB1Qy1czmlC2PXE5nZSURFVdqqg4AyT/wCkn9aYJozXKAKKKKAKKK7QFpHoVydCk1iXK2wC9rau7eS5Q5/lA2nk/dceeIkGn39yiPbWN3Mj52NFAzBsecEDnHvWv6mJXobTFHhfg88fVut3fn+ngY9sZzgYXply9j0dp93B+NbyzXClmYhnVJtmRnwp5AGBkknOTmmbY6NNZqMX8FeB4ozaXIeb8Jey2ZP+ox6v0rq2N42zbZ3LdyQxJiFjuceVHHLDB4816DDqD3g0SSeJGkkgGXDOCO8q274O70+hARjGGyacj6gur97dZooQLzUZ7GXZvHywJgNvq9LfPf1DngfnlmZKwY9TzcW1wySOtvMUjYI7CMkIxOACccHPsa7LZ3cIlM1pcRiFgkpeJl7bHwGyOCfsa9CXqK5srW+1OC3txcfFx3JBDFC8sy7sjdz+CmPcc85NNa/rVxFpms2sccOyxmis4S4MmUKxElwxIc/KXlgfJ+9TmZGjGuZidG05tV1KKxVzG82VVthbDAEjIHOOOT7eccUzf2c+n3b2l2mydApdc5xuUMB+xFWel302rdaWd/cduOa81OJn7Uaqql5BnC4x7++c+TkkmpnX57mq2U2MGWxRio8D5ki8Z5/hzySck1N7lHFZbMxRRRUmR//Z" - }, - { - "position": 5, - "title": "ChatGPT on the App Store - Apple", - "link": "https://apps.apple.com/us/app/chatgpt/id6448311069", - "source": "Apple", - "domain": "apps.apple.com", - "displayed_link": "https://apps.apple.com › app › chatgpt", - "snippet": "This official app is free, syncs your history across devices, and brings you the newest model improvements from OpenAI. With ChatGPT in your pocket ...", - "snippet_highlighted_words": ["ChatGPT"], - "rich_snippet": { - "detected_extensions": { - "rating": 4.9, - "reviews": 1026513 - }, - "extensions": ["Rating: 4.9", "1,026,513 reviews", "Free", "iOS", "Business/Productivity"] - }, - "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC5UlEQVR4Aa1XQ3hkQRjc+ynX2OZtbfu+tm3b1nlt27a9O4qNS5xxbdd+cTKvXydT31fJoPuvmvf6/ejw86dBlX6CwwQXCq6t5cLaz/xV4+ld6F8r9NdgsCAjIwf5+UUoLCwBydf8jN+JNQbBddzjDQM+gocErRSyWm2QgWu4lntq9/q01UAfwYKCgmK43W6ognu4lzEE+6oamCboLC0tR3vBGIwlOF2vgZm5uQWoqamBXrhcLpw5cxZ79uxFKxCxrGBMxpYZ6Eu33KAXNDp+/AQEBgbzv8Y6Kxi7+e1ofuAKVS/7zp27KE7i6dNnem5HAbVaM3CYh0YF/PWRkdEUpxHoQe3BPNTcQJCgTc9pT0tLh8VigdPpBLFv3368evVKBC7A16/fkJmZKX06qCXo39jAej67Wnjx4iVGjBiJ0NBwBAeHYsCAgTh48BCuXLmCKVOmIioqBrwS4eGRGDduPMxmMzyBWtRsbMCglWSePXuOkJAwCuhmnz79YLVaPSUrGjDWGQhgCvWEyspKdOrURUk8JiYO799/0Exg1KQ2DQxjHveEO3fuKomTPBcyUJPaNLCQxcQTNm3arGzAYDBABmoK7UU0sE7rAC5dukxJPCgoRPy6DMhATWpLDWzbtl35Cty//0DBgOQW3LhxU9nAsGEj4HA4dN0CySHkwvy6bKfECRMmISsrS34IZY8hMXnyFAZV5rFjx6WPoa5E9PnzZ2XxpKQUlJaWaiUik1IqXrBgkZKB06fPwBOKiv4fwA3Ni5FdK3NVVFSgd+++usRnzJilXIzII7JynJOTAxaa7t17Yt68+bh37z6+fPmKCxcuYvToMejVqzdWrVrNMi0rx4cVGxIFKDQkCi2ZAhRaMklTavWqeF6epCltxuneasvLyurb8lmqg0lfLw4m/dozmh0RtBUV6R/NuJZ7avf6eGs4ZeIwMoVmZrYcTvkZv+MarlUZTlUZIDi8diRfX8uFtZ8FqMb7Bx+2VJbBTrlcAAAAAElFTkSuQmCC" - }, - { - "position": 6, - "title": "What is ChatGPT and why does it matter? Here's what you ...", - "link": "https://www.zdnet.com/article/what-is-chatgpt-and-why-does-it-matter-heres-everything-you-need-to-know/", - "source": "ZDNET", - "domain": "www.zdnet.com", - "displayed_link": "https://www.zdnet.com › ... › Artificial Intelligence", - "snippet": "ChatGPT is an AI chatbot with natural language processing (NLP) that allows you to have human-like conversations to complete various tasks. The ...", - "snippet_highlighted_words": ["ChatGPT"], - "date": "Jun 17, 2024", - "favicon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAMFBMVEXQ/0rQ/0vM+0oDAxDV/0yixjxlfCpTZiV/nDK75USTtTg7SR/F8Uiu1UAfJhhyjC65DF56AAAAAXRSTlP3Yz+/2QAAANRJREFUKJHdkkGSxCAIRaOAAore/7YDJNPVSVfPAeYtdPEK5FMex1/U8pV/JutMatwVH9JaouTH1ok3SWsEjWGNBXve5JQ5qS+XJLJB8V3iZK8AZhBEAb5JBVjNEFPaU65tIibR1saiZ2XgbzpDH1H2ZtWttB316SSpZ5TeWymNtSfK501X2wFWo23mVR7DE49f2VYrkdPONVaEXmq9JHVIGUQSl/ia1jxFyOYT2YeUletTIyJ5SEIf4WoLfJNCE73EhJKoJHv9hHjFyQNzeefp8jvHD3ZbC4DWezICAAAAAElFTkSuQmCC" - } - ], - "inline_images": { - "images": [ - { - "title": "upload.wikimedia.org/wikipedia/commons/e/ef/ChatGP...", - "source": { - "name": "en.wikipedia.org", - "link": "https://en.wikipedia.org/wiki/ChatGPT" - }, - "original": { - "link": "https://upload.wikimedia.org/wikipedia/commons/e/ef/ChatGPT-Logo.svg", - "height": 800, - "width": 800, - "size": "1KB" - }, - "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALoAAAC6CAMAAAAu0KfDAAAAaVBMVEX///8AAAD7+/uysrJ5eXmoqKiYmJhZWVn4+PiVlZXw8PDj4+P09PTr6+vNzc2vr6/V1dWMjIxmZmY7OzvExMSCgoIgICBMTEygoKBUVFQUFBQxMTG4uLhDQ0O+vr7b29snJydubm4LCwtts+PWAAAPGElEQVR4nM1d6YKiMAxWBKlcIocoooDv/5CrjjpNmrTlcNz82x2Oz5KmX46mi8VMIoI42ZbZ+VRfl9e6891108aRM9fjPyVOmHhZvVSl2hVp8G10Gom3uyMB+yldf0i+jZCRcOdfeeAPOfUr8W2YqoTZ3oD7IfXJ+zZSKE7Y2+B+SvMfKX24oWYmL8fL/2JvPM3cpKV2w2+DvkvgmiYnJefLt3EvFsl5BPC77L6tNNuRwG/Sf1VpxFqHrd53WoN5TL+HPNgxoPb9ZnspiiRJiqLx2CU2/9rqGpUkoNMhDQN50RRxeMlo7MV/hHxfxszlTUUY/y5ZOEIEaXJZeQfvkrRRJD4/fR0CuX/QUZTEPamfyCsr+D9VuUrC6KPQ1RlaHUxvTGgVUxWp9JLP8bQVft11Z2HsxKWioBJy6g8fMp6J8u0LOx0NXEvsd/SfAB/76DVH+3sLa+y3ydPOPmk38A373YB720HUoZx53Urg4+uD/diEh2EEedkdOHM7SpCia00iFM92mkriz7hwHeCj7bWlPY0hyMvrYS7kYQce7Frf14/B/ZBqJqWB60pmqS3hwcrvZqSbZbamwEKcW6ubxErnBea+65al2x9VpvDGPodbBTS9totNJBmr5P4hScM4iKIoiOMwLdbMb9xPxx4CG5HZ3OL0DPBrVqjBSEc0NPpmKvQLGAqL6RN4NO668thpEmwoIzrRSArgNJjtIjeEy2ylXceiA+YaN5nmVQXgWUZenTChjvxi/F7hocN3WdoEUkTRy49aGy4PSsZkrK04oULyltk4++6EG0ScrnoAYsvQlcyazB7wrSOiN6L1VMK3061GIs1p4Mchsy3BDxlqZsRlp+jdTXSWNmV8ObMXCKXFSjMsTHxxyS9f8ZMm2jFDbqfksmCFt6ZMNwl9hnyU7C0rxpuo4hFOTwCnem2tMoKLcN1oOn2Hw0VQzyOXlBg+z7ezMk7BfPibnOgFomWU/LweHZ5I4bhbsfdooyGqZ+rXxx41n+/e64TVZNEAGGeL+aIuCbJQQYAtt+on0zx7+CW3xutTveuukkbnzKxBxeRQFvyEpnFoDK67Qr1C+rpuMxX3TVLwSIOR2Zr8MUxgBKle+1Kv5CIpisTCZoCg2Vl7qRJTvEsuT3XsH12o35oZDGJR3u6qs8aoUS14um6iFoS25KtQ/u14sihU6WY/Ddnd+MUs697kNjtgfdH4CQrrufu1An62FbpHjVrvDHxDDoRdS8PcAwFaPsSphgXz9f3JzgDoe8OqJwq8Aug/EXg3sxzeVqIeI3db9XY99MzgwCdEsNq/6FTek/0tbkXFtGX/CipaQz+t9EMebshFt3Y13mcsKzFD/VL0wNN7AG2hl4almnBbXu/SzA/Z+NKenoOMnEQZ7KD7BrpC2ABJui2nNXJQhE65IuZ3lp5kBd3XmgonNGZkzikNPpYvohZUlCU6yl9mOvRwbRM03dFWXr6EoGARHJQKPGQy9ANill1PY89JlZdnyFp9C0xSofjqROgpZjl+G4XMjM0JjZBVuVS88wgm85H5nARdyQzsf/jNhQkznRWtkXlVr3wWmODq0V+10F9ZPRp6jIOm53fuLMKhqffIIlMla8RRsY7gk17xLxsNXQ2abmRYbUlHJrstANhKf8rx0gHsjwJutMIkuFg2Q2bZKRhf0pdVXvZlThg6sOmVok7jRl30yCB2BFcRDY29Pv5i1EF3ALNQF6wx0APM4zsuPlzSwYRl+RpCHXTgjqpzeAR0pRTj5PI0oaUDhMvTU+Vl6FjXZWRXRdNHQFcKYPQRMMEUWi2zh8rL5g9lUkHcRbU+g6EHO8Vkn3Z6Nhxvaa2p7x9Lng4Z1IlCfhPFiIdB90i6YkpXxkzgr96BehAXrqbyhNpTDGgAdKEW+7wfbSiOCo+0ytfyyrUBxgAEdU/UQ+2hc5mB55AZUnErbcTwIZA5xvIcwRxgEPRgg7yJHP3bqPIen3L/EcgL25z90xDomRo03cQxqltaVoaYZ6gvzkMhAUC9SBfRCnqHE83VwwSHyn8b/MBWVxDhQ7MOqDr5NCvoGOGbhTQYPFuJ+hRNNQeyf3JBdzUT9Fyu3IoPSOX3nh58wGXTsCMh20Y6TzYYOnYyUxzi8Q1WXrnhR5Cqg4AkHRMfCL1ScxlOgrTm6usDpXTmGMUchS6MOwI6lyVUUg6uQeWJSD9iMKKX/ka/dgD0nM/mC6zBV6PKK8QGUhgAnaCNg6AzkZSnqCqvDZTejJ/CKEF9wowKczSVcIsE2T1TbkBZ0sDrAXR64ttBr7cWFQwRrjjpDMn/Ffa8JZVx5B9GB4GtoB8tSy8ivOBc9dsicS5UJoiyXSfZlyX9skOOAic/t2oTrDjXIvm4cnApnwDdMjHtUMxWO71TuCacfq+dicNMgr7M1xp9Q+5L+f5GgDnSWYPPQ79nLPl74Fyt33QA6BJp2D8DPeuRsTnz2xxgru3toAIviax4/Qz0w6JBH+DKlqA4cG16aTvgX93fQV8TdPjMqTysoXgbQjmOXFOeL4COVXIKdMKh85nEK4wDvtYlMIHJxU1+Ps7TT4N+3w2BwNOVnAGw7i9yHsgqV1E3AiZxhgvIVOg3dotTBCQdATa8e/1vaboPjQzYNDQd+iJeo5GnWCCwJvUrhglKSciEMNqQfFr/XjQDdCX0pCQA7gJSO2+yBSg9GZRVUkLvgZkF+kIUQGuoWDwIi/gvSwRcADWr8bgRs/6unRP6AuUYqTvJ8hJYWM9UbihB8J+GBrNBl5WStHNAr99TsgeYmAILsUamIL/vDZ0NuqyTtP8h6/XbB4YFJRmHot0gn+W4fa8VfwAdxHVfhgKVCLA7GhwcQqyzF5I/gC57Fr/7NxLA4qiE0ku4BPkfQJfTu9c3YxHQsOq6FERY5f8MOigv+bXhqOK60nlcLRUR/AvoMs+UaCAq+tOWXFMJ8sFu9QjovXSJbMJRlEy/j0k0OD4yOJgxArqs1XJCDK5LN+Ohmat3wRHBoSGkEdDlS0BZqrLfb6MPZ6Y4QT4scDcCumwdXRB9VFJQuAgEI1G4wZBw6UToPRimQAms6kon7xLjEGLObxXDQeo5R/2GRN3uuTdkC2Ncv2idGhgBXWZoJRojKneWM6WTL2l9BMomITMOuuxoohwEk+A3dYHwsJU3psFGQpe/sCaZJwtdOvkrMa7G0CcfR0OXlQIpMp/lPhqMvFITokn5jocu3w790FBXGXE0JcgtE+3H10o2HHokPwdVC+jrIkwd6mzKG26+ycsCD4cu76rP4UgyhXtv6UzZQlNRSb4JpjBHeZaimIu5/ZtvUHl9Kc/DUk2ALisFtI2OzHuZuVUbVF5TQHX6YTjjoYPtVdA2Br30p/Vix2zGMyXI6bK196o8Hrr8RU/QwAAK09xLJ+lS//3WlCAnigXfC8No6LFsBXy40IAdwQ/bw5ZO6hPkBXZeS8mSjYa+krUAhcdC2R78GMJ4S6uuLkEe4sZwR/BDx0IH6ow5Xii/8mXDudLJPbexfY3LKVAHgbHQQaKxQwSPhM6XTi6JnUTigoDvXXzRSOgBMHl43BjoCyXT9hIfd4lL0NyoCTdrJHS45mA7AXatgEWfVXmwraLF+zd9aivsOOhwX4MSOAE+EhouMmp0k/PmpcnOQUkJ0anvUdB78GTlW4I5rKz43Eb35y7DAq8BXH/AUdChI5Gpm5NkekPsBW6Y6sNzGCmNRfiCujHQYS8EakuGzFCpYk2FGb5HGNmVo6ZJxAjo6IO7RKgKzGLyvUzpJJS9vqXKYOgx9APIXcqAr9PeqDD0dri/NtRv3xwKPUBvJNdCQFg5Zu6smM04P2LqH/w7Kyyh442Ge/Kx5vKShxDM8CVnQ62uXM1gB11JojD+gsxXzpr4RULvJOo2eu81Xsk4bKBHa/wizgDI5rPWRezEheAGpq0YiEJbQE975R0c24blJdrZFmAfvDO0VIlxbyUjdLFRFPPIf1bZDLGtEp7ilNLHrA3tfoju4QboolF9tFwTQQQv2JgSW+mr8Kwz7GEIGmJi4x8LtgCEK2L163SDCUqs9sY+Rk5zV999aWqpQvmJSn9M0AaCsmGGrpTgt/Idvt4S31vU6HNIbUl650rzU9NC3Zma8oCr52gOTbfJIJ5t6JHQmdqHCWBH66m4BdOJuj6rX0q/i01jW14C03hDmk6r4mCH742DYhnaE0ZseutCyjCpX2u7o0NQdNSVa4rwuINo2kAIdEjy0d3ShOLwPYXpTYVjCZJUls1LUa+148jzdpQI2FPOXAM2ZkPcTewbjqMsYTXi7AKHyGL+AGeD3IIxMB2xTZ0X9JDhPU9D5rSTfMcPA+3CdO6wjooxut++XepDxIEZcm0Ok8qonDaWJxn8Cm65pq3rwVIwRk6fOY5UffEbvadIiqp3hjzMryiVnE+p2VZHP9Ki6ysvGNdPUe3o2FspPKfkp9I010A82Z3SBjJV8xKG7N1N4gsdprma3Cc8vaadf0Zwj8zTBylW3Kpv7h8IjVpvsegPxH71efDCo07ouYtnMYYpKCSbfLIDnf/NCketGIkKrrv9tbfSWvC9LDiiETvTqivzkjaMgyASURCHbeIxinLvbm9HPiBtmuOcQh0h8nu3LMs+04XxjpYnPcJt1KY+A3aSakN0Btnbnh6EdnFOcxHeEms76mnF/uQgqG/dXEfHOLtxR6ic7L86WrlnPJGTa2+lk8r+/bhtN73hdaTESsTSIPWAI7IE/qozH5FH9RvlxVRoJYuDI8amAxgGi5MOOQ/U3hOPlO13sx7v9JSwN/XL+RXXjj4JpROzLho6Cfwa5+lYqWyOj01VNZx8WgwrUeGVloa+NE23eK0+abZDqWj0YbJ10eJflUTzpdzVffwIdwS9y0zLqE4cEUVtUjTbbXNJQiGEs0gIVaorjryGZGGW2trzb4Q5VqEvwhgU9wdhwkTztEekfFTY/sHHndc8Tmu9f6cNe2qVJkLzcUm1J93tO/0huQMOA/yAcGEMGzGf3fBZUXepWMqgCNWHpBl1mPW3TAuUdvgR4gMI8mdFXIYd3F4bqgr+VJQEvE76EQHRj8q6s1Kb/cDg95+I2Bopcu3bHF/8FUnWuDeyLP6u+YRTMZfE6aWkjGXXr5L/dcB/RURxu1q7WV5fl9f6VPW7bRIHs/Gsf2zY1viSH96vAAAAAElFTkSuQmCC" - }, - { - "title": "Introducing ChatGPT | OpenAI", - "source": { - "name": "OpenAI", - "link": "https://openai.com/index/chatgpt/" - }, - "original": { - "link": "https://images.ctfassets.net/kftzwdyauwt9/40in10B8KtAGrQvwRv5cop/8241bb17c283dced48ea034a41d7464a/chatgpt_diagram_light.png?w=3840&q=90&fm=webp", - "height": 1153, - "width": 1940, - "size": "93KB" - }, - "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALgAAABtCAMAAAAlHltpAAAAwFBMVEX////P6t38/Pz4+Pj09PTe8Ofr6+vw8PDJyczExMTk5OTY2NjMzMz1+vjM6dvV1dW7u7ve3t61tbWtra2lpaXg4OQAAACfn5/W7eGNjY3p9O+VlZV3d3fI4dSCgoJiYmKpvLO1yb9ra2tXV1dJSUmisqrB2MzR0ddBQUFrbXU0NDSPkJaIlo9venSQn5fb9+klKCZJUExZYl5+i4N3eIJQUVs5PEdARlOUlKWioa9YW2esrLk3PU7BwMkiKT6BgomhiaRPAAAOkElEQVR4nO1ci3bbNhKlAAQwgKnxjBmWsmI5kRPXjpPdJn2k7eb//2oHpB58SSalxGnPKWxRMs3BXAKDwZ0BqCw7upDjRZP0aeJZZhzWIfgxqtOBGU4IcdPlK+DEcIrazRHaM+mtzbk6QrICriAEa/R0+Qo4lTL6QO0R2jNGmVBHdXslQxiljgg6XZqstXNO2BHaTyjf2cZTc/MKRqsqQte176+//o8mGSftu6Cbt/XJwQattXFeq9peQsXmE6tU7O+MYCxEb3xU0ctgJUQ8eluClYHm1ke7T7bSrKWWBUhbcCm9jNLiG75kJDH4YIU0uSn3AmcSFcgQAS/lEquQNkgwylqw3gMOv0LKPeMH0fqwyBG+LfAYvQ0IOs8Ru8HbyUPhDgFXKJ8HlFIIFj/KcuFCIUtpZF7YuUv/C3uBU8Sdm9xDLhcuiduYW2lhEfJFrsocdJSFHlZPGSOEEsFwoKBrwherXuk3EzT9+xDwTJAkj1fhOCNECTzS9Gkti5XRA6bChMMLKFaQFLokzBQjwooEAq+hGT0wGBxHM2u54mqgp1slbq/DqCrEG85afVn7ZEX5I66iPTj1+sgSiiRJJR4keGP3NHi2yMuFl3M/L6O6dvMcXubF3JcF/ixMXOSxiMUwhkqzmdt5EXUBi6K4xv4tc/kyLopwvdhnYU3gIl4XKDMPZSzLIs7RWENRxqIIMA9zWdp5sPmeCiwoabhUAQSP3IJZGKmdwrFpJFgADSYOD4+6ry0Pkimv8DojrTPAU4UgwR32d7WNo2JrrDFGC5A4zgwi8EblORqLVQb7b2+LH1++sx8/QfVp0hVX0RqcidppZRwoUMbjm8EfCdxQMKCctsFzeMTwpqk+TbqycftygePJXnv/amHn87iwMF+8Qn+6mIcFvI45vtky5otwDA3cp/o06ZrqVIVWvxldf6LVKbDoqiOvT5JDTnGy6tOk/9E2/n3Kv8CfuvwL/BsU8myw/Fj/81HxH4bFn40EflD74fLsfFNmWLZ/nP84Dviw+Gz2wzjgbfFNDbPzHx4H/mK2Kcv7+3fvt381gG9ZL2neTA3r2UbjbLV6uNpKn++At3ln52Z22mezq8uO9pHA3z/M3i9nbdFac+QepMSAK3CwVgPyQgzdgLaBP6DqIeDIQjBe00YmKRvanKQB/N3Fh/+8u2yIpyBCMMGV4FoIxgTlXDFOQQjFG6IvVsvZuyHgyEu91kYjZIUvBA/GOoBOiz+8mN1f9oFToxXeKVIq5FY6ANdmAPjl/X8v3r172xLHYBmM93i/MSAVxpv23uRBY6xqDW3c8+X91WoI+MGyA37182onfj5k4wO1NbRjd93ctIFjjMeZIgwwWhSCc8YdtjgVPMWcjc56/35nZA3gQjptkH1qriwwAIZ/boncDvj72Yud+A44QUIrMfhQGMhoJ+Ue4Jezhu4N8IqVZSlIJpwwTgSe4DvRm9XN1dXyCg/4drPEw3LZMhVXJSSQNYNHQ5Ua7X1rp89m56vlcrVKgisUT9KryyZwa71Kxm25dLAH+Orn1f1qdXX/sFoPkxq4tXjLvspjRK6jt1HGuBO9vFo+oNIK/VV1WN60TaXipSkaFylSrzhoo8WvUCWCrwSX6cNVE3iG7YV9nnJCFasdBH5+ieUGf2bLZos7Zwg4rpmyDvs5FaX4VvQGJZaXeLy8XKYKzm9uWsCpUk6hoQgzkA14NkPpVC5vZrV4qqU5OEFzivEOViFMLyRfA8e7Xl5drlLzNYEfLC/Qfz9gk2OjLx+wux8esOOvmsCVjyCDDcEMhCjPZlcP99hPxQ2qXd3cr36+XN0vG8BZSnQZrCKitYiu+Bp46unKVpfL8cDPN6OjktgOj0aLcxzJGj2qGshhPJs1xTdvTVMRhGMNPDkF1rvzjfbmRLR1DYfLj7PB8qI1S04Xnzzlt8qLx3EjTRostc4R9G5YvOrqMezwgPiE0ksQPCWtJc+P1/SmV9koMXGX7y6Ej7CVnga8p318ORL43ZdPZ5vP4hf761Z6EnD62/TFmnV5/ntXdJxm+fl2m3qkX7582UpPa/Gzx6/ZU+gfvcrGqdQN82Swo+/ThsjRDY793IP0uAwDA+m3YrnGSKPwc+2v/+bBMpEAWtfsnDLGEqGpG28ScB0lHJs87K/hjtLMEbYaunK9sqyqg6jXPZCpDZsEd0i2J4DdFWWVkKYNYBRw6pAO7AWeYbgUeB68dmmtLpee24GWTSs99KgWFzILBVPQOvk1JiDFnMXQhUjuBHJ5ACEHAF4jzw4vj9HiM35x4TNodeRIr3LWCCP5jgLWplItADfTBUOVMgd5bhuraGRs6wudkeu3DunzdOD+9vOWLNDb4rYN3EifHE0qPh1UAOhVkdaiDEZXWzh//IYG5bhyGmN8JJeaO8dxHGjHjeIN98dMVrye/4TebTrwePvnVor9mn9qAafSSLRukB40hm5Oc4Duci55ZWlW5hjmvd2eo9lZivKD9D4EBxjFYQyo8dZDem8I2+yni4sLLltYxwGnoRFJqnLrm3bukGy2DJBsZziNYjzg/XB8a5w8S2EyVywNfSZSdJ+oPVOJ2TcuEyDLwqv2hphRflxxjO1qk7RoCqnHPdkCJ05wlwI3jBZBpBjQDKxDaxWsDG3tZ2gqhDBFGWcUQ/yMoYnwPiQusebORp5RLa6Ucaq2Oup0Arm2wdpUMDYHnFykDGjcIG3keR84KT2aRXsrw1mGguByn1JBEidlGaT2Q76e96aRccBBm7X/r+JqBM52wDPBFE6n1aQqhGCEiqF13YBmq9u2f5YpnCJU6k7hNMXOwhGihqYvknd3Tk3340zKLa4TucpZ2g5DKxSPrGBb2Tv1aO2H9l2cDNzakNKrEg6vmRN/ROj2XNFNyVLyZ13eVJt/pgHvTjlnGWicZtHf6H2bBihnhCUrzFjKU4mGG3tc3fNNefNm+/H5zquMLr2Yc0RgATHyKK2x6ACQEsF25E7SrLpxyMTQ7Y/O5aMiIsZ1Jgh6TEFFo88maX5+GnDy+1Tgypq0ZpBWDLhPmdkdl5ikuZen+tamosApQDqD9EUgl9GA/HOTpp2iuQ/lVK9ygupJV/dyBPUERI+pK3s64GnfUttYauDohTUDB84oCsoZZAUWu/fxCicA75H38cCJZcJk7ZmiBq65jBwplHWel947nFOQfTTmusau3Za+CcCHM1nIJQW6+LQAQwiStEyIvvUanr3FAEo2zWVzFU4NaSM3slONU3i1+7FJEHXi2lrinSHrbmy9PBm4WKiQI5v3XuTRh7RPNcTugho6og8XAZliFzhRCp0sQztRe6IxhZGNcCmuwU9IkTdVjwfO/hpkh4xXbBXjpkwZZPFaCNUlDxjxqTKGjDX/UQOPUoE3Hm934m6x8cD5b90z7RupMyLDdg9kjgGUM81k2LrFGRGMK41BzDTHcrKpKGlyJKvYcviGQ8ta6G/NdDr/8OEn14r8vrcf50aakFbwmUFLxAlKgulD0tY5097PPRF49+rRwIngvUl7imLobgP/9uywLiaYbvKw0uwkkgEwJuzJA7OIKlhAv0LrTxuNJ65IjAbOXC8fWbtDjN1T+G4HM5tZ9uXuT27uPv7PubvbX7S7u/u0dh9TVySOobVZSpfiYTDpSVp/9Ur55ROn+cdPCg+f0+Hl2uKeqMXDK070RTu3P0ozk3p9oNtDLf00g9PwPC9Je5L4zrR2M3Fsk1+kXyVBcgfKgG8/jnCS5knAlbUd13CWyWikMukxGB64SFujVOh64vmCZPAa5S+ayL8CcAI7PPXzV2zwyR/R86ZnGU4c0qUERRA27XXBEK2XQqEgXdXiLelpbqHLRTYrEr7gZR5sDBaQq4WY99PMQysSI22cafAxZ9OTntuyBzin3JGUSNNccZpSdK63updlL5FFxPaKxFkWNZIfT3FKVmnbibPaM4eEvu21/bVj5qcjvMoWeJcybmy8P3H19rgLBXm07X0lZ5n31rOAh6CiZU5JHAd4B52HEqocLxyxeLWtoEsuaxuXENLAx6jHCul8/cRZ916shPTIS8uAJ5As1evs8bLYan91pSvgSMp0WsDFl5K8WlLRHddAX1uSlSXi/9A4e5aeqGQCOTFnzIgqw68EGyAeIu919hTgvWc2189IjKjH2fWKRBMA2nhexjJYi28LXuQL8D7PfV88HpGtPVTqPVlpg2jQWhgm08NLw5dqHpMltxoumQrvjYahJ3nFcYn9daGqm8paL15p4OiBkwV7iaR+WLoIxoWideqp8iouhk6Prb0Ky+pHCGnG+xsPNyWluF3bCkYB1zj008vJ0PTkf/8UnBZgNUbjou2SJmiWAb3SonXqKYAbinF4WsNVvOkSJ2j2Od5zeUrM2Z1Px+XHTXrY0YDAaaCpOh0o3yLYj0RpiCZv72F4qphzqLJ0EOCNoR7DeK6dSHtzcQrpOBBIO9oDcbYZmD7FisSeUre4Q0qMPMFXcae1QSL4DsErIs/ky4zBdcPNfusViUOV7SCkvddkvShP+7wJjJUMef9Q0nOkrq9uKmkPtWNKpLVdJbhSUpi0H6OdxAQZooU2m//OmSyVvgIAXSVyI29zGzTGIdaa3LYoWV5gCPWqBbUmWQGUokpxgfesFU1v477742TgBFkZzURyLmklPn2oHrBgbd+XjKTD5itxDAJSBBRjETxEnfuy9AMhUK+4p1sD6klPEm8/K+72fa3DSNVjLjK3dzj1poO7vSPZ83SopSuSVX/fhko7TdKowLE9vMC97+H8o8oo4Lf+kzEf45/g5vkncB/D7brtalobvbGLvCx8ASEvdRl9HgZshah9xPGbAfefP2fEfv5M8XDLMnl7KxrAM8qIMBjK0LT4w4jBscEGV1V68dwJZZyRVptfdge+AdAMlln6IoTThsyU8hUGJ5U++rRKGOIRX0N0tOrTpOtgWRlhPCOcdAnONyzfNZA4SfVp0v8Cn676NOl/KvBTy/8BsqneOZjJbOsAAAAASUVORK5CYII=" - }, - { - "title": "What Is ChatGPT? Everything You Need to Know | TechTarget", - "source": { - "name": "TechTarget", - "link": "https://www.techtarget.com/whatis/definition/ChatGPT" - }, - "original": { - "link": "https://cdn.ttgtmedia.com/rms/onlineimages/chatgpt_screenshot-f_mobile.jpg", - "height": 252, - "width": 559, - "size": "12KB" - }, - "thumbnail": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFwAzAMBIgACEQEDEQH/xAAcAAEAAgMBAQEAAAAAAAAAAAAAAwQBAgcIBgX/xABBEAABAgQCBAoGBwkBAAAAAAABAAIDBBESITETFEFRBQciNmF0gaGy0SMkMnGR8AZSVGKSscEVQkNEU3KCouEz/8QAFwEBAQEBAAAAAAAAAAAAAAAAAAEDAv/EABsRAQACAgMAAAAAAAAAAAAAAAABEQJBEyFR/9oADAMBAAIRAxEAPwDmMJzGua6JB0gDiXC4tuFMq7FvEfBdDc2HLOa40tcYpNOynzVTwp2CxkNroLXFuZNRd0Lcz8scpUD/ACd5rpH51rli11cu5fp/tCU+xj8bvNYbPy7X3GXBF1aVNKUySh3LiN5hQutx/EugLn3Es+76DQ3NwBm4/jX3d7t6ipkUN7t6Xu3qD8v6RjhayD+x7rqu0ltnRT2u1X6TdkPRmCHWi68HPsVbhThaHwaIZjMivMS6mjAOWeZG9WNchAML4rWXtDmhxpgrpljEcmUxPfjJbPZNdLUpmWuz+PuWSJ63B0tXH913ZtWNbhUrp2U31CxrsE5TEM7PaCjVmk9X2pbH7rsO9bUm/rQMsqHP5oo9egCtZhmGeIWwnINK6wyn9wQbMbOXN0joFv7wa0+asqmJ2C6lJhhrliMUM7BH8xD/ABBBcRUxOQjSkdhqaDEJrsEgETDCD94ILiKprcM5RmYGhxQTkEkDTsqcsRigtooDEo4NL8TlgjYhcKgmnSEE6KG929L3b0EyKG929L3b0HkDaibUVQQ5IhyQegeJHmFB61H8a+9XwXEjzCg9aj+NfeooiIgrzkjKTwaJyXhxg2tt4rSua0miIdGMjNggMwrCuoAraqzbnAgMix2YZQoV1e4olRdohFLhc2ba4bfQHE9HetdNgQZlhycPVzln8VtpH3O9YmwTiGmBliMsMUue1xrMTZG7QfraisOmDUgTbWkF2BgHGmzsWNMQL9ba3AAky5zph+q2cYrWD1maJBpXQAn4UWXOeORrMy0jC7QZmvu6UGrYjza0zUK53s+rkCtUEyCQTNtpQV9XOa2D3gubrM0SRWur5dyGI4EERpugOWr16fq9iDR8UtFHzjeSaOpLkrbWKHGbZS3CkE49KzV5qNYmwbqD0GXdj71qYrqYzM20DGplqfm1AEY0I1xl2dTLmtKblu1zosQMhTLCa1oYOGGeKwHuMeG3WJoH6pg0DqZ40VxgLWAOcXEZk7UBgcGARHBzt4FFsiICIiAiIg8g7UTaiIIckRB6C4kGk/QGFT7XH8a++scvg+IzmDC63H8a6CoqKxyWOUqIIrHJY5SogiscmjO4KVEEVjkscpUQRWO6Esd0KVEEVjk0blKiCKx3QljlKiCKxyWOUqIIrHJY5SogiscljlKiDx1tRDnmE7QqgidoTtCD0JxGcwYXW4/jXQVz7iN5hQutx/GugqKIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIPIkKNNNhQmQ4QcxsVzmcgGrqCvvw2LcxZkEuMlDx3yopl7lFCZGOjdDjNYXOIb6W20gZ9HvVh8KfcHF06x1uJ9caTls5W7BVEUzGjhphR5aFCNP6AaezBWODvo5wzwnK61wfwfEjy5cWh7XsFSMwAXAn4L858R8SmkiOfTAXEmitS3CvCUnA0EpwhNQIOJ0cOK5ranPAKTelh3jiM5gwqfao/jXQVz7iM5gwutx/GugoCIiAiIgIiICItHutpgTiBgg3RVROA/wY46NEcFnWxWmijYiv/mUFlFrDde0OoRXY4UK2QEREBERAREQEREHkBjZctbpIr2uLzfRlQ1v640UhgyVR6287/Qf9ViRlIMcyjYjT6WK8OIOwB1PyCscOcGS0jLh8APuMcs5Tq4Cvkqj8mO2CxwECI6I2mJcy0hRoiD0JxGcwYXW4/jXQVz7iM5gwutx/GugqKIiINIhOFBXL81m1qpPjRNO5t2AJwoFpp4to5QrX6oQfoWtS1qpse90MOL8SDsHksh78eX3DyQW7Wpa35KqGI8QwQ7Gh2BHOeGe33DyQW7W/JS1vyVUc54pyzj0DyS9+PL7h5ILdrfkpa1VL309vZuHklz6e3/qPJBbtalrVWufaTecxsHktQ593tn4DyQWYnJY4saHOAwBNKrZip3vuIv2DYPJSQXvMQAuqN1AgtIiICIiD/9k=" - }, - { - "title": "ChatGPT Tutorial - A Crash Course on Chat GPT for Beginners", - "source": { - "name": "YouTube", - "link": "https://m.youtube.com/watch?v=JTxsNm9IdYU" - }, - "original": { - "link": "https://i.ytimg.com/vi/JTxsNm9IdYU/maxresdefault.jpg", - "height": 720, - "width": 1280, - "size": "134KB" - }, - "thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRIDD6PSH-o5_a4uY4vMZypbGD47mIWLL6VsTXNuADpOw&s" - }, - { - "title": "Introducing ChatGPT and Whisper APIs | OpenAI", - "source": { - "name": "OpenAI", - "link": "https://openai.com/index/introducing-chatgpt-and-whisper-apis/" - }, - "original": { - "link": "https://images.ctfassets.net/kftzwdyauwt9/44fefabe-41f8-4dbf-d80656c1f876/8dec20d14a894ae52ae07449452a89c5/introducing-chatgpt-and-whisper-apis.jpg?w=3840&q=90&fm=webp", - "height": 2048, - "width": 2048, - "size": "93KB" - }, - "thumbnail": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ_IQLO0924Gl1jYnj0yWaeKwSWj8tbTbk0Jc6cAvQv6A&s" - } - ] - }, - "inline_videos": [ - { - "position": 1, - "title": "2 MINUTES AGO: OpenAI Just Released the Most Powerful ...", - "link": "https://www.youtube.com/watch?v=7idowVzHZ9g", - "source": "YouTube", - "channel": "AI Uncovered", - "date": "1 day ago", - "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAAAgMEBQYHAf/EADgQAAEDAgQEAwYFBAIDAAAAAAECAwQAEQUGEiETMUFRFCKRBzJSYXGBFiNCobEVwdHwYsIzcpL/xAAZAQADAQEBAAAAAAAAAAAAAAAAAQIDBAX/xAAkEQACAQMEAgIDAAAAAAAAAAAAAQIDERIEFCExQVEyUhMi8P/aAAwDAQACEQMRAD8A4hatfh0cQsneIt+ZNdUb90pNgPpe9ZK1b7GUpZyhggQk6lRUkC3cmrghNmGWfOSefWrPBITsyU220nU4s2HyqHIirYcSh4WVbUodq6N7PILDbfiVutF5z9OoXSKmbxRcVdmsy9lmPEZYWHEeLadQ5ckcwb2+lbDEcNYxXDXI0+CH2lo82koIHzBJqHChx1XdWVKIOrTq2va3L6Vo4jbQQOGAFWt9rk/3NTTlKxFVPLo4U1HeyvjsjBJQUWffjqXbds/4rF5qiJjYw7oACHPMK7D7ZMMUIcHFkjzRpAbWQP0L2/kiuVZqPHahyD7xBSa18CTuZu1FhS7V5ppFCKLUu1FqAG6LVYYThMrFpRYiBsFKdS3HXAhDabgXJPzIHck1rMTyFHjQnHhNfiFh7hKemMlTT3TiflhRaRflr5gg/KkBgrUGpEyK7ClvxJKdD7DimnE3vZSSQR6imKAE2ry1LtXhFIBs0V6edFADlq6zliO3jDGFYVOUlbsRJWCAf/Gi10knqL27WFcq03BHeuhZYmON4iiY2gFC4irrH6VFCwR63/f7VdpoaipQk/KMnmV0KxaQ5YFOvl/anYmHScQjeJRhxaQlN0rYQbk/QG/3t0NTsHjxpmKOonpCm1kix610nL2VsEhJ48dpSjzCFLNqzqSSfJpTi2uCg9mmMTfGjCZTjq9Q1ILir+Ujoe24NXftBxjEIuIogwn5qefkiAhSu+43pgltjPMF5ASHF3SQkAWFdDxHL0PGwhcgLStN9LrStKhfmL9qxU/CNZU18mcMxjHYy8NmwJQnmWWtSHVylqBIIO4OxFUmJL4mFM9w8f4NdazrkXDYOXcZmcZ+TKEVS0LeXq0afNYdq488rVhbXzdP8VvTfZhUK/TRal2pIUg7BSfWtDMTavLU62W9aC4RwyoX35jrWjcdyih2MpuM+pPHCXkOKc0qbsvzAhQOq5QLf8b9TQBGyXLjRcQlNzC1wpEUoSh5zhtqcSpK0BS/07p2PK9r7XrqOJ4uP6a/ILr8JbkNLwmLUXYsY2IToUkFJUoqAIbvq0m4AukczjO5W8JEEqOpuVpWX1qLpa1EHSLBVykG3Ig8ue9lrdym9FRHaW83dbxSTxVcG5c0KUNVjtwvdHRV6kZTZilR5+PYhMhpUGH5C3EahYm5uTbpc3Nul7VWkVqVOZRD7auG4I916kK4vFH5t03OrSRw9tt786J7uTlQJpgNSG5BQkx+KpZso2JSPNbbcEnnsR1FO4jKkV5anPKQSFCw570m6SbBQ9aAGiN6KcKd6KAJATWkyxP4ALDly2pKuXMG19vrVAE1Mgu8BwLtyJP7H/NUibnSM25djYXlmHLw0hxyyH+MOuqx/g03lXGy80QsgHqO1Ly8XsdyO0yErddgFUc6VAHQmykEi4vsSLC/u1jIji4GIutFZa1AlJIvY1nWjdXNKErNlw5GzJiGb+PhkY2bXqQ4SAlKQN73+9dxwkYgzhpVibjKpJUb8H3bdK4DgmISXpC+Ni05p5J8vBZuL9q6rluVOEUlzEVSo36eOyULH71y85HdJJU73K/2sY0uLlp+I0fzpygzf/gd1ftcfeuNSUcOJGa+qj+1bPPuKJxbHUx2llTUYlPy19fSw/esfiBCpFh7qEgCu5RsjznK/JZZAjMSs74IxKbS4yuWnUhYuFWBIB+4FTcbzJmnEyYWMKeELxSboVBS0kEL2GoIB/eq1nCXWsD/AKwh9xp9EkJabQhSV7AHWFdLX5j1vXuIZmx7EovhcQxiZJY1JVw3XCRcG4P2NSuehvjs6NIw/BEe0DNL7GLLdxAwZeuCYJSlv8oXs5qsbbdOtVfs3gt4LlV7Hp0Rh9jEZAiv8ZaUlqELh1xIO58xFwOib1lGW8VfQ7jjeJOmTKSUSFg/mELcDVib7hW//wA2pUjBZk6W1hkueVIgsjhiQkaWWiTewCiAAdI576vlV/ikYvU0le76EJexfImap8GBK4TzTvh1rLSF8RrUFJPmBG40m471Z+1nGsRmZpxLCZD6VQIUq8dkNIToOgdQLn3jzNUU9udMxxqLPmLdl3aYLrhuUGwGn56SbfanJECTPdenYrPUh9xpLzjklPmUpWqyTdQN7I6A/TuKDY3WgvJq8oSX8KyTBk4fIdgmXizzc2XHgJlOBKWroTpIO17epNTnGcUi5rxGevNjjHDwVmW7OXhDalllShZHCuACL3vz6VlMKGOYP4ZvBsYmRfGrQHEtakJBKAvVa5CrJ5nY7WqLMexFx/GXsQxqUuRwgh9RBX4loqSEgkq2SSUm1qX45Aq9N9P+6LjN2bWnxgb+F4orEMXw9x1xeJrw5EfZVtKOHuDbfnUjPWa8bfy5gEdcxJaxTCuJMSGGxxVcQi99O2wHK1Y7FMLXhojFxSjx29YBQBbl2Ub8/lUaRKkSW47ch9x1EZvhspUbhtF76R2FzSxsXGakrogqTvRTpG9FMZKCaUBagKR8Q9a91I+JPrTuSarDJMqB7PsTlwnVtOR57B1oNj5goH/p6VSz8YRinEXKCI8xl0J8SE+RwG+6kjkduY2+laRDkKFk6RgUh1BkyQZMgJWDoUQNCfqEpST8ya5vIbdYWWyUqSuytuRHT6VMmOJ1PJ2cWMJU23MYRu4ga0eZCwVDe49auM953bdirZwIfmqA1vkW0BVx5e58tcnixvCJZlNJIeT5hrKVJ325W++/7VdsxC/lSRjC30lbs9KeFsNKUoI1fcqT6VMUkVKWXJDgWSpxw7htBJJ6kmoqSOMlaxcagVD7082tKYjoCk3UpI59N6Z1J+JPrWhJuH5MdmGHw07LugLLigG0aCCT1KieXasJptT/AImVYNeJVwdGyL7W5elN3T3HrUQp4dl1KmdhPmta5ta1r16tS1qKlrUokWJUq5tXupPcetF09x61pczEWN73N73vQsrWSVrUonmVG96Xt3HrXm3cUXAQVLUEhS1kJFkgqJsOwpNj3PID7U5t3FFh8qLgIVqUEpUtRCRZIJuEj5Ugop2vDSGMFO9FOkb0UcAWwy5F7D0pwZdhgXUAB8xVO5jcsYopDDyXGS6EoSEixF+htepuaMQiKirhNuqL4WNQCdtuhP8AjtXbudPZ2gLF+ycMuQ+iU+lLGXInwj0qtyliDq1Ox5LwLTbadF7DTbaw/wB6VF/EU2JPkFRQ+2XCEpJ8oSD+m3y60bqhZPAMX7L38NxPgHpXv4aiXvoTf6UziuML/orUiEoMyHSk6SQVJTvew/3aoUDNS3ElmWhCDwyEvpvsq3Mj69qb1ND6CxZPcyvFUbpVoPyFM/h0Mq8yEOt9wLEVFy5mFDDLzeJuurULrStR1bW9369qaxnMAnwGjFU5FdS9uhLm5FtjcVD1FFq6hyPFlzDyjPkKKDhz4cYKhJQU2KEb6fryGwvXv4bifCn0qEvM6WUwklhtwuNpU6pKvcv2+fWokrHkRsdXLZU5IZU0EBsnSBy5fz9zRHUU49xuLFlwctRPhT6V5+GYnwj0qnnY6nEXIrjJei8F7zDXcKTtvtbfblTsPNYTPkeMKlRN+EEI8wsdvUd6vc0b/AMWWYyzE+FPpQcsRLck+lZ5OY5cfEX3m3i8yteyHL2032sOhtT+OZi8ayz4B2RH0qPERfSTysbj70t3Qt8B4P2XH4YifCn0rw5Yi/Cn0qtjZpkCE+X20KeSkFoi4B3A3/mohzDNkty9UgMktAtpTtZQKb2PzGo091Q+gsZF0csR+gTSDlljsKoVZhnKVHUXCCz71iQHf/YU/NzC/KhAIcLD6XQfyyRqTY/36UbnTc/oPGXsthldk9vWiq05slAJDbSOQvqvuaKe40n1DGRnlc6DubkkmiivKND0gWrznz3oooAdC1kC61GwsLnpSLUUVohBbY0miipGKVva9JtRRSYhaORpCqKKb6GAFOJSNNeUU4gB3NulJIsdq9opMBFza19qORoopAKtfnRRRQB//9k=" - }, - { - "position": 2, - "title": "OpenAI Secretly Released a NEW ChatGPT Model and It’s ...", - "link": "https://www.youtube.com/watch?v=uh4baKXL6K4", - "source": "YouTube", - "channel": "Unveiling AI News", - "date": "21 hours ago", - "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAGAAIDBAUBB//EADYQAAIBAwMCAwYFAwQDAAAAAAECAwAEEQUSIQYxE0FRFCIyYXGBQpGhscEjYnIHFlLwFTOC/8QAGgEAAgMBAQAAAAAAAAAAAAAAAgQAAQMFBv/EACkRAAICAQQBAQgDAAAAAAAAAAABAhEDBBIhMUFRBRMiMnGh0fAzYZH/2gAMAwEAAhEDEQA/APJJDU0EWxdxHvH9KjRd8yqe2c1eVUz75GcE4IOPvVisSuFmnbbCNi+cjfwPOro0SAKpuJp5GYZBDADFMaW4RhIjbSoGCF5+lQzXFyyLJk7QCBxwKzcmNRh6l9NLsYoxzOAT73vZ2/PtWXr+lXFmFmWRJrRvgdDyP8h5VH7dMMNubag4GfOprXVpFlXeVCtw4PIP1qk5BNR8GDSrYu1t0nYQhRGeVB8h6VWZUP4VrQAogkHI71Nv3L+9OdUHkBUR2jtjmoQaeK7mmkj1pZqFDs13NNzXQahKHU4VHkeop4PlUBZIlWI6rqyjuQPvViLnGKsykWVHFKur2pVZhZy0GZ2YjIRSTWtpVq093vlGQVzgeX3qvp8aRl1fksvPzq1FO8NyFQ4JA3DyrOY1iXJr2FkuqTMkUAeNfiHY961LHpcSPiVO3ljgfLFaX+nyNEJGdC2Rgue3/eaML24trRN8rxx57FiBSk2zpQS9Dzu+6QgEp8BAuTnFYOodMojf+sJx5DvXo8+rWk91EltLFK/Jbw2zxig3U+o7GS9KmRs84jA+GghKW7g1lGFcgBrVsbbw0f4kZ0J+QwR+9ZdG3VEcLWceoRHKOwV/kw4/Yj8qGJdqjLDj6U+ujnNU6NnoLwvatT8bZj2Ndu/Hf2iHt9s0Ua9eWqWfU+t6PLFa3630cMscWAVkWRx4ifJ1OT/cG9RQZ07pdvrV7NbyTeAqW7Sh9owCCAM/LmteDo6EK/tbXIljtopHijMakOzOpGWIGBt9aJRbFsmqxY3UnyEUmuy/7n1prvULgW9rokbwtAVLRuy2xYpnjcTnP3pt/NPd2hu+mZJJNYuLG1ZJiEW7kh3TiUjb+PcIgdvO0D50N2PSdpfoZIbmdI1M0bB9pKyqyhF4453Dt9qoDp6NuoItJFwy7YBJcuVzsOzewA88dqm1lLVYm2r67DZm1Q6fqH/iGH+4Rb2IvjbFQxk3T7s44zs8Pd8+/NZ/UGnNrWnXVlosFvc6hFfwyXcdoUCh2tkWRlxxs8UPkjgH5UO2mhadqonOk3VwxS2aURXCqhVwwABb4cEHPB4q1a9IJJqiWlzcPHELOKWWQbW2ySEAKMcEZPf0FTayS1eKN26oKdRmvbq7E3Rc1v4LancnUZEZPDP9TCGX1h2dvw/F51WsrrTLTQ5XvIra4tJNOWKYQKB7rahcAtGDyCBhl+goXTRNOOmNLLJereJdCyaPamwTEHz77cj61Yl6as/EaO3mvGaC8jtZ5niXw2ZmCttI5GNw796m1lPVY7oLbu0a0jjtun7nxtSigsUkn0/aZntNjbmhyfNthODn4c8ZoU6zt1t+oHxKsjSwxSyMqovvlBuyEJXdnvg4zmp7jpbTba7tYLiW+j9puTbojpHuzkAP/gc/WsS/itbe9lhsjMY42KkygAlgSD28qumgI6iGX5RL2pUl7UqgA57hormEICxLDgdznjFalxYXFjOBcJtZuQQQQR9RxWJLKYNQgm4/pujjPyOf4r2a70uG7hezigVLe3gz4oH485GP1/KsMsttHR0+NStmjpaBNJg8GP4YwQvqaFtZ0bVtWc3F47JHvwkCjOU+fzPpRhp06qiKQFAUDAq9K6CMkNxilV6j9eAV6K6Xj0yaW7kQqWO1UfnA+lDvVfS9rFrk0iRYE+JUOcDPmKM11SUQzzRWs08aDEaQ4y7feg3qXqK4lu7eG5szA4ALLkNtPOeRwfKri3douSXTMq/0uReltTjUDaoWYDPYq2Tj7bqCISJYSjeXH2r0m5vFktJYsZSSJlI9cggivOLeMRQ734JH6U1ibcbEsySlwR2t1PYtcCEgGaFoJMjPunGf2rVHU+pOreP7NcBoo4XE8AcMqElcg+eSeay7lOzj71CnB+tapsWljhLmSNW01nUhIbayESe0XUcqwxRhV8RSNoA7AZAqZoeoINUutXMEi3UFxiZ8AgO/G3b55zjAz3rP02ZLXU7O5lzshnSRsDJwGBOKMZurrIxs1tBK0rSxTOGUAOyuvz/4oPvV8eWL5VOM0sWO77/foZl6nU2Utn0+KFLlGtY4YERUGTuYAA+6xxnn0pA9WIY7CGJ4ZGhTa0JVS0cQ2j3wcYGeefOren6tp2mXLPbNdzpc3guJTJGAYwA3A55OW703TtZ03TYI7FDPJbLHPumkt1b3pNoA2ZwVGOcnmpuj6gSxZ1H+L7P9/Jlay+ueDctqEKxo10k8rJt4lKkL2PmM01+qNSkBz7MHdkeSRYFDSshDKWI7nIq9Lf6JPZXdlLczRpNNHKrwWKoAVUgjYDjzHNDLhRIwjJZATtJGCRUYeOEZqpw6/r6FuK+nTUhqAI9oE3jZxxuznt6ZrrStNK8r/E7Fmx6mqi1OlUaOKRaU8V2o1PFKrMqHX8W9NwHbg/SvQen+v9Pj6e8HU5ZE1CKPw8bCRNgYU5H65oGicSrg43eYqrPZ85i7elBKKkuRnHlcHwe0Wd0Lq0hni4EqBgD5ZqxNI7QHc+0fib0FB/RWpSSaH4UwO6zbw2/w/C37j/5oklmM0e2Mggjn6UjOO10dCGTdGyOPVLmS0C2emS+Go90uVTI9QM80Ma1c3nhqj6csabi3vSKzE/nRncQXE1qqRssYxgMPKgnWNN8CUFr1pWPbK4/mih2G5raZssjHTpnlG0tGw257ZGP5oRun3NsXsO/1rf1y4MVuIA2Xc+XoKH1QscDvTkVSOdOVsfF78RB8uKrlMHB8qvbQiY8hVZhuJNEDuGqM4rd6esDeXCxRwmWZyFjT1JrGjWiroy6Wy1a2uJDiNJV8QkZ9wnDfoTS+p+Q7PsRKWouraTr/AAK4Ok7DwzHMZHm8MHxrYo6FyiuFVce8u10y7Oi5YAH1G+qNBGmxLlTtkTfGzxGJxg4Ksp7EEHzI7EE16GIfBgilC3YggYiMowZZv6aoqnbu8b3UXbhV4zuweSLdfT2/hCyhlWV7Zn4TO2BTt/pD1wwY8cDdgUq4qNNHoIZZ5d0Zcpp3x1S+3g8wlXaxFMAqece+ajxXQj0eJytKbo4KlSmAU5aIyZODxXKaDxXagFFgqVOVODU0MhdirDnHepZI6jhikaZfCRnb0UVZjGYWf6fLMdUulWMtAYMSt5Kcjbn68/rRLc2E0Mu62baP+PlTdBiGn9OaaYlCrPPmdh+JyWXB+mBW867/AC7UrlXxD+GXwgdf6xqULm3KhPQt/FDmpXM+4vPNub5Uaa1bvcTLGF4HnjtWVL02kRFxqHNuoyEB5kPkPp61UOXQU5cGHf29vP0/psdxhblzLIr+ag7cZ+WAPzoc9me3ciZcN5fT5Vt6tO1xfAD4IxgAeX/eKsxFvZ9pOR5ZFN7aQlKdvgF5Pe4HambKJ1MUhxcW0cnzK811tL0+Ye4skJ9VbI/I1e0H3gMhMCrNrO8DZU/WtWTQZMZt545f7T7pP8frWdNZzQNtmiZD/cKCcFJUzbBrJ4JqcHTLttrl5a7/AGWeSHeMP4Tsu4ehweapXN7JLHs4C/Ko/CrnhVktPBeDo5PbuqyRcXLvsqMuTmm7KuGKmGOtqOX7y3ZV20sVYMdMK1CbhlKnYpVC7N5UV5kVhkFgDW94McERSJAi/KlSrSBzW+At6ekaPpU7Dx4xGCM8cetasBy7JxtC9sV2lWWXyO4W6RRviVKbSRuYg4rI1QYgcc/F60qVTH0iZHywQMaeOfdHxH96vxRJ4ZG3ilSrfwJ2yMxpub3R3rqoo8qVKrBseow3HFTFiwKNyvoe1KlVA2Dd2ipcyoi4UOQBUJA9KVKszdDSBTGA9KVKqDRGwFRMBSpVDSJGaVKlUCP/2Q==" - }, - { - "position": 3, - "title": "OpenAI's ChatGPT Does Research… And Breaks Itself!", - "link": "https://www.youtube.com/watch?v=iC-wRBsAhEs", - "source": "YouTube", - "channel": "Two Minute Papers", - "date": "2 days ago", - "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFMAlAMBIgACEQEDEQH/xAAcAAACAgMBAQAAAAAAAAAAAAAEBQMGAAECBwj/xAA+EAACAQMCBAMEBwYEBwAAAAABAgMABBEFEgYTITEiQVFhcZGhBxSBkrHB0RUjMjNCYhZScoNzgpOiwuHw/8QAGwEAAgMBAQEAAAAAAAAAAAAAAgQBAwUGAAf/xAAsEQACAgEDAgUDBAMAAAAAAAABAgADEQQhMRJBBRMiUZEUMkJxgcHRFVJh/9oADAMBAAIRAxEAPwCsoGA7CpEVicEYqYReA49KVcLcNrrPCfEGry313HNpUIeKKMjbIdpOGz18vKsjT1+bnBjdjdMtUMMkSqGBHTvW74MY+/lVd/w8q/R6eJWu7s3AvBbC3HVSCwGfXPWrA3AGkW93ZaJrHEl1bcQ3kQaOKGLfDGTnCsfPqCO65x5dK16mIG8TdczvQI2+uwHPXePxr2b93PbmOTBBGCDXz7BwVcpofEk8t3cftfQ7pYjbKw5cqHb4s9+oLEe4e2rZN9HPI4n03Sm1O75F1Zyzyy5GVaMqGA8sZdPnRs+YKpiWXVtOOnz+XKY+FvyoWMBuxBrx+4eKaRooruZld2EZzk7euCfsq2fQ8qniG7gvGaWPl7cFjjIJpivXZPSRFtR4KyoLQdj+/wA+0vG01mKf6lovKHNtQzIe69ytJmjwadSxXGRMW2l6mw0hxmtFaLtrdp5UjUdWOBTC80KWFC8TCVQOuO4+yvNYqnBM8tLupZRsIjK1rFTvESpA6Ejv6VHBbmONUZy5H9R7mizA6RjOd5xtruAYnjPtphbaXc3C7ooyV9ewqW60e4swkj7WXIyVPagNicZlqUWfdjaJp9Eiutd+v3CgrGPAD6+tb4k1KTTNNkngTc4GAPStcR65HprxQIMzSMAB6VDxbHv0RjjJIpGbQE8gvbu6vbl7iZ2Z2PU5rKbQaHcSJvOEyegNZVfmCNDT2EcRqQRG3THTzrX0Za/HofB3FTRahb2uptEjWSSOm+RwrY2q38XXHTBqG6iaUjaxBrjTuFrSTxSRgBcE4JGBkD19SKxabtPRX1AnJ5EZdLXbBG0eanxZLq/0Y51TUrafWY9RR1gYokhVXBB2Ljp7cUz1OXhzX+KtJ4vbiXT7O2gWKSezuJAs4eMlgoXueuB09Omc1ltwTojACeBnX/iMCPnUXEXAeiW1gs1skmSem5yfzprS6qu84QyqyspzBuCOL9MvvpE4ludQnhtdM1eIFGupBGDyiqoDnzKljin/APjmwuOHOIbuW7t11K2kvINPRpVDyxvgoUHcjO0f8tU3ROEdOur6GKRDtZgDgn9avMf0b8PyZ2hmwcEiRunzp0iUg5nhVpLy4Z9jpGyrtGe/2Vc/onVjq0rLnwqCce+rDrv0aWGm2s10h3JuyAScgfGj/o90e00m4Nyi/wA4FCMk9B1qAMHMse4sip2E9UiYMoHspRqWlGW6VoEAEn8XoDTOGZWAx06VKTt3OWJU+XpRq7IciL2VrauGgtjp0VtGAUVpAc78daG1KeWyYcoArID38jRSXqOTtIwDjrQOsTLM0caEHbknFWVgs+WlVxWuohNojZMmu7eyluZOXCwVz2LdhT2z0pOWHuM5P9PpRjCG1DTOyJEo6ZAG2rrNSMECK0aJshmk6BYkVFwAowBVY4u1vkW4SDDAyBCR65pPf65Nc67Fy5GWFiVCA9MVH9VF1ZOs7MMTluvc9aVTB3mlapT0mL+INJm1HVLV41GEALOewqXiC4/dxwA5XHWmd3eRW9vzLqQQxAeZ6mkvEKtfaUt1YMAAuV6dxUvkjaeoKq4LcRI80aNhnUH0JrKFsNKjltw902+UnqSa3VPkmOHXL7SRIx6L8abaNbm6uWtBtDTxMiEt/WPEv/copWq0fpzPFdRSRna6OGU+hHauRFgVgTxHCpIwJcIbO7EEcvIZkKg7o/EPlQPFFyg0+ODcOZ32nvTqRJGiGqaWziKYlpokPWN/6unpnv8AHsaovFcg1K/2uxklzgsp6gYrV0CJTewGcYz+o/5FrA1qiTcMZXVLYv0BbOTR1ldS2XE9xcLeMbOSQl0HUA1XdOE1ruVnZtgO0kYrvS5S9vnPdjWh4heaVDVyjTVdZw0vGp6iNZ0e4hiULMpyFJ/iANc8LoW0tjJECgYjcO6mkulIHSUOzYC5yKN0DUZYbF4IlyGDsW9Kq0Ore/qDdpOopFeMRsutpHNyUJO04z61ZBzJbH9ywLMK8hvtTa2k3Rgc1mPX0x7Ptoix4y1G16R3BA8wUU/lW/VpLLUDjAmLdrK6XKEEz0G4tZosGQEZ86YaZZhBzpMEnsPSqHHx9dSJsnWCQepUg01suNkmIh2pGzdh5H7aK3T3hNx8Sqi/TeZnJ/eXOWQL2NI+KZkfTo0H8bSqo9maHXVZZbWWdXQFThapWq8Wm6cxiIPErZVm7k+uB+FZ1hRB6pvaSi3UP6BxC9VENvqtkkJBKthqMvbyU21x9RTmXCHaAe2aqh1pWkDmJd4OQdgPWi4OIShYgJ4jlspjPwxVH1AAOOZoWeE3MROuKbeXUbG3iJHPQeMZ86MtbiKLRYrORxzQgXHqaBF8L7fJtCndggUs1SV0MLIcESDrSFPiFxvFbgRW/RisEdxGdlp11HEw8IBckVlF20VzLCr81uvsrdboVscTJNiA4zFSrR1iv75ffXSabJ5zQfeo20shHKpe5gAz18VfPnbI2nRgiHaNJe6Jqtzewy8+yuSGmsyMMGAxuU9s4H2/CncuicM8Vs95akC5U4kltn2SI396+vvFA7LVQCL2A59DVTvOFbqbWZtR0/VY7WRmyssUpRx7Mg9q1PD9a6Dy7/tHErKKW6kbBno0PDEMdtyJZ+euMBpIxux7SK6veE9Lu4QDFyJtoBmgwhJ9SOxqoWtxxdZLg8T2Nwo7C5twxH2rg06i4zGnaWv7Xlhu9SZ2CR2cbKrjyJznHfv8q2k1Wnt2BidiWA5JkUnD11o9vcyB4bqLYcu3gZR7c9PnVNs9WSxRWMmN2cAdc05u3uOIWMuuXciw947OBSEX0z6n2n5UHJw/ZyQLAMyRjtvGCPdij0aV/UM2DgxfUs/lYU7yr3V8PrsV2kUdyEYOYpBhX8WSp99LxdiSWRnEULu7PyUbpGCchR7BU+pwJZX8tpHnbEdoyaWT2MU8xlfuRg104BrQPWM7cTAHTY5S04yeYwa4WJeY4LIvVgPTzoyO8trnUTLpqTJaGQ8kTEb9oHnjp3zSK1sFguOaHOAMBc9Kc6ZY3uoTtHpygyxpv6vtwMgd/tosu6l29OxGIJWut+hfVvnP8S5QXnL0udGbxLE7Y+w1Q4tReKOa1a0jdZihW5Y9YsZyB78j/wC7WzUdOudL0UXWo9biX9zhD4PFkDr7B1qiXEIubZomOM4wfSuauyjYadr4anmVFlz2Px2jBXBqVtTZbb9nJYRs8kyyC7OdyoO6+mO/x92EA019uFxn/V0pnYQ/VLZYt24jJJ9tL4VN+Zq5t1GFYFcb5jvT54khZGciRjkD5U5fTrazjWfVG3OOqQg/jQGi6aZPq93G8Ql7KJUYqDuPXtim1zw5f3UjSS39m7HvmQ/pTWi0ulqIv1DAE8DM5rxvVW23NVplO3J/qJ7nWbhpTym5aDoFXyrKYHg6+JyLmz/6v/qsrc/yWk7WCcv9Bqf9DIFiizjmZPoJP0FbFuFPikY+zdj8qiAkJG4x7fRutTJjPdcegFYf0tA/AfE6HzXP5TsRR46Nj/cP6VJEFVsB8/7proSZx5++o3uYkYBpcN5KDk/CpXT0k7IPieZ3A5hgAVclEHtbLH8K6DIXGAGI7ELj8qEWbf8A1Ig9XcE/AfrU8EqIMfWQfbuA/CmlRV4EoLE8mGb2QFXTDAdtrAn4ipYH65xj4n8qCNzbxRl5LhFUf31pBcXy5PMt7U+WcSSe/wDyj5+6jGYBlf46hW/vbc2c9tzVBSYlwpU9MFie/u7+yj14e4alRQuozxuAASkwIJ9eqmpLqwh3AJDGqr2AUYFaSIKOiL92rQ7gYBlfQh3Ilf4s0y20W3gmsL+S65jlWVkHhGM56fpV14M02Cx0sTrMr3N3GjSElSE89o6+3vVe1eFXtJGZBlFJU47HFPOHbmOK2RRNGqFQR46lrLCvSTBFVYOQI/nEF5by2tzGGjOC8UoDKfQjyPXzrznjPSorLUrZ7KLlQTsBJyznDZ9M4GR28ulegyXcIjyLqLPoZBVT150uG8XLnRTnYJOo/wBPX5UtZX1jGI7pb2pfqBixdA0/cR+15VHkeUrf+QpNJbNFxCNNacywlwFmRcFlI/iAP2/A07i0+3u4edbeNB0JBOVPoR5VCdHCSiQSOHHY7u1UNQvtNGrxG8cvmW+wuY7GC3t4lVoIlIwRhjn1JyO/WiP2hFv6xgIR1zhjn7CtVmOJSuHLFvNg5U/EVJh0OUmyP8snX5j881eACMETMbqzkneWNmil8aSIinyMTfqfxrKrpuCOhRyf7WUj5kfhWVHlV+0jqb3gcSgkEjJ9tGRAZrVZQjeGdpxIeZLy36p3xUn1eA4zDGfeorVZRn7oH4yaO3gHaGP7ooqG3hJ6wx/dFarKsEAwJYo59YZZY1IhGY8Ljb8KblfD/E/3zWVlT3g9oDcIM92++aFaMerfeNZWUcAxTrWVMIVnAOcgMetXDSUURphQOg8qysqPeePaNJQNnYUjvANx6DvWVlRCEWrBENVs5OWpYzJnIyG6+Y7H7adcb6bZWuqMtvbRxqUDEKOmTWVlVNzGElZjgi3fy0+FF/V4cfyY/uitVleWFZODbw5/kx/dFZWVlFKp/9k=" - } - ], - "inline_videos_more_link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&tbm=vid&q=chatgpt&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ8ccDegQIIhAH", - "related_searches": [ - { - "query": "ChatGPT login", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+login&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhUEAE" - }, - { - "query": "ChatGPT free", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+free&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhXEAE" - }, - { - "query": "ChatGPT 4", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+4&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhREAE" - }, - { - "query": "ChatGPT app", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+app&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhQEAE" - }, - { - "query": "ChatGPT download", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+download&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhPEAE" - }, - { - "query": "ChatGPT OpenAI", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+OpenAI&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhOEAE" - }, - { - "query": "ChatGPT website", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+website&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhVEAE" - }, - { - "query": "ChatGPT free online", - "link": "https://www.google.com/search?sca_esv=acb05f42373aaad6&gl=us&hl=en&q=ChatGPT+free+online&sa=X&ved=2ahUKEwi17_rnppWIAxX2rokEHfAoEzYQ1QJ6BAhWEAE" - } - ], - "pagination": { - "current": 1, - "next": "https://www.google.com/search?q=chatgpt&oq=chatgpt&gl=us&hl=en&start=10&ie=UTF-8" - } -} diff --git a/backend/open_webui/retrieval/web/testdata/searxng.json b/backend/open_webui/retrieval/web/testdata/searxng.json deleted file mode 100644 index 0e6952baa8..0000000000 --- a/backend/open_webui/retrieval/web/testdata/searxng.json +++ /dev/null @@ -1,476 +0,0 @@ -{ - "query": "python", - "number_of_results": 116000000, - "results": [ - { - "url": "https://www.python.org/", - "title": "Welcome to Python.org", - "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.", - "engine": "bing", - "parsed_url": ["https", "www.python.org", "/", "", "", ""], - "template": "default.html", - "engines": ["bing", "qwant", "duckduckgo"], - "positions": [1, 1, 1], - "score": 9.0, - "category": "general" - }, - { - "url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)", - "title": "Python (programming language) - Wikipedia", - "content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.", - "engine": "bing", - "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""], - "template": "default.html", - "engines": ["bing", "qwant", "duckduckgo"], - "positions": [4, 3, 2], - "score": 3.25, - "category": "general" - }, - { - "url": "https://docs.python.org/3/tutorial/index.html", - "title": "The Python Tutorial \u2014 Python 3.12.3 documentation", - "content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026", - "engine": "bing", - "parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""], - "template": "default.html", - "engines": ["bing", "qwant", "duckduckgo"], - "positions": [5, 5, 3], - "score": 2.2, - "category": "general" - }, - { - "url": "https://www.python.org/downloads/", - "title": "Download Python | Python.org", - "content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.", - "engine": "bing", - "parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""], - "template": "default.html", - "engines": ["bing", "duckduckgo"], - "positions": [2, 2], - "score": 2.0, - "category": "general" - }, - { - "url": "https://www.python.org/about/gettingstarted/", - "title": "Python For Beginners | Python.org", - "content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.", - "engine": "bing", - "parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""], - "template": "default.html", - "engines": ["bing", "qwant", "duckduckgo"], - "positions": [9, 4, 4], - "score": 1.8333333333333333, - "category": "general" - }, - { - "url": "https://www.python.org/shell/", - "title": "Welcome to Python.org", - "content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.", - "engine": "bing", - "parsed_url": ["https", "www.python.org", "/shell/", "", "", ""], - "template": "default.html", - "engines": ["bing", "qwant", "duckduckgo"], - "positions": [3, 10, 8], - "score": 1.675, - "category": "general" - }, - { - "url": "https://realpython.com/", - "title": "Python Tutorials \u2013 Real Python", - "content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.", - "engine": "bing", - "parsed_url": ["https", "realpython.com", "/", "", "", ""], - "template": "default.html", - "engines": ["bing", "qwant", "duckduckgo"], - "positions": [6, 6, 5], - "score": 1.6, - "category": "general" - }, - { - "url": "https://wiki.nerdvpn.de/wiki/Python", - "title": "Python", - "content": "Topics referred to by the same term", - "engine": "wikipedia", - "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""], - "template": "default.html", - "engines": ["wikipedia"], - "positions": [1], - "score": 1.0, - "category": "general" - }, - { - "title": "Online Python - IDE, Editor, Compiler, Interpreter", - "content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.", - "url": "https://www.online-python.com/", - "engine": "duckduckgo", - "parsed_url": ["https", "www.online-python.com", "/", "", "", ""], - "template": "default.html", - "engines": ["qwant", "duckduckgo"], - "positions": [8, 6], - "score": 0.5833333333333333, - "category": "general" - }, - { - "url": "https://micropython.org/", - "title": "MicroPython - Python for microcontrollers", - "content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...", - "img_src": null, - "engine": "google", - "parsed_url": ["https", "micropython.org", "/", "", "", ""], - "template": "default.html", - "engines": ["google"], - "positions": [1], - "score": 1.0, - "category": "general" - }, - { - "url": "https://dictionary.cambridge.org/uk/dictionary/english/python", - "title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary", - "content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.", - "img_src": null, - "engine": "google", - "parsed_url": [ - "https", - "dictionary.cambridge.org", - "/uk/dictionary/english/python", - "", - "", - "" - ], - "template": "default.html", - "engines": ["google"], - "positions": [2], - "score": 0.5, - "category": "general" - }, - { - "url": "https://www.codetoday.co.uk/code", - "title": "Web-based Python Editor (with Turtle graphics)", - "content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...", - "img_src": null, - "engine": "google", - "parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""], - "template": "default.html", - "engines": ["google"], - "positions": [3], - "score": 0.3333333333333333, - "category": "general" - }, - { - "url": "https://snapcraft.io/docs/python-plugin", - "title": "The python plugin | Snapcraft documentation", - "content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...", - "img_src": null, - "engine": "google", - "parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""], - "template": "default.html", - "engines": ["google"], - "positions": [4], - "score": 0.25, - "category": "general" - }, - { - "url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/", - "title": "Latest Python Developer News", - "content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...", - "img_src": null, - "engine": "google", - "parsed_url": [ - "https", - "www.developer-tech.com", - "/categories/developer-languages/developer-languages-python/", - "", - "", - "" - ], - "template": "default.html", - "engines": ["google"], - "positions": [5], - "score": 0.2, - "category": "general" - }, - { - "url": "https://subjectguides.york.ac.uk/coding/python", - "title": "Coding: a Practical Guide - Python - Subject Guides", - "content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.", - "img_src": null, - "engine": "google", - "parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""], - "template": "default.html", - "engines": ["google"], - "positions": [6], - "score": 0.16666666666666666, - "category": "general" - }, - { - "url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/", - "title": "Getting Started - Python - Salford PsyTech Home - The Hub", - "content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.", - "img_src": null, - "engine": "google", - "parsed_url": [ - "https", - "hub.salford.ac.uk", - "/psytech/python/getting-started-python/", - "", - "", - "" - ], - "template": "default.html", - "engines": ["google"], - "positions": [7], - "score": 0.14285714285714285, - "category": "general" - }, - { - "url": "https://snapcraft.io/docs/python-apps", - "title": "Python apps | Snapcraft documentation", - "content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...", - "img_src": null, - "engine": "google", - "parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""], - "template": "default.html", - "engines": ["google"], - "positions": [8], - "score": 0.125, - "category": "general" - }, - { - "url": "https://anvil.works/", - "title": "Anvil | Build Web Apps with Nothing but Python", - "content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum", - "img_src": null, - "engine": "google", - "parsed_url": ["https", "anvil.works", "/", "", "", ""], - "template": "default.html", - "engines": ["google"], - "positions": [9], - "score": 0.1111111111111111, - "category": "general" - }, - { - "url": "https://docs.python.org/", - "title": "Python 3.12.3 documentation", - "content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.", - "engine": "bing", - "parsed_url": ["https", "docs.python.org", "/", "", "", ""], - "template": "default.html", - "engines": ["bing", "duckduckgo"], - "positions": [7, 13], - "score": 0.43956043956043955, - "category": "general" - }, - { - "title": "How to Use Python: Your First Steps - Real Python", - "content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.", - "url": "https://realpython.com/python-first-steps/", - "engine": "duckduckgo", - "parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""], - "template": "default.html", - "engines": ["qwant", "duckduckgo"], - "positions": [14, 7], - "score": 0.42857142857142855, - "category": "general" - }, - { - "title": "The Python Tutorial \u2014 Python 3.11.8 documentation", - "content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...", - "url": "https://docs.python.org/3.11/tutorial/", - "engine": "duckduckgo", - "parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [7], - "score": 0.14285714285714285, - "category": "general" - }, - { - "url": "https://realpython.com/python-introduction/", - "title": "Introduction to Python 3 \u2013 Real Python", - "content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.", - "engine": "bing", - "parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""], - "template": "default.html", - "engines": ["bing"], - "positions": [8], - "score": 0.125, - "category": "general" - }, - { - "title": "Our Documentation | Python.org", - "content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.", - "url": "https://www.python.org/doc/", - "engine": "duckduckgo", - "parsed_url": ["https", "www.python.org", "/doc/", "", "", ""], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [9], - "score": 0.1111111111111111, - "category": "general" - }, - { - "title": "Welcome to Python.org", - "url": "http://www.get-python.org/shell/", - "content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.", - "engine": "qwant", - "parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""], - "template": "default.html", - "engines": ["qwant"], - "positions": [9], - "score": 0.1111111111111111, - "category": "general" - }, - { - "title": "About Python\u2122 | Python.org", - "content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.", - "url": "https://www.python.org/about/", - "engine": "duckduckgo", - "parsed_url": ["https", "www.python.org", "/about/", "", "", ""], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [11], - "score": 0.09090909090909091, - "category": "general" - }, - { - "title": "Online Python Compiler (Interpreter) - Programiz", - "content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.", - "url": "https://www.programiz.com/python-programming/online-compiler/", - "engine": "duckduckgo", - "parsed_url": [ - "https", - "www.programiz.com", - "/python-programming/online-compiler/", - "", - "", - "" - ], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [12], - "score": 0.08333333333333333, - "category": "general" - }, - { - "title": "Welcome to Python.org", - "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.", - "url": "https://www.python.org/?downloads", - "engine": "duckduckgo", - "parsed_url": ["https", "www.python.org", "/", "", "downloads", ""], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [15], - "score": 0.06666666666666667, - "category": "general" - }, - { - "url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective", - "title": "The Importance of Python and its Growing Influence on ...", - "content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.", - "img_src": null, - "engine": "google", - "parsed_url": [ - "https", - "www.matillion.com", - "/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective", - "", - "", - "" - ], - "template": "default.html", - "engines": ["google"], - "positions": [10], - "score": 0.1, - "category": "general" - }, - { - "title": "BeginnersGuide - Python Wiki", - "content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...", - "url": "https://wiki.python.org/moin/BeginnersGuide", - "engine": "duckduckgo", - "parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [16], - "score": 0.0625, - "category": "general" - }, - { - "title": "Learn Python - Free Interactive Python Tutorial", - "content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.", - "url": "https://www.learnpython.org/", - "engine": "duckduckgo", - "parsed_url": ["https", "www.learnpython.org", "/", "", "", ""], - "template": "default.html", - "engines": ["duckduckgo"], - "positions": [17], - "score": 0.058823529411764705, - "category": "general" - } - ], - "answers": [], - "corrections": [], - "infoboxes": [ - { - "infobox": "Python", - "id": "https://en.wikipedia.org/wiki/Python_(programming_language)", - "content": "general-purpose programming language", - "img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png", - "urls": [ - { - "title": "Official website", - "url": "https://www.python.org/", - "official": true - }, - { - "title": "Wikipedia (en)", - "url": "https://en.wikipedia.org/wiki/Python_(programming_language)" - }, - { - "title": "Wikidata", - "url": "http://www.wikidata.org/entity/Q28865" - } - ], - "attributes": [ - { - "label": "Inception", - "value": "Wednesday, February 20, 1991", - "entity": "P571" - }, - { - "label": "Developer", - "value": "Python Software Foundation, Guido van Rossum", - "entity": "P178" - }, - { - "label": "Copyright license", - "value": "Python Software Foundation License", - "entity": "P275" - }, - { - "label": "Programmed in", - "value": "C, Python", - "entity": "P277" - }, - { - "label": "Software version identifier", - "value": "3.12.3, 3.13.0a6", - "entity": "P348" - } - ], - "engine": "wikidata", - "engines": ["wikidata"] - } - ], - "suggestions": [ - "python turtle", - "micro python tutorial", - "python docs", - "python compiler", - "snapcraft python", - "micropython vs python", - "python online", - "python download" - ], - "unresponsive_engines": [] -} diff --git a/backend/open_webui/retrieval/web/testdata/serper.json b/backend/open_webui/retrieval/web/testdata/serper.json deleted file mode 100644 index b269eaf5b3..0000000000 --- a/backend/open_webui/retrieval/web/testdata/serper.json +++ /dev/null @@ -1,190 +0,0 @@ -{ - "searchParameters": { - "q": "apple inc", - "gl": "us", - "hl": "en", - "autocorrect": true, - "page": 1, - "type": "search" - }, - "knowledgeGraph": { - "title": "Apple", - "type": "Technology company", - "website": "http://www.apple.com/", - "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0", - "description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.", - "descriptionSource": "Wikipedia", - "descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.", - "attributes": { - "Headquarters": "Cupertino, CA", - "CEO": "Tim Cook (Aug 24, 2011–)", - "Founded": "April 1, 1976, Los Altos, CA", - "Sales": "1 (800) 692-7753", - "Products": "iPhone, Apple Watch, iPad, and more", - "Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne", - "Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more" - } - }, - "organic": [ - { - "title": "Apple", - "link": "https://www.apple.com/", - "snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", - "sitelinks": [ - { - "title": "Support", - "link": "https://support.apple.com/" - }, - { - "title": "iPhone", - "link": "https://www.apple.com/iphone/" - }, - { - "title": "Apple makes business better.", - "link": "https://www.apple.com/business/" - }, - { - "title": "Mac", - "link": "https://www.apple.com/mac/" - } - ], - "position": 1 - }, - { - "title": "Apple Inc. - Wikipedia", - "link": "https://en.wikipedia.org/wiki/Apple_Inc.", - "snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...", - "attributes": { - "Products": "AirPods; Apple Watch; iPad; iPhone; Mac", - "Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne", - "Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S", - "Industry": "Consumer electronics; Software services; Online services" - }, - "sitelinks": [ - { - "title": "History", - "link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." - }, - { - "title": "Timeline of Apple Inc. products", - "link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products" - }, - { - "title": "List of software by Apple Inc.", - "link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc." - }, - { - "title": "Apple Store", - "link": "https://en.wikipedia.org/wiki/Apple_Store" - } - ], - "position": 2 - }, - { - "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica", - "link": "https://www.britannica.com/topic/Apple-Inc", - "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...", - "date": "Aug 31, 2022", - "attributes": { - "Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts", - "Date": "1976 - present", - "Areas Of Involvement": "peripheral device" - }, - "position": 3 - }, - { - "title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com", - "link": "https://www.bloomberg.com/quote/AAPL:US", - "snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.", - "position": 4 - }, - { - "title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance", - "link": "https://finance.yahoo.com/quote/AAPL/profile/", - "snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...", - "position": 5 - }, - { - "title": "AAPL | Apple Inc. Stock Price & News - WSJ", - "link": "https://www.wsj.com/market-data/quotes/AAPL", - "snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...", - "position": 6 - }, - { - "title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData", - "link": "https://www.globaldata.com/company-profile/apple-inc/", - "snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...", - "position": 7 - }, - { - "title": "Apple Inc (AAPL) Stock Price & News - Google Finance", - "link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en", - "snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...", - "position": 8 - } - ], - "peopleAlsoAsk": [ - { - "question": "What does Apple Inc mean?", - "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022", - "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica", - "link": "https://www.britannica.com/topic/Apple-Inc" - }, - { - "question": "Is Apple and Apple Inc same?", - "snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.", - "title": "Apple Inc. - Wikipedia", - "link": "https://en.wikipedia.org/wiki/Apple_Inc." - }, - { - "question": "Who owns Apple Inc?", - "snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.", - "title": "Who Owns Apple In 2022? - FourWeekMBA", - "link": "https://fourweekmba.com/who-owns-apple/" - }, - { - "question": "What products does Apple Inc offer?", - "snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.", - "title": "More items...", - "link": "https://www.apple.com/business/" - } - ], - "relatedSearches": [ - { - "query": "Who invented the iPhone" - }, - { - "query": "Apple Inc competitors" - }, - { - "query": "Apple iPad" - }, - { - "query": "iPhones" - }, - { - "query": "Apple Inc us" - }, - { - "query": "Apple company history" - }, - { - "query": "Apple Store" - }, - { - "query": "Apple customer service" - }, - { - "query": "Apple Watch" - }, - { - "query": "Apple Inc Industry" - }, - { - "query": "Apple Inc registered address" - }, - { - "query": "Apple Inc Bloomberg" - } - ] -} diff --git a/backend/open_webui/retrieval/web/testdata/serply.json b/backend/open_webui/retrieval/web/testdata/serply.json deleted file mode 100644 index 0fc2a31e4d..0000000000 --- a/backend/open_webui/retrieval/web/testdata/serply.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "ads": [], - "ads_count": 0, - "answers": [], - "results": [ - { - "title": "Apple", - "link": "https://www.apple.com/", - "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", - "additional_links": [ - { - "text": "AppleApplehttps://www.apple.com", - "href": "https://www.apple.com/" - } - ], - "cite": {}, - "subdomains": [ - { - "title": "Support", - "link": "https://support.apple.com/", - "description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair" - }, - { - "title": "Store", - "link": "https://www.apple.com/store", - "description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..." - }, - { - "title": "Mac", - "link": "https://www.apple.com/mac/", - "description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini" - }, - { - "title": "iPad", - "link": "https://www.apple.com/ipad/", - "description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..." - }, - { - "title": "Watch", - "link": "https://www.apple.com/watch/", - "description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..." - } - ], - "realPosition": 1 - }, - { - "title": "Apple", - "link": "https://www.apple.com/", - "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", - "additional_links": [ - { - "text": "AppleApplehttps://www.apple.com", - "href": "https://www.apple.com/" - } - ], - "cite": {}, - "realPosition": 2 - }, - { - "title": "Apple Inc.", - "link": "https://en.wikipedia.org/wiki/Apple_Inc.", - "description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...", - "additional_links": [ - { - "text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc", - "href": "https://en.wikipedia.org/wiki/Apple_Inc." - }, - { - "text": "", - "href": "https://en.wikipedia.org/wiki/Apple_Inc." - }, - { - "text": "History", - "href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." - }, - { - "text": "List of Apple products", - "href": "https://en.wikipedia.org/wiki/List_of_Apple_products" - }, - { - "text": "Litigation involving Apple Inc.", - "href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc." - }, - { - "text": "Apple Park", - "href": "https://en.wikipedia.org/wiki/Apple_Park" - } - ], - "cite": { - "domain": "https://en.wikipedia.org › wiki › Apple_Inc", - "span": " › wiki › Apple_Inc" - }, - "realPosition": 3 - }, - { - "title": "Apple Inc. (AAPL) Company Profile & Facts", - "link": "https://finance.yahoo.com/quote/AAPL/profile/", - "description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...", - "additional_links": [ - { - "text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile", - "href": "https://finance.yahoo.com/quote/AAPL/profile/" - } - ], - "cite": { - "domain": "https://finance.yahoo.com › quote › AAPL › profile", - "span": " › quote › AAPL › profile" - }, - "realPosition": 4 - }, - { - "title": "Apple Inc - Company Profile and News", - "link": "https://www.bloomberg.com/profile/company/AAPL:US", - "description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...", - "additional_links": [ - { - "text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US", - "href": "https://www.bloomberg.com/profile/company/AAPL:US" - }, - { - "text": "", - "href": "https://www.bloomberg.com/profile/company/AAPL:US" - } - ], - "cite": { - "domain": "https://www.bloomberg.com › company › AAPL:US", - "span": " › company › AAPL:US" - }, - "realPosition": 5 - }, - { - "title": "Apple Inc. | History, Products, Headquarters, & Facts", - "link": "https://www.britannica.com/money/Apple-Inc", - "description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...", - "additional_links": [ - { - "text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc", - "href": "https://www.britannica.com/money/Apple-Inc" - }, - { - "text": "", - "href": "https://www.britannica.com/money/Apple-Inc" - } - ], - "cite": { - "domain": "https://www.britannica.com › money › Apple-Inc", - "span": " › money › Apple-Inc" - }, - "realPosition": 6 - } - ], - "shopping_ads": [], - "places": [ - { - "title": "Apple Inc." - }, - { - "title": "Apple Inc" - }, - { - "title": "Apple Inc" - } - ], - "related_searches": { - "images": [], - "text": [ - { - "title": "apple inc full form", - "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE" - }, - { - "title": "apple company history", - "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE" - }, - { - "title": "apple store", - "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE" - }, - { - "title": "apple id", - "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE" - }, - { - "title": "apple inc industry", - "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE" - }, - { - "title": "apple login", - "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE" - } - ] - }, - "image_results": [], - "carousel": [], - "total": 2450000000, - "knowledge_graph": "", - "related_questions": [ - "What does the Apple Inc do?", - "Why did Apple change to Apple Inc?", - "Who owns Apple Inc.?", - "What is Apple Inc best known for?" - ], - "carousel_count": 0, - "ts": 2.491065263748169, - "device_type": null -} diff --git a/backend/open_webui/retrieval/web/testdata/serpstack.json b/backend/open_webui/retrieval/web/testdata/serpstack.json deleted file mode 100644 index a82f689d8b..0000000000 --- a/backend/open_webui/retrieval/web/testdata/serpstack.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "request": { - "success": true, - "total_time_taken": 3.4, - "processed_timestamp": 1714968442, - "search_url": "http://www.google.com/search?q=mcdonalds\u0026gl=us\u0026hl=en\u0026safe=0\u0026num=10" - }, - "search_parameters": { - "engine": "google", - "type": "web", - "device": "desktop", - "auto_location": "1", - "google_domain": "google.com", - "gl": "us", - "hl": "en", - "safe": "0", - "news_type": "all", - "exclude_autocorrected_results": "0", - "images_color": "any", - "page": "1", - "num": "10", - "output": "json", - "csv_fields": "search_parameters.query,organic_results.position,organic_results.title,organic_results.url,organic_results.domain", - "query": "mcdonalds", - "action": "search", - "access_key": "aac48e007e15c532bb94ffb34532a4b2", - "error": {} - }, - "search_information": { - "total_results": 1170000000, - "time_taken_displayed": 0.49, - "detected_location": {}, - "did_you_mean": {}, - "no_results_for_original_query": false, - "showing_results_for": {} - }, - "organic_results": [ - { - "position": 1, - "title": "Our Full McDonald\u0027s Food Menu", - "snippet": "", - "prerender": false, - "cached_page_url": {}, - "related_pages_url": {}, - "url": "https://www.mcdonalds.com/us/en-us/full-menu.html", - "domain": "www.mcdonalds.com", - "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a full-menu" - }, - { - "position": 2, - "title": "McDonald\u0027s", - "snippet": "McDonald\u0027s is the world\u0027s largest fast food restaurant chain, serving over 69 million customers daily in over 100 countries in more than 40,000 outlets as of\u00a0...", - "prerender": false, - "cached_page_url": {}, - "related_pages_url": {}, - "url": "https://en.wikipedia.org/wiki/McDonald%27s", - "domain": "en.wikipedia.org", - "displayed_url": "https://en.wikipedia.org \u203a wiki \u203a McDonald\u0027s" - }, - { - "position": 3, - "title": "Restaurants Near Me: Nearby McDonald\u0027s Locations", - "snippet": "", - "prerender": false, - "cached_page_url": {}, - "related_pages_url": {}, - "url": "https://www.mcdonalds.com/us/en-us/restaurant-locator.html", - "domain": "www.mcdonalds.com", - "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a restaurant-locator" - }, - { - "position": 4, - "title": "Download the McDonald\u0027s App: Deals, Promotions \u0026 ...", - "snippet": "Download the McDonald\u0027s app for Mobile Order \u0026 Pay, exclusive deals and coupons, menu information and special promotions.", - "prerender": false, - "cached_page_url": {}, - "related_pages_url": {}, - "url": "https://www.mcdonalds.com/us/en-us/download-app.html", - "domain": "www.mcdonalds.com", - "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a download-app" - }, - { - "position": 5, - "title": "McDonald\u0027s Restaurant Careers in the US", - "snippet": "McDonald\u0027s restaurant jobs are one-of-a-kind \u2013 just like you. Restaurants are hiring across all levels, from Crew team to Management. Apply today!", - "prerender": false, - "cached_page_url": {}, - "related_pages_url": {}, - "url": "https://jobs.mchire.com/", - "domain": "jobs.mchire.com", - "displayed_url": "https://jobs.mchire.com" - } - ], - "inline_images": [ - { - "image_url": "https://serpstack-assets.apilayer.net/2418910010831954152.png", - "title": "" - } - ], - "local_results": [ - { - "position": 1, - "title": "McDonald\u0027s", - "coordinates": { - "latitude": 0, - "longitude": 0 - }, - "address": "", - "rating": 0, - "reviews": 0, - "type": "", - "price": {}, - "url": 0 - }, - { - "position": 2, - "title": "McDonald\u0027s", - "coordinates": { - "latitude": 0, - "longitude": 0 - }, - "address": "", - "rating": 0, - "reviews": 0, - "type": "", - "price": {}, - "url": 0 - }, - { - "position": 3, - "title": "McDonald\u0027s", - "coordinates": { - "latitude": 0, - "longitude": 0 - }, - "address": "", - "rating": 0, - "reviews": 0, - "type": "", - "price": {}, - "url": 0 - } - ], - "top_stories": [ - { - "block_position": 1, - "title": "Menu nutrition", - "url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=mcdonald%27s+double+quarter+pounder+with+cheese\u0026stick=H4sIAAAAAAAAAONgFuLUz9U3ME-vLDBX4tVP1zc0TCsuNE0ytjTTUs5OttJPy89P0c9NzSuNLyjKL8tMSS2yAvNS80qKMlOLF7Hq5ian5Ocl5qSoFyuk5Jcm5aQqFJYmFpWkFikU5JfmATUolGeWZCgkZ6SmFqcCAM4ilJtxAAAA\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qri56BAh0EAM", - "source": "", - "uploaded": "", - "uploaded_utc": "2024-05-06T04:07:22.082Z" - }, - { - "block_position": 2, - "title": "Profiles", - "url": "https://www.instagram.com/McDonalds", - "source": "", - "uploaded": "", - "uploaded_utc": "2024-05-06T04:07:22.082Z" - }, - { - "block_position": 3, - "title": "People also search for", - "url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-L5Wg8IL2sxPFxxcDEhVbocy-LJPZIvZySijw0ho2hfZ-KtV-sSEEJ9lw7JuEkXHDnRK5y4Dm8aqbiLwugbLbslwjG3hO_gpDTFZK2VoUGZPy2nrmOBCy0G3PoOfoiEtct2GSZlUz0uufG-xP8emtNzQKQpvjkAm5Zmi57iVZueiD62upz7-x2N3dAbwtm6FkInAPRw1yR91zuT7F3lEaPblTW3LaRwCDC0bvaRCh9x4N9zHgY1OOQa_rzts2jf5WpXcuw4Y%3D\u0026q=Burger+King\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qs9oBKAB6BAhzEAI", - "source": "", - "uploaded": "", - "uploaded_utc": "2024-05-06T04:07:22.082Z" - } - ], - "related_questions": [ - { - "question": "What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?", - "answer": "", - "title": "", - "displayed_url": "" - }, - { - "question": "Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?", - "answer": "", - "title": "", - "displayed_url": "" - }, - { - "question": "What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?", - "answer": "", - "title": "", - "displayed_url": "" - }, - { - "question": "Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?", - "answer": "", - "title": "", - "displayed_url": "" - } - ], - "knowledge_graph": { - "title": "", - "type": "Fast-food restaurant company", - "image_urls": ["https://serpstack-assets.apilayer.net/2418910010831954152.png"], - "description": "McDonald\u0027s Corporation is an American multinational fast food chain, founded in 1940 as a restaurant operated by Richard and Maurice McDonald, in San Bernardino, California, United States.", - "source": { - "name": "Wikipedia", - "url": "https://en.wikipedia.org/wiki/McDonald\u0027s" - }, - "people_also_search_for": [], - "known_attributes": [ - { - "attribute": "kc:/business/business_operation:founder", - "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg", - "name": "Founder: ", - "value": "Ray Kroc" - }, - { - "attribute": "kc:/organization/organization:ceo", - "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHUQAg", - "name": "CEO: ", - "value": "Chris Kempczinski (Nov 1, 2019\u2013)" - }, - { - "attribute": "kc:/business/employer:revenue", - "link": "", - "name": "Revenue: ", - "value": "25.49\u00a0billion USD (2023)" - }, - { - "attribute": "kc:/organization/organization:founded", - "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Des+Plaines\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm_yqLtI_DBi5PXGOtg_Z3qrzzEP6mcih1nN7h5A7v6OefnEJiC7a8dBR-v9LxlRubfyR6vlMr3fZ3TmVKWwz9FRpvZb1eYNt-RM7KIDKQlwGEIgINvzhxjUrv6uxSmceduzxd8W7Pkz71XGwxF0F8OlSzHlx\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECG4QAg", - "name": "Founded: ", - "value": "April 15, 1955, Des Plaines, IL" - }, - { - "attribute": "kc:/organization/organization:headquarters", - "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chicago\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-46AEJ_kJbUIEvsvEEZqteiYJvXVXs2ScRNDvFFpjfeAaW3dxtpTGCgcsf5RMdi6IdzOdtjJMN3ZaFwqZOmdi7tC6r0Mh1O9bnP3HrVDB9hH02m7aA6f70dCAfTdpOFnGxDU6wVMAI5MxWBE3wTugtUDOK-\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHYQAg", - "name": "Headquarters: ", - "value": "Chicago, IL" - }, - { - "attribute": "kc:/organization/organization:president", - "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHEQAg", - "name": "President: ", - "value": "Chris Kempczinski" - } - ], - "website": "https://www.mcdonalds.com/us/en-us.html", - "profiles": [ - { - "name": "Instagram", - "url": "https://www.instagram.com/McDonalds" - }, - { - "name": "X (Twitter)", - "url": "https://twitter.com/McDonalds" - }, - { - "name": "Facebook", - "url": "https://www.facebook.com/McDonaldsUS" - }, - { - "name": "YouTube", - "url": "https://www.youtube.com/user/McDonaldsUS" - }, - { - "name": "Pinterest", - "url": "https://www.pinterest.com/mcdonalds" - } - ], - "founded": "April 15, 1955, Des Plaines, IL", - "headquarters": "Chicago, IL", - "founders": [ - { - "name": "Ray Kroc", - "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg" - } - ] - } -} diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py index 6c1ea4b1bf..afa73a9e0e 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -1,4 +1,5 @@ import asyncio +import ipaddress import logging import socket import ssl @@ -11,38 +12,44 @@ from typing import ( Dict, Iterator, List, + Literal, Optional, Sequence, Union, - Literal, ) -from fastapi.concurrency import run_in_threadpool import aiohttp +import aiohttp.resolver import certifi +import requests +import urllib3.connection +import urllib3.connectionpool import validators +from requests.adapters import HTTPAdapter +from fastapi.concurrency import run_in_threadpool from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader from langchain_community.document_loaders.base import BaseLoader from langchain_core.documents import Document - -from open_webui.retrieval.loaders.tavily import TavilyLoader -from open_webui.retrieval.loaders.external_web import ExternalWebLoader -from open_webui.constants import ERROR_MESSAGES from open_webui.config import ( ENABLE_RAG_LOCAL_WEB_FETCH, - PLAYWRIGHT_WS_URL, - PLAYWRIGHT_TIMEOUT, - WEB_LOADER_ENGINE, - WEB_LOADER_TIMEOUT, + EXTERNAL_WEB_LOADER_API_KEY, + EXTERNAL_WEB_LOADER_URL, FIRECRAWL_API_BASE_URL, FIRECRAWL_API_KEY, FIRECRAWL_TIMEOUT, + PLAYWRIGHT_TIMEOUT, + PLAYWRIGHT_WS_URL, TAVILY_API_KEY, TAVILY_EXTRACT_DEPTH, - EXTERNAL_WEB_LOADER_URL, - EXTERNAL_WEB_LOADER_API_KEY, WEB_FETCH_FILTER_LIST, + WEB_LOADER_ENGINE, + WEB_LOADER_TIMEOUT, ) +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS, AIOHTTP_CLIENT_SESSION_SSL, USER_AGENT +from open_webui.retrieval.loaders.external_web import ExternalWebLoader +from open_webui.retrieval.loaders.tavily import TavilyLoader +from open_webui.retrieval.web.firecrawl import scrape_firecrawl_url from open_webui.utils.misc import is_string_allowed log = logging.getLogger(__name__) @@ -64,19 +71,25 @@ def validate_url(url: Union[str, Sequence[str]]): if isinstance(validators.url(url), validators.ValidationError): raise ValueError(ERROR_MESSAGES.INVALID_URL) + # Reject parser-confusing chars: urlparse and requests/aiohttp split + # on these differently, e.g. http://127.0.0.1\@1.1.1.1 → urlparse + # extracts 1.1.1.1 (public, passes filter) while requests connects + # to 127.0.0.1 (internal). Same shape with tab/CR/LF. + if any(ch in url for ch in ('\\', '\t', '\n', '\r')): + log.warning(f'Blocked URL with parser-confusing char: {url!r}') + raise ValueError(ERROR_MESSAGES.INVALID_URL) + parsed_url = urllib.parse.urlparse(url) # Protocol validation - only allow http/https - if parsed_url.scheme not in ["http", "https"]: - log.warning( - f"Blocked non-HTTP(S) protocol: {parsed_url.scheme} in URL: {url}" - ) + if parsed_url.scheme not in ['http', 'https']: + log.warning(f'Blocked non-HTTP(S) protocol: {parsed_url.scheme} in URL: {url}') raise ValueError(ERROR_MESSAGES.INVALID_URL) # Blocklist check using unified filtering logic if WEB_FETCH_FILTER_LIST: if not is_string_allowed(url, WEB_FETCH_FILTER_LIST): - log.warning(f"URL blocked by filter list: {url}") + log.warning(f'URL blocked by filter list: {url}') raise ValueError(ERROR_MESSAGES.INVALID_URL) if not ENABLE_RAG_LOCAL_WEB_FETCH: @@ -85,12 +98,10 @@ def validate_url(url: Union[str, Sequence[str]]): # Get IPv4 and IPv6 addresses ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname) # Check if any of the resolved addresses are private - # This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader - for ip in ipv4_addresses: - if validators.ipv4(ip, private=True): - raise ValueError(ERROR_MESSAGES.INVALID_URL) - for ip in ipv6_addresses: - if validators.ipv6(ip, private=True): + # DNS rebinding is mitigated at the connection layer; see _SSRFSafeResolver / _SSRFSafeAdapter + for ip in ipv4_addresses + ipv6_addresses: + addr = ipaddress.ip_address(ip) + if not addr.is_global: raise ValueError(ERROR_MESSAGES.INVALID_URL) return True elif isinstance(url, Sequence): @@ -106,29 +117,104 @@ def safe_validate_urls(url: Sequence[str]) -> Sequence[str]: if validate_url(u): valid_urls.append(u) except Exception as e: - log.debug(f"Invalid URL {u}: {str(e)}") + log.debug(f'Invalid URL {u}: {str(e)}') continue return valid_urls +def _ssrf_safe_new_conn(self): + """Resolve DNS, validate all IPs are global, connect to validated IP. + + Replaces urllib3's _new_conn so the DNS lookup that feeds the actual TCP + connect is the same one we validate — no second resolution, no rebinding + window. + """ + host = getattr(self, '_dns_host', self.host) + port = self.port + infos = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + if not infos: + raise OSError(f'getaddrinfo for {host!r} returned empty list') + if not ENABLE_RAG_LOCAL_WEB_FETCH: + for _, _, _, _, sa in infos: + if not ipaddress.ip_address(sa[0]).is_global: + raise ValueError(ERROR_MESSAGES.INVALID_URL) + err = None + for fam, typ, proto, _, sa in infos: + sock = None + try: + sock = socket.socket(fam, typ, proto) + if self.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(self.timeout) + if getattr(self, 'source_address', None): + sock.bind(self.source_address) + for opt in getattr(self, 'socket_options', None) or (): + sock.setsockopt(*opt) + sock.connect(sa) + return sock + except OSError as exc: + err = exc + if sock is not None: + sock.close() + raise err or OSError(f'connect to {host!r}:{port} failed') + + +class _SafeHTTPConn(urllib3.connection.HTTPConnection): + _new_conn = _ssrf_safe_new_conn + + +class _SafeHTTPSConn(urllib3.connection.HTTPSConnection): + _new_conn = _ssrf_safe_new_conn + + +class _SafeHTTPPool(urllib3.connectionpool.HTTPConnectionPool): + ConnectionCls = _SafeHTTPConn + + +class _SafeHTTPSPool(urllib3.connectionpool.HTTPSConnectionPool): + ConnectionCls = _SafeHTTPSConn + + +class _SSRFSafeAdapter(HTTPAdapter): + """requests transport adapter that validates resolved IPs at connect time.""" + + def init_poolmanager(self, *args, **kwargs): + super().init_poolmanager(*args, **kwargs) + self.poolmanager.pool_classes_by_scheme = { + 'http': _SafeHTTPPool, + 'https': _SafeHTTPSPool, + } + + +class _SSRFSafeResolver(aiohttp.resolver.DefaultResolver): + """aiohttp resolver that rejects non-global IPs unless local fetch is on.""" + + async def resolve(self, host, port=0, family=socket.AF_INET): + results = await super().resolve(host, port, family) + if not ENABLE_RAG_LOCAL_WEB_FETCH: + for entry in results: + if not ipaddress.ip_address(entry['host']).is_global: + raise ValueError(ERROR_MESSAGES.INVALID_URL) + return results + + def extract_metadata(soup, url): - metadata = {"source": url} - if title := soup.find("title"): - metadata["title"] = title.get_text() - if description := soup.find("meta", attrs={"name": "description"}): - metadata["description"] = description.get("content", "No description found.") - if html := soup.find("html"): - metadata["language"] = html.get("lang", "No language found.") + metadata = {'source': url} + if title := soup.find('title'): + metadata['title'] = title.get_text() + if description := soup.find('meta', attrs={'name': 'description'}): + metadata['description'] = description.get('content', 'No description found.') + if html := soup.find('html'): + metadata['language'] = html.get('lang', 'No language found.') return metadata def verify_ssl_cert(url: str) -> bool: """Verify SSL certificate for the given URL.""" - if not url.startswith("https://"): + if not url.startswith('https://'): return True try: - hostname = url.split("://")[-1].split("/")[0] + hostname = url.split('://')[-1].split('/')[0] context = ssl.create_default_context(cafile=certifi.where()) with context.wrap_socket(ssl.socket(), server_hostname=hostname) as s: s.connect((hostname, 443)) @@ -136,7 +222,7 @@ def verify_ssl_cert(url: str) -> bool: except ssl.SSLError: return False except Exception as e: - log.warning(f"SSL verification failed for {url}: {str(e)}") + log.warning(f'SSL verification failed for {url}: {str(e)}') return False @@ -168,14 +254,14 @@ class URLProcessingMixin: async def _safe_process_url(self, url: str) -> bool: """Perform safety checks before processing a URL.""" if self.verify_ssl and not await self._verify_ssl_cert(url): - raise ValueError(f"SSL certificate verification failed for {url}") + raise ValueError(f'SSL certificate verification failed for {url}') await self._wait_for_rate_limit() return True def _safe_process_url_sync(self, url: str) -> bool: """Synchronous version of safety checks.""" - if self.verify_ssl and not self._verify_ssl_cert(url): - raise ValueError(f"SSL certificate verification failed for {url}") + if self.verify_ssl and not verify_ssl_cert(url): + raise ValueError(f'SSL certificate verification failed for {url}') self._sync_wait_for_rate_limit() return True @@ -191,40 +277,19 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): api_key: Optional[str] = None, api_url: Optional[str] = None, timeout: Optional[int] = None, - mode: Literal["crawl", "scrape", "map"] = "scrape", + mode: Literal['crawl', 'scrape', 'map'] = 'scrape', proxy: Optional[Dict[str, str]] = None, params: Optional[Dict] = None, ): - """Concurrent document loader for FireCrawl operations. - - Executes multiple FireCrawlLoader instances concurrently using thread pooling - to improve bulk processing efficiency. - Args: - web_paths: List of URLs/paths to process. - verify_ssl: If True, verify SSL certificates. - trust_env: If True, use proxy settings from environment variables. - requests_per_second: Number of requests per second to limit to. - continue_on_failure (bool): If True, continue loading other URLs on failure. - api_key: API key for FireCrawl service. Defaults to None - (uses FIRE_CRAWL_API_KEY environment variable if not provided). - api_url: Base URL for FireCrawl API. Defaults to official API endpoint. - mode: Operation mode selection: - - 'crawl': Website crawling mode - - 'scrape': Direct page scraping (default) - - 'map': Site map generation - proxy: Proxy override settings for the FireCrawl API. - params: The parameters to pass to the Firecrawl API. - For more details, visit: https://docs.firecrawl.dev/sdks/python#batch-scrape - """ - proxy_server = proxy.get("server") if proxy else None + proxy_server = proxy.get('server') if proxy else None if trust_env and not proxy_server: env_proxies = urllib.request.getproxies() - env_proxy_server = env_proxies.get("https") or env_proxies.get("http") + env_proxy_server = env_proxies.get('https') or env_proxies.get('http') if env_proxy_server: if proxy: - proxy["server"] = env_proxy_server + proxy['server'] = env_proxy_server else: - proxy = {"server": env_proxy_server} + proxy = {'server': env_proxy_server} self.web_paths = web_paths self.verify_ssl = verify_ssl self.requests_per_second = requests_per_second @@ -232,90 +297,38 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): self.trust_env = trust_env self.continue_on_failure = continue_on_failure self.api_key = api_key - self.api_url = api_url + self.api_url = (api_url or 'https://api.firecrawl.dev').rstrip('/') self.timeout = timeout self.mode = mode self.params = params or {} def lazy_load(self) -> Iterator[Document]: - """Load documents using FireCrawl batch_scrape.""" - log.debug( - "Starting FireCrawl batch scrape for %d URLs, mode: %s, params: %s", - len(self.web_paths), - self.mode, - self.params, - ) try: - from firecrawl import FirecrawlApp - - firecrawl = FirecrawlApp(api_key=self.api_key, api_url=self.api_url) - result = firecrawl.batch_scrape( - self.web_paths, - formats=["markdown"], - skip_tls_verification=not self.verify_ssl, - ignore_invalid_urls=True, - remove_base64_images=True, - max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values - wait_timeout=self.timeout if self.timeout else len(self.web_paths) * 3, - **self.params, - ) - - if result.status != "completed": - raise RuntimeError( - f"FireCrawl batch scrape did not complete successfully. result: {result}" + for url in self.web_paths: + doc = scrape_firecrawl_url( + self.api_url, + self.api_key, + url, + verify_ssl=self.verify_ssl, + timeout=self.timeout, + params=self.params, ) - - for data in result.data: - metadata = data.metadata or {} - yield Document( - page_content=data.markdown or "", - metadata={"source": metadata.url or metadata.source_url or ""}, - ) - + if doc is not None: + yield doc except Exception as e: if self.continue_on_failure: - log.exception(f"Error extracting content from URLs: {e}") + log.warning(f'Error extracting content from URLs with Firecrawl: {e}') else: raise e async def alazy_load(self): - """Async version of lazy_load.""" - log.debug( - "Starting FireCrawl batch scrape for %d URLs, mode: %s, params: %s", - len(self.web_paths), - self.mode, - self.params, - ) try: - from firecrawl import FirecrawlApp - - firecrawl = FirecrawlApp(api_key=self.api_key, api_url=self.api_url) - result = firecrawl.batch_scrape( - self.web_paths, - formats=["markdown"], - skip_tls_verification=not self.verify_ssl, - ignore_invalid_urls=True, - remove_base64_images=True, - max_age=300000, # 5 minutes https://docs.firecrawl.dev/features/fast-scraping#common-maxage-values - wait_timeout=self.timeout if self.timeout else len(self.web_paths) * 3, - **self.params, - ) - - if result.status != "completed": - raise RuntimeError( - f"FireCrawl batch scrape did not complete successfully. result: {result}" - ) - - for data in result.data: - metadata = data.metadata or {} - yield Document( - page_content=data.markdown or "", - metadata={"source": metadata.url or metadata.source_url or ""}, - ) - + docs = await run_in_threadpool(lambda: list(self.lazy_load())) + for doc in docs: + yield doc except Exception as e: if self.continue_on_failure: - log.exception(f"Error extracting content from URLs: {e}") + log.warning(f'Error extracting content from URLs with Firecrawl: {e}') else: raise e @@ -325,7 +338,7 @@ class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): self, web_paths: Union[str, List[str]], api_key: str, - extract_depth: Literal["basic", "advanced"] = "basic", + extract_depth: Literal['basic', 'advanced'] = 'basic', continue_on_failure: bool = True, requests_per_second: Optional[float] = None, verify_ssl: bool = True, @@ -345,15 +358,15 @@ class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): proxy: Optional proxy configuration. """ # Initialize proxy configuration if using environment variables - proxy_server = proxy.get("server") if proxy else None + proxy_server = proxy.get('server') if proxy else None if trust_env and not proxy_server: env_proxies = urllib.request.getproxies() - env_proxy_server = env_proxies.get("https") or env_proxies.get("http") + env_proxy_server = env_proxies.get('https') or env_proxies.get('http') if env_proxy_server: if proxy: - proxy["server"] = env_proxy_server + proxy['server'] = env_proxy_server else: - proxy = {"server": env_proxy_server} + proxy = {'server': env_proxy_server} # Store parameters for creating TavilyLoader instances self.web_paths = web_paths if isinstance(web_paths, list) else [web_paths] @@ -376,14 +389,14 @@ class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): self._safe_process_url_sync(url) valid_urls.append(url) except Exception as e: - log.warning(f"SSL verification failed for {url}: {str(e)}") + log.warning(f'SSL verification failed for {url}: {str(e)}') if not self.continue_on_failure: raise e if not valid_urls: if self.continue_on_failure: - log.warning("No valid URLs to process after SSL verification") + log.warning('No valid URLs to process after SSL verification') return - raise ValueError("No valid URLs to process after SSL verification") + raise ValueError('No valid URLs to process after SSL verification') try: loader = TavilyLoader( urls=valid_urls, @@ -394,7 +407,7 @@ class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): yield from loader.lazy_load() except Exception as e: if self.continue_on_failure: - log.exception(f"Error extracting content from URLs: {e}") + log.exception(f'Error extracting content from URLs: {e}') else: raise e @@ -406,15 +419,15 @@ class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): await self._safe_process_url(url) valid_urls.append(url) except Exception as e: - log.warning(f"SSL verification failed for {url}: {str(e)}") + log.warning(f'SSL verification failed for {url}: {str(e)}') if not self.continue_on_failure: raise e if not valid_urls: if self.continue_on_failure: - log.warning("No valid URLs to process after SSL verification") + log.warning('No valid URLs to process after SSL verification') return - raise ValueError("No valid URLs to process after SSL verification") + raise ValueError('No valid URLs to process after SSL verification') try: loader = TavilyLoader( @@ -427,7 +440,7 @@ class SafeTavilyLoader(BaseLoader, RateLimitMixin, URLProcessingMixin): yield document except Exception as e: if self.continue_on_failure: - log.exception(f"Error loading URLs: {e}") + log.exception(f'Error loading URLs: {e}') else: raise e @@ -462,15 +475,15 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing ): """Initialize with additional safety parameters and remote browser support.""" - proxy_server = proxy.get("server") if proxy else None + proxy_server = proxy.get('server') if proxy else None if trust_env and not proxy_server: env_proxies = urllib.request.getproxies() - env_proxy_server = env_proxies.get("https") or env_proxies.get("http") + env_proxy_server = env_proxies.get('https') or env_proxies.get('http') if env_proxy_server: if proxy: - proxy["server"] = env_proxy_server + proxy['server'] = env_proxy_server else: - proxy = {"server": env_proxy_server} + proxy = {'server': env_proxy_server} # We'll set headless to False if using playwright_ws_url since it's handled by the remote browser super().__init__( @@ -487,6 +500,62 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing self.trust_env = trust_env self.playwright_timeout = playwright_timeout + def _intercept_navigation_sync(self, route, request=None): + req = request or route.request + + if req.resource_type != 'document': + route.continue_() + return + + try: + validate_url(req.url) + except Exception: + route.abort() + return + + if AIOHTTP_CLIENT_ALLOW_REDIRECTS: + resp = route.fetch() + else: + try: + resp = route.fetch(max_redirects=0) + except TypeError: + route.abort() + return + + if 300 <= resp.status < 400: + route.abort() + return + + route.fulfill(response=resp) + + async def _intercept_navigation(self, route, request=None): + req = request or route.request + + if req.resource_type != 'document': + await route.continue_() + return + + try: + await run_in_threadpool(validate_url, req.url) + except Exception: + await route.abort() + return + + if AIOHTTP_CLIENT_ALLOW_REDIRECTS: + resp = await route.fetch() + else: + try: + resp = await route.fetch(max_redirects=0) + except TypeError: + await route.abort() + return + + if 300 <= resp.status < 400: + await route.abort() + return + + await route.fulfill(response=resp) + def lazy_load(self) -> Iterator[Document]: """Safely load URLs synchronously with support for remote browser.""" from playwright.sync_api import sync_playwright @@ -502,16 +571,17 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing try: self._safe_process_url_sync(url) page = browser.new_page() + page.route('**/*', self._intercept_navigation_sync) response = page.goto(url, timeout=self.playwright_timeout) if response is None: - raise ValueError(f"page.goto() returned None for url {url}") + raise ValueError(f'page.goto() returned None for url {url}') text = self.evaluator.evaluate(page, browser, response) - metadata = {"source": url} + metadata = {'source': url} yield Document(page_content=text, metadata=metadata) except Exception as e: if self.continue_on_failure: - log.exception(f"Error loading {url}: {e}") + log.exception(f'Error loading {url}: {e}') continue raise e browser.close() @@ -525,24 +595,23 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing if self.playwright_ws_url: browser = await p.chromium.connect(self.playwright_ws_url) else: - browser = await p.chromium.launch( - headless=self.headless, proxy=self.proxy - ) + browser = await p.chromium.launch(headless=self.headless, proxy=self.proxy) for url in self.urls: try: await self._safe_process_url(url) page = await browser.new_page() + await page.route('**/*', self._intercept_navigation) response = await page.goto(url, timeout=self.playwright_timeout) if response is None: - raise ValueError(f"page.goto() returned None for url {url}") + raise ValueError(f'page.goto() returned None for url {url}') text = await self.evaluator.evaluate_async(page, browser, response) - metadata = {"source": url} + metadata = {'source': url} yield Document(page_content=text, metadata=metadata) except Exception as e: if self.continue_on_failure: - log.exception(f"Error loading {url}: {e}") + log.exception(f'Error loading {url}: {e}') continue raise e await browser.close() @@ -560,10 +629,32 @@ class SafeWebBaseLoader(WebBaseLoader): super().__init__(*args, **kwargs) self.trust_env = trust_env - async def _fetch( - self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5 - ) -> str: - async with aiohttp.ClientSession(trust_env=self.trust_env) as session: + # Propagate USER_AGENT env var so that both the sync _scrape() and + # async _fetch() paths present a real UA instead of python-requests/2.x + # which gets blocked by Cloudflare, Wikipedia, and similar bot-detection. + # _fetch() forwards self.session.headers to the aiohttp session, so + # setting it here covers both code-paths. + if USER_AGENT: + self.session.headers['User-Agent'] = USER_AGENT + + # Prevent redirect-based SSRF on the synchronous _scrape() path. + # validate_url() is called once on the originally-submitted URL, but the + # parent WebBaseLoader's _scrape() invokes self.session.get(url, **self.requests_kwargs) + # which by default follows redirects. Without the override below, an attacker + # can submit a public URL that 302-redirects to an internal address (RFC1918, + # 127.0.0.1, 169.254.169.254, etc.) and the redirected target is fetched without + # re-validation. Matches the policy enforced on the async _fetch() path below. + self.requests_kwargs = { + **(self.requests_kwargs or {}), + 'allow_redirects': AIOHTTP_CLIENT_ALLOW_REDIRECTS, + } + + self.session.mount('http://', _SSRFSafeAdapter()) + self.session.mount('https://', _SSRFSafeAdapter()) + + async def _fetch(self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5) -> str: + connector = aiohttp.TCPConnector(resolver=_SSRFSafeResolver()) + async with aiohttp.ClientSession(trust_env=self.trust_env, connector=connector) as session: for i in range(retries): try: kwargs: Dict = dict( @@ -571,12 +662,13 @@ class SafeWebBaseLoader(WebBaseLoader): cookies=self.session.cookies.get_dict(), ) if not self.session.verify: - kwargs["ssl"] = False + kwargs['ssl'] = False + else: + kwargs['ssl'] = AIOHTTP_CLIENT_SESSION_SSL async with session.get( url, **(self.requests_kwargs | kwargs), - allow_redirects=False, ) as response: if self.raise_for_status: response.raise_for_status() @@ -585,16 +677,11 @@ class SafeWebBaseLoader(WebBaseLoader): if i == retries - 1: raise else: - log.warning( - f"Error fetching {url} with attempt " - f"{i + 1}/{retries}: {e}. Retrying..." - ) + log.warning(f'Error fetching {url} with attempt {i + 1}/{retries}: {e}. Retrying...') await asyncio.sleep(cooldown * backoff**i) - raise ValueError("retry count exceeded") + raise ValueError('retry count exceeded') - def _unpack_fetch_results( - self, results: Any, urls: List[str], parser: Union[str, None] = None - ) -> List[Any]: + def _unpack_fetch_results(self, results: Any, urls: List[str], parser: Union[str, None] = None) -> List[Any]: """Unpack fetch results into BeautifulSoup objects.""" from bs4 import BeautifulSoup @@ -602,17 +689,15 @@ class SafeWebBaseLoader(WebBaseLoader): for i, result in enumerate(results): url = urls[i] if parser is None: - if url.endswith(".xml"): - parser = "xml" + if url.endswith('.xml'): + parser = 'xml' else: parser = self.default_parser self._check_parser(parser) final_results.append(BeautifulSoup(result, parser, **self.bs_kwargs)) return final_results - async def ascrape_all( - self, urls: List[str], parser: Union[str, None] = None - ) -> List[Any]: + async def ascrape_all(self, urls: List[str], parser: Union[str, None] = None) -> List[Any]: """Async fetch all urls, then return soups for all results.""" results = await self.fetch_all(urls) return self._unpack_fetch_results(results, urls, parser=parser) @@ -630,22 +715,20 @@ class SafeWebBaseLoader(WebBaseLoader): yield Document(page_content=text, metadata=metadata) except Exception as e: # Log the error and continue with the next URL - log.exception(f"Error loading {path}: {e}") + log.exception(f'Error loading {path}: {e}') async def alazy_load(self) -> AsyncIterator[Document]: """Async lazy load text from the url(s) in web_path.""" results = await self.ascrape_all(self.web_paths) for path, soup in zip(self.web_paths, results): text = soup.get_text(**self.bs_get_text_kwargs) - metadata = {"source": path} - if title := soup.find("title"): - metadata["title"] = title.get_text() - if description := soup.find("meta", attrs={"name": "description"}): - metadata["description"] = description.get( - "content", "No description found." - ) - if html := soup.find("html"): - metadata["language"] = html.get("lang", "No language found.") + metadata = {'source': path} + if title := soup.find('title'): + metadata['title'] = title.get_text() + if description := soup.find('meta', attrs={'name': 'description'}): + metadata['description'] = description.get('content', 'No description found.') + if html := soup.find('html'): + metadata['language'] = html.get('lang', 'No language found.') yield Document(page_content=text, metadata=metadata) async def aload(self) -> list[Document]: @@ -663,18 +746,18 @@ def get_web_loader( safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls) if not safe_urls: - log.warning(f"All provided URLs were blocked or invalid: {urls}") + log.warning(f'All provided URLs were blocked or invalid: {urls}') raise ValueError(ERROR_MESSAGES.INVALID_URL) web_loader_args = { - "web_paths": safe_urls, - "verify_ssl": verify_ssl, - "requests_per_second": requests_per_second, - "continue_on_failure": True, - "trust_env": trust_env, + 'web_paths': safe_urls, + 'verify_ssl': verify_ssl, + 'requests_per_second': requests_per_second, + 'continue_on_failure': True, + 'trust_env': trust_env, } - if WEB_LOADER_ENGINE.value == "" or WEB_LOADER_ENGINE.value == "safe_web": + if WEB_LOADER_ENGINE.value == '' or WEB_LOADER_ENGINE.value == 'safe_web': WebLoaderClass = SafeWebBaseLoader request_kwargs = {} @@ -685,42 +768,42 @@ def get_web_loader( timeout_value = None if timeout_value: - request_kwargs["timeout"] = timeout_value + request_kwargs['timeout'] = timeout_value if request_kwargs: - web_loader_args["requests_kwargs"] = request_kwargs + web_loader_args['requests_kwargs'] = request_kwargs - if WEB_LOADER_ENGINE.value == "playwright": + if WEB_LOADER_ENGINE.value == 'playwright': WebLoaderClass = SafePlaywrightURLLoader - web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value + web_loader_args['playwright_timeout'] = PLAYWRIGHT_TIMEOUT.value if PLAYWRIGHT_WS_URL.value: - web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value + web_loader_args['playwright_ws_url'] = PLAYWRIGHT_WS_URL.value - if WEB_LOADER_ENGINE.value == "firecrawl": + if WEB_LOADER_ENGINE.value == 'firecrawl': WebLoaderClass = SafeFireCrawlLoader - web_loader_args["api_key"] = FIRECRAWL_API_KEY.value - web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value + web_loader_args['api_key'] = FIRECRAWL_API_KEY.value + web_loader_args['api_url'] = FIRECRAWL_API_BASE_URL.value if FIRECRAWL_TIMEOUT.value: try: - web_loader_args["timeout"] = int(FIRECRAWL_TIMEOUT.value) + web_loader_args['timeout'] = int(FIRECRAWL_TIMEOUT.value) except ValueError: pass - if WEB_LOADER_ENGINE.value == "tavily": + if WEB_LOADER_ENGINE.value == 'tavily': WebLoaderClass = SafeTavilyLoader - web_loader_args["api_key"] = TAVILY_API_KEY.value - web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value + web_loader_args['api_key'] = TAVILY_API_KEY.value + web_loader_args['extract_depth'] = TAVILY_EXTRACT_DEPTH.value - if WEB_LOADER_ENGINE.value == "external": + if WEB_LOADER_ENGINE.value == 'external': WebLoaderClass = ExternalWebLoader - web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value - web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value + web_loader_args['external_url'] = EXTERNAL_WEB_LOADER_URL.value + web_loader_args['external_api_key'] = EXTERNAL_WEB_LOADER_API_KEY.value if WebLoaderClass: web_loader = WebLoaderClass(**web_loader_args) log.debug( - "Using WEB_LOADER_ENGINE %s for %s URLs", + 'Using WEB_LOADER_ENGINE %s for %s URLs', web_loader.__class__.__name__, len(safe_urls), ) @@ -728,6 +811,6 @@ def get_web_loader( return web_loader else: raise ValueError( - f"Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. " + f'Invalid WEB_LOADER_ENGINE: {WEB_LOADER_ENGINE.value}. ' "Please set it to 'safe_web', 'playwright', 'firecrawl', or 'tavily'." ) diff --git a/backend/open_webui/retrieval/web/yacy.py b/backend/open_webui/retrieval/web/yacy.py index 2419717b24..969acb7398 100644 --- a/backend/open_webui/retrieval/web/yacy.py +++ b/backend/open_webui/retrieval/web/yacy.py @@ -2,8 +2,8 @@ import logging from typing import Optional import requests -from requests.auth import HTTPDigestAuth from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from requests.auth import HTTPDigestAuth log = logging.getLogger(__name__) @@ -41,29 +41,29 @@ def search_yacy( yacy_auth = HTTPDigestAuth(username, password) params = { - "query": query, - "contentdom": "text", - "resource": "global", - "maximumRecords": count, - "nav": "none", + 'query': query, + 'contentdom': 'text', + 'resource': 'global', + 'maximumRecords': count, + 'nav': 'none', } # Check if provided a json API URL - if not query_url.endswith("yacysearch.json"): + if not query_url.endswith('yacysearch.json'): # Strip all query parameters from the URL - query_url = query_url.rstrip("/") + "/yacysearch.json" + query_url = query_url.rstrip('/') + '/yacysearch.json' - log.debug(f"searching {query_url}") + log.debug(f'searching {query_url}') response = requests.get( query_url, auth=yacy_auth, headers={ - "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot", - "Accept": "text/html", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "en-US,en;q=0.5", - "Connection": "keep-alive", + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Accept': 'text/html', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', }, params=params, ) @@ -71,15 +71,15 @@ def search_yacy( response.raise_for_status() # Raise an exception for HTTP errors. json_response = response.json() - results = json_response.get("channels", [{}])[0].get("items", []) - sorted_results = sorted(results, key=lambda x: x.get("ranking", 0), reverse=True) + results = json_response.get('channels', [{}])[0].get('items', []) + sorted_results = sorted(results, key=lambda x: x.get('ranking', 0), reverse=True) if filter_list: sorted_results = get_filtered_results(sorted_results, filter_list) return [ SearchResult( - link=result["link"], - title=result.get("title"), - snippet=result.get("description"), + link=result['link'], + title=result.get('title'), + snippet=result.get('description'), ) for result in sorted_results[:count] ] diff --git a/backend/open_webui/retrieval/web/yandex.py b/backend/open_webui/retrieval/web/yandex.py new file mode 100644 index 0000000000..d338db11a3 --- /dev/null +++ b/backend/open_webui/retrieval/web/yandex.py @@ -0,0 +1,149 @@ +import base64 +import io +import json +import logging +import os +from typing import List, Optional +from xml.etree import ElementTree as ET +from xml.etree.ElementTree import Element + +import requests +from fastapi import Request +from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.headers import include_user_info_headers + +log = logging.getLogger(__name__) + + +def xml_element_contents_to_string(element: Element) -> str: + if element is None: + return '' + buffer = [element.text if element.text else ''] + + for child in element: + buffer.append(xml_element_contents_to_string(child)) + + buffer.append(element.tail if element.tail else '') + + return ''.join(buffer) + + +def search_yandex( + request: Request, + yandex_search_url: str, + yandex_search_api_key: str, + yandex_search_config: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + user=None, +) -> List[SearchResult]: + try: + headers = { + 'User-Agent': 'Open WebUI (https://github.com/open-webui/open-webui) RAG Bot', + 'Authorization': f'Api-Key {yandex_search_api_key}', + } + + if user is not None: + headers = include_user_info_headers(headers, user) + + chat_id = getattr(request.state, 'chat_id', None) + if chat_id: + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = str(chat_id) + + payload = {} if yandex_search_config == '' else json.loads(yandex_search_config) + + if type(payload.get('query', None)) != dict: + payload['query'] = {} + + if 'searchType' not in payload['query']: + payload['query']['searchType'] = 'SEARCH_TYPE_RU' + + payload['query']['queryText'] = query + + if type(payload.get('groupSpec', None)) != dict: + payload['groupSpec'] = {} + + if 'groupMode' not in payload['groupSpec']: + payload['groupSpec']['groupMode'] = 'GROUP_MODE_DEEP' + + payload['groupSpec']['groupsOnPage'] = count + payload['groupSpec']['docsInGroup'] = 1 + + response = requests.post( + ('https://searchapi.api.cloud.yandex.net/v2/web/search' if yandex_search_url == '' else yandex_search_url), + headers=headers, + json=payload, + ) + + response.raise_for_status() + + response_body = response.json() + if 'rawData' not in response_body: + raise Exception(f'No `rawData` in response body: {response_body}') + + search_result_body_bytes = base64.decodebytes(bytes(response_body['rawData'], 'utf-8')) + + doc_root = ET.parse(io.BytesIO(search_result_body_bytes)) + + results = [] + + for group in doc_root.findall('response/results/grouping/group'): + results.append( + { + 'url': xml_element_contents_to_string(group.find('doc/url')).strip('\n'), + 'title': xml_element_contents_to_string(group.find('doc/title')).strip('\n'), + 'snippet': xml_element_contents_to_string(group.find('doc/passages/passage')), + } + ) + + results = get_filtered_results(results, filter_list) + + results = [ + SearchResult( + link=result.get('url'), + title=result.get('title'), + snippet=result.get('snippet'), + ) + for result in results[:count] + ] + + log.info(f'Yandex search results: {results}') + + return results + except Exception as e: + log.error(f'Error in search: {e}') + + return [] + + +if __name__ == '__main__': + from fastapi import FastAPI + from starlette.datastructures import Headers + + result = search_yandex( + Request( + { + 'type': 'http', + 'asgi.version': '3.0', + 'asgi.spec_version': '2.0', + 'method': 'GET', + 'path': '/internal', + 'query_string': b'', + 'headers': Headers({}).raw, + 'client': ('127.0.0.1', 12345), + 'server': ('127.0.0.1', 80), + 'scheme': 'http', + 'app': FastAPI(), + }, + None, + ), + os.environ.get('YANDEX_WEB_SEARCH_URL', ''), + os.environ.get('YANDEX_WEB_SEARCH_API_KEY', ''), + os.environ.get('YANDEX_WEB_SEARCH_CONFIG', '{"query": {"searchType": "SEARCH_TYPE_COM"}}'), + 'TOP movies of the past year', + 3, + ) + + print(result) diff --git a/backend/open_webui/retrieval/web/ydc.py b/backend/open_webui/retrieval/web/ydc.py new file mode 100644 index 0000000000..446fa5f16d --- /dev/null +++ b/backend/open_webui/retrieval/web/ydc.py @@ -0,0 +1,73 @@ +import logging +from typing import List, Optional + +import requests +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + + +def search_youcom( + api_key: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + language: str = 'EN', +) -> List[SearchResult]: + """Search using You.com's YDC Index API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A You.com API key + query (str): The query to search for + count (int): Maximum number of results to return + filter_list (list[str], optional): Domain filter list + language (str): Language code for search results (default: "EN") + """ + url = 'https://ydc-index.io/v1/search' + headers = { + 'Accept': 'application/json', + 'X-API-KEY': api_key, + } + params = { + 'query': query, + 'count': count, + 'language': language, + } + + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = json_response.get('results', {}).get('web', []) + + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result['url'], + title=result.get('title'), + snippet=_build_snippet(result), + ) + for result in results[:count] + ] + + +def _build_snippet(result: dict) -> str: + """Combine the description and snippets list into a single string. + + The You.com API returns a short ``description`` plus a ``snippets`` + list with richer passages. Merging them gives downstream retrieval + (embedding, BM25, bypass-loader context) the most content to work with. + """ + parts: list[str] = [] + + description = result.get('description') + if description: + parts.append(description) + + snippets = result.get('snippets') + if snippets and isinstance(snippets, list): + parts.extend(snippets) + + return '\n\n'.join(parts) diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py new file mode 100644 index 0000000000..fcef30342c --- /dev/null +++ b/backend/open_webui/routers/analytics.py @@ -0,0 +1,442 @@ +import logging +from collections import defaultdict +from datetime import datetime, timedelta +from typing import Optional + +from fastapi import APIRouter, Depends, Query +from open_webui.internal.db import get_async_session +from open_webui.models.chat_messages import ChatMessageModel, ChatMessages +from open_webui.models.chats import Chats +from open_webui.models.feedbacks import Feedbacks +from open_webui.models.groups import Groups +from open_webui.models.users import Users +from open_webui.utils.auth import get_admin_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + + +router = APIRouter() + + +#################### +# Response Models +#################### + + +class ModelAnalyticsEntry(BaseModel): + model_id: str + count: int + + +class ModelAnalyticsResponse(BaseModel): + models: list[ModelAnalyticsEntry] + + +class UserAnalyticsEntry(BaseModel): + user_id: str + name: Optional[str] = None + email: Optional[str] = None + count: int + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + + +class UserAnalyticsResponse(BaseModel): + users: list[UserAnalyticsEntry] + + +#################### +# Endpoints +#################### + + +@router.get('/models', response_model=ModelAnalyticsResponse) +async def get_model_analytics( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get message counts per model.""" + counts = await ChatMessages.get_message_count_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + models = [ + ModelAnalyticsEntry(model_id=model_id, count=count) + for model_id, count in sorted(counts.items(), key=lambda x: -x[1]) + ] + return ModelAnalyticsResponse(models=models) + + +@router.get('/users', response_model=UserAnalyticsResponse) +async def get_user_analytics( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + limit: int = Query(50, description='Max users to return'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get message counts and token usage per user with user info.""" + counts = await ChatMessages.get_message_count_by_user( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + token_usage = await ChatMessages.get_token_usage_by_user( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + + # Get user info for top users + top_user_ids = [uid for uid, _ in sorted(counts.items(), key=lambda x: -x[1])[:limit]] + user_info = {u.id: u for u in await Users.get_users_by_user_ids(top_user_ids, db=db)} + + users = [] + for user_id in top_user_ids: + u = user_info.get(user_id) + tokens = token_usage.get(user_id, {}) + users.append( + UserAnalyticsEntry( + user_id=user_id, + name=u.name if u else None, + email=u.email if u else None, + count=counts[user_id], + input_tokens=tokens.get('input_tokens', 0), + output_tokens=tokens.get('output_tokens', 0), + total_tokens=tokens.get('total_tokens', 0), + ) + ) + + return UserAnalyticsResponse(users=users) + + +@router.get('/messages', response_model=list[ChatMessageModel]) +async def get_messages( + model_id: Optional[str] = Query(None, description='Filter by model ID'), + user_id: Optional[str] = Query(None, description='Filter by user ID'), + chat_id: Optional[str] = Query(None, description='Filter by chat ID'), + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + skip: int = Query(0), + limit: int = Query(50, le=100), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Query messages with filters.""" + if chat_id: + return await ChatMessages.get_messages_by_chat_id(chat_id=chat_id, db=db) + elif model_id: + return await ChatMessages.get_messages_by_model_id( + model_id=model_id, + start_date=start_date, + end_date=end_date, + skip=skip, + limit=limit, + db=db, + ) + elif user_id: + return await ChatMessages.get_messages_by_user_id(user_id=user_id, skip=skip, limit=limit, db=db) + else: + # Return empty if no filter specified + return [] + + +class SummaryResponse(BaseModel): + total_messages: int + total_chats: int + total_models: int + total_users: int + + +@router.get('/summary', response_model=SummaryResponse) +async def get_summary( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get summary statistics for the dashboard.""" + model_counts = await ChatMessages.get_message_count_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + user_counts = await ChatMessages.get_message_count_by_user( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + chat_counts = await ChatMessages.get_message_count_by_chat( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + + return SummaryResponse( + total_messages=sum(model_counts.values()), + total_chats=len(chat_counts), + total_models=len(model_counts), + total_users=len(user_counts), + ) + + +class DailyStatsEntry(BaseModel): + date: str + models: dict[str, int] + + +class DailyStatsResponse(BaseModel): + data: list[DailyStatsEntry] + + +@router.get('/daily', response_model=DailyStatsResponse) +async def get_daily_stats( + start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'), + end_date: Optional[int] = Query(None, description='End timestamp (epoch)'), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + granularity: str = Query('daily', description="Granularity: 'hourly' or 'daily'"), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get message counts grouped by model for time-series chart.""" + if granularity == 'hourly': + counts = await ChatMessages.get_hourly_message_counts_by_model(start_date=start_date, end_date=end_date, db=db) + else: + counts = await ChatMessages.get_daily_message_counts_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + return DailyStatsResponse( + data=[DailyStatsEntry(date=date, models=models) for date, models in sorted(counts.items())] + ) + + +class TokenUsageEntry(BaseModel): + model_id: str + input_tokens: int + output_tokens: int + total_tokens: int + message_count: int + + +class TokenUsageResponse(BaseModel): + models: list[TokenUsageEntry] + total_input_tokens: int + total_output_tokens: int + total_tokens: int + + +@router.get('/tokens', response_model=TokenUsageResponse) +async def get_token_usage( + start_date: Optional[int] = Query(None), + end_date: Optional[int] = Query(None), + group_id: Optional[str] = Query(None, description='Filter by user group ID'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get token usage aggregated by model.""" + usage = await ChatMessages.get_token_usage_by_model( + start_date=start_date, end_date=end_date, group_id=group_id, db=db + ) + + models = [ + TokenUsageEntry(model_id=model_id, **data) + for model_id, data in sorted(usage.items(), key=lambda x: -x[1]['total_tokens']) + ] + + total_input = sum(m.input_tokens for m in models) + total_output = sum(m.output_tokens for m in models) + + return TokenUsageResponse( + models=models, + total_input_tokens=total_input, + total_output_tokens=total_output, + total_tokens=total_input + total_output, + ) + + +#################### +# Model Chats Browser +#################### + + +class ModelChatEntry(BaseModel): + chat_id: str + user_id: Optional[str] = None + user_name: Optional[str] = None + first_message: Optional[str] = None + updated_at: int + + +class ModelChatsResponse(BaseModel): + chats: list[ModelChatEntry] + total: int + + +@router.get('/models/{model_id:path}/chats', response_model=ModelChatsResponse) +async def get_model_chats( + model_id: str, + start_date: Optional[int] = Query(None), + end_date: Optional[int] = Query(None), + skip: int = Query(0), + limit: int = Query(50, le=100), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get chats that used a specific model, with preview and feedback info.""" + + # Get chat IDs that used this model + chat_ids = await ChatMessages.get_chat_ids_by_model_id( + model_id=model_id, + start_date=start_date, + end_date=end_date, + skip=skip, + limit=limit, + db=db, + ) + + if not chat_ids: + return ModelChatsResponse(chats=[], total=0) + + # Get chat details from messages only + chats_data = [] + for chat_id in chat_ids: + messages = await ChatMessages.get_messages_by_chat_id(chat_id, db=db) + if not messages: + continue + + # Get user_id from first user message + first_user_msg = next((m for m in messages if m.role == 'user'), None) + user_id = first_user_msg.user_id if first_user_msg else None + + # Extract first message content as preview + first_message = None + if first_user_msg and first_user_msg.content: + content = first_user_msg.content + if isinstance(content, str): + first_message = content[:200] + elif isinstance(content, list): + text_parts = [b.get('text', '') for b in content if isinstance(b, dict)] + first_message = ' '.join(text_parts)[:200] + + # Get user info + user_name = None + if user_id: + user_info = await Users.get_user_by_id(user_id, db=db) + user_name = user_info.name if user_info else None + + # Timestamps from messages + updated_at = max(m.created_at for m in messages) if messages else 0 + + chats_data.append( + ModelChatEntry( + chat_id=chat_id, + user_id=user_id, + user_name=user_name, + first_message=first_message, + updated_at=updated_at, + ) + ) + + return ModelChatsResponse(chats=chats_data, total=len(chats_data)) + + +#################### +# Model Overview +#################### + + +class HistoryEntry(BaseModel): + date: str + won: int = 0 + lost: int = 0 + + +class TagEntry(BaseModel): + tag: str + count: int + + +class ModelOverviewResponse(BaseModel): + history: list[HistoryEntry] + tags: list[TagEntry] + + +@router.get('/models/{model_id:path}/overview', response_model=ModelOverviewResponse) +async def get_model_overview( + model_id: str, + days: int = Query(30, description='Number of days of history (0 for all)'), + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Get model overview with feedback history and chat tags.""" + + # Get chat IDs that used this model + chat_ids = await ChatMessages.get_chat_ids_by_model_id( + model_id=model_id, + start_date=None, + end_date=None, + skip=0, + limit=10000, # Get all chats + db=db, + ) + + # Get feedback history per day + history_counts: dict[str, dict] = defaultdict(lambda: {'won': 0, 'lost': 0}) + + # Calculate start date for history + now = datetime.now() + start_dt = None + if days > 0: + start_dt = now - timedelta(days=days) + + for chat_id in chat_ids: + feedbacks = await Feedbacks.get_feedbacks_by_chat_id(chat_id, db=db) + for fb in feedbacks: + if fb.data and 'rating' in fb.data: + rating = fb.data['rating'] + fb_date = datetime.fromtimestamp(fb.created_at) + + # Filter by date range + if start_dt and fb_date < start_dt: + continue + + date_str = fb_date.strftime('%Y-%m-%d') + if rating == 1: + history_counts[date_str]['won'] += 1 + elif rating == -1: + history_counts[date_str]['lost'] += 1 + + # Fill in missing days + history = [] + if history_counts or days > 0: + end_dt = now + if days > 0: + current = start_dt + elif history_counts: + # Find earliest date + min_date = min(history_counts.keys()) + current = datetime.strptime(min_date, '%Y-%m-%d') + else: + current = now + + while current <= end_dt: + date_str = current.strftime('%Y-%m-%d') + counts = history_counts.get(date_str, {'won': 0, 'lost': 0}) + history.append( + HistoryEntry( + date=date_str, + won=counts['won'], + lost=counts['lost'], + ) + ) + current += timedelta(days=1) + + # Get chat tags + tag_counts: dict[str, int] = defaultdict(int) + for chat_id in chat_ids: + chat = await Chats.get_chat_by_id(chat_id, db=db) + if chat and chat.meta: + for tag in chat.meta.get('tags', []): + tag_counts[tag] += 1 + + # Sort by count and take top 10 + tags = [TagEntry(tag=tag, count=count) for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1])[:10]] + + return ModelOverviewResponse(history=history, tags=tags) diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index 52e0182cad..bf4c16acb9 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -1,104 +1,95 @@ +"""Audio router — TTS speech synthesis and STT transcription endpoints.""" + +import asyncio +import base64 import hashlib +import html +import io import json import logging +import mimetypes import os import uuid -import html -import base64 -from functools import lru_cache -from pydub import AudioSegment -from pydub.silence import split_on_silence -from concurrent.futures import ThreadPoolExecutor +from fnmatch import fnmatch +from pathlib import Path from typing import Optional -from fnmatch import fnmatch -import aiohttp import aiofiles -import requests -import mimetypes - +import aiohttp from fastapi import ( + APIRouter, Depends, - FastAPI, File, Form, HTTPException, Request, UploadFile, status, - APIRouter, ) -from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from pydantic import BaseModel +from pydub import AudioSegment +from pydub.silence import split_on_silence +from pydub.utils import mediainfo - -from open_webui.utils.misc import strict_match_mime_type -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission -from open_webui.utils.headers import include_user_info_headers from open_webui.config import ( - WHISPER_MODEL_AUTO_UPDATE, - WHISPER_COMPUTE_TYPE, - WHISPER_MODEL_DIR, - WHISPER_VAD_FILTER, CACHE_DIR, - WHISPER_LANGUAGE, - WHISPER_MULTILINGUAL, ELEVENLABS_API_BASE_URL, + WHISPER_COMPUTE_TYPE, + WHISPER_LANGUAGE, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + WHISPER_MULTILINGUAL, + WHISPER_VAD_FILTER, ) - from open_webui.constants import ERROR_MESSAGES from open_webui.env import ( - ENV, AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + BYPASS_PYDUB_PREPROCESSING, DEVICE_TYPE, ENABLE_FORWARD_USER_INFO_HEADERS, + ENV, ) - - -router = APIRouter() - -# Constants -MAX_FILE_SIZE_MB = 20 -MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes -AZURE_MAX_FILE_SIZE_MB = 200 -AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes +from open_webui.utils.access_control import has_permission +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.misc import strict_match_mime_type +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) +router = APIRouter() -SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech" +# --- Constants --- + +MAX_FILE_SIZE_MB: int = 20 +MAX_FILE_SIZE: int = MAX_FILE_SIZE_MB * 1024 * 1024 +AZURE_MAX_FILE_SIZE_MB: int = 200 +AZURE_MAX_FILE_SIZE: int = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 + +SPEECH_CACHE_DIR = CACHE_DIR / 'audio' / 'speech' SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) -########################################## -# -# Utility functions -# -########################################## - -from pydub import AudioSegment -from pydub.utils import mediainfo - def is_audio_conversion_required(file_path): """ Check if the given audio file needs conversion to mp3. """ - SUPPORTED_FORMATS = {"flac", "m4a", "mp3", "mp4", "mpeg", "wav", "webm"} + SUPPORTED_FORMATS = {'flac', 'm4a', 'mp3', 'mp4', 'mpeg', 'wav', 'webm'} if not os.path.isfile(file_path): - log.error(f"File not found: {file_path}") + log.error(f'File not found: {file_path}') return False try: info = mediainfo(file_path) - codec_name = info.get("codec_name", "").lower() - codec_type = info.get("codec_type", "").lower() - codec_tag_string = info.get("codec_tag_string", "").lower() + codec_name = info.get('codec_name', '').lower() + codec_type = info.get('codec_type', '').lower() + codec_tag_string = info.get('codec_tag_string', '').lower() - if codec_name == "aac" and codec_type == "audio" and codec_tag_string == "mp4a": + if codec_name == 'aac' and codec_type == 'audio' and codec_tag_string == 'mp4a': # File is AAC/mp4a audio, recommend mp3 conversion return True @@ -108,54 +99,93 @@ def is_audio_conversion_required(file_path): return True except Exception as e: - log.error(f"Error getting audio format: {e}") + log.error(f'Error getting audio format: {e}') return False def convert_audio_to_mp3(file_path): """Convert audio file to mp3 format.""" try: - output_path = os.path.splitext(file_path)[0] + ".mp3" + output_path = os.path.splitext(file_path)[0] + '.mp3' audio = AudioSegment.from_file(file_path) - audio.export(output_path, format="mp3") - log.info(f"Converted {file_path} to {output_path}") + audio.export(output_path, format='mp3') + log.info(f'Converted {file_path} to {output_path}') return output_path except Exception as e: - log.error(f"Error converting audio file: {e}") + log.error(f'Error converting audio file: {e}') return None +def transcode_audio_to_mp3(audio_data: bytes, content_type_header: str, output_path: str) -> bool: + """ + Transcode audio bytes to MP3 if the Content-Type indicates a non-MP3 format. + + Handles raw PCM audio (e.g. Gemini-TTS via OpenRouter/LiteLLM) by parsing + optional rate/channels from the Content-Type params, defaulting to 24kHz, + 16-bit, mono. For other non-MP3 formats, uses pydub auto-detection. + + Returns True if transcoding was performed, False if the data is already MP3. + Respects BYPASS_PYDUB_PREPROCESSING — when set, writes raw bytes and logs a warning. + """ + mime_type = content_type_header.split(';')[0].strip().lower() + + if mime_type in ('audio/mpeg', 'audio/mp3'): + return False + + if BYPASS_PYDUB_PREPROCESSING: + log.warning( + f'TTS returned {mime_type} but BYPASS_PYDUB_PREPROCESSING is set; writing raw audio without transcoding' + ) + return False + + if mime_type in ('audio/pcm', 'audio/l16', 'audio/raw'): + # Parse optional rate/channels from Content-Type params, + # default: 24kHz, 16-bit, mono (standard for Gemini TTS). + ct_params = {} + for part in content_type_header.split(';')[1:]: + key_val = part.strip().split('=') + if len(key_val) == 2: + ct_params[key_val[0].strip().lower()] = key_val[1].strip() + + sample_rate = int(ct_params.get('rate', 24000)) + channels = int(ct_params.get('channels', 1)) + + audio_segment = AudioSegment.from_raw( + io.BytesIO(audio_data), + sample_width=2, + frame_rate=sample_rate, + channels=channels, + ) + else: + audio_segment = AudioSegment.from_file(io.BytesIO(audio_data)) + + audio_segment.export(str(output_path), format='mp3') + log.info(f'Transcoded {mime_type} audio to MP3: {output_path}') + return True + + def set_faster_whisper_model(model: str, auto_update: bool = False): whisper_model = None if model: from faster_whisper import WhisperModel faster_whisper_kwargs = { - "model_size_or_path": model, - "device": DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu", - "compute_type": WHISPER_COMPUTE_TYPE, - "download_root": WHISPER_MODEL_DIR, - "local_files_only": not auto_update, + 'model_size_or_path': model, + 'device': DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == 'cuda' else 'cpu', + 'compute_type': WHISPER_COMPUTE_TYPE, + 'download_root': WHISPER_MODEL_DIR, + 'local_files_only': not auto_update, } try: whisper_model = WhisperModel(**faster_whisper_kwargs) except Exception: - log.warning( - "WhisperModel initialization failed, attempting download with local_files_only=False" - ) - faster_whisper_kwargs["local_files_only"] = False + log.warning('WhisperModel initialization failed, attempting download with local_files_only=False') + faster_whisper_kwargs['local_files_only'] = False whisper_model = WhisperModel(**faster_whisper_kwargs) return whisper_model -########################################## -# -# Audio API -# -########################################## - - class TTSConfigForm(BaseModel): OPENAI_API_BASE_URL: str OPENAI_API_KEY: str @@ -168,6 +198,8 @@ class TTSConfigForm(BaseModel): AZURE_SPEECH_REGION: str AZURE_SPEECH_BASE_URL: str AZURE_SPEECH_OUTPUT_FORMAT: str + MISTRAL_API_KEY: str + MISTRAL_API_BASE_URL: str class STTConfigForm(BaseModel): @@ -176,6 +208,7 @@ class STTConfigForm(BaseModel): ENGINE: str MODEL: str SUPPORTED_CONTENT_TYPES: list[str] = [] + ALLOWED_EXTENSIONS: list[str] = [] WHISPER_MODEL: str DEEPGRAM_API_KEY: str AZURE_API_KEY: str @@ -193,46 +226,48 @@ class AudioConfigUpdateForm(BaseModel): stt: STTConfigForm -@router.get("/config") +@router.get('/config') async def get_audio_config(request: Request, user=Depends(get_admin_user)): return { - "tts": { - "OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL, - "OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY, - "OPENAI_PARAMS": request.app.state.config.TTS_OPENAI_PARAMS, - "API_KEY": request.app.state.config.TTS_API_KEY, - "ENGINE": request.app.state.config.TTS_ENGINE, - "MODEL": request.app.state.config.TTS_MODEL, - "VOICE": request.app.state.config.TTS_VOICE, - "SPLIT_ON": request.app.state.config.TTS_SPLIT_ON, - "AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION, - "AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL, - "AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT, + 'tts': { + 'OPENAI_API_BASE_URL': request.app.state.config.TTS_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.TTS_OPENAI_API_KEY, + 'OPENAI_PARAMS': request.app.state.config.TTS_OPENAI_PARAMS, + 'API_KEY': request.app.state.config.TTS_API_KEY, + 'ENGINE': request.app.state.config.TTS_ENGINE, + 'MODEL': request.app.state.config.TTS_MODEL, + 'VOICE': request.app.state.config.TTS_VOICE, + 'SPLIT_ON': request.app.state.config.TTS_SPLIT_ON, + 'AZURE_SPEECH_REGION': request.app.state.config.TTS_AZURE_SPEECH_REGION, + 'AZURE_SPEECH_BASE_URL': request.app.state.config.TTS_AZURE_SPEECH_BASE_URL, + 'AZURE_SPEECH_OUTPUT_FORMAT': request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT, + 'MISTRAL_API_KEY': request.app.state.config.TTS_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.TTS_MISTRAL_API_BASE_URL, }, - "stt": { - "OPENAI_API_BASE_URL": request.app.state.config.STT_OPENAI_API_BASE_URL, - "OPENAI_API_KEY": request.app.state.config.STT_OPENAI_API_KEY, - "ENGINE": request.app.state.config.STT_ENGINE, - "MODEL": request.app.state.config.STT_MODEL, - "SUPPORTED_CONTENT_TYPES": request.app.state.config.STT_SUPPORTED_CONTENT_TYPES, - "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, - "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, - "AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY, - "AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION, - "AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES, - "AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL, - "AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS, - "MISTRAL_API_KEY": request.app.state.config.AUDIO_STT_MISTRAL_API_KEY, - "MISTRAL_API_BASE_URL": request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL, - "MISTRAL_USE_CHAT_COMPLETIONS": request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + 'stt': { + 'OPENAI_API_BASE_URL': request.app.state.config.STT_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.STT_OPENAI_API_KEY, + 'ENGINE': request.app.state.config.STT_ENGINE, + 'MODEL': request.app.state.config.STT_MODEL, + 'SUPPORTED_CONTENT_TYPES': request.app.state.config.STT_SUPPORTED_CONTENT_TYPES, + 'ALLOWED_EXTENSIONS': request.app.state.config.STT_ALLOWED_EXTENSIONS, + 'WHISPER_MODEL': request.app.state.config.WHISPER_MODEL, + 'DEEPGRAM_API_KEY': request.app.state.config.DEEPGRAM_API_KEY, + 'AZURE_API_KEY': request.app.state.config.AUDIO_STT_AZURE_API_KEY, + 'AZURE_REGION': request.app.state.config.AUDIO_STT_AZURE_REGION, + 'AZURE_LOCALES': request.app.state.config.AUDIO_STT_AZURE_LOCALES, + 'AZURE_BASE_URL': request.app.state.config.AUDIO_STT_AZURE_BASE_URL, + 'AZURE_MAX_SPEAKERS': request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS, + 'MISTRAL_API_KEY': request.app.state.config.AUDIO_STT_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL, + 'MISTRAL_USE_CHAT_COMPLETIONS': request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, }, } -@router.post("/config/update") -async def update_audio_config( - request: Request, form_data: AudioConfigUpdateForm, user=Depends(get_admin_user) -): +@router.post('/config/update') +async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)): + # TTS settings request.app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL request.app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY request.app.state.config.TTS_OPENAI_PARAMS = form_data.tts.OPENAI_PARAMS @@ -242,104 +277,316 @@ async def update_audio_config( request.app.state.config.TTS_VOICE = form_data.tts.VOICE request.app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON request.app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION - request.app.state.config.TTS_AZURE_SPEECH_BASE_URL = ( - form_data.tts.AZURE_SPEECH_BASE_URL - ) - request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = ( - form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT - ) + request.app.state.config.TTS_AZURE_SPEECH_BASE_URL = form_data.tts.AZURE_SPEECH_BASE_URL + request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT + request.app.state.config.TTS_MISTRAL_API_KEY = form_data.tts.MISTRAL_API_KEY + request.app.state.config.TTS_MISTRAL_API_BASE_URL = form_data.tts.MISTRAL_API_BASE_URL + # STT settings request.app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL request.app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY request.app.state.config.STT_ENGINE = form_data.stt.ENGINE request.app.state.config.STT_MODEL = form_data.stt.MODEL - request.app.state.config.STT_SUPPORTED_CONTENT_TYPES = ( - form_data.stt.SUPPORTED_CONTENT_TYPES - ) - + request.app.state.config.STT_SUPPORTED_CONTENT_TYPES = form_data.stt.SUPPORTED_CONTENT_TYPES + request.app.state.config.STT_ALLOWED_EXTENSIONS = form_data.stt.ALLOWED_EXTENSIONS request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES request.app.state.config.AUDIO_STT_AZURE_BASE_URL = form_data.stt.AZURE_BASE_URL - request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = ( - form_data.stt.AZURE_MAX_SPEAKERS - ) + request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = form_data.stt.AZURE_MAX_SPEAKERS request.app.state.config.AUDIO_STT_MISTRAL_API_KEY = form_data.stt.MISTRAL_API_KEY - request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = ( - form_data.stt.MISTRAL_API_BASE_URL - ) - request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = ( - form_data.stt.MISTRAL_USE_CHAT_COMPLETIONS - ) + request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL = form_data.stt.MISTRAL_API_BASE_URL + request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS = form_data.stt.MISTRAL_USE_CHAT_COMPLETIONS - if request.app.state.config.STT_ENGINE == "": + if request.app.state.config.STT_ENGINE == '': request.app.state.faster_whisper_model = set_faster_whisper_model( form_data.stt.WHISPER_MODEL, WHISPER_MODEL_AUTO_UPDATE ) else: request.app.state.faster_whisper_model = None + + return { - "tts": { - "ENGINE": request.app.state.config.TTS_ENGINE, - "MODEL": request.app.state.config.TTS_MODEL, - "VOICE": request.app.state.config.TTS_VOICE, - "OPENAI_API_BASE_URL": request.app.state.config.TTS_OPENAI_API_BASE_URL, - "OPENAI_API_KEY": request.app.state.config.TTS_OPENAI_API_KEY, - "OPENAI_PARAMS": request.app.state.config.TTS_OPENAI_PARAMS, - "API_KEY": request.app.state.config.TTS_API_KEY, - "SPLIT_ON": request.app.state.config.TTS_SPLIT_ON, - "AZURE_SPEECH_REGION": request.app.state.config.TTS_AZURE_SPEECH_REGION, - "AZURE_SPEECH_BASE_URL": request.app.state.config.TTS_AZURE_SPEECH_BASE_URL, - "AZURE_SPEECH_OUTPUT_FORMAT": request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT, + 'tts': { + 'ENGINE': request.app.state.config.TTS_ENGINE, + 'MODEL': request.app.state.config.TTS_MODEL, + 'VOICE': request.app.state.config.TTS_VOICE, + 'OPENAI_API_BASE_URL': request.app.state.config.TTS_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.TTS_OPENAI_API_KEY, + 'OPENAI_PARAMS': request.app.state.config.TTS_OPENAI_PARAMS, + 'API_KEY': request.app.state.config.TTS_API_KEY, + 'SPLIT_ON': request.app.state.config.TTS_SPLIT_ON, + 'AZURE_SPEECH_REGION': request.app.state.config.TTS_AZURE_SPEECH_REGION, + 'AZURE_SPEECH_BASE_URL': request.app.state.config.TTS_AZURE_SPEECH_BASE_URL, + 'AZURE_SPEECH_OUTPUT_FORMAT': request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT, + 'MISTRAL_API_KEY': request.app.state.config.TTS_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.TTS_MISTRAL_API_BASE_URL, }, - "stt": { - "OPENAI_API_BASE_URL": request.app.state.config.STT_OPENAI_API_BASE_URL, - "OPENAI_API_KEY": request.app.state.config.STT_OPENAI_API_KEY, - "ENGINE": request.app.state.config.STT_ENGINE, - "MODEL": request.app.state.config.STT_MODEL, - "SUPPORTED_CONTENT_TYPES": request.app.state.config.STT_SUPPORTED_CONTENT_TYPES, - "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, - "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, - "AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY, - "AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION, - "AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES, - "AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL, - "AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS, - "MISTRAL_API_KEY": request.app.state.config.AUDIO_STT_MISTRAL_API_KEY, - "MISTRAL_API_BASE_URL": request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL, - "MISTRAL_USE_CHAT_COMPLETIONS": request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + 'stt': { + 'OPENAI_API_BASE_URL': request.app.state.config.STT_OPENAI_API_BASE_URL, + 'OPENAI_API_KEY': request.app.state.config.STT_OPENAI_API_KEY, + 'ENGINE': request.app.state.config.STT_ENGINE, + 'MODEL': request.app.state.config.STT_MODEL, + 'SUPPORTED_CONTENT_TYPES': request.app.state.config.STT_SUPPORTED_CONTENT_TYPES, + 'ALLOWED_EXTENSIONS': request.app.state.config.STT_ALLOWED_EXTENSIONS, + 'WHISPER_MODEL': request.app.state.config.WHISPER_MODEL, + 'DEEPGRAM_API_KEY': request.app.state.config.DEEPGRAM_API_KEY, + 'AZURE_API_KEY': request.app.state.config.AUDIO_STT_AZURE_API_KEY, + 'AZURE_REGION': request.app.state.config.AUDIO_STT_AZURE_REGION, + 'AZURE_LOCALES': request.app.state.config.AUDIO_STT_AZURE_LOCALES, + 'AZURE_BASE_URL': request.app.state.config.AUDIO_STT_AZURE_BASE_URL, + 'AZURE_MAX_SPEAKERS': request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS, + 'MISTRAL_API_KEY': request.app.state.config.AUDIO_STT_MISTRAL_API_KEY, + 'MISTRAL_API_BASE_URL': request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL, + 'MISTRAL_USE_CHAT_COMPLETIONS': request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, }, } def load_speech_pipeline(request): - from transformers import pipeline from datasets import load_dataset + from transformers import pipeline if request.app.state.speech_synthesiser is None: - request.app.state.speech_synthesiser = pipeline( - "text-to-speech", "microsoft/speecht5_tts" - ) + request.app.state.speech_synthesiser = pipeline('text-to-speech', 'microsoft/speecht5_tts') if request.app.state.speech_speaker_embeddings_dataset is None: request.app.state.speech_speaker_embeddings_dataset = load_dataset( - "Matthijs/cmu-arctic-xvectors", split="validation" + 'Matthijs/cmu-arctic-xvectors', split='validation' ) -@router.post("/speech") +async def _raise_tts_error(exc: Exception, r=None) -> None: + """Raise a standardised HTTPException from a TTS provider failure.""" + code = r.status if r is not None else 500 + detail = 'Open WebUI: Server Connection Error' + if r is not None: + try: + res = await r.json() + if 'error' in res: + msg = res['error'] + detail = f'External: {msg.get("message", msg) if isinstance(msg, dict) else msg}' + elif 'message' in res: + detail = f'External: {res["message"]}' + except Exception: + detail = f'External: {exc}' + raise HTTPException(status_code=code, detail=detail) + + +async def _write_tts_cache( + file_path: Path, audio: bytes, body_path: Path, payload: dict, +) -> None: + """Persist audio + request metadata to the speech cache.""" + async with aiofiles.open(file_path, 'wb') as f: + await f.write(audio) + async with aiofiles.open(body_path, 'w') as f: + await f.write(json.dumps(payload)) + + +async def _tts_openai(request, payload, file_path, file_body_path, user): + """Generate speech via an OpenAI-compatible TTS endpoint.""" + payload['model'] = request.app.state.config.TTS_MODEL + if not payload.get('voice'): + payload['voice'] = request.app.state.config.TTS_VOICE + payload = {**payload, **(request.app.state.config.TTS_OPENAI_PARAMS or {})} + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {request.app.state.config.TTS_OPENAI_API_KEY}', + } + if ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + + r = None + try: + session = await get_session() + r = await session.post( + url=f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech', + json=payload, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + + audio_data = await r.read() + content_type = r.headers.get('Content-Type', 'audio/mpeg') + + if not await asyncio.to_thread( + transcode_audio_to_mp3, audio_data, content_type, file_path + ): + async with aiofiles.open(file_path, 'wb') as f: + await f.write(audio_data) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +async def _tts_elevenlabs(request, payload, file_path, file_body_path, user): + """Generate speech via the ElevenLabs TTS API.""" + voice_id = payload.get('voice', '') + if voice_id not in await get_available_voices(request): + raise HTTPException(status_code=400, detail='Invalid voice id') + + r = None + try: + session = await get_session() + async with session.post( + f'{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}', + json={ + 'text': payload['input'], + 'model_id': request.app.state.config.TTS_MODEL, + 'voice_settings': {'stability': 0.5, 'similarity_boost': 0.5}, + }, + headers={ + 'Accept': 'audio/mpeg', + 'Content-Type': 'application/json', + 'xi-api-key': request.app.state.config.TTS_API_KEY, + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + await _write_tts_cache(file_path, await r.read(), file_body_path, payload) + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +async def _tts_azure(request, payload, file_path, file_body_path, user): + """Generate speech via Azure Cognitive Services TTS.""" + az_region = request.app.state.config.TTS_AZURE_SPEECH_REGION or 'eastus' + az_base = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL + language = request.app.state.config.TTS_VOICE + locale = '-'.join(language.split('-')[:2]) + output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT + + ssml = ( + f'' + f'{html.escape(payload["input"])}' + f'' + ) + + r = None + try: + session = await get_session() + async with session.post( + (az_base or f'https://{az_region}.tts.speech.microsoft.com') + '/cognitiveservices/v1', + headers={ + 'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY, + 'Content-Type': 'application/ssml+xml', + 'X-Microsoft-OutputFormat': output_format, + }, + data=ssml, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + await _write_tts_cache(file_path, await r.read(), file_body_path, payload) + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +async def _tts_transformers(request, payload, file_path, file_body_path, user): + """Generate speech via the local HuggingFace SpeechT5 pipeline (thread-offloaded).""" + import soundfile as sf + import torch + + load_speech_pipeline(request) + + embeddings = request.app.state.speech_speaker_embeddings_dataset + model_name = request.app.state.config.TTS_MODEL + + idx = 6799 + try: + idx = embeddings['filename'].index(model_name) + except (ValueError, KeyError): + log.debug(f'Speaker embedding not found for {model_name}, using default index {idx}') + + def _run_pipeline(): + speaker_embedding = torch.tensor(embeddings[idx]['xvector']).unsqueeze(0) + wav = request.app.state.speech_synthesiser( + payload['input'], # raw text to synthesize + forward_params={ + 'speaker_embeddings': speaker_embedding, + }, + ) + sf.write(str(file_path), wav['audio'], samplerate=wav['sampling_rate']) + + await asyncio.to_thread(_run_pipeline) + + # Audio file already written by sf.write; just persist the request metadata. + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + return FileResponse(file_path) + + +async def _tts_mistral(request, payload, file_path, file_body_path, user): + """Generate speech via the Mistral TTS API.""" + api_key = request.app.state.config.TTS_MISTRAL_API_KEY + api_base_url = request.app.state.config.TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1' + + if not api_key: + raise HTTPException(status_code=400, detail='Mistral API key is required for Mistral TTS') + + r = None + try: + session = await get_session() + r = await session.post( + url=f'{api_base_url}/audio/speech', + json={ + 'input': payload.get('input', ''), # text to synthesize + 'model': request.app.state.config.TTS_MODEL or 'voxtral-mini-tts-2603', + 'voice_id': payload.get('voice', ''), + 'response_format': 'mp3', + }, + headers={ + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + + res = await r.json() + audio_b64 = res.get('audio_data', '') + if not audio_b64: + raise ValueError('No audio_data in Mistral TTS response') + + await _write_tts_cache(file_path, base64.b64decode(audio_b64), file_body_path, payload) + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +# Dispatcher map: engine name -> handler +_TTS_ENGINES = { + 'openai': _tts_openai, + 'elevenlabs': _tts_elevenlabs, + 'azure': _tts_azure, + 'transformers': _tts_transformers, + 'mistral': _tts_mistral, +} + + +@router.post('/speech') async def speech(request: Request, user=Depends(get_verified_user)): - if request.app.state.config.TTS_ENGINE == "": + engine = request.app.state.config.TTS_ENGINE + if engine == '': raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if user.role != "admin" and not has_permission( - user.id, "chat.tts", request.app.state.config.USER_PERMISSIONS + if user.role != 'admin' and not await has_permission( + user.id, 'chat.tts', request.app.state.config.USER_PERMISSIONS ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -349,743 +596,465 @@ async def speech(request: Request, user=Depends(get_verified_user)): body = await request.body() name = hashlib.sha256( body - + str(request.app.state.config.TTS_ENGINE).encode("utf-8") - + str(request.app.state.config.TTS_MODEL).encode("utf-8") + + str(engine).encode('utf-8') + + str(request.app.state.config.TTS_MODEL).encode('utf-8') ).hexdigest() - file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") - file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") + file_path = SPEECH_CACHE_DIR.joinpath(f'{name}.mp3') + file_body_path = SPEECH_CACHE_DIR.joinpath(f'{name}.json') - # Check if the file already exists in the cache + # Return cached result if available if file_path.is_file(): return FileResponse(file_path) - payload = None try: - payload = json.loads(body.decode("utf-8")) + payload = json.loads(body) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail='Invalid JSON payload') + + handler = _TTS_ENGINES.get(engine) + if handler is None: + raise HTTPException(status_code=400, detail=f'Unsupported TTS engine: {engine}') + + return await handler(request, payload, file_path, file_body_path, user) + + +async def _transcribe_whisper(request, file_path, languages, file_dir, id): + if request.app.state.faster_whisper_model is None: + request.app.state.faster_whisper_model = set_faster_whisper_model(request.app.state.config.WHISPER_MODEL) + + model = request.app.state.faster_whisper_model + + def _run(): + segments, info = model.transcribe( + file_path, beam_size=5, vad_filter=WHISPER_VAD_FILTER, + language=languages[0], multilingual=WHISPER_MULTILINGUAL, + ) + log.info("Detected language '%s' with probability %f" % (info.language, info.language_probability)) + return ''.join([segment.text for segment in list(segments)]) + + transcript = await asyncio.to_thread(_run) + data = {'text': transcript.strip()} + + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + + log.debug(data) + return data + + +async def _transcribe_openai(request, file_path, filename, languages, file_dir, id, user=None): + """Transcribe audio via an OpenAI-compatible STT endpoint.""" + r = None + try: + session = await get_session() + for language in languages: + payload = {'model': request.app.state.config.STT_MODEL} + if language: + payload['language'] = language + + headers = {'Authorization': f'Bearer {request.app.state.config.STT_OPENAI_API_KEY}'} + if user and ENABLE_FORWARD_USER_INFO_HEADERS: + headers = include_user_info_headers(headers, user) + + form_data = aiohttp.FormData() + for key, value in payload.items(): + form_data.add_field(key, str(value)) + form_data.add_field('file', open(file_path, 'rb'), filename=filename) + + r = await session.post( + url=f'{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions', + headers=headers, data=form_data, ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + if r.status == 200: + break + + r.raise_for_status() + data = await r.json() + + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + return data except Exception as e: log.exception(e) - raise HTTPException(status_code=400, detail="Invalid JSON payload") + detail = None + if r is not None: + try: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + raise Exception(detail if detail else 'Open WebUI: Server Connection Error') + + +async def _transcribe_deepgram(request, file_path, languages, file_dir, id): + """Transcribe audio via the Deepgram listen API with language fallback.""" + content_type = mimetypes.guess_type(file_path)[0] or 'audio/wav' + + async with aiofiles.open(file_path, 'rb') as f: + audio_bytes = await f.read() + + api_key = request.app.state.config.DEEPGRAM_API_KEY + stt_model = request.app.state.config.STT_MODEL r = None - if request.app.state.config.TTS_ENGINE == "openai": - payload["model"] = request.app.state.config.TTS_MODEL + try: + session = await get_session() + for lang in languages: + query: dict = {'smart_format': 'true'} + if stt_model: + query['model'] = stt_model + if lang: + query['language'] = lang - try: - timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - async with aiohttp.ClientSession( - timeout=timeout, trust_env=True - ) as session: - payload = { - **payload, - **(request.app.state.config.TTS_OPENAI_PARAMS or {}), - } - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}", - } - if ENABLE_FORWARD_USER_INFO_HEADERS: - headers = include_user_info_headers(headers, user) - - r = await session.post( - url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", - json=payload, - headers=headers, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) - - r.raise_for_status() - - async with aiofiles.open(file_path, "wb") as f: - await f.write(await r.read()) - - async with aiofiles.open(file_body_path, "w") as f: - await f.write(json.dumps(payload)) - - return FileResponse(file_path) - - except Exception as e: - log.exception(e) - detail = None - - status_code = 500 - detail = f"Open WebUI: Server Connection Error" - - if r is not None: - status_code = r.status - - try: - res = await r.json() - if "error" in res: - detail = f"External: {res['error']}" - except Exception: - detail = f"External: {e}" - - raise HTTPException( - status_code=status_code, - detail=detail, + r = await session.post( + 'https://api.deepgram.com/v1/listen', + headers={'Authorization': f'Token {api_key}', 'Content-Type': content_type}, + params=query, + data=audio_bytes, + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) + if r.status == 200: + break - elif request.app.state.config.TTS_ENGINE == "elevenlabs": - voice_id = payload.get("voice", "") - - if voice_id not in get_available_voices(request): - raise HTTPException( - status_code=400, - detail="Invalid voice id", - ) + r.raise_for_status() + body = await r.json() + # Parse the Deepgram response structure try: - timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - async with aiohttp.ClientSession( - timeout=timeout, trust_env=True - ) as session: - async with session.post( - f"{ELEVENLABS_API_BASE_URL}/v1/text-to-speech/{voice_id}", - json={ - "text": payload["input"], - "model_id": request.app.state.config.TTS_MODEL, - "voice_settings": {"stability": 0.5, "similarity_boost": 0.5}, - }, - headers={ - "Accept": "audio/mpeg", - "Content-Type": "application/json", - "xi-api-key": request.app.state.config.TTS_API_KEY, - }, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - r.raise_for_status() + transcript = ( + body['results']['channels'][0]['alternatives'][0] + .get('transcript', '') + .strip() + ) + except (KeyError, IndexError) as exc: + log.error(f'Malformed Deepgram response: {exc}') + raise Exception('Failed to parse Deepgram response') from exc - async with aiofiles.open(file_path, "wb") as f: - await f.write(await r.read()) - - async with aiofiles.open(file_body_path, "w") as f: - await f.write(json.dumps(payload)) - - return FileResponse(file_path) - - except Exception as e: - log.exception(e) - detail = None + data = {'text': transcript} + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + return data + except Exception as e: + log.exception(e) + detail = 'Open WebUI: Server Connection Error' + if r is not None: try: - if r.status != 200: - res = await r.json() - if "error" in res: - detail = f"External: {res['error'].get('message', '')}" + res = await r.json() + msg = res.get('error', {}).get('message', '') if isinstance(res.get('error'), dict) else str(res.get('error', '')) + if msg: + detail = f'External: {msg}' except Exception: - detail = f"External: {e}" + detail = f'External: {e}' + raise Exception(detail) - raise HTTPException( - status_code=getattr(r, "status", 500) if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - elif request.app.state.config.TTS_ENGINE == "azure": - try: - payload = json.loads(body.decode("utf-8")) - except Exception as e: - log.exception(e) - raise HTTPException(status_code=400, detail="Invalid JSON payload") - region = request.app.state.config.TTS_AZURE_SPEECH_REGION or "eastus" - base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL - language = request.app.state.config.TTS_VOICE - locale = "-".join(request.app.state.config.TTS_VOICE.split("-")[:1]) - output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT +async def _transcribe_azure(request, file_path, filename, file_dir, id): + """Transcribe audio via Azure Cognitive Services batch transcription.""" + if not os.path.isfile(file_path): + raise HTTPException(status_code=400, detail='Audio file not found') - try: - data = f""" - {html.escape(payload["input"])} - """ - timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - async with aiohttp.ClientSession( - timeout=timeout, trust_env=True - ) as session: - async with session.post( - (base_url or f"https://{region}.tts.speech.microsoft.com") - + "/cognitiveservices/v1", - headers={ - "Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY, - "Content-Type": "application/ssml+xml", - "X-Microsoft-OutputFormat": output_format, - }, - data=data, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - r.raise_for_status() - - async with aiofiles.open(file_path, "wb") as f: - await f.write(await r.read()) - - async with aiofiles.open(file_body_path, "w") as f: - await f.write(json.dumps(payload)) - - return FileResponse(file_path) - - except Exception as e: - log.exception(e) - detail = None - - try: - if r.status != 200: - res = await r.json() - if "error" in res: - detail = f"External: {res['error'].get('message', '')}" - except Exception: - detail = f"External: {e}" - - raise HTTPException( - status_code=getattr(r, "status", 500) if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - elif request.app.state.config.TTS_ENGINE == "transformers": - payload = None - try: - payload = json.loads(body.decode("utf-8")) - except Exception as e: - log.exception(e) - raise HTTPException(status_code=400, detail="Invalid JSON payload") - - import torch - import soundfile as sf - - load_speech_pipeline(request) - - embeddings_dataset = request.app.state.speech_speaker_embeddings_dataset - - speaker_index = 6799 - try: - speaker_index = embeddings_dataset["filename"].index( - request.app.state.config.TTS_MODEL - ) - except Exception: - pass - - speaker_embedding = torch.tensor( - embeddings_dataset[speaker_index]["xvector"] - ).unsqueeze(0) - - speech = request.app.state.speech_synthesiser( - payload["input"], - forward_params={"speaker_embeddings": speaker_embedding}, + audio_size = os.path.getsize(file_path) + if audio_size > AZURE_MAX_FILE_SIZE: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'File size ({audio_size // (1024*1024)}MB) exceeds Azure limit of {AZURE_MAX_FILE_SIZE_MB}MB', ) - sf.write(file_path, speech["audio"], samplerate=speech["sampling_rate"]) + api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY + region = request.app.state.config.AUDIO_STT_AZURE_REGION or 'eastus' + locale_str = request.app.state.config.AUDIO_STT_AZURE_LOCALES + base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL + max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS or 3 - async with aiofiles.open(file_body_path, "w") as f: - await f.write(json.dumps(payload)) + # Default to a broad set of locales when none are configured + if len(locale_str) < 2: + locale_str = ','.join([ + 'en-US', 'es-ES', 'es-MX', 'fr-FR', 'hi-IN', + 'it-IT', 'de-DE', 'en-GB', 'en-IN', 'ja-JP', + 'ko-KR', 'pt-BR', 'zh-CN', + ]) - return FileResponse(file_path) + if not api_key or not region: + raise HTTPException(status_code=400, detail='Azure API key and region are required for Azure STT') + + # Build the transcription definition payload + definition = json.dumps( + {'locales': locale_str.split(','), 'diarization': {'maxSpeakers': max_speakers, 'enabled': True}} + if locale_str else {} + ) + endpoint = ( + base_url or f'https://{region}.api.cognitive.microsoft.com' + ) + '/speechtotext/transcriptions:transcribe?api-version=2024-11-15' + + form_data = aiohttp.FormData() + form_data.add_field('definition', definition) + form_data.add_field('audio', open(file_path, 'rb'), filename=filename) + + r = None + try: + session = await get_session() + r = await session.post( + url=endpoint, data=form_data, + headers={'Ocp-Apim-Subscription-Key': api_key}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + response = await r.json() + + if not response.get('combinedPhrases'): + raise ValueError('No transcription found in response') + + transcript = response['combinedPhrases'][0].get('text', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + + data = {'text': transcript} + + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + + log.debug(data) + return data + + except (KeyError, IndexError, ValueError) as e: + log.exception('Error parsing Azure response') + raise HTTPException(status_code=500, detail=f'Failed to parse Azure response: {str(e)}') + except aiohttp.ClientResponseError as e: + log.exception(e) + detail = None + try: + if r is not None and r.status != 200: + res = await r.json() + if 'code' in res and 'message' in res: + azure_code = res.get('innerError', {}).get('code', res['code']) + user_facing_codes = { + 'EmptyAudioFile', 'AudioLengthLimitExceeded', + 'NoLanguageIdentified', 'MultipleLanguagesIdentified', + } + if azure_code in user_facing_codes: + detail = res['message'] + else: + log.error(f'Azure STT error [{azure_code}]: {res["message"]}') + detail = 'An error occurred during transcription.' + elif 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + except Exception: + detail = f'External: {e}' + raise HTTPException( + status_code=e.status if e.status else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) -def transcription_handler(request, file_path, metadata, user=None): + +async def transcription_handler(request, file_path, metadata, user=None): filename = os.path.basename(file_path) file_dir = os.path.dirname(file_path) - id = filename.split(".")[0] + id = filename.split('.')[0] metadata = metadata or {} languages = [ - metadata.get("language", None) if not WHISPER_LANGUAGE else WHISPER_LANGUAGE, + metadata.get('language', None) if not WHISPER_LANGUAGE else WHISPER_LANGUAGE, None, # Always fallback to None in case transcription fails ] - if request.app.state.config.STT_ENGINE == "": - if request.app.state.faster_whisper_model is None: - request.app.state.faster_whisper_model = set_faster_whisper_model( - request.app.state.config.WHISPER_MODEL + if request.app.state.config.STT_ENGINE == '': + return await _transcribe_whisper(request, file_path, languages, file_dir, id) + elif request.app.state.config.STT_ENGINE == 'openai': + return await _transcribe_openai(request, file_path, filename, languages, file_dir, id, user) + elif request.app.state.config.STT_ENGINE == 'deepgram': + return await _transcribe_deepgram(request, file_path, languages, file_dir, id) + elif request.app.state.config.STT_ENGINE == 'azure': + return await _transcribe_azure(request, file_path, filename, file_dir, id) + + elif request.app.state.config.STT_ENGINE == 'mistral': + return await _transcribe_mistral(request, file_path, filename, metadata, file_dir, id) + + +async def _transcribe_mistral(request, file_path, filename, metadata, file_dir, id): + """Transcribe audio via the Mistral STT API.""" + if not os.path.isfile(file_path): + raise HTTPException(status_code=400, detail='Audio file not found') + + file_size = os.path.getsize(file_path) + if file_size > MAX_FILE_SIZE: + raise HTTPException(status_code=400, detail=f'File size exceeds limit of {MAX_FILE_SIZE_MB}MB') + + api_key = request.app.state.config.AUDIO_STT_MISTRAL_API_KEY + api_base_url = request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1' + use_chat_completions = request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS + + if not api_key: + raise HTTPException(status_code=400, detail='Mistral API key is required for Mistral STT') + + r = None + try: + model = request.app.state.config.STT_MODEL or 'voxtral-mini-latest' + log.info( + f'Mistral STT - model: {model}, ' + f'method: {"chat_completions" if use_chat_completions else "transcriptions"}' + ) + + session = await get_session() + if use_chat_completions: + audio_file_to_use = file_path + if is_audio_conversion_required(file_path): + log.debug('Converting audio to mp3 for chat completions API') + converted_path = await asyncio.to_thread(convert_audio_to_mp3, file_path) + if converted_path: + audio_file_to_use = converted_path + else: + log.error('Audio conversion failed') + raise HTTPException( + status_code=500, + detail='Audio conversion failed. Chat completions API requires mp3 or wav format.', + ) + + async with aiofiles.open(audio_file_to_use, 'rb') as audio_file: + raw = await audio_file.read() + audio_base64 = { + 'data': base64.b64encode(raw).decode('utf-8'), + 'format': mimetypes.guess_extension(mimetypes.guess_type(audio_file_to_use)[0]).lstrip('.'), + } + + language = metadata.get('language', None) if metadata else None + text_instruction = ( + f'Transcribe this audio exactly as spoken in {language}. Do not translate it.' + if language + else 'Transcribe this audio exactly as spoken in its original language. Do not translate it to another language.' ) - model = request.app.state.faster_whisper_model - segments, info = model.transcribe( - file_path, - beam_size=5, - vad_filter=WHISPER_VAD_FILTER, - language=languages[0], - multilingual=WHISPER_MULTILINGUAL, - ) - log.info( - "Detected language '%s' with probability %f" - % (info.language, info.language_probability) - ) + payload = { + 'model': model, + 'messages': [{ + 'role': 'user', + 'content': [ + {'type': 'input_audio', 'input_audio': audio_base64}, + {'type': 'text', 'text': text_instruction}, + ], + }], + } - transcript = "".join([segment.text for segment in list(segments)]) - data = {"text": transcript.strip()} + r = await session.post( + url=f'{api_base_url}/chat/completions', json=payload, + headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + response = await r.json() - # save the transcript to a json file - transcript_file = f"{file_dir}/{id}.json" - with open(transcript_file, "w") as f: - json.dump(data, f) + transcript = response.get('choices', [{}])[0].get('message', {}).get('content', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + data = {'text': transcript} + + else: + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type: + mime_type = 'audio/webm' + + form_data = aiohttp.FormData() + form_data.add_field('model', model) + + language = metadata.get('language', None) if metadata else None + if language: + form_data.add_field('language', language) + + form_data.add_field('file', open(file_path, 'rb'), filename=filename, content_type=mime_type) + + r = await session.post( + url=f'{api_base_url}/audio/transcriptions', data=form_data, + headers={'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + response = await r.json() + + transcript = response.get('text', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + data = {'text': transcript} + + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) log.debug(data) return data - elif request.app.state.config.STT_ENGINE == "openai": - r = None + + except ValueError as e: + log.exception('Error parsing Mistral response') + raise HTTPException(status_code=500, detail=f'Failed to parse Mistral response: {str(e)}') + except aiohttp.ClientResponseError as e: + log.exception(e) + detail = None try: - for language in languages: - payload = { - "model": request.app.state.config.STT_MODEL, - } - - if language: - payload["language"] = language - - headers = { - "Authorization": f"Bearer {request.app.state.config.STT_OPENAI_API_KEY}" - } - if user and ENABLE_FORWARD_USER_INFO_HEADERS: - headers = include_user_info_headers(headers, user) - - r = requests.post( - url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions", - headers=headers, - files={"file": (filename, open(file_path, "rb"))}, - data=payload, - ) - - if r.status_code == 200: - # Successful transcription - break - - r.raise_for_status() - data = r.json() - - # save the transcript to a json file - transcript_file = f"{file_dir}/{id}.json" - with open(transcript_file, "w") as f: - json.dump(data, f) - - return data - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"External: {res['error'].get('message', '')}" - except Exception: - detail = f"External: {e}" - - raise Exception(detail if detail else "Open WebUI: Server Connection Error") - - elif request.app.state.config.STT_ENGINE == "deepgram": - try: - # Determine the MIME type of the file - mime, _ = mimetypes.guess_type(file_path) - if not mime: - mime = "audio/wav" # fallback to wav if undetectable - - # Read the audio file - with open(file_path, "rb") as f: - file_data = f.read() - - # Build headers and parameters - headers = { - "Authorization": f"Token {request.app.state.config.DEEPGRAM_API_KEY}", - "Content-Type": mime, - } - - for language in languages: - params = {} - if request.app.state.config.STT_MODEL: - params["model"] = request.app.state.config.STT_MODEL - - if language: - params["language"] = language - - # Make request to Deepgram API - r = requests.post( - "https://api.deepgram.com/v1/listen?smart_format=true", - headers=headers, - params=params, - data=file_data, - ) - - if r.status_code == 200: - # Successful transcription - break - - r.raise_for_status() - response_data = r.json() - - # Extract transcript from Deepgram response - try: - transcript = response_data["results"]["channels"][0]["alternatives"][ - 0 - ].get("transcript", "") - except (KeyError, IndexError) as e: - log.error(f"Malformed response from Deepgram: {str(e)}") - raise Exception( - "Failed to parse Deepgram response - unexpected response format" - ) - data = {"text": transcript.strip()} - - # Save transcript - transcript_file = f"{file_dir}/{id}.json" - with open(transcript_file, "w") as f: - json.dump(data, f) - - return data - - except Exception as e: - log.exception(e) - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"External: {res['error'].get('message', '')}" - except Exception: - detail = f"External: {e}" - raise Exception(detail if detail else "Open WebUI: Server Connection Error") - - elif request.app.state.config.STT_ENGINE == "azure": - # Check file exists and size - if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail="Audio file not found") - - # Check file size (Azure has a larger limit of 200MB) - file_size = os.path.getsize(file_path) - if file_size > AZURE_MAX_FILE_SIZE: - raise HTTPException( - status_code=400, - detail=f"File size exceeds Azure's limit of {AZURE_MAX_FILE_SIZE_MB}MB", - ) - - api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY - region = request.app.state.config.AUDIO_STT_AZURE_REGION or "eastus" - locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES - base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL - max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS or 3 - - # IF NO LOCALES, USE DEFAULTS - if len(locales) < 2: - locales = [ - "en-US", - "es-ES", - "es-MX", - "fr-FR", - "hi-IN", - "it-IT", - "de-DE", - "en-GB", - "en-IN", - "ja-JP", - "ko-KR", - "pt-BR", - "zh-CN", - ] - locales = ",".join(locales) - - if not api_key or not region: - raise HTTPException( - status_code=400, - detail="Azure API key is required for Azure STT", - ) - - r = None - try: - # Prepare the request - data = { - "definition": json.dumps( - { - "locales": locales.split(","), - "diarization": {"maxSpeakers": max_speakers, "enabled": True}, - } - if locales - else {} - ) - } - - url = ( - base_url or f"https://{region}.api.cognitive.microsoft.com" - ) + "/speechtotext/transcriptions:transcribe?api-version=2024-11-15" - - # Use context manager to ensure file is properly closed - with open(file_path, "rb") as audio_file: - r = requests.post( - url=url, - files={"audio": audio_file}, - data=data, - headers={ - "Ocp-Apim-Subscription-Key": api_key, - }, - ) - - r.raise_for_status() - response = r.json() - - # Extract transcript from response - if not response.get("combinedPhrases"): - raise ValueError("No transcription found in response") - - # Get the full transcript from combinedPhrases - transcript = response["combinedPhrases"][0].get("text", "").strip() - if not transcript: - raise ValueError("Empty transcript in response") - - data = {"text": transcript} - - # Save transcript to json file (consistent with other providers) - transcript_file = f"{file_dir}/{id}.json" - with open(transcript_file, "w") as f: - json.dump(data, f) - - log.debug(data) - return data - - except (KeyError, IndexError, ValueError) as e: - log.exception("Error parsing Azure response") - raise HTTPException( - status_code=500, - detail=f"Failed to parse Azure response: {str(e)}", - ) - except requests.exceptions.RequestException as e: - log.exception(e) - detail = None - - try: - if r is not None and r.status_code != 200: - res = r.json() - if "error" in res: - detail = f"External: {res['error'].get('message', '')}" - except Exception: - detail = f"External: {e}" - - raise HTTPException( - status_code=getattr(r, "status_code", 500) if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - elif request.app.state.config.STT_ENGINE == "mistral": - # Check file exists - if not os.path.exists(file_path): - raise HTTPException(status_code=400, detail="Audio file not found") - - # Check file size - file_size = os.path.getsize(file_path) - if file_size > MAX_FILE_SIZE: - raise HTTPException( - status_code=400, - detail=f"File size exceeds limit of {MAX_FILE_SIZE_MB}MB", - ) - - api_key = request.app.state.config.AUDIO_STT_MISTRAL_API_KEY - api_base_url = ( - request.app.state.config.AUDIO_STT_MISTRAL_API_BASE_URL - or "https://api.mistral.ai/v1" - ) - use_chat_completions = ( - request.app.state.config.AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS - ) - - if not api_key: - raise HTTPException( - status_code=400, - detail="Mistral API key is required for Mistral STT", - ) - - r = None - try: - # Use voxtral-mini-latest as the default model for transcription - model = request.app.state.config.STT_MODEL or "voxtral-mini-latest" - - log.info( - f"Mistral STT - model: {model}, " - f"method: {'chat_completions' if use_chat_completions else 'transcriptions'}" - ) - - if use_chat_completions: - # Use chat completions API with audio input - # This method requires mp3 or wav format - audio_file_to_use = file_path - - if is_audio_conversion_required(file_path): - log.debug("Converting audio to mp3 for chat completions API") - converted_path = convert_audio_to_mp3(file_path) - if converted_path: - audio_file_to_use = converted_path - else: - log.error("Audio conversion failed") - raise HTTPException( - status_code=500, - detail="Audio conversion failed. Chat completions API requires mp3 or wav format.", - ) - - # Read and encode audio file as base64 - with open(audio_file_to_use, "rb") as audio_file: - audio_base64 = base64.b64encode(audio_file.read()).decode("utf-8") - - # Prepare chat completions request - url = f"{api_base_url}/chat/completions" - - # Add language instruction if specified - language = metadata.get("language", None) if metadata else None - if language: - text_instruction = f"Transcribe this audio exactly as spoken in {language}. Do not translate it." + if r is not None and r.status != 200: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' else: - text_instruction = "Transcribe this audio exactly as spoken in its original language. Do not translate it to another language." - - payload = { - "model": model, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "input_audio", - "input_audio": audio_base64, - }, - {"type": "text", "text": text_instruction}, - ], - } - ], - } - - r = requests.post( - url=url, - json=payload, - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - ) - - r.raise_for_status() - response = r.json() - - # Extract transcript from chat completion response - transcript = ( - response.get("choices", [{}])[0] - .get("message", {}) - .get("content", "") - .strip() - ) - if not transcript: - raise ValueError("Empty transcript in response") - - data = {"text": transcript} - - else: - # Use dedicated transcriptions API - url = f"{api_base_url}/audio/transcriptions" - - # Determine the MIME type - mime_type, _ = mimetypes.guess_type(file_path) - if not mime_type: - mime_type = "audio/webm" - - # Use context manager to ensure file is properly closed - with open(file_path, "rb") as audio_file: - files = {"file": (filename, audio_file, mime_type)} - data_form = {"model": model} - - # Add language if specified in metadata - language = metadata.get("language", None) if metadata else None - if language: - data_form["language"] = language - - r = requests.post( - url=url, - files=files, - data=data_form, - headers={ - "Authorization": f"Bearer {api_key}", - }, - ) - - r.raise_for_status() - response = r.json() - - # Extract transcript from response - transcript = response.get("text", "").strip() - if not transcript: - raise ValueError("Empty transcript in response") - - data = {"text": transcript} - - # Save transcript to json file (consistent with other providers) - transcript_file = f"{file_dir}/{id}.json" - with open(transcript_file, "w") as f: - json.dump(data, f) - - log.debug(data) - return data - - except ValueError as e: - log.exception("Error parsing Mistral response") - raise HTTPException( - status_code=500, - detail=f"Failed to parse Mistral response: {str(e)}", - ) - except requests.exceptions.RequestException as e: - log.exception(e) - detail = None - - try: - if r is not None and r.status_code != 200: - res = r.json() - if "error" in res: - detail = f"External: {res['error'].get('message', '')}" - else: - detail = f"External: {r.text}" - except Exception: - detail = f"External: {e}" - - raise HTTPException( - status_code=getattr(r, "status_code", 500) if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - -def transcribe( - request: Request, file_path: str, metadata: Optional[dict] = None, user=None -): - log.info(f"transcribe: {file_path} {metadata}") - - if is_audio_conversion_required(file_path): - file_path = convert_audio_to_mp3(file_path) - - try: - file_path = compress_audio(file_path) - except Exception as e: - log.exception(e) - - # Always produce a list of chunk paths (could be one entry if small) - try: - chunk_paths = split_audio(file_path, MAX_FILE_SIZE) - print(f"Chunk paths: {chunk_paths}") - except Exception as e: - log.exception(e) + detail = f'External: {await r.text()}' + except Exception: + detail = f'External: {e}' raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + status_code=e.status if e.status else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', ) + +async def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None, user=None): + log.info(f'transcribe: {file_path} {metadata}') + + if BYPASS_PYDUB_PREPROCESSING: + log.info('Bypassing pydub preprocessing (BYPASS_PYDUB_PREPROCESSING=true)') + chunk_paths = [file_path] + else: + if is_audio_conversion_required(file_path): + file_path = await asyncio.to_thread(convert_audio_to_mp3, file_path) + if not file_path: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Audio conversion failed. The audio file may be corrupted or empty.', + ) + + try: + file_path = await asyncio.to_thread(compress_audio, file_path) + except Exception as e: + log.exception(e) + + # Always produce a list of chunk paths (could be one entry if small) + try: + chunk_paths = await asyncio.to_thread(split_audio, file_path, MAX_FILE_SIZE) + print(f'Chunk paths: {chunk_paths}') + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + results = [] try: - with ThreadPoolExecutor() as executor: - # Submit tasks for each chunk_path - futures = [ - executor.submit( - transcription_handler, request, chunk_path, metadata, user + tasks = [ + transcription_handler(request, chunk_path, metadata, user) + for chunk_path in chunk_paths + ] + for coro in asyncio.as_completed(tasks): + try: + results.append(await coro) + except HTTPException: + raise + except Exception as transcribe_exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Error transcribing chunk: {transcribe_exc}', ) - for chunk_path in chunk_paths - ] - # Gather results as they complete - for future in futures: - try: - results.append(future.result()) - except Exception as transcribe_exc: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Error transcribing chunk: {transcribe_exc}", - ) finally: # Clean up only the temporary chunks, never the original file for chunk_path in chunk_paths: @@ -1096,22 +1065,20 @@ def transcribe( pass return { - "text": " ".join([result["text"] for result in results]), + 'text': ' '.join([result['text'] for result in results]), } def compress_audio(file_path): if os.path.getsize(file_path) > MAX_FILE_SIZE: - id = os.path.splitext(os.path.basename(file_path))[ - 0 - ] # Handles names with multiple dots + id = os.path.splitext(os.path.basename(file_path))[0] # Handles names with multiple dots file_dir = os.path.dirname(file_path) audio = AudioSegment.from_file(file_path) audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio - compressed_path = os.path.join(file_dir, f"{id}_compressed.mp3") - audio.export(compressed_path, format="mp3", bitrate="32k") + compressed_path = os.path.join(file_dir, f'{id}_compressed.mp3') + audio.export(compressed_path, format='mp3', bitrate='32k') # log.debug(f"Compressed audio to {compressed_path}") # Uncomment if log is defined return compressed_path @@ -1119,7 +1086,7 @@ def compress_audio(file_path): return file_path -def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"): +def split_audio(file_path, max_bytes, format='mp3', bitrate='32k'): """ Splits audio into chunks not exceeding max_bytes. Returns a list of chunk file paths. If audio fits, returns list with original path. @@ -1142,7 +1109,7 @@ def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"): while start < duration_ms: end = min(start + approx_chunk_ms, duration_ms) chunk = audio[start:end] - chunk_path = f"{base}_chunk_{i}.{format}" + chunk_path = f'{base}_chunk_{i}.{format}' chunk.export(chunk_path, format=format, bitrate=bitrate) # Reduce chunk duration if still too large @@ -1153,7 +1120,7 @@ def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"): if os.path.getsize(chunk_path) > max_bytes: os.remove(chunk_path) - raise Exception("Audio chunk cannot be reduced below max file size.") + raise Exception('Audio chunk cannot be reduced below max file size.') chunks.append(chunk_path) start = end @@ -1162,24 +1129,22 @@ def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"): return chunks -@router.post("/transcriptions") -def transcription( +@router.post('/transcriptions') +async def transcription( request: Request, file: UploadFile = File(...), language: Optional[str] = Form(None), user=Depends(get_verified_user), ): - if user.role != "admin" and not has_permission( - user.id, "chat.stt", request.app.state.config.USER_PERMISSIONS + if user.role != 'admin' and not await has_permission( + user.id, 'chat.stt', request.app.state.config.USER_PERMISSIONS ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - log.info(f"file.content_type: {file.content_type}") - stt_supported_content_types = getattr( - request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", [] - ) + log.info(f'file.content_type: {file.content_type}') + stt_supported_content_types = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', []) if not strict_match_mime_type(stt_supported_content_types, file.content_type): raise HTTPException( @@ -1188,199 +1153,219 @@ def transcription( ) try: - ext = file.filename.split(".")[-1] + safe_name = os.path.basename(file.filename) if file.filename else '' + ext = safe_name.rsplit('.', 1)[-1].lower() if '.' in safe_name else '' + + allowed_extensions = getattr(request.app.state.config, 'STT_ALLOWED_EXTENSIONS', []) + if allowed_extensions and ext not in allowed_extensions: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Invalid audio file extension', + ) + id = uuid.uuid4() - filename = f"{id}.{ext}" - contents = file.file.read() + filename = f'{id}.{ext}' + contents = await file.read() - file_dir = f"{CACHE_DIR}/audio/transcriptions" + file_dir = os.path.join(CACHE_DIR, 'audio', 'transcriptions') os.makedirs(file_dir, exist_ok=True) - file_path = f"{file_dir}/{filename}" + file_path = os.path.join(file_dir, filename) - with open(file_path, "wb") as f: + # Defense-in-depth: ensure resolved path stays within intended directory + if not os.path.realpath(file_path).startswith(os.path.realpath(file_dir)): + raise ValueError('Invalid file path detected') + + with open(file_path, 'wb') as f: f.write(contents) try: metadata = None if language: - metadata = {"language": language} + metadata = {'language': language} - result = transcribe(request, file_path, metadata, user) + result = await transcribe(request, file_path, metadata, user) return { **result, - "filename": os.path.basename(file_path), + 'filename': os.path.basename(file_path), } + except HTTPException: + raise except Exception as e: log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail='Transcription failed.', ) + except HTTPException: + raise except Exception as e: log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + detail='Transcription failed.', ) -def get_available_models(request: Request) -> list[dict]: +async def get_available_models(request: Request) -> list[dict]: + """Return the list of available TTS models for the configured engine.""" available_models = [] - if request.app.state.config.TTS_ENGINE == "openai": - # Use custom endpoint if not using the official OpenAI API URL - if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith( - "https://api.openai.com" - ): - try: - response = requests.get( - f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models" - ) - response.raise_for_status() - data = response.json() - available_models = data.get("models", []) - except Exception as e: - log.error(f"Error fetching models from custom endpoint: {str(e)}") - available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}] - else: - available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}] - elif request.app.state.config.TTS_ENGINE == "elevenlabs": - try: - response = requests.get( - f"{ELEVENLABS_API_BASE_URL}/v1/models", - headers={ - "xi-api-key": request.app.state.config.TTS_API_KEY, - "Content-Type": "application/json", - }, - timeout=5, - ) - response.raise_for_status() - models = response.json() + engine = request.app.state.config.TTS_ENGINE + _timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + + if engine == 'openai': + base_url = request.app.state.config.TTS_OPENAI_API_BASE_URL + if not base_url.startswith('https://api.openai.com'): + session = await get_session() + try: + async with session.get( + f'{base_url}/audio/models', + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + data = await resp.json() + available_models = data.get('models', []) + except Exception as e: + log.debug(f'/audio/models not available, trying /models fallback: {e}') + try: + async with session.get( + f'{base_url}/models', + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + data = await resp.json() + available_models = data.get('data', data.get('models', [])) + except Exception as e2: + log.error(f'Error fetching models from custom endpoint: {e2}') + available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}] + else: + available_models = [{'id': 'tts-1'}, {'id': 'tts-1-hd'}] + + elif engine == 'elevenlabs': + try: + session = await get_session() + async with session.get( + f'{ELEVENLABS_API_BASE_URL}/v1/models', + headers={ + 'xi-api-key': request.app.state.config.TTS_API_KEY, + 'Content-Type': 'application/json', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + models = await resp.json() + available_models = [{'name': m['name'], 'id': m['model_id']} for m in models] + except Exception as e: + log.error(f'Error fetching models: {e}') + + elif engine == 'mistral': + available_models = [{'id': 'voxtral-mini-tts-2603'}] - available_models = [ - {"name": model["name"], "id": model["model_id"]} for model in models - ] - except requests.RequestException as e: - log.error(f"Error fetching voices: {str(e)}") return available_models -@router.get("/models") +@router.get('/models') async def get_models(request: Request, user=Depends(get_verified_user)): - return {"models": get_available_models(request)} + return {'models': await get_available_models(request)} + +_OPENAI_DEFAULT_VOICES = { + 'alloy': 'alloy', 'echo': 'echo', 'fable': 'fable', + 'onyx': 'onyx', 'nova': 'nova', 'shimmer': 'shimmer', +} -def get_available_voices(request) -> dict: - """Returns {voice_id: voice_name} dict""" - available_voices = {} - if request.app.state.config.TTS_ENGINE == "openai": - # Use custom endpoint if not using the official OpenAI API URL - if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith( - "https://api.openai.com" - ): +async def get_available_voices(request) -> dict: + """Return ``{voice_id: voice_name}`` for the configured TTS engine.""" + engine = request.app.state.config.TTS_ENGINE + _timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + + if engine == 'openai': + base_url = request.app.state.config.TTS_OPENAI_API_BASE_URL + if not base_url.startswith('https://api.openai.com'): try: - response = requests.get( - f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices" - ) - response.raise_for_status() - data = response.json() - voices_list = data.get("voices", []) - available_voices = {voice["id"]: voice["name"] for voice in voices_list} + session = await get_session() + async with session.get( + f'{base_url}/audio/voices', + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + data = await resp.json() + return {v['id']: v['name'] for v in data.get('voices', [])} except Exception as e: - log.error(f"Error fetching voices from custom endpoint: {str(e)}") - available_voices = { - "alloy": "alloy", - "echo": "echo", - "fable": "fable", - "onyx": "onyx", - "nova": "nova", - "shimmer": "shimmer", - } - else: - available_voices = { - "alloy": "alloy", - "echo": "echo", - "fable": "fable", - "onyx": "onyx", - "nova": "nova", - "shimmer": "shimmer", - } - elif request.app.state.config.TTS_ENGINE == "elevenlabs": + log.error(f'Error fetching voices from custom endpoint: {e}') + return dict(_OPENAI_DEFAULT_VOICES) + return dict(_OPENAI_DEFAULT_VOICES) + + if engine == 'elevenlabs': try: - available_voices = get_elevenlabs_voices( - api_key=request.app.state.config.TTS_API_KEY - ) - except Exception: - # Avoided @lru_cache with exception - pass - elif request.app.state.config.TTS_ENGINE == "azure": + session = await get_session() + async with session.get( + f'{ELEVENLABS_API_BASE_URL}/v1/voices', + headers={ + 'xi-api-key': request.app.state.config.TTS_API_KEY, + 'Content-Type': 'application/json', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + voices_data = await resp.json() + return {v['voice_id']: v['name'] for v in voices_data.get('voices', [])} + except Exception as e: + log.error(f'Error fetching ElevenLabs voices: {e}') + return {} + + if engine == 'azure': try: region = request.app.state.config.TTS_AZURE_SPEECH_REGION base_url = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL - url = ( - base_url or f"https://{region}.tts.speech.microsoft.com" - ) + "/cognitiveservices/voices/list" - headers = { - "Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY - } + url = (base_url or f'https://{region}.tts.speech.microsoft.com') + '/cognitiveservices/voices/list' - response = requests.get(url, headers=headers) - response.raise_for_status() - voices = response.json() + session = await get_session() + async with session.get( + url, + headers={'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + voices = await resp.json() + return {v['ShortName']: f'{v["DisplayName"]} ({v["ShortName"]})' for v in voices} + except Exception as e: + log.error(f'Error fetching Azure voices: {e}') + return {} - for voice in voices: - available_voices[voice["ShortName"]] = ( - f"{voice['DisplayName']} ({voice['ShortName']})" - ) - except requests.RequestException as e: - log.error(f"Error fetching voices: {str(e)}") + if engine == 'mistral': + api_key = request.app.state.config.TTS_MISTRAL_API_KEY + api_base_url = request.app.state.config.TTS_MISTRAL_API_BASE_URL or 'https://api.mistral.ai/v1' + if api_key: + try: + session = await get_session() + async with session.get( + f'{api_base_url}/audio/voices', + headers={'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout, + ) as resp: + resp.raise_for_status() + voices_data = await resp.json() + items = voices_data.get('items', []) if isinstance(voices_data, dict) else voices_data + result = {} + for v in items: + if isinstance(v, dict): + vid = v.get('voice_id', v.get('id', '')) + if vid: + result[vid] = v.get('name', vid) + return result + except Exception as e: + log.error(f'Error fetching Mistral voices: {e}') - return available_voices + return {} -@lru_cache -def get_elevenlabs_voices(api_key: str) -> dict: - """ - Note, set the following in your .env file to use Elevenlabs: - AUDIO_TTS_ENGINE=elevenlabs - AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key - AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices - AUDIO_TTS_MODEL=eleven_multilingual_v2 - """ - - try: - # TODO: Add retries - response = requests.get( - f"{ELEVENLABS_API_BASE_URL}/v1/voices", - headers={ - "xi-api-key": api_key, - "Content-Type": "application/json", - }, - ) - response.raise_for_status() - voices_data = response.json() - - voices = {} - for voice in voices_data.get("voices", []): - voices[voice["voice_id"]] = voice["name"] - except requests.RequestException as e: - # Avoid @lru_cache with exception - log.error(f"Error fetching voices: {str(e)}") - raise RuntimeError(f"Error fetching voices: {str(e)}") - - return voices - - -@router.get("/voices") +@router.get('/voices') async def get_voices(request: Request, user=Depends(get_verified_user)): - return { - "voices": [ - {"id": k, "name": v} for k, v in get_available_voices(request).items() - ] - } + return {'voices': [{'id': k, 'name': v} for k, v in (await get_available_voices(request)).items()]} diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 30d4ebe4cc..434a6349de 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -1,91 +1,147 @@ -import re -import uuid -import time +from __future__ import annotations + +import asyncio import datetime import logging -from aiohttp import ClientSession +import re +import time import urllib +import uuid +from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS +from typing import List, Optional - +from aiohttp import ClientSession +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import JSONResponse, RedirectResponse, Response +from ldap3 import NONE, Connection, Server, Tls +from ldap3.utils.conv import escape_filter_chars +from open_webui.config import ( + ENABLE_LDAP, + ENABLE_OAUTH_SIGNUP, + ENABLE_PASSWORD_AUTH, + OAUTH_MERGE_ACCOUNTS_BY_EMAIL, + OAUTH_PROVIDERS, + OPENID_END_SESSION_ENDPOINT, + OPENID_PROVIDER_URL, +) +from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + ENABLE_INITIAL_ADMIN_SIGNUP, + ENABLE_OAUTH_TOKEN_EXCHANGE, + WEBUI_AUTH, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_GROUPS_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + WEBUI_AUTH_TRUSTED_ROLE_HEADER, +) +from open_webui.internal.db import get_async_session from open_webui.models.auths import ( AddUserForm, ApiKey, Auths, - Token, LdapForm, SigninForm, SigninResponse, SignupForm, + Token, UpdatePasswordForm, ) -from open_webui.models.users import ( - UserProfileImageResponse, - Users, - UpdateProfileForm, - UserStatus, -) from open_webui.models.groups import Groups from open_webui.models.oauth_sessions import OAuthSessions - -from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES -from open_webui.env import ( - WEBUI_AUTH, - WEBUI_AUTH_TRUSTED_EMAIL_HEADER, - WEBUI_AUTH_TRUSTED_NAME_HEADER, - WEBUI_AUTH_TRUSTED_GROUPS_HEADER, - WEBUI_AUTH_COOKIE_SAME_SITE, - WEBUI_AUTH_COOKIE_SECURE, - WEBUI_AUTH_SIGNOUT_REDIRECT_URL, - ENABLE_INITIAL_ADMIN_SIGNUP, +from open_webui.models.users import ( + UpdateProfileForm, + UserModel, + UserProfileImageResponse, + Users, + UserStatus, ) -from fastapi import APIRouter, Depends, HTTPException, Request, status -from fastapi.responses import RedirectResponse, Response, JSONResponse -from open_webui.config import ( - OPENID_PROVIDER_URL, - ENABLE_OAUTH_SIGNUP, - ENABLE_LDAP, - ENABLE_PASSWORD_AUTH, -) -from pydantic import BaseModel - -from open_webui.utils.misc import parse_duration, validate_email_format +from open_webui.utils.access_control import get_permissions, has_permission from open_webui.utils.auth import ( - validate_password, - verify_password, - decode_token, - invalidate_token, create_api_key, create_token, + decode_token, get_admin_user, - get_verified_user, get_current_user, - get_password_hash, get_http_authorization_cred, + get_password_hash, + get_verified_user, + invalidate_token, + validate_password, + verify_password, ) -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session -from open_webui.utils.webhook import post_webhook -from open_webui.utils.access_control import get_permissions, has_permission from open_webui.utils.groups import apply_default_group_assignment - -from open_webui.utils.redis import get_redis_client +from open_webui.utils.misc import parse_duration, validate_email_format +from open_webui.utils.oauth import auth_manager_config from open_webui.utils.rate_limit import RateLimiter - - -from typing import Optional, List - -from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS - -from ldap3 import Server, Connection, NONE, Tls -from ldap3.utils.conv import escape_filter_chars +from open_webui.utils.redis import get_redis_client +from open_webui.utils.webhook import post_webhook +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter() log = logging.getLogger(__name__) -signin_rate_limiter = RateLimiter( - redis_client=get_redis_client(), limit=5 * 3, window=60 * 3 -) +# Forgive us our failed attempts, as we forgive those +# who exceed their allotted rate against this gate. +signin_rate_limiter = RateLimiter(redis_client=get_redis_client(), limit=5 * 3, window=60 * 3) + + +async def create_session_response( + request: Request, user, db, response: Response = None, set_cookie: bool = False +) -> dict: + """ + Create JWT token and build session response for a user. + Shared helper for signin, signup, ldap_auth, add_user, and token_exchange endpoints. + + Args: + request: FastAPI request object + user: User object + db: Database session + response: FastAPI response object (required if set_cookie is True) + set_cookie: Whether to set the auth cookie on the response + """ + expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) + expires_at = None + if expires_delta: + expires_at = int(time.time()) + int(expires_delta.total_seconds()) + + token = create_token( + data={'id': user.id}, + expires_delta=expires_delta, + ) + + if set_cookie and response: + datetime_expires_at = datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) if expires_at else None + max_age = int(expires_delta.total_seconds()) if expires_delta else None + response.set_cookie( + key='token', + value=token, + expires=datetime_expires_at, + httponly=True, + samesite=WEBUI_AUTH_COOKIE_SAME_SITE, + secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': max_age} if max_age is not None else {}), + ) + + user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) + + return { + 'token': token, + 'token_type': 'Bearer', + 'expires_at': expires_at, + 'id': user.id, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'profile_image_url': f'/api/v1/users/{user.id}/profile/image', + 'permissions': user_permissions, + } + ############################ # GetSessionUser @@ -93,33 +149,39 @@ signin_rate_limiter = RateLimiter( class SessionUserResponse(Token, UserProfileImageResponse): - expires_at: Optional[int] = None - permissions: Optional[dict] = None + expires_at: int | None = None + permissions: dict | None = None class SessionUserInfoResponse(SessionUserResponse, UserStatus): - bio: Optional[str] = None - gender: Optional[str] = None - date_of_birth: Optional[datetime.date] = None + bio: str | None = None + gender: str | None = None + date_of_birth: datetime.date | None = None -@router.get("/", response_model=SessionUserInfoResponse) +@router.get('/', response_model=SessionUserInfoResponse) async def get_session_user( request: Request, response: Response, user=Depends(get_current_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - - auth_header = request.headers.get("Authorization") - auth_token = get_http_authorization_cred(auth_header) - token = auth_token.credentials - data = decode_token(token) + token = None + auth_header = request.headers.get('Authorization') + if auth_header: + auth_token = get_http_authorization_cred(auth_header) + if auth_token is not None: + token = auth_token.credentials + if token is None: + token = request.cookies.get('token') + if token is None and getattr(request.state, 'token', None): + token = request.state.token.credentials + data = decode_token(token) if token else None expires_at = None if data: - expires_at = data.get("exp") + expires_at = data.get('exp') if (expires_at is not None) and int(time.time()) > expires_at: raise HTTPException( @@ -128,55 +190,53 @@ async def get_session_user( ) # Set the cookie token + max_age = int(expires_at - time.time()) if expires_at else None response.set_cookie( - key="token", + key='token', value=token, - expires=( - datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) - if expires_at - else None - ), + expires=(datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) if expires_at else None), httponly=True, # Ensures the cookie is not accessible via JavaScript samesite=WEBUI_AUTH_COOKIE_SAME_SITE, secure=WEBUI_AUTH_COOKIE_SECURE, + **({'max_age': max_age} if max_age is not None else {}), ) - user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS, db=db - ) + user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) - return { - "token": token, - "token_type": "Bearer", - "expires_at": expires_at, - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - "profile_image_url": user.profile_image_url, - "bio": user.bio, - "gender": user.gender, - "date_of_birth": user.date_of_birth, - "status_emoji": user.status_emoji, - "status_message": user.status_message, - "status_expires_at": user.status_expires_at, - "permissions": user_permissions, + response_data = { + 'token': token, + 'token_type': 'Bearer', + 'expires_at': expires_at, + 'id': user.id, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'profile_image_url': user.profile_image_url, + 'bio': user.bio, + 'gender': user.gender, + 'date_of_birth': user.date_of_birth, + 'status_emoji': user.status_emoji, + 'status_message': user.status_message, + 'status_expires_at': user.status_expires_at, + 'permissions': user_permissions, } + return response_data + ############################ # Update Profile ############################ -@router.post("/update/profile", response_model=UserProfileImageResponse) +@router.post('/update/profile', response_model=UserProfileImageResponse) async def update_profile( form_data: UpdateProfileForm, session_user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if session_user: - user = Users.update_user_by_id( + user = await Users.update_user_by_id( session_user.id, form_data.model_dump(), db=db, @@ -198,19 +258,19 @@ class UpdateTimezoneForm(BaseModel): timezone: str -@router.post("/update/timezone") +@router.post('/update/timezone') async def update_timezone( form_data: UpdateTimezoneForm, session_user=Depends(get_current_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if session_user: - Users.update_user_by_id( + await Users.update_user_by_id( session_user.id, - {"timezone": form_data.timezone}, + {'timezone': form_data.timezone}, db=db, ) - return {"status": True} + return {'status': True} else: raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) @@ -220,16 +280,17 @@ async def update_timezone( ############################ -@router.post("/update/password", response_model=bool) +@router.post('/update/password', response_model=bool) async def update_password( form_data: UpdatePasswordForm, session_user=Depends(get_current_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): + # Trusted-header auth mode delegates passwords to the reverse proxy if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: - raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACTION_PROHIBITED) if session_user: - user = Auths.authenticate_user( + user = await Auths.authenticate_user( session_user.email, lambda pw: verify_password(form_data.password, pw), db=db, @@ -237,11 +298,11 @@ async def update_password( if user: try: - validate_password(form_data.password) + validate_password(form_data.new_password) except Exception as e: raise HTTPException(400, detail=str(e)) hashed = get_password_hash(form_data.new_password) - return Auths.update_user_password_by_id(user.id, hashed, db=db) + return await Auths.update_user_password_by_id(user.id, hashed, db=db) else: raise HTTPException(400, detail=ERROR_MESSAGES.INCORRECT_PASSWORD) else: @@ -251,16 +312,16 @@ async def update_password( ############################ # LDAP Authentication ############################ -@router.post("/ldap", response_model=SessionUserResponse) +@router.post('/ldap', response_model=SessionUserResponse) async def ldap_auth( request: Request, response: Response, form_data: LdapForm, - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): # Security checks FIRST - before loading any config if not request.app.state.config.ENABLE_LDAP: - raise HTTPException(400, detail="LDAP authentication is not enabled") + raise HTTPException(400, detail='LDAP authentication is not enabled') if not ENABLE_PASSWORD_AUTH: raise HTTPException( @@ -268,6 +329,14 @@ async def ldap_auth( detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) + # Reject empty passwords before attempting the LDAP bind. + # Per RFC 4513 §5.1.2, a Simple Bind with a non-empty DN but empty + # password is "unauthenticated simple authentication" — many LDAP + # servers (OpenLDAP default, some AD configs) return success for these, + # which would grant access without valid credentials. + if not form_data.password or not form_data.password.strip(): + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + # NOW load LDAP config variables LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST @@ -280,14 +349,8 @@ async def ldap_auth( LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE - LDAP_VALIDATE_CERT = ( - CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE - ) - LDAP_CIPHERS = ( - request.app.state.config.LDAP_CIPHERS - if request.app.state.config.LDAP_CIPHERS - else "ALL" - ) + LDAP_VALIDATE_CERT = CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE + LDAP_CIPHERS = request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS else 'ALL' try: tls = Tls( @@ -297,8 +360,8 @@ async def ldap_auth( ciphers=LDAP_CIPHERS, ) except Exception as e: - log.error(f"TLS configuration error: {str(e)}") - raise HTTPException(400, detail="Failed to configure TLS for LDAP connection.") + log.error(f'TLS configuration error: {str(e)}') + raise HTTPException(400, detail='Failed to configure TLS for LDAP connection.') try: server = Server( @@ -312,43 +375,38 @@ async def ldap_auth( server, LDAP_APP_DN, LDAP_APP_PASSWORD, - auto_bind="NONE", - authentication="SIMPLE" if LDAP_APP_DN else "ANONYMOUS", + auto_bind='NONE', + authentication='SIMPLE' if LDAP_APP_DN else 'ANONYMOUS', ) - if not connection_app.bind(): - raise HTTPException(400, detail="Application account bind failed") + if not await asyncio.to_thread(connection_app.bind): + raise HTTPException(400, detail='Application account bind failed') - ENABLE_LDAP_GROUP_MANAGEMENT = ( - request.app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT - ) + ENABLE_LDAP_GROUP_MANAGEMENT = request.app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT ENABLE_LDAP_GROUP_CREATION = request.app.state.config.ENABLE_LDAP_GROUP_CREATION LDAP_ATTRIBUTE_FOR_GROUPS = request.app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS search_attributes = [ - f"{LDAP_ATTRIBUTE_FOR_USERNAME}", - f"{LDAP_ATTRIBUTE_FOR_MAIL}", - "cn", + f'{LDAP_ATTRIBUTE_FOR_USERNAME}', + f'{LDAP_ATTRIBUTE_FOR_MAIL}', + 'cn', ] if ENABLE_LDAP_GROUP_MANAGEMENT: - search_attributes.append(f"{LDAP_ATTRIBUTE_FOR_GROUPS}") - log.info( - f"LDAP Group Management enabled. Adding {LDAP_ATTRIBUTE_FOR_GROUPS} to search attributes" - ) - log.info(f"LDAP search attributes: {search_attributes}") + search_attributes.append(f'{LDAP_ATTRIBUTE_FOR_GROUPS}') + log.info(f'LDAP Group Management enabled. Adding {LDAP_ATTRIBUTE_FOR_GROUPS} to search attributes') + log.info(f'LDAP search attributes: {search_attributes}') - search_success = connection_app.search( + search_success = await asyncio.to_thread( + connection_app.search, search_base=LDAP_SEARCH_BASE, - search_filter=f"(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})", + search_filter=f'(&({LDAP_ATTRIBUTE_FOR_USERNAME}={escape_filter_chars(form_data.user.lower())}){LDAP_SEARCH_FILTERS})', attributes=search_attributes, ) if not search_success or not connection_app.entries: - raise HTTPException(400, detail="User not found in the LDAP server") + raise HTTPException(400, detail='User not found in the LDAP server') entry = connection_app.entries[0] - entry_username = entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"].value - email = entry[ - f"{LDAP_ATTRIBUTE_FOR_MAIL}" - ].value # retrieve the Attribute value + entry_username = entry[f'{LDAP_ATTRIBUTE_FOR_USERNAME}'].value + email = entry[f'{LDAP_ATTRIBUTE_FOR_MAIL}'].value # retrieve the Attribute value username_list = [] # list of usernames from LDAP attribute if isinstance(entry_username, list): @@ -358,7 +416,7 @@ async def ldap_auth( # TODO: support multiple emails if LDAP returns a list if not email: - raise HTTPException(400, "User does not have a valid email address.") + raise HTTPException(400, 'User does not have a valid email address.') elif isinstance(email, str): email = email.lower() elif isinstance(email, list): @@ -366,47 +424,43 @@ async def ldap_auth( else: email = str(email).lower() - cn = str(entry["cn"]) # common name + cn = str(entry['cn']) # common name user_dn = entry.entry_dn # user distinguished name user_groups = [] if ENABLE_LDAP_GROUP_MANAGEMENT and LDAP_ATTRIBUTE_FOR_GROUPS in entry: group_dns = entry[LDAP_ATTRIBUTE_FOR_GROUPS] - log.info(f"LDAP raw group DNs for user {username_list}: {group_dns}") + log.info(f'LDAP raw group DNs for user {username_list}: {group_dns}') if group_dns: - log.info(f"LDAP group_dns original: {group_dns}") - log.info(f"LDAP group_dns type: {type(group_dns)}") - log.info(f"LDAP group_dns length: {len(group_dns)}") + log.info(f'LDAP group_dns original: {group_dns}') + log.info(f'LDAP group_dns type: {type(group_dns)}') + log.info(f'LDAP group_dns length: {len(group_dns)}') - if hasattr(group_dns, "value"): + if hasattr(group_dns, 'value'): group_dns = group_dns.value - log.info(f"Extracted .value property: {group_dns}") - elif hasattr(group_dns, "__iter__") and not isinstance( - group_dns, (str, bytes) - ): + log.info(f'Extracted .value property: {group_dns}') + elif hasattr(group_dns, '__iter__') and not isinstance(group_dns, (str, bytes)): group_dns = list(group_dns) - log.info(f"Converted to list: {group_dns}") + log.info(f'Converted to list: {group_dns}') if isinstance(group_dns, list): group_dns = [str(item) for item in group_dns] else: group_dns = [str(group_dns)] - log.info( - f"LDAP group_dns after processing - type: {type(group_dns)}, length: {len(group_dns)}" - ) + log.info(f'LDAP group_dns after processing - type: {type(group_dns)}, length: {len(group_dns)}') for group_idx, group_dn in enumerate(group_dns): group_dn = str(group_dn) - log.info(f"Processing group DN #{group_idx + 1}: {group_dn}") + log.info(f'Processing group DN #{group_idx + 1}: {group_dn}') try: group_cn = None - for item in group_dn.split(","): + for item in group_dn.split(','): item = item.strip() - if item.upper().startswith("CN="): + if item.upper().startswith('CN='): group_cn = item[3:] break @@ -414,22 +468,16 @@ async def ldap_auth( user_groups.append(group_cn) else: - log.warning( - f"Could not extract CN from group DN: {group_dn}" - ) + log.warning(f'Could not extract CN from group DN: {group_dn}') except Exception as e: - log.warning( - f"Failed to extract group name from DN {group_dn}: {e}" - ) + log.warning(f'Failed to extract group name from DN {group_dn}: {e}') - log.info( - f"LDAP groups for user {username_list}: {user_groups} (total: {len(user_groups)})" - ) + log.info(f'LDAP groups for user {username_list}: {user_groups} (total: {len(user_groups)})') else: - log.info(f"No groups found for user {username_list}") + log.info(f'No groups found for user {username_list}') elif ENABLE_LDAP_GROUP_MANAGEMENT: log.warning( - f"LDAP Group Management enabled but {LDAP_ATTRIBUTE_FOR_GROUPS} attribute not found in user entry" + f'LDAP Group Management enabled but {LDAP_ATTRIBUTE_FOR_GROUPS} attribute not found in user entry' ) if username_list and form_data.user.lower() in username_list: @@ -437,114 +485,78 @@ async def ldap_auth( server, user_dn, form_data.password, - auto_bind="NONE", - authentication="SIMPLE", + auto_bind='NONE', + authentication='SIMPLE', ) - if not connection_user.bind(): - raise HTTPException(400, "Authentication failed.") + if not await asyncio.to_thread(connection_user.bind): + raise HTTPException(400, 'Authentication failed.') - user = Users.get_user_by_email(email, db=db) + user = await Users.get_user_by_email(email, db=db) if not user: try: - role = ( - "admin" - if not Users.has_users(db=db) - else request.app.state.config.DEFAULT_USER_ROLE - ) - - user = Auths.insert_new_auth( + # Insert with default role first to avoid TOCTOU race on + # first-user registration. Matches signup_handler pattern. + user = await Auths.insert_new_auth( email=email, password=str(uuid.uuid4()), name=cn, - role=role, + role=request.app.state.config.DEFAULT_USER_ROLE, db=db, ) if not user: - raise HTTPException( - 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR - ) + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) - apply_default_group_assignment( + # Atomically check if this is the only user *after* the + # insert. Only the single user present should become admin. + if await Users.get_num_users(db=db) == 1: + await Users.update_user_role_by_id(user.id, 'admin', db=db) + user = await Users.get_user_by_id(user.id, db=db) + + await apply_default_group_assignment( request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db, ) + if request.app.state.config.WEBHOOK_URL: + await post_webhook( + request.app.state.WEBUI_NAME, + request.app.state.config.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + 'action': 'signup', + 'message': WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + 'user': user.model_dump_json(exclude_none=True), + }, + ) + except HTTPException: raise except Exception as err: - log.error(f"LDAP user creation error: {str(err)}") - raise HTTPException( - 500, detail="Internal error occurred during LDAP user creation." - ) + log.error(f'LDAP user creation error: {str(err)}') + raise HTTPException(500, detail='Internal error occurred during LDAP user creation.') - user = Auths.authenticate_user_by_email(email, db=db) + user = await Auths.authenticate_user_by_email(email, db=db) if user: - expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) - expires_at = None - if expires_delta: - expires_at = int(time.time()) + int(expires_delta.total_seconds()) - - token = create_token( - data={"id": user.id}, - expires_delta=expires_delta, - ) - - # Set the cookie token - response.set_cookie( - key="token", - value=token, - expires=( - datetime.datetime.fromtimestamp( - expires_at, datetime.timezone.utc - ) - if expires_at - else None - ), - httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_AUTH_COOKIE_SAME_SITE, - secure=WEBUI_AUTH_COOKIE_SECURE, - ) - - user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS, db=db - ) - - if ( - user.role != "admin" - and ENABLE_LDAP_GROUP_MANAGEMENT - and user_groups - ): + if ENABLE_LDAP_GROUP_MANAGEMENT and user_groups: if ENABLE_LDAP_GROUP_CREATION: - Groups.create_groups_by_group_names(user.id, user_groups, db=db) + await Groups.create_groups_by_group_names(user.id, user_groups, db=db) try: - Groups.sync_groups_by_group_names(user.id, user_groups, db=db) - log.info( - f"Successfully synced groups for user {user.id}: {user_groups}" - ) + await Groups.sync_groups_by_group_names(user.id, user_groups, db=db) + log.info(f'Successfully synced groups for user {user.id}: {user_groups}') except Exception as e: - log.error(f"Failed to sync groups for user {user.id}: {e}") + log.error(f'Failed to sync groups for user {user.id}: {e}') - return { - "token": token, - "token_type": "Bearer", - "expires_at": expires_at, - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - "profile_image_url": user.profile_image_url, - "permissions": user_permissions, - } + return await create_session_response(request, user, db, response, set_cookie=True) else: raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) else: - raise HTTPException(400, "User record mismatch.") + raise HTTPException(400, 'User record mismatch.') except Exception as e: - log.error(f"LDAP authentication error: {str(e)}") - raise HTTPException(400, detail="LDAP authentication failed.") + log.error(f'LDAP authentication error: {str(e)}') + raise HTTPException(400, detail='LDAP authentication failed.') ############################ @@ -552,12 +564,12 @@ async def ldap_auth( ############################ -@router.post("/signin", response_model=SessionUserResponse) +@router.post('/signin', response_model=SessionUserResponse) async def signin( request: Request, response: Response, form_data: SigninForm, - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not ENABLE_PASSWORD_AUTH: raise HTTPException( @@ -567,7 +579,7 @@ async def signin( if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers: - raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER) email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower() name = email @@ -575,50 +587,59 @@ async def signin( if WEBUI_AUTH_TRUSTED_NAME_HEADER: name = request.headers.get(WEBUI_AUTH_TRUSTED_NAME_HEADER, email) try: - name = urllib.parse.unquote(name, encoding="utf-8") + name = urllib.parse.unquote(name, encoding='utf-8') except Exception as e: pass - if not Users.get_user_by_email(email.lower(), db=db): - await signup( + if not await Users.get_user_by_email(email.lower(), db=db): + await signup_handler( request, - response, - SignupForm(email=email, password=str(uuid.uuid4()), name=name), + email, + str(uuid.uuid4()), + name, db=db, ) - user = Auths.authenticate_user_by_email(email, db=db) - if WEBUI_AUTH_TRUSTED_GROUPS_HEADER and user and user.role != "admin": - group_names = request.headers.get( - WEBUI_AUTH_TRUSTED_GROUPS_HEADER, "" - ).split(",") - group_names = [name.strip() for name in group_names if name.strip()] + user = await Auths.authenticate_user_by_email(email, db=db) + if user: + if WEBUI_AUTH_TRUSTED_GROUPS_HEADER: + group_names = request.headers.get(WEBUI_AUTH_TRUSTED_GROUPS_HEADER, '').split(',') + group_names = [name.strip() for name in group_names if name.strip()] - if group_names: - Groups.sync_groups_by_group_names(user.id, group_names, db=db) + if group_names: + await Groups.sync_groups_by_group_names(user.id, group_names, db=db) + + if WEBUI_AUTH_TRUSTED_ROLE_HEADER: + trusted_role = request.headers.get(WEBUI_AUTH_TRUSTED_ROLE_HEADER, '').lower().strip() + if trusted_role in {'admin', 'user', 'pending'}: + if user.role != trusted_role: + await Users.update_user_role_by_id(user.id, trusted_role, db=db) + elif trusted_role: + log.warning(f'Ignoring invalid trusted role header value: {trusted_role}') elif WEBUI_AUTH == False: - admin_email = "admin@localhost" - admin_password = "admin" + admin_email = 'admin@localhost' + admin_password = 'admin' - if Users.get_user_by_email(admin_email.lower(), db=db): - user = Auths.authenticate_user( + if await Users.get_user_by_email(admin_email.lower(), db=db): + user = await Auths.authenticate_user( admin_email.lower(), lambda pw: verify_password(admin_password, pw), db=db, ) else: - if Users.has_users(db=db): + if await Users.has_users(db=db): raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS) - await signup( + await signup_handler( request, - response, - SignupForm(email=admin_email, password=admin_password, name="User"), + admin_email, + admin_password, + 'User', db=db, ) - user = Auths.authenticate_user( + user = await Auths.authenticate_user( admin_email.lower(), lambda pw: verify_password(admin_password, pw), db=db, @@ -630,64 +651,23 @@ async def signin( detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, ) - password_bytes = form_data.password.encode("utf-8") + password_bytes = form_data.password.encode('utf-8') if len(password_bytes) > 72: # TODO: Implement other hashing algorithms that support longer passwords - log.info("Password too long, truncating to 72 bytes for bcrypt") + log.info('Password too long, truncating to 72 bytes for bcrypt') password_bytes = password_bytes[:72] # decode safely — ignore incomplete UTF-8 sequences - form_data.password = password_bytes.decode("utf-8", errors="ignore") + form_data.password = password_bytes.decode('utf-8', errors='ignore') - user = Auths.authenticate_user( + user = await Auths.authenticate_user( form_data.email.lower(), lambda pw: verify_password(form_data.password, pw), db=db, ) if user: - - expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) - expires_at = None - if expires_delta: - expires_at = int(time.time()) + int(expires_delta.total_seconds()) - - token = create_token( - data={"id": user.id}, - expires_delta=expires_delta, - ) - - datetime_expires_at = ( - datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) - if expires_at - else None - ) - - # Set the cookie token - response.set_cookie( - key="token", - value=token, - expires=datetime_expires_at, - httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_AUTH_COOKIE_SAME_SITE, - secure=WEBUI_AUTH_COOKIE_SECURE, - ) - - user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS, db=db - ) - - return { - "token": token, - "token_type": "Bearer", - "expires_at": expires_at, - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - "profile_image_url": user.profile_image_url, - "permissions": user_permissions, - } + return await create_session_response(request, user, db, response, set_cookie=True) else: raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) @@ -697,36 +677,90 @@ async def signin( ############################ -@router.post("/signup", response_model=SessionUserResponse) +async def signup_handler( + request: Request, + email: str, + password: str, + name: str, + profile_image_url: str = '/user.png', + *, + db: AsyncSession, +) -> UserModel: + """ + Core user-creation logic shared by the signup endpoint and + trusted-header / no-auth auto-registration flows. + + Returns the newly created UserModel. + Raises HTTPException on failure. + """ + # Insert with default role first to avoid TOCTOU race on first signup. + # If has_users() is checked before insert, concurrent requests during + # first-user registration can all see an empty table and each get admin. + hashed = get_password_hash(password) + + user = await Auths.insert_new_auth( + email=email.lower(), + password=hashed, + name=name, + profile_image_url=profile_image_url, + role=request.app.state.config.DEFAULT_USER_ROLE, + db=db, + ) + if not user: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + + # Atomically check if this is the only user *after* the insert. + # Only the single user present at this point should become admin. + if await Users.get_num_users(db=db) == 1: + await Users.update_user_role_by_id(user.id, 'admin', db=db) + user = await Users.get_user_by_id(user.id, db=db) + request.app.state.config.ENABLE_SIGNUP = False + + if request.app.state.config.WEBHOOK_URL: + await post_webhook( + request.app.state.WEBUI_NAME, + request.app.state.config.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + 'action': 'signup', + 'message': WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + 'user': user.model_dump_json(exclude_none=True), + }, + ) + + await apply_default_group_assignment( + request.app.state.config.DEFAULT_GROUP_ID, + user.id, + db=db, + ) + + return user + + +@router.post('/signup', response_model=SessionUserResponse) async def signup( request: Request, response: Response, form_data: SignupForm, - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - has_users = Users.has_users(db=db) + has_users = await Users.has_users(db=db) if WEBUI_AUTH: - if ( - not request.app.state.config.ENABLE_SIGNUP - or not request.app.state.config.ENABLE_LOGIN_FORM - ): - if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP: - raise HTTPException( - status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED - ) + if has_users: + if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM: + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + # Don't gate the first admin on ENABLE_SIGNUP: it auto-disables and can persist stale across a DB reset. + elif not request.app.state.config.ENABLE_LOGIN_FORM and not ENABLE_INITIAL_ADMIN_SIGNUP: + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) else: if has_users: - raise HTTPException( - status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED - ) + raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) if not validate_email_format(form_data.email.lower()): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT - ) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) - if Users.get_user_by_email(form_data.email.lower(), db=db): + if await Users.get_user_by_email(form_data.email.lower(), db=db): raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) try: @@ -735,152 +769,94 @@ async def signup( except Exception as e: raise HTTPException(400, detail=str(e)) - hashed = get_password_hash(form_data.password) - - role = "admin" if not has_users else request.app.state.config.DEFAULT_USER_ROLE - user = Auths.insert_new_auth( - form_data.email.lower(), - hashed, + user = await signup_handler( + request, + form_data.email, + form_data.password, form_data.name, form_data.profile_image_url, - role, db=db, ) - - if user: - expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) - expires_at = None - if expires_delta: - expires_at = int(time.time()) + int(expires_delta.total_seconds()) - - token = create_token( - data={"id": user.id}, - expires_delta=expires_delta, - ) - - datetime_expires_at = ( - datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc) - if expires_at - else None - ) - - # Set the cookie token - response.set_cookie( - key="token", - value=token, - expires=datetime_expires_at, - httponly=True, # Ensures the cookie is not accessible via JavaScript - samesite=WEBUI_AUTH_COOKIE_SAME_SITE, - secure=WEBUI_AUTH_COOKIE_SECURE, - ) - - if request.app.state.config.WEBHOOK_URL: - await post_webhook( - request.app.state.WEBUI_NAME, - request.app.state.config.WEBHOOK_URL, - WEBHOOK_MESSAGES.USER_SIGNUP(user.name), - { - "action": "signup", - "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), - "user": user.model_dump_json(exclude_none=True), - }, - ) - - user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS, db=db - ) - - if not has_users: - # Disable signup after the first user is created - request.app.state.config.ENABLE_SIGNUP = False - - apply_default_group_assignment( - request.app.state.config.DEFAULT_GROUP_ID, - user.id, - db=db, - ) - - return { - "token": token, - "token_type": "Bearer", - "expires_at": expires_at, - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - "profile_image_url": user.profile_image_url, - "permissions": user_permissions, - } - else: - raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + return await create_session_response(request, user, db, response, set_cookie=True) + except HTTPException: + raise except Exception as err: - log.error(f"Signup error: {str(err)}") - raise HTTPException(500, detail="An internal error occurred during signup.") + log.error(f'Signup error: {str(err)}') + raise HTTPException(500, detail='An internal error occurred during signup.') -@router.get("/signout") -async def signout( - request: Request, response: Response, db: Session = Depends(get_session) -): - +@router.post('/signout') +async def signout(request: Request, response: Response, db: AsyncSession = Depends(get_async_session)): # get auth token from headers or cookies token = None - auth_header = request.headers.get("Authorization") + auth_header = request.headers.get('Authorization') if auth_header: auth_cred = get_http_authorization_cred(auth_header) - token = auth_cred.credentials - else: - token = request.cookies.get("token") + if auth_cred is not None: + token = auth_cred.credentials + if token is None: + token = request.cookies.get('token') if token: await invalidate_token(request, token) - response.delete_cookie("token") - response.delete_cookie("oui-session") - response.delete_cookie("oauth_id_token") + response.delete_cookie('token') + response.delete_cookie('oui-session') + response.delete_cookie('oauth_id_token') - oauth_session_id = request.cookies.get("oauth_session_id") + oauth_session_id = request.cookies.get('oauth_session_id') if oauth_session_id: - response.delete_cookie("oauth_session_id") + response.delete_cookie('oauth_session_id') + + session = await OAuthSessions.get_session_by_id(oauth_session_id, db=db) + + # If a custom end_session_endpoint is configured (e.g. AWS Cognito), redirect + # there directly instead of attempting OIDC discovery. + if OPENID_END_SESSION_ENDPOINT.value: + return JSONResponse( + status_code=200, + content={ + 'status': True, + 'redirect_url': OPENID_END_SESSION_ENDPOINT.value, + }, + headers=response.headers, + ) - session = OAuthSessions.get_session_by_id(oauth_session_id, db=db) oauth_server_metadata_url = ( - request.app.state.oauth_manager.get_server_metadata_url(session.provider) - if session - else None + request.app.state.oauth_manager.get_server_metadata_url(session.provider) if session else None ) or OPENID_PROVIDER_URL.value if session and oauth_server_metadata_url: - oauth_id_token = session.token.get("id_token") + oauth_id_token = session.token.get('id_token') try: async with ClientSession(trust_env=True) as session: - async with session.get(oauth_server_metadata_url) as r: + async with session.get(oauth_server_metadata_url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r: if r.status == 200: openid_data = await r.json() - logout_url = openid_data.get("end_session_endpoint") + logout_url = openid_data.get('end_session_endpoint') if logout_url: return JSONResponse( status_code=200, content={ - "status": True, - "redirect_url": f"{logout_url}?id_token_hint={oauth_id_token}" + 'status': True, + 'redirect_url': f'{logout_url}?id_token_hint={oauth_id_token}' + ( - f"&post_logout_redirect_uri={WEBUI_AUTH_SIGNOUT_REDIRECT_URL}" + f'&post_logout_redirect_uri={WEBUI_AUTH_SIGNOUT_REDIRECT_URL}' if WEBUI_AUTH_SIGNOUT_REDIRECT_URL - else "" + else '' ), }, headers=response.headers, ) else: - raise Exception("Failed to fetch OpenID configuration") + raise Exception('Failed to fetch OpenID configuration') except Exception as e: - log.error(f"OpenID signout error: {str(e)}") + log.error(f'OpenID signout error: {str(e)}') raise HTTPException( status_code=500, - detail="Failed to sign out from the OpenID provider.", + detail='Failed to sign out from the OpenID provider.', headers=response.headers, ) @@ -888,15 +864,38 @@ async def signout( return JSONResponse( status_code=200, content={ - "status": True, - "redirect_url": WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + 'status': True, + 'redirect_url': WEBUI_AUTH_SIGNOUT_REDIRECT_URL, }, headers=response.headers, ) - return JSONResponse( - status_code=200, content={"status": True}, headers=response.headers - ) + return JSONResponse(status_code=200, content={'status': True}, headers=response.headers) + + +############################ +# OAuth Session Management +############################ + + +@router.delete('/oauth/sessions/{provider:path}', response_model=bool) +async def delete_oauth_session_by_provider( + provider: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Disconnect the current user's OAuth session for a specific provider. + The provider string matches the 'provider' field in the oauth_session table + (e.g. 'mcp:server-id' for MCP connections). + """ + result = await OAuthSessions.delete_sessions_by_user_id_and_provider(user.id, provider, db=db) + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='No OAuth session found for this provider', + ) + return True ############################ @@ -904,19 +903,17 @@ async def signout( ############################ -@router.post("/add", response_model=SigninResponse) +@router.post('/add', response_model=SigninResponse) async def add_user( request: Request, form_data: AddUserForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not validate_email_format(form_data.email.lower()): - raise HTTPException( - status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT - ) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) - if Users.get_user_by_email(form_data.email.lower(), db=db): + if await Users.get_user_by_email(form_data.email.lower(), db=db): raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) try: @@ -926,7 +923,7 @@ async def add_user( raise HTTPException(400, detail=str(e)) hashed = get_password_hash(form_data.password) - user = Auths.insert_new_auth( + user = await Auths.insert_new_auth( form_data.email.lower(), hashed, form_data.name, @@ -936,29 +933,30 @@ async def add_user( ) if user: - apply_default_group_assignment( + await apply_default_group_assignment( request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db, ) - token = create_token(data={"id": user.id}) + expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN) + token = create_token(data={'id': user.id}, expires_delta=expires_delta) return { - "token": token, - "token_type": "Bearer", - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - "profile_image_url": user.profile_image_url, + 'token': token, + 'token_type': 'Bearer', + 'id': user.id, + 'email': user.email, + 'name': user.name, + 'role': user.role, + 'profile_image_url': f'/api/v1/users/{user.id}/profile/image', } else: raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + except HTTPException: + raise except Exception as err: - log.error(f"Add user error: {str(err)}") - raise HTTPException( - 500, detail="An internal error occurred while adding the user." - ) + log.error(f'Add user error: {str(err)}') + raise HTTPException(500, detail='An internal error occurred while adding the user.') ############################ @@ -966,29 +964,29 @@ async def add_user( ############################ -@router.get("/admin/details") +@router.get('/admin/details') async def get_admin_details( - request: Request, user=Depends(get_current_user), db: Session = Depends(get_session) + request: Request, user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session) ): if request.app.state.config.SHOW_ADMIN_DETAILS: admin_email = request.app.state.config.ADMIN_EMAIL admin_name = None - log.info(f"Admin details - Email: {admin_email}, Name: {admin_name}") + log.info(f'Admin details - Email: {admin_email}, Name: {admin_name}') if admin_email: - admin = Users.get_user_by_email(admin_email, db=db) + admin = await Users.get_user_by_email(admin_email, db=db) if admin: admin_name = admin.name else: - admin = Users.get_first_user(db=db) + admin = await Users.get_first_user(db=db) if admin: admin_email = admin.email admin_name = admin.name return { - "name": admin_name, - "email": admin_email, + 'name': admin_name, + 'email': admin_email, } else: raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) @@ -999,37 +997,41 @@ async def get_admin_details( ############################ -@router.get("/admin/config") +@router.get('/admin/config') async def get_admin_config(request: Request, user=Depends(get_admin_user)): return { - "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, - "ADMIN_EMAIL": request.app.state.config.ADMIN_EMAIL, - "WEBUI_URL": request.app.state.config.WEBUI_URL, - "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP, - "ENABLE_API_KEYS": request.app.state.config.ENABLE_API_KEYS, - "ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, - "API_KEYS_ALLOWED_ENDPOINTS": request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS, - "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, - "DEFAULT_GROUP_ID": request.app.state.config.DEFAULT_GROUP_ID, - "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN, - "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, - "ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING, - "ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS, - "FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT, - "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, - "ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES, - "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, - "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, - "ENABLE_USER_STATUS": request.app.state.config.ENABLE_USER_STATUS, - "PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE, - "PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT, - "RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK, + 'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS, + 'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL, + 'WEBUI_URL': request.app.state.config.WEBUI_URL, + 'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP, + 'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS, + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + 'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS, + 'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE, + 'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID, + 'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN, + 'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING, + 'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING, + 'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS, + 'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT, + 'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT, + 'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL, + 'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS, + 'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS, + 'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR, + 'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES, + 'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES, + 'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS, + 'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS, + 'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE, + 'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK, } class AdminConfig(BaseModel): SHOW_ADMIN_DETAILS: bool - ADMIN_EMAIL: Optional[str] = None + ADMIN_EMAIL: str | None = None WEBUI_URL: str ENABLE_SIGNUP: bool ENABLE_API_KEYS: bool @@ -1041,158 +1043,156 @@ class AdminConfig(BaseModel): ENABLE_COMMUNITY_SHARING: bool ENABLE_MESSAGE_RATING: bool ENABLE_FOLDERS: bool - FOLDER_MAX_FILE_COUNT: Optional[int | str] = None + FOLDER_MAX_FILE_COUNT: int | str | None = None + AUTOMATION_MAX_COUNT: int | str | None = None + AUTOMATION_MIN_INTERVAL: int | str | None = None + ENABLE_AUTOMATIONS: bool ENABLE_CHANNELS: bool + ENABLE_CALENDAR: bool ENABLE_MEMORIES: bool ENABLE_NOTES: bool ENABLE_USER_WEBHOOKS: bool ENABLE_USER_STATUS: bool - PENDING_USER_OVERLAY_TITLE: Optional[str] = None - PENDING_USER_OVERLAY_CONTENT: Optional[str] = None - RESPONSE_WATERMARK: Optional[str] = None + PENDING_USER_OVERLAY_TITLE: str | None = None + PENDING_USER_OVERLAY_CONTENT: str | None = None + RESPONSE_WATERMARK: str | None = None -@router.post("/admin/config") -async def update_admin_config( - request: Request, form_data: AdminConfig, user=Depends(get_admin_user) -): +@router.post('/admin/config') +async def update_admin_config(request: Request, form_data: AdminConfig, user=Depends(get_admin_user)): request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS request.app.state.config.ADMIN_EMAIL = form_data.ADMIN_EMAIL request.app.state.config.WEBUI_URL = form_data.WEBUI_URL request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP request.app.state.config.ENABLE_API_KEYS = form_data.ENABLE_API_KEYS - request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ( - form_data.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS - ) - request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS = ( - form_data.API_KEYS_ALLOWED_ENDPOINTS - ) + request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = form_data.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS + request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS = form_data.API_KEYS_ALLOWED_ENDPOINTS request.app.state.config.ENABLE_FOLDERS = form_data.ENABLE_FOLDERS request.app.state.config.FOLDER_MAX_FILE_COUNT = ( - int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else "" + int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else '' ) + request.app.state.config.AUTOMATION_MAX_COUNT = ( + int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else '' + ) + request.app.state.config.AUTOMATION_MIN_INTERVAL = ( + int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else '' + ) + request.app.state.config.ENABLE_AUTOMATIONS = form_data.ENABLE_AUTOMATIONS request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS + request.app.state.config.ENABLE_CALENDAR = form_data.ENABLE_CALENDAR request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES - if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]: + if form_data.DEFAULT_USER_ROLE in ['pending', 'user', 'admin']: request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE request.app.state.config.DEFAULT_GROUP_ID = form_data.DEFAULT_GROUP_ID - pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$" + pattern = r'^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$' # Check if the input string matches the pattern if re.match(pattern, form_data.JWT_EXPIRES_IN): request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN - request.app.state.config.ENABLE_COMMUNITY_SHARING = ( - form_data.ENABLE_COMMUNITY_SHARING - ) + request.app.state.config.ENABLE_COMMUNITY_SHARING = form_data.ENABLE_COMMUNITY_SHARING request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS request.app.state.config.ENABLE_USER_STATUS = form_data.ENABLE_USER_STATUS - request.app.state.config.PENDING_USER_OVERLAY_TITLE = ( - form_data.PENDING_USER_OVERLAY_TITLE - ) - request.app.state.config.PENDING_USER_OVERLAY_CONTENT = ( - form_data.PENDING_USER_OVERLAY_CONTENT - ) + request.app.state.config.PENDING_USER_OVERLAY_TITLE = form_data.PENDING_USER_OVERLAY_TITLE + request.app.state.config.PENDING_USER_OVERLAY_CONTENT = form_data.PENDING_USER_OVERLAY_CONTENT request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK return { - "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, - "ADMIN_EMAIL": request.app.state.config.ADMIN_EMAIL, - "WEBUI_URL": request.app.state.config.WEBUI_URL, - "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP, - "ENABLE_API_KEYS": request.app.state.config.ENABLE_API_KEYS, - "ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, - "API_KEYS_ALLOWED_ENDPOINTS": request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS, - "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, - "DEFAULT_GROUP_ID": request.app.state.config.DEFAULT_GROUP_ID, - "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN, - "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, - "ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING, - "ENABLE_FOLDERS": request.app.state.config.ENABLE_FOLDERS, - "FOLDER_MAX_FILE_COUNT": request.app.state.config.FOLDER_MAX_FILE_COUNT, - "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, - "ENABLE_MEMORIES": request.app.state.config.ENABLE_MEMORIES, - "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, - "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, - "ENABLE_USER_STATUS": request.app.state.config.ENABLE_USER_STATUS, - "PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE, - "PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT, - "RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK, + 'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS, + 'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL, + 'WEBUI_URL': request.app.state.config.WEBUI_URL, + 'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP, + 'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS, + 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + 'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS, + 'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE, + 'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID, + 'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN, + 'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING, + 'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING, + 'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS, + 'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT, + 'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT, + 'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL, + 'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS, + 'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS, + 'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR, + 'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES, + 'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES, + 'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS, + 'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS, + 'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE, + 'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT, + 'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK, } class LdapServerConfig(BaseModel): label: str host: str - port: Optional[int] = None - attribute_for_mail: str = "mail" - attribute_for_username: str = "uid" + port: int | None = None + attribute_for_mail: str = 'mail' + attribute_for_username: str = 'uid' app_dn: str app_dn_password: str search_base: str - search_filters: str = "" + search_filters: str = '' use_tls: bool = True - certificate_path: Optional[str] = None + certificate_path: str | None = None validate_cert: bool = True - ciphers: Optional[str] = "ALL" + ciphers: str | None = 'ALL' -@router.get("/admin/config/ldap/server", response_model=LdapServerConfig) +@router.get('/admin/config/ldap/server', response_model=LdapServerConfig) async def get_ldap_server(request: Request, user=Depends(get_admin_user)): return { - "label": request.app.state.config.LDAP_SERVER_LABEL, - "host": request.app.state.config.LDAP_SERVER_HOST, - "port": request.app.state.config.LDAP_SERVER_PORT, - "attribute_for_mail": request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL, - "attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME, - "app_dn": request.app.state.config.LDAP_APP_DN, - "app_dn_password": request.app.state.config.LDAP_APP_PASSWORD, - "search_base": request.app.state.config.LDAP_SEARCH_BASE, - "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS, - "use_tls": request.app.state.config.LDAP_USE_TLS, - "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE, - "validate_cert": request.app.state.config.LDAP_VALIDATE_CERT, - "ciphers": request.app.state.config.LDAP_CIPHERS, + 'label': request.app.state.config.LDAP_SERVER_LABEL, + 'host': request.app.state.config.LDAP_SERVER_HOST, + 'port': request.app.state.config.LDAP_SERVER_PORT, + 'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL, + 'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME, + 'app_dn': request.app.state.config.LDAP_APP_DN, + 'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD, + 'search_base': request.app.state.config.LDAP_SEARCH_BASE, + 'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS, + 'use_tls': request.app.state.config.LDAP_USE_TLS, + 'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE, + 'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT, + 'ciphers': request.app.state.config.LDAP_CIPHERS, } -@router.post("/admin/config/ldap/server") -async def update_ldap_server( - request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user) -): +@router.post('/admin/config/ldap/server') +async def update_ldap_server(request: Request, form_data: LdapServerConfig, user=Depends(get_admin_user)): required_fields = [ - "label", - "host", - "attribute_for_mail", - "attribute_for_username", - "app_dn", - "app_dn_password", - "search_base", + 'label', + 'host', + 'attribute_for_mail', + 'attribute_for_username', + 'search_base', ] for key in required_fields: value = getattr(form_data, key) if not value: - raise HTTPException(400, detail=f"Required field {key} is empty") + raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY(key)) request.app.state.config.LDAP_SERVER_LABEL = form_data.label request.app.state.config.LDAP_SERVER_HOST = form_data.host request.app.state.config.LDAP_SERVER_PORT = form_data.port request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = form_data.attribute_for_mail - request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = ( - form_data.attribute_for_username - ) - request.app.state.config.LDAP_APP_DN = form_data.app_dn - request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password + request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = form_data.attribute_for_username + request.app.state.config.LDAP_APP_DN = form_data.app_dn or '' + request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password or '' request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters request.app.state.config.LDAP_USE_TLS = form_data.use_tls @@ -1201,37 +1201,35 @@ async def update_ldap_server( request.app.state.config.LDAP_CIPHERS = form_data.ciphers return { - "label": request.app.state.config.LDAP_SERVER_LABEL, - "host": request.app.state.config.LDAP_SERVER_HOST, - "port": request.app.state.config.LDAP_SERVER_PORT, - "attribute_for_mail": request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL, - "attribute_for_username": request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME, - "app_dn": request.app.state.config.LDAP_APP_DN, - "app_dn_password": request.app.state.config.LDAP_APP_PASSWORD, - "search_base": request.app.state.config.LDAP_SEARCH_BASE, - "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS, - "use_tls": request.app.state.config.LDAP_USE_TLS, - "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE, - "validate_cert": request.app.state.config.LDAP_VALIDATE_CERT, - "ciphers": request.app.state.config.LDAP_CIPHERS, + 'label': request.app.state.config.LDAP_SERVER_LABEL, + 'host': request.app.state.config.LDAP_SERVER_HOST, + 'port': request.app.state.config.LDAP_SERVER_PORT, + 'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL, + 'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME, + 'app_dn': request.app.state.config.LDAP_APP_DN, + 'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD, + 'search_base': request.app.state.config.LDAP_SEARCH_BASE, + 'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS, + 'use_tls': request.app.state.config.LDAP_USE_TLS, + 'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE, + 'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT, + 'ciphers': request.app.state.config.LDAP_CIPHERS, } -@router.get("/admin/config/ldap") +@router.get('/admin/config/ldap') async def get_ldap_config(request: Request, user=Depends(get_admin_user)): - return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP} + return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP} class LdapConfigForm(BaseModel): - enable_ldap: Optional[bool] = None + enable_ldap: bool | None = None -@router.post("/admin/config/ldap") -async def update_ldap_config( - request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user) -): +@router.post('/admin/config/ldap') +async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)): request.app.state.config.ENABLE_LDAP = form_data.enable_ldap - return {"ENABLE_LDAP": request.app.state.config.ENABLE_LDAP} + return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP} ############################ @@ -1240,12 +1238,13 @@ async def update_ldap_config( # create api key -@router.post("/api_key", response_model=ApiKey) +@router.post('/api_key', response_model=ApiKey) async def generate_api_key( - request: Request, user=Depends(get_current_user), db: Session = Depends(get_session) + request: Request, user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session) ): - if not request.app.state.config.ENABLE_API_KEYS or not has_permission( - user.id, "features.api_keys", request.app.state.config.USER_PERMISSIONS + if not request.app.state.config.ENABLE_API_KEYS or ( + user.role != 'admin' + and not await has_permission(user.id, 'features.api_keys', request.app.state.config.USER_PERMISSIONS) ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -1253,33 +1252,142 @@ async def generate_api_key( ) api_key = create_api_key() - success = Users.update_user_api_key_by_id(user.id, api_key, db=db) + success = await Users.update_user_api_key_by_id(user.id, api_key, db=db) if success: return { - "api_key": api_key, + 'api_key': api_key, } else: raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR) # delete api key -@router.delete("/api_key", response_model=bool) -async def delete_api_key( - user=Depends(get_current_user), db: Session = Depends(get_session) -): - return Users.delete_user_api_key_by_id(user.id, db=db) +@router.delete('/api_key', response_model=bool) +async def delete_api_key(user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session)): + return await Users.delete_user_api_key_by_id(user.id, db=db) # get api key -@router.get("/api_key", response_model=ApiKey) -async def get_api_key( - user=Depends(get_current_user), db: Session = Depends(get_session) -): - api_key = Users.get_user_api_key_by_id(user.id, db=db) +@router.get('/api_key', response_model=ApiKey) +async def get_api_key(user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session)): + api_key = await Users.get_user_api_key_by_id(user.id, db=db) if api_key: return { - "api_key": api_key, + 'api_key': api_key, } else: raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + + +############################ +# Token Exchange +############################ + + +class TokenExchangeForm(BaseModel): + token: str # OAuth access token from external provider + + +@router.post('/oauth/{provider}/token/exchange', response_model=SessionUserResponse) +async def token_exchange( + request: Request, + response: Response, + provider: str, + form_data: TokenExchangeForm, + db: AsyncSession = Depends(get_async_session), +): + """ + Exchange an external OAuth provider token for an OpenWebUI JWT. + This endpoint is disabled by default. Set ENABLE_OAUTH_TOKEN_EXCHANGE=True to enable. + """ + if not ENABLE_OAUTH_TOKEN_EXCHANGE: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='Token exchange is disabled', + ) + + provider = provider.lower() + + # Check if provider is configured + if provider not in OAUTH_PROVIDERS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider), + ) + # Get the OAuth client for this provider + oauth_manager = request.app.state.oauth_manager + client = oauth_manager.get_client(provider) + if not client: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider), + ) + + # Validate the token by calling the userinfo endpoint + try: + token_data = {'access_token': form_data.token, 'token_type': 'Bearer'} + user_data = await client.userinfo(token=token_data) + + if not user_data: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Invalid token or unable to fetch user info', + ) + except Exception as e: + log.warning(f'Token exchange failed for provider {provider}: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Invalid token or unable to validate with provider', + ) + + # Extract user information from the token claims + email_claim = request.app.state.config.OAUTH_EMAIL_CLAIM + username_claim = request.app.state.config.OAUTH_USERNAME_CLAIM + + # Get sub claim + sub = user_data.get(request.app.state.config.OAUTH_SUB_CLAIM or OAUTH_PROVIDERS[provider].get('sub_claim', 'sub')) + if not sub: + log.warning(f'Token exchange failed: sub claim missing from user data') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Token missing required 'sub' claim", + ) + + email = user_data.get(email_claim, '') + if not email: + log.warning(f'Token exchange failed: email claim missing from user data') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Token missing required email claim', + ) + email = email.lower() + + # Enforce domain allowlist — same check as the normal OAuth callback + if ( + '*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS + and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS + ): + log.warning(f'Token exchange denied: email domain not in allowed domains list') + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Try to find the user by OAuth sub + user = await Users.get_user_by_oauth_sub(provider, sub, db=db) + + if not user and OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value: + # Try to find by email if merge is enabled + user = await Users.get_user_by_email(email, db=db) + if user: + # Link the OAuth sub to this user + await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) + + if not user: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='User not found. Please sign in via the web interface first.', + ) + + return await create_session_response(request, user, db) diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py new file mode 100644 index 0000000000..fced12978a --- /dev/null +++ b/backend/open_webui/routers/automations.py @@ -0,0 +1,303 @@ +import asyncio +import logging +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from open_webui.models.automations import ( + AutomationForm, + AutomationListResponse, + AutomationModel, + AutomationResponse, + AutomationRunModel, + AutomationRuns, + Automations, +) +from open_webui.utils.access_control import has_permission +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.automations import ( + execute_automation, + next_n_runs_ns, + next_run_ns, + rrule_interval_seconds, + validate_rrule, +) +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +router = APIRouter() + +PAGE_ITEM_COUNT = 30 + + +############################ +# Helpers +############################ + + +async def check_automations_permission(request, user): + if not request.app.state.config.ENABLE_AUTOMATIONS: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + if user.role != 'admin' and not await has_permission( + user.id, 'features.automations', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +def check_automation_access(automation, user): + if not automation: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + if user.role != 'admin' and user.id != automation.user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +async def check_automation_limits(request, user, rrule_str: str, db, is_create: bool = False): + """Enforce global automation limits. Admins bypass all checks.""" + if user.role == 'admin': + return + + # Max count (create only) + if is_create: + max_count = request.app.state.config.AUTOMATION_MAX_COUNT + if max_count: + max_count = int(max_count) + if max_count > 0 and await Automations.count_by_user(user.id, db=db) >= max_count: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.AUTOMATION_LIMIT_EXCEEDED(max_count), + ) + + # Min interval (create + update) + min_interval = request.app.state.config.AUTOMATION_MIN_INTERVAL + if min_interval: + min_interval = int(min_interval) + if min_interval > 0: + interval = rrule_interval_seconds(rrule_str) + if interval is not None and interval < min_interval: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.AUTOMATION_TOO_FREQUENT(min_interval), + ) + + +async def enrich_automation(automation: AutomationModel, db: AsyncSession, tz: str = None) -> AutomationResponse: + """Full enrichment for single-item views (includes next_runs computation).""" + last_run = await AutomationRuns.get_latest(automation.id, db=db) + return AutomationResponse( + **automation.model_dump(), + last_run=last_run, + next_runs=next_n_runs_ns(automation.data['rrule'], tz=tz), + ) + + +############################ +# GetAutomationItems (paginated) +############################ + + +@router.get('/list') +async def get_automation_items( + request: Request, + query: Optional[str] = None, + status: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + limit = PAGE_ITEM_COUNT + page = max(1, page) + skip = (page - 1) * limit + + result = await Automations.search_automations( + user_id=user.id, + query=query, + status=status, + skip=skip, + limit=limit, + db=db, + ) + + # Batch-fetch latest runs in a single query instead of N+1 + ids = [item.id for item in result.items] + latest_runs = await AutomationRuns.get_latest_batch(ids, db=db) if ids else {} + + return { + 'items': [ + AutomationResponse( + **item.model_dump(), + last_run=latest_runs.get(item.id), + ) + for item in result.items + ], + 'total': result.total, + } + + +############################ +# CreateNewAutomation +############################ + + +@router.post('/create', response_model=AutomationResponse) +async def create_new_automation( + request: Request, + form_data: AutomationForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + try: + validate_rrule(form_data.data.rrule, tz=user.timezone) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + await check_automation_limits(request, user, form_data.data.rrule, db, is_create=True) + + tz = user.timezone + automation = await Automations.insert(user.id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db) + return await enrich_automation(automation, db, tz=tz) + + +############################ +# GetAutomationById +############################ + + +@router.get('/{id}', response_model=AutomationResponse) +async def get_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + return await enrich_automation(automation, db, tz=user.timezone) + + +############################ +# UpdateAutomationById +############################ + + +@router.post('/{id}/update', response_model=AutomationResponse) +async def update_automation_by_id( + request: Request, + id: str, + form_data: AutomationForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + + try: + validate_rrule(form_data.data.rrule, tz=user.timezone) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + await check_automation_limits(request, user, form_data.data.rrule, db, is_create=False) + + tz = user.timezone + updated = await Automations.update_by_id(id, form_data, next_run_ns(form_data.data.rrule, tz=tz), db=db) + return await enrich_automation(updated, db, tz=tz) + + +############################ +# ToggleAutomationById +############################ + + +@router.post('/{id}/toggle', response_model=AutomationResponse) +async def toggle_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + toggled = await Automations.toggle(id, next_run_ns(automation.data['rrule'], tz=user.timezone), db=db) + return await enrich_automation(toggled, db, tz=user.timezone) + + +############################ +# RunAutomationById +############################ + + +@router.post('/{id}/run') +async def run_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + asyncio.create_task(execute_automation(request.app, automation)) + return await enrich_automation(automation, db, tz=user.timezone) + + +############################ +# DeleteAutomationById +############################ + + +@router.delete('/{id}/delete') +async def delete_automation_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + await AutomationRuns.delete_by_automation(id, db=db) + return await Automations.delete(id, db=db) + + +############################ +# GetAutomationRuns +############################ + + +@router.get('/{id}/runs', response_model=list[AutomationRunModel]) +async def get_automation_runs( + request: Request, + id: str, + skip: int = 0, + limit: int = 50, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await check_automations_permission(request, user) + automation = await Automations.get_by_id(id, db=db) + check_automation_access(automation, user) + return await AutomationRuns.get_by_automation(id, skip=skip, limit=limit, db=db) diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py new file mode 100644 index 0000000000..5d397ea868 --- /dev/null +++ b/backend/open_webui/routers/calendar.py @@ -0,0 +1,419 @@ +import logging +import time +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.constants import ERROR_MESSAGES +from open_webui.models.access_grants import AccessGrants +from open_webui.models.calendar import ( + CalendarEventAttendees, + CalendarEventForm, + CalendarEventListResponse, + CalendarEventModel, + CalendarEvents, + CalendarEventUpdateForm, + CalendarEventUserResponse, + CalendarForm, + CalendarModel, + Calendars, + CalendarUpdateForm, + RSVPForm, +) +from open_webui.models.groups import Groups +from open_webui.models.users import UserModel +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.auth import get_verified_user +from open_webui.utils.calendar import expand_recurring_event + +log = logging.getLogger(__name__) + +router = APIRouter() + +SCHEDULED_TASKS_CALENDAR_ID = '__scheduled_tasks__' + + +async def check_calendar_permission(request: Request, user): + """Check global feature flag AND per-user permission for calendar access.""" + if not request.app.state.config.ENABLE_CALENDAR: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + if user.role != 'admin' and not await has_permission( + user.id, 'features.calendar', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +async def _user_has_automations(request: Request, user) -> bool: + """Check if automations feature is available to this user.""" + if not getattr(request.app.state.config, 'ENABLE_AUTOMATIONS', False): + return False + if user.role == 'admin': + return True + return await has_permission(user.id, 'features.automations', request.app.state.config.USER_PERMISSIONS) + + +async def _check_calendar_access(calendar_id: str, user: UserModel, permission: str = 'write') -> CalendarModel: + """Verify user has access to a calendar. Returns the calendar or raises 403/404.""" + cal = await Calendars.get_calendar_by_id(calendar_id) + if not cal: + raise HTTPException(status_code=404, detail='Calendar not found') + if cal.user_id == user.id or user.role == 'admin': + return cal + user_groups = await Groups.get_groups_by_member_id(user.id) + user_group_ids = [g.id for g in user_groups] + if await AccessGrants.has_access( + user_id=user.id, + resource_type='calendar', + resource_id=cal.id, + permission=permission, + user_group_ids=user_group_ids, + ): + return cal + raise HTTPException(status_code=403, detail='Access denied') + + +#################### +# Calendar CRUD (static paths first) +#################### + + +@router.get('/', response_model=list[CalendarModel]) +async def get_calendars(request: Request, user: UserModel = Depends(get_verified_user)): + """List user's calendars (owned + shared), plus a virtual Scheduled Tasks calendar + when automations are available.""" + await check_calendar_permission(request, user) + calendars = await Calendars.get_calendars_by_user(user.id) + + if await _user_has_automations(request, user): + now = int(time.time_ns()) + calendars.append( + CalendarModel( + id=SCHEDULED_TASKS_CALENDAR_ID, + user_id=user.id, + name='Scheduled Tasks', + color='#8b5cf6', + is_default=False, + is_system=True, + created_at=now, + updated_at=now, + ) + ) + + return calendars + + +@router.post('/create', response_model=CalendarModel) +async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)): + """Create a new user calendar.""" + await check_calendar_permission(request, user) + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/models pattern). Without this, any verified user + # could create a calendar with `principal_id='*' permission='read'|'write'`, + # making their events readable or writable by any other verified user. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_calendars', + ) + return await Calendars.insert_new_calendar(user.id, form_data) + + +#################### +# Event CRUD (before /{calendar_id} to avoid route conflicts) +#################### + + +@router.get('/events') +async def get_events( + request: Request, + start: str, + end: str, + calendar_ids: Optional[str] = None, + user: UserModel = Depends(get_verified_user), +): + """Get events in date range. + + Args: + start: ISO 8601 datetime string (e.g. 2026-04-01T00:00:00) + end: ISO 8601 datetime string (e.g. 2026-05-01T00:00:00) + calendar_ids: optional comma-separated list to filter + + Includes: + - Stored events from the database + - Virtual events computed from active automation RRULEs (Scheduled Tasks calendar) + """ + await check_calendar_permission(request, user) + from datetime import datetime + + try: + start_dt = datetime.fromisoformat(start.replace('Z', '+00:00')) + end_dt = datetime.fromisoformat(end.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail='Invalid date format. Use ISO 8601 (e.g. 2026-04-01T00:00:00)') + + NS = 1_000_000 + start_ns = int(start_dt.timestamp() * 1000) * NS + end_ns = int(end_dt.timestamp() * 1000) * NS + cal_id_list = calendar_ids.split(',') if calendar_ids else None + + # 1. Stored events + events = await CalendarEvents.get_events_by_range( + user_id=user.id, + start=start_ns, + end=end_ns, + calendar_ids=cal_id_list, + ) + + # Expand recurring stored events + expanded = [] + for event in events: + event_dict = event.model_dump() + if event_dict.get('rrule'): + instances = expand_recurring_event(event_dict, start_ns, end_ns, tz=user.timezone) + for inst in instances: + expanded.append(CalendarEventUserResponse(**{**inst, 'user': event.user})) + else: + expanded.append(event) + + # 2. Virtual automation events (Scheduled Tasks calendar) + if await _user_has_automations(request, user) and ( + cal_id_list is None or SCHEDULED_TASKS_CALENDAR_ID in cal_id_list + ): + try: + from open_webui.models.automations import AutomationRuns, Automations + + # Future runs: expand RRULEs for active automations only + active_automations = await Automations.get_active_by_user(user.id) + for auto in active_automations: + rrule_str = auto.data.get('rrule', '') if auto.data else '' + if not rrule_str: + continue + + virtual = { + 'id': f'auto_{auto.id}', + 'calendar_id': SCHEDULED_TASKS_CALENDAR_ID, + 'user_id': user.id, + 'title': auto.name, + 'description': auto.data.get('prompt', '') if auto.data else '', + 'start_at': auto.next_run_at or 0, + 'end_at': None, + 'all_day': False, + 'rrule': rrule_str, + 'color': None, + 'location': None, + 'data': None, + 'meta': {'automation_id': auto.id}, + 'is_cancelled': False, + 'attendees': [], + 'created_at': auto.created_at, + 'updated_at': auto.updated_at, + 'user': None, + } + + # Only expand into the future — past runs are handled below + now_ns = int(time.time_ns()) + rrule_start = max(start_ns, now_ns) + instances = expand_recurring_event(virtual, rrule_start, end_ns, tz=user.timezone) + for inst in instances: + expanded.append(CalendarEventUserResponse(**inst)) + + # Past runs: single range query joined with automation + runs_with_auto = await AutomationRuns.get_runs_by_user_range(user.id, start_ns, end_ns) + for run, auto in runs_with_auto: + expanded.append( + CalendarEventUserResponse( + id=f'run_{run.id}', + calendar_id=SCHEDULED_TASKS_CALENDAR_ID, + user_id=user.id, + title=auto.name, + description=run.error if run.status == 'error' else '', + start_at=run.created_at, + end_at=None, + all_day=False, + color=None, + location=None, + data=None, + meta={ + 'automation_id': auto.id, + 'run_id': run.id, + 'chat_id': run.chat_id, + 'status': run.status, + }, + is_cancelled=False, + attendees=[], + created_at=run.created_at, + updated_at=run.created_at, + user=None, + ) + ) + except Exception as e: + log.warning(f'Failed to compute automation events: {e}', exc_info=True) + + return [e.model_dump() if hasattr(e, 'model_dump') else e for e in expanded] + + +@router.post('/events/create', response_model=CalendarEventModel) +async def create_event(request: Request, form_data: CalendarEventForm, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + await _check_calendar_access(form_data.calendar_id, user, 'write') + return await CalendarEvents.insert_new_event(user.id, form_data) + + +@router.get('/events/search', response_model=CalendarEventListResponse) +async def search_events( + request: Request, + query: Optional[str] = None, + skip: int = 0, + limit: int = 30, + user: UserModel = Depends(get_verified_user), +): + await check_calendar_permission(request, user) + return await CalendarEvents.search_events(user_id=user.id, query=query, skip=skip, limit=limit) + + +@router.get('/events/{event_id}', response_model=CalendarEventModel) +async def get_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + raise HTTPException(status_code=404, detail='Event not found') + + await _check_calendar_access(event.calendar_id, user, 'read') + + return event + + +@router.post('/events/{event_id}/update', response_model=CalendarEventModel) +async def update_event( + request: Request, event_id: str, form_data: CalendarEventUpdateForm, user: UserModel = Depends(get_verified_user) +): + await check_calendar_permission(request, user) + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + raise HTTPException(status_code=404, detail='Event not found') + + await _check_calendar_access(event.calendar_id, user, 'write') + + # A new calendar_id in the form moves the event; require write access on the + # destination too, mirroring create_event. Without this, write on the source + # calendar alone is enough to inject an event into any other calendar. + if form_data.calendar_id is not None and form_data.calendar_id != event.calendar_id: + await _check_calendar_access(form_data.calendar_id, user, 'write') + + updated = await CalendarEvents.update_event_by_id(event_id, form_data) + if not updated: + raise HTTPException(status_code=500, detail='Failed to update') + return updated + + +@router.delete('/events/{event_id}/delete') +async def delete_event(request: Request, event_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + event = await CalendarEvents.get_event_by_id(event_id) + if not event: + raise HTTPException(status_code=404, detail='Event not found') + + await _check_calendar_access(event.calendar_id, user, 'write') + + result = await CalendarEvents.delete_event_by_id(event_id) + if not result: + raise HTTPException(status_code=500, detail='Failed to delete') + return {'status': True} + + +@router.post('/events/{event_id}/rsvp', response_model=dict) +async def rsvp_event( + request: Request, event_id: str, form_data: RSVPForm, user: UserModel = Depends(get_verified_user) +): + """Update own RSVP status for an event.""" + await check_calendar_permission(request, user) + if form_data.status not in ('accepted', 'declined', 'tentative', 'pending'): + raise HTTPException(status_code=400, detail='Invalid status') + + result = await CalendarEventAttendees.update_rsvp(event_id, user.id, form_data.status) + if not result: + raise HTTPException(status_code=404, detail='Not an attendee of this event') + return {'status': True, 'rsvp': result.status} + + +#################### +# Calendar by ID (dynamic path — MUST come after /events* routes) +#################### + + +@router.get('/{calendar_id}', response_model=CalendarModel) +async def get_calendar_by_id(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + cal = await _check_calendar_access(calendar_id, user, 'read') + return cal + + +@router.post('/{calendar_id}/update', response_model=CalendarModel) +async def update_calendar( + request: Request, calendar_id: str, form_data: CalendarUpdateForm, user: UserModel = Depends(get_verified_user) +): + await check_calendar_permission(request, user) + cal = await _check_calendar_access(calendar_id, user, 'write') + + # Only owner/admin can change access grants + if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=403, detail='Only owner can manage sharing') + + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/models pattern). The owner-only check above + # only restricts WHO can set grants; this filter restricts WHICH grants + # they may set, so a non-admin owner cannot make their calendar + # publicly readable/writable without the corresponding sharing permission. + if form_data.access_grants is not None: + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_calendars', + ) + + updated = await Calendars.update_calendar_by_id(calendar_id, form_data) + if not updated: + raise HTTPException(status_code=500, detail='Failed to update') + return updated + + +@router.delete('/{calendar_id}/delete') +async def delete_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + + # Block deletion of the virtual Scheduled Tasks calendar + if calendar_id == SCHEDULED_TASKS_CALENDAR_ID: + raise HTTPException(status_code=400, detail='System calendars cannot be deleted') + + cal = await _check_calendar_access(calendar_id, user, 'write') + + # Only owner/admin can delete + if cal.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=403, detail='Only owner can delete calendar') + + # Block deletion of default calendar + if cal.is_default: + raise HTTPException(status_code=400, detail='Default calendar cannot be deleted') + + result = await Calendars.delete_calendar_by_id(calendar_id) + if not result: + raise HTTPException(status_code=500, detail='Failed to delete') + return {'status': True} + + +@router.post('/{calendar_id}/default') +async def set_default_calendar(request: Request, calendar_id: str, user: UserModel = Depends(get_verified_user)): + await check_calendar_permission(request, user) + cal = await Calendars.set_default_calendar(user.id, calendar_id) + if not cal: + raise HTTPException(status_code=404, detail='Calendar not found') + return cal diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 4e697142bf..11d3a4a871 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1,93 +1,145 @@ -import json -import logging import base64 import io +import json +import logging from typing import Optional - -from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks -from fastapi.responses import Response, StreamingResponse, FileResponse -from pydantic import BaseModel -from pydantic import field_validator - -from open_webui.socket.main import ( - emit_to_users, - enter_room_for_users, - sio, - get_user_ids_from_room, +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status +from fastapi.responses import FileResponse, Response, StreamingResponse +from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import STATIC_DIR +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_public_write_access_grant +from open_webui.models.channels import ( + ChannelForm, + ChannelModel, + ChannelResponse, + Channels, + ChannelWebhookForm, + ChannelWebhookModel, + CreateChannelForm, +) +from open_webui.models.groups import Groups +from open_webui.models.messages import ( + MessageForm, + MessageModel, + MessageResponse, + Messages, + MessageWithReactionsResponse, ) from open_webui.models.users import ( UserIdNameResponse, UserIdNameStatusResponse, UserListResponse, + UserModel, UserModelResponse, - Users, UserNameResponse, + Users, ) - -from open_webui.models.groups import Groups -from open_webui.models.channels import ( - Channels, - ChannelModel, - ChannelForm, - ChannelResponse, - CreateChannelForm, - ChannelWebhookModel, - ChannelWebhookForm, +from open_webui.socket.main import ( + emit_to_users, + enter_room_for_users, + get_user_ids_from_room, + sio, ) -from open_webui.models.messages import ( - Messages, - MessageModel, - MessageResponse, - MessageWithReactionsResponse, - MessageForm, -) - - +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.channels import extract_mentions, replace_mentions from open_webui.utils.files import get_image_base64_from_file_id - -from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT -from open_webui.constants import ERROR_MESSAGES -from open_webui.env import STATIC_DIR - - from open_webui.utils.models import ( get_all_models, get_filtered_models, ) -from open_webui.utils.chat import generate_chat_completion - - -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import ( - has_access, - get_users_with_access, - get_permitted_group_and_user_ids, - has_permission, -) from open_webui.utils.webhook import post_webhook -from open_webui.utils.channels import extract_mentions, replace_mentions -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session +from pydantic import BaseModel, field_validator +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) router = APIRouter() +async def channel_has_access( + user_id: str, + channel: ChannelModel, + permission: str = 'read', + strict: bool = True, + db: Optional[AsyncSession] = None, +) -> bool: + if await AccessGrants.has_access( + user_id=user_id, + resource_type='channel', + resource_id=channel.id, + permission=permission, + db=db, + ): + return True + + if not strict and permission == 'write' and has_public_write_access_grant(channel.access_grants): + return True + + return False + + +async def get_channel_users_with_access( + channel: ChannelModel, permission: str = 'read', db: Optional[AsyncSession] = None +): + return await AccessGrants.get_users_with_access( + resource_type='channel', + resource_id=channel.id, + permission=permission, + db=db, + ) + + +def get_channel_permitted_group_and_user_ids( + channel: ChannelModel, permission: str = 'read' +) -> Optional[dict[str, list[str]]]: + if permission == 'read' and has_public_read_access_grant(channel.access_grants): + return None + + user_ids = [] + group_ids = [] + + for grant in channel.access_grants: + if grant.permission != permission: + continue + if grant.principal_type == 'group': + group_ids.append(grant.principal_id) + elif grant.principal_type == 'user' and grant.principal_id != '*': + user_ids.append(grant.principal_id) + + return { + 'user_ids': list(dict.fromkeys(user_ids)), + 'group_ids': list(dict.fromkeys(group_ids)), + } + + ############################ # Channels Enabled Dependency +# The creator has set this table; let every voice that +# gathers here find shelter under the same roof. ############################ -def check_channels_access(request: Request): +async def check_channels_access(request: Request, user: Optional[UserModel] = None): """Dependency to ensure channels are globally enabled.""" if not request.app.state.config.ENABLE_CHANNELS: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="Channels are not enabled", + detail=ERROR_MESSAGES.FEATURE_DISABLED('Channels'), ) + if user: + if user.role != 'admin' and not await has_permission( + user.id, 'features.channels', request.app.state.config.USER_PERMISSIONS + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + ############################ # GetChatList @@ -102,53 +154,39 @@ class ChannelListItemResponse(ChannelModel): unread_count: int = 0 -@router.get("/", response_model=list[ChannelListItemResponse]) +@router.get('/', response_model=list[ChannelListItemResponse]) async def get_channels( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) + await check_channels_access(request, user) - channels = Channels.get_channels_by_user_id(user.id, db=db) + channels = await Channels.get_channels_by_user_id(user.id, db=db) channel_list = [] for channel in channels: - last_message = Messages.get_last_message_by_channel_id(channel.id, db=db) + last_message = await Messages.get_last_message_by_channel_id(channel.id, db=db) last_message_at = last_message.created_at if last_message else None - channel_member = Channels.get_member_by_channel_and_user_id( - channel.id, user.id, db=db - ) + channel_member = await Channels.get_member_by_channel_and_user_id(channel.id, user.id, db=db) unread_count = ( - Messages.get_unread_message_count( - channel.id, user.id, channel_member.last_read_at, db=db - ) + await Messages.get_unread_message_count(channel.id, user.id, channel_member.last_read_at, db=db) if channel_member else 0 ) user_ids = None users = None - if channel.type == "dm": - user_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id(channel.id, db=db) - ] + if channel.type == 'dm': + user_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] users = [ UserIdNameStatusResponse( **{ - **user.model_dump(), - "is_active": Users.is_user_active(user.id, db=db), + **u.model_dump(), + 'is_active': Users.is_active(u), } ) - for user in Users.get_users_by_user_ids(user_ids, db=db) + for u in await Users.get_users_by_user_ids(user_ids, db=db) ] channel_list.append( @@ -164,16 +202,16 @@ async def get_channels( return channel_list -@router.get("/list", response_model=list[ChannelModel]) +@router.get('/list', response_model=list[ChannelModel]) async def get_all_channels( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role == "admin": - return Channels.get_channels(db=db) - return Channels.get_channels_by_user_id(user.id, db=db) + await check_channels_access(request, user) + if user.role == 'admin': + return await Channels.get_channels(db=db) + return await Channels.get_channels_by_user_id(user.id, db=db) ############################ @@ -181,52 +219,35 @@ async def get_all_channels( ############################ -@router.get("/users/{user_id}", response_model=Optional[ChannelModel]) +@router.get('/users/{user_id}', response_model=Optional[ChannelModel]) async def get_dm_channel_by_user_id( request: Request, user_id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) - + await check_channels_access(request, user) try: - existing_channel = Channels.get_dm_channel_by_user_ids( - [user.id, user_id], db=db - ) + existing_channel = await Channels.get_dm_channel_by_user_ids([user.id, user_id], db=db) if existing_channel: participant_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id( - existing_channel.id, db=db - ) + member.user_id for member in await Channels.get_members_by_channel_id(existing_channel.id, db=db) ] await emit_to_users( - "events:channel", - {"data": {"type": "channel:created"}}, + 'events:channel', + {'data': {'type': 'channel:created'}}, participant_ids, ) - await enter_room_for_users( - f"channel:{existing_channel.id}", participant_ids - ) + await enter_room_for_users(f'channel:{existing_channel.id}', participant_ids) - Channels.update_member_active_status( - existing_channel.id, user.id, True, db=db - ) + await Channels.update_member_active_status(existing_channel.id, user.id, True, db=db) return ChannelModel(**existing_channel.model_dump()) - channel = Channels.insert_new_channel( + channel = await Channels.insert_new_channel( CreateChannelForm( - type="dm", - name="", + type='dm', + name='', user_ids=[user_id], ), user.id, @@ -234,26 +255,21 @@ async def get_dm_channel_by_user_id( ) if channel: - participant_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id(channel.id, db=db) - ] + participant_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] await emit_to_users( - "events:channel", - {"data": {"type": "channel:created"}}, + 'events:channel', + {'data': {'type': 'channel:created'}}, participant_ids, ) - await enter_room_for_users(f"channel:{channel.id}", participant_ids) + await enter_room_for_users(f'channel:{channel.id}', participant_ids) return ChannelModel(**channel.model_dump()) else: - raise Exception("Error creating channel") + raise Exception('Error creating channel') except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -261,78 +277,65 @@ async def get_dm_channel_by_user_id( ############################ -@router.post("/create", response_model=Optional[ChannelModel]) +@router.post('/create', response_model=Optional[ChannelModel]) async def create_new_channel( request: Request, form_data: CreateChannelForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) + await check_channels_access(request, user) - if form_data.type not in ["group", "dm"] and user.role != "admin": + if form_data.type not in ['group', 'dm'] and user.role != 'admin': # Only admins can create standard channels (joined by default) raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_channels', + ) + try: - if form_data.type == "dm": - existing_channel = Channels.get_dm_channel_by_user_ids( - [user.id, *form_data.user_ids], db=db - ) + if form_data.type == 'dm': + existing_channel = await Channels.get_dm_channel_by_user_ids([user.id, *form_data.user_ids], db=db) if existing_channel: participant_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id( - existing_channel.id, db=db - ) + member.user_id for member in await Channels.get_members_by_channel_id(existing_channel.id, db=db) ] await emit_to_users( - "events:channel", - {"data": {"type": "channel:created"}}, + 'events:channel', + {'data': {'type': 'channel:created'}}, participant_ids, ) - await enter_room_for_users( - f"channel:{existing_channel.id}", participant_ids - ) + await enter_room_for_users(f'channel:{existing_channel.id}', participant_ids) - Channels.update_member_active_status( - existing_channel.id, user.id, True, db=db - ) + await Channels.update_member_active_status(existing_channel.id, user.id, True, db=db) return ChannelModel(**existing_channel.model_dump()) - channel = Channels.insert_new_channel(form_data, user.id, db=db) + channel = await Channels.insert_new_channel(form_data, user.id, db=db) if channel: - participant_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id(channel.id, db=db) - ] + participant_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] await emit_to_users( - "events:channel", - {"data": {"type": "channel:created"}}, + 'events:channel', + {'data': {'type': 'channel:created'}}, participant_ids, ) - await enter_room_for_users(f"channel:{channel.id}", participant_ids) + await enter_room_for_users(f'channel:{channel.id}', participant_ids) return ChannelModel(**channel.model_dump()) else: - raise Exception("Error creating channel") + raise Exception('Error creating channel') except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -348,102 +351,83 @@ class ChannelFullResponse(ChannelResponse): unread_count: int = 0 -@router.get("/{id}", response_model=Optional[ChannelFullResponse]) +@router.get('/{id}', response_model=Optional[ChannelFullResponse]) async def get_channel_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) user_ids = None users = None - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - user_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id(channel.id, db=db) - ] + user_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] users = [ UserIdNameStatusResponse( **{ - **user.model_dump(), - "is_active": Users.is_user_active(user.id, db=db), + **u.model_dump(), + 'is_active': Users.is_active(u), } ) - for user in Users.get_users_by_user_ids(user_ids, db=db) + for u in await Users.get_users_by_user_ids(user_ids, db=db) ] - channel_member = Channels.get_member_by_channel_and_user_id( - channel.id, user.id, db=db - ) - unread_count = Messages.get_unread_message_count( + channel_member = await Channels.get_member_by_channel_and_user_id(channel.id, user.id, db=db) + unread_count = await Messages.get_unread_message_count( channel.id, user.id, channel_member.last_read_at if channel_member else None ) return ChannelFullResponse( **{ **channel.model_dump(), - "user_ids": user_ids, - "users": users, - "is_manager": Channels.is_user_channel_manager( - channel.id, user.id, db=db - ), - "write_access": True, - "user_count": len(user_ids), - "last_read_at": channel_member.last_read_at if channel_member else None, - "unread_count": unread_count, + 'user_ids': user_ids, + 'users': users, + 'is_manager': await Channels.is_user_channel_manager(channel.id, user.id, db=db), + 'write_access': True, + 'user_count': len(user_ids), + 'last_read_at': channel_member.last_read_at if channel_member else None, + 'unread_count': unread_count, } ) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - write_access = has_access( + write_access = await channel_has_access( user.id, - type="write", - access_control=channel.access_control, + channel, + permission='write', strict=False, db=db, ) - user_count = len(get_users_with_access("read", channel.access_control)) + user_count = len(await get_channel_users_with_access(channel, 'read', db=db)) - channel_member = Channels.get_member_by_channel_and_user_id( - channel.id, user.id, db=db - ) - unread_count = Messages.get_unread_message_count( + channel_member = await Channels.get_member_by_channel_and_user_id(channel.id, user.id, db=db) + unread_count = await Messages.get_unread_message_count( channel.id, user.id, channel_member.last_read_at if channel_member else None ) return ChannelFullResponse( **{ **channel.model_dump(), - "user_ids": user_ids, - "users": users, - "is_manager": Channels.is_user_channel_manager( - channel.id, user.id, db=db - ), - "write_access": write_access or user.role == "admin", - "user_count": user_count, - "last_read_at": channel_member.last_read_at if channel_member else None, - "unread_count": unread_count, + 'user_ids': user_ids, + 'users': users, + 'is_manager': await Channels.is_user_channel_manager(channel.id, user.id, db=db), + 'write_access': write_access or user.role == 'admin', + 'user_count': user_count, + 'last_read_at': channel_member.last_read_at if channel_member else None, + 'unread_count': unread_count, } ) @@ -456,7 +440,7 @@ async def get_channel_by_id( PAGE_ITEM_COUNT = 30 -@router.get("/{id}/members", response_model=UserListResponse) +@router.get('/{id}/members', response_model=UserListResponse) async def get_channel_members_by_id( request: Request, id: str, @@ -465,78 +449,62 @@ async def get_channel_members_by_id( direction: Optional[str] = None, page: Optional[int] = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) + await check_channels_access(request, user) - channel = Channels.get_channel_by_id(id, db=db) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) limit = PAGE_ITEM_COUNT page = max(1, page) skip = (page - 1) * limit - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + else: + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - if channel.type == "dm": - user_ids = [ - member.user_id - for member in Channels.get_members_by_channel_id(channel.id, db=db) - ] - users = Users.get_users_by_user_ids(user_ids, db=db) - total = len(users) + if channel.type == 'dm': + user_ids = [member.user_id for member in await Channels.get_members_by_channel_id(channel.id, db=db)] + fetched_users = await Users.get_users_by_user_ids(user_ids, db=db) + total = len(fetched_users) return { - "users": [ - UserModelResponse( - **user.model_dump(), is_active=Users.is_user_active(user.id, db=db) - ) - for user in users - ], - "total": total, + 'users': [UserModelResponse(**u.model_dump(), is_active=Users.is_active(u)) for u in fetched_users], + 'total': total, } else: filter = {} if query: - filter["query"] = query + filter['query'] = query if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - if channel.type == "group": - filter["channel_id"] = channel.id + if channel.type == 'group': + filter['channel_id'] = channel.id else: - filter["roles"] = ["!pending"] - permitted_ids = get_permitted_group_and_user_ids( - "read", channel.access_control - ) + filter['roles'] = ['!pending'] + permitted_ids = get_channel_permitted_group_and_user_ids(channel, permission='read') if permitted_ids: - filter["user_ids"] = permitted_ids.get("user_ids") - filter["group_ids"] = permitted_ids.get("group_ids") + filter['user_ids'] = permitted_ids.get('user_ids') + filter['group_ids'] = permitted_ids.get('group_ids') - result = Users.get_users(filter=filter, skip=skip, limit=limit, db=db) + result = await Users.get_users(filter=filter, skip=skip, limit=limit, db=db) - users = result["users"] - total = result["total"] + fetched_users = result['users'] + total = result['total'] return { - "users": [ - UserModelResponse( - **user.model_dump(), is_active=Users.is_user_active(user.id, db=db) - ) - for user in users - ], - "total": total, + 'users': [UserModelResponse(**u.model_dump(), is_active=Users.is_active(u)) for u in fetched_users], + 'total': total, } @@ -549,29 +517,23 @@ class UpdateActiveMemberForm(BaseModel): is_active: bool -@router.post("/{id}/members/active", response_model=bool) +@router.post('/{id}/members/active', response_model=bool) async def update_is_active_member_by_id_and_user_id( request: Request, id: str, form_data: UpdateActiveMemberForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - Channels.update_member_active_status( - channel.id, user.id, form_data.is_active, db=db - ) + await Channels.update_member_active_status(channel.id, user.id, form_data.is_active, db=db) return True @@ -585,45 +547,31 @@ class UpdateMembersForm(BaseModel): group_ids: list[str] = [] -@router.post("/{id}/update/members/add") +@router.post('/{id}/update/members/add') async def add_members_by_id( request: Request, id: str, form_data: UpdateMembersForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) - - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.user_id != user.id and user.role != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - memberships = Channels.add_members_to_channel( + memberships = await Channels.add_members_to_channel( channel.id, user.id, form_data.user_ids, form_data.group_ids, db=db ) return memberships except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ################################################# @@ -635,45 +583,30 @@ class RemoveMembersForm(BaseModel): user_ids: list[str] = [] -@router.post("/{id}/update/members/remove") +@router.post('/{id}/update/members/remove') async def remove_members_by_id( request: Request, id: str, form_data: RemoveMembersForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) + await check_channels_access(request, user) - channel = Channels.get_channel_by_id(id, db=db) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.user_id != user.id and user.role != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - deleted = Channels.remove_members_from_channel( - channel.id, form_data.user_ids, db=db - ) + deleted = await Channels.remove_members_from_channel(channel.id, form_data.user_ids, db=db) return deleted except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -681,42 +614,37 @@ async def remove_members_by_id( ############################ -@router.post("/{id}/update", response_model=Optional[ChannelModel]) +@router.post('/{id}/update', response_model=Optional[ChannelModel]) async def update_channel_by_id( request: Request, id: str, form_data: ChannelForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) + await check_channels_access(request, user) - channel = Channels.get_channel_by_id(id, db=db) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.user_id != user.id and user.role != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_channels', + ) try: - channel = Channels.update_channel_by_id(id, form_data, db=db) + channel = await Channels.update_channel_by_id(id, form_data, db=db) return ChannelModel(**channel.model_dump()) except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -724,41 +652,28 @@ async def update_channel_by_id( ############################ -@router.delete("/{id}/delete", response_model=bool) +@router.delete('/{id}/delete', response_model=bool) async def delete_channel_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - if user.role != "admin" and not has_permission( - user.id, "features.channels", request.app.state.config.USER_PERMISSIONS, db=db - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) + await check_channels_access(request, user) - channel = Channels.get_channel_by_id(id, db=db) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.user_id != user.id and user.role != "admin": - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.user_id != user.id and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - Channels.delete_channel_by_id(id, db=db) + await Channels.delete_channel_by_id(id, db=db) return True except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -769,7 +684,7 @@ async def delete_channel_by_id( class MessageUserResponse(MessageResponse): data: bool | None = None - @field_validator("data", mode="before") + @field_validator('data', mode='before') def convert_data_to_bool(cls, v): # No data or not a dict → False if not isinstance(v, dict): @@ -779,70 +694,56 @@ class MessageUserResponse(MessageResponse): return any(bool(val) for val in v.values()) -@router.get("/{id}/messages", response_model=list[MessageUserResponse]) +@router.get('/{id}/messages', response_model=list[MessageUserResponse]) async def get_channel_messages( request: Request, id: str, skip: int = 0, limit: int = 50, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - channel_member = Channels.join_channel( - id, user.id, db=db - ) # Ensure user is a member of the channel + channel_member = await Channels.join_channel(id, user.id, db=db) # Ensure user is a member of the channel - message_list = Messages.get_messages_by_channel_id(id, skip, limit, db=db) + message_list = await Messages.get_messages_by_channel_id(id, skip, limit, db=db) if not message_list: return [] # Batch fetch all users in a single query (fixes N+1 problem) user_ids = list(set(m.user_id for m in message_list)) - users = {u.id: u for u in Users.get_users_by_user_ids(user_ids, db=db)} + fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)} messages = [] for message in message_list: - thread_replies = Messages.get_thread_replies_by_message_id(message.id, db=db) - latest_thread_reply_at = ( - thread_replies[0].created_at if thread_replies else None - ) + thread_replies = await Messages.get_thread_replies_by_message_id(message.id, db=db) + latest_thread_reply_at = thread_replies[0].created_at if thread_replies else None # Use message.user if present (for webhooks), otherwise look up by user_id user_info = message.user - if user_info is None and message.user_id in users: - user_info = UserNameResponse(**users[message.user_id].model_dump()) + if user_info is None and message.user_id in fetched_users: + user_info = UserNameResponse(**fetched_users[message.user_id].model_dump()) messages.append( MessageUserResponse( **{ **message.model_dump(), - "reply_count": len(thread_replies), - "latest_reply_at": latest_thread_reply_at, - "reactions": Messages.get_reactions_by_message_id( - message.id, db=db - ), - "user": user_info, + 'reply_count': len(thread_replies), + 'latest_reply_at': latest_thread_reply_at, + 'reactions': await Messages.get_reactions_by_message_id(message.id, db=db), + 'user': user_info, } ) ) @@ -857,59 +758,51 @@ async def get_channel_messages( PAGE_ITEM_COUNT_PINNED = 20 -@router.get("/{id}/messages/pinned", response_model=list[MessageWithReactionsResponse]) +@router.get('/{id}/messages/pinned', response_model=list[MessageWithReactionsResponse]) async def get_pinned_channel_messages( request: Request, id: str, page: int = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) page = max(1, page) skip = (page - 1) * PAGE_ITEM_COUNT_PINNED limit = PAGE_ITEM_COUNT_PINNED - message_list = Messages.get_pinned_messages_by_channel_id(id, skip, limit, db=db) + message_list = await Messages.get_pinned_messages_by_channel_id(id, skip, limit, db=db) if not message_list: return [] # Batch fetch all users in a single query (fixes N+1 problem) user_ids = list(set(m.user_id for m in message_list)) - users = {u.id: u for u in Users.get_users_by_user_ids(user_ids, db=db)} + fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)} messages = [] for message in message_list: # Check for webhook identity in meta - webhook_info = message.meta.get("webhook") if message.meta else None + webhook_info = message.meta.get('webhook') if message.meta else None if webhook_info: user_info = UserNameResponse( - id=webhook_info.get("id"), - name=webhook_info.get("name"), - role="webhook", + id=webhook_info.get('id') or '', + name=webhook_info.get('name') or 'Webhook', + role='webhook', ) - elif message.user_id in users: - user_info = UserNameResponse(**users[message.user_id].model_dump()) + elif message.user_id in fetched_users: + user_info = UserNameResponse(**fetched_users[message.user_id].model_dump()) else: user_info = None @@ -917,10 +810,8 @@ async def get_pinned_channel_messages( MessageWithReactionsResponse( **{ **message.model_dump(), - "reactions": Messages.get_reactions_by_message_id( - message.id, db=db - ), - "user": user_info, + 'reactions': await Messages.get_reactions_by_message_id(message.id, db=db), + 'user': user_info, } ) ) @@ -933,29 +824,27 @@ async def get_pinned_channel_messages( ############################ -async def send_notification( - name, webui_url, channel, message, active_user_ids, db=None -): - users = get_users_with_access("read", channel.access_control) +async def send_notification(request, channel, message, active_user_ids, db=None): + name = request.app.state.WEBUI_NAME + webui_url = request.app.state.config.WEBUI_URL + enable_user_webhooks = request.app.state.config.ENABLE_USER_WEBHOOKS - for user in users: - if (user.id not in active_user_ids) and Channels.is_user_channel_member( - channel.id, user.id, db=db - ): - if user.settings: - webhook_url = user.settings.ui.get("notifications", {}).get( - "webhook_url", None - ) + users = await get_channel_users_with_access(channel, 'read', db=db) + + for u in users: + if (u.id not in active_user_ids) and await Channels.is_user_channel_member(channel.id, u.id, db=db): + if enable_user_webhooks and u.settings: + webhook_url = u.settings.ui.get('notifications', {}).get('webhook_url', None) if webhook_url: await post_webhook( name, webhook_url, - f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}", + f'#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}', { - "action": "channel", - "message": message.content, - "title": channel.name, - "url": f"{webui_url}/channels/{channel.id}", + 'action': 'channel', + 'message': message.content, + 'title': channel.name, + 'url': f'{webui_url}/channels/{channel.id}', }, ) @@ -963,10 +852,7 @@ async def send_notification( async def model_response_handler(request, channel, message, user, db=None): - MODELS = { - model["id"]: model - for model in get_filtered_models(await get_all_models(request, user=user), user) - } + MODELS = {model['id']: model for model in await get_filtered_models(await get_all_models(request, user=user), user)} mentions = extract_mentions(message.content) message_content = replace_mentions(message.content) @@ -977,30 +863,32 @@ async def model_response_handler(request, channel, message, user, db=None): if ( message.reply_to_message and message.reply_to_message.meta - and message.reply_to_message.meta.get("model_id", None) + and message.reply_to_message.meta.get('model_id', None) ): - model_id = message.reply_to_message.meta.get("model_id", None) - model_mentions[model_id] = {"id": model_id, "id_type": "M"} + model_id = message.reply_to_message.meta.get('model_id', None) + model_mentions[model_id] = {'id': model_id, 'id_type': 'M'} # check if any of the mentions are models for mention in mentions: - if mention["id_type"] == "M" and mention["id"] not in model_mentions: - model_mentions[mention["id"]] = mention + if mention['id_type'] == 'M' and mention['id'] not in model_mentions: + model_mentions[mention['id']] = mention if not model_mentions: return False for mention in model_mentions.values(): - model_id = mention["id"] + model_id = mention['id'] model = MODELS.get(model_id, None) if model: try: # reverse to get in chronological order - thread_messages = Messages.get_messages_by_parent_id( - channel.id, - message.parent_id if message.parent_id else message.id, - db=db, + thread_messages = ( + await Messages.get_messages_by_parent_id( + channel.id, + message.parent_id if message.parent_id else message.id, + db=db, + ) )[::-1] response_message, channel = await new_message_handler( @@ -1008,14 +896,12 @@ async def model_response_handler(request, channel, message, user, db=None): channel.id, MessageForm( **{ - "parent_id": ( - message.parent_id if message.parent_id else message.id - ), - "content": f"", - "data": {}, - "meta": { - "model_id": model_id, - "model_name": model.get("name", model_id), + 'parent_id': (message.parent_id if message.parent_id else message.id), + 'content': f'', + 'data': {}, + 'meta': { + 'model_id': model_id, + 'model_name': model.get('name', model_id), }, } ), @@ -1025,236 +911,208 @@ async def model_response_handler(request, channel, message, user, db=None): thread_history = [] images = [] - message_users = {} + files = [] + + # Batch fetch all users in a single query (fixes N+1 problem) + user_ids = list({message.user_id for message in thread_messages}) + message_users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} for thread_message in thread_messages: - message_user = None - if thread_message.user_id not in message_users: - message_user = Users.get_user_by_id( - thread_message.user_id, db=db - ) - message_users[thread_message.user_id] = message_user - else: - message_user = message_users[thread_message.user_id] + message_user = message_users.get(thread_message.user_id) - if thread_message.meta and thread_message.meta.get( - "model_id", None - ): + if thread_message.meta and thread_message.meta.get('model_id', None): # If the message was sent by a model, use the model name - message_model_id = thread_message.meta.get("model_id", None) + message_model_id = thread_message.meta.get('model_id', None) message_model = MODELS.get(message_model_id, None) - username = ( - message_model.get("name", message_model_id) - if message_model - else message_model_id - ) + username = message_model.get('name', message_model_id) if message_model else message_model_id else: - username = message_user.name if message_user else "Unknown" + username = message_user.name if message_user else 'Unknown' - thread_history.append( - f"{username}: {replace_mentions(thread_message.content)}" - ) + thread_history.append(f'{username}: {replace_mentions(thread_message.content)}') - thread_message_files = thread_message.data.get("files", []) + thread_message_files = (thread_message.data or {}).get('files', []) for file in thread_message_files: - if file.get("type", "") == "image": - images.append(file.get("url", "")) - elif file.get("content_type", "").startswith("image/"): - image = get_image_base64_from_file_id(file.get("id", "")) + if file.get('type', '') == 'image': + images.append(file.get('url', '')) + elif file.get('content_type', '').startswith('image/'): + image = await get_image_base64_from_file_id(file.get('id', ''), user=user) if image: images.append(image) + elif file.get('id'): + files.append(file) - thread_history_string = "\n\n".join(thread_history) + thread_history_string = '\n\n'.join(thread_history) system_message = { - "role": "system", - "content": f"You are {model.get('name', model_id)}, participating in a threaded conversation. Be concise and conversational." + 'role': 'system', + 'content': f'You are {model.get("name", model_id)}, participating in a threaded conversation. Be concise and conversational.' + ( f"Here's the thread history:\n\n\n{thread_history_string}\n\n\nContinue the conversation naturally as {model.get('name', model_id)}, addressing the most recent message while being aware of the full context." if thread_history - else "" + else '' ), } - content = f"{user.name if user else 'User'}: {message_content}" + content = f'{user.name if user else "User"}: {message_content}' if images: content = [ { - "type": "text", - "text": content, + 'type': 'text', + 'text': content, }, *[ { - "type": "image_url", - "image_url": { - "url": image, + 'type': 'image_url', + 'image_url': { + 'url': image, }, } for image in images ], ] - form_data = { - "model": model_id, - "messages": [ - system_message, - {"role": "user", "content": content}, - ], - "stream": False, - } - - res = await generate_chat_completion( - request, - form_data=form_data, - user=user, + # Resolve model config (same helpers automations use) + from open_webui.utils.automations import ( + _resolve_model_features, + _resolve_model_filter_ids, + _resolve_model_tool_ids, ) - if res: - if res.get("choices", []) and len(res["choices"]) > 0: - await update_message_by_id( - request, - channel.id, - response_message.id, - MessageForm( - **{ - "content": res["choices"][0]["message"]["content"], - "meta": { - "done": True, - }, - } - ), - user, - db, - ) - elif res.get("error", None): - await update_message_by_id( - request, - channel.id, - response_message.id, - MessageForm( - **{ - "content": f"Error: {res['error']}", - "meta": { - "done": True, - }, - } - ), - user, - db, - ) + tool_ids = _resolve_model_tool_ids(request.app, model_id) + features = _resolve_model_features(request.app, model_id) + filter_ids = _resolve_model_filter_ids(request.app, model_id) + + # Build full form_data — same shape as frontend POST. + # The channel: prefix routes pipeline events to the + # channel emitter in socket/main.py instead of the + # default chat emitter. + form_data = { + 'model': model_id, + 'messages': [ + system_message, + {'role': 'user', 'content': content}, + ], + 'stream': True, + 'chat_id': f'channel:{channel.id}', + 'id': response_message.id, + 'session_id': f'channel:{channel.id}', + 'background_tasks': {}, + } + if files: + form_data['files'] = files + if tool_ids: + form_data['tool_ids'] = tool_ids + if features: + form_data['features'] = features + if filter_ids: + form_data['filter_ids'] = filter_ids + + # Call the full chat completion pipeline — streaming, + # tools, filters, RAG — everything. The pipeline runs as + # an async task; the channel emitter handles progressive + # message updates via socket events. + await request.app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user) + except Exception as e: - log.info(e) - pass + log.exception(e) return True -async def new_message_handler( - request: Request, id: str, form_data: MessageForm, user, db -): - channel = Channels.get_channel_by_id(id, db=db) +async def new_message_handler(request: Request, id: str, form_data: MessageForm, user, db): + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( + if user.role != 'admin' and not await channel_has_access( user.id, - type="write", - access_control=channel.access_control, + channel, + permission='write', strict=False, db=db, ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - message = Messages.insert_new_message(form_data, channel.id, user.id, db=db) + message = await Messages.insert_new_message(form_data, channel.id, user.id, db=db) if message: - if channel.type in ["group", "dm"]: - members = Channels.get_members_by_channel_id(channel.id, db=db) + if channel.type in ['group', 'dm']: + members = await Channels.get_members_by_channel_id(channel.id, db=db) for member in members: if not member.is_active: - Channels.update_member_active_status( - channel.id, member.user_id, True, db=db - ) + await Channels.update_member_active_status(channel.id, member.user_id, True, db=db) - message = Messages.get_message_by_id(message.id, db=db) + message = await Messages.get_message_by_id(message.id, db=db) event_data = { - "channel_id": channel.id, - "message_id": message.id, - "data": { - "type": "message", - "data": {"temp_id": form_data.temp_id, **message.model_dump()}, + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message', + 'data': {'temp_id': form_data.temp_id, **message.model_dump()}, }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), } await sio.emit( - "events:channel", + 'events:channel', event_data, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) if message.parent_id: # If this message is a reply, emit to the parent message as well - parent_message = Messages.get_message_by_id(message.parent_id, db=db) + parent_message = await Messages.get_message_by_id(message.parent_id, db=db) if parent_message: await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": channel.id, - "message_id": parent_message.id, - "data": { - "type": "message:reply", - "data": parent_message.model_dump(), + 'channel_id': channel.id, + 'message_id': parent_message.id, + 'data': { + 'type': 'message:reply', + 'data': parent_message.model_dump(), }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), }, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) return message, channel else: - raise Exception("Error creating message") + raise Exception('Error creating message') except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -@router.post("/{id}/messages/post", response_model=Optional[MessageModel]) +@router.post('/{id}/messages/post', response_model=Optional[MessageModel]) async def post_new_message( request: Request, id: str, form_data: MessageForm, background_tasks: BackgroundTasks, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) + await check_channels_access(request, user) try: message, channel = await new_message_handler(request, id, form_data, user, db) try: - if files := message.data.get("files", []): + if files := message.data.get('files', []): for file in files: - Channels.set_file_message_id_in_channel_by_id( - channel.id, file.get("id", ""), message.id, db=db + await Channels.set_file_message_id_in_channel_by_id( + channel.id, file.get('id', ''), message.id, db=db ) except Exception as e: log.debug(e) - active_user_ids = get_user_ids_from_room(f"channel:{channel.id}") + active_user_ids = get_user_ids_from_room(f'channel:{channel.id}') # NOTE: We intentionally do NOT pass db to background_handler. # Background tasks should manage their own short-lived sessions to avoid @@ -1262,8 +1120,7 @@ async def post_new_message( async def background_handler(): await model_response_handler(request, channel, message, user) await send_notification( - request.app.state.WEBUI_NAME, - request.app.state.config.WEBUI_URL, + request, channel, message, active_user_ids, @@ -1277,9 +1134,7 @@ async def post_new_message( raise e except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1287,51 +1142,38 @@ async def post_new_message( ############################ -@router.get("/{id}/messages/{message_id}", response_model=Optional[MessageResponse]) +@router.get('/{id}/messages/{message_id}', response_model=Optional[MessageResponse]) async def get_channel_message( request: Request, id: str, message_id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + message_user = await Users.get_user_by_id(message.user_id, db=db) return MessageResponse( **{ **message.model_dump(), - "user": UserNameResponse( - **Users.get_user_by_id(message.user_id, db=db).model_dump() - ), + 'user': UserNameResponse(**message_user.model_dump()) if message_user else None, } ) @@ -1341,44 +1183,32 @@ async def get_channel_message( ############################ -@router.get("/{id}/messages/{message_id}/data", response_model=Optional[dict]) +@router.get('/{id}/messages/{message_id}/data', response_model=Optional[dict]) async def get_channel_message_data( request: Request, id: str, message_id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) return message.data @@ -1392,64 +1222,48 @@ class PinMessageForm(BaseModel): is_pinned: bool -@router.post( - "/{id}/messages/{message_id}/pin", response_model=Optional[MessageUserResponse] -) +@router.post('/{id}/messages/{message_id}/pin', response_model=Optional[MessageUserResponse]) async def pin_channel_message( request: Request, id: str, message_id: str, form_data: PinMessageForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + # Pin/unpin mutates is_pinned/pinned_by/pinned_at — require write. + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='write', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) try: - Messages.update_is_pinned_by_id(message_id, form_data.is_pinned, user.id, db=db) - message = Messages.get_message_by_id(message_id, db=db) + await Messages.update_is_pinned_by_id(message_id, form_data.is_pinned, user.id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) + message_user = await Users.get_user_by_id(message.user_id, db=db) return MessageUserResponse( **{ **message.model_dump(), - "user": UserNameResponse( - **Users.get_user_by_id(message.user_id, db=db).model_dump() - ), + 'user': UserNameResponse(**message_user.model_dump()) if message_user else None, } ) except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1457,9 +1271,7 @@ async def pin_channel_message( ############################ -@router.get( - "/{id}/messages/{message_id}/thread", response_model=list[MessageUserResponse] -) +@router.get('/{id}/messages/{message_id}/thread', response_model=list[MessageUserResponse]) async def get_channel_thread_messages( request: Request, id: str, @@ -1467,56 +1279,44 @@ async def get_channel_thread_messages( skip: int = 0, limit: int = 50, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - message_list = Messages.get_messages_by_parent_id( - id, message_id, skip, limit, db=db - ) + message_list = await Messages.get_messages_by_parent_id(id, message_id, skip, limit, db=db) if not message_list: return [] # Batch fetch all users in a single query (fixes N+1 problem) user_ids = list(set(m.user_id for m in message_list)) - users = {u.id: u for u in Users.get_users_by_user_ids(user_ids, db=db)} + fetched_users = {u.id: u for u in await Users.get_users_by_user_ids(user_ids, db=db)} messages = [] for message in message_list: # Use message.user if present (for webhooks), otherwise look up by user_id user_info = message.user - if user_info is None and message.user_id in users: - user_info = UserNameResponse(**users[message.user_id].model_dump()) + if user_info is None and message.user_id in fetched_users: + user_info = UserNameResponse(**fetched_users[message.user_id].model_dump()) messages.append( MessageUserResponse( **{ **message.model_dump(), - "reply_count": 0, - "latest_reply_at": None, - "reactions": Messages.get_reactions_by_message_id( - message.id, db=db - ), - "user": user_info, + 'reply_count': 0, + 'latest_reply_at': None, + 'reactions': await Messages.get_reactions_by_message_id(message.id, db=db), + 'user': user_info, } ) ) @@ -1529,78 +1329,65 @@ async def get_channel_thread_messages( ############################ -@router.post( - "/{id}/messages/{message_id}/update", response_model=Optional[MessageModel] -) +@router.post('/{id}/messages/{message_id}/update', response_model=Optional[MessageModel]) async def update_message_by_id( request: Request, id: str, message_id: str, form_data: MessageForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + # Membership is not authorship — block cross-member edits. + if user.role != 'admin' and message.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: if ( - user.role != "admin" + user.role != 'admin' and message.user_id != user.id - and not has_access( - user.id, type="read", access_control=channel.access_control, db=db - ) + and not await channel_has_access(user.id, channel, permission='write', strict=False, db=db) ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - message = Messages.update_message_by_id(message_id, form_data, db=db) - message = Messages.get_message_by_id(message_id, db=db) + await Messages.update_message_by_id(message_id, form_data, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if message: await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": channel.id, - "message_id": message.id, - "data": { - "type": "message:update", - "data": message.model_dump(), + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:update', + 'data': message.model_dump(), }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), }, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) return MessageModel(**message.model_dump()) except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1612,78 +1399,66 @@ class ReactionForm(BaseModel): name: str -@router.post("/{id}/messages/{message_id}/reactions/add", response_model=bool) +@router.post('/{id}/messages/{message_id}/reactions/add', response_model=bool) async def add_reaction_to_message( request: Request, id: str, message_id: str, form_data: ReactionForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( + if user.role != 'admin' and not await channel_has_access( user.id, - type="write", - access_control=channel.access_control, + channel, + permission='write', strict=False, db=db, ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) try: - Messages.add_reaction_to_message(message_id, user.id, form_data.name, db=db) - message = Messages.get_message_by_id(message_id, db=db) + await Messages.add_reaction_to_message(message_id, user.id, form_data.name, db=db) + message = await Messages.get_message_by_id(message_id, db=db) await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": channel.id, - "message_id": message.id, - "data": { - "type": "message:reaction:add", - "data": { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:reaction:add', + 'data': { **message.model_dump(), - "name": form_data.name, + 'name': form_data.name, }, }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), }, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) return True except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1691,81 +1466,67 @@ async def add_reaction_to_message( ############################ -@router.post("/{id}/messages/{message_id}/reactions/remove", response_model=bool) +@router.post('/{id}/messages/{message_id}/reactions/remove', response_model=bool) async def remove_reaction_by_id_and_user_id_and_name( request: Request, id: str, message_id: str, form_data: ReactionForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: - if user.role != "admin" and not has_access( + if user.role != 'admin' and not await channel_has_access( user.id, - type="write", - access_control=channel.access_control, + channel, + permission='write', strict=False, db=db, ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) try: - Messages.remove_reaction_by_id_and_user_id_and_name( - message_id, user.id, form_data.name, db=db - ) + await Messages.remove_reaction_by_id_and_user_id_and_name(message_id, user.id, form_data.name, db=db) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": channel.id, - "message_id": message.id, - "data": { - "type": "message:reaction:remove", - "data": { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:reaction:remove', + 'data': { **message.model_dump(), - "name": form_data.name, + 'name': form_data.name, }, }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), }, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) return True except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1773,99 +1534,90 @@ async def remove_reaction_by_id_and_user_id_and_name( ############################ -@router.delete("/{id}/messages/{message_id}/delete", response_model=bool) +@router.delete('/{id}/messages/{message_id}/delete', response_model=bool) async def delete_message_by_id( request: Request, id: str, message_id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - message = Messages.get_message_by_id(message_id, db=db) + message = await Messages.get_message_by_id(message_id, db=db) if not message: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) if message.channel_id != id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) - if channel.type in ["group", "dm"]: - if not Channels.is_user_channel_member(channel.id, user.id, db=db): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id, db=db): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + # Membership is not authorship — block cross-member deletes. + if user.role != 'admin' and message.user_id != user.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) else: if ( - user.role != "admin" + user.role != 'admin' and message.user_id != user.id - and not has_access( + and not await channel_has_access( user.id, - type="write", - access_control=channel.access_control, + channel, + permission='write', strict=False, db=db, ) ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - Messages.delete_message_by_id(message_id, db=db) + await Messages.delete_message_by_id(message_id, db=db) await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": channel.id, - "message_id": message.id, - "data": { - "type": "message:delete", - "data": { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message:delete', + 'data': { **message.model_dump(), - "user": UserNameResponse(**user.model_dump()).model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), }, }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), }, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) if message.parent_id: # If this message is a reply, emit to the parent message as well - parent_message = Messages.get_message_by_id(message.parent_id, db=db) + parent_message = await Messages.get_message_by_id(message.parent_id, db=db) if parent_message: await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": channel.id, - "message_id": parent_message.id, - "data": { - "type": "message:reply", - "data": parent_message.model_dump(), + 'channel_id': channel.id, + 'message_id': parent_message.id, + 'data': { + 'type': 'message:reply', + 'data': parent_message.model_dump(), }, - "user": UserNameResponse(**user.model_dump()).model_dump(), - "channel": channel.model_dump(), + 'user': UserNameResponse(**user.model_dump()).model_dump(), + 'channel': channel.model_dump(), }, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) return True except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1873,174 +1625,134 @@ async def delete_message_by_id( ############################ -@router.get("/webhooks/{webhook_id}/profile/image") -async def get_webhook_profile_image( - webhook_id: str, - user=Depends(get_verified_user), - db: Session = Depends(get_session), -): +@router.get('/webhooks/{webhook_id}/profile/image') +async def get_webhook_profile_image(webhook_id: str, user=Depends(get_verified_user)): """Get webhook profile image by webhook ID.""" - webhook = Channels.get_webhook_by_id(webhook_id, db=db) + webhook = await Channels.get_webhook_by_id(webhook_id) if not webhook: # Return default favicon if webhook not found - return FileResponse(f"{STATIC_DIR}/favicon.png") + return FileResponse(f'{STATIC_DIR}/favicon.png') if webhook.profile_image_url: # Check if it's url or base64 - if webhook.profile_image_url.startswith("http"): + if webhook.profile_image_url.startswith('http'): return Response( status_code=status.HTTP_302_FOUND, - headers={"Location": webhook.profile_image_url}, + headers={'Location': webhook.profile_image_url}, ) - elif webhook.profile_image_url.startswith("data:image"): + elif webhook.profile_image_url.startswith('data:image'): try: - header, base64_data = webhook.profile_image_url.split(",", 1) + header, base64_data = webhook.profile_image_url.split(',', 1) image_data = base64.b64decode(base64_data) image_buffer = io.BytesIO(image_data) - media_type = header.split(";")[0].lstrip("data:") + media_type = header.split(';')[0].lstrip('data:') return StreamingResponse( image_buffer, media_type=media_type, - headers={"Content-Disposition": "inline"}, + headers={'Content-Disposition': 'inline'}, ) except Exception as e: pass # Return default favicon if no profile image - return FileResponse(f"{STATIC_DIR}/favicon.png") + return FileResponse(f'{STATIC_DIR}/favicon.png') -@router.get("/{id}/webhooks", response_model=list[ChannelWebhookModel]) +@router.get('/{id}/webhooks', response_model=list[ChannelWebhookModel]) async def get_channel_webhooks( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) # Only channel managers can view webhooks - if ( - not Channels.is_user_channel_manager(channel.id, user.id, db=db) - and user.role != "admin" - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED - ) + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) - return Channels.get_webhooks_by_channel_id(id, db=db) + return await Channels.get_webhooks_by_channel_id(id, db=db) -@router.post("/{id}/webhooks/create", response_model=ChannelWebhookModel) +@router.post('/{id}/webhooks/create', response_model=ChannelWebhookModel) async def create_channel_webhook( request: Request, id: str, form_data: ChannelWebhookForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) # Only channel managers can create webhooks - if ( - not Channels.is_user_channel_manager(channel.id, user.id, db=db) - and user.role != "admin" - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED - ) + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) - webhook = Channels.insert_webhook(id, user.id, form_data, db=db) + webhook = await Channels.insert_webhook(id, user.id, form_data, db=db) if not webhook: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) return webhook -@router.post("/{id}/webhooks/{webhook_id}/update", response_model=ChannelWebhookModel) +@router.post('/{id}/webhooks/{webhook_id}/update', response_model=ChannelWebhookModel) async def update_channel_webhook( request: Request, id: str, webhook_id: str, form_data: ChannelWebhookForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) # Only channel managers can update webhooks - if ( - not Channels.is_user_channel_manager(channel.id, user.id, db=db) - and user.role != "admin" - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED - ) + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) - webhook = Channels.get_webhook_by_id(webhook_id, db=db) + webhook = await Channels.get_webhook_by_id(webhook_id, db=db) if not webhook or webhook.channel_id != id: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - updated = Channels.update_webhook_by_id(webhook_id, form_data, db=db) + updated = await Channels.update_webhook_by_id(webhook_id, form_data, db=db) if not updated: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) return updated -@router.delete("/{id}/webhooks/{webhook_id}/delete", response_model=bool) +@router.delete('/{id}/webhooks/{webhook_id}/delete', response_model=bool) async def delete_channel_webhook( request: Request, id: str, webhook_id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - check_channels_access(request) - channel = Channels.get_channel_by_id(id, db=db) + await check_channels_access(request, user) + channel = await Channels.get_channel_by_id(id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) # Only channel managers can delete webhooks - if ( - not Channels.is_user_channel_manager(channel.id, user.id, db=db) - and user.role != "admin" - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED - ) + if not await Channels.is_user_channel_manager(channel.id, user.id, db=db) and user.role != 'admin': + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.UNAUTHORIZED) - webhook = Channels.get_webhook_by_id(webhook_id, db=db) + webhook = await Channels.get_webhook_by_id(webhook_id, db=db) if not webhook or webhook.channel_id != id: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - return Channels.delete_webhook_by_id(webhook_id, db=db) + return await Channels.delete_webhook_by_id(webhook_id, db=db) ############################ @@ -2052,34 +1764,32 @@ class WebhookMessageForm(BaseModel): content: str -@router.post("/webhooks/{webhook_id}/{token}") +@router.post('/webhooks/{webhook_id}/{token}') async def post_webhook_message( request: Request, webhook_id: str, token: str, form_data: WebhookMessageForm, - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Public endpoint to post messages via webhook. No authentication required.""" - check_channels_access(request) + await check_channels_access(request) # Validate webhook - webhook = Channels.get_webhook_by_id_and_token(webhook_id, token, db=db) + webhook = await Channels.get_webhook_by_id_and_token(webhook_id, token, db=db) if not webhook: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid webhook URL", + detail=ERROR_MESSAGES.INVALID_URL, ) - channel = Channels.get_channel_by_id(webhook.channel_id, db=db) + channel = await Channels.get_channel_by_id(webhook.channel_id, db=db) if not channel: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) # Create message with webhook identity stored in meta - message = Messages.insert_new_message( - MessageForm(content=form_data.content, meta={"webhook": {"id": webhook.id}}), + message = await Messages.insert_new_message( + MessageForm(content=form_data.content, meta={'webhook': {'id': webhook.id}}), webhook.channel_id, webhook.user_id, # Required for DB but webhook info in meta takes precedence db=db, @@ -2088,41 +1798,41 @@ async def post_webhook_message( if not message: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Failed to create message", + detail=ERROR_MESSAGES.DEFAULT('Failed to create message'), ) # Update last_used_at - Channels.update_webhook_last_used_at(webhook_id, db=db) + await Channels.update_webhook_last_used_at(webhook_id, db=db) # Get full message and emit event - message = Messages.get_message_by_id(message.id, db=db) + message = await Messages.get_message_by_id(message.id, db=db) event_data = { - "channel_id": channel.id, - "message_id": message.id, - "data": { - "type": "message", - "data": { + 'channel_id': channel.id, + 'message_id': message.id, + 'data': { + 'type': 'message', + 'data': { **message.model_dump(), - "user": { - "id": webhook.id, - "name": webhook.name, - "role": "webhook", + 'user': { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', }, }, }, - "user": { - "id": webhook.id, - "name": webhook.name, - "role": "webhook", + 'user': { + 'id': webhook.id, + 'name': webhook.name, + 'role': 'webhook', }, - "channel": channel.model_dump(), + 'channel': channel.model_dump(), } await sio.emit( - "events:channel", + 'events:channel', event_data, - to=f"channel:{channel.id}", + to=f'channel:{channel.id}', ) - return {"success": True, "message_id": message.id} + return {'success': True, 'message_id': message.id} diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 9a43234aa6..256455fffb 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -1,39 +1,41 @@ +from __future__ import annotations + +import asyncio import json import logging from typing import Optional -from sqlalchemy.orm import Session -import asyncio +from uuid import uuid4 + +from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse - - -from open_webui.utils.misc import get_message_list -from open_webui.socket.main import get_event_emitter -from open_webui.models.chats import ( - ChatForm, - ChatImportForm, - ChatUsageStatsListResponse, - ChatsImportForm, - ChatResponse, - Chats, - ChatTitleIdResponse, - ChatStatsExport, - AggregateChatStats, - ChatBody, - ChatHistoryStats, - MessageStats, -) -from open_webui.models.tags import TagModel, Tags -from open_webui.models.folders import Folders -from open_webui.internal.db import get_session - from open_webui.config import ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Request, status -from pydantic import BaseModel - - +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.chats import ( + AggregateChatStats, + ChatBody, + ChatForm, + ChatHistoryStats, + ChatImportForm, + ChatResponse, + Chats, + ChatsImportForm, + ChatStatsExport, + ChatTitleIdResponse, + ChatUsageStatsListResponse, + MessageStats, +) +from open_webui.models.folders import Folders +from open_webui.models.shared_chats import SharedChatResponse, SharedChats +from open_webui.models.tags import TagModel, Tags +from open_webui.socket.main import get_event_emitter +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission +from open_webui.utils.middleware import serialize_output +from open_webui.utils.misc import get_message_list +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -41,24 +43,26 @@ router = APIRouter() ############################ # GetChatList +# Let the record outlive the session, so that what was +# learned here not need to be learned again. ############################ -@router.get("/", response_model=list[ChatTitleIdResponse]) -@router.get("/list", response_model=list[ChatTitleIdResponse]) -def get_session_user_chat_list( +@router.get('/', response_model=list[ChatTitleIdResponse]) +@router.get('/list', response_model=list[ChatTitleIdResponse]) +async def get_session_user_chat_list( user=Depends(get_verified_user), - page: Optional[int] = None, - include_pinned: Optional[bool] = False, - include_folders: Optional[bool] = False, - db: Session = Depends(get_session), + page: int | None = None, + include_pinned: bool | None = False, + include_folders: bool | None = False, + db: AsyncSession = Depends(get_async_session), ): try: if page is not None: limit = 60 skip = (page - 1) * limit - return Chats.get_chat_title_id_list_by_user_id( + return await Chats.get_chat_title_id_list_by_user_id( user.id, include_folders=include_folders, include_pinned=include_pinned, @@ -67,7 +71,7 @@ def get_session_user_chat_list( db=db, ) else: - return Chats.get_chat_title_id_list_by_user_id( + return await Chats.get_chat_title_id_list_by_user_id( user.id, include_folders=include_folders, include_pinned=include_pinned, @@ -75,9 +79,7 @@ def get_session_user_chat_list( ) except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -86,26 +88,26 @@ def get_session_user_chat_list( ############################ -@router.get("/stats/usage", response_model=ChatUsageStatsListResponse) -def get_session_user_chat_usage_stats( - items_per_page: Optional[int] = 50, - page: Optional[int] = 1, +@router.get('/stats/usage', response_model=ChatUsageStatsListResponse) +async def get_session_user_chat_usage_stats( + items_per_page: int | None = 50, + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: limit = items_per_page skip = (page - 1) * limit - result = Chats.get_chats_by_user_id(user.id, skip=skip, limit=limit, db=db) + result = await Chats.get_chats_by_user_id(user.id, skip=skip, limit=limit, db=db) chats = result.items total = result.total chat_stats = [] for chat in chats: - messages_map = chat.chat.get("history", {}).get("messages", {}) - message_id = chat.chat.get("history", {}).get("currentId") + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') if messages_map and message_id: try: @@ -115,30 +117,24 @@ def get_session_user_chat_usage_stats( history_assistant_messages = [] for message in messages_map.values(): - if message.get("role", "") == "user": + if message.get('role', '') == 'user': history_user_messages.append(message) - elif message.get("role", "") == "assistant": + elif message.get('role', '') == 'assistant': history_assistant_messages.append(message) - model = message.get("model", None) + model = message.get('model', None) if model: if model not in history_models: history_models[model] = 0 history_models[model] += 1 average_user_message_content_length = ( - sum( - len(message.get("content", "")) - for message in history_user_messages - ) + sum(len(message.get('content', '')) for message in history_user_messages) / len(history_user_messages) if len(history_user_messages) > 0 else 0 ) average_assistant_message_content_length = ( - sum( - len(message.get("content", "")) - for message in history_assistant_messages - ) + sum(len(message.get('content', '')) for message in history_assistant_messages) / len(history_assistant_messages) if len(history_assistant_messages) > 0 else 0 @@ -146,53 +142,45 @@ def get_session_user_chat_usage_stats( response_times = [] for message in history_assistant_messages: - user_message_id = message.get("parentId", None) + user_message_id = message.get('parentId', None) if user_message_id and user_message_id in messages_map: user_message = messages_map[user_message_id] - response_time = message.get( - "timestamp", 0 - ) - user_message.get("timestamp", 0) + response_time = message.get('timestamp', 0) - user_message.get('timestamp', 0) response_times.append(response_time) - average_response_time = ( - sum(response_times) / len(response_times) - if len(response_times) > 0 - else 0 - ) + average_response_time = sum(response_times) / len(response_times) if len(response_times) > 0 else 0 message_list = get_message_list(messages_map, message_id) message_count = len(message_list) models = {} for message in reversed(message_list): - if message.get("role") == "assistant": - model = message.get("model", None) + if message.get('role') == 'assistant': + model = message.get('model', None) if model: if model not in models: models[model] = 0 models[model] += 1 - annotation = message.get("annotation", {}) + annotation = message.get('annotation', {}) chat_stats.append( { - "id": chat.id, - "models": models, - "message_count": message_count, - "history_models": history_models, - "history_message_count": history_message_count, - "history_user_message_count": len(history_user_messages), - "history_assistant_message_count": len( - history_assistant_messages - ), - "average_response_time": average_response_time, - "average_user_message_content_length": average_user_message_content_length, - "average_assistant_message_content_length": average_assistant_message_content_length, - "tags": chat.meta.get("tags", []), - "last_message_at": message_list[-1].get("timestamp", None), - "updated_at": chat.updated_at, - "created_at": chat.created_at, + 'id': chat.id, + 'models': models, + 'message_count': message_count, + 'history_models': history_models, + 'history_message_count': history_message_count, + 'history_user_message_count': len(history_user_messages), + 'history_assistant_message_count': len(history_assistant_messages), + 'average_response_time': average_response_time, + 'average_user_message_content_length': average_user_message_content_length, + 'average_assistant_message_content_length': average_assistant_message_content_length, + 'tags': chat.meta.get('tags', []), + 'last_message_at': message_list[-1].get('timestamp', None), + 'updated_at': chat.updated_at, + 'created_at': chat.created_at, } ) except Exception as e: @@ -202,9 +190,7 @@ def get_session_user_chat_usage_stats( except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -216,29 +202,25 @@ CHAT_EXPORT_PAGE_ITEM_COUNT = 10 class ChatStatsExportList(BaseModel): - type: str = "chats" + type: str = 'chats' items: list[ChatStatsExport] total: int page: int -def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: +def _process_chat_for_export(chat) -> ChatStatsExport | None: try: def get_message_content_length(message): - content = message.get("content", "") + content = message.get('content', '') if isinstance(content, str): return len(content) elif isinstance(content, list): - return sum( - len(item.get("text", "")) - for item in content - if item.get("type") == "text" - ) + return sum(len(item.get('text', '')) for item in content if item.get('type') == 'text') return 0 - messages_map = chat.chat.get("history", {}).get("messages", {}) - message_id = chat.chat.get("history", {}).get("currentId") + messages_map = chat.chat.get('history', {}).get('messages', {}) + message_id = chat.chat.get('history', {}).get('currentId') history_models = {} history_message_count = len(messages_map) @@ -251,14 +233,14 @@ def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: content_length = get_message_content_length(message) # Extract rating safely - rating = message.get("annotation", {}).get("rating") - tags = message.get("annotation", {}).get("tags") + rating = message.get('annotation', {}).get('rating') + tags = message.get('annotation', {}).get('tags') message_stat = MessageStats( - id=message.get("id"), - role=message.get("role"), - model=message.get("model"), - timestamp=message.get("timestamp"), + id=message.get('id'), + role=message.get('role'), + model=message.get('model'), + timestamp=message.get('timestamp'), content_length=content_length, token_count=None, # Populate if available, e.g. message.get("info", {}).get("token_count") rating=rating, @@ -268,31 +250,29 @@ def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: export_messages[key] = message_stat # --- Aggregation Logic (copied/adapted from usage stats) --- - role = message.get("role", "") - if role == "user": + role = message.get('role', '') + if role == 'user': history_user_messages.append(message) - elif role == "assistant": + elif role == 'assistant': history_assistant_messages.append(message) - model = message.get("model") + model = message.get('model') if model: if model not in history_models: history_models[model] = 0 history_models[model] += 1 except Exception as e: - log.debug(f"Error processing message {key}: {e}") + log.debug(f'Error processing message {key}: {e}') continue # Calculate Averages average_user_message_content_length = ( - sum(get_message_content_length(m) for m in history_user_messages) - / len(history_user_messages) + sum(get_message_content_length(m) for m in history_user_messages) / len(history_user_messages) if history_user_messages else 0 ) average_assistant_message_content_length = ( - sum(get_message_content_length(m) for m in history_assistant_messages) - / len(history_assistant_messages) + sum(get_message_content_length(m) for m in history_assistant_messages) / len(history_assistant_messages) if history_assistant_messages else 0 ) @@ -300,26 +280,24 @@ def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: # Response Times response_times = [] for message in history_assistant_messages: - user_message_id = message.get("parentId", None) + user_message_id = message.get('parentId', None) if user_message_id and user_message_id in messages_map: user_message = messages_map[user_message_id] # Ensure timestamps exist - t1 = message.get("timestamp") - t0 = user_message.get("timestamp") + t1 = message.get('timestamp') + t0 = user_message.get('timestamp') if t1 and t0: response_times.append(t1 - t0) - average_response_time = ( - sum(response_times) / len(response_times) if response_times else 0 - ) + average_response_time = sum(response_times) / len(response_times) if response_times else 0 # Current Message List Logic (Main path) message_list = get_message_list(messages_map, message_id) message_count = len(message_list) models = {} for message in reversed(message_list): - if message.get("role") == "assistant": - model = message.get("model") + if message.get('role') == 'assistant': + model = message.get('model') if model: if model not in models: models[model] = 0 @@ -339,36 +317,31 @@ def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: ) # Construct Chat Body - chat_body = ChatBody( - history=ChatHistoryStats(messages=export_messages, currentId=message_id) - ) + chat_body = ChatBody(history=ChatHistoryStats(messages=export_messages, currentId=message_id)) return ChatStatsExport( id=chat.id, user_id=chat.user_id, created_at=chat.created_at, updated_at=chat.updated_at, - tags=chat.meta.get("tags", []), + tags=chat.meta.get('tags', []), stats=stats, chat=chat_body, ) except Exception as e: - log.exception(f"Error exporting stats for chat {chat.id}: {e}") + log.exception(f'Error exporting stats for chat {chat.id}: {e}') return None -def calculate_chat_stats( - user_id, skip=0, limit=10, filter=None, db: Optional[Session] = None -): +async def calculate_chat_stats(user_id, skip=0, limit=10, filter=None): if filter is None: filter = {} - result = Chats.get_chats_by_user_id( + result = await Chats.get_chats_by_user_id( user_id, skip=skip, limit=limit, filter=filter, - db=db, ) chat_stats_export_list = [] @@ -380,12 +353,12 @@ def calculate_chat_stats( return chat_stats_export_list, result.total -def generate_chat_stats_jsonl_generator(user_id, filter): +async def generate_chat_stats_jsonl_generator(user_id, filter): """ - Synchronous generator for streaming chat stats export. + Async generator for streaming chat stats export. NOTE: We intentionally do NOT pass a shared db session here. Instead, we let - each batch create its own short-lived session via get_db_context(None). + each batch create its own short-lived session via get_async_db_context(None). This is critical for SQLite in low-resource environments because: 1. SQLite uses file-level locking 2. Holding a session open for the entire streaming duration blocks other requests @@ -396,12 +369,12 @@ def generate_chat_stats_jsonl_generator(user_id, filter): while True: # Each batch gets its own session that closes after the query - result = Chats.get_chats_by_user_id( + result = await Chats.get_chats_by_user_id( user_id, filter=filter, skip=skip, limit=limit, - db=None, # Let get_db_context create a fresh session per batch + db=None, # Let get_async_db_context create a fresh session per batch ) if not result.items: break @@ -410,26 +383,23 @@ def generate_chat_stats_jsonl_generator(user_id, filter): try: chat_stat = _process_chat_for_export(chat) if chat_stat: - yield chat_stat.model_dump_json() + "\n" + yield chat_stat.model_dump_json() + '\n' except Exception as e: - log.exception(f"Error processing chat {chat.id}: {e}") + log.exception(f'Error processing chat {chat.id}: {e}') skip += limit -@router.get("/stats/export", response_model=ChatStatsExportList) +@router.get('/stats/export', response_model=ChatStatsExportList) async def export_chat_stats( request: Request, - updated_at: Optional[int] = None, - page: Optional[int] = 1, + updated_at: int | None = None, + page: int | None = 1, stream: bool = False, user=Depends(get_verified_user), - db: Session = Depends(get_session), ): # Check if the user has permission to share/export chats - if (user.role != "admin") and ( - not request.app.state.config.ENABLE_COMMUNITY_SHARING - ): + if (user.role != 'admin') and (not request.app.state.config.ENABLE_COMMUNITY_SHARING): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, @@ -437,36 +407,28 @@ async def export_chat_stats( try: # Fetch chats with date filtering - filter = {"order_by": "updated_at", "direction": "asc"} + filter = {'order_by': 'updated_at', 'direction': 'asc'} if updated_at: - filter["updated_at"] = updated_at + filter['updated_at'] = updated_at if stream: return StreamingResponse( generate_chat_stats_jsonl_generator(user.id, filter), - media_type="application/x-ndjson", - headers={ - "Content-Disposition": f"attachment; filename=chat-stats-export-{user.id}.jsonl" - }, + media_type='application/x-ndjson', + headers={'Content-Disposition': f'attachment; filename=chat-stats-export-{user.id}.jsonl'}, ) else: limit = CHAT_EXPORT_PAGE_ITEM_COUNT skip = (page - 1) * limit - chat_stats_export_list, total = await asyncio.to_thread( - calculate_chat_stats, user.id, skip, limit, filter, db=db - ) + chat_stats_export_list, total = await calculate_chat_stats(user.id, skip, limit, filter) - return ChatStatsExportList( - items=chat_stats_export_list, total=total, page=page - ) + return ChatStatsExportList(items=chat_stats_export_list, total=total, page=page) except Exception as e: - log.debug(f"Error exporting chat stats: {e}") - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + log.debug(f'Error exporting chat stats: {e}') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -474,28 +436,26 @@ async def export_chat_stats( ############################ -@router.get("/stats/export/{chat_id}", response_model=Optional[ChatStatsExport]) +@router.get('/stats/export/{chat_id}', response_model=ChatStatsExport | None) async def export_single_chat_stats( request: Request, chat_id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """ Export stats for exactly one chat by ID. Returns ChatStatsExport for the specified chat. """ # Check if the user has permission to share/export chats - if (user.role != "admin") and ( - not request.app.state.config.ENABLE_COMMUNITY_SHARING - ): + if (user.role != 'admin') and (not request.app.state.config.ENABLE_COMMUNITY_SHARING): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) try: - chat = Chats.get_chat_by_id(chat_id, db=db) + chat = await Chats.get_chat_by_id(chat_id, db=db) if not chat: raise HTTPException( @@ -504,19 +464,19 @@ async def export_single_chat_stats( ) # Verify the chat belongs to the user (unless admin) - if chat.user_id != user.id and user.role != "admin": + if chat.user_id != user.id and user.role != 'admin': raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Process the chat for export - chat_stats = await asyncio.to_thread(_process_chat_for_export, chat) + # Process the chat for export (pure computation, no DB) + chat_stats = _process_chat_for_export(chat) if not chat_stats: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Failed to process chat stats", + detail='Failed to process chat stats', ) return chat_stats @@ -524,28 +484,25 @@ async def export_single_chat_stats( except HTTPException: raise except Exception as e: - log.debug(f"Error exporting single chat stats: {e}") - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + log.debug(f'Error exporting single chat stats: {e}') + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) -@router.delete("/", response_model=bool) +@router.delete('/', response_model=bool) async def delete_all_user_chats( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - - if user.role == "user" and not has_permission( - user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS + if user.role == 'user' and not await has_permission( + user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - result = Chats.delete_chats_by_user_id(user.id, db=db) + result = await Chats.delete_chats_by_user_id(user.id, db=db) return result @@ -554,37 +511,29 @@ async def delete_all_user_chats( ############################ -@router.get("/list/user/{user_id}", response_model=list[ChatTitleIdResponse]) +@router.get('/list/user/{user_id}', response_model=list[ChatTitleIdResponse]) async def get_user_chat_list_by_user_id( - user_id: str, - page: Optional[int] = None, - query: Optional[str] = None, - order_by: Optional[str] = None, - direction: Optional[str] = None, - user=Depends(get_admin_user), - db: Session = Depends(get_session), + user_id: str, page: int | None = None, query: str | None = None, + order_by: str | None = None, direction: str | None = None, + user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): + """List chat summaries for a given user (admin-only endpoint).""" if not ENABLE_ADMIN_CHAT_ACCESS: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - - if page is None: - page = 1 + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + effective_page = page if page is not None else 1 limit = 60 - skip = (page - 1) * limit + skip = (effective_page - 1) * limit filter = {} if query: - filter["query"] = query + filter['query'] = query if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - return Chats.get_chat_list_by_user_id( + return await Chats.get_chat_list_by_user_id( user_id, include_archived=True, filter=filter, skip=skip, limit=limit, db=db ) @@ -594,20 +543,30 @@ async def get_user_chat_list_by_user_id( ############################ -@router.post("/new", response_model=Optional[ChatResponse]) +@router.post('/new', response_model=ChatResponse | None) async def create_new_chat( form_data: ChatForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): + # Reject a folder_id that doesn't belong to the caller. Without this the + # row is persisted with a dangling foreign reference — no read path + # surfaces it across users (all chat reads are user_id-filtered), but + # the row state is meaningless and downstream consumers shouldn't have + # to assume the column is clean. Also catches non-UUID / nonexistent IDs. + if form_data.folder_id is not None: + if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + try: - chat = Chats.insert_new_chat(user.id, form_data, db=db) + chat = await Chats.insert_new_chat(str(uuid4()), user.id, form_data, db=db) return ChatResponse(**chat.model_dump()) except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -615,20 +574,18 @@ async def create_new_chat( ############################ -@router.post("/import", response_model=list[ChatResponse]) +@router.post('/import', response_model=list[ChatResponse]) async def import_chats( form_data: ChatsImportForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: - chats = Chats.import_chats(user.id, form_data.chats, db=db) + chats = await Chats.import_chats(user.id, form_data.chats, db=db) return chats except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -636,12 +593,12 @@ async def import_chats( ############################ -@router.get("/search", response_model=list[ChatTitleIdResponse]) -def search_user_chats( +@router.get('/search', response_model=list[ChatTitleIdResponse]) +async def search_user_chats( text: str, - page: Optional[int] = None, + page: int | None = None, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if page is None: page = 1 @@ -651,19 +608,17 @@ def search_user_chats( chat_list = [ ChatTitleIdResponse(**chat.model_dump()) - for chat in Chats.get_chats_by_user_id_and_search_text( - user.id, text, skip=skip, limit=limit, db=db - ) + for chat in await Chats.get_chats_by_user_id_and_search_text(user.id, text, skip=skip, limit=limit, db=db) ] # Delete tag if no chat is found - words = text.strip().split(" ") - if page == 1 and len(words) == 1 and words[0].startswith("tag:"): - tag_id = words[0].replace("tag:", "") + words = text.strip().split(' ') + if page == 1 and len(words) == 1 and words[0].startswith('tag:'): + tag_id = words[0].replace('tag:', '') if len(chat_list) == 0: - if Tags.get_tag_by_name_and_user_id(tag_id, user.id, db=db): - log.debug(f"deleting tag: {tag_id}") - Tags.delete_tag_by_name_and_user_id(tag_id, user.id, db=db) + if await Tags.get_tag_by_name_and_user_id(tag_id, user.id, db=db): + log.debug(f'deleting tag: {tag_id}') + await Tags.delete_tag_by_name_and_user_id(tag_id, user.id, db=db) return chat_list @@ -673,48 +628,41 @@ def search_user_chats( ############################ -@router.get("/folder/{folder_id}", response_model=list[ChatResponse]) +@router.get('/folder/{folder_id}', response_model=list[ChatResponse]) async def get_chats_by_folder_id( - folder_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + folder_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): folder_ids = [folder_id] - children_folders = Folders.get_children_folders_by_id_and_user_id( - folder_id, user.id, db=db - ) + children_folders = await Folders.get_children_folders_by_id_and_user_id(folder_id, user.id, db=db) if children_folders: folder_ids.extend([folder.id for folder in children_folders]) return [ ChatResponse(**chat.model_dump()) - for chat in Chats.get_chats_by_folder_ids_and_user_id( - folder_ids, user.id, db=db - ) + for chat in await Chats.get_chats_by_folder_ids_and_user_id(folder_ids, user.id, db=db) ] -@router.get("/folder/{folder_id}/list") +@router.get('/folder/{folder_id}/list') async def get_chat_list_by_folder_id( folder_id: str, - page: Optional[int] = 1, + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: limit = 10 skip = (page - 1) * limit + chats = await Chats.get_chats_by_folder_id_and_user_id(folder_id, user.id, skip=skip, limit=limit, db=db) return [ - {"title": chat.title, "id": chat.id, "updated_at": chat.updated_at} - for chat in Chats.get_chats_by_folder_id_and_user_id( - folder_id, user.id, skip=skip, limit=limit, db=db - ) + {'title': chat.title, 'id': chat.id, 'updated_at': chat.updated_at, 'last_read_at': chat.last_read_at} + for chat in chats ] except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -722,27 +670,55 @@ async def get_chat_list_by_folder_id( ############################ -@router.get("/pinned", response_model=list[ChatTitleIdResponse]) -async def get_user_pinned_chats( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - return [ - ChatTitleIdResponse(**chat.model_dump()) - for chat in Chats.get_pinned_chats_by_user_id(user.id, db=db) - ] +@router.get('/pinned', response_model=list[ChatTitleIdResponse]) +async def get_user_pinned_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Chats.get_pinned_chats_by_user_id(user.id, db=db) ############################ # GetChats ############################ +CHAT_EXPORT_BATCH_SIZE = 100 -@router.get("/all", response_model=list[ChatResponse]) -async def get_user_chats( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - result = Chats.get_chats_by_user_id(user.id, db=db) - return [ChatResponse(**chat.model_dump()) for chat in result.items] + +async def generate_chat_export_ndjson(user_id: str): + """ + Async generator that streams all user chats as NDJSON (one JSON object per line). + + Uses short-lived DB sessions per batch to avoid holding locks for the + entire duration, which is critical for SQLite environments. + """ + skip = 0 + + while True: + result = await Chats.get_chats_by_user_id( + user_id, + skip=skip, + limit=CHAT_EXPORT_BATCH_SIZE, + db=None, + ) + if not result.items: + break + + for chat in result.items: + try: + yield ChatResponse(**chat.model_dump()).model_dump_json() + '\n' + except Exception as e: + log.exception(f'Error serializing chat {chat.id}: {e}') + + if len(result.items) < CHAT_EXPORT_BATCH_SIZE: + break + + skip += CHAT_EXPORT_BATCH_SIZE + + +@router.get('/all') +async def get_user_chats(user=Depends(get_verified_user)): + return StreamingResponse( + generate_chat_export_ndjson(user.id), + media_type='application/x-ndjson', + ) ############################ @@ -750,14 +726,9 @@ async def get_user_chats( ############################ -@router.get("/all/archived", response_model=list[ChatResponse]) -async def get_user_archived_chats( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - return [ - ChatResponse(**chat.model_dump()) - for chat in Chats.get_archived_chats_by_user_id(user.id, db=db) - ] +@router.get('/all/archived', response_model=list[ChatResponse]) +async def get_user_archived_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_archived_chats_by_user_id(user.id, db=db)] ############################ @@ -765,18 +736,14 @@ async def get_user_archived_chats( ############################ -@router.get("/all/tags", response_model=list[TagModel]) -async def get_all_user_tags( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): +@router.get('/all/tags', response_model=list[TagModel]) +async def get_all_user_tags(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): try: - tags = Tags.get_tags_by_user_id(user.id, db=db) + tags = await Tags.get_tags_by_user_id(user.id, db=db) return tags except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -784,16 +751,11 @@ async def get_all_user_tags( ############################ -@router.get("/all/db", response_model=list[ChatResponse]) -async def get_all_user_chats_in_db( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): +@router.get('/all/db', response_model=list[ChatResponse]) +async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): if not ENABLE_ADMIN_EXPORT: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - return [ChatResponse(**chat.model_dump()) for chat in Chats.get_chats(db=db)] + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_chats(db=db)] ############################ @@ -801,14 +763,14 @@ async def get_all_user_chats_in_db( ############################ -@router.get("/archived", response_model=list[ChatTitleIdResponse]) +@router.get('/archived', response_model=list[ChatTitleIdResponse]) async def get_archived_session_user_chat_list( - page: Optional[int] = None, - query: Optional[str] = None, - order_by: Optional[str] = None, - direction: Optional[str] = None, + page: int | None = None, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if page is None: page = 1 @@ -818,24 +780,19 @@ async def get_archived_session_user_chat_list( filter = {} if query: - filter["query"] = query + filter['query'] = query if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - chat_list = [ - ChatTitleIdResponse(**chat.model_dump()) - for chat in Chats.get_archived_chat_list_by_user_id( - user.id, - filter=filter, - skip=skip, - limit=limit, - db=db, - ) - ] - - return chat_list + return await Chats.get_archived_chat_list_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) ############################ @@ -843,11 +800,9 @@ async def get_archived_session_user_chat_list( ############################ -@router.post("/archive/all", response_model=bool) -async def archive_all_chats( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - return Chats.archive_all_chats_by_user_id(user.id, db=db) +@router.post('/archive/all', response_model=bool) +async def archive_all_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Chats.archive_all_chats_by_user_id(user.id, db=db) ############################ @@ -855,11 +810,46 @@ async def archive_all_chats( ############################ -@router.post("/unarchive/all", response_model=bool) -async def unarchive_all_chats( - user=Depends(get_verified_user), db: Session = Depends(get_session) +@router.post('/unarchive/all', response_model=bool) +async def unarchive_all_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Chats.unarchive_all_chats_by_user_id(user.id, db=db) + + +############################ +# GetSharedChats +############################ + + +@router.get('/shared', response_model=list[SharedChatResponse]) +async def get_shared_session_user_chat_list( + page: int | None = None, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), ): - return Chats.unarchive_all_chats_by_user_id(user.id, db=db) + if page is None: + page = 1 + + limit = 60 + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + return await SharedChats.get_by_user_id( + user.id, + filter=filter, + skip=skip, + limit=limit, + db=db, + ) ############################ @@ -867,27 +857,40 @@ async def unarchive_all_chats( ############################ -@router.get("/share/{share_id}", response_model=Optional[ChatResponse]) +@router.get('/share/{share_id}', response_model=ChatResponse | None) async def get_shared_chat_by_id( - share_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + share_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - if user.role == "pending": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) + if user.role == 'pending': + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) - if user.role == "user" or (user.role == "admin" and not ENABLE_ADMIN_CHAT_ACCESS): - chat = Chats.get_chat_by_share_id(share_id, db=db) - elif user.role == "admin" and ENABLE_ADMIN_CHAT_ACCESS: - chat = Chats.get_chat_by_id(share_id, db=db) + chat = await Chats.get_chat_by_share_id(share_id, db=db) - if chat: - return ChatResponse(**chat.model_dump()) + # Fallback: admins can also access any chat directly by chat ID + if not chat and user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS: + chat = await Chats.get_chat_by_id(share_id, db=db) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) + if not chat: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) + + # Look up the original chat_id to check access grants (admins bypass) + if user.role != 'admin' or not ENABLE_ADMIN_CHAT_ACCESS: + shared = await SharedChats.get_by_id(share_id, db=db) + if shared and shared.user_id != user.id: + has_grant = await AccessGrants.has_access( + user_id=user.id, + resource_type='shared_chat', + resource_id=shared.chat_id, + permission='read', + db=db, + ) + if not has_grant: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + return ChatResponse(**chat.model_dump()) ############################ @@ -900,21 +903,21 @@ class TagForm(BaseModel): class TagFilterForm(TagForm): - skip: Optional[int] = 0 - limit: Optional[int] = 50 + skip: int | None = 0 + limit: int | None = 50 -@router.post("/tags", response_model=list[ChatTitleIdResponse]) +@router.post('/tags', response_model=list[ChatTitleIdResponse]) async def get_user_chat_list_by_tag_name( form_data: TagFilterForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chats = Chats.get_chat_list_by_user_id_and_tag_name( + chats = await Chats.get_chat_list_by_user_id_and_tag_name( user.id, form_data.name, form_data.skip, form_data.limit, db=db ) if len(chats) == 0: - Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) + await Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) return chats @@ -924,19 +927,29 @@ async def get_user_chat_list_by_tag_name( ############################ -@router.get("/{id}", response_model=Optional[ChatResponse]) -async def get_chat_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) +@router.get('/{id}', response_model=ChatResponse | None) +async def get_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + + if not chat: + # Check if user has access via access grants (shared_chat grants) + if user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS: + chat = await Chats.get_chat_by_id(id, db=db) + else: + has_grant = await AccessGrants.has_access( + user_id=user.id, + resource_type='shared_chat', + resource_id=id, + permission='read', + db=db, + ) + if has_grant: + chat = await Chats.get_chat_by_id(id, db=db) if chat: return ChatResponse(**chat.model_dump()) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) ############################ @@ -944,17 +957,36 @@ async def get_chat_by_id( ############################ -@router.post("/{id}", response_model=Optional[ChatResponse]) +@router.post('/{id}', response_model=ChatResponse | None) async def update_chat_by_id( id: str, form_data: ChatForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: updated_chat = {**chat.chat, **form_data.chat} - chat = Chats.update_chat_by_id(id, updated_chat, db=db) + + # Re-derive content from output for assistant messages so that frontend + # edits to output items are reflected in content. Only when output + # actually changed — otherwise content set independently of output + # (e.g. a `replace` event or an outlet filter footer) would be reverted. + existing_messages = (chat.chat.get('history') or {}).get('messages') or {} + for msg_id, msg in updated_chat.get('history', {}).get('messages', {}).items(): + if msg.get('role') == 'assistant' and msg.get('output'): + if msg.get('output') != existing_messages.get(msg_id, {}).get('output'): + msg['content'] = serialize_output(msg['output']) + + chat = await Chats.update_chat_by_id(id, updated_chat, db=db) + + # Reconcile chat_message rows with the committed blob. + # This is the only caller where the frontend pushes a full + # history with potential edits, deletions, or new branches. + messages = (updated_chat.get('history') or {}).get('messages') or {} + if messages: + await Chats.reconcile_messages_by_chat_id(id, user.id, messages) + return ChatResponse(**chat.model_dump()) else: raise HTTPException( @@ -970,15 +1002,15 @@ class MessageForm(BaseModel): content: str -@router.post("/{id}/messages/{message_id}", response_model=Optional[ChatResponse]) +@router.post('/{id}/messages/{message_id}', response_model=ChatResponse | None) async def update_chat_message_by_id( id: str, message_id: str, form_data: MessageForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id(id, db=db) + chat = await Chats.get_chat_by_id(id, db=db) if not chat: raise HTTPException( @@ -986,26 +1018,25 @@ async def update_chat_message_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - if chat.user_id != user.id and user.role != "admin": + if chat.user_id != user.id and user.role != 'admin': raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - chat = Chats.upsert_message_to_chat_by_id_and_message_id( + chat = await Chats.upsert_message_to_chat_by_id_and_message_id( id, message_id, { - "content": form_data.content, + 'content': form_data.content, }, - db=db, ) - event_emitter = get_event_emitter( + event_emitter = await get_event_emitter( { - "user_id": user.id, - "chat_id": id, - "message_id": message_id, + 'user_id': chat.user_id, + 'chat_id': id, + 'message_id': message_id, }, False, ) @@ -1013,11 +1044,11 @@ async def update_chat_message_by_id( if event_emitter: await event_emitter( { - "type": "chat:message", - "data": { - "chat_id": id, - "message_id": message_id, - "content": form_data.content, + 'type': 'chat:message', + 'data': { + 'chat_id': id, + 'message_id': message_id, + 'content': form_data.content, }, } ) @@ -1033,15 +1064,15 @@ class EventForm(BaseModel): data: dict -@router.post("/{id}/messages/{message_id}/event", response_model=Optional[bool]) +@router.post('/{id}/messages/{message_id}/event', response_model=bool | None) async def send_chat_message_event_by_id( id: str, message_id: str, form_data: EventForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id(id, db=db) + chat = await Chats.get_chat_by_id(id, db=db) if not chat: raise HTTPException( @@ -1049,17 +1080,17 @@ async def send_chat_message_event_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - if chat.user_id != user.id and user.role != "admin": + if chat.user_id != user.id and user.role != 'admin': raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - event_emitter = get_event_emitter( + event_emitter = await get_event_emitter( { - "user_id": user.id, - "chat_id": id, - "message_id": message_id, + 'user_id': chat.user_id, + 'chat_id': id, + 'message_id': message_id, } ) @@ -1069,7 +1100,7 @@ async def send_chat_message_event_by_id( else: return False return True - except: + except Exception: return False @@ -1078,47 +1109,41 @@ async def send_chat_message_event_by_id( ############################ -@router.delete("/{id}", response_model=bool) +@router.delete('/{id}', response_model=bool) async def delete_chat_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role == "admin": - chat = Chats.get_chat_by_id(id, db=db) + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) if not chat: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - for tag in chat.meta.get("tags", []): - if Chats.count_chats_by_tag_name_and_user_id(tag, user.id, db=db) == 1: - Tags.delete_tag_by_name_and_user_id(tag, user.id, db=db) + await Chats.delete_orphan_tags_for_user(chat.meta.get('tags', []), user.id, threshold=1, db=db) - result = Chats.delete_chat_by_id(id, db=db) + result = await Chats.delete_chat_by_id(id, db=db) return result else: - if not has_permission( - user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - chat = Chats.get_chat_by_id(id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if not chat: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - for tag in chat.meta.get("tags", []): - if Chats.count_chats_by_tag_name_and_user_id(tag, user.id, db=db) == 1: - Tags.delete_tag_by_name_and_user_id(tag, user.id, db=db) + await Chats.delete_orphan_tags_for_user(chat.meta.get('tags', []), user.id, threshold=1, db=db) - result = Chats.delete_chat_by_id_and_user_id(id, user.id, db=db) + result = await Chats.delete_chat_by_id_and_user_id(id, user.id, db=db) return result @@ -1127,17 +1152,15 @@ async def delete_chat_by_id( ############################ -@router.get("/{id}/pinned", response_model=Optional[bool]) +@router.get('/{id}/pinned', response_model=bool | None) async def get_pinned_status_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: return chat.pinned else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1145,18 +1168,14 @@ async def get_pinned_status_by_id( ############################ -@router.post("/{id}/pin", response_model=Optional[ChatResponse]) -async def pin_chat_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) +@router.post('/{id}/pin', response_model=ChatResponse | None) +async def pin_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: - chat = Chats.toggle_chat_pinned_by_id(id, db=db) + chat = await Chats.toggle_chat_pinned_by_id(id, db=db) return chat else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1165,34 +1184,34 @@ async def pin_chat_by_id( class CloneForm(BaseModel): - title: Optional[str] = None + title: str | None = None -@router.post("/{id}/clone", response_model=Optional[ChatResponse]) +@router.post('/{id}/clone', response_model=ChatResponse | None) async def clone_chat_by_id( form_data: CloneForm, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: updated_chat = { **chat.chat, - "originalChatId": chat.id, - "branchPointMessageId": chat.chat["history"]["currentId"], - "title": form_data.title if form_data.title else f"Clone of {chat.title}", + 'originalChatId': chat.id, + 'branchPointMessageId': chat.chat['history']['currentId'], + 'title': form_data.title if form_data.title else f'Clone of {chat.title}', } - chats = Chats.import_chats( + chats = await Chats.import_chats( user.id, [ ChatImportForm( **{ - "chat": updated_chat, - "meta": chat.meta, - "pinned": chat.pinned, - "folder_id": chat.folder_id, + 'chat': updated_chat, + 'meta': chat.meta, + 'pinned': chat.pinned, + 'folder_id': chat.folder_id, } ) ], @@ -1208,9 +1227,7 @@ async def clone_chat_by_id( detail=ERROR_MESSAGES.DEFAULT(), ) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1218,50 +1235,67 @@ async def clone_chat_by_id( ############################ -@router.post("/{id}/clone/shared", response_model=Optional[ChatResponse]) +@router.post('/{id}/clone/shared', response_model=ChatResponse | None) async def clone_shared_chat_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): + chat = await Chats.get_chat_by_share_id(id, db=db) - if user.role == "admin": - chat = Chats.get_chat_by_id(id, db=db) - else: - chat = Chats.get_chat_by_share_id(id, db=db) + # Fallback: admins can also access any chat directly by chat ID + if not chat and user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS: + chat = await Chats.get_chat_by_id(id, db=db) - if chat: - updated_chat = { - **chat.chat, - "originalChatId": chat.id, - "branchPointMessageId": chat.chat["history"]["currentId"], - "title": f"Clone of {chat.title}", - } - - chats = Chats.import_chats( - user.id, - [ - ChatImportForm( - **{ - "chat": updated_chat, - "meta": chat.meta, - "pinned": chat.pinned, - "folder_id": chat.folder_id, - } - ) - ], - db=db, + if not chat: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, ) - if chats: - chat = chats[0] - return ChatResponse(**chat.model_dump()) - else: + # Enforce access grants (owner and admins bypass) + shared = await SharedChats.get_by_id(id, db=db) + if shared and user.role != 'admin' and shared.user_id != user.id: + has_grant = await AccessGrants.has_access( + user_id=user.id, + resource_type='shared_chat', + resource_id=shared.chat_id, + permission='read', + db=db, + ) + if not has_grant: raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ERROR_MESSAGES.DEFAULT(), + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + + updated_chat = { + **chat.chat, + 'originalChatId': chat.id, + 'branchPointMessageId': chat.chat['history']['currentId'], + 'title': f'Clone of {chat.title}', + } + + chats = await Chats.import_chats( + user.id, + [ + ChatImportForm( + **{ + 'chat': updated_chat, + 'meta': chat.meta, + 'pinned': chat.pinned, + 'folder_id': chat.folder_id, + } + ) + ], + db=db, + ) + + if chats: + chat = chats[0] + return ChatResponse(**chat.model_dump()) else: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), ) @@ -1270,105 +1304,156 @@ async def clone_shared_chat_by_id( ############################ -@router.post("/{id}/archive", response_model=Optional[ChatResponse]) -async def archive_chat_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) +@router.post('/{id}/archive', response_model=ChatResponse | None) +async def archive_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: - chat = Chats.toggle_chat_archive_by_id(id, db=db) + chat = await Chats.toggle_chat_archive_by_id(id, db=db) - # Delete tags if chat is archived + tag_ids = chat.meta.get('tags', []) if chat.archived: - for tag_id in chat.meta.get("tags", []): - if ( - Chats.count_chats_by_tag_name_and_user_id(tag_id, user.id, db=db) - == 0 - ): - log.debug(f"deleting tag: {tag_id}") - Tags.delete_tag_by_name_and_user_id(tag_id, user.id, db=db) + # Archived chats are excluded from count — clean up orphans + await Chats.delete_orphan_tags_for_user(tag_ids, user.id, db=db) else: - for tag_id in chat.meta.get("tags", []): - tag = Tags.get_tag_by_name_and_user_id(tag_id, user.id, db=db) - if tag is None: - log.debug(f"inserting tag: {tag_id}") - tag = Tags.insert_new_tag(tag_id, user.id, db=db) + # Unarchived — ensure tag rows exist + await Tags.ensure_tags_exist(tag_ids, user.id, db=db) return ChatResponse(**chat.model_dump()) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# ShareChatById -############################ +# --- Share Chat --- -@router.post("/{id}/share", response_model=Optional[ChatResponse]) +@router.post('/{id}/share', response_model=ChatResponse | None) async def share_chat_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if (user.role != "admin") and ( - not has_permission( - user.id, "chat.share", request.app.state.config.USER_PERMISSIONS - ) + if user.role != 'admin' and not await has_permission( + user.id, 'chat.share', request.app.state.config.USER_PERMISSIONS ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - if chat: - if chat.share_id: - shared_chat = Chats.update_shared_chat_by_chat_id(chat.id, db=db) - return ChatResponse(**shared_chat.model_dump()) + # If a share already exists, re-snapshot it + if chat.share_id: + shared = await SharedChats.update(chat.share_id, db=db) + if shared: + chat = await Chats.get_chat_by_id(id, db=db) + return ChatResponse(**chat.model_dump()) - shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id, db=db) - if not shared_chat: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ERROR_MESSAGES.DEFAULT(), - ) - return ChatResponse(**shared_chat.model_dump()) + # Create a new share + shared = await SharedChats.create(id, user.id, db=db) + if not shared: + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DEFAULT()) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) + chat = await Chats.update_chat_share_id_by_id(id, shared.id, db=db) + if not chat: + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DEFAULT()) + + return ChatResponse(**chat.model_dump()) -############################ -# DeletedSharedChatById -############################ +# --- Delete Shared Chat --- -@router.delete("/{id}/share", response_model=Optional[bool]) +@router.delete('/{id}/share', response_model=bool | None) async def delete_shared_chat_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) - if chat: - if not chat.share_id: - return False + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - result = Chats.delete_shared_chat_by_chat_id(id, db=db) - update_result = Chats.update_chat_share_id_by_id(id, None, db=db) + if not chat.share_id: + return False - return result and update_result != None + await SharedChats.delete_by_chat_id(id, db=db) + await Chats.update_chat_share_id_by_id(id, None, db=db) + await AccessGrants.set_access_grants('shared_chat', id, [], db=db) + + return True + + +############################ +# UpdateSharedChatAccessById +############################ + + +class ChatAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/shared/{id}/access/update', response_model=ChatResponse | None) +async def update_shared_chat_access_by_id( + request: Request, + id: str, + form_data: ChatAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) else: + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, ) + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_chats', + ) + + await AccessGrants.set_access_grants('shared_chat', id, form_data.access_grants, db=db) + + return ChatResponse(**chat.model_dump()) + + +############################ +# GetSharedChatAccessById +############################ + + +@router.get('/shared/{id}/access', response_model=list) +async def get_shared_chat_access_by_id( + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'admin': + chat = await Chats.get_chat_by_id(id, db=db) + else: + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + if not chat: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db) + return [ + { + 'id': g.id, + 'principal_type': g.principal_type, + 'principal_id': g.principal_id, + 'permission': g.permission, + } + for g in grants + ] + ############################ # UpdateChatFolderIdById @@ -1376,26 +1461,31 @@ async def delete_shared_chat_by_id( class ChatFolderIdForm(BaseModel): - folder_id: Optional[str] = None + folder_id: str | None = None -@router.post("/{id}/folder", response_model=Optional[ChatResponse]) +@router.post('/{id}/folder', response_model=ChatResponse | None) async def update_chat_folder_id_by_id( id: str, form_data: ChatFolderIdForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: - chat = Chats.update_chat_folder_id_by_id_and_user_id( - id, user.id, form_data.folder_id, db=db - ) + # Same ownership check as the create path — reject foreign / dangling + # folder_id values. None is allowed (moves the chat out of any folder). + if form_data.folder_id is not None: + if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + chat = await Chats.update_chat_folder_id_by_id_and_user_id(id, user.id, form_data.folder_id, db=db) return ChatResponse(**chat.model_dump()) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1403,18 +1493,14 @@ async def update_chat_folder_id_by_id( ############################ -@router.get("/{id}/tags", response_model=list[TagModel]) -async def get_chat_tags_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) +@router.get('/{id}/tags', response_model=list[TagModel]) +async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: - tags = chat.meta.get("tags", []) - return Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) + tags = chat.meta.get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) ############################ @@ -1422,36 +1508,32 @@ async def get_chat_tags_by_id( ############################ -@router.post("/{id}/tags", response_model=list[TagModel]) +@router.post('/{id}/tags', response_model=list[TagModel]) async def add_tag_by_id_and_tag_name( id: str, form_data: TagForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: - tags = chat.meta.get("tags", []) - tag_id = form_data.name.replace(" ", "_").lower() + tags = chat.meta.get('tags', []) + tag_id = form_data.name.replace(' ', '_').lower() - if tag_id == "none": + if tag_id == 'none': raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Tag name cannot be 'None'"), ) if tag_id not in tags: - Chats.add_chat_tag_by_id_and_user_id_and_tag_name( - id, user.id, form_data.name, db=db - ) + await Chats.add_chat_tag_by_id_and_user_id_and_tag_name(id, user.id, form_data.name, db=db) - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) - tags = chat.meta.get("tags", []) - return Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + tags = chat.meta.get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -1459,53 +1541,22 @@ async def add_tag_by_id_and_tag_name( ############################ -@router.delete("/{id}/tags", response_model=list[TagModel]) +@router.delete('/{id}/tags', response_model=list[TagModel]) async def delete_tag_by_id_and_tag_name( id: str, form_data: TagForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: - Chats.delete_tag_by_id_and_user_id_and_tag_name( - id, user.id, form_data.name, db=db - ) + await Chats.delete_tag_by_id_and_user_id_and_tag_name(id, user.id, form_data.name, db=db) - if ( - Chats.count_chats_by_tag_name_and_user_id(form_data.name, user.id, db=db) - == 0 - ): - Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) + if await Chats.count_chats_by_tag_name_and_user_id(form_data.name, user.id, db=db) == 0: + await Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) - tags = chat.meta.get("tags", []) - return Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) + chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) + tags = chat.meta.get('tags', []) + return await Tags.get_tags_by_ids_and_user_id(tags, user.id, db=db) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) - - -############################ -# DeleteAllTagsById -############################ - - -@router.delete("/{id}/tags/all", response_model=Optional[bool]) -async def delete_all_tags_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db) - if chat: - Chats.delete_all_tags_by_id_and_user_id(id, user.id, db=db) - - for tag in chat.meta.get("tags", []): - if Chats.count_chats_by_tag_name_and_user_id(tag, user.id, db=db) == 0: - Tags.delete_tag_by_name_and_user_id(tag, user.id, db=db) - - return True - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND) diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 152e2c3edc..c40131f41f 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -1,33 +1,34 @@ -import logging -import copy -from fastapi import APIRouter, Depends, Request, HTTPException -from pydantic import BaseModel, ConfigDict -import aiohttp +from __future__ import annotations +import copy +import logging from typing import Optional -from open_webui.env import AIOHTTP_CLIENT_TIMEOUT +import aiohttp +from fastapi import APIRouter, Depends, HTTPException, Request +from mcp.shared.auth import OAuthMetadata +from open_webui.config import BannerModel, async_save_config, get_config, save_config +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.models.oauth_sessions import OAuthSessions from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.config import get_config, save_config -from open_webui.config import BannerModel - +from open_webui.utils.headers import get_custom_headers +from open_webui.utils.mcp.client import MCPClient +from open_webui.utils.oauth import ( + OAuthClientInformationFull, + decrypt_data, + encrypt_data, + get_discovery_urls, + get_oauth_client_info_with_dynamic_client_registration, + get_oauth_client_info_with_static_credentials, + resolve_oauth_client_info, +) from open_webui.utils.tools import ( get_tool_server_data, get_tool_server_url, + set_terminal_servers, set_tool_servers, ) -from open_webui.utils.mcp.client import MCPClient -from open_webui.models.oauth_sessions import OAuthSessions - - -from open_webui.utils.oauth import ( - get_discovery_urls, - get_oauth_client_info_with_dynamic_client_registration, - encrypt_data, - decrypt_data, - OAuthClientInformationFull, -) -from mcp.shared.auth import OAuthMetadata +from pydantic import BaseModel, ConfigDict router = APIRouter() @@ -36,6 +37,8 @@ log = logging.getLogger(__name__) ############################ # ImportConfig +# Thy configuration come, thy settings be done, +# in production as it is in development. ############################ @@ -43,9 +46,10 @@ class ImportConfigForm(BaseModel): config: dict -@router.post("/import", response_model=dict) -async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user)): - save_config(form_data.config) +@router.post('/import', response_model=dict) +async def import_config(request: Request, form_data: ImportConfigForm, user=Depends(get_admin_user)): + await async_save_config(form_data.config) + request.app.state.config._sync_to_redis() return get_config() @@ -54,7 +58,7 @@ async def import_config(form_data: ImportConfigForm, user=Depends(get_admin_user ############################ -@router.get("/export", response_model=dict) +@router.get('/export', response_model=dict) async def export_config(user=Depends(get_admin_user)): return get_config() @@ -69,67 +73,73 @@ class ConnectionsConfigForm(BaseModel): ENABLE_BASE_MODELS_CACHE: bool -@router.get("/connections", response_model=ConnectionsConfigForm) +@router.get('/connections', response_model=ConnectionsConfigForm) async def get_connections_config(request: Request, user=Depends(get_admin_user)): return { - "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS, - "ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE, + 'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE, } -@router.post("/connections", response_model=ConnectionsConfigForm) +@router.post('/connections', response_model=ConnectionsConfigForm) async def set_connections_config( request: Request, form_data: ConnectionsConfigForm, user=Depends(get_admin_user), ): - request.app.state.config.ENABLE_DIRECT_CONNECTIONS = ( - form_data.ENABLE_DIRECT_CONNECTIONS - ) - request.app.state.config.ENABLE_BASE_MODELS_CACHE = ( - form_data.ENABLE_BASE_MODELS_CACHE - ) + request.app.state.config.ENABLE_DIRECT_CONNECTIONS = form_data.ENABLE_DIRECT_CONNECTIONS + request.app.state.config.ENABLE_BASE_MODELS_CACHE = form_data.ENABLE_BASE_MODELS_CACHE return { - "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS, - "ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE, + 'ENABLE_DIRECT_CONNECTIONS': request.app.state.config.ENABLE_DIRECT_CONNECTIONS, + 'ENABLE_BASE_MODELS_CACHE': request.app.state.config.ENABLE_BASE_MODELS_CACHE, } class OAuthClientRegistrationForm(BaseModel): url: str client_id: str - client_name: Optional[str] = None + client_name: str | None = None + client_secret: str | None = None + oauth_server_url: str | None = None -@router.post("/oauth/clients/register") +@router.post('/oauth/clients/register') async def register_oauth_client( request: Request, form_data: OAuthClientRegistrationForm, - type: Optional[str] = None, + type: str | None = None, user=Depends(get_admin_user), ): try: oauth_client_id = form_data.client_id if type: - oauth_client_id = f"{type}:{form_data.client_id}" + oauth_client_id = f'{type}:{form_data.client_id}' - oauth_client_info = ( - await get_oauth_client_info_with_dynamic_client_registration( - request, oauth_client_id, form_data.url + oauth_server_url = form_data.oauth_server_url if form_data.oauth_server_url else form_data.url + + if form_data.client_secret: + # Static credentials: skip dynamic registration, build from provided credentials + oauth_client_info = await get_oauth_client_info_with_static_credentials( + request, + oauth_client_id, + oauth_server_url, + oauth_client_id=form_data.client_id, + oauth_client_secret=form_data.client_secret, + ) + else: + oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration( + request, oauth_client_id, oauth_server_url ) - ) return { - "status": True, - "oauth_client_info": encrypt_data( - oauth_client_info.model_dump(mode="json") - ), + 'status': True, + 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), } except Exception as e: - log.debug(f"Failed to register OAuth client: {e}") + log.debug(f'Failed to register OAuth client: {e}') raise HTTPException( status_code=400, - detail=f"Failed to register OAuth client", + detail=f'Failed to register OAuth client', ) @@ -141,44 +151,45 @@ async def register_oauth_client( class ToolServerConnection(BaseModel): url: str path: str - type: Optional[str] = "openapi" # openapi, mcp - auth_type: Optional[str] - headers: Optional[dict | str] = None - key: Optional[str] - config: Optional[dict] + type: str | None = 'openapi' # openapi, mcp + auth_type: str | None + headers: dict | str | None = None + key: str | None + config: dict | None + info: dict | None = None - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class ToolServersConfigForm(BaseModel): TOOL_SERVER_CONNECTIONS: list[ToolServerConnection] -@router.get("/tool_servers", response_model=ToolServersConfigForm) +@router.get('/tool_servers', response_model=ToolServersConfigForm) async def get_tool_servers_config(request: Request, user=Depends(get_admin_user)): return { - "TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS, + 'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS, } -@router.post("/tool_servers", response_model=ToolServersConfigForm) +@router.post('/tool_servers', response_model=ToolServersConfigForm) async def set_tool_servers_config( request: Request, form_data: ToolServersConfigForm, user=Depends(get_admin_user), ): for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: - server_type = connection.get("type", "openapi") - auth_type = connection.get("auth_type", "none") + server_type = connection.get('type', 'openapi') + auth_type = connection.get('auth_type', 'none') - if auth_type == "oauth_2.1": + if auth_type in ('oauth_2.1', 'oauth_2.1_static'): # Remove existing OAuth clients for tool servers - server_id = connection.get("info", {}).get("id") - client_key = f"{server_type}:{server_id}" + server_id = connection.get('info', {}).get('id') + client_key = f'{server_type}:{server_id}' try: request.app.state.oauth_client_manager.remove_client(client_key) - except: + except Exception: pass # Set new tool server connections @@ -189,78 +200,211 @@ async def set_tool_servers_config( await set_tool_servers(request) for connection in request.app.state.config.TOOL_SERVER_CONNECTIONS: - server_type = connection.get("type", "openapi") - if server_type == "mcp": - server_id = connection.get("info", {}).get("id") - auth_type = connection.get("auth_type", "none") + server_type = connection.get('type', 'openapi') + if server_type == 'mcp': + server_id = connection.get('info', {}).get('id') + auth_type = connection.get('auth_type', 'none') - if auth_type == "oauth_2.1" and server_id: + if auth_type in ('oauth_2.1', 'oauth_2.1_static') and server_id: try: - oauth_client_info = connection.get("info", {}).get( - "oauth_client_info", "" - ) - oauth_client_info = decrypt_data(oauth_client_info) - + oauth_client_info = resolve_oauth_client_info(connection) request.app.state.oauth_client_manager.add_client( - f"{server_type}:{server_id}", + f'{server_type}:{server_id}', OAuthClientInformationFull(**oauth_client_info), ) except Exception as e: - log.debug(f"Failed to add OAuth client for MCP tool server: {e}") + log.debug(f'Failed to add OAuth client for MCP tool server: {e}') continue return { - "TOOL_SERVER_CONNECTIONS": request.app.state.config.TOOL_SERVER_CONNECTIONS, + 'TOOL_SERVER_CONNECTIONS': request.app.state.config.TOOL_SERVER_CONNECTIONS, } -@router.post("/tool_servers/verify") -async def verify_tool_servers_config( - request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user) +class TerminalServerConnection(BaseModel): + id: str | None = '' + name: str | None = '' + + enabled: bool | None = True + + url: str + path: str | None = '/openapi.json' + + key: str | None = '' + auth_type: str | None = 'bearer' + + config: dict | None = None + + # Orchestrator policy fields + server_type: str | None = None # "orchestrator", "terminal" + policy_id: str | None = None + policy: dict | None = None # cached policy data + + model_config = ConfigDict(extra='allow') + + +class TerminalServersConfigForm(BaseModel): + TERMINAL_SERVER_CONNECTIONS: list[TerminalServerConnection] + + +@router.get('/terminal_servers') +async def get_terminal_servers_config(request: Request, user=Depends(get_admin_user)): + return { + 'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS, + } + + +@router.post('/terminal_servers') +async def set_terminal_servers_config( + request: Request, + form_data: TerminalServersConfigForm, + user=Depends(get_admin_user), ): + request.app.state.config.TERMINAL_SERVER_CONNECTIONS = [ + connection.model_dump() for connection in form_data.TERMINAL_SERVER_CONNECTIONS + ] + + await set_terminal_servers(request) + + return { + 'TERMINAL_SERVER_CONNECTIONS': request.app.state.config.TERMINAL_SERVER_CONNECTIONS, + } + + +@router.post('/terminal_servers/verify') +async def verify_terminal_server_connection( + request: Request, form_data: TerminalServerConnection, user=Depends(get_admin_user) +): + """ + Verify the connection to a terminal server by detecting its type. + + Tries GET {url}/api/v1/policies (orchestrator) then GET {url}/api/config + (plain terminal). Returns ``{status: true, type: "orchestrator"|"terminal"}``. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + # Orchestrators expose a policies API; plain terminals don't. + try: + async with session.get( + f'{base_url}/api/v1/policies', headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.ok: + return {'status': True, 'type': 'orchestrator'} + except Exception: + pass + + # Fall back to open-terminal config endpoint. + try: + async with session.get( + f'{base_url}/api/config', headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.ok: + return {'status': True, 'type': 'terminal'} + except Exception: + pass + + except Exception as e: + log.debug(f'Failed to connect to the terminal server: {e}') + + raise HTTPException(status_code=400, detail='Failed to connect to the terminal server') + + +class TerminalServerPolicyForm(BaseModel): + url: str + key: str | None = '' + auth_type: str | None = 'bearer' + policy_id: str + policy_data: dict + + +@router.post('/terminal_servers/policy') +async def put_terminal_server_policy( + request: Request, form_data: TerminalServerPolicyForm, user=Depends(get_admin_user) +): + """ + Proxy a policy PUT to an orchestrator terminal server. + """ + base_url = (form_data.url or '').rstrip('/') + if not base_url: + raise HTTPException(status_code=400, detail='Terminal server URL is required') + + headers = {'Content-Type': 'application/json'} + if form_data.auth_type == 'bearer' and form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + + try: + async with aiohttp.ClientSession( + trust_env=True, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as session: + policy_url = f'{base_url}/api/v1/policies/{form_data.policy_id}' + async with session.put( + policy_url, headers=headers, json=form_data.policy_data, ssl=AIOHTTP_CLIENT_SESSION_SSL + ) as resp: + if resp.ok: + return await resp.json() + detail = await resp.text() + raise HTTPException(status_code=resp.status, detail=detail) + except HTTPException: + raise + except Exception as e: + log.debug(f'Failed to save policy to terminal server: {e}') + raise HTTPException(status_code=400, detail='Failed to save policy to terminal server') + + +@router.post('/tool_servers/verify') +async def verify_tool_servers_config(request: Request, form_data: ToolServerConnection, user=Depends(get_admin_user)): """ Verify the connection to the tool server. """ try: - if form_data.type == "mcp": - if form_data.auth_type == "oauth_2.1": - discovery_urls = get_discovery_urls(form_data.url) + if form_data.type == 'mcp': + if form_data.auth_type in ('oauth_2.1', 'oauth_2.1_static'): + oauth_server_url = ( + form_data.info.get('oauth_server_url') + if form_data.info and form_data.info.get('oauth_server_url') + else form_data.url + ) + discovery_urls = await get_discovery_urls(oauth_server_url) for discovery_url in discovery_urls: - log.debug( - f"Trying to fetch OAuth 2.1 discovery document from {discovery_url}" - ) + log.debug(f'Trying to fetch OAuth 2.1 discovery document from {discovery_url}') async with aiohttp.ClientSession( trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), ) as session: async with session.get( - discovery_url + discovery_url, ssl=AIOHTTP_CLIENT_SESSION_SSL ) as oauth_server_metadata_response: if oauth_server_metadata_response.status == 200: try: - oauth_server_metadata = ( - OAuthMetadata.model_validate( - await oauth_server_metadata_response.json() - ) + oauth_server_metadata = OAuthMetadata.model_validate( + await oauth_server_metadata_response.json() ) return { - "status": True, - "oauth_server_metadata": oauth_server_metadata.model_dump( - mode="json" - ), + 'status': True, + 'oauth_server_metadata': oauth_server_metadata.model_dump(mode='json'), } except Exception as e: - log.info( - f"Failed to parse OAuth 2.1 discovery document: {e}" - ) + log.info(f'Failed to parse OAuth 2.1 discovery document: {e}') raise HTTPException( status_code=400, - detail=f"Failed to parse OAuth 2.1 discovery document from {discovery_url}", + detail=f'Failed to parse OAuth 2.1 discovery document from {discovery_url}', ) raise HTTPException( status_code=400, - detail=f"Failed to fetch OAuth 2.1 discovery document from {discovery_urls}", + detail=f'Failed to fetch OAuth 2.1 discovery document from {discovery_urls}', ) else: try: @@ -268,42 +412,43 @@ async def verify_tool_servers_config( headers = None token = None - if form_data.auth_type == "bearer": + if form_data.auth_type == 'bearer': token = form_data.key - elif form_data.auth_type == "session": + elif form_data.auth_type == 'session': token = request.state.token.credentials - elif form_data.auth_type == "system_oauth": + elif form_data.auth_type == 'system_oauth': oauth_token = None try: - if request.cookies.get("oauth_session_id", None): + if request.cookies.get('oauth_session_id', None): oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, - request.cookies.get("oauth_session_id", None), + request.cookies.get('oauth_session_id', None), ) if oauth_token: - token = oauth_token.get("access_token", "") + token = oauth_token.get('access_token', '') except Exception as e: pass if token: - headers = {"Authorization": f"Bearer {token}"} + headers = {'Authorization': f'Bearer {token}'} if form_data.headers and isinstance(form_data.headers, dict): if headers is None: headers = {} - headers.update(form_data.headers) + custom_headers = get_custom_headers(form_data.headers, user) + headers.update(custom_headers) await client.connect(form_data.url, headers=headers) specs = await client.list_tool_specs() return { - "status": True, - "specs": specs, + 'status': True, + 'specs': specs, } except Exception as e: - log.debug(f"Failed to create MCP client: {e}") + log.debug(f'Failed to create MCP client: {e}') raise HTTPException( status_code=400, - detail=f"Failed to create MCP client", + detail=f'Failed to create MCP client', ) finally: if client: @@ -311,43 +456,42 @@ async def verify_tool_servers_config( else: # openapi token = None headers = None - if form_data.auth_type == "bearer": + if form_data.auth_type == 'bearer': token = form_data.key - elif form_data.auth_type == "session": + elif form_data.auth_type == 'session': token = request.state.token.credentials - elif form_data.auth_type == "system_oauth": + elif form_data.auth_type == 'system_oauth': try: - if request.cookies.get("oauth_session_id", None): - oauth_token = ( - await request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get('oauth_session_id', None): + oauth_token = await request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get('oauth_session_id', None), ) if oauth_token: - token = oauth_token.get("access_token", "") + token = oauth_token.get('access_token', '') except Exception as e: pass if token: - headers = {"Authorization": f"Bearer {token}"} + headers = {'Authorization': f'Bearer {token}'} if form_data.headers and isinstance(form_data.headers, dict): if headers is None: headers = {} - headers.update(form_data.headers) + custom_headers = get_custom_headers(form_data.headers, user) + headers.update(custom_headers) url = get_tool_server_url(form_data.url, form_data.path) return await get_tool_server_data(url, headers=headers) except HTTPException as e: raise e except Exception as e: - log.debug(f"Failed to connect to the tool server: {e}") + log.debug(f'Failed to connect to the tool server: {e}') raise HTTPException( status_code=400, - detail=f"Failed to connect to the tool server", + detail=f'Failed to connect to the tool server', ) @@ -357,106 +501,83 @@ async def verify_tool_servers_config( class CodeInterpreterConfigForm(BaseModel): ENABLE_CODE_EXECUTION: bool CODE_EXECUTION_ENGINE: str - CODE_EXECUTION_JUPYTER_URL: Optional[str] - CODE_EXECUTION_JUPYTER_AUTH: Optional[str] - CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str] - CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str] - CODE_EXECUTION_JUPYTER_TIMEOUT: Optional[int] + CODE_EXECUTION_JUPYTER_URL: str | None + CODE_EXECUTION_JUPYTER_AUTH: str | None + CODE_EXECUTION_JUPYTER_AUTH_TOKEN: str | None + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: str | None + CODE_EXECUTION_JUPYTER_TIMEOUT: int | None ENABLE_CODE_INTERPRETER: bool CODE_INTERPRETER_ENGINE: str - CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str] - CODE_INTERPRETER_JUPYTER_URL: Optional[str] - CODE_INTERPRETER_JUPYTER_AUTH: Optional[str] - CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str] - CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] - CODE_INTERPRETER_JUPYTER_TIMEOUT: Optional[int] + CODE_INTERPRETER_PROMPT_TEMPLATE: str | None + CODE_INTERPRETER_JUPYTER_URL: str | None + CODE_INTERPRETER_JUPYTER_AUTH: str | None + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: str | None + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: str | None + CODE_INTERPRETER_JUPYTER_TIMEOUT: int | None -@router.get("/code_execution", response_model=CodeInterpreterConfigForm) +@router.get('/code_execution', response_model=CodeInterpreterConfigForm) async def get_code_execution_config(request: Request, user=Depends(get_admin_user)): return { - "ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION, - "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, - "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL, - "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, - "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, - "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, - "CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, - "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, - "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, - "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, - "CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, - "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, - "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, - "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, - "CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, + 'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION, + 'CODE_EXECUTION_ENGINE': request.app.state.config.CODE_EXECUTION_ENGINE, + 'CODE_EXECUTION_JUPYTER_URL': request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + 'CODE_EXECUTION_JUPYTER_AUTH': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + 'CODE_EXECUTION_JUPYTER_TIMEOUT': request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, + 'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER, + 'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE, + 'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, + 'CODE_INTERPRETER_JUPYTER_URL': request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + 'CODE_INTERPRETER_JUPYTER_AUTH': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + 'CODE_INTERPRETER_JUPYTER_TIMEOUT': request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, } -@router.post("/code_execution", response_model=CodeInterpreterConfigForm) +@router.post('/code_execution', response_model=CodeInterpreterConfigForm) async def set_code_execution_config( request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) ): - request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE - request.app.state.config.CODE_EXECUTION_JUPYTER_URL = ( - form_data.CODE_EXECUTION_JUPYTER_URL - ) - request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = ( - form_data.CODE_EXECUTION_JUPYTER_AUTH - ) - request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = ( - form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN - ) - request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( - form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD - ) - request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = ( - form_data.CODE_EXECUTION_JUPYTER_TIMEOUT - ) + request.app.state.config.CODE_EXECUTION_JUPYTER_URL = form_data.CODE_EXECUTION_JUPYTER_URL + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = form_data.CODE_EXECUTION_JUPYTER_AUTH + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = form_data.CODE_EXECUTION_JUPYTER_TIMEOUT request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE - request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = ( - form_data.CODE_INTERPRETER_PROMPT_TEMPLATE - ) + request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = form_data.CODE_INTERPRETER_PROMPT_TEMPLATE - request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = ( - form_data.CODE_INTERPRETER_JUPYTER_URL - ) + request.app.state.config.CODE_INTERPRETER_JUPYTER_URL = form_data.CODE_INTERPRETER_JUPYTER_URL - request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = ( - form_data.CODE_INTERPRETER_JUPYTER_AUTH - ) + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = form_data.CODE_INTERPRETER_JUPYTER_AUTH - request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = ( - form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN - ) - request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ( - form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD - ) - request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = ( - form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT - ) + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = form_data.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN + request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD + request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT return { - "ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION, - "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, - "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL, - "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, - "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, - "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, - "CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, - "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, - "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, - "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, - "CODE_INTERPRETER_JUPYTER_URL": request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, - "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, - "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, - "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, - "CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, + 'ENABLE_CODE_EXECUTION': request.app.state.config.ENABLE_CODE_EXECUTION, + 'CODE_EXECUTION_ENGINE': request.app.state.config.CODE_EXECUTION_ENGINE, + 'CODE_EXECUTION_JUPYTER_URL': request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + 'CODE_EXECUTION_JUPYTER_AUTH': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + 'CODE_EXECUTION_JUPYTER_TIMEOUT': request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, + 'ENABLE_CODE_INTERPRETER': request.app.state.config.ENABLE_CODE_INTERPRETER, + 'CODE_INTERPRETER_ENGINE': request.app.state.config.CODE_INTERPRETER_ENGINE, + 'CODE_INTERPRETER_PROMPT_TEMPLATE': request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, + 'CODE_INTERPRETER_JUPYTER_URL': request.app.state.config.CODE_INTERPRETER_JUPYTER_URL, + 'CODE_INTERPRETER_JUPYTER_AUTH': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH, + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD': request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + 'CODE_INTERPRETER_JUPYTER_TIMEOUT': request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, } @@ -464,31 +585,44 @@ async def set_code_execution_config( # SetDefaultModels ############################ class ModelsConfigForm(BaseModel): - DEFAULT_MODELS: Optional[str] - DEFAULT_PINNED_MODELS: Optional[str] - MODEL_ORDER_LIST: Optional[list[str]] + DEFAULT_MODELS: str | None + DEFAULT_PINNED_MODELS: str | None + MODEL_ORDER_LIST: list[str | None] + DEFAULT_MODEL_METADATA: dict | None = None + DEFAULT_MODEL_PARAMS: dict | None = None -@router.get("/models", response_model=ModelsConfigForm) -async def get_models_config(request: Request, user=Depends(get_admin_user)): +@router.get('/models/defaults') +async def get_models_defaults(request: Request, user=Depends(get_verified_user)): return { - "DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS, - "DEFAULT_PINNED_MODELS": request.app.state.config.DEFAULT_PINNED_MODELS, - "MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST, + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, } -@router.post("/models", response_model=ModelsConfigForm) -async def set_models_config( - request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user) -): +@router.get('/models', response_model=ModelsConfigForm) +async def get_models_config(request: Request, user=Depends(get_admin_user)): + return { + 'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS, + 'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS, + 'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST, + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, + 'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS, + } + + +@router.post('/models', response_model=ModelsConfigForm) +async def set_models_config(request: Request, form_data: ModelsConfigForm, user=Depends(get_admin_user)): request.app.state.config.DEFAULT_MODELS = form_data.DEFAULT_MODELS request.app.state.config.DEFAULT_PINNED_MODELS = form_data.DEFAULT_PINNED_MODELS request.app.state.config.MODEL_ORDER_LIST = form_data.MODEL_ORDER_LIST + request.app.state.config.DEFAULT_MODEL_METADATA = form_data.DEFAULT_MODEL_METADATA + request.app.state.config.DEFAULT_MODEL_PARAMS = form_data.DEFAULT_MODEL_PARAMS return { - "DEFAULT_MODELS": request.app.state.config.DEFAULT_MODELS, - "DEFAULT_PINNED_MODELS": request.app.state.config.DEFAULT_PINNED_MODELS, - "MODEL_ORDER_LIST": request.app.state.config.MODEL_ORDER_LIST, + 'DEFAULT_MODELS': request.app.state.config.DEFAULT_MODELS, + 'DEFAULT_PINNED_MODELS': request.app.state.config.DEFAULT_PINNED_MODELS, + 'MODEL_ORDER_LIST': request.app.state.config.MODEL_ORDER_LIST, + 'DEFAULT_MODEL_METADATA': request.app.state.config.DEFAULT_MODEL_METADATA, + 'DEFAULT_MODEL_PARAMS': request.app.state.config.DEFAULT_MODEL_PARAMS, } @@ -501,14 +635,14 @@ class SetDefaultSuggestionsForm(BaseModel): suggestions: list[PromptSuggestion] -@router.post("/suggestions", response_model=list[PromptSuggestion]) +@router.post('/suggestions', response_model=list[PromptSuggestion]) async def set_default_suggestions( request: Request, form_data: SetDefaultSuggestionsForm, user=Depends(get_admin_user), ): data = form_data.model_dump() - request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"] + request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data['suggestions'] return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS @@ -521,18 +655,18 @@ class SetBannersForm(BaseModel): banners: list[BannerModel] -@router.post("/banners", response_model=list[BannerModel]) +@router.post('/banners', response_model=list[BannerModel]) async def set_banners( request: Request, form_data: SetBannersForm, user=Depends(get_admin_user), ): data = form_data.model_dump() - request.app.state.config.BANNERS = data["banners"] + request.app.state.config.BANNERS = data['banners'] return request.app.state.config.BANNERS -@router.get("/banners", response_model=list[BannerModel]) +@router.get('/banners', response_model=list[BannerModel]) async def get_banners( request: Request, user=Depends(get_verified_user), diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index 22bb20df9b..eb79bfa63c 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -1,27 +1,25 @@ -from typing import Optional import logging -from fastapi import APIRouter, Depends, HTTPException, status, Request -from fastapi.concurrency import run_in_threadpool -from pydantic import BaseModel +from typing import Optional -from open_webui.models.users import Users, UserModel +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.concurrency import run_in_threadpool +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session from open_webui.models.feedbacks import ( - FeedbackIdResponse, - FeedbackModel, - FeedbackResponse, FeedbackForm, - FeedbackUserResponse, + FeedbackIdResponse, FeedbackListResponse, + FeedbackModel, + Feedbacks, + FeedbackUserResponse, LeaderboardFeedbackData, ModelHistoryEntry, ModelHistoryResponse, - Feedbacks, ) - -from open_webui.constants import ERROR_MESSAGES +from open_webui.models.users import UserModel, Users from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -30,6 +28,8 @@ router = APIRouter() # Leaderboard Elo Rating Computation +# The judgment has already been rendered with grace; +# the scales have been balanced by a hand that never errs. # # How it works: # 1. Each model starts with a rating of 1000 @@ -51,9 +51,7 @@ router = APIRouter() import os -EMBEDDING_MODEL_NAME = os.environ.get( - "AUXILIARY_EMBEDDING_MODEL", "TaylorAI/bge-micro-v2" -) +EMBEDDING_MODEL_NAME = os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2') _embedding_model = None @@ -65,13 +63,11 @@ def _get_embedding_model(): _embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME) except Exception as e: - log.error(f"Embedding model load failed: {e}") + log.error(f'Embedding model load failed: {e}') return _embedding_model -def _calculate_elo( - feedbacks: list[LeaderboardFeedbackData], similarities: dict = None -) -> dict: +def _calculate_elo(feedbacks: list[LeaderboardFeedbackData], similarities: dict = None) -> dict: """ Calculate Elo ratings for models based on user feedback. @@ -90,35 +86,33 @@ def _calculate_elo( def get_or_create_stats(model_id): if model_id not in model_stats: - model_stats[model_id] = {"rating": 1000.0, "won": 0, "lost": 0} + model_stats[model_id] = {'rating': 1000.0, 'won': 0, 'lost': 0} return model_stats[model_id] for feedback in feedbacks: data = feedback.data or {} - winner_id = data.get("model_id") - rating_value = str(data.get("rating", "")) - if not winner_id or rating_value not in ("1", "-1"): + winner_id = data.get('model_id') + rating_value = str(data.get('rating', '')) + if not winner_id or rating_value not in ('1', '-1'): continue - won = rating_value == "1" + won = rating_value == '1' weight = similarities.get(feedback.id, 1.0) if similarities else 1.0 - for opponent_id in data.get("sibling_model_ids") or []: + for opponent_id in data.get('sibling_model_ids') or []: winner = get_or_create_stats(winner_id) opponent = get_or_create_stats(opponent_id) - expected = 1 / (1 + 10 ** ((opponent["rating"] - winner["rating"]) / 400)) + expected = 1 / (1 + 10 ** ((opponent['rating'] - winner['rating']) / 400)) - winner["rating"] += K_FACTOR * ((1 if won else 0) - expected) * weight - opponent["rating"] += ( - K_FACTOR * ((0 if won else 1) - (1 - expected)) * weight - ) + winner['rating'] += K_FACTOR * ((1 if won else 0) - expected) * weight + opponent['rating'] += K_FACTOR * ((0 if won else 1) - (1 - expected)) * weight if won: - winner["won"] += 1 - opponent["lost"] += 1 + winner['won'] += 1 + opponent['lost'] += 1 else: - winner["lost"] += 1 - opponent["won"] += 1 + winner['lost'] += 1 + opponent['won'] += 1 return model_stats @@ -139,16 +133,13 @@ def _get_top_tags(feedbacks: list[LeaderboardFeedbackData], limit: int = 5) -> d for feedback in feedbacks: data = feedback.data or {} - model_id = data.get("model_id") + model_id = data.get('model_id') if model_id: - for tag in data.get("tags", []): + for tag in data.get('tags', []): tag_counts[model_id][tag] += 1 return { - model_id: [ - {"tag": tag, "count": count} - for tag, count in sorted(tags.items(), key=lambda x: -x[1])[:limit] - ] + model_id: [{'tag': tag, 'count': count} for tag, count in sorted(tags.items(), key=lambda x: -x[1])[:limit]] for model_id, tags in tag_counts.items() } @@ -172,14 +163,7 @@ def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str) if not embedding_model: return {} - all_tags = list( - { - tag - for feedback in feedbacks - if feedback.data - for tag in feedback.data.get("tags", []) - } - ) + all_tags = list({tag for feedback in feedbacks if feedback.data for tag in feedback.data.get('tags', [])}) if not all_tags: return {} @@ -187,23 +171,18 @@ def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str) tag_embeddings = embedding_model.encode(all_tags) query_embedding = embedding_model.encode([query])[0] except Exception as e: - log.error(f"Embedding error: {e}") + log.error(f'Embedding error: {e}') return {} # Vectorized cosine similarity tag_norms = np.linalg.norm(tag_embeddings, axis=1) query_norm = np.linalg.norm(query_embedding) - similarities = np.dot(tag_embeddings, query_embedding) / ( - tag_norms * query_norm + 1e-9 - ) + similarities = np.dot(tag_embeddings, query_embedding) / (tag_norms * query_norm + 1e-9) tag_similarity_map = dict(zip(all_tags, similarities.tolist())) return { feedback.id: max( - ( - tag_similarity_map.get(tag, 0) - for tag in (feedback.data or {}).get("tags", []) - ), + (tag_similarity_map.get(tag, 0) for tag in (feedback.data or {}).get('tags', [])), default=0, ) for feedback in feedbacks @@ -223,20 +202,18 @@ class LeaderboardResponse(BaseModel): entries: list[LeaderboardEntry] -@router.get("/leaderboard", response_model=LeaderboardResponse) +@router.get('/leaderboard', response_model=LeaderboardResponse) async def get_leaderboard( query: Optional[str] = None, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Get model leaderboard with Elo ratings. Query filters by tag similarity.""" - feedbacks = Feedbacks.get_feedbacks_for_leaderboard(db=db) + feedbacks = await Feedbacks.get_feedbacks_for_leaderboard(db=db) similarities = None if query and query.strip(): - similarities = await run_in_threadpool( - _compute_similarities, feedbacks, query.strip() - ) + similarities = await run_in_threadpool(_compute_similarities, feedbacks, query.strip()) elo_stats = _calculate_elo(feedbacks, similarities) tags_by_model = _get_top_tags(feedbacks) @@ -245,10 +222,10 @@ async def get_leaderboard( [ LeaderboardEntry( model_id=mid, - rating=round(s["rating"]), - won=s["won"], - lost=s["lost"], - count=s["won"] + s["lost"], + rating=round(s['rating']), + won=s['won'], + lost=s['lost'], + count=s['won'] + s['lost'], top_tags=tags_by_model.get(mid, []), ) for mid, s in elo_stats.items() @@ -260,17 +237,15 @@ async def get_leaderboard( return LeaderboardResponse(entries=entries) -@router.get("/leaderboard/{model_id}/history", response_model=ModelHistoryResponse) +@router.get('/leaderboard/{model_id}/history', response_model=ModelHistoryResponse) async def get_model_history( model_id: str, days: int = 30, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Get daily win/loss history for a specific model.""" - history = Feedbacks.get_model_evaluation_history( - model_id=model_id, days=days, db=db - ) + history = await Feedbacks.get_model_evaluation_history(model_id=model_id, days=days, db=db) return ModelHistoryResponse(model_id=model_id, history=history) @@ -279,11 +254,11 @@ async def get_model_history( ############################ -@router.get("/config") +@router.get('/config') async def get_config(request: Request, user=Depends(get_admin_user)): return { - "ENABLE_EVALUATION_ARENA_MODELS": request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS, - "EVALUATION_ARENA_MODELS": request.app.state.config.EVALUATION_ARENA_MODELS, + 'ENABLE_EVALUATION_ARENA_MODELS': request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS, + 'EVALUATION_ARENA_MODELS': request.app.state.config.EVALUATION_ARENA_MODELS, } @@ -297,7 +272,7 @@ class UpdateConfigForm(BaseModel): EVALUATION_ARENA_MODELS: Optional[list[dict]] = None -@router.post("/config") +@router.post('/config') async def update_config( request: Request, form_data: UpdateConfigForm, @@ -309,68 +284,62 @@ async def update_config( if form_data.EVALUATION_ARENA_MODELS is not None: config.EVALUATION_ARENA_MODELS = form_data.EVALUATION_ARENA_MODELS return { - "ENABLE_EVALUATION_ARENA_MODELS": config.ENABLE_EVALUATION_ARENA_MODELS, - "EVALUATION_ARENA_MODELS": config.EVALUATION_ARENA_MODELS, + 'ENABLE_EVALUATION_ARENA_MODELS': config.ENABLE_EVALUATION_ARENA_MODELS, + 'EVALUATION_ARENA_MODELS': config.EVALUATION_ARENA_MODELS, } -@router.get("/feedbacks/all", response_model=list[FeedbackResponse]) -async def get_all_feedbacks( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - feedbacks = Feedbacks.get_all_feedbacks(db=db) - return feedbacks +@router.get('/feedbacks/models', response_model=list[str]) +async def get_feedback_model_ids(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Feedbacks.get_distinct_model_ids(db=db) -@router.get("/feedbacks/all/ids", response_model=list[FeedbackIdResponse]) -async def get_all_feedback_ids( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - return Feedbacks.get_all_feedback_ids(db=db) +@router.get('/feedbacks/all/ids', response_model=list[FeedbackIdResponse]) +async def get_all_feedback_ids(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Feedbacks.get_all_feedback_ids(db=db) -@router.delete("/feedbacks/all") -async def delete_all_feedbacks( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - success = Feedbacks.delete_all_feedbacks(db=db) +@router.delete('/feedbacks/all') +async def delete_all_feedbacks(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + success = await Feedbacks.delete_all_feedbacks(db=db) return success -@router.get("/feedbacks/all/export", response_model=list[FeedbackModel]) +@router.get('/feedbacks/all/export', response_model=list[FeedbackModel]) async def export_all_feedbacks( - user=Depends(get_admin_user), db: Session = Depends(get_session) + model_id: Optional[str] = None, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), ): - feedbacks = Feedbacks.get_all_feedbacks(db=db) + feedbacks = await Feedbacks.get_all_feedbacks(db=db) + if model_id: + feedbacks = [f for f in feedbacks if f.data and f.data.get('model_id') == model_id] return feedbacks -@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse]) -async def get_feedbacks( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id, db=db) +@router.get('/feedbacks/user', response_model=list[FeedbackUserResponse]) +async def get_feedbacks(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + feedbacks = await Feedbacks.get_feedbacks_by_user_id(user.id, db=db) return feedbacks -@router.delete("/feedbacks", response_model=bool) -async def delete_feedbacks( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - success = Feedbacks.delete_feedbacks_by_user_id(user.id, db=db) +@router.delete('/feedbacks', response_model=bool) +async def delete_feedbacks(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + success = await Feedbacks.delete_feedbacks_by_user_id(user.id, db=db) return success PAGE_ITEM_COUNT = 30 -@router.get("/feedbacks/list", response_model=FeedbackListResponse) +@router.get('/feedbacks/list', response_model=FeedbackListResponse) async def get_feedbacks( order_by: Optional[str] = None, direction: Optional[str] = None, page: Optional[int] = 1, + model_id: Optional[str] = None, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): limit = PAGE_ITEM_COUNT @@ -379,24 +348,24 @@ async def get_feedbacks( filter = {} if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction + if model_id: + filter['model_id'] = model_id - result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit, db=db) + result = await Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit, db=db) return result -@router.post("/feedback", response_model=FeedbackModel) +@router.post('/feedback', response_model=FeedbackModel) async def create_feedback( request: Request, form_data: FeedbackForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - feedback = Feedbacks.insert_new_feedback( - user_id=user.id, form_data=form_data, db=db - ) + feedback = await Feedbacks.insert_new_feedback(user_id=user.id, form_data=form_data, db=db) if not feedback: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -406,61 +375,47 @@ async def create_feedback( return feedback -@router.get("/feedback/{id}", response_model=FeedbackModel) -async def get_feedback_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - if user.role == "admin": - feedback = Feedbacks.get_feedback_by_id(id=id, db=db) +@router.get('/feedback/{id}', response_model=FeedbackModel) +async def get_feedback_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin': + feedback = await Feedbacks.get_feedback_by_id(id=id, db=db) else: - feedback = Feedbacks.get_feedback_by_id_and_user_id( - id=id, user_id=user.id, db=db - ) + feedback = await Feedbacks.get_feedback_by_id_and_user_id(id=id, user_id=user.id, db=db) if not feedback: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) return feedback -@router.post("/feedback/{id}", response_model=FeedbackModel) +@router.post('/feedback/{id}', response_model=FeedbackModel) async def update_feedback_by_id( id: str, form_data: FeedbackForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role == "admin": - feedback = Feedbacks.update_feedback_by_id(id=id, form_data=form_data, db=db) + if user.role == 'admin': + feedback = await Feedbacks.update_feedback_by_id(id=id, form_data=form_data, db=db) else: - feedback = Feedbacks.update_feedback_by_id_and_user_id( - id=id, user_id=user.id, form_data=form_data, db=db - ) + feedback = await Feedbacks.update_feedback_by_id_and_user_id(id=id, user_id=user.id, form_data=form_data, db=db) if not feedback: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) return feedback -@router.delete("/feedback/{id}") +@router.delete('/feedback/{id}') async def delete_feedback_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - if user.role == "admin": - success = Feedbacks.delete_feedback_by_id(id=id, db=db) + if user.role == 'admin': + success = await Feedbacks.delete_feedback_by_id(id=id, db=db) else: - success = Feedbacks.delete_feedback_by_id_and_user_id( - id=id, user_id=user.id, db=db - ) + success = await Feedbacks.delete_feedback_by_id_and_user_id(id=id, user_id=user.id, db=db) if not success: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) return success diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index e3dd63525a..3e036613d4 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -1,196 +1,229 @@ +import asyncio +import hashlib +import json import logging import os import uuid -import json from pathlib import Path from typing import Optional from urllib.parse import quote -import asyncio from fastapi import ( - BackgroundTasks, APIRouter, + BackgroundTasks, Depends, File, Form, HTTPException, + Query, Request, UploadFile, status, - Query, ) - from fastapi.responses import FileResponse, StreamingResponse -from sqlalchemy.orm import Session -from open_webui.internal.db import get_session, SessionLocal - +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STORAGE_LOCAL_CACHE, STORAGE_PROVIDER, UPLOAD_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT - +from open_webui.internal.db import get_async_db_context, get_async_session +from open_webui.models.access_grants import AccessGrants from open_webui.models.channels import Channels -from open_webui.models.users import Users +from open_webui.models.chats import Chats from open_webui.models.files import ( FileForm, + FileListResponse, FileModel, FileModelResponse, Files, ) -from open_webui.models.chats import Chats -from open_webui.models.knowledge import Knowledges from open_webui.models.groups import Groups - - -from open_webui.routers.retrieval import ProcessFileForm, process_file +from open_webui.models.knowledge import Knowledges +from open_webui.models.users import Users +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT from open_webui.routers.audio import transcribe - +from open_webui.routers.retrieval import ProcessFileForm, process_file from open_webui.storage.provider import Storage - - from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access from open_webui.utils.misc import strict_match_mime_type from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) router = APIRouter() -############################ -# Check if the current user has access to a file through any knowledge bases the user may be in. -############################ - - -# TODO: Optimize this function to use the knowledge_file table for faster lookups. -def has_access_to_file( - file_id: Optional[str], - access_type: str, - user=Depends(get_verified_user), - db: Optional[Session] = None, -) -> bool: - file = Files.get_file_by_id(file_id, db=db) - log.debug(f"Checking if user has {access_type} access to file") - if not file: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=ERROR_MESSAGES.NOT_FOUND, - ) - - # Check if the file is associated with any knowledge bases the user has access to - knowledge_bases = Knowledges.get_knowledges_by_file_id(file_id, db=db) - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user.id, db=db) - } - for knowledge_base in knowledge_bases: - if knowledge_base.user_id == user.id or has_access( - user.id, access_type, knowledge_base.access_control, user_group_ids, db=db - ): - return True - - knowledge_base_id = file.meta.get("collection_name") if file.meta else None - if knowledge_base_id: - knowledge_bases = Knowledges.get_knowledge_bases_by_user_id( - user.id, access_type, db=db - ) - for knowledge_base in knowledge_bases: - if knowledge_base.id == knowledge_base_id: - return True - - # Check if the file is associated with any channels the user has access to - channels = Channels.get_channels_by_file_id_and_user_id(file_id, user.id, db=db) - if access_type == "read" and channels: - return True - - # Check if the file is associated with any chats the user has access to - # TODO: Granular access control for chats - chats = Chats.get_shared_chats_by_file_id(file_id, db=db) - if chats: - return True - - return False - +from open_webui.utils.access_control.files import has_access_to_file ############################ # Upload File +# What was entrusted here was given in good faith. Let it +# be returned the same way, whole and undiminished. ############################ -def process_uploaded_file( +def _is_text_file(file_path: str, chunk_size: int = 8192) -> bool: + """Check if a file is likely a text file by reading a chunk and decoding it. + + Tries UTF-8 first, then falls back to Latin-1 (which accepts every byte + in 0x00–0xFF) so that legacy-encoded files from Windows environments are + not misclassified as binary. + + This catches files whose extensions are mis-mapped by mimetypes/browsers + (e.g. TypeScript .ts → video/mp2t) without maintaining an extension whitelist. + """ + try: + resolved = Storage.get_file(file_path) + with open(resolved, 'rb') as f: + chunk = f.read(chunk_size) + if not chunk: + return False + # Null bytes are a strong indicator of binary content + if b'\x00' in chunk: + return False + try: + chunk.decode('utf-8') + except UnicodeDecodeError: + # Latin-1 always succeeds (every byte is valid), so this + # effectively just means "the file has no null bytes and is + # therefore likely text, even if not valid UTF-8". + chunk.decode('latin-1') + return True + except Exception: + return False + + +def _cleanup_local_cache(file_path: str) -> None: + """Remove the local cached copy of a cloud-stored file after processing.""" + if STORAGE_LOCAL_CACHE or STORAGE_PROVIDER == 'local': + return + try: + local_filename = os.path.basename(file_path) + local_path = os.path.join(UPLOAD_DIR, local_filename) + if os.path.isfile(local_path): + os.remove(local_path) + log.debug(f'Cleaned up local cache: {local_path}') + except OSError as e: + log.warning(f'Failed to clean up local cache for {file_path}: {e}') + + +async def process_uploaded_file( request, file, file_path, file_item, file_metadata, user, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ): - def _process_handler(db_session): + async def _process_handler(db_session): try: - if file.content_type: - stt_supported_content_types = getattr( - request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", [] + content_type = file.content_type + + # Detect mis-labeled text files (e.g. .ts → video/mp2t) + if content_type and content_type.startswith(('image/', 'video/')): + if _is_text_file(file_path): + content_type = 'text/plain' + + stt_supported = getattr( + request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', [] + ) + + if content_type and strict_match_mime_type(stt_supported, content_type): + # Audio / STT-supported files → transcribe then index + file_path_processed = await asyncio.to_thread(Storage.get_file, file_path) + result = await transcribe( + request, + file_path_processed, + file_metadata, + user, + ) + await process_file( + request, + ProcessFileForm(file_id=file_item.id, content=result.get('text', '')), + user=user, + db=db_session, ) - if strict_match_mime_type( - stt_supported_content_types, file.content_type - ): - file_path_processed = Storage.get_file(file_path) - result = transcribe( - request, file_path_processed, file_metadata, user - ) - - process_file( - request, - ProcessFileForm( - file_id=file_item.id, content=result.get("text", "") - ), - user=user, - db=db_session, - ) - elif (not file.content_type.startswith(("image/", "video/"))) or ( - request.app.state.config.CONTENT_EXTRACTION_ENGINE == "external" - ): - process_file( - request, - ProcessFileForm(file_id=file_item.id), - user=user, + elif ( + content_type + and content_type.startswith(('image/', 'video/')) + and request.app.state.config.CONTENT_EXTRACTION_ENGINE != 'external' + ): + # Media files without an external extraction engine + if content_type.startswith('video/'): + # Videos are stored as-is for downstream multimodal + # processing (Tools, vision models). Attempting text + # extraction causes "Timeout reached while detecting + # encoding" errors. + log.info(f'Video file detected ({content_type}), skipping text extraction') + await Files.update_file_data_by_id( + file_item.id, + {'status': 'completed'}, db=db_session, ) else: raise Exception( - f"File type {file.content_type} is not supported for processing" + f'File type {content_type} is not supported for processing' ) + else: - log.info( - f"File type {file.content_type} is not provided, but trying to process anyway" - ) - process_file( + # Documents, or any file when an external engine is configured + if not content_type: + log.info( + f'File type {file.content_type} is not provided, ' + 'but trying to process anyway' + ) + await process_file( request, ProcessFileForm(file_id=file_item.id), user=user, db=db_session, ) + # Auto-link to Knowledge Collection when uploaded from one (#24807). + # Mirrors POST /knowledge/{id}/file/add so linking doesn't depend + # on the frontend staying connected after upload. + knowledge_id = file_metadata.get('knowledge_id') + if knowledge_id: + try: + await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=knowledge_id, + file_id=file_item.id, + user_id=user.id, + directory_id=file_metadata.get('directory_id'), + ) + await process_file( + request, + ProcessFileForm(file_id=file_item.id, collection_name=knowledge_id), + user=user, + db=db_session, + ) + log.info(f'Linked file {file_item.id} to knowledge {knowledge_id}') + except Exception as e: + log.warning(f'Failed to link file {file_item.id} to knowledge {knowledge_id}: {e}') + except Exception as e: - log.error(f"Error processing file: {file_item.id}") - Files.update_file_data_by_id( + log.error(f'Error processing file: {file_item.id}') + await Files.update_file_data_by_id( file_item.id, { - "status": "failed", - "error": str(e.detail) if hasattr(e, "detail") else str(e), + 'status': 'failed', + 'error': str(e.detail) if hasattr(e, 'detail') else str(e), }, db=db_session, ) - if db: - _process_handler(db) - else: - with SessionLocal() as db_session: - _process_handler(db_session) + try: + if db: + await _process_handler(db) + else: + async with get_async_db_context() as db_session: + await _process_handler(db_session) + finally: + _cleanup_local_cache(file_path) -@router.post("/", response_model=FileModelResponse) -def upload_file( +@router.post('/', response_model=FileModelResponse) +async def upload_file( request: Request, background_tasks: BackgroundTasks, file: UploadFile = File(...), @@ -198,9 +231,9 @@ def upload_file( process: bool = Query(True), process_in_background: bool = Query(True), user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - return upload_file_handler( + return await upload_file_handler( request, file=file, metadata=metadata, @@ -212,7 +245,7 @@ def upload_file( ) -def upload_file_handler( +async def upload_file_handler( request: Request, file: UploadFile = File(...), metadata: Optional[dict | str] = Form(None), @@ -220,9 +253,9 @@ def upload_file_handler( process_in_background: bool = Query(True), user=Depends(get_verified_user), background_tasks: Optional[BackgroundTasks] = None, - db: Optional[Session] = None, + db: Optional[AsyncSession] = None, ): - log.info(f"file.content_type: {file.content_type} {process}") + log.info(f'file.content_type: {file.content_type} {process}') if isinstance(metadata, str): try: @@ -230,7 +263,7 @@ def upload_file_handler( except json.JSONDecodeError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Invalid metadata format"), + detail=ERROR_MESSAGES.DEFAULT('Invalid metadata format'), ) file_metadata = metadata if metadata else {} @@ -239,8 +272,8 @@ def upload_file_handler( filename = os.path.basename(unsanitized_filename) file_extension = os.path.splitext(filename)[1] - # Remove the leading dot from the file extension - file_extension = file_extension[1:] if file_extension else "" + # Remove the leading dot from the file extension and lowercase it + file_extension = file_extension[1:].lower() if file_extension else '' if process and request.app.state.config.ALLOWED_FILE_EXTENSIONS: request.app.state.config.ALLOWED_FILE_EXTENSIONS = [ @@ -250,55 +283,55 @@ def upload_file_handler( if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT( - f"File type {file_extension} is not allowed" - ), + detail=ERROR_MESSAGES.DEFAULT(f'File type {file_extension} is not allowed'), ) # replace filename with uuid id = str(uuid.uuid4()) name = filename - filename = f"{id}_{filename}" - contents, file_path = Storage.upload_file( + filename = f'{id}_{filename}' + contents, file_path = await asyncio.to_thread( + Storage.upload_file, file.file, filename, { - "OpenWebUI-User-Email": user.email, - "OpenWebUI-User-Id": user.id, - "OpenWebUI-User-Name": user.name, - "OpenWebUI-File-Id": id, + 'OpenWebUI-User-Email': user.email, + 'OpenWebUI-User-Id': user.id, + 'OpenWebUI-User-Name': user.name, + 'OpenWebUI-File-Id': id, }, ) - file_item = Files.insert_new_file( + # SHA-256 of raw uploaded bytes for incremental sync diffing. + # If the client pre-computed and sent file_hash, use that. + file_hash = file_metadata.get('file_hash') or hashlib.sha256(contents).hexdigest() + + file_item = await Files.insert_new_file( user.id, FileForm( **{ - "id": id, - "filename": name, - "path": file_path, - "data": { - **({"status": "pending"} if process else {}), + 'id': id, + 'filename': name, + 'path': file_path, + 'data': { + **({'status': 'pending'} if process else {}), }, - "meta": { - "name": name, - "content_type": file.content_type, - "size": len(contents), - "data": file_metadata, + 'meta': { + 'name': name, + 'content_type': (file.content_type if isinstance(file.content_type, str) else None), + 'size': len(contents), + 'file_hash': file_hash, + 'data': file_metadata, }, } ), db=db, ) - if "channel_id" in file_metadata: - channel = Channels.get_channel_by_id_and_user_id( - file_metadata["channel_id"], user.id, db=db - ) + if 'channel_id' in file_metadata: + channel = await Channels.get_channel_by_id_and_user_id(file_metadata['channel_id'], user.id, db=db) if channel: - Channels.add_file_to_channel_by_id( - channel.id, file_item.id, user.id, db=db - ) + await Channels.add_file_to_channel_by_id(channel.id, file_item.id, user.id, db=db) if process: if background_tasks and process_in_background: @@ -311,9 +344,9 @@ def upload_file_handler( file_metadata, user, ) - return {"status": True, **file_item.model_dump()} + return {'status': True, **file_item.model_dump()} else: - process_uploaded_file( + await process_uploaded_file( request, file, file_path, @@ -322,21 +355,23 @@ def upload_file_handler( user, db=db, ) - return {"status": True, **file_item.model_dump()} + return {'status': True, **file_item.model_dump()} else: if file_item: return file_item else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error uploading file"), + detail=ERROR_MESSAGES.DEFAULT('Error uploading file'), ) + except HTTPException as e: + raise e except Exception as e: log.exception(e) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error uploading file"), + detail=ERROR_MESSAGES.DEFAULT('Error uploading file'), ) @@ -345,23 +380,27 @@ def upload_file_handler( ############################ -@router.get("/", response_model=list[FileModelResponse]) +PAGE_SIZE = 50 + + +@router.get('/', response_model=FileListResponse) async def list_files( user=Depends(get_verified_user), + page: int = Query(1, ge=1, description='Page number (1-indexed)'), content: bool = Query(True), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role == "admin": - files = Files.get_files(db=db) - else: - files = Files.get_files_by_user_id(user.id, db=db) + skip = (page - 1) * PAGE_SIZE + user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id + + result = await Files.get_file_list(user_id=user_id, skip=skip, limit=PAGE_SIZE, db=db) if not content: - for file in files: - if "content" in file.data: - del file.data["content"] + for file in result.items: + if file.data and 'content' in file.data: + del file.data['content'] - return files + return result ############################ @@ -369,29 +408,27 @@ async def list_files( ############################ -@router.get("/search", response_model=list[FileModelResponse]) +@router.get('/search', response_model=list[FileModelResponse]) async def search_files( filename: str = Query( ..., description="Filename pattern to search for. Supports wildcards such as '*.txt'", ), content: bool = Query(True), - skip: int = Query(0, ge=0, description="Number of files to skip"), - limit: int = Query( - 100, ge=1, le=1000, description="Maximum number of files to return" - ), + skip: int = Query(0, ge=0, description='Number of files to skip'), + limit: int = Query(100, ge=1, le=1000, description='Maximum number of files to return'), user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """ Search for files by filename with support for wildcard patterns. Uses SQL-based filtering with pagination for better performance. """ - # Determine user_id: null for admin (search all), user.id for regular users - user_id = None if user.role == "admin" else user.id + # Determine user_id: null for admin with bypass (search all), user.id otherwise + user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id # Use optimized database query with pagination - files = Files.search_files( + files = await Files.search_files( user_id=user_id, filename=filename, skip=skip, @@ -402,13 +439,13 @@ async def search_files( if not files: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="No files found matching the pattern.", + detail='No files found matching the pattern.', ) if not content: for file in files: - if file.data and "content" in file.data: - del file.data["content"] + if file.data and 'content' in file.data: + del file.data['content'] return files @@ -418,27 +455,25 @@ async def search_files( ############################ -@router.delete("/all") -async def delete_all_files( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - result = Files.delete_all_files(db=db) +@router.delete('/all') +async def delete_all_files(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + result = await Files.delete_all_files(db=db) if result: try: - Storage.delete_all_files() - VECTOR_DB_CLIENT.reset() + await asyncio.to_thread(Storage.delete_all_files) + await ASYNC_VECTOR_DB_CLIENT.reset() except Exception as e: log.exception(e) - log.error("Error deleting files") + log.error('Error deleting files') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error deleting files"), + detail=ERROR_MESSAGES.DEFAULT('Error deleting files'), ) - return {"message": "All files deleted successfully"} + return {'message': 'All files deleted successfully'} else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error deleting files"), + detail=ERROR_MESSAGES.DEFAULT('Error deleting files'), ) @@ -447,11 +482,9 @@ async def delete_all_files( ############################ -@router.get("/{id}", response_model=Optional[FileModel]) -async def get_file_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - file = Files.get_file_by_id(id, db=db) +@router.get('/{id}', response_model=Optional[FileModel]) +async def get_file_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -459,11 +492,7 @@ async def get_file_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "read", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): return file else: raise HTTPException( @@ -472,14 +501,14 @@ async def get_file_by_id( ) -@router.get("/{id}/process/status") +@router.get('/{id}/process/status') async def get_file_process_status( id: str, stream: bool = Query(False), user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - file = Files.get_file_by_id(id, db=db) + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -487,11 +516,7 @@ async def get_file_process_status( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "read", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): if stream: MAX_FILE_PROCESSING_DURATION = 3600 * 2 @@ -500,34 +525,34 @@ async def get_file_process_status( # Each poll creates its own short-lived session to avoid holding a # connection for hours. A WebSocket push would be more efficient. for _ in range(MAX_FILE_PROCESSING_DURATION): - file_item = Files.get_file_by_id(file_id) # Creates own session + file_item = await Files.get_file_by_id(file_id) # Creates own session if file_item: - data = file_item.model_dump().get("data", {}) - status = data.get("status") + data = file_item.model_dump().get('data', {}) + status = data.get('status') if status: - event = {"status": status} - if status == "failed": - event["error"] = data.get("error") + event = {'status': status} + if status == 'failed': + event['error'] = data.get('error') - yield f"data: {json.dumps(event)}\n\n" - if status in ("completed", "failed"): + yield f'data: {json.dumps(event)}\n\n' + if status in ('completed', 'failed'): break else: # Legacy break else: - yield f"data: {json.dumps({'status': 'not_found'})}\n\n" + yield f'data: {json.dumps({"status": "not_found"})}\n\n' break await asyncio.sleep(1) return StreamingResponse( event_stream(file.id), - media_type="text/event-stream", + media_type='text/event-stream', ) else: - return {"status": file.data.get("status", "pending")} + return {'status': file.data.get('status', 'pending')} else: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -540,11 +565,11 @@ async def get_file_process_status( ############################ -@router.get("/{id}/data/content") +@router.get('/{id}/data/content') async def get_file_data_content_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - file = Files.get_file_by_id(id, db=db) + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -552,12 +577,8 @@ async def get_file_data_content_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "read", user, db=db) - ): - return {"content": file.data.get("content", "")} + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): + return {'content': file.data.get('content', '')} else: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -574,15 +595,15 @@ class ContentForm(BaseModel): content: str -@router.post("/{id}/data/content/update") +@router.post('/{id}/data/content/update') async def update_file_data_content_by_id( request: Request, id: str, form_data: ContentForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - file = Files.get_file_by_id(id, db=db) + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -590,23 +611,38 @@ async def update_file_data_content_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "write", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'write', user, db=db): try: - process_file( + await process_file( request, ProcessFileForm(file_id=id, content=form_data.content), user=user, + db=db, ) - file = Files.get_file_by_id(id=id, db=db) + file = await Files.get_file_by_id(id=id, db=db) except Exception as e: log.exception(e) - log.error(f"Error processing file: {file.id}") + log.error(f'Error processing file: {file.id}') - return {"content": file.data.get("content", "")} + # Propagate content change to all knowledge collections referencing + # this file. Without this the old embeddings remain in the knowledge + # collection and RAG returns both stale and current data (#20558). + knowledges = await Knowledges.get_knowledges_by_file_id(id, db=db) + for knowledge in knowledges: + try: + # Remove old embeddings for this file from the KB collection + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': id}) + # Re-add from the now-updated file-{file_id} collection + await process_file( + request, + ProcessFileForm(file_id=id, collection_name=knowledge.id), + user=user, + db=db, + ) + except Exception as e: + log.warning(f'Failed to update knowledge {knowledge.id} after content change for file {id}: {e}') + + return {'content': file.data.get('content', '')} else: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -619,14 +655,14 @@ async def update_file_data_content_by_id( ############################ -@router.get("/{id}/content") +@router.get('/{id}/content') async def get_file_content_by_id( id: str, user=Depends(get_verified_user), attachment: bool = Query(False), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - file = Files.get_file_by_id(id, db=db) + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -634,42 +670,30 @@ async def get_file_content_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "read", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): try: - file_path = Storage.get_file(file.path) + file_path = await asyncio.to_thread(Storage.get_file, file.path) file_path = Path(file_path) # Check if the file already exists in the cache if file_path.is_file(): # Handle Unicode filenames - filename = file.meta.get("name", file.filename) + filename = file.meta.get('name', file.filename) encoded_filename = quote(filename) # RFC5987 encoding - content_type = file.meta.get("content_type") - filename = file.meta.get("name", file.filename) + content_type = file.meta.get('content_type') + filename = file.meta.get('name', file.filename) encoded_filename = quote(filename) headers = {} if attachment: - headers["Content-Disposition"] = ( - f"attachment; filename*=UTF-8''{encoded_filename}" - ) + headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{encoded_filename}" else: - if content_type == "application/pdf" or filename.lower().endswith( - ".pdf" - ): - headers["Content-Disposition"] = ( - f"inline; filename*=UTF-8''{encoded_filename}" - ) - content_type = "application/pdf" - elif content_type != "text/plain": - headers["Content-Disposition"] = ( - f"attachment; filename*=UTF-8''{encoded_filename}" - ) + if content_type == 'application/pdf' or filename.lower().endswith('.pdf'): + headers['Content-Disposition'] = f"inline; filename*=UTF-8''{encoded_filename}" + content_type = 'application/pdf' + elif content_type != 'text/plain': + headers['Content-Disposition'] = f"attachment; filename*=UTF-8''{encoded_filename}" return FileResponse(file_path, headers=headers, media_type=content_type) @@ -678,12 +702,14 @@ async def get_file_content_by_id( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) + except HTTPException as e: + raise e except Exception as e: log.exception(e) - log.error("Error getting file content") + log.error('Error getting file content') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error getting file content"), + detail=ERROR_MESSAGES.DEFAULT('Error getting file content'), ) else: raise HTTPException( @@ -692,11 +718,11 @@ async def get_file_content_by_id( ) -@router.get("/{id}/content/html") +@router.get('/{id}/content/html') async def get_html_file_content_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - file = Files.get_file_by_id(id, db=db) + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -704,37 +730,35 @@ async def get_html_file_content_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - file_user = Users.get_user_by_id(file.user_id, db=db) - if not file_user.role == "admin": + file_user = await Users.get_user_by_id(file.user_id, db=db) + if not file_user or file_user.role != 'admin': raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "read", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): try: - file_path = Storage.get_file(file.path) + file_path = await asyncio.to_thread(Storage.get_file, file.path) file_path = Path(file_path) # Check if the file already exists in the cache if file_path.is_file(): - log.info(f"file_path: {file_path}") + log.info(f'file_path: {file_path}') return FileResponse(file_path) else: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) + except HTTPException as e: + raise e except Exception as e: log.exception(e) - log.error("Error getting file content") + log.error('Error getting file content') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error getting file content"), + detail=ERROR_MESSAGES.DEFAULT('Error getting file content'), ) else: raise HTTPException( @@ -743,11 +767,11 @@ async def get_html_file_content_by_id( ) -@router.get("/{id}/content/{file_name}") +@router.get('/{id}/content/{file_name}') async def get_file_content_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - file = Files.get_file_by_id(id, db=db) + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -755,22 +779,16 @@ async def get_file_content_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "read", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db): file_path = file.path # Handle Unicode filenames - filename = file.meta.get("name", file.filename) + filename = file.meta.get('name', file.filename) encoded_filename = quote(filename) # RFC5987 encoding - headers = { - "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}" - } + headers = {'Content-Disposition': f"attachment; filename*=UTF-8''{encoded_filename}"} if file_path: - file_path = Storage.get_file(file_path) + file_path = await asyncio.to_thread(Storage.get_file, file_path) file_path = Path(file_path) # Check if the file already exists in the cache @@ -783,16 +801,16 @@ async def get_file_content_by_id( ) else: # File path doesn’t exist, return the content as .txt if possible - file_content = file.content.get("content", "") + file_content = file.data.get('content', '') file_name = file.filename # Create a generator that encodes the file content def generator(): - yield file_content.encode("utf-8") + yield file_content.encode('utf-8') return StreamingResponse( generator(), - media_type="text/plain", + media_type='text/plain', headers=headers, ) else: @@ -802,16 +820,54 @@ async def get_file_content_by_id( ) +############################ +# Rename File By Id +############################ + + +class FileRenameForm(BaseModel): + filename: str + + +@router.post('/{id}/rename') +async def rename_file_by_id( + id: str, + form_data: FileRenameForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + file = await Files.get_file_by_id(id, db=db) + + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'write', user, db=db): + result = await Files.update_file_name_by_id(id, form_data.filename, db=db) + if result: + return result + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error renaming file'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + ############################ # Delete File By Id ############################ -@router.delete("/{id}") -async def delete_file_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - file = Files.get_file_by_id(id, db=db) +@router.delete('/{id}') +async def delete_file_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + file = await Files.get_file_by_id(id, db=db) if not file: raise HTTPException( @@ -819,29 +875,37 @@ async def delete_file_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if ( - file.user_id == user.id - or user.role == "admin" - or has_access_to_file(id, "write", user, db=db) - ): + if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'write', user, db=db): + # Clean up KB associations and embeddings before deleting + knowledges = await Knowledges.get_knowledges_by_file_id(id, db=db) + for knowledge in knowledges: + # Remove KB-file relationship + await Knowledges.remove_file_from_knowledge_by_id(knowledge.id, id, db=db) + # Clean KB embeddings (same logic as /knowledge/{id}/file/remove) + try: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': id}) + if file.hash: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'hash': file.hash}) + except Exception as e: + log.debug(f'KB embedding cleanup for {knowledge.id}: {e}') - result = Files.delete_file_by_id(id, db=db) + result = await Files.delete_file_by_id(id, db=db) if result: try: - Storage.delete_file(file.path) - VECTOR_DB_CLIENT.delete(collection_name=f"file-{id}") + await asyncio.to_thread(Storage.delete_file, file.path) + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=f'file-{id}') except Exception as e: log.exception(e) - log.error("Error deleting files") + log.error('Error deleting files') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error deleting files"), + detail=ERROR_MESSAGES.DEFAULT('Error deleting files'), ) - return {"message": "File deleted successfully"} + return {'message': 'File deleted successfully'} else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error deleting file"), + detail=ERROR_MESSAGES.DEFAULT('Error deleting file'), ) else: raise HTTPException( diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index 1c9b2229cf..8d77de4894 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -1,38 +1,29 @@ import logging +import mimetypes import os import shutil import uuid from pathlib import Path from typing import Optional -from pydantic import BaseModel -import mimetypes - +from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status +from fastapi.responses import FileResponse, StreamingResponse +from open_webui.config import UPLOAD_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from open_webui.models.chats import Chats from open_webui.models.folders import ( FolderForm, - FolderUpdateForm, FolderModel, FolderNameIdResponse, Folders, + FolderUpdateForm, ) -from open_webui.models.chats import Chats -from open_webui.models.files import Files -from open_webui.models.knowledge import Knowledges - - -from open_webui.config import UPLOAD_DIR -from open_webui.constants import ERROR_MESSAGES -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session - - -from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Request -from fastapi.responses import FileResponse, StreamingResponse - - -from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_permission - +from open_webui.utils.access_control.files import get_accessible_folder_files +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -45,11 +36,11 @@ router = APIRouter() ############################ -@router.get("/", response_model=list[FolderNameIdResponse]) +@router.get('/', response_model=list[FolderNameIdResponse]) async def get_folders( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if request.app.state.config.ENABLE_FOLDERS is False: raise HTTPException( @@ -57,9 +48,9 @@ async def get_folders( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - if user.role != "admin" and not has_permission( + if user.role != 'admin' and not await has_permission( user.id, - "features.folders", + 'features.folders', request.app.state.config.USER_PERMISSIONS, db=db, ): @@ -68,38 +59,19 @@ async def get_folders( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - folders = Folders.get_folders_by_user_id(user.id, db=db) + folders = await Folders.get_folders_by_user_id(user.id, db=db) # Verify folder data integrity folder_list = [] for folder in folders: - if folder.parent_id and not Folders.get_folder_by_id_and_user_id( - folder.parent_id, user.id, db=db - ): - folder = Folders.update_folder_parent_id_by_id_and_user_id( - folder.id, user.id, None, db=db - ) + if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db): + folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db) - if folder.data: - if "files" in folder.data: - valid_files = [] - for file in folder.data["files"]: - - if file.get("type") == "file": - if Files.check_access_by_user_id( - file.get("id"), user.id, "read", db=db - ): - valid_files.append(file) - elif file.get("type") == "collection": - if Knowledges.check_access_by_user_id( - file.get("id"), user.id, "read", db=db - ): - valid_files.append(file) - else: - valid_files.append(file) - - folder.data["files"] = valid_files - Folders.update_folder_by_id_and_user_id( + if folder.data and 'files' in folder.data: + accessible_files = await get_accessible_folder_files(folder.data['files'], user, db=db) + if len(accessible_files) != len(folder.data.get('files', [])): + folder.data['files'] = accessible_files + await Folders.update_folder_by_id_and_user_id( folder.id, user.id, FolderUpdateForm(data=folder.data), db=db ) @@ -113,31 +85,31 @@ async def get_folders( ############################ -@router.post("/") -def create_folder( +@router.post('/') +async def create_folder( form_data: FolderForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - folder = Folders.get_folder_by_parent_id_and_user_id_and_name( - None, user.id, form_data.name, db=db + folder = await Folders.get_folder_by_parent_id_and_user_id_and_name( + form_data.parent_id, user.id, form_data.name, db=db ) if folder: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), ) try: - folder = Folders.insert_new_folder(user.id, form_data, db=db) + folder = await Folders.insert_new_folder(user.id, form_data, form_data.parent_id, db=db) return folder except Exception as e: log.exception(e) - log.error("Error creating folder") + log.error('Error creating folder') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error creating folder"), + detail=ERROR_MESSAGES.DEFAULT('Error creating folder'), ) @@ -146,11 +118,9 @@ def create_folder( ############################ -@router.get("/{id}", response_model=Optional[FolderModel]) -async def get_folder_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db) +@router.get('/{id}', response_model=Optional[FolderModel]) +async def get_folder_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) if folder: return folder else: @@ -165,38 +135,45 @@ async def get_folder_by_id( ############################ -@router.post("/{id}/update") +@router.post('/{id}/update') async def update_folder_name_by_id( id: str, form_data: FolderUpdateForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) if folder: - if form_data.name is not None: # Check if folder with same name exists - existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name( + existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name( folder.parent_id, user.id, form_data.name, db=db ) if existing_folder and existing_folder.id != id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), + ) + + # Validate read access to every file/collection being attached. + # Folder files are consumed by chat middleware as RAG context. + if form_data.data and isinstance(form_data.data.get('files'), list): + accessible_files = await get_accessible_folder_files(form_data.data['files'], user, db=db) + if len(accessible_files) != len(form_data.data['files']): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) try: - folder = Folders.update_folder_by_id_and_user_id( - id, user.id, form_data, db=db - ) + folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db) return folder except Exception as e: log.exception(e) - log.error(f"Error updating folder: {id}") + log.error(f'Error updating folder: {id}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating folder"), + detail=ERROR_MESSAGES.DEFAULT('Error updating folder'), ) else: raise HTTPException( @@ -214,36 +191,34 @@ class FolderParentIdForm(BaseModel): parent_id: Optional[str] = None -@router.post("/{id}/update/parent") +@router.post('/{id}/update/parent') async def update_folder_parent_id_by_id( id: str, form_data: FolderParentIdForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) if folder: - existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name( + existing_folder = await Folders.get_folder_by_parent_id_and_user_id_and_name( form_data.parent_id, user.id, folder.name, db=db ) if existing_folder: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Folder already exists"), + detail=ERROR_MESSAGES.DEFAULT('Folder already exists'), ) try: - folder = Folders.update_folder_parent_id_by_id_and_user_id( - id, user.id, form_data.parent_id, db=db - ) + folder = await Folders.update_folder_parent_id_by_id_and_user_id(id, user.id, form_data.parent_id, db=db) return folder except Exception as e: log.exception(e) - log.error(f"Error updating folder: {id}") + log.error(f'Error updating folder: {id}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating folder"), + detail=ERROR_MESSAGES.DEFAULT('Error updating folder'), ) else: raise HTTPException( @@ -261,26 +236,26 @@ class FolderIsExpandedForm(BaseModel): is_expanded: bool -@router.post("/{id}/update/expanded") +@router.post('/{id}/update/expanded') async def update_folder_is_expanded_by_id( id: str, form_data: FolderIsExpandedForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - folder = Folders.get_folder_by_id_and_user_id(id, user.id, db=db) + folder = await Folders.get_folder_by_id_and_user_id(id, user.id, db=db) if folder: try: - folder = Folders.update_folder_is_expanded_by_id_and_user_id( + folder = await Folders.update_folder_is_expanded_by_id_and_user_id( id, user.id, form_data.is_expanded, db=db ) return folder except Exception as e: log.exception(e) - log.error(f"Error updating folder: {id}") + log.error(f'Error updating folder: {id}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating folder"), + detail=ERROR_MESSAGES.DEFAULT('Error updating folder'), ) else: raise HTTPException( @@ -294,55 +269,49 @@ async def update_folder_is_expanded_by_id( ############################ -@router.delete("/{id}") +@router.delete('/{id}') async def delete_folder_by_id( request: Request, id: str, delete_contents: Optional[bool] = True, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if Chats.count_chats_by_folder_id_and_user_id(id, user.id, db=db): - chat_delete_permission = has_permission( - user.id, "chat.delete", request.app.state.config.USER_PERMISSIONS, db=db + if await Chats.count_chats_by_folder_id_and_user_id(id, user.id, db=db): + chat_delete_permission = await has_permission( + user.id, 'chat.delete', request.app.state.config.USER_PERMISSIONS, db=db ) - if user.role != "admin" and not chat_delete_permission: + if user.role != 'admin' and not chat_delete_permission: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) folders = [] - folders.append(Folders.get_folder_by_id_and_user_id(id, user.id, db=db)) + folders.append(await Folders.get_folder_by_id_and_user_id(id, user.id, db=db)) while folders: folder = folders.pop() if folder: try: - folder_ids = Folders.delete_folder_by_id_and_user_id(id, user.id, db=db) + folder_ids = await Folders.delete_folder_by_id_and_user_id(folder.id, user.id, db=db) for folder_id in folder_ids: if delete_contents: - Chats.delete_chats_by_user_id_and_folder_id( - user.id, folder_id, db=db - ) + await Chats.delete_chats_by_user_id_and_folder_id(user.id, folder_id, db=db) else: - Chats.move_chats_by_user_id_and_folder_id( - user.id, folder_id, None, db=db - ) + await Chats.move_chats_by_user_id_and_folder_id(user.id, folder_id, None, db=db) return True except Exception as e: log.exception(e) - log.error(f"Error deleting folder: {id}") + log.error(f'Error deleting folder: {id}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error deleting folder"), + detail=ERROR_MESSAGES.DEFAULT('Error deleting folder'), ) finally: # Get all subfolders - subfolders = Folders.get_folders_by_parent_id_and_user_id( - folder.id, user.id, db=db - ) + subfolders = await Folders.get_folders_by_parent_id_and_user_id(folder.id, user.id, db=db) folders.extend(subfolders) else: diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index ad47318911..58ec93657e 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -1,33 +1,34 @@ -import os -import re +from __future__ import annotations import logging -import aiohttp +import os +import re from pathlib import Path from typing import Optional -from open_webui.env import AIOHTTP_CLIENT_TIMEOUT +import aiohttp +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.config import CACHE_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.internal.db import get_async_session from open_webui.models.functions import ( FunctionForm, FunctionModel, FunctionResponse, + Functions, FunctionUserResponse, FunctionWithValvesModel, - Functions, ) +from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.plugin import ( + get_function_module_from_cache, load_function_module_by_id, replace_imports, - get_function_module_from_cache, + resolve_valves_schema_options, ) -from open_webui.config import CACHE_DIR -from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Request, status -from open_webui.utils.auth import get_admin_user, get_verified_user from pydantic import BaseModel, HttpUrl -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session - +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -36,21 +37,19 @@ router = APIRouter() ############################ # GetFunctions +# Our daily functions give us, and forgive us +# our deprecated methods, as we refactor those who depend on us. ############################ -@router.get("/", response_model=list[FunctionResponse]) -async def get_functions( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - return Functions.get_functions(db=db) +@router.get('/', response_model=list[FunctionResponse]) +async def get_functions(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Functions.get_functions(db=db) -@router.get("/list", response_model=list[FunctionUserResponse]) -async def get_function_list( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - return Functions.get_function_list(db=db) +@router.get('/list', response_model=list[FunctionUserResponse]) +async def get_function_list(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Functions.get_function_list(db=db) ############################ @@ -58,13 +57,13 @@ async def get_function_list( ############################ -@router.get("/export", response_model=list[FunctionModel | FunctionWithValvesModel]) +@router.get('/export', response_model=list[FunctionModel | FunctionWithValvesModel]) async def get_functions( include_valves: bool = False, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - return Functions.get_functions(include_valves=include_valves, db=db) + return await Functions.get_functions(include_valves=include_valves, db=db) ############################ @@ -78,46 +77,41 @@ class LoadUrlForm(BaseModel): def github_url_to_raw_url(url: str) -> str: # Handle 'tree' (folder) URLs (add main.py at the end) - m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url) + m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url) if m1: org, repo, branch, path = m1.groups() - return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py" + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py' # Handle 'blob' (file) URLs - m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url) + m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url) if m2: org, repo, branch, path = m2.groups() - return ( - f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}" - ) + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}' # No match; return as-is return url -@router.post("/load/url", response_model=Optional[dict]) -async def load_function_from_url( - request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user) -): +@router.post('/load/url', response_model=dict | None) +async def load_function_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)): # NOTE: This is NOT a SSRF vulnerability: # This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use, # and does NOT accept untrusted user input. Access is enforced by authentication. url = str(form_data.url) if not url: - raise HTTPException(status_code=400, detail="Please enter a valid URL") + raise HTTPException(status_code=400, detail='Please enter a valid URL') url = github_url_to_raw_url(url) - url_parts = url.rstrip("/").split("/") + url_parts = url.rstrip('/').split('/') file_name = url_parts[-1] function_name = ( file_name[:-3] - if ( - file_name.endswith(".py") - and (not file_name.startswith(("main.py", "index.py", "__init__.py"))) - ) - else url_parts[-2] if len(url_parts) > 1 else "function" + if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py')))) + else url_parts[-2] + if len(url_parts) > 1 + else 'function' ) try: @@ -125,23 +119,19 @@ async def load_function_from_url( trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) ) as session: async with session.get( - url, headers={"Content-Type": "application/json"} + url, headers={'Content-Type': 'application/json'}, ssl=AIOHTTP_CLIENT_SESSION_SSL ) as resp: if resp.status != 200: - raise HTTPException( - status_code=resp.status, detail="Failed to fetch the function" - ) + raise HTTPException(status_code=resp.status, detail='Failed to fetch the function') data = await resp.text() if not data: - raise HTTPException( - status_code=400, detail="No data received from the URL" - ) + raise HTTPException(status_code=400, detail='No data received from the URL') return { - "name": function_name, - "content": data, + 'name': function_name, + 'content': data, } except Exception as e: - raise HTTPException(status_code=500, detail=f"Error importing function: {e}") + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.DEFAULT(e)) ############################ @@ -153,36 +143,32 @@ class SyncFunctionsForm(BaseModel): functions: list[FunctionWithValvesModel] = [] -@router.post("/sync", response_model=list[FunctionWithValvesModel]) +@router.post('/sync', response_model=list[FunctionWithValvesModel]) async def sync_functions( request: Request, form_data: SyncFunctionsForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: for function in form_data.functions: function.content = replace_imports(function.content) - function_module, function_type, frontmatter = load_function_module_by_id( + function_module, function_type, frontmatter = await load_function_module_by_id( function.id, content=function.content, ) - if hasattr(function_module, "Valves") and function.valves: + if hasattr(function_module, 'Valves') and function.valves: Valves = function_module.Valves try: - Valves( - **{k: v for k, v in function.valves.items() if v is not None} - ) + Valves(**{k: v for k, v in function.valves.items() if v is not None}) except Exception as e: - log.exception( - f"Error validating valves for function {function.id}: {e}" - ) + log.exception(f'Error validating valves for function {function.id}: {e}') raise e - return Functions.sync_functions(user.id, form_data.functions, db=db) + return await Functions.sync_functions(user.id, form_data.functions, db=db) except Exception as e: - log.exception(f"Failed to load a function: {e}") + log.exception(f'Failed to load a function: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -194,26 +180,26 @@ async def sync_functions( ############################ -@router.post("/create", response_model=Optional[FunctionResponse]) +@router.post('/create', response_model=FunctionResponse | None) async def create_new_function( request: Request, form_data: FunctionForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not form_data.id.isidentifier(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Only alphanumeric characters and underscores are allowed in the id", + detail='Only alphanumeric characters and underscores are allowed in the id', ) form_data.id = form_data.id.lower() - function = Functions.get_function_by_id(form_data.id, db=db) + function = await Functions.get_function_by_id(form_data.id, db=db) if function is None: try: form_data.content = replace_imports(form_data.content) - function_module, function_type, frontmatter = load_function_module_by_id( + function_module, function_type, frontmatter = await load_function_module_by_id( form_data.id, content=form_data.content, ) @@ -222,27 +208,23 @@ async def create_new_function( FUNCTIONS = request.app.state.FUNCTIONS FUNCTIONS[form_data.id] = function_module - function = Functions.insert_new_function( - user.id, function_type, form_data, db=db - ) + function = await Functions.insert_new_function(user.id, function_type, form_data, db=db) - function_cache_dir = CACHE_DIR / "functions" / form_data.id + function_cache_dir = CACHE_DIR / 'functions' / form_data.id function_cache_dir.mkdir(parents=True, exist_ok=True) - if function_type == "filter" and getattr(function_module, "toggle", None): - Functions.update_function_metadata_by_id( - form_data.id, {"toggle": True}, db=db - ) + if function_type == 'filter' and getattr(function_module, 'toggle', None): + await Functions.update_function_metadata_by_id(form_data.id, {'toggle': True}, db=db) if function: return function else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error creating function"), + detail=ERROR_MESSAGES.DEFAULT('Error creating function'), ) except Exception as e: - log.exception(f"Failed to create a new function: {e}") + log.exception(f'Failed to create a new function: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -259,11 +241,9 @@ async def create_new_function( ############################ -@router.get("/id/{id}", response_model=Optional[FunctionModel]) -async def get_function_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): - function = Functions.get_function_by_id(id, db=db) +@router.get('/id/{id}', response_model=FunctionModel | None) +async def get_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + function = await Functions.get_function_by_id(id, db=db) if function: return function @@ -279,22 +259,18 @@ async def get_function_by_id( ############################ -@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel]) -async def toggle_function_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): - function = Functions.get_function_by_id(id, db=db) +@router.post('/id/{id}/toggle', response_model=FunctionModel | None) +async def toggle_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + function = await Functions.get_function_by_id(id, db=db) if function: - function = Functions.update_function_by_id( - id, {"is_active": not function.is_active}, db=db - ) + function = await Functions.update_function_by_id(id, {'is_active': not function.is_active}, db=db) if function: return function else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), ) else: raise HTTPException( @@ -308,22 +284,18 @@ async def toggle_function_by_id( ############################ -@router.post("/id/{id}/toggle/global", response_model=Optional[FunctionModel]) -async def toggle_global_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): - function = Functions.get_function_by_id(id, db=db) +@router.post('/id/{id}/toggle/global', response_model=FunctionModel | None) +async def toggle_global_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + function = await Functions.get_function_by_id(id, db=db) if function: - function = Functions.update_function_by_id( - id, {"is_global": not function.is_global}, db=db - ) + function = await Functions.update_function_by_id(id, {'is_global': not function.is_global}, db=db) if function: return function else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), ) else: raise HTTPException( @@ -337,38 +309,36 @@ async def toggle_global_by_id( ############################ -@router.post("/id/{id}/update", response_model=Optional[FunctionModel]) +@router.post('/id/{id}/update', response_model=FunctionModel | None) async def update_function_by_id( request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: form_data.content = replace_imports(form_data.content) - function_module, function_type, frontmatter = load_function_module_by_id( - id, content=form_data.content - ) + function_module, function_type, frontmatter = await load_function_module_by_id(id, content=form_data.content) form_data.meta.manifest = frontmatter FUNCTIONS = request.app.state.FUNCTIONS FUNCTIONS[id] = function_module - updated = {**form_data.model_dump(exclude={"id"}), "type": function_type} + updated = {**form_data.model_dump(exclude={'id'}), 'type': function_type} log.debug(updated) - function = Functions.update_function_by_id(id, updated, db=db) + function = await Functions.update_function_by_id(id, updated, db=db) - if function_type == "filter" and getattr(function_module, "toggle", None): - Functions.update_function_metadata_by_id(id, {"toggle": True}, db=db) + if function_type == 'filter' and getattr(function_module, 'toggle', None): + await Functions.update_function_metadata_by_id(id, {'toggle': True}, db=db) if function: return function else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), ) except Exception as e: @@ -383,14 +353,14 @@ async def update_function_by_id( ############################ -@router.delete("/id/{id}/delete", response_model=bool) +@router.delete('/id/{id}/delete', response_model=bool) async def delete_function_by_id( request: Request, id: str, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - result = Functions.delete_function_by_id(id, db=db) + result = await Functions.delete_function_by_id(id, db=db) if result: FUNCTIONS = request.app.state.FUNCTIONS @@ -405,14 +375,14 @@ async def delete_function_by_id( ############################ -@router.get("/id/{id}/valves", response_model=Optional[dict]) +@router.get('/id/{id}/valves', response_model=dict | None) async def get_function_valves_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) + id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) ): - function = Functions.get_function_by_id(id, db=db) + function = await Functions.get_function_by_id(id, db=db) if function: try: - valves = Functions.get_function_valves_by_id(id, db=db) + valves = await Functions.get_function_valves_by_id(id, db=db) return valves except Exception as e: raise HTTPException( @@ -431,22 +401,23 @@ async def get_function_valves_by_id( ############################ -@router.get("/id/{id}/valves/spec", response_model=Optional[dict]) +@router.get('/id/{id}/valves/spec', response_model=dict | None) async def get_function_valves_spec_by_id( request: Request, id: str, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - function = Functions.get_function_by_id(id, db=db) + function = await Functions.get_function_by_id(id, db=db) if function: - function_module, function_type, frontmatter = get_function_module_from_cache( - request, id - ) + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) - if hasattr(function_module, "Valves"): + if hasattr(function_module, 'Valves'): Valves = function_module.Valves - return Valves.schema() + schema = Valves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(Valves, schema, user) + return schema return None else: raise HTTPException( @@ -460,21 +431,19 @@ async def get_function_valves_spec_by_id( ############################ -@router.post("/id/{id}/valves/update", response_model=Optional[dict]) +@router.post('/id/{id}/valves/update', response_model=dict | None) async def update_function_valves_by_id( request: Request, id: str, form_data: dict, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - function = Functions.get_function_by_id(id, db=db) + function = await Functions.get_function_by_id(id, db=db) if function: - function_module, function_type, frontmatter = get_function_module_from_cache( - request, id - ) + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) - if hasattr(function_module, "Valves"): + if hasattr(function_module, 'Valves'): Valves = function_module.Valves try: @@ -482,10 +451,10 @@ async def update_function_valves_by_id( valves = Valves(**form_data) valves_dict = valves.model_dump(exclude_unset=True) - Functions.update_function_valves_by_id(id, valves_dict, db=db) + await Functions.update_function_valves_by_id(id, valves_dict, db=db) return valves_dict except Exception as e: - log.exception(f"Error updating function values by id {id}: {e}") + log.exception(f'Error updating function values by id {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -508,16 +477,14 @@ async def update_function_valves_by_id( ############################ -@router.get("/id/{id}/valves/user", response_model=Optional[dict]) +@router.get('/id/{id}/valves/user', response_model=dict | None) async def get_function_user_valves_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - function = Functions.get_function_by_id(id, db=db) + function = await Functions.get_function_by_id(id, db=db) if function: try: - user_valves = Functions.get_user_valves_by_id_and_user_id( - id, user.id, db=db - ) + user_valves = await Functions.get_user_valves_by_id_and_user_id(id, user.id, db=db) return user_valves except Exception as e: raise HTTPException( @@ -531,22 +498,23 @@ async def get_function_user_valves_by_id( ) -@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict]) +@router.get('/id/{id}/valves/user/spec', response_model=dict | None) async def get_function_user_valves_spec_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - function = Functions.get_function_by_id(id, db=db) + function = await Functions.get_function_by_id(id, db=db) if function: - function_module, function_type, frontmatter = get_function_module_from_cache( - request, id - ) + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) - if hasattr(function_module, "UserValves"): + if hasattr(function_module, 'UserValves'): UserValves = function_module.UserValves - return UserValves.schema() + schema = UserValves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(UserValves, schema, user) + return schema return None else: raise HTTPException( @@ -555,34 +523,30 @@ async def get_function_user_valves_spec_by_id( ) -@router.post("/id/{id}/valves/user/update", response_model=Optional[dict]) +@router.post('/id/{id}/valves/user/update', response_model=dict | None) async def update_function_user_valves_by_id( request: Request, id: str, form_data: dict, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - function = Functions.get_function_by_id(id, db=db) + function = await Functions.get_function_by_id(id, db=db) if function: - function_module, function_type, frontmatter = get_function_module_from_cache( - request, id - ) + function_module, function_type, frontmatter = await get_function_module_from_cache(request, id) - if hasattr(function_module, "UserValves"): + if hasattr(function_module, 'UserValves'): UserValves = function_module.UserValves try: form_data = {k: v for k, v in form_data.items() if v is not None} user_valves = UserValves(**form_data) user_valves_dict = user_valves.model_dump(exclude_unset=True) - Functions.update_user_valves_by_id_and_user_id( - id, user.id, user_valves_dict, db=db - ) + await Functions.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db) return user_valves_dict except Exception as e: - log.exception(f"Error updating function user valves by id {id}: {e}") + log.exception(f'Error updating function user valves by id {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py index cc0cb8f5a3..3a06cb287c 100755 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -1,26 +1,27 @@ +import logging import os from pathlib import Path from typing import Optional -import logging - -from open_webui.models.users import Users, UserInfoResponse -from open_webui.models.groups import ( - Groups, - GroupForm, - GroupUpdateForm, - GroupResponse, - UserIdsForm, -) +from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Request, status - -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session - +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import ( + GroupForm, + GroupInfoResponse, + GroupResponse, + Groups, + GroupUpdateForm, + UserIdsForm, +) +from open_webui.models.knowledge import Knowledges +from open_webui.models.models import Models +from open_webui.models.tools import Tools +from open_webui.models.users import UserInfoResponse, Users from open_webui.utils.auth import get_admin_user, get_verified_user - +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -31,22 +32,21 @@ router = APIRouter() ############################ -@router.get("/", response_model=list[GroupResponse]) +@router.get('/', response_model=list[GroupResponse]) async def get_groups( share: Optional[bool] = None, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - filter = {} # Admins can share to all groups regardless of share setting - if user.role != "admin": - filter["member_id"] = user.id + if user.role != 'admin': + filter['member_id'] = user.id if share is not None: - filter["share"] = share + filter['share'] = share - groups = Groups.get_groups(filter=filter, db=db) + groups = await Groups.get_groups(filter=filter, db=db) return groups @@ -56,26 +56,26 @@ async def get_groups( ############################ -@router.post("/create", response_model=Optional[GroupResponse]) +@router.post('/create', response_model=Optional[GroupResponse]) async def create_new_group( form_data: GroupForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: - group = Groups.insert_new_group(user.id, form_data, db=db) + group = await Groups.insert_new_group(user.id, form_data, db=db) if group: return GroupResponse( **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id, db=db), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error creating group"), + detail=ERROR_MESSAGES.DEFAULT('Error creating group'), ) except Exception as e: - log.exception(f"Error creating a new group: {e}") + log.exception(f'Error creating a new group: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -87,15 +87,28 @@ async def create_new_group( ############################ -@router.get("/id/{id}", response_model=Optional[GroupResponse]) -async def get_group_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): - group = Groups.get_group_by_id(id, db=db) +@router.get('/id/{id}', response_model=Optional[GroupResponse]) +async def get_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + group = await Groups.get_group_by_id(id, db=db) if group: return GroupResponse( **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id, db=db), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get('/id/{id}/info', response_model=Optional[GroupInfoResponse]) +async def get_group_info_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + group = await Groups.get_group_by_id(id, db=db) + if group: + return GroupInfoResponse( + **group.model_dump(), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), ) else: raise HTTPException( @@ -114,16 +127,14 @@ class GroupExportResponse(GroupResponse): pass -@router.get("/id/{id}/export", response_model=Optional[GroupExportResponse]) -async def export_group_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): - group = Groups.get_group_by_id(id, db=db) +@router.get('/id/{id}/export', response_model=Optional[GroupExportResponse]) +async def export_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + group = await Groups.get_group_by_id(id, db=db) if group: return GroupExportResponse( **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id, db=db), - user_ids=Groups.get_group_user_ids_by_id(group.id, db=db), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), + user_ids=await Groups.get_group_user_ids_by_id(group.id, db=db), ) else: raise HTTPException( @@ -137,15 +148,13 @@ async def export_group_by_id( ############################ -@router.post("/id/{id}/users", response_model=list[UserInfoResponse]) -async def get_users_in_group( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): +@router.post('/id/{id}/users', response_model=list[UserInfoResponse]) +async def get_users_in_group(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): try: - users = Users.get_users_by_group_id(id, db=db) + users = await Users.get_users_by_group_id(id, db=db) return users except Exception as e: - log.exception(f"Error adding users to group {id}: {e}") + log.exception(f'Error adding users to group {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -157,27 +166,27 @@ async def get_users_in_group( ############################ -@router.post("/id/{id}/update", response_model=Optional[GroupResponse]) +@router.post('/id/{id}/update', response_model=Optional[GroupResponse]) async def update_group_by_id( id: str, form_data: GroupUpdateForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: - group = Groups.update_group_by_id(id, form_data, db=db) + group = await Groups.update_group_by_id(id, form_data, db=db) if group: return GroupResponse( **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id, db=db), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating group"), + detail=ERROR_MESSAGES.DEFAULT('Error updating group'), ) except Exception as e: - log.exception(f"Error updating group {id}: {e}") + log.exception(f'Error updating group {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -189,57 +198,57 @@ async def update_group_by_id( ############################ -@router.post("/id/{id}/users/add", response_model=Optional[GroupResponse]) +@router.post('/id/{id}/users/add', response_model=Optional[GroupResponse]) async def add_user_to_group( id: str, form_data: UserIdsForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: if form_data.user_ids: - form_data.user_ids = Users.get_valid_user_ids(form_data.user_ids, db=db) + form_data.user_ids = await Users.get_valid_user_ids(form_data.user_ids, db=db) - group = Groups.add_users_to_group(id, form_data.user_ids, db=db) + group = await Groups.add_users_to_group(id, form_data.user_ids, db=db) if group: return GroupResponse( **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id, db=db), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error adding users to group"), + detail=ERROR_MESSAGES.DEFAULT('Error adding users to group'), ) except Exception as e: - log.exception(f"Error adding users to group {id}: {e}") + log.exception(f'Error adding users to group {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), ) -@router.post("/id/{id}/users/remove", response_model=Optional[GroupResponse]) +@router.post('/id/{id}/users/remove', response_model=Optional[GroupResponse]) async def remove_users_from_group( id: str, form_data: UserIdsForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: - group = Groups.remove_users_from_group(id, form_data.user_ids, db=db) + group = await Groups.remove_users_from_group(id, form_data.user_ids, db=db) if group: return GroupResponse( **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id, db=db), + member_count=await Groups.get_group_member_count_by_id(group.id, db=db), ) else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error removing users from group"), + detail=ERROR_MESSAGES.DEFAULT('Error removing users from group'), ) except Exception as e: - log.exception(f"Error removing users from group {id}: {e}") + log.exception(f'Error removing users from group {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), @@ -251,22 +260,105 @@ async def remove_users_from_group( ############################ -@router.delete("/id/{id}/delete", response_model=bool) -async def delete_group_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): +@router.delete('/id/{id}/delete', response_model=bool) +async def delete_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): try: - result = Groups.delete_group_by_id(id, db=db) + result = await Groups.delete_group_by_id(id, db=db) if result: return result else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error deleting group"), + detail=ERROR_MESSAGES.DEFAULT('Error deleting group'), ) except Exception as e: - log.exception(f"Error deleting group {id}: {e}") + log.exception(f'Error deleting group {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), ) + + +############################ +# PreviewGroupAccess +############################ + + +@router.get('/id/{id}/preview') +async def preview_group_access( + id: str, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Show what resources a group can access (preview audit).""" + group = await Groups.get_group_by_id(id, db=db) + if not group: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + group_ids = {group.id} + + # Batch-check accessible resources using existing AccessGrants + all_models = await Models.get_all_models(db=db) + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id='', + resource_type='model', + resource_ids=[m.id for m in all_models], + permission='read', + user_group_ids=group_ids, + db=db, + ) + + all_knowledge = await Knowledges.get_knowledge_bases(db=db) + accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids( + user_id='', + resource_type='knowledge', + resource_ids=[k.id for k in all_knowledge], + permission='read', + user_group_ids=group_ids, + db=db, + ) + + all_tools = await Tools.get_tools(defer_content=True, db=db) + accessible_tool_ids = await AccessGrants.get_accessible_resource_ids( + user_id='', + resource_type='tool', + resource_ids=[t.id for t in all_tools], + permission='read', + user_group_ids=group_ids, + db=db, + ) + + active_models = [m for m in all_models if m.is_active] + + return { + 'group': {'id': group.id, 'name': group.name}, + 'models': { + 'items': [ + {'id': m.id, 'name': m.name} + for m in active_models + if m.id in accessible_model_ids + ], + 'total': len(active_models), + }, + 'knowledge': { + 'items': [ + {'id': k.id, 'name': k.name} + for k in all_knowledge + if k.id in accessible_knowledge_ids + ], + 'total': len(all_knowledge), + }, + 'tools': { + 'items': [ + {'id': t.id, 'name': t.name} + for t in all_tools + if t.id in accessible_tool_ids + ], + 'total': len(all_tools), + }, + 'permissions': group.permissions or {}, + } + diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 0fc6930b81..ba824f33c1 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -1,105 +1,116 @@ +from __future__ import annotations + import asyncio import base64 -import uuid import io import json import logging import mimetypes import re +import uuid from pathlib import Path from typing import Optional +from urllib.parse import quote, urlparse -from urllib.parse import quote -import requests +import aiohttp from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile from fastapi.responses import FileResponse - -from open_webui.config import CACHE_DIR +from open_webui.config import ( + CACHE_DIR, + IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, + IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, +) from open_webui.constants import ERROR_MESSAGES -from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS - +from open_webui.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS, AIOHTTP_CLIENT_SESSION_SSL, ENABLE_FORWARD_USER_INFO_HEADERS +from open_webui.internal.db import get_async_session from open_webui.models.chats import Chats -from open_webui.routers.files import upload_file_handler, get_file_content_by_id -from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.retrieval.web.utils import validate_url +from open_webui.routers.files import get_file_content_by_id, upload_file_handler from open_webui.utils.access_control import has_permission +from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.headers import include_user_info_headers -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session from open_webui.utils.images.comfyui import ( ComfyUICreateImageForm, ComfyUIEditImageForm, ComfyUIWorkflow, - comfyui_upload_image, comfyui_create_image, comfyui_edit_image, + comfyui_upload_image, ) +from open_webui.utils.session_pool import get_session from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -IMAGE_CACHE_DIR = CACHE_DIR / "image" / "generations" +# An image can lie as easily as it can illuminate. Let what +# is generated here be honest about what it shows. +IMAGE_CACHE_DIR = CACHE_DIR / 'image' / 'generations' IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) router = APIRouter() -def set_image_model(request: Request, model: str): - log.info(f"Setting image model to {model}") +async def set_image_model(request: Request, model: str): + log.info(f'Setting image model to {model}') request.app.state.config.IMAGE_GENERATION_MODEL = model - if request.app.state.config.IMAGE_GENERATION_ENGINE in ["", "automatic1111"]: + if request.app.state.config.IMAGE_GENERATION_ENGINE in ['', 'automatic1111']: api_auth = get_automatic1111_api_auth(request) try: - r = requests.get( - url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", - headers={"authorization": api_auth}, - ) - options = r.json() - if model != options["sd_model_checkpoint"]: - options["sd_model_checkpoint"] = model - r = requests.post( - url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + headers={'authorization': api_auth}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + options = await r.json() + if model != options['sd_model_checkpoint']: + options['sd_model_checkpoint'] = model + async with session.post( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', json=options, - headers={"authorization": api_auth}, - ) + headers={'authorization': api_auth}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() except Exception as e: - log.debug(f"{e}") + log.debug(f'{e}') return request.app.state.config.IMAGE_GENERATION_MODEL -def get_image_model(request): - if request.app.state.config.IMAGE_GENERATION_ENGINE == "openai": +async def get_image_model(request): + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai': return ( request.app.state.config.IMAGE_GENERATION_MODEL if request.app.state.config.IMAGE_GENERATION_MODEL - else "dall-e-2" + else 'dall-e-2' ) - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini': return ( request.app.state.config.IMAGE_GENERATION_MODEL if request.app.state.config.IMAGE_GENERATION_MODEL - else "imagen-3.0-generate-002" + else 'imagen-3.0-generate-002' ) - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': return ( - request.app.state.config.IMAGE_GENERATION_MODEL - if request.app.state.config.IMAGE_GENERATION_MODEL - else "" + request.app.state.config.IMAGE_GENERATION_MODEL if request.app.state.config.IMAGE_GENERATION_MODEL else '' ) elif ( - request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111" - or request.app.state.config.IMAGE_GENERATION_ENGINE == "" + request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111' + or request.app.state.config.IMAGE_GENERATION_ENGINE == '' ): try: - r = requests.get( - url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", - headers={"authorization": get_automatic1111_api_auth(request)}, - ) - options = r.json() - return options["sd_model_checkpoint"] + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + options = await r.json() + return options['sd_model_checkpoint'] except Exception as e: - request.app.state.config.ENABLE_IMAGE_GENERATION = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) @@ -109,17 +120,17 @@ class ImagesConfig(BaseModel): IMAGE_GENERATION_ENGINE: str IMAGE_GENERATION_MODEL: str - IMAGE_SIZE: Optional[str] - IMAGE_STEPS: Optional[int] + IMAGE_SIZE: str | None + IMAGE_STEPS: int | None IMAGES_OPENAI_API_BASE_URL: str IMAGES_OPENAI_API_KEY: str IMAGES_OPENAI_API_VERSION: str - IMAGES_OPENAI_API_PARAMS: Optional[dict | str] + IMAGES_OPENAI_API_PARAMS: dict | str | None AUTOMATIC1111_BASE_URL: str - AUTOMATIC1111_API_AUTH: Optional[dict | str] - AUTOMATIC1111_PARAMS: Optional[dict | str] + AUTOMATIC1111_API_AUTH: dict | str | None + AUTOMATIC1111_PARAMS: dict | str | None COMFYUI_BASE_URL: str COMFYUI_API_KEY: str @@ -133,7 +144,7 @@ class ImagesConfig(BaseModel): ENABLE_IMAGE_EDIT: bool IMAGE_EDIT_ENGINE: str IMAGE_EDIT_MODEL: str - IMAGE_EDIT_SIZE: Optional[str] + IMAGE_EDIT_SIZE: str | None IMAGES_EDIT_OPENAI_API_BASE_URL: str IMAGES_EDIT_OPENAI_API_KEY: str @@ -146,80 +157,71 @@ class ImagesConfig(BaseModel): IMAGES_EDIT_COMFYUI_WORKFLOW_NODES: list[dict] -@router.get("/config", response_model=ImagesConfig) +@router.get('/config', response_model=ImagesConfig) async def get_config(request: Request, user=Depends(get_admin_user)): return { - "ENABLE_IMAGE_GENERATION": request.app.state.config.ENABLE_IMAGE_GENERATION, - "ENABLE_IMAGE_PROMPT_GENERATION": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION, - "IMAGE_GENERATION_ENGINE": request.app.state.config.IMAGE_GENERATION_ENGINE, - "IMAGE_GENERATION_MODEL": request.app.state.config.IMAGE_GENERATION_MODEL, - "IMAGE_SIZE": request.app.state.config.IMAGE_SIZE, - "IMAGE_STEPS": request.app.state.config.IMAGE_STEPS, - "IMAGES_OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL, - "IMAGES_OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY, - "IMAGES_OPENAI_API_VERSION": request.app.state.config.IMAGES_OPENAI_API_VERSION, - "IMAGES_OPENAI_API_PARAMS": request.app.state.config.IMAGES_OPENAI_API_PARAMS, - "AUTOMATIC1111_BASE_URL": request.app.state.config.AUTOMATIC1111_BASE_URL, - "AUTOMATIC1111_API_AUTH": request.app.state.config.AUTOMATIC1111_API_AUTH, - "AUTOMATIC1111_PARAMS": request.app.state.config.AUTOMATIC1111_PARAMS, - "COMFYUI_BASE_URL": request.app.state.config.COMFYUI_BASE_URL, - "COMFYUI_API_KEY": request.app.state.config.COMFYUI_API_KEY, - "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW, - "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES, - "IMAGES_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL, - "IMAGES_GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY, - "IMAGES_GEMINI_ENDPOINT_METHOD": request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, - "ENABLE_IMAGE_EDIT": request.app.state.config.ENABLE_IMAGE_EDIT, - "IMAGE_EDIT_ENGINE": request.app.state.config.IMAGE_EDIT_ENGINE, - "IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL, - "IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE, - "IMAGES_EDIT_OPENAI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL, - "IMAGES_EDIT_OPENAI_API_KEY": request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY, - "IMAGES_EDIT_OPENAI_API_VERSION": request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION, - "IMAGES_EDIT_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL, - "IMAGES_EDIT_GEMINI_API_KEY": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, - "IMAGES_EDIT_COMFYUI_BASE_URL": request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, - "IMAGES_EDIT_COMFYUI_API_KEY": request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, - "IMAGES_EDIT_COMFYUI_WORKFLOW": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, - "IMAGES_EDIT_COMFYUI_WORKFLOW_NODES": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + 'ENABLE_IMAGE_GENERATION': request.app.state.config.ENABLE_IMAGE_GENERATION, + 'ENABLE_IMAGE_PROMPT_GENERATION': request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION, + 'IMAGE_GENERATION_ENGINE': request.app.state.config.IMAGE_GENERATION_ENGINE, + 'IMAGE_GENERATION_MODEL': request.app.state.config.IMAGE_GENERATION_MODEL, + 'IMAGE_SIZE': request.app.state.config.IMAGE_SIZE, + 'IMAGE_STEPS': request.app.state.config.IMAGE_STEPS, + 'IMAGES_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_OPENAI_API_BASE_URL, + 'IMAGES_OPENAI_API_KEY': request.app.state.config.IMAGES_OPENAI_API_KEY, + 'IMAGES_OPENAI_API_VERSION': request.app.state.config.IMAGES_OPENAI_API_VERSION, + 'IMAGES_OPENAI_API_PARAMS': request.app.state.config.IMAGES_OPENAI_API_PARAMS, + 'AUTOMATIC1111_BASE_URL': request.app.state.config.AUTOMATIC1111_BASE_URL, + 'AUTOMATIC1111_API_AUTH': request.app.state.config.AUTOMATIC1111_API_AUTH, + 'AUTOMATIC1111_PARAMS': request.app.state.config.AUTOMATIC1111_PARAMS, + 'COMFYUI_BASE_URL': request.app.state.config.COMFYUI_BASE_URL, + 'COMFYUI_API_KEY': request.app.state.config.COMFYUI_API_KEY, + 'COMFYUI_WORKFLOW': request.app.state.config.COMFYUI_WORKFLOW, + 'COMFYUI_WORKFLOW_NODES': request.app.state.config.COMFYUI_WORKFLOW_NODES, + 'IMAGES_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_GEMINI_API_BASE_URL, + 'IMAGES_GEMINI_API_KEY': request.app.state.config.IMAGES_GEMINI_API_KEY, + 'IMAGES_GEMINI_ENDPOINT_METHOD': request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, + 'ENABLE_IMAGE_EDIT': request.app.state.config.ENABLE_IMAGE_EDIT, + 'IMAGE_EDIT_ENGINE': request.app.state.config.IMAGE_EDIT_ENGINE, + 'IMAGE_EDIT_MODEL': request.app.state.config.IMAGE_EDIT_MODEL, + 'IMAGE_EDIT_SIZE': request.app.state.config.IMAGE_EDIT_SIZE, + 'IMAGES_EDIT_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL, + 'IMAGES_EDIT_OPENAI_API_KEY': request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY, + 'IMAGES_EDIT_OPENAI_API_VERSION': request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION, + 'IMAGES_EDIT_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL, + 'IMAGES_EDIT_GEMINI_API_KEY': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, + 'IMAGES_EDIT_COMFYUI_BASE_URL': request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + 'IMAGES_EDIT_COMFYUI_API_KEY': request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, + 'IMAGES_EDIT_COMFYUI_WORKFLOW': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, } -@router.post("/config/update") -async def update_config( - request: Request, form_data: ImagesConfig, user=Depends(get_admin_user) -): +@router.post('/config/update') +async def update_config(request: Request, form_data: ImagesConfig, user=Depends(get_admin_user)): request.app.state.config.ENABLE_IMAGE_GENERATION = form_data.ENABLE_IMAGE_GENERATION # Create Image - request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ( - form_data.ENABLE_IMAGE_PROMPT_GENERATION - ) + request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = form_data.ENABLE_IMAGE_PROMPT_GENERATION request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.IMAGE_GENERATION_ENGINE - set_image_model(request, form_data.IMAGE_GENERATION_MODEL) - if ( - form_data.IMAGE_SIZE == "auto" - and not form_data.IMAGE_GENERATION_MODEL.startswith("gpt-image") + await set_image_model(request, form_data.IMAGE_GENERATION_MODEL) + if form_data.IMAGE_SIZE == 'auto' and not re.match( + IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL ): raise HTTPException( status_code=400, detail=ERROR_MESSAGES.INCORRECT_FORMAT( - " (auto is only allowed with gpt-image models)." + f' (auto is only allowed with models matching {IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN}).' ), ) - pattern = r"^\d+x\d+$" - if ( - form_data.IMAGE_SIZE == "auto" - or form_data.IMAGE_SIZE == "" - or re.match(pattern, form_data.IMAGE_SIZE) - ): + pattern = r'^\d+x\d+$' + if form_data.IMAGE_SIZE == 'auto' or form_data.IMAGE_SIZE == '' or re.match(pattern, form_data.IMAGE_SIZE): request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE else: raise HTTPException( status_code=400, - detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."), + detail=ERROR_MESSAGES.INCORRECT_FORMAT(' (e.g., 512x512).'), ) if form_data.IMAGE_STEPS >= 0: @@ -227,36 +229,26 @@ async def update_config( else: raise HTTPException( status_code=400, - detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."), + detail=ERROR_MESSAGES.INCORRECT_FORMAT(' (e.g., 50).'), ) - request.app.state.config.IMAGES_OPENAI_API_BASE_URL = ( - form_data.IMAGES_OPENAI_API_BASE_URL - ) + request.app.state.config.IMAGES_OPENAI_API_BASE_URL = form_data.IMAGES_OPENAI_API_BASE_URL request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.IMAGES_OPENAI_API_KEY - request.app.state.config.IMAGES_OPENAI_API_VERSION = ( - form_data.IMAGES_OPENAI_API_VERSION - ) - request.app.state.config.IMAGES_OPENAI_API_PARAMS = ( - form_data.IMAGES_OPENAI_API_PARAMS - ) + request.app.state.config.IMAGES_OPENAI_API_VERSION = form_data.IMAGES_OPENAI_API_VERSION + request.app.state.config.IMAGES_OPENAI_API_PARAMS = form_data.IMAGES_OPENAI_API_PARAMS request.app.state.config.AUTOMATIC1111_BASE_URL = form_data.AUTOMATIC1111_BASE_URL request.app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH request.app.state.config.AUTOMATIC1111_PARAMS = form_data.AUTOMATIC1111_PARAMS - request.app.state.config.COMFYUI_BASE_URL = form_data.COMFYUI_BASE_URL.strip("/") + request.app.state.config.COMFYUI_BASE_URL = form_data.COMFYUI_BASE_URL.strip('/') request.app.state.config.COMFYUI_API_KEY = form_data.COMFYUI_API_KEY request.app.state.config.COMFYUI_WORKFLOW = form_data.COMFYUI_WORKFLOW request.app.state.config.COMFYUI_WORKFLOW_NODES = form_data.COMFYUI_WORKFLOW_NODES - request.app.state.config.IMAGES_GEMINI_API_BASE_URL = ( - form_data.IMAGES_GEMINI_API_BASE_URL - ) + request.app.state.config.IMAGES_GEMINI_API_BASE_URL = form_data.IMAGES_GEMINI_API_BASE_URL request.app.state.config.IMAGES_GEMINI_API_KEY = form_data.IMAGES_GEMINI_API_KEY - request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = ( - form_data.IMAGES_GEMINI_ENDPOINT_METHOD - ) + request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD = form_data.IMAGES_GEMINI_ENDPOINT_METHOD # Edit Image request.app.state.config.ENABLE_IMAGE_EDIT = form_data.ENABLE_IMAGE_EDIT @@ -264,252 +256,263 @@ async def update_config( request.app.state.config.IMAGE_EDIT_MODEL = form_data.IMAGE_EDIT_MODEL request.app.state.config.IMAGE_EDIT_SIZE = form_data.IMAGE_EDIT_SIZE - request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = ( - form_data.IMAGES_EDIT_OPENAI_API_BASE_URL - ) - request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY = ( - form_data.IMAGES_EDIT_OPENAI_API_KEY - ) - request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = ( - form_data.IMAGES_EDIT_OPENAI_API_VERSION - ) + request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL = form_data.IMAGES_EDIT_OPENAI_API_BASE_URL + request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY = form_data.IMAGES_EDIT_OPENAI_API_KEY + request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION = form_data.IMAGES_EDIT_OPENAI_API_VERSION - request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = ( - form_data.IMAGES_EDIT_GEMINI_API_BASE_URL - ) - request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY = ( - form_data.IMAGES_EDIT_GEMINI_API_KEY - ) + request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL = form_data.IMAGES_EDIT_GEMINI_API_BASE_URL + request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY = form_data.IMAGES_EDIT_GEMINI_API_KEY - request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = ( - form_data.IMAGES_EDIT_COMFYUI_BASE_URL.strip("/") - ) - request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = ( - form_data.IMAGES_EDIT_COMFYUI_API_KEY - ) - request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = ( - form_data.IMAGES_EDIT_COMFYUI_WORKFLOW - ) - request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = ( - form_data.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES - ) + request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL = form_data.IMAGES_EDIT_COMFYUI_BASE_URL.strip('/') + request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY = form_data.IMAGES_EDIT_COMFYUI_API_KEY + request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW = form_data.IMAGES_EDIT_COMFYUI_WORKFLOW + request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = form_data.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES return { - "ENABLE_IMAGE_GENERATION": request.app.state.config.ENABLE_IMAGE_GENERATION, - "ENABLE_IMAGE_PROMPT_GENERATION": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION, - "IMAGE_GENERATION_ENGINE": request.app.state.config.IMAGE_GENERATION_ENGINE, - "IMAGE_GENERATION_MODEL": request.app.state.config.IMAGE_GENERATION_MODEL, - "IMAGE_SIZE": request.app.state.config.IMAGE_SIZE, - "IMAGE_STEPS": request.app.state.config.IMAGE_STEPS, - "IMAGES_OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL, - "IMAGES_OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY, - "IMAGES_OPENAI_API_VERSION": request.app.state.config.IMAGES_OPENAI_API_VERSION, - "IMAGES_OPENAI_API_PARAMS": request.app.state.config.IMAGES_OPENAI_API_PARAMS, - "AUTOMATIC1111_BASE_URL": request.app.state.config.AUTOMATIC1111_BASE_URL, - "AUTOMATIC1111_API_AUTH": request.app.state.config.AUTOMATIC1111_API_AUTH, - "AUTOMATIC1111_PARAMS": request.app.state.config.AUTOMATIC1111_PARAMS, - "COMFYUI_BASE_URL": request.app.state.config.COMFYUI_BASE_URL, - "COMFYUI_API_KEY": request.app.state.config.COMFYUI_API_KEY, - "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW, - "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES, - "IMAGES_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL, - "IMAGES_GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY, - "IMAGES_GEMINI_ENDPOINT_METHOD": request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, - "ENABLE_IMAGE_EDIT": request.app.state.config.ENABLE_IMAGE_EDIT, - "IMAGE_EDIT_ENGINE": request.app.state.config.IMAGE_EDIT_ENGINE, - "IMAGE_EDIT_MODEL": request.app.state.config.IMAGE_EDIT_MODEL, - "IMAGE_EDIT_SIZE": request.app.state.config.IMAGE_EDIT_SIZE, - "IMAGES_EDIT_OPENAI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL, - "IMAGES_EDIT_OPENAI_API_KEY": request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY, - "IMAGES_EDIT_OPENAI_API_VERSION": request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION, - "IMAGES_EDIT_GEMINI_API_BASE_URL": request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL, - "IMAGES_EDIT_GEMINI_API_KEY": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, - "IMAGES_EDIT_COMFYUI_BASE_URL": request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, - "IMAGES_EDIT_COMFYUI_API_KEY": request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, - "IMAGES_EDIT_COMFYUI_WORKFLOW": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, - "IMAGES_EDIT_COMFYUI_WORKFLOW_NODES": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + 'ENABLE_IMAGE_GENERATION': request.app.state.config.ENABLE_IMAGE_GENERATION, + 'ENABLE_IMAGE_PROMPT_GENERATION': request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION, + 'IMAGE_GENERATION_ENGINE': request.app.state.config.IMAGE_GENERATION_ENGINE, + 'IMAGE_GENERATION_MODEL': request.app.state.config.IMAGE_GENERATION_MODEL, + 'IMAGE_SIZE': request.app.state.config.IMAGE_SIZE, + 'IMAGE_STEPS': request.app.state.config.IMAGE_STEPS, + 'IMAGES_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_OPENAI_API_BASE_URL, + 'IMAGES_OPENAI_API_KEY': request.app.state.config.IMAGES_OPENAI_API_KEY, + 'IMAGES_OPENAI_API_VERSION': request.app.state.config.IMAGES_OPENAI_API_VERSION, + 'IMAGES_OPENAI_API_PARAMS': request.app.state.config.IMAGES_OPENAI_API_PARAMS, + 'AUTOMATIC1111_BASE_URL': request.app.state.config.AUTOMATIC1111_BASE_URL, + 'AUTOMATIC1111_API_AUTH': request.app.state.config.AUTOMATIC1111_API_AUTH, + 'AUTOMATIC1111_PARAMS': request.app.state.config.AUTOMATIC1111_PARAMS, + 'COMFYUI_BASE_URL': request.app.state.config.COMFYUI_BASE_URL, + 'COMFYUI_API_KEY': request.app.state.config.COMFYUI_API_KEY, + 'COMFYUI_WORKFLOW': request.app.state.config.COMFYUI_WORKFLOW, + 'COMFYUI_WORKFLOW_NODES': request.app.state.config.COMFYUI_WORKFLOW_NODES, + 'IMAGES_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_GEMINI_API_BASE_URL, + 'IMAGES_GEMINI_API_KEY': request.app.state.config.IMAGES_GEMINI_API_KEY, + 'IMAGES_GEMINI_ENDPOINT_METHOD': request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD, + 'ENABLE_IMAGE_EDIT': request.app.state.config.ENABLE_IMAGE_EDIT, + 'IMAGE_EDIT_ENGINE': request.app.state.config.IMAGE_EDIT_ENGINE, + 'IMAGE_EDIT_MODEL': request.app.state.config.IMAGE_EDIT_MODEL, + 'IMAGE_EDIT_SIZE': request.app.state.config.IMAGE_EDIT_SIZE, + 'IMAGES_EDIT_OPENAI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL, + 'IMAGES_EDIT_OPENAI_API_KEY': request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY, + 'IMAGES_EDIT_OPENAI_API_VERSION': request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION, + 'IMAGES_EDIT_GEMINI_API_BASE_URL': request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL, + 'IMAGES_EDIT_GEMINI_API_KEY': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, + 'IMAGES_EDIT_COMFYUI_BASE_URL': request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + 'IMAGES_EDIT_COMFYUI_API_KEY': request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, + 'IMAGES_EDIT_COMFYUI_WORKFLOW': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, } def get_automatic1111_api_auth(request: Request): if request.app.state.config.AUTOMATIC1111_API_AUTH is None: - return "" + return '' else: - auth1111_byte_string = request.app.state.config.AUTOMATIC1111_API_AUTH.encode( - "utf-8" - ) + auth1111_byte_string = request.app.state.config.AUTOMATIC1111_API_AUTH.encode('utf-8') auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string) - auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8") - return f"Basic {auth1111_base64_encoded_string}" + auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode('utf-8') + return f'Basic {auth1111_base64_encoded_string}' -@router.get("/config/url/verify") +@router.get('/config/url/verify') async def verify_url(request: Request, user=Depends(get_admin_user)): - if request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111": + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111': try: - r = requests.get( - url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", - headers={"authorization": get_automatic1111_api_auth(request)}, - ) - r.raise_for_status() - return True + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options', + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return True except Exception: - request.app.state.config.ENABLE_IMAGE_GENERATION = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': headers = None if request.app.state.config.COMFYUI_API_KEY: - headers = { - "Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}" - } + headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'} try: - r = requests.get( - url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info", + session = await get_session() + async with session.get( + url=f'{request.app.state.config.COMFYUI_BASE_URL}/object_info', headers=headers, - ) - r.raise_for_status() - return True + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + return True except Exception: - request.app.state.config.ENABLE_IMAGE_GENERATION = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL) else: return True -@router.get("/models") -def get_models(request: Request, user=Depends(get_verified_user)): +@router.get('/models') +async def get_models(request: Request, user=Depends(get_verified_user)): try: - if request.app.state.config.IMAGE_GENERATION_ENGINE == "openai": + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai': return [ - {"id": "dall-e-2", "name": "DALL·E 2"}, - {"id": "dall-e-3", "name": "DALL·E 3"}, - {"id": "gpt-image-1", "name": "GPT-IMAGE 1"}, - {"id": "gpt-image-1.5", "name": "GPT-IMAGE 1.5"}, + {'id': 'dall-e-2', 'name': 'DALL·E 2'}, + {'id': 'dall-e-3', 'name': 'DALL·E 3'}, + {'id': 'gpt-image-1', 'name': 'GPT-IMAGE 1'}, + {'id': 'gpt-image-1.5', 'name': 'GPT-IMAGE 1.5'}, ] - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini': return [ - {"id": "imagen-3.0-generate-002", "name": "imagen-3.0 generate-002"}, + {'id': 'imagen-3.0-generate-002', 'name': 'imagen-3.0 generate-002'}, ] - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': # TODO - get models from comfyui - headers = { - "Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}" - } - r = requests.get( - url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info", + headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'} + session = await get_session() + async with session.get( + url=f'{request.app.state.config.COMFYUI_BASE_URL}/object_info', headers=headers, - ) - info = r.json() + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + info = await r.json() workflow = json.loads(request.app.state.config.COMFYUI_WORKFLOW) model_node_id = None for node in request.app.state.config.COMFYUI_WORKFLOW_NODES: - if node["type"] == "model": - if node["node_ids"]: - model_node_id = node["node_ids"][0] + if node['type'] == 'model': + if node['node_ids']: + model_node_id = node['node_ids'][0] break if model_node_id: model_list_key = None - log.info(workflow[model_node_id]["class_type"]) - for key in info[workflow[model_node_id]["class_type"]]["input"][ - "required" - ]: - if "_name" in key: + log.info(workflow[model_node_id]['class_type']) + for key in info[workflow[model_node_id]['class_type']]['input']['required']: + if '_name' in key: model_list_key = key break if model_list_key: return list( map( - lambda model: {"id": model, "name": model}, - info[workflow[model_node_id]["class_type"]]["input"][ - "required" - ][model_list_key][0], + lambda model: {'id': model, 'name': model}, + info[workflow[model_node_id]['class_type']]['input']['required'][model_list_key][0], ) ) else: return list( map( - lambda model: {"id": model, "name": model}, - info["CheckpointLoaderSimple"]["input"]["required"][ - "ckpt_name" - ][0], + lambda model: {'id': model, 'name': model}, + info['CheckpointLoaderSimple']['input']['required']['ckpt_name'][0], ) ) elif ( - request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111" - or request.app.state.config.IMAGE_GENERATION_ENGINE == "" + request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111' + or request.app.state.config.IMAGE_GENERATION_ENGINE == '' ): - r = requests.get( - url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models", - headers={"authorization": get_automatic1111_api_auth(request)}, - ) - models = r.json() + session = await get_session() + async with session.get( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models', + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + models = await r.json() return list( map( - lambda model: {"id": model["title"], "name": model["model_name"]}, + lambda model: {'id': model['title'], 'name': model['model_name']}, models, ) ) except Exception as e: - request.app.state.config.ENABLE_IMAGE_GENERATION = False raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) class CreateImageForm(BaseModel): - model: Optional[str] = None + model: str | None = None prompt: str - size: Optional[str] = None + size: str | None = None n: int = 1 - steps: Optional[int] = None - negative_prompt: Optional[str] = None + steps: int | None = None + negative_prompt: str | None = None GenerateImageForm = CreateImageForm # Alias for backward compatibility -def get_image_data(data: str, headers=None): - try: - if data.startswith("http://") or data.startswith("https://"): - if headers: - r = requests.get(data, headers=headers) - else: - r = requests.get(data) +def _is_same_origin(url: str, base_url: str) -> bool: + """Compare scheme + hostname + port of two URLs. - r.raise_for_status() - if r.headers["content-type"].split("/")[0] == "image": - mime_type = r.headers["content-type"] - return r.content, mime_type + Pure string-prefix matching (``startswith``) is vulnerable to + userinfo injection (``http://host:port@evil.com/``) and suffix + confusion (``http://host:portevil.com/``). Parsing both URLs + and comparing the three origin components eliminates those + attack vectors. + """ + def _default_port(scheme: str) -> int: + return 443 if scheme == 'https' else 80 + + parsed = urlparse(url) + trusted = urlparse(base_url) + return ( + parsed.scheme == trusted.scheme + and parsed.hostname == trusted.hostname + and (parsed.port or _default_port(parsed.scheme)) + == (trusted.port or _default_port(trusted.scheme)) + ) + + +async def get_image_data(data: str, headers=None, trusted_base_url: str | None = None): + try: + if data.startswith('http://') or data.startswith('https://'): + # Defense-in-depth: gate before fetch (mirrors load_url_image). + # For URLs originating from an admin-configured backend (e.g. + # ComfyUI on a private network), skip SSRF validation only when + # the URL shares the exact same origin (scheme + host + port) + # as the admin-configured base. This avoids both the global + # ENABLE_RAG_LOCAL_WEB_FETCH hammer and a blanket trust flag + # that would follow arbitrary redirects. + if trusted_base_url and _is_same_origin(data, trusted_base_url): + log.debug(f'Skipping URL validation for trusted backend: {data}') else: - log.error("Url does not point to an image.") - return None + validate_url(data) + session = await get_session() + async with session.get( + data, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + content_type = r.headers.get('content-type', '') + if content_type.split('/')[0] == 'image': + return await r.read(), content_type + else: + log.error('Url does not point to an image.') + return None, None else: - if "," in data: - header, encoded = data.split(",", 1) - mime_type = header.split(";")[0].lstrip("data:") + if ',' in data: + header, encoded = data.split(',', 1) + mime_type = header.split(';')[0].lstrip('data:') img_data = base64.b64decode(encoded) else: - mime_type = "image/png" + mime_type = 'image/png' img_data = base64.b64decode(data) return img_data, mime_type except Exception as e: - log.exception(f"Error loading image data: {e}") + log.exception(f'Error loading image data: {e}') return None, None -def upload_image(request, image_data, content_type, metadata, user, db=None): +async def upload_image(request, image_data, content_type, metadata, user, db=None): + if image_data is None or content_type is None: + raise ValueError('Failed to retrieve image data from the generation backend') image_format = mimetypes.guess_extension(content_type) file = UploadFile( file=io.BytesIO(image_data), - filename=f"generated-image{image_format}", # will be converted to a unique ID on upload_file + filename=f'generated-image{image_format}', # will be converted to a unique ID on upload_file headers={ - "content-type": content_type, + 'content-type': content_type, }, ) - file_item = upload_file_handler( + file_item = await upload_file_handler( request, file=file, metadata=metadata, @@ -519,11 +522,11 @@ def upload_image(request, image_data, content_type, metadata, user, db=None): if file_item and file_item.id: # If chat_id and message_id are provided in metadata, link the file to the chat message - chat_id = metadata.get("chat_id") - message_id = metadata.get("message_id") + chat_id = metadata.get('chat_id') + message_id = metadata.get('message_id') if chat_id and message_id: - Chats.insert_chat_files( + await Chats.insert_chat_files( chat_id=chat_id, message_id=message_id, file_ids=[file_item.id], @@ -531,22 +534,20 @@ def upload_image(request, image_data, content_type, metadata, user, db=None): db=db, ) - url = request.app.url_path_for("get_file_content_by_id", id=file_item.id) + url = request.app.url_path_for('get_file_content_by_id', id=file_item.id) return file_item, url -@router.post("/generations") -async def generate_images( - request: Request, form_data: CreateImageForm, user=Depends(get_verified_user) -): +@router.post('/generations') +async def generate_images(request: Request, form_data: CreateImageForm, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_IMAGE_GENERATION: raise HTTPException( status_code=403, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - if user.role != "admin" and not has_permission( - user.id, "features.image_generation", request.app.state.config.USER_PERMISSIONS + if user.role != 'admin' and not await has_permission( + user.id, 'features.image_generation', request.app.state.config.USER_PERMISSIONS ): raise HTTPException( status_code=403, @@ -559,60 +560,56 @@ async def generate_images( async def image_generations( request: Request, form_data: CreateImageForm, - metadata: Optional[dict] = None, + metadata: dict | None = None, user=None, ): # if IMAGE_SIZE = 'auto', default WidthxHeight to the 512x512 default # This is only relevant when the user has set IMAGE_SIZE to 'auto' with an # image model other than gpt-image-1, which is warned about on settings save - size = "512x512" - if ( - request.app.state.config.IMAGE_SIZE - and "x" in request.app.state.config.IMAGE_SIZE - ): + size = '512x512' + if request.app.state.config.IMAGE_SIZE and 'x' in request.app.state.config.IMAGE_SIZE: size = request.app.state.config.IMAGE_SIZE - if form_data.size and "x" in form_data.size: + if form_data.size and 'x' in form_data.size: size = form_data.size - width, height = tuple(map(int, size.split("x"))) + width, height = tuple(map(int, size.split('x'))) metadata = metadata or {} - model = get_image_model(request) + model = await get_image_model(request) - r = None try: - if request.app.state.config.IMAGE_GENERATION_ENGINE == "openai": - + if request.app.state.config.IMAGE_GENERATION_ENGINE == 'openai': headers = { - "Authorization": f"Bearer {request.app.state.config.IMAGES_OPENAI_API_KEY}", - "Content-Type": "application/json", + 'Authorization': f'Bearer {request.app.state.config.IMAGES_OPENAI_API_KEY}', + 'Content-Type': 'application/json', } if ENABLE_FORWARD_USER_INFO_HEADERS: headers = include_user_info_headers(headers, user) - url = f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations" + url = f'{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations' if request.app.state.config.IMAGES_OPENAI_API_VERSION: - url = f"{url}?api-version={request.app.state.config.IMAGES_OPENAI_API_VERSION}" + url = f'{url}?api-version={request.app.state.config.IMAGES_OPENAI_API_VERSION}' data = { - "model": model, - "prompt": form_data.prompt, - "n": form_data.n, - "size": ( - form_data.size - if form_data.size - else request.app.state.config.IMAGE_SIZE + 'model': model, + 'prompt': form_data.prompt, + 'n': form_data.n, + **( + {'size': form_data.size or request.app.state.config.IMAGE_SIZE} + if (form_data.size or request.app.state.config.IMAGE_SIZE) + else {} ), **( {} - if request.app.state.config.IMAGE_GENERATION_MODEL.startswith( - "gpt-image" + if re.match( + IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, + request.app.state.config.IMAGE_GENERATION_MODEL, ) - else {"response_format": "b64_json"} + else {'response_format': 'b64_json'} ), **( {} @@ -621,126 +618,109 @@ async def image_generations( ), } - # Use asyncio.to_thread for the requests.post call - r = await asyncio.to_thread( - requests.post, + session = await get_session() + async with session.post( url=url, json=data, headers=headers, - ) - - r.raise_for_status() - res = r.json() + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json(content_type=None) images = [] - for image in res["data"]: - if image_url := image.get("url", None): - image_data, content_type = get_image_data(image_url, headers) + for image in res['data']: + if image_url := image.get('url', None): + image_data, content_type = await get_image_data( + image_url, + {k: v for k, v in headers.items() if k != 'Content-Type'}, + ) else: - image_data, content_type = get_image_data(image["b64_json"]) + image_data, content_type = await get_image_data(image['b64_json']) - _, url = upload_image( - request, image_data, content_type, {**data, **metadata}, user - ) - images.append({"url": url}) + _, url = await upload_image(request, image_data, content_type, {**data, **metadata}, user) + images.append({'url': url}) return images - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'gemini': headers = { - "Content-Type": "application/json", - "x-goog-api-key": request.app.state.config.IMAGES_GEMINI_API_KEY, + 'Content-Type': 'application/json', + 'x-goog-api-key': request.app.state.config.IMAGES_GEMINI_API_KEY, } data = {} if ( - request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == "" - or request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == "predict" + request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == '' + or request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == 'predict' ): - model = f"{model}:predict" + model = f'{model}:predict' data = { - "instances": {"prompt": form_data.prompt}, - "parameters": { - "sampleCount": form_data.n, - "outputOptions": {"mimeType": "image/png"}, + 'instances': {'prompt': form_data.prompt}, + 'parameters': { + 'sampleCount': form_data.n, + 'outputOptions': {'mimeType': 'image/png'}, }, } - elif ( - request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD - == "generateContent" - ): - model = f"{model}:generateContent" - data = {"contents": [{"parts": [{"text": form_data.prompt}]}]} + elif request.app.state.config.IMAGES_GEMINI_ENDPOINT_METHOD == 'generateContent': + model = f'{model}:generateContent' + data = {'contents': [{'parts': [{'text': form_data.prompt}]}]} - # Use asyncio.to_thread for the requests.post call - r = await asyncio.to_thread( - requests.post, - url=f"{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}", + session = await get_session() + async with session.post( + url=f'{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}', json=data, headers=headers, - ) - - r.raise_for_status() - res = r.json() + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json(content_type=None) images = [] - if model.endswith(":predict"): - for image in res["predictions"]: - image_data, content_type = get_image_data( - image["bytesBase64Encoded"] - ) - _, url = upload_image( - request, image_data, content_type, {**data, **metadata}, user - ) - images.append({"url": url}) - elif model.endswith(":generateContent"): - for image in res["candidates"]: - for part in image["content"]["parts"]: - if part.get("inlineData", {}).get("data"): - image_data, content_type = get_image_data( - part["inlineData"]["data"] - ) - _, url = upload_image( + if model.endswith(':predict'): + for image in res['predictions']: + image_data, content_type = await get_image_data(image['bytesBase64Encoded']) + _, url = await upload_image(request, image_data, content_type, {**data, **metadata}, user) + images.append({'url': url}) + elif model.endswith(':generateContent'): + for image in res['candidates']: + for part in image['content']['parts']: + if part.get('inlineData', {}).get('data'): + image_data, content_type = await get_image_data(part['inlineData']['data']) + _, url = await upload_image( request, image_data, content_type, {**data, **metadata}, user, ) - images.append({"url": url}) + images.append({'url': url}) return images - elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": + elif request.app.state.config.IMAGE_GENERATION_ENGINE == 'comfyui': data = { - "prompt": form_data.prompt, - "width": width, - "height": height, - "n": form_data.n, + 'prompt': form_data.prompt, + 'width': width, + 'height': height, + 'n': form_data.n, } - if ( - request.app.state.config.IMAGE_STEPS is not None - or form_data.steps is not None - ): - data["steps"] = ( - form_data.steps - if form_data.steps is not None - else request.app.state.config.IMAGE_STEPS - ) + if request.app.state.config.IMAGE_STEPS is not None or form_data.steps is not None: + data['steps'] = form_data.steps if form_data.steps is not None else request.app.state.config.IMAGE_STEPS if form_data.negative_prompt is not None: - data["negative_prompt"] = form_data.negative_prompt + data['negative_prompt'] = form_data.negative_prompt form_data = ComfyUICreateImageForm( **{ - "workflow": ComfyUIWorkflow( + 'workflow': ComfyUIWorkflow( **{ - "workflow": request.app.state.config.COMFYUI_WORKFLOW, - "nodes": request.app.state.config.COMFYUI_WORKFLOW_NODES, + 'workflow': request.app.state.config.COMFYUI_WORKFLOW, + 'nodes': request.app.state.config.COMFYUI_WORKFLOW_NODES, } ), **data, @@ -749,148 +729,140 @@ async def image_generations( res = await comfyui_create_image( model, form_data, - user.id, + str(uuid.uuid4()), request.app.state.config.COMFYUI_BASE_URL, request.app.state.config.COMFYUI_API_KEY, ) - log.debug(f"res: {res}") + log.debug(f'res: {res}') images = [] - for image in res["data"]: + for image in res['data']: headers = None if request.app.state.config.COMFYUI_API_KEY: - headers = { - "Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}" - } + headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'} - image_data, content_type = get_image_data(image["url"], headers) - _, url = upload_image( + image_data, content_type = await get_image_data( + image['url'], headers, + trusted_base_url=request.app.state.config.COMFYUI_BASE_URL, + ) + _, url = await upload_image( request, image_data, content_type, {**form_data.model_dump(exclude_none=True), **metadata}, user, ) - images.append({"url": url}) + images.append({'url': url}) return images elif ( - request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111" - or request.app.state.config.IMAGE_GENERATION_ENGINE == "" + request.app.state.config.IMAGE_GENERATION_ENGINE == 'automatic1111' + or request.app.state.config.IMAGE_GENERATION_ENGINE == '' ): if form_data.model: - set_image_model(request, form_data.model) + await set_image_model(request, form_data.model) data = { - "prompt": form_data.prompt, - "batch_size": form_data.n, - "width": width, - "height": height, + 'prompt': form_data.prompt, + 'batch_size': form_data.n, + 'width': width, + 'height': height, } - if ( - request.app.state.config.IMAGE_STEPS is not None - or form_data.steps is not None - ): - data["steps"] = ( - form_data.steps - if form_data.steps is not None - else request.app.state.config.IMAGE_STEPS - ) + if request.app.state.config.IMAGE_STEPS is not None or form_data.steps is not None: + data['steps'] = form_data.steps if form_data.steps is not None else request.app.state.config.IMAGE_STEPS if form_data.negative_prompt is not None: - data["negative_prompt"] = form_data.negative_prompt + data['negative_prompt'] = form_data.negative_prompt if request.app.state.config.AUTOMATIC1111_PARAMS: data = {**data, **request.app.state.config.AUTOMATIC1111_PARAMS} - # Use asyncio.to_thread for the requests.post call - r = await asyncio.to_thread( - requests.post, - url=f"{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img", + session = await get_session() + async with session.post( + url=f'{request.app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img', json=data, - headers={"authorization": get_automatic1111_api_auth(request)}, - ) - - res = r.json() - log.debug(f"res: {res}") + headers={'authorization': get_automatic1111_api_auth(request)}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + res = await r.json(content_type=None) + log.debug(f'res: {res}') images = [] - for image in res["images"]: - image_data, content_type = get_image_data(image) - _, url = upload_image( + for image in res['images']: + image_data, content_type = await get_image_data(image) + _, url = await upload_image( request, image_data, content_type, - {**data, "info": res["info"], **metadata}, + {**data, 'info': res['info'], **metadata}, user, ) - images.append({"url": url}) + images.append({'url': url}) return images except Exception as e: error = e - if r != None: - data = r.json() - if "error" in data: - error = data["error"]["message"] + if isinstance(e, aiohttp.ClientResponseError): + error = e.message raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) class EditImageForm(BaseModel): image: str | list[str] # base64-encoded image(s) or URL(s) prompt: str - model: Optional[str] = None - size: Optional[str] = None - n: Optional[int] = None - negative_prompt: Optional[str] = None + model: str | None = None + size: str | None = None + n: int | None = None + negative_prompt: str | None = None + background: str | None = None -@router.post("/edit") +@router.post('/edit') async def image_edits( request: Request, form_data: EditImageForm, - metadata: Optional[dict] = None, + metadata: dict | None = None, user=Depends(get_verified_user), ): size = None width, height = None, None metadata = metadata or {} - if ( - request.app.state.config.IMAGE_EDIT_SIZE - and "x" in request.app.state.config.IMAGE_EDIT_SIZE - ) or (form_data.size and "x" in form_data.size): - size = ( - form_data.size - if form_data.size - else request.app.state.config.IMAGE_EDIT_SIZE - ) - width, height = tuple(map(int, size.split("x"))) + if (request.app.state.config.IMAGE_EDIT_SIZE and 'x' in request.app.state.config.IMAGE_EDIT_SIZE) or ( + form_data.size and 'x' in form_data.size + ): + size = form_data.size if form_data.size else request.app.state.config.IMAGE_EDIT_SIZE + width, height = tuple(map(int, size.split('x'))) - model = ( - request.app.state.config.IMAGE_EDIT_MODEL - if form_data.model is None - else form_data.model - ) + model = request.app.state.config.IMAGE_EDIT_MODEL if form_data.model is None else form_data.model try: async def load_url_image(data): - if data.startswith("data:"): + if data.startswith('data:'): return data - if data.startswith("http://") or data.startswith("https://"): - r = await asyncio.to_thread(requests.get, data) - r.raise_for_status() + if data.startswith('http://') or data.startswith('https://'): + # Validate URL to prevent SSRF attacks against local/private networks. + # allow_redirects=False prevents redirect-based SSRF: validate_url() is + # called only on the originally-submitted URL; following 3xx redirects + # without re-validation would let an attacker reach private IPs via a + # public host that redirects internally (e.g. cloud-metadata exfil). + validate_url(data) + session = await get_session() + async with session.get( + data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS + ) as r: + r.raise_for_status() - image_data = base64.b64encode(r.content).decode("utf-8") - return f"data:{r.headers['content-type']};base64,{image_data}" + image_data = base64.b64encode(await r.read()).decode('utf-8') + return f'data:{r.headers["content-type"]};base64,{image_data}' else: file_id = None - if data.startswith("/api/v1/files"): - file_id = data.split("/api/v1/files/")[1].split("/content")[0] + if data.startswith('/api/v1/files'): + file_id = data.split('/api/v1/files/')[1].split('/content')[0] else: file_id = data @@ -898,55 +870,59 @@ async def image_edits( if isinstance(file_response, FileResponse): file_path = file_response.path - with open(file_path, "rb") as f: + with open(file_path, 'rb') as f: file_bytes = f.read() - image_data = base64.b64encode(file_bytes).decode("utf-8") + image_data = base64.b64encode(file_bytes).decode('utf-8') mime_type, _ = mimetypes.guess_type(file_path) - return f"data:{mime_type};base64,{image_data}" + return f'data:{mime_type};base64,{image_data}' return data # Load image(s) from URL(s) if necessary if isinstance(form_data.image, str): form_data.image = await load_url_image(form_data.image) elif isinstance(form_data.image, list): - form_data.image = [await load_url_image(img) for img in form_data.image] + # Load all images in parallel for better performance + form_data.image = list(await asyncio.gather(*[load_url_image(img) for img in form_data.image])) except Exception as e: raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) - def get_image_file_item(base64_string, param_name="image"): + def get_image_file_item(base64_string, param_name='image'): data = base64_string - header, encoded = data.split(",", 1) - mime_type = header.split(";")[0].lstrip("data:") + header, encoded = data.split(',', 1) + mime_type = header.split(';')[0].lstrip('data:') image_data = base64.b64decode(encoded) return ( param_name, ( - f"{uuid.uuid4()}.png", + f'{uuid.uuid4()}.png', io.BytesIO(image_data), - mime_type if mime_type else "image/png", + mime_type if mime_type else 'image/png', ), ) - r = None try: - if request.app.state.config.IMAGE_EDIT_ENGINE == "openai": + if request.app.state.config.IMAGE_EDIT_ENGINE == 'openai': headers = { - "Authorization": f"Bearer {request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY}", + 'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_OPENAI_API_KEY}', } if ENABLE_FORWARD_USER_INFO_HEADERS: headers = include_user_info_headers(headers, user) data = { - "model": model, - "prompt": form_data.prompt, - **({"n": form_data.n} if form_data.n else {}), - **({"size": size} if size else {}), + 'model': model, + 'prompt': form_data.prompt, + **({'n': form_data.n} if form_data.n else {}), + **({'size': size} if size else {}), + **({'background': form_data.background} if form_data.background else {}), **( {} - if request.app.state.config.IMAGE_EDIT_MODEL.startswith("gpt-image") - else {"response_format": "b64_json"} + if re.match( + IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, + request.app.state.config.IMAGE_EDIT_MODEL, + ) + else {'response_format': 'b64_json'} ), } @@ -955,98 +931,109 @@ async def image_edits( files = [get_image_file_item(form_data.image)] elif isinstance(form_data.image, list): for img in form_data.image: - files.append(get_image_file_item(img, "image[]")) + files.append(get_image_file_item(img, 'image[]')) - url_search_params = "" + url_search_params = '' if request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION: - url_search_params += f"?api-version={request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION}" + url_search_params += f'?api-version={request.app.state.config.IMAGES_EDIT_OPENAI_API_VERSION}' - # Use asyncio.to_thread for the requests.post call - r = await asyncio.to_thread( - requests.post, - url=f"{request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL}/images/edits{url_search_params}", + # Build multipart form data for aiohttp + form = aiohttp.FormData() + for key, value in data.items(): + if isinstance(value, dict): + form.add_field(key, json.dumps(value)) + else: + form.add_field(key, str(value)) + for param_name, (filename, file_obj, content_type_val) in files: + form.add_field( + param_name, + file_obj, + filename=filename, + content_type=content_type_val, + ) + + session = await get_session() + async with session.post( + url=f'{request.app.state.config.IMAGES_EDIT_OPENAI_API_BASE_URL}/images/edits{url_search_params}', headers=headers, - files=files, - data=data, - ) - - r.raise_for_status() - res = r.json() + data=form, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json(content_type=None) images = [] - for image in res["data"]: - if image_url := image.get("url", None): - image_data, content_type = get_image_data(image_url, headers) + for image in res['data']: + if image_url := image.get('url', None): + image_data, content_type = await get_image_data( + image_url, + {k: v for k, v in headers.items() if k != 'Content-Type'}, + ) else: - image_data, content_type = get_image_data(image["b64_json"]) + image_data, content_type = await get_image_data(image['b64_json']) - _, url = upload_image( - request, image_data, content_type, {**data, **metadata}, user - ) - images.append({"url": url}) + _, url = await upload_image(request, image_data, content_type, {**data, **metadata}, user) + images.append({'url': url}) return images - elif request.app.state.config.IMAGE_EDIT_ENGINE == "gemini": + elif request.app.state.config.IMAGE_EDIT_ENGINE == 'gemini': headers = { - "Content-Type": "application/json", - "x-goog-api-key": request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, + 'Content-Type': 'application/json', + 'x-goog-api-key': request.app.state.config.IMAGES_EDIT_GEMINI_API_KEY, } - model = f"{model}:generateContent" - data = {"contents": [{"parts": [{"text": form_data.prompt}]}]} + model = f'{model}:generateContent' + data = {'contents': [{'parts': [{'text': form_data.prompt}]}]} if isinstance(form_data.image, str): - data["contents"][0]["parts"].append( + data['contents'][0]['parts'].append( { - "inline_data": { - "mime_type": "image/png", - "data": form_data.image.split(",", 1)[1], + 'inline_data': { + 'mime_type': 'image/png', + 'data': form_data.image.split(',', 1)[1], } } ) elif isinstance(form_data.image, list): - data["contents"][0]["parts"].extend( + data['contents'][0]['parts'].extend( [ { - "inline_data": { - "mime_type": "image/png", - "data": image.split(",", 1)[1], + 'inline_data': { + 'mime_type': 'image/png', + 'data': image.split(',', 1)[1], } } for image in form_data.image ] ) - # Use asyncio.to_thread for the requests.post call - r = await asyncio.to_thread( - requests.post, - url=f"{request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL}/models/{model}", + session = await get_session() + async with session.post( + url=f'{request.app.state.config.IMAGES_EDIT_GEMINI_API_BASE_URL}/models/{model}', json=data, headers=headers, - ) - - r.raise_for_status() - res = r.json() + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + res = await r.json(content_type=None) images = [] - for image in res["candidates"]: - for part in image["content"]["parts"]: - if part.get("inlineData", {}).get("data"): - image_data, content_type = get_image_data( - part["inlineData"]["data"] - ) - _, url = upload_image( + for image in res['candidates']: + for part in image['content']['parts']: + if part.get('inlineData', {}).get('data'): + image_data, content_type = await get_image_data(part['inlineData']['data']) + _, url = await upload_image( request, image_data, content_type, {**data, **metadata}, user, ) - images.append({"url": url}) + images.append({'url': url}) return images - elif request.app.state.config.IMAGE_EDIT_ENGINE == "comfyui": + elif request.app.state.config.IMAGE_EDIT_ENGINE == 'comfyui': try: files = [] if isinstance(form_data.image, str): @@ -1063,25 +1050,25 @@ async def image_edits( request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, ) - comfyui_images.append(res.get("name", file_item[1][0])) + comfyui_images.append(res.get('name', file_item[1][0])) except Exception as e: - log.debug(f"Error uploading images to ComfyUI: {e}") - raise Exception("Failed to upload images to ComfyUI.") + log.debug(f'Error uploading images to ComfyUI: {e}') + raise Exception('Failed to upload images to ComfyUI.') data = { - "image": comfyui_images, - "prompt": form_data.prompt, - **({"width": width} if width is not None else {}), - **({"height": height} if height is not None else {}), - **({"n": form_data.n} if form_data.n else {}), + 'image': comfyui_images, + 'prompt': form_data.prompt, + **({'width': width} if width is not None else {}), + **({'height': height} if height is not None else {}), + **({'n': form_data.n} if form_data.n else {}), } form_data = ComfyUIEditImageForm( **{ - "workflow": ComfyUIWorkflow( + 'workflow': ComfyUIWorkflow( **{ - "workflow": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, - "nodes": request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + 'workflow': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW, + 'nodes': request.app.state.config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, } ), **data, @@ -1090,52 +1077,47 @@ async def image_edits( res = await comfyui_edit_image( model, form_data, - user.id, + str(uuid.uuid4()), request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY, ) - log.debug(f"res: {res}") + log.debug(f'res: {res}') image_urls = set() - for image in res["data"]: - image_urls.add(image["url"]) + for image in res['data']: + image_urls.add(image['url']) image_urls = list(image_urls) # Prioritize output type URLs if available - output_type_urls = [url for url in image_urls if "type=output" in url] + output_type_urls = [url for url in image_urls if 'type=output' in url] if output_type_urls: image_urls = output_type_urls - log.debug(f"Image URLs: {image_urls}") + log.debug(f'Image URLs: {image_urls}') images = [] for image_url in image_urls: headers = None if request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY: - headers = { - "Authorization": f"Bearer {request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY}" - } + headers = {'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY}'} - image_data, content_type = get_image_data(image_url, headers) - _, url = upload_image( + image_data, content_type = await get_image_data( + image_url, headers, + trusted_base_url=request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + ) + _, url = await upload_image( request, image_data, content_type, {**form_data.model_dump(exclude_none=True), **metadata}, user, ) - images.append({"url": url}) + images.append({'url': url}) return images except Exception as e: error = e - if r != None: - data = r.text - try: - data = json.loads(data) - if "error" in data: - error = data["error"]["message"] - except Exception: - error = data + if isinstance(e, aiohttp.ClientResponseError): + error = e.message raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 9fc30424ca..46f4e56b4b 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -1,40 +1,43 @@ -from typing import List, Optional -from pydantic import BaseModel -from fastapi import APIRouter, Depends, HTTPException, status, Request, Query -from fastapi.responses import StreamingResponse -from fastapi.concurrency import run_in_threadpool -import logging -import io -import zipfile +from __future__ import annotations -from sqlalchemy.orm import Session -from open_webui.internal.db import get_session +import asyncio +import io +import logging +import zipfile +from typing import List, Optional +from urllib.parse import quote + +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from fastapi.responses import StreamingResponse +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.files import FileMetadataResponse, FileModel, FileModelResponse, Files from open_webui.models.groups import Groups from open_webui.models.knowledge import ( + KnowledgeDirectoryForm, + KnowledgeDirectoryModel, KnowledgeFileListResponse, - Knowledges, KnowledgeForm, KnowledgeResponse, + Knowledges, KnowledgeUserResponse, ) -from open_webui.models.files import Files, FileModel, FileMetadataResponse -from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT +from open_webui.models.models import ModelForm, Models +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT from open_webui.routers.retrieval import ( - process_file, - ProcessFileForm, - process_files_batch, BatchProcessFilesForm, + ProcessFileForm, + process_file, + process_files_batch, ) from open_webui.storage.provider import Storage - -from open_webui.constants import ERROR_MESSAGES -from open_webui.utils.auth import get_verified_user, get_admin_user -from open_webui.utils.access_control import has_access, has_permission - - -from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL -from open_webui.models.models import Models, ModelForm - +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -50,7 +53,9 @@ PAGE_ITEM_COUNT = 30 # Knowledge Base Embedding ############################ -KNOWLEDGE_BASES_COLLECTION = "knowledge-bases" +# Knowledge that sits unread serves no one. Let what is +# stored here find the ones who need it. +KNOWLEDGE_BASES_COLLECTION = 'knowledge-bases' async def embed_knowledge_base_metadata( @@ -61,42 +66,42 @@ async def embed_knowledge_base_metadata( ) -> bool: """Generate and store embedding for knowledge base.""" try: - content = f"{name}\n\n{description}" if description else name + content = f'{name}\n\n{description}' if description else name embedding = await request.app.state.EMBEDDING_FUNCTION(content) - VECTOR_DB_CLIENT.upsert( + await ASYNC_VECTOR_DB_CLIENT.upsert( collection_name=KNOWLEDGE_BASES_COLLECTION, items=[ { - "id": knowledge_base_id, - "text": content, - "vector": embedding, - "metadata": { - "knowledge_base_id": knowledge_base_id, + 'id': knowledge_base_id, + 'text': content, + 'vector': embedding, + 'metadata': { + 'knowledge_base_id': knowledge_base_id, }, } ], ) return True except Exception as e: - log.error(f"Failed to embed knowledge base {knowledge_base_id}: {e}") + log.error(f'Failed to embed knowledge base {knowledge_base_id}: {e}') return False -def remove_knowledge_base_metadata_embedding(knowledge_base_id: str) -> bool: +async def remove_knowledge_base_metadata_embedding(knowledge_base_id: str) -> bool: """Remove knowledge base embedding.""" try: - VECTOR_DB_CLIENT.delete( + await ASYNC_VECTOR_DB_CLIENT.delete( collection_name=KNOWLEDGE_BASES_COLLECTION, ids=[knowledge_base_id], ) return True except Exception as e: - log.debug(f"Failed to remove embedding for {knowledge_base_id}: {e}") + log.debug(f'Failed to remove embedding for {knowledge_base_id}: {e}') return False class KnowledgeAccessResponse(KnowledgeUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class KnowledgeAccessListResponse(BaseModel): @@ -104,26 +109,37 @@ class KnowledgeAccessListResponse(BaseModel): total: int -@router.get("/", response_model=KnowledgeAccessListResponse) +@router.get('/', response_model=KnowledgeAccessListResponse) async def get_knowledge_bases( - page: Optional[int] = 1, + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): page = max(page, 1) limit = PAGE_ITEM_COUNT skip = (page - 1) * limit filter = {} - if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: if groups: - filter["group_ids"] = [group.id for group in groups] + filter['group_ids'] = [group.id for group in groups] - filter["user_id"] = user.id + filter['user_id'] = user.id - result = Knowledges.search_knowledge_bases( - user.id, filter=filter, skip=skip, limit=limit, db=db + result = await Knowledges.search_knowledge_bases(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable knowledge IDs in a single query instead of N has_access calls + knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items] + writable_knowledge_base_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='knowledge', + resource_ids=knowledge_base_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, ) return KnowledgeAccessListResponse( @@ -132,10 +148,8 @@ async def get_knowledge_bases( **knowledge_base.model_dump(), write_access=( user.id == knowledge_base.user_id - or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or has_access( - user.id, "write", knowledge_base.access_control, db=db - ) + or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or knowledge_base.id in writable_knowledge_base_ids ), ) for knowledge_base in result.items @@ -144,13 +158,13 @@ async def get_knowledge_bases( ) -@router.get("/search", response_model=KnowledgeAccessListResponse) +@router.get('/search', response_model=KnowledgeAccessListResponse) async def search_knowledge_bases( - query: Optional[str] = None, - view_option: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + view_option: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): page = max(page, 1) limit = PAGE_ITEM_COUNT @@ -158,19 +172,30 @@ async def search_knowledge_bases( filter = {} if query: - filter["query"] = query + filter['query'] = query if view_option: - filter["view_option"] = view_option + filter['view_option'] = view_option - if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: if groups: - filter["group_ids"] = [group.id for group in groups] + filter['group_ids'] = [group.id for group in groups] - filter["user_id"] = user.id + filter['user_id'] = user.id - result = Knowledges.search_knowledge_bases( - user.id, filter=filter, skip=skip, limit=limit, db=db + result = await Knowledges.search_knowledge_bases(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable knowledge IDs in a single query instead of N has_access calls + knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items] + writable_knowledge_base_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='knowledge', + resource_ids=knowledge_base_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, ) return KnowledgeAccessListResponse( @@ -179,10 +204,8 @@ async def search_knowledge_bases( **knowledge_base.model_dump(), write_access=( user.id == knowledge_base.user_id - or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or has_access( - user.id, "write", knowledge_base.access_control, db=db - ) + or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or knowledge_base.id in writable_knowledge_base_ids ), ) for knowledge_base in result.items @@ -191,12 +214,13 @@ async def search_knowledge_bases( ) -@router.get("/search/files", response_model=KnowledgeFileListResponse) +@router.get('/search/files', response_model=KnowledgeFileListResponse) async def search_knowledge_files( - query: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + include_content: bool = Query(False, description='Include file content in search (expensive).'), + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): page = max(page, 1) limit = PAGE_ITEM_COUNT @@ -204,17 +228,17 @@ async def search_knowledge_files( filter = {} if query: - filter["query"] = query + filter['query'] = query + if include_content: + filter['include_content'] = True - groups = Groups.get_groups_by_member_id(user.id, db=db) + groups = await Groups.get_groups_by_member_id(user.id, db=db) if groups: - filter["group_ids"] = [group.id for group in groups] + filter['group_ids'] = [group.id for group in groups] - filter["user_id"] = user.id + filter['user_id'] = user.id - return Knowledges.search_knowledge_files( - filter=filter, skip=skip, limit=limit, db=db - ) + return await Knowledges.search_knowledge_files(filter=filter, skip=skip, limit=limit, db=db) ############################ @@ -222,35 +246,33 @@ async def search_knowledge_files( ############################ -@router.post("/create", response_model=Optional[KnowledgeResponse]) +@router.post('/create', response_model=KnowledgeResponse | None) async def create_new_knowledge( request: Request, form_data: KnowledgeForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), ): - if user.role != "admin" and not has_permission( - user.id, "workspace.knowledge", request.app.state.config.USER_PERMISSIONS, db=db + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (has_permission, filter_allowed_access_grants, insert_new_knowledge) manage their own sessions. + # This prevents holding a connection during embed_knowledge_base_metadata() + # which makes external embedding API calls (1-5+ seconds). + if user.role != 'admin' and not await has_permission( + user.id, 'workspace.knowledge', request.app.state.config.USER_PERMISSIONS ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - # Check if user can share publicly - if ( - user.role != "admin" - and form_data.access_control == None - and not has_permission( - user.id, - "sharing.public_knowledge", - request.app.state.config.USER_PERMISSIONS, - db=db, - ) - ): - form_data.access_control = {} + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_knowledge', + ) - knowledge = Knowledges.insert_new_knowledge(user.id, form_data, db=db) + knowledge = await Knowledges.insert_new_knowledge(user.id, form_data) if knowledge: # Embed knowledge base for semantic search @@ -273,66 +295,57 @@ async def create_new_knowledge( ############################ -@router.post("/reindex", response_model=bool) +@router.post('/reindex', response_model=bool) async def reindex_knowledge_files( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin": + if user.role != 'admin': raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - knowledge_bases = Knowledges.get_knowledge_bases(db=db) + knowledge_bases = await Knowledges.get_knowledge_bases(db=db) - log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases") + log.info(f'Starting reindexing for {len(knowledge_bases)} knowledge bases') for knowledge_base in knowledge_bases: try: - files = Knowledges.get_files_by_id(knowledge_base.id, db=db) + files = await Knowledges.get_files_by_id(knowledge_base.id, db=db) try: - if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id): - VECTOR_DB_CLIENT.delete_collection( - collection_name=knowledge_base.id - ) + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id): + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=knowledge_base.id) except Exception as e: - log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}") + log.error(f'Error deleting collection {knowledge_base.id}: {str(e)}') continue # Skip, don't raise failed_files = [] for file in files: try: - await run_in_threadpool( - process_file, + await process_file( request, - ProcessFileForm( - file_id=file.id, collection_name=knowledge_base.id - ), + ProcessFileForm(file_id=file.id, collection_name=knowledge_base.id), user=user, db=db, ) except Exception as e: - log.error( - f"Error processing file {file.filename} (ID: {file.id}): {str(e)}" - ) - failed_files.append({"file_id": file.id, "error": str(e)}) + log.error(f'Error processing file {file.filename} (ID: {file.id}): {str(e)}') + failed_files.append({'file_id': file.id, 'error': str(e)}) continue except Exception as e: - log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}") + log.error(f'Error processing knowledge base {knowledge_base.id}: {str(e)}') # Don't raise, just continue continue if failed_files: - log.warning( - f"Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}" - ) + log.warning(f'Failed to process {len(failed_files)} files in knowledge base {knowledge_base.id}') for failed in failed_files: - log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}") + log.warning(f'File ID: {failed["file_id"]}, Error: {failed["error"]}') - log.info(f"Reindexing completed.") + log.info(f'Reindexing completed.') return True @@ -341,23 +354,28 @@ async def reindex_knowledge_files( ############################ -@router.post("/metadata/reindex", response_model=dict) +@router.post('/metadata/reindex', response_model=dict) async def reindex_knowledge_base_metadata_embeddings( request: Request, user=Depends(get_admin_user), - db: Session = Depends(get_session), ): - """Batch embed all existing knowledge bases. Admin only.""" - knowledge_bases = Knowledges.get_knowledge_bases(db=db) - log.info(f"Reindexing embeddings for {len(knowledge_bases)} knowledge bases") + """Batch embed all existing knowledge bases. Admin only. + + NOTE: We intentionally do NOT use Depends(get_async_session) here. + This endpoint loops through ALL knowledge bases and calls embed_knowledge_base_metadata() + for each one, making N external embedding API calls. Holding a session during + this entire operation would exhaust the connection pool. + """ + knowledge_bases = await Knowledges.get_knowledge_bases() + log.info(f'Reindexing embeddings for {len(knowledge_bases)} knowledge bases') success_count = 0 for kb in knowledge_bases: if await embed_knowledge_base_metadata(request, kb.id, kb.name, kb.description): success_count += 1 - log.info(f"Embedding reindex complete: {success_count}/{len(knowledge_bases)}") - return {"total": len(knowledge_bases), "success": success_count} + log.info(f'Embedding reindex complete: {success_count}/{len(knowledge_bases)}') + return {'total': len(knowledge_bases), 'success': success_count} ############################ @@ -366,29 +384,38 @@ async def reindex_knowledge_base_metadata_embeddings( class KnowledgeFilesResponse(KnowledgeResponse): - files: Optional[list[FileMetadataResponse]] = None - write_access: Optional[bool] = False + files: list[FileMetadataResponse | None] = None + write_access: bool | None = False -@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse]) -async def get_knowledge_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) +@router.get('/{id}', response_model=KnowledgeFilesResponse | None) +async def get_knowledge_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if knowledge: if ( - user.role == "admin" + user.role == 'admin' or knowledge.user_id == user.id - or has_access(user.id, "read", knowledge.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + db=db, + ) ): - return KnowledgeFilesResponse( **knowledge.model_dump(), write_access=( user.id == knowledge.user_id - or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or has_access(user.id, "write", knowledge.access_control, db=db) + or (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) ), ) else: @@ -408,15 +435,18 @@ async def get_knowledge_by_id( ############################ -@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/update', response_model=KnowledgeFilesResponse | None) async def update_knowledge_by_id( request: Request, id: str, form_data: KnowledgeForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), ): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations manage their own short-lived sessions internally. + # This prevents holding a connection during embed_knowledge_base_metadata() + # which makes external embedding API calls (1-5+ seconds). + knowledge = await Knowledges.get_knowledge_by_id(id=id) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -425,28 +455,28 @@ async def update_knowledge_by_id( # Is the user the original creator, in a group with write access, or an admin if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Check if user can share publicly - if ( - user.role != "admin" - and form_data.access_control == None - and not has_permission( - user.id, - "sharing.public_knowledge", - request.app.state.config.USER_PERMISSIONS, - db=db, - ) - ): - form_data.access_control = {} + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_knowledge', + ) - knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data, db=db) + knowledge = await Knowledges.update_knowledge_by_id(id=id, form_data=form_data) if knowledge: # Re-embed knowledge base for semantic search await embed_knowledge_base_metadata( @@ -457,7 +487,7 @@ async def update_knowledge_by_id( ) return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id), ) else: raise HTTPException( @@ -466,24 +496,147 @@ async def update_knowledge_by_id( ) +############################ +# UpdateKnowledgeAccessById +############################ + + +class KnowledgeAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/{id}/access/update', response_model=KnowledgeFilesResponse | None) +async def update_knowledge_access_by_id( + request: Request, + id: str, + form_data: KnowledgeAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_knowledge', + ) + + knowledge.access_grants = await AccessGrants.set_access_grants('knowledge', id, form_data.access_grants, db=db) + + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(id, db=db), + ) + + +############################ +# GetPendingKnowledgeFiles +############################ + + +@router.get('/{id}/files/pending') +async def get_pending_knowledge_files( + id: str, + stream: bool = Query(False), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Return files that are being processed for this knowledge base but not yet linked. + + After a file is uploaded with ``knowledge_id`` in its metadata, the backend + processes it in a background task before linking it to the ``knowledge_file`` + join table. During this window the file is invisible to the normal file + list endpoint. This endpoint exposes those in-flight files so the frontend + can show them with a processing indicator even after a page reload. + + When ``stream=true``, returns an SSE stream that polls every 3 seconds + and emits the current pending file list. Closes when no files remain. + """ + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if not ( + user.role == 'admin' + or knowledge.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if not stream: + return await Files.get_pending_files_for_knowledge(id, db=db) + + async def event_stream(knowledge_id: str): + MAX_POLL_DURATION = 3600 # 1 hour max + for _ in range(MAX_POLL_DURATION // 3): + pending = await Files.get_pending_files_for_knowledge(knowledge_id) + data = [f.model_dump() for f in pending] + yield f'data: {json.dumps(data)}\n\n' + if len(pending) == 0: + break + await asyncio.sleep(3) + + return StreamingResponse( + event_stream(id), + media_type='text/event-stream', + ) + + ############################ # GetKnowledgeFilesById ############################ -@router.get("/{id}/files", response_model=KnowledgeFileListResponse) +@router.get('/{id}/files', response_model=KnowledgeFileListResponse) async def get_knowledge_files_by_id( id: str, - query: Optional[str] = None, - view_option: Optional[str] = None, - order_by: Optional[str] = None, - direction: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + include_content: bool = Query(False, description='Include file content in search (expensive).'), + view_option: str | None = None, + order_by: str | None = None, + direction: str | None = None, + directory_id: str | None = Query(None, description='Filter by directory ID. Pass empty string for root.'), + page: int | None = 1, + limit: int | None = Query(None, description='Page size (admin only). Defaults to 30.'), user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -491,9 +644,15 @@ async def get_knowledge_files_by_id( ) if not ( - user.role == "admin" + user.role == 'admin' or knowledge.user_id == user.id - or has_access(user.id, "read", knowledge.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='read', + db=db, + ) ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -502,22 +661,29 @@ async def get_knowledge_files_by_id( page = max(page, 1) - limit = 30 + # Allow admins to configure page size; non-admins always get the default + if user.role == 'admin' and limit is not None: + limit = max(1, limit) + else: + limit = PAGE_ITEM_COUNT skip = (page - 1) * limit filter = {} if query: - filter["query"] = query + filter['query'] = query + if include_content: + filter['include_content'] = True if view_option: - filter["view_option"] = view_option + filter['view_option'] = view_option if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction + # directory_id filtering: present in filter = scope to that directory (None = root) + if directory_id is not None: + filter['directory_id'] = directory_id if directory_id else None - return Knowledges.search_files_by_id( - id, user.id, filter=filter, skip=skip, limit=limit, db=db - ) + return await Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db) ############################ @@ -527,17 +693,18 @@ async def get_knowledge_files_by_id( class KnowledgeFileIdForm(BaseModel): file_id: str + directory_id: Optional[str] = None -@router.post("/{id}/file/add", response_model=Optional[KnowledgeFilesResponse]) -def add_file_to_knowledge_by_id( +@router.post('/{id}/file/add', response_model=KnowledgeFilesResponse | None) +async def add_file_to_knowledge_by_id( request: Request, id: str, form_data: KnowledgeFileIdForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -546,15 +713,21 @@ def add_file_to_knowledge_by_id( if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - file = Files.get_file_by_id(form_data.file_id, db=db) + file = await Files.get_file_by_id(form_data.file_id, db=db) if not file: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -566,9 +739,17 @@ def add_file_to_knowledge_by_id( detail=ERROR_MESSAGES.FILE_NOT_PROCESSED, ) + # KB write-access alone is not enough — caller must also be able to read the file. + if file.user_id != user.id and user.role != 'admin': + if not await has_access_to_file(file.id, 'read', user, db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + # Add content to the vector database try: - process_file( + await process_file( request, ProcessFileForm(file_id=form_data.file_id, collection_name=id), user=user, @@ -576,8 +757,12 @@ def add_file_to_knowledge_by_id( ) # Add file to knowledge base - Knowledges.add_file_to_knowledge_by_id( - knowledge_id=id, file_id=form_data.file_id, user_id=user.id, db=db + await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=id, + file_id=form_data.file_id, + user_id=user.id, + directory_id=form_data.directory_id, + db=db, ) except Exception as e: log.debug(e) @@ -589,7 +774,7 @@ def add_file_to_knowledge_by_id( if knowledge: return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), ) else: raise HTTPException( @@ -598,15 +783,15 @@ def add_file_to_knowledge_by_id( ) -@router.post("/{id}/file/update", response_model=Optional[KnowledgeFilesResponse]) -def update_file_from_knowledge_by_id( +@router.post('/{id}/file/update', response_model=KnowledgeFilesResponse | None) +async def update_file_from_knowledge_by_id( request: Request, id: str, form_data: KnowledgeFileIdForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -615,30 +800,40 @@ def update_file_from_knowledge_by_id( if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): - raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - file = Files.get_file_by_id(form_data.file_id, db=db) + file = await Files.get_file_by_id(form_data.file_id, db=db) if not file: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.NOT_FOUND, ) + # Validate the file actually belongs to this knowledge base + if not await Knowledges.has_file(knowledge_id=id, file_id=form_data.file_id, db=db): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + # Remove content from the vector database - VECTOR_DB_CLIENT.delete( - collection_name=knowledge.id, filter={"file_id": form_data.file_id} - ) + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=knowledge.id, filter={'file_id': form_data.file_id}) # Add content to the vector database try: - process_file( + await process_file( request, ProcessFileForm(file_id=form_data.file_id, collection_name=id), user=user, @@ -653,7 +848,7 @@ def update_file_from_knowledge_by_id( if knowledge: return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), ) else: raise HTTPException( @@ -667,15 +862,15 @@ def update_file_from_knowledge_by_id( ############################ -@router.post("/{id}/file/remove", response_model=Optional[KnowledgeFilesResponse]) -def remove_file_from_knowledge_by_id( +@router.post('/{id}/file/remove', response_model=KnowledgeFilesResponse | None) +async def remove_file_from_knowledge_by_id( id: str, form_data: KnowledgeFileIdForm, delete_file: bool = Query(True), user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -684,57 +879,69 @@ def remove_file_from_knowledge_by_id( if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - file = Files.get_file_by_id(form_data.file_id, db=db) + file = await Files.get_file_by_id(form_data.file_id, db=db) if not file: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.NOT_FOUND, ) - Knowledges.remove_file_from_knowledge_by_id( - knowledge_id=id, file_id=form_data.file_id, db=db - ) + # Validate the file actually belongs to this knowledge base + if not await Knowledges.has_file(knowledge_id=id, file_id=form_data.file_id, db=db): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + await Knowledges.remove_file_from_knowledge_by_id(knowledge_id=id, file_id=form_data.file_id, db=db) # Remove content from the vector database try: - VECTOR_DB_CLIENT.delete( - collection_name=knowledge.id, filter={"file_id": form_data.file_id} + await ASYNC_VECTOR_DB_CLIENT.delete( + collection_name=knowledge.id, filter={'file_id': form_data.file_id} ) # Remove by file_id first - VECTOR_DB_CLIENT.delete( - collection_name=knowledge.id, filter={"hash": file.hash} + await ASYNC_VECTOR_DB_CLIENT.delete( + collection_name=knowledge.id, filter={'hash': file.hash} ) # Remove by hash as well in case of duplicates except Exception as e: - log.debug("This was most likely caused by bypassing embedding processing") + log.debug('This was most likely caused by bypassing embedding processing') log.debug(e) pass - if delete_file: + # Anyone with write permission or higher can delete files + if delete_file and (file.user_id == user.id or user.role == 'admin'): try: # Remove the file's collection from vector database - file_collection = f"file-{form_data.file_id}" - if VECTOR_DB_CLIENT.has_collection(collection_name=file_collection): - VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection) + file_collection = f'file-{form_data.file_id}' + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name=file_collection): + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=file_collection) except Exception as e: - log.debug("This was most likely caused by bypassing embedding processing") + log.debug('This was most likely caused by bypassing embedding processing') log.debug(e) pass # Delete file from database - Files.delete_file_by_id(form_data.file_id, db=db) + await Files.delete_file_by_id(form_data.file_id, db=db) if knowledge: return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), ) else: raise HTTPException( @@ -748,11 +955,11 @@ def remove_file_from_knowledge_by_id( ############################ -@router.delete("/{id}/delete", response_model=bool) +@router.delete('/{id}/delete', response_model=bool) async def delete_knowledge_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -761,54 +968,51 @@ async def delete_knowledge_by_id( if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - log.info(f"Deleting knowledge base: {id} (name: {knowledge.name})") + log.info(f'Deleting knowledge base: {id} (name: {knowledge.name})') # Get all models - models = Models.get_all_models(db=db) - log.info(f"Found {len(models)} models to check for knowledge base {id}") + models = await Models.get_all_models(db=db) + log.info(f'Found {len(models)} models to check for knowledge base {id}') # Update models that reference this knowledge base for model in models: - if model.meta and hasattr(model.meta, "knowledge"): + if model.meta and hasattr(model.meta, 'knowledge'): knowledge_list = model.meta.knowledge or [] # Filter out the deleted knowledge base - updated_knowledge = [k for k in knowledge_list if k.get("id") != id] + updated_knowledge = [k for k in knowledge_list if k.get('id') != id] # If the knowledge list changed, update the model if len(updated_knowledge) != len(knowledge_list): - log.info(f"Updating model {model.id} to remove knowledge base {id}") + log.info(f'Updating model {model.id} to remove knowledge base {id}') model.meta.knowledge = updated_knowledge - # Create a ModelForm for the update - model_form = ModelForm( - id=model.id, - name=model.name, - base_model_id=model.base_model_id, - meta=model.meta, - params=model.params, - access_control=model.access_control, - is_active=model.is_active, - ) - Models.update_model_by_id(model.id, model_form, db=db) + model_form = ModelForm(**model.model_dump()) + await Models.update_model_by_id(model.id, model_form, db=db) # Clean up vector DB try: - VECTOR_DB_CLIENT.delete_collection(collection_name=id) + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=id) except Exception as e: log.debug(e) pass # Remove knowledge base embedding - remove_knowledge_base_metadata_embedding(id) + await remove_knowledge_base_metadata_embedding(id) - result = Knowledges.delete_knowledge_by_id(id=id, db=db) + result = await Knowledges.delete_knowledge_by_id(id=id, db=db) return result @@ -817,11 +1021,14 @@ async def delete_knowledge_by_id( ############################ -@router.post("/{id}/reset", response_model=Optional[KnowledgeResponse]) +@router.post('/{id}/reset', response_model=KnowledgeResponse | None) async def reset_knowledge_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, + include_directories: bool = Query(True), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), ): - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -830,8 +1037,14 @@ async def reset_knowledge_by_id( if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -839,32 +1052,211 @@ async def reset_knowledge_by_id( ) try: - VECTOR_DB_CLIENT.delete_collection(collection_name=id) + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=id) except Exception as e: log.debug(e) pass - knowledge = Knowledges.reset_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.reset_knowledge_by_id(id=id, include_directories=include_directories, db=db) return knowledge +############################ +# SyncKnowledgeDiff +############################ + + +class FileManifestEntry(BaseModel): + filename: str # basename: "readme.md" + path: str # relative dir: "docs/api" or "" for root + checksum: str # SHA-256 of raw bytes + size: int + + +class SyncDiffForm(BaseModel): + manifest: list[FileManifestEntry] + + +class SyncDiffResponse(BaseModel): + added: list[dict] # [{filename, path}] — new files + modified: list[dict] # [{filename, path, stale_file_id}] — changed files + deleted: list[dict] # [{file_id, filename}] — files to remove + mkdir: list[str] # directory paths to create + rmdir: list[str] # directory IDs to remove + unmodified_count: int + directory_map: dict[str, str] # existing path → directory ID + + +@router.post('/{id}/sync/diff', response_model=SyncDiffResponse) +async def sync_knowledge_diff( + id: str, + form_data: SyncDiffForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Compare a local file manifest against the knowledge base to determine + which files need uploading, removing, and which directories to create/remove. + """ + await _verify_knowledge_write_access(id, user, db) + + # ── Index existing state ── + knowledge_files = await Knowledges.get_files_with_directory_ids(id, db=db) + existing_directories = await Knowledges.get_all_directories(id, db=db) + + # Build directory path lookups + directory_path_by_id: dict[str, str] = {} + directory_id_by_path: dict[str, str] = {} + for directory in existing_directories: + segments = [directory.name] + parent_id = directory.parent_id + while parent_id: + parent = next((d for d in existing_directories if d.id == parent_id), None) + if not parent: + break + segments.insert(0, parent.name) + parent_id = parent.parent_id + full_path = '/'.join(segments) + directory_path_by_id[directory.id] = full_path + directory_id_by_path[full_path] = directory.id + + # Index existing files by (path, filename) → {file_id, checksum} + indexed_files: dict[tuple[str, str], dict] = {} + for file_model, directory_id in knowledge_files: + file_path = directory_path_by_id.get(directory_id, '') if directory_id else '' + stored_checksum = (file_model.meta or {}).get('file_hash') + indexed_files[(file_path, file_model.filename)] = { + 'file_id': file_model.id, + 'checksum': stored_checksum, + } + + # ── Diff files ── + added: list[dict] = [] + modified: list[dict] = [] + deleted: list[dict] = [] + unmodified_count = 0 + manifest_keys: set[tuple[str, str]] = set() + + for entry in form_data.manifest: + key = (entry.path, entry.filename) + manifest_keys.add(key) + + if key not in indexed_files: + added.append({'filename': entry.filename, 'path': entry.path}) + elif indexed_files[key]['checksum'] != entry.checksum: + modified.append( + { + 'filename': entry.filename, + 'path': entry.path, + 'stale_file_id': indexed_files[key]['file_id'], + } + ) + else: + unmodified_count += 1 + + for key, file_info in indexed_files.items(): + if key not in manifest_keys: + deleted.append({'file_id': file_info['file_id'], 'filename': key[1]}) + + # ── Diff directories ── + required_directory_paths: set[str] = set() + for entry in form_data.manifest: + if entry.path: + segments = entry.path.split('/') + for depth in range(len(segments)): + required_directory_paths.add('/'.join(segments[: depth + 1])) + + mkdir = sorted([p for p in required_directory_paths if p not in directory_id_by_path], key=lambda p: p.count('/')) + + orphaned_directory_paths = set(directory_id_by_path) - required_directory_paths + rmdir = [directory_id_by_path[p] for p in orphaned_directory_paths] + + return SyncDiffResponse( + added=added, + modified=modified, + deleted=deleted, + mkdir=mkdir, + rmdir=rmdir, + unmodified_count=unmodified_count, + directory_map=directory_id_by_path, + ) + + +############################ +# SyncKnowledgeCleanup +############################ + + +class SyncCleanupForm(BaseModel): + file_ids: list[str] # file IDs to delete + dir_ids: list[str] = [] # directory IDs to rmdir + + +@router.post('/{id}/sync/cleanup') +async def sync_knowledge_cleanup( + id: str, + form_data: SyncCleanupForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Remove stale files and orphaned directories from a knowledge base + after an incremental sync. + """ + await _verify_knowledge_write_access(id, user, db) + + # ── Remove deleted files ── + for file_id in form_data.file_ids: + file = await Files.get_file_by_id(file_id, db=db) + if not file: + continue + + await Knowledges.remove_file_from_knowledge_by_id(id, file_id, db=db) + + try: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=id, filter={'file_id': file_id}) + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=id, filter={'hash': file.hash}) + except Exception: + pass + + try: + collection_name = f'file-{file_id}' + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name): + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name) + except Exception: + pass + + if file.user_id == user.id or user.role == 'admin': + await Files.delete_file_by_id(file_id, db=db) + try: + await asyncio.to_thread(Storage.delete_file, file.path) + except Exception: + pass + + # ── Remove orphaned directories (children before parents) ── + for dir_id in reversed(form_data.dir_ids): + await Knowledges.delete_directory(dir_id, move_files_to_parent=False, db=db) + + return {'status': True} + + ############################ # AddFilesToKnowledge ############################ -@router.post("/{id}/files/batch/add", response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/files/batch/add', response_model=KnowledgeFilesResponse | None) async def add_files_to_knowledge_batch( request: Request, id: str, form_data: list[KnowledgeFileIdForm], user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """ Add multiple files to a knowledge base """ - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -873,25 +1265,59 @@ async def add_files_to_knowledge_batch( if ( knowledge.user_id != user.id - and not has_access(user.id, "write", knowledge.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - # Get files content - log.info(f"files/batch/add - {len(form_data)} files") - files: List[FileModel] = [] + # Batch-fetch all files to avoid N+1 queries + log.info(f'files/batch/add - {len(form_data)} files') + file_ids = [form.file_id for form in form_data] + files = await Files.get_files_by_ids(file_ids, db=db) + + # Verify all requested files were found + found_ids = {file.id for file in files} + missing_ids = [fid for fid in file_ids if fid not in found_ids] + if missing_ids: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'File {missing_ids[0]} not found', + ) + + # Per-file read-access check — same gate as the single-file endpoint. + if user.role != 'admin': + for file in files: + if file.user_id != user.id and not await has_access_to_file(file.id, 'read', user, db=db): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Filter out files already linked to this knowledge base to prevent + # duplicate embeddings in the vector DB (issue #10679). + new_entries = [] for form in form_data: - file = Files.get_file_by_id(form.file_id, db=db) - if not file: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"File {form.file_id} not found", - ) - files.append(file) + if not await Knowledges.has_file(knowledge_id=id, file_id=form.file_id, db=db): + new_entries.append(form) + + if not new_entries: + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + ) + + # Narrow the file list to only new files for processing + new_file_ids = {form.file_id for form in new_entries} + files = [f for f in files if f.id in new_file_ids] # Process files try: @@ -902,33 +1328,33 @@ async def add_files_to_knowledge_batch( db=db, ) except Exception as e: - log.error( - f"add_files_to_knowledge_batch: Exception occurred: {e}", exc_info=True - ) + log.error(f'add_files_to_knowledge_batch: Exception occurred: {e}', exc_info=True) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) # Only add files that were successfully processed - successful_file_ids = [r.file_id for r in result.results if r.status == "completed"] + successful_file_ids = [r.file_id for r in result.results if r.status == 'completed'] + dir_map = {form.file_id: form.directory_id for form in new_entries} for file_id in successful_file_ids: - Knowledges.add_file_to_knowledge_by_id( - knowledge_id=id, file_id=file_id, user_id=user.id, db=db + await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=id, file_id=file_id, user_id=user.id, + directory_id=dir_map.get(file_id), db=db, ) # If there were any errors, include them in the response if result.errors: - error_details = [f"{err.file_id}: {err.error}" for err in result.errors] + error_details = [f'{err.file_id}: {err.error}' for err in result.errors] return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), warnings={ - "message": "Some files failed to process", - "errors": error_details, + 'message': 'Some files failed to process', + 'errors': error_details, }, ) return KnowledgeFilesResponse( **knowledge.model_dump(), - files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), ) @@ -937,44 +1363,219 @@ async def add_files_to_knowledge_batch( ############################ -@router.get("/{id}/export") -async def export_knowledge_by_id( - id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): +@router.get('/{id}/export') +async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): """ Export a knowledge base as a zip file containing .txt files. Admin only. """ - knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) if not knowledge: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - files = Knowledges.get_files_by_id(id, db=db) + files = await Knowledges.get_files_by_id(id, db=db) # Create zip file in memory zip_buffer = io.BytesIO() - with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: for file in files: - content = file.data.get("content", "") if file.data else "" + content = file.data.get('content', '') if file.data else '' if content: # Use original filename with .txt extension filename = file.filename - if not filename.endswith(".txt"): - filename = f"{filename}.txt" + if not filename.endswith('.txt'): + filename = f'{filename}.txt' zf.writestr(filename, content) zip_buffer.seek(0) # Sanitize knowledge name for filename - safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in knowledge.name) - zip_filename = f"{safe_name}.zip" + # ASCII-safe fallback for the basic filename parameter (latin-1 safe) + safe_name = ''.join(c if c.isascii() and (c.isalnum() or c in ' -_') else '_' for c in knowledge.name) + zip_filename = f'{safe_name}.zip' + + # Use RFC 5987 filename* for non-ASCII names so the browser gets the real name + quoted_name = quote(f'{knowledge.name}.zip') + content_disposition = f'attachment; filename="{zip_filename}"; filename*=UTF-8\'\'{quoted_name}' return StreamingResponse( zip_buffer, - media_type="application/zip", - headers={"Content-Disposition": f"attachment; filename={zip_filename}"}, + media_type='application/zip', + headers={'Content-Disposition': content_disposition}, ) + + +############################ +# Directory endpoints +############################ + + +class KnowledgeDirectoryCreateForm(BaseModel): + name: str + parent_id: Optional[str] = None + + +class KnowledgeDirectoryUpdateForm(BaseModel): + name: Optional[str] = None + parent_id: Optional[str] = '__unset__' + + +class KnowledgeFileMoveForm(BaseModel): + file_id: str + directory_id: Optional[str] = None + + +async def _verify_knowledge_write_access(id: str, user, db: AsyncSession): + """Verify the user has write access to the knowledge base. Returns the knowledge model.""" + knowledge = await Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + if ( + knowledge.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='knowledge', + resource_id=knowledge.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return knowledge + + +@router.post('/{id}/dirs/create', response_model=KnowledgeDirectoryModel) +async def create_knowledge_directory( + id: str, + form_data: KnowledgeDirectoryCreateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + directory = await Knowledges.create_directory( + knowledge_id=id, + name=form_data.name, + user_id=user.id, + parent_id=form_data.parent_id, + db=db, + ) + if not directory: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Failed to create directory. A directory with this name may already exist at this level.', + ) + return directory + + +@router.post('/{id}/dirs/{dir_id}/update', response_model=KnowledgeDirectoryModel) +async def update_knowledge_directory( + id: str, + dir_id: str, + form_data: KnowledgeDirectoryUpdateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + # Verify directory belongs to this knowledge base + directory = await Knowledges.get_directory_by_id(dir_id, db=db) + if not directory or directory.knowledge_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + result = await Knowledges.update_directory( + directory_id=dir_id, + name=form_data.name, + parent_id=form_data.parent_id, + db=db, + ) + if not result: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Failed to update directory. This may be caused by a naming conflict or circular move.', + ) + return result + + +@router.delete('/{id}/dirs/{dir_id}/delete') +async def delete_knowledge_directory( + id: str, + dir_id: str, + move_files: bool = Query(True, description='If true, move contained files to parent. If false, delete them.'), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + # Verify directory belongs to this knowledge base + directory = await Knowledges.get_directory_by_id(dir_id, db=db) + if not directory or directory.knowledge_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + success = await Knowledges.delete_directory( + directory_id=dir_id, + move_files_to_parent=move_files, + db=db, + ) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to delete directory.', + ) + return {'status': True} + + +@router.post('/{id}/file/move') +async def move_file_in_knowledge( + id: str, + form_data: KnowledgeFileMoveForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + # Verify file belongs to this knowledge base + if not await Knowledges.has_file(knowledge_id=id, file_id=form_data.file_id, db=db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # If target directory is set, verify it belongs to this knowledge base + if form_data.directory_id: + directory = await Knowledges.get_directory_by_id(form_data.directory_id, db=db) + if not directory or directory.knowledge_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Target directory not found.', + ) + + success = await Knowledges.move_file_to_directory( + knowledge_id=id, + file_id=form_data.file_id, + directory_id=form_data.directory_id, + db=db, + ) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to move file.', + ) + return {'status': True} diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index e0ba36c76f..e2086609ae 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -1,38 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException, Request, status -from pydantic import BaseModel -import logging +from __future__ import annotations + import asyncio +import logging from typing import Optional -from open_webui.models.memories import Memories, MemoryModel -from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT -from open_webui.utils.auth import get_verified_user -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session - -from open_webui.utils.access_control import has_permission +from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from open_webui.models.memories import Memories, MemoryModel +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.utils.access_control import has_permission +from open_webui.utils.auth import get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) router = APIRouter() -@router.get("/ef") -async def get_embeddings(request: Request): - return {"result": await request.app.state.EMBEDDING_FUNCTION("hello world")} - - ############################ # GetMemories +# Let what is remembered here spare someone the cost +# of learning it twice. ############################ -@router.get("/", response_model=list[MemoryModel]) +@router.get('/', response_model=list[MemoryModel]) async def get_memories( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( @@ -40,15 +38,13 @@ async def get_memories( detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - return Memories.get_memories_by_user_id(user.id, db=db) + return await Memories.get_memories_by_user_id(user.id, db=db) ############################ @@ -61,42 +57,43 @@ class AddMemoryForm(BaseModel): class MemoryUpdateModel(BaseModel): - content: Optional[str] = None + content: str | None = None -@router.post("/add", response_model=Optional[MemoryModel]) +@router.post('/add', response_model=MemoryModel | None) async def add_memory( - request: Request, - form_data: AddMemoryForm, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user), ): + """Persist a new memory and embed it into the user's vector collection. + + Does NOT use ``Depends(get_async_session)`` — database operations manage their + own short-lived sessions so a connection is not held during the external + embedding API call (``EMBEDDING_FUNCTION``), which can take 1-5+ seconds. + """ if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - memory = Memories.insert_new_memory(user.id, form_data.content, db=db) + memory = await Memories.insert_new_memory(user.id, form_data.content) vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) - VECTOR_DB_CLIENT.upsert( - collection_name=f"user-memory-{user.id}", + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=f'user-memory-{user.id}', items=[ { - "id": memory.id, - "text": memory.content, - "vector": vector, - "metadata": {"created_at": memory.created_at}, + 'id': memory.id, + 'text': memory.content, + 'vector': vector, + 'metadata': {'created_at': memory.created_at}, } ], ) @@ -111,90 +108,125 @@ async def add_memory( class QueryMemoryForm(BaseModel): content: str - k: Optional[int] = 1 + k: int | None = 1 -@router.post("/query") +@router.post('/query') async def query_memory( request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), ): + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (get_memories_by_user_id) manage their own short-lived sessions. + # This prevents holding a connection during EMBEDDING_FUNCTION() + # which makes external embedding API calls (1-5+ seconds). if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - memories = Memories.get_memories_by_user_id(user.id, db=db) + memories = await Memories.get_memories_by_user_id(user.id) if not memories: - raise HTTPException(status_code=404, detail="No memories found for user") + raise HTTPException(status_code=404, detail='No memories found for user') vector = await request.app.state.EMBEDDING_FUNCTION(form_data.content, user=user) - results = VECTOR_DB_CLIENT.search( - collection_name=f"user-memory-{user.id}", + results = await ASYNC_VECTOR_DB_CLIENT.search( + collection_name=f'user-memory-{user.id}', vectors=[vector], limit=form_data.k, ) + # Filter results by relevance threshold to avoid returning unrelated + # memories. Vector similarity search always returns the top-K nearest + # neighbours even when they are completely irrelevant; applying the + # same RELEVANCE_THRESHOLD used by RAG ensures only genuinely matching + # memories are surfaced (distances are normalised to 0→1, higher is + # better). + relevance_threshold = getattr(request.app.state.config, 'RELEVANCE_THRESHOLD', 0.0) + if results and relevance_threshold > 0.0 and results.distances and results.distances[0]: + from open_webui.retrieval.vector.main import SearchResult + + filtered_ids = [] + filtered_docs = [] + filtered_metas = [] + filtered_dists = [] + + for idx, score in enumerate(results.distances[0]): + if score >= relevance_threshold: + if results.ids and results.ids[0]: + filtered_ids.append(results.ids[0][idx]) + if results.documents and results.documents[0]: + filtered_docs.append(results.documents[0][idx]) + if results.metadatas and results.metadatas[0]: + filtered_metas.append(results.metadatas[0][idx]) + filtered_dists.append(score) + + results = SearchResult( + ids=[filtered_ids] if filtered_ids else [[]], + documents=[filtered_docs] if filtered_docs else [[]], + metadatas=[filtered_metas] if filtered_metas else [[]], + distances=[filtered_dists] if filtered_dists else [[]], + ) + return results ############################ # ResetMemoryFromVectorDB ############################ -@router.post("/reset", response_model=bool) +@router.post('/reset', response_model=bool) async def reset_memory_from_vector_db( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), ): + """Reset user's memory vector embeddings. + + CRITICAL: We intentionally do NOT use Depends(get_async_session) here. + This endpoint generates embeddings for ALL user memories in parallel using + asyncio.gather(). A user with 100 memories would trigger 100 embedding API + calls simultaneously. With a session held, this could block a connection + for MINUTES, completely exhausting the connection pool. + """ if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}") + await ASYNC_VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}') - memories = Memories.get_memories_by_user_id(user.id, db=db) + memories = await Memories.get_memories_by_user_id(user.id) # Generate vectors in parallel vectors = await asyncio.gather( - *[ - request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) - for memory in memories - ] + *[request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) for memory in memories] ) - VECTOR_DB_CLIENT.upsert( - collection_name=f"user-memory-{user.id}", + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=f'user-memory-{user.id}', items=[ { - "id": memory.id, - "text": memory.content, - "vector": vectors[idx], - "metadata": { - "created_at": memory.created_at, - "updated_at": memory.updated_at, + 'id': memory.id, + 'text': memory.content, + 'vector': vectors[idx], + 'metadata': { + 'created_at': memory.created_at, + 'updated_at': memory.updated_at, }, } for idx, memory in enumerate(memories) @@ -209,11 +241,11 @@ async def reset_memory_from_vector_db( ############################ -@router.delete("/delete/user", response_model=bool) +@router.delete('/delete/user', response_model=bool) async def delete_memory_by_user_id( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( @@ -221,19 +253,17 @@ async def delete_memory_by_user_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - result = Memories.delete_memories_by_user_id(user.id, db=db) + result = await Memories.delete_memories_by_user_id(user.id, db=db) if result: try: - VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}") + await ASYNC_VECTOR_DB_CLIENT.delete_collection(f'user-memory-{user.id}') except Exception as e: log.error(e) return True @@ -246,47 +276,46 @@ async def delete_memory_by_user_id( ############################ -@router.post("/{memory_id}/update", response_model=Optional[MemoryModel]) +@router.post('/{memory_id}/update', response_model=MemoryModel | None) async def update_memory_by_id( memory_id: str, request: Request, form_data: MemoryUpdateModel, user=Depends(get_verified_user), - db: Session = Depends(get_session), ): + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (update_memory_by_id_and_user_id) manage their own + # short-lived sessions. This prevents holding a connection during + # EMBEDDING_FUNCTION() which makes external API calls (1-5+ seconds). if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - memory = Memories.update_memory_by_id_and_user_id( - memory_id, user.id, form_data.content, db=db - ) + memory = await Memories.update_memory_by_id_and_user_id(memory_id, user.id, form_data.content) if memory is None: - raise HTTPException(status_code=404, detail="Memory not found") + raise HTTPException(status_code=404, detail=ERROR_MESSAGES.NOT_FOUND) if form_data.content is not None: vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) - VECTOR_DB_CLIENT.upsert( - collection_name=f"user-memory-{user.id}", + await ASYNC_VECTOR_DB_CLIENT.upsert( + collection_name=f'user-memory-{user.id}', items=[ { - "id": memory.id, - "text": memory.content, - "vector": vector, - "metadata": { - "created_at": memory.created_at, - "updated_at": memory.updated_at, + 'id': memory.id, + 'text': memory.content, + 'vector': vector, + 'metadata': { + 'created_at': memory.created_at, + 'updated_at': memory.updated_at, }, } ], @@ -300,12 +329,12 @@ async def update_memory_by_id( ############################ -@router.delete("/{memory_id}", response_model=bool) +@router.delete('/{memory_id}', response_model=bool) async def delete_memory_by_id( memory_id: str, request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( @@ -313,20 +342,16 @@ async def delete_memory_by_id( detail=ERROR_MESSAGES.NOT_FOUND, ) - if not has_permission( - user.id, "features.memories", request.app.state.config.USER_PERMISSIONS - ): + if not await has_permission(user.id, 'features.memories', request.app.state.config.USER_PERMISSIONS): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db) + result = await Memories.delete_memory_by_id_and_user_id(memory_id, user.id, db=db) if result: - VECTOR_DB_CLIENT.delete( - collection_name=f"user-memory-{user.id}", ids=[memory_id] - ) + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=f'user-memory-{user.id}', ids=[memory_id]) return True return False diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index a1f642bbce..f3737cb66d 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -1,71 +1,103 @@ -from typing import Optional -import io -import base64 -import json +from __future__ import annotations + import asyncio +import base64 +import io +import json import logging +import posixpath +from typing import Optional +from urllib.parse import unquote -from open_webui.models.groups import Groups -from open_webui.models.models import ( - ModelForm, - ModelModel, - ModelResponse, - ModelListResponse, - ModelAccessListResponse, - ModelAccessResponse, - Models, -) - -from pydantic import BaseModel -from open_webui.constants import ERROR_MESSAGES from fastapi import ( APIRouter, Depends, HTTPException, Request, - status, Response, + status, ) -from fastapi.responses import FileResponse, StreamingResponse - - +from fastapi.responses import RedirectResponse, StreamingResponse +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.models import ( + ModelAccessListResponse, + ModelAccessResponse, + ModelForm, + ModelListResponse, + ModelMeta, + ModelModel, + ModelParams, + ModelResponse, + Models, +) +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access, has_permission -from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) router = APIRouter() +def _safe_static_redirect_path(url: str) -> str | None: + """ + If url is a same-origin static asset path, return a normalized path safe for + RedirectResponse Location. Otherwise None (caller should fall back to default). + Rejects traversal (..), encoded dots, query/fragment, and non-/static targets. + """ + if not url or not isinstance(url, str): + return None + path = url.split('?', 1)[0].split('#', 1)[0].strip() + for _ in range(2): + decoded = unquote(path) + if decoded == path: + break + path = decoded + if '\x00' in path or '\\' in path: + return None + if not path.startswith('/'): + return None + normalized = posixpath.normpath(path) + if normalized in ('.', '/'): + return None + if not (normalized == '/static' or normalized.startswith('/static/')): + return None + if normalized == '/static': + return '/static/' + return normalized + + def is_valid_model_id(model_id: str) -> bool: return model_id and len(model_id) <= 256 ########################### # GetModels +# Let each model here be judged by what it does and not +# by what it claims. The house deserves honest servants. ########################### PAGE_ITEM_COUNT = 30 -@router.get( - "/list", response_model=ModelAccessListResponse -) # do NOT use "/" as path, conflicts with main.py +@router.get('/list', response_model=ModelAccessListResponse) # do NOT use "/" as path, conflicts with main.py async def get_models( - query: Optional[str] = None, - view_option: Optional[str] = None, - tag: Optional[str] = None, - order_by: Optional[str] = None, - direction: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + view_option: str | None = None, + tag: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - limit = PAGE_ITEM_COUNT page = max(1, page) @@ -73,36 +105,58 @@ async def get_models( filter = {} if query: - filter["query"] = query + filter['query'] = query if view_option: - filter["view_option"] = view_option + filter['view_option'] = view_option if tag: - filter["tag"] = tag + filter['tag'] = tag if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) + # Pre-fetch user group IDs once - used for both filter and write_access check + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: if groups: - filter["group_ids"] = [group.id for group in groups] + filter['group_ids'] = [group.id for group in groups] - filter["user_id"] = user.id + filter['user_id'] = user.id - result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) - return ModelAccessListResponse( - items=[ + result = await Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable model IDs in a single query instead of N has_access calls + model_ids = [model.id for model in result.items] + writable_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=model_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + + # Strip profile_image_url from meta — images are served via /model/profile/image. + items = [] + for model in result.items: + data = model.model_dump() + if data.get('meta'): + data['meta'].pop('profile_image_url', None) + items.append( ModelAccessResponse( - **model.model_dump(), + **data, write_access=( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) or user.id == model.user_id - or has_access(user.id, "write", model.access_control, db=db) + or model.id in writable_model_ids ), ) - for model in result.items - ], + ) + + return ModelAccessListResponse( + items=items, total=result.total, ) @@ -112,11 +166,9 @@ async def get_models( ########################### -@router.get("/base", response_model=list[ModelResponse]) -async def get_base_models( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - return Models.get_base_models(db=db) +@router.get('/base', response_model=list[ModelResponse]) +async def get_base_models(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + return await Models.get_base_models(db=db) ########################### @@ -124,25 +176,14 @@ async def get_base_models( ########################### -@router.get("/tags", response_model=list[str]) -async def get_model_tags( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: - models = Models.get_models(db=db) - else: - models = Models.get_models_by_user_id(user.id, db=db) - - tags_set = set() - for model in models: - if model.meta: - meta = model.meta.model_dump() - for tag in meta.get("tags", []): - tags_set.add((tag.get("name"))) - - tags = [tag for tag in tags_set] - tags.sort() - return tags +@router.get('/tags', response_model=list[str]) +async def get_model_tags(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + tags = await Models.get_all_tags( + user_id=user.id, + is_admin=(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL), + db=db, + ) + return sorted(tags) ############################ @@ -150,22 +191,21 @@ async def get_model_tags( ############################ -@router.post("/create", response_model=Optional[ModelModel]) +@router.post('/create', response_model=ModelModel | None) async def create_new_model( - request: Request, - form_data: ModelForm, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + request: Request, form_data: ModelForm, + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "workspace.models", request.app.state.config.USER_PERMISSIONS, db=db + """Create a new workspace model entry.""" + if user.role != 'admin' and not await has_permission( + user.id, 'workspace.models', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - model = Models.get_model_by_id(form_data.id, db=db) + model = await Models.get_model_by_id(form_data.id, db=db) if model: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -179,7 +219,15 @@ async def create_new_model( ) else: - model = Models.insert_new_model(form_data, user.id, db=db) + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_models', + ) + + model = await Models.insert_new_model(form_data, user.id, db=db) if model: return model else: @@ -194,15 +242,15 @@ async def create_new_model( ############################ -@router.get("/export", response_model=list[ModelModel]) +@router.get('/export', response_model=list[ModelModel]) async def export_models( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( + if user.role != 'admin' and not await has_permission( user.id, - "workspace.models_export", + 'workspace.models_export', request.app.state.config.USER_PERMISSIONS, db=db, ): @@ -211,10 +259,10 @@ async def export_models( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: - return Models.get_models(db=db) + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Models.get_models(db=db) else: - return Models.get_models_by_user_id(user.id, db=db) + return await Models.get_models_by_user_id(user.id, db=db) ############################ @@ -226,16 +274,16 @@ class ModelsImportForm(BaseModel): models: list[dict] -@router.post("/import", response_model=bool) +@router.post('/import', response_model=bool) async def import_models( request: Request, user=Depends(get_verified_user), form_data: ModelsImportForm = (...), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( + if user.role != 'admin' and not await has_permission( user.id, - "workspace.models_import", + 'workspace.models_import', request.app.state.config.USER_PERMISSIONS, db=db, ): @@ -246,32 +294,85 @@ async def import_models( try: data = form_data.models if isinstance(data, list): + # Batch-fetch all existing models in one query to avoid N+1 + model_ids = [ + model_data.get('id') + for model_data in data + if model_data.get('id') and is_valid_model_id(model_data.get('id')) + ] + existing_models = { + model.id: model for model in (await Models.get_models_by_ids(model_ids, db=db) if model_ids else []) + } + + # Batch-resolve write permissions in one query instead of + # per-model has_access calls (N+1 avoidance). + existing_model_ids = list(existing_models.keys()) + if user.role != 'admin' and existing_model_ids: + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + writable_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=existing_model_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + else: + writable_model_ids = set(existing_model_ids) + for model_data in data: - # Here, you can add logic to validate model_data if needed - model_id = model_data.get("id") + model_id = model_data.get('id') if model_id and is_valid_model_id(model_id): - existing_model = Models.get_model_by_id(model_id, db=db) + existing_model = existing_models.get(model_id) if existing_model: - # Update existing model - model_data["meta"] = model_data.get("meta", {}) - model_data["params"] = model_data.get("params", {}) + # Enforce ownership/write-access before allowing overwrite + if ( + user.role != 'admin' + and existing_model.user_id != user.id + and model_id not in writable_model_ids + ): + log.warning( + 'import_models: user %s skipped model %s (no write access)', + user.id, + model_id, + ) + continue - updated_model = ModelForm( - **{**existing_model.model_dump(), **model_data} - ) - Models.update_model_by_id(model_id, updated_model, db=db) + # Update existing model + model_data['meta'] = model_data.get('meta', {}) + model_data['params'] = model_data.get('params', {}) + + updated_model = ModelForm(**{**existing_model.model_dump(), **model_data}) + # Only filter access_grants when explicitly provided + # in the payload to avoid altering existing ACLs on + # metadata-only imports. + if 'access_grants' in model_data: + updated_model.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + updated_model.access_grants, + 'sharing.public_models', + ) + await Models.update_model_by_id(model_id, updated_model, db=db) else: # Insert new model - model_data["meta"] = model_data.get("meta", {}) - model_data["params"] = model_data.get("params", {}) + model_data['meta'] = model_data.get('meta', {}) + model_data['params'] = model_data.get('params', {}) new_model = ModelForm(**model_data) - Models.insert_new_model( - user_id=user.id, form_data=new_model, db=db + new_model.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + new_model.access_grants, + 'sharing.public_models', ) + await Models.insert_new_model(user_id=user.id, form_data=new_model, db=db) return True else: - raise HTTPException(status_code=400, detail="Invalid JSON format") + raise HTTPException(status_code=400, detail='Invalid JSON format') except Exception as e: log.exception(e) raise HTTPException(status_code=500, detail=str(e)) @@ -286,14 +387,14 @@ class SyncModelsForm(BaseModel): models: list[ModelModel] = [] -@router.post("/sync", response_model=list[ModelModel]) +@router.post('/sync', response_model=list[ModelModel]) async def sync_models( request: Request, form_data: SyncModelsForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - return Models.sync_models(user.id, form_data.models, db=db) + return await Models.sync_models(user.id, form_data.models, db=db) ########################### @@ -306,24 +407,41 @@ class ModelIdForm(BaseModel): # Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id -@router.get("/model", response_model=Optional[ModelAccessResponse]) -async def get_model_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - model = Models.get_model_by_id(id, db=db) +@router.get('/model', response_model=ModelAccessResponse | None) +async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + model = await Models.get_model_by_id(id, db=db) if model: - if ( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or model.user_id == user.id - or has_access(user.id, "read", model.access_control, db=db) + write_access = ( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + ) + + if write_access or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='read', + db=db, ): + model_dict = model.model_dump() + # Strip params (system prompt and other admin-curated config) + # for read-only callers — matches the params strip already + # enforced on /api/models in utils/models.py. Owners, admins + # under BYPASS_ADMIN_ACCESS_CONTROL, and write-grant holders + # still receive the full object so the workspace edit UI keeps + # working for users who legitimately curate the model. + if not write_access: + model_dict['params'] = {} return ModelAccessResponse( - **model.model_dump(), - write_access=( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == model.user_id - or has_access(user.id, "write", model.access_control, db=db) - ), + **model_dict, + write_access=write_access, ) else: raise HTTPException( @@ -342,43 +460,84 @@ async def get_model_by_id( ########################### -@router.get("/model/profile/image") -def get_model_profile_image( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) +@router.get('/model/profile/image') +async def get_model_profile_image( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), ): - model = Models.get_model_by_id(id, db=db) + profile_image_url = None + updated_at = None - if model: - etag = f'"{model.updated_at}"' if model.updated_at else None + # First, check the database for regular models + model_meta = await Models.get_model_meta_by_id(id, db=db) + if model_meta: + meta, updated_at = model_meta + profile_image_url = (meta or {}).get('profile_image_url') - if model.meta.profile_image_url: - if model.meta.profile_image_url.startswith("http"): + # Fallback: check arena models stored in config (not in the DB) + if not profile_image_url: + arena_models = getattr( + getattr(request.app.state, 'config', None), + 'EVALUATION_ARENA_MODELS', + [], + ) + for arena_model in arena_models: + if arena_model.get('id') == id: + profile_image_url = arena_model.get('meta', {}).get('profile_image_url') + break + + if profile_image_url: + if profile_image_url.startswith('http'): + if ENABLE_PROFILE_IMAGE_URL_FORWARDING: return Response( status_code=status.HTTP_302_FOUND, - headers={"Location": model.meta.profile_image_url}, + headers={'Location': profile_image_url}, ) - elif model.meta.profile_image_url.startswith("data:image"): - try: - header, base64_data = model.meta.profile_image_url.split(",", 1) - image_data = base64.b64decode(base64_data) - image_buffer = io.BytesIO(image_data) - media_type = header.split(";")[0].lstrip("data:") + # When forwarding is disabled, fall through to the + # default image to prevent client-side IP/UA/Referer + # leaks via 302 redirect to external origins. + elif profile_image_url.startswith('data:image'): + try: + header, base64_data = profile_image_url.split(',', 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + media_type = header.split(';')[0].lstrip('data:').lower() - headers = {"Content-Disposition": "inline"} - if etag: - headers["ETag"] = etag - - return StreamingResponse( - image_buffer, - media_type=media_type, - headers=headers, + # only serve known-safe raster types inline; reject SVG/unknown (can run script on our origin) + if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: + return RedirectResponse( + url='/static/favicon.png', + status_code=status.HTTP_302_FOUND, ) - except Exception as e: - pass - return FileResponse(f"{STATIC_DIR}/favicon.png") - else: - return FileResponse(f"{STATIC_DIR}/favicon.png") + headers = { + 'Content-Disposition': 'inline', + 'X-Content-Type-Options': 'nosniff', + } + if updated_at: + headers['ETag'] = f'"{updated_at}"' + + return StreamingResponse( + image_buffer, + media_type=media_type, + headers=headers, + ) + except Exception: + pass + else: + safe_static = _safe_static_redirect_path(profile_image_url) + if safe_static: + return RedirectResponse( + url=safe_static, + status_code=status.HTTP_302_FOUND, + ) + + return RedirectResponse( + url='/static/favicon.png', + status_code=status.HTTP_302_FOUND, + ) ############################ @@ -386,25 +545,29 @@ def get_model_profile_image( ############################ -@router.post("/model/toggle", response_model=Optional[ModelResponse]) -async def toggle_model_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - model = Models.get_model_by_id(id, db=db) +@router.post('/model/toggle', response_model=ModelResponse | None) +async def toggle_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + model = await Models.get_model_by_id(id, db=db) if model: if ( - user.role == "admin" + user.role == 'admin' or model.user_id == user.id - or has_access(user.id, "write", model.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) ): - model = Models.toggle_model_by_id(id, db=db) + model = await Models.toggle_model_by_id(id, db=db) if model: return model else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + detail=ERROR_MESSAGES.DEFAULT('Error updating function'), ) else: raise HTTPException( @@ -423,47 +586,133 @@ async def toggle_model_by_id( ############################ -@router.post("/model/update", response_model=Optional[ModelModel]) +@router.post('/model/update', response_model=ModelModel | None) async def update_model_by_id( - form_data: ModelForm, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + request: Request, form_data: ModelForm, + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - model = Models.get_model_by_id(form_data.id, db=db) + """Update a workspace model's configuration.""" + model = await Models.get_model_by_id(form_data.id, db=db) if not model: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, ) if ( model.user_id != user.id - and not has_access(user.id, "write", model.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - model = Models.update_model_by_id( - form_data.id, ModelForm(**form_data.model_dump()), db=db + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_models', ) + + model = await Models.update_model_by_id(form_data.id, ModelForm(**form_data.model_dump()), db=db) return model +############################ +# UpdateModelAccessById +############################ + + +class ModelAccessGrantsForm(BaseModel): + id: str + name: str | None = None + access_grants: list[dict] + + +@router.post('/model/access/update', response_model=ModelModel | None) +async def update_model_access_by_id( + request: Request, + form_data: ModelAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + model = await Models.get_model_by_id(form_data.id, db=db) + + # Non-preset models (e.g. direct Ollama/OpenAI models) may not have a DB + # entry yet. Create a minimal one so access grants can be stored. + if not model: + if user.role != 'admin': + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + model = await Models.insert_new_model( + ModelForm( + id=form_data.id, + name=form_data.name or form_data.id, + meta=ModelMeta(), + params=ModelParams(), + ), + user.id, + db=db, + ) + if not model: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT('Error creating model entry'), + ) + + if ( + model.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_models', + ) + + await AccessGrants.set_access_grants('model', form_data.id, form_data.access_grants, db=db) + + await Models.update_model_updated_at_by_id(form_data.id, db=db) + + return await Models.get_model_by_id(form_data.id, db=db) + + ############################ # DeleteModelById ############################ -@router.post("/model/delete", response_model=bool) +@router.post('/model/delete', response_model=bool) async def delete_model_by_id( form_data: ModelIdForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - model = Models.get_model_by_id(form_data.id, db=db) + model = await Models.get_model_by_id(form_data.id, db=db) if not model: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -471,22 +720,26 @@ async def delete_model_by_id( ) if ( - user.role != "admin" + user.role != 'admin' and model.user_id != user.id - and not has_access(user.id, "write", model.access_control, db=db) + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - result = Models.delete_model_by_id(form_data.id, db=db) + result = await Models.delete_model_by_id(form_data.id, db=db) return result -@router.delete("/delete/all", response_model=bool) -async def delete_all_models( - user=Depends(get_admin_user), db: Session = Depends(get_session) -): - result = Models.delete_all_models(db=db) +@router.delete('/delete/all', response_model=bool) +async def delete_all_models(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + result = await Models.delete_all_models(db=db) return result diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 56730e2b6a..6dccc73f6d 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -2,39 +2,47 @@ import json import logging from typing import Optional - -from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks -from pydantic import BaseModel - -from open_webui.socket.main import sio - -from open_webui.models.groups import Groups -from open_webui.models.users import Users, UserResponse -from open_webui.models.notes import ( - NoteListResponse, - Notes, - NoteModel, - NoteForm, - NoteUserResponse, -) - +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status from open_webui.config import ( BYPASS_ADMIN_ACCESS_CONTROL, ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_EXPORT, ) from open_webui.constants import ERROR_MESSAGES - - +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.notes import ( + NoteForm, + NoteListResponse, + NoteModel, + Notes, + NoteUserResponse, +) +from open_webui.models.users import UserResponse, Users +from open_webui.socket.main import sio +from open_webui.utils.access_control import ( + filter_allowed_access_grants, + has_permission, + has_public_read_access_grant, + has_public_write_access_grant, +) from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access, has_permission -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) router = APIRouter() + +def _truncate_note_data(data: Optional[dict], max_length: int = 1000) -> Optional[dict]: + if not data: + return data + md = (data.get('content') or {}).get('md') or '' + return {'content': {'md': md[:max_length]}} + + ############################ # GetNotes ############################ @@ -44,20 +52,21 @@ class NoteItemResponse(BaseModel): id: str title: str data: Optional[dict] + is_pinned: Optional[bool] = False updated_at: int created_at: int user: Optional[UserResponse] = None -@router.get("/", response_model=list[NoteItemResponse]) +@router.get('/', response_model=list[NoteItemResponse]) async def get_notes( request: Request, page: Optional[int] = None, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -70,18 +79,22 @@ async def get_notes( limit = 60 skip = (page - 1) * limit - notes = Notes.get_notes_by_user_id(user.id, "read", skip=skip, limit=limit, db=db) + notes = await Notes.get_notes_by_user_id(user.id, 'read', skip=skip, limit=limit, db=db) if not notes: return [] user_ids = list(set(note.user_id for note in notes)) - users = {user.id: user for user in Users.get_users_by_user_ids(user_ids, db=db)} + users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} + + pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) return [ NoteUserResponse( **{ **note.model_dump(), - "user": UserResponse(**users[note.user_id].model_dump()), + 'is_pinned': note.id in pinned_note_ids, + 'data': _truncate_note_data(note.data), + 'user': UserResponse(**users[note.user_id].model_dump()), } ) for note in notes @@ -89,7 +102,47 @@ async def get_notes( ] -@router.get("/search", response_model=NoteListResponse) +############################ +# GetPinnedNotes +############################ + + +@router.get('/pinned', response_model=list[NoteItemResponse]) +async def get_pinned_notes( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + notes = await Notes.get_pinned_notes_by_user_id(user.id, 'read', db=db) + if not notes: + return [] + + user_ids = list(set(note.user_id for note in notes)) + users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)} + + return [ + NoteUserResponse( + **{ + **note.model_dump(), + 'is_pinned': True, + 'data': _truncate_note_data(note.data), + 'user': UserResponse(**users[note.user_id].model_dump()), + } + ) + for note in notes + if note.user_id in users + ] + + +@router.get('/search', response_model=NoteListResponse) async def search_notes( request: Request, query: Optional[str] = None, @@ -99,10 +152,10 @@ async def search_notes( direction: Optional[str] = None, page: Optional[int] = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -117,24 +170,29 @@ async def search_notes( filter = {} if query: - filter["query"] = query + filter['query'] = query if view_option: - filter["view_option"] = view_option + filter['view_option'] = view_option if permission: - filter["permission"] = permission + filter['permission'] = permission if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: - groups = Groups.get_groups_by_member_id(user.id, db=db) + if not user.role == 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL: + groups = await Groups.get_groups_by_member_id(user.id, db=db) if groups: - filter["group_ids"] = [group.id for group in groups] + filter['group_ids'] = [group.id for group in groups] - filter["user_id"] = user.id + filter['user_id'] = user.id - return Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db) + result = await Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db) + pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) + for note in result.items: + note.is_pinned = note.id in pinned_note_ids + note.data = _truncate_note_data(note.data) + return result ############################ @@ -142,29 +200,36 @@ async def search_notes( ############################ -@router.post("/create", response_model=Optional[NoteModel]) +@router.post('/create', response_model=Optional[NoteModel]) async def create_new_note( request: Request, form_data: NoteForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_notes', + db=db, + ) + try: - note = Notes.insert_new_note(user.id, form_data, db=db) + note = await Notes.insert_new_note(user.id, form_data, db=db) return note except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) ############################ @@ -176,52 +241,57 @@ class NoteResponse(NoteModel): write_access: bool = False -@router.get("/{id}", response_model=Optional[NoteResponse]) +@router.get('/{id}', response_model=Optional[NoteResponse]) async def get_note_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - note = Notes.get_note_by_id(id, db=db) + note = await Notes.get_note_by_id(id, db=db) if not note: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if user.role != "admin" and ( + if user.role != 'admin' and ( user.id != note.user_id and ( - not has_access( - user.id, type="read", access_control=note.access_control, db=db + not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + db=db, ) ) ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) write_access = ( - user.role == "admin" + user.role == 'admin' or (user.id == note.user_id) - or has_access( - user.id, - type="write", - access_control=note.access_control, - strict=False, + or await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', db=db, ) + or has_public_write_access_grant(note.access_grants) ) - return NoteResponse(**note.model_dump(), write_access=write_access) + pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) + return NoteResponse( + **{**note.model_dump(), 'is_pinned': note.id in pinned_note_ids}, + write_access=write_access, + ) ############################ @@ -229,108 +299,202 @@ async def get_note_by_id( ############################ -@router.post("/{id}/update", response_model=Optional[NoteModel]) +@router.post('/{id}/update', response_model=Optional[NoteModel]) async def update_note_by_id( request: Request, id: str, form_data: NoteForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - note = Notes.get_note_by_id(id, db=db) + note = await Notes.get_note_by_id(id, db=db) if not note: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if user.role != "admin" and ( + if user.role != 'admin' and ( user.id != note.user_id - and not has_access( - user.id, type="write", access_control=note.access_control, db=db - ) - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) - - # Check if user can share publicly - if ( - user.role != "admin" - and form_data.access_control == None - and not has_permission( - user.id, - "sharing.public_notes", - request.app.state.config.USER_PERMISSIONS, + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', db=db, ) ): - form_data.access_control = {} + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_notes', + db=db, + ) try: - note = Notes.update_note_by_id(id, form_data, db=db) + note = await Notes.update_note_by_id(id, form_data, db=db) + pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) + note.is_pinned = note.id in pinned_note_ids + await sio.emit( - "note-events", + 'note-events', note.model_dump(), - to=f"note:{note.id}", + to=f'note:{note.id}', ) return note except Exception as e: log.exception(e) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) + + +############################ +# UpdateNoteAccessById +############################ + + +class NoteAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/{id}/access/update', response_model=Optional[NoteModel]) +async def update_note_access_by_id( + request: Request, + id: str, + form_data: NoteAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, ) + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_notes', + ) + + await AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db) + + note = await Notes.get_note_by_id(id, db=db) + pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) + note.is_pinned = note.id in pinned_note_ids + return note + + +############################ +# PinNoteById +############################ + + +@router.post('/{id}/pin', response_model=Optional[NoteModel]) +async def pin_note_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + note = await Notes.get_note_by_id(id, db=db) + if not note: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) + + if user.role != 'admin' and ( + user.id != note.user_id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + db=db, + ) + ): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) + + note = await Notes.toggle_note_pinned_by_id(id, user.id, db=db) + pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db) + note.is_pinned = note.id in pinned_note_ids + return note + ############################ # DeleteNoteById ############################ -@router.delete("/{id}/delete", response_model=bool) +@router.delete('/{id}/delete', response_model=bool) async def delete_note_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( - user.id, "features.notes", request.app.state.config.USER_PERMISSIONS, db=db + if user.role != 'admin' and not await has_permission( + user.id, 'features.notes', request.app.state.config.USER_PERMISSIONS, db=db ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - note = Notes.get_note_by_id(id, db=db) + note = await Notes.get_note_by_id(id, db=db) if not note: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) - if user.role != "admin" and ( + if user.role != 'admin' and ( user.id != note.user_id - and not has_access( - user.id, type="write", access_control=note.access_control, db=db + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='write', + db=db, ) ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()) try: - note = Notes.delete_note_by_id(id, db=db) + note = await Notes.delete_note_by_id(id, db=db) return True except Exception as e: log.exception(e) - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() - ) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 0af35de38f..e315121ac4 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -1,6 +1,4 @@ -# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances. -# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin, -# least connections, or least response time for better resource utilization and performance optimization. +from __future__ import annotations import asyncio import json @@ -10,196 +8,169 @@ import random import re import time from datetime import datetime - from typing import Optional, Union from urllib.parse import urlparse + import aiohttp from aiocache import cached -import requests - -from open_webui.utils.headers import include_user_info_headers -from open_webui.models.chats import Chats -from open_webui.models.users import UserModel - -from open_webui.env import ( - ENABLE_FORWARD_USER_INFO_HEADERS, -) - -from fastapi import ( - Depends, - FastAPI, - File, - HTTPException, - Request, - UploadFile, - APIRouter, -) -from fastapi.middleware.cors import CORSMiddleware +from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile from fastapi.responses import StreamingResponse from pydantic import BaseModel, ConfigDict, validator -from starlette.background import BackgroundTask -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession -from open_webui.internal.db import get_session - - -from open_webui.models.models import Models -from open_webui.utils.misc import ( - calculate_sha256, +from open_webui.config import UPLOAD_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + BYPASS_MODEL_ACCESS_CONTROL, + ENABLE_FORWARD_USER_INFO_HEADERS, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + MODELS_CACHE_TTL, ) +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.models import Models +from open_webui.models.users import UserModel +from open_webui.utils.access_control import check_model_access +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.misc import calculate_sha256 from open_webui.utils.payload import ( apply_model_params_to_body_ollama, apply_model_params_to_body_openai, apply_system_prompt_to_body, ) -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access - - -from open_webui.config import ( - UPLOAD_DIR, -) -from open_webui.env import ( - ENV, - MODELS_CACHE_TTL, - AIOHTTP_CLIENT_SESSION_SSL, - AIOHTTP_CLIENT_TIMEOUT, - AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, - BYPASS_MODEL_ACCESS_CONTROL, -) -from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.session_pool import cleanup_response, get_session, stream_wrapper log = logging.getLogger(__name__) - -########################################## -# -# Utility functions -# -########################################## +# Headers that become stale after aiohttp auto-decompresses the upstream +# response body. Forwarding them verbatim causes desktop / programmatic +# clients to attempt decompression of an already-decoded payload, resulting +# in ZlibError. See https://github.com/aio-libs/aiohttp/issues/4462. +_STRIP_PROXY_HEADERS = frozenset({'Content-Encoding', 'Content-Length', 'Transfer-Encoding'}) -async def send_get_request(url, key=None, user: UserModel = None): - timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) +def _clean_proxy_headers(raw_headers) -> dict: + """Return a copy of *raw_headers* with stale encoding headers removed.""" + return {k: v for k, v in raw_headers.items() if k not in _STRIP_PROXY_HEADERS} + + +async def send_get_request( + url: str, + key: str | None = None, + user: UserModel | None = None, +): + """Issue a GET request to an Ollama backend and return JSON, or *None* on failure.""" try: - async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), - } + session = await get_session() + headers: dict = { + 'Content-Type': 'application/json', + } + if key: + headers['Authorization'] = f'Bearer {key}' + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - async with session.get( - url, - headers=headers, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as response: - return await response.json() - except Exception as e: - # Handle connection error here - log.error(f"Connection error: {e}") + async with session.get( + url, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) as r: + return await r.json() + except Exception as exc: + log.error(f'Connection error: {exc}') return None -async def cleanup_response( - response: Optional[aiohttp.ClientResponse], - session: Optional[aiohttp.ClientSession], -): - if response: - response.close() - if session: - await session.close() - - -async def send_post_request( +async def send_request( url: str, - payload: Union[str, bytes], - stream: bool = True, - key: Optional[str] = None, - content_type: Optional[str] = None, + method: str = 'POST', + *, + payload: Union[str, bytes | None] = None, + key: str | None = None, user: UserModel = None, - metadata: Optional[dict] = None, + stream: bool = False, + content_type: str | None = None, + metadata: dict | None = None, ): - r = None + streaming = False try: - session = aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) + session = await get_session() headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), + 'Content-Type': 'application/json', + **({'Authorization': f'Bearer {key}'} if key else {}), } if ENABLE_FORWARD_USER_INFO_HEADERS and user: headers = include_user_info_headers(headers, user) - if metadata and metadata.get("chat_id"): - headers["X-OpenWebUI-Chat-Id"] = metadata.get("chat_id") + if metadata and metadata.get('chat_id'): + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id') - r = await session.post( + r = await session.request( + method, url, data=payload, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), ) - if r.ok is False: + if not r.ok: try: res = await r.json() - await cleanup_response(r, session) - if "error" in res: - raise HTTPException(status_code=r.status, detail=res["error"]) - except HTTPException as e: - raise e # Re-raise HTTPException to be handled by FastAPI + if 'error' in res: + raise HTTPException(status_code=r.status, detail=res['error']) + except HTTPException: + raise except Exception as e: - log.error(f"Failed to parse error response: {e}") - raise HTTPException( - status_code=r.status, - detail=f"Open WebUI: Server Connection Error", - ) + log.error(f'Failed to parse error response: {e}') + raise HTTPException( + status_code=r.status, + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + + r.raise_for_status() - r.raise_for_status() # Raises an error for bad responses (4xx, 5xx) if stream: - response_headers = dict(r.headers) - + response_headers = _clean_proxy_headers(r.headers) if content_type: - response_headers["Content-Type"] = content_type + response_headers['Content-Type'] = content_type + streaming = True return StreamingResponse( - r.content, + stream_wrapper(r), status_code=r.status, headers=response_headers, - background=BackgroundTask( - cleanup_response, response=r, session=session - ), ) else: - res = await r.json() - return res + try: + return await r.json() + except Exception: + return None - except HTTPException as e: - raise e # Re-raise HTTPException to be handled by FastAPI + except HTTPException: + raise except Exception as e: - detail = f"Ollama: {e}" - raise HTTPException( status_code=r.status if r else 500, - detail=detail if e else "Open WebUI: Server Connection Error", + detail=f'Ollama: {e}' if str(e) else ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: - if not stream: - await cleanup_response(r, session) + if not streaming: + await cleanup_response(r) def get_api_key(idx, url, configs): parsed_url = urlparse(url) - base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" - return configs.get(str(idx), configs.get(base_url, {})).get( - "key", None - ) # Legacy support + base_url = f'{parsed_url.scheme}://{parsed_url.netloc}' + return configs.get(str(idx), configs.get(base_url, {})).get('key', None) # Legacy support ########################################## @@ -211,454 +182,340 @@ def get_api_key(idx, url, configs): router = APIRouter() -@router.head("/") -@router.get("/") -async def get_status(): - return {"status": True} +@router.head('/') +@router.get('/') +async def get_status() -> dict: + """Health-check endpoint.""" + return {'status': True} class ConnectionVerificationForm(BaseModel): url: str - key: Optional[str] = None + key: str | None = None -@router.post("/verify") +@router.post('/verify') async def verify_connection( - form_data: ConnectionVerificationForm, user=Depends(get_admin_user) + form_data: ConnectionVerificationForm, + user=Depends(get_admin_user), ): - url = form_data.url - key = form_data.key + """Verify that an Ollama backend at *form_data.url* is reachable.""" + try: + session = await get_session() + headers: dict = {} + if form_data.key: + headers['Authorization'] = f'Bearer {form_data.key}' + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - async with aiohttp.ClientSession( - trust_env=True, - timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), - ) as session: - try: - headers = { - **({"Authorization": f"Bearer {key}"} if key else {}), - } + async with session.get( + f'{form_data.url}/api/version', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) as r: + if r.status != 200: + detail = f'HTTP Error: {r.status}' + res = await r.json() + if 'error' in res: + detail = f'External Error: {res["error"]}' + raise Exception(detail) - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - async with session.get( - f"{url}/api/version", - headers=headers, - ssl=AIOHTTP_CLIENT_SESSION_SSL, - ) as r: - if r.status != 200: - detail = f"HTTP Error: {r.status}" - res = await r.json() - - if "error" in res: - detail = f"External Error: {res['error']}" - raise Exception(detail) - - data = await r.json() - return data - except aiohttp.ClientError as e: - log.exception(f"Client error: {str(e)}") - raise HTTPException( - status_code=500, detail="Open WebUI: Server Connection Error" - ) - except Exception as e: - log.exception(f"Unexpected error: {e}") - error_detail = f"Unexpected error: {str(e)}" - raise HTTPException(status_code=500, detail=error_detail) + return await r.json() + except aiohttp.ClientError as exc: + log.exception(f'Client error: {exc}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + except Exception as exc: + log.exception(f'Unexpected error: {exc}') + raise HTTPException(status_code=500, detail=f'Unexpected error: {exc}') -@router.get("/config") -async def get_config(request: Request, user=Depends(get_admin_user)): +@router.get('/config') +async def get_config( + request: Request, + user=Depends(get_admin_user), +) -> dict: + """Return the current Ollama connection configuration.""" return { - "ENABLE_OLLAMA_API": request.app.state.config.ENABLE_OLLAMA_API, - "OLLAMA_BASE_URLS": request.app.state.config.OLLAMA_BASE_URLS, - "OLLAMA_API_CONFIGS": request.app.state.config.OLLAMA_API_CONFIGS, + 'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API, + 'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS, + 'OLLAMA_API_CONFIGS': request.app.state.config.OLLAMA_API_CONFIGS, } class OllamaConfigForm(BaseModel): - ENABLE_OLLAMA_API: Optional[bool] = None + """Payload for updating the Ollama connection configuration.""" + + ENABLE_OLLAMA_API: bool | None = None OLLAMA_BASE_URLS: list[str] OLLAMA_API_CONFIGS: dict -@router.post("/config/update") +@router.post('/config/update') async def update_config( - request: Request, form_data: OllamaConfigForm, user=Depends(get_admin_user) -): + request: Request, + form_data: OllamaConfigForm, + user=Depends(get_admin_user), +) -> dict: + """Persist updated Ollama connection settings.""" request.app.state.config.ENABLE_OLLAMA_API = form_data.ENABLE_OLLAMA_API - request.app.state.config.OLLAMA_BASE_URLS = form_data.OLLAMA_BASE_URLS request.app.state.config.OLLAMA_API_CONFIGS = form_data.OLLAMA_API_CONFIGS - # Remove the API configs that are not in the API URLS - keys = list(map(str, range(len(request.app.state.config.OLLAMA_BASE_URLS)))) + # Prune stale config entries that no longer map to a URL index + valid_keys = {str(i) for i in range(len(request.app.state.config.OLLAMA_BASE_URLS))} request.app.state.config.OLLAMA_API_CONFIGS = { - key: value - for key, value in request.app.state.config.OLLAMA_API_CONFIGS.items() - if key in keys + k: v for k, v in request.app.state.config.OLLAMA_API_CONFIGS.items() if k in valid_keys } return { - "ENABLE_OLLAMA_API": request.app.state.config.ENABLE_OLLAMA_API, - "OLLAMA_BASE_URLS": request.app.state.config.OLLAMA_BASE_URLS, - "OLLAMA_API_CONFIGS": request.app.state.config.OLLAMA_API_CONFIGS, + 'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API, + 'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS, + 'OLLAMA_API_CONFIGS': request.app.state.config.OLLAMA_API_CONFIGS, } -def merge_ollama_models_lists(model_lists): - merged_models = {} +def merge_models_lists(model_lists) -> list[dict]: + """De-duplicate model entries across multiple Ollama backends, tracking which URL index hosts each model.""" + merged: dict[str, dict] = {} + for idx, entries in enumerate(model_lists): + if entries is None: + continue + for entry in entries: + model_id = entry.get('model') + if model_id is None: + continue + if model_id not in merged: + entry['urls'] = [idx] + merged[model_id] = entry + else: + merged[model_id]['urls'].append(idx) + return list(merged.values()) - for idx, model_list in enumerate(model_lists): - if model_list is not None: - for model in model_list: - id = model.get("model") - if id is not None: - if id not in merged_models: - model["urls"] = [idx] - merged_models[id] = model - else: - merged_models[id]["urls"].append(idx) - return list(merged_models.values()) +def _resolve_api_config(request: Request, idx: int, url: str) -> dict: + """Look up the API config for a backend by numeric index, falling back to URL key (legacy).""" + api_configs = request.app.state.config.OLLAMA_API_CONFIGS + return api_configs.get(str(idx), api_configs.get(url, {})) @cached( ttl=MODELS_CACHE_TTL, - key=lambda _, user: f"ollama_all_models_{user.id}" if user else "ollama_all_models", + key=lambda _, user: f'ollama_all_models_{user.id}' if user else 'ollama_all_models', ) -async def get_all_models(request: Request, user: UserModel = None): - log.info("get_all_models()") - if request.app.state.config.ENABLE_OLLAMA_API: - request_tasks = [] - for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): - if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and ( - url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support - ): - request_tasks.append(send_get_request(f"{url}/api/tags", user=user)) - else: - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(idx), - request.app.state.config.OLLAMA_API_CONFIGS.get( - url, {} - ), # Legacy support - ) +async def get_all_models(request: Request, user: UserModel | None = None): + """Aggregate model tags from every enabled Ollama backend.""" + log.info('get_all_models()') - enable = api_config.get("enable", True) - key = api_config.get("key", None) + if not request.app.state.config.ENABLE_OLLAMA_API: + models_dict: dict = {'models': []} + request.app.state.OLLAMA_MODELS = {} + return models_dict - if enable: - request_tasks.append( - send_get_request(f"{url}/api/tags", key, user=user) - ) - else: - request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) + # Fan-out tag requests to every backend + tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + api_config = _resolve_api_config(request, idx, url) + if not api_config: + tasks.append(send_get_request(f'{url}/api/tags', user=user)) + elif api_config.get('enable', True): + tasks.append(send_get_request(f'{url}/api/tags', api_config.get('key'), user=user)) + else: + tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) - responses = await asyncio.gather(*request_tasks) + responses = await asyncio.gather(*tasks) - for idx, response in enumerate(responses): - if response: - url = request.app.state.config.OLLAMA_BASE_URLS[idx] - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(idx), - request.app.state.config.OLLAMA_API_CONFIGS.get( - url, {} - ), # Legacy support - ) + # Post-process each response: apply prefix_id, tags, model filtering + for idx, response in enumerate(responses): + if not response: + continue + url = request.app.state.config.OLLAMA_BASE_URLS[idx] + api_config = _resolve_api_config(request, idx, url) - connection_type = api_config.get("connection_type", "local") + connection_type = api_config.get('connection_type', 'local') + prefix_id = api_config.get('prefix_id') + allowed_tags = api_config.get('tags', []) + allowed_model_ids = api_config.get('model_ids', []) - prefix_id = api_config.get("prefix_id", None) - tags = api_config.get("tags", []) - model_ids = api_config.get("model_ids", []) + if allowed_model_ids and 'models' in response: + response['models'] = [ + m for m in response['models'] if m['model'] in allowed_model_ids + ] - if len(model_ids) != 0 and "models" in response: - response["models"] = list( - filter( - lambda model: model["model"] in model_ids, - response["models"], - ) - ) + for m in response.get('models', []): + if prefix_id: + m['model'] = f'{prefix_id}.{m["model"]}' + if allowed_tags: + m['tags'] = allowed_tags + if connection_type: + m['connection_type'] = connection_type - for model in response.get("models", []): - if prefix_id: - model["model"] = f"{prefix_id}.{model['model']}" - - if tags: - model["tags"] = tags - - if connection_type: - model["connection_type"] = connection_type - - models = { - "models": merge_ollama_models_lists( - map( - lambda response: response.get("models", []) if response else None, - responses, - ) - ) - } - - try: - loaded_models = await get_ollama_loaded_models(request, user=user) - expires_map = { - m["model"]: m["expires_at"] - for m in loaded_models["models"] - if "expires_at" in m - } - - for m in models["models"]: - if m["model"] in expires_map: - # Parse ISO8601 datetime with offset, get unix timestamp as int - dt = datetime.fromisoformat(expires_map[m["model"]]) - m["expires_at"] = int(dt.timestamp()) - except Exception as e: - log.debug(f"Failed to get loaded models: {e}") - - else: - models = {"models": []} - - request.app.state.OLLAMA_MODELS = { - model["model"]: model for model in models["models"] + models_dict = { + 'models': merge_models_lists( + r.get('models', []) if r else None for r in responses + ) } - return models + + # Annotate with expiry info from loaded-model state + try: + loaded = await get_ollama_loaded_models(request, user=user) + expires_map = {m['model']: m['expires_at'] for m in loaded['models'] if 'expires_at' in m} + for m in models_dict['models']: + if m['model'] in expires_map: + dt = datetime.fromisoformat(expires_map[m['model']]) + m['expires_at'] = int(dt.timestamp()) + except Exception as exc: + log.debug(f'Failed to get loaded models: {exc}') + + request.app.state.OLLAMA_MODELS = {m['model']: m for m in models_dict['models']} + return models_dict async def get_filtered_models(models, user, db=None): - # Filter models based on user access control - filtered_models = [] - for model in models.get("models", []): - model_info = Models.get_model_by_id(model["model"], db=db) - if model_info: - if user.id == model_info.user_id or has_access( - user.id, type="read", access_control=model_info.access_control, db=db - ): - filtered_models.append(model) - return filtered_models + """Return only the models the given *user* is allowed to access.""" + model_ids = [m['model'] for m in models.get('models', [])] + model_infos = {mi.id: mi for mi in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {g.id for g in await Groups.get_groups_by_member_id(user.id, db=db)} + + accessible_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + return [ + m for m in models.get('models', []) + if (mi := model_infos.get(m['model'])) and (user.id == mi.user_id or mi.id in accessible_ids) + ] -@router.get("/api/tags") -@router.get("/api/tags/{url_idx}") +@router.get('/api/tags') +@router.get('/api/tags/{url_idx}') async def get_ollama_tags( - request: Request, url_idx: Optional[int] = None, user=Depends(get_verified_user) + request: Request, + url_idx: int | None = None, + user=Depends(get_verified_user), ): - models = [] + """List Ollama model tags, optionally from a specific backend.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) if url_idx is None: - models = await get_all_models(request, user=user) + result = await get_all_models(request, user=user) else: url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + result = await send_request(f'{url}/api/tags', 'GET', key=key, user=user) - r = None - try: - headers = { - **({"Authorization": f"Bearer {key}"} if key else {}), - } + if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL: + result['models'] = await get_filtered_models(result, user) - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.request( - method="GET", - url=f"{url}/api/tags", - headers=headers, - ) - r.raise_for_status() - - models = r.json() - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" - - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL: - models["models"] = await get_filtered_models(models, user) - - return models + return result -@router.get("/api/ps") -async def get_ollama_loaded_models(request: Request, user=Depends(get_admin_user)): - """ - List models that are currently loaded into Ollama memory, and which node they are loaded on. - """ - if request.app.state.config.ENABLE_OLLAMA_API: - request_tasks = [] - for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): - if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and ( - url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support - ): - request_tasks.append(send_get_request(f"{url}/api/ps", user=user)) - else: - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(idx), - request.app.state.config.OLLAMA_API_CONFIGS.get( - url, {} - ), # Legacy support - ) +@router.get('/api/ps') +async def get_ollama_loaded_models( + request: Request, + user=Depends(get_admin_user), +) -> dict: + """List models currently loaded in Ollama memory across all backends.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + return {'models': []} - enable = api_config.get("enable", True) - key = api_config.get("key", None) - - if enable: - request_tasks.append( - send_get_request(f"{url}/api/ps", key, user=user) - ) - else: - request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) - - responses = await asyncio.gather(*request_tasks) - - for idx, response in enumerate(responses): - if response: - url = request.app.state.config.OLLAMA_BASE_URLS[idx] - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(idx), - request.app.state.config.OLLAMA_API_CONFIGS.get( - url, {} - ), # Legacy support - ) - - prefix_id = api_config.get("prefix_id", None) - - for model in response.get("models", []): - if prefix_id: - model["model"] = f"{prefix_id}.{model['model']}" - - models = { - "models": merge_ollama_models_lists( - map( - lambda response: response.get("models", []) if response else None, - responses, - ) - ) - } - else: - models = {"models": []} - - return models - - -@router.get("/api/version") -@router.get("/api/version/{url_idx}") -async def get_ollama_versions(request: Request, url_idx: Optional[int] = None): - if request.app.state.config.ENABLE_OLLAMA_API: - if url_idx is None: - # returns lowest version - request_tasks = [] - - for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(idx), - request.app.state.config.OLLAMA_API_CONFIGS.get( - url, {} - ), # Legacy support - ) - - enable = api_config.get("enable", True) - key = api_config.get("key", None) - - if enable: - request_tasks.append( - send_get_request( - f"{url}/api/version", - key, - ) - ) - - responses = await asyncio.gather(*request_tasks) - responses = list(filter(lambda x: x is not None, responses)) - - if len(responses) > 0: - lowest_version = min( - responses, - key=lambda x: tuple( - map(int, re.sub(r"^v|-.*", "", x["version"]).split(".")) - ), - ) - - return {"version": lowest_version["version"]} - else: - raise HTTPException( - status_code=500, - detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND, - ) + tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + api_config = _resolve_api_config(request, idx, url) + if not api_config: + tasks.append(send_get_request(f'{url}/api/ps', user=user)) + elif api_config.get('enable', True): + tasks.append(send_get_request(f'{url}/api/ps', api_config.get('key'), user=user)) else: - url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) - r = None - try: - r = requests.request(method="GET", url=f"{url}/api/version") - r.raise_for_status() + responses = await asyncio.gather(*tasks) - return r.json() - except Exception as e: - log.exception(e) + for idx, response in enumerate(responses): + if not response: + continue + api_config = _resolve_api_config( + request.app.state.config, idx, request.app.state.config.OLLAMA_BASE_URLS[idx] + ) + prefix_id = api_config.get('prefix_id') + if prefix_id: + for m in response.get('models', []): + m['model'] = f'{prefix_id}.{m["model"]}' - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" + return { + 'models': merge_models_lists( + r.get('models', []) if r else None for r in responses + ) + } - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - else: - return {"version": False} + +@router.get('/api/version') +@router.get('/api/version/{url_idx}') +async def get_ollama_versions( + request: Request, + url_idx: int | None = None, +): + """Return the lowest Ollama version across all configured backends.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + return {'version': False} + + if url_idx is not None: + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + return await send_request(f'{url}/api/version', 'GET') + + # Fan-out to every enabled backend + tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), + ) + if api_config.get('enable', True): + tasks.append(send_get_request(f'{url}/api/version', api_config.get('key'))) + + raw = await asyncio.gather(*tasks) + valid = [r for r in raw if r is not None] + + if not valid: + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND) + + lowest = min( + valid, + key=lambda v: tuple(map(int, re.sub(r'^v|-.*', '', v['version']).split('.'))), + ) + return {'version': lowest['version']} class ModelNameForm(BaseModel): - model: Optional[str] = None - model_config = ConfigDict( - extra="allow", - ) + """Generic form carrying an optional model identifier.""" + + model: str | None = None + model_config = ConfigDict(extra='allow') -@router.post("/api/unload") +@router.post('/api/unload') async def unload_model( request: Request, form_data: ModelNameForm, user=Depends(get_admin_user), ): form_data = form_data.model_dump(exclude_none=True) - model_name = form_data.get("model", form_data.get("name")) + model = form_data.get('model', form_data.get('name')) - if not model_name: - raise HTTPException( - status_code=400, detail="Missing name of the model to unload." - ) + if not model: + raise HTTPException(status_code=400, detail='Missing name of the model to unload.') # Refresh/load models if needed, get mapping from name to URLs await get_all_models(request, user=user) models = request.app.state.OLLAMA_MODELS - # Canonicalize model name (if not supplied with version) - if ":" not in model_name: - model_name = f"{model_name}:latest" - - if model_name not in models: - raise HTTPException( - status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model_name) - ) - url_indices = models[model_name]["urls"] + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model)) + url_indices = models[model]['urls'] # Send unload to ALL url_indices results = [] @@ -670,572 +527,451 @@ async def unload_model( ) key = get_api_key(idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - prefix_id = api_config.get("prefix_id", None) - if prefix_id and model_name.startswith(f"{prefix_id}."): - model_name = model_name[len(f"{prefix_id}.") :] + prefix_id = api_config.get('prefix_id', None) + if prefix_id and model.startswith(f'{prefix_id}.'): + model = model[len(f'{prefix_id}.') :] - payload = {"model": model_name, "keep_alive": 0, "prompt": ""} + payload = {'model': model, 'keep_alive': 0, 'prompt': ''} try: - res = await send_post_request( - url=f"{url}/api/generate", + res = await send_request( + f'{url}/api/generate', payload=json.dumps(payload), - stream=False, key=key, user=user, ) - results.append({"url_idx": idx, "success": True, "response": res}) + results.append({'url_idx': idx, 'success': True, 'response': res}) except Exception as e: - log.exception(f"Failed to unload model on node {idx}: {e}") - errors.append({"url_idx": idx, "success": False, "error": str(e)}) + log.exception(f'Failed to unload model on node {idx}: {e}') + errors.append({'url_idx': idx, 'success': False, 'error': str(e)}) if len(errors) > 0: raise HTTPException( status_code=500, - detail=f"Failed to unload model on {len(errors)} nodes: {errors}", + detail=f'Failed to unload model on {len(errors)} nodes: {errors}', ) - return {"status": True} + return {'status': True} -@router.post("/api/pull") -@router.post("/api/pull/{url_idx}") +@router.post('/api/pull') +@router.post('/api/pull/{url_idx}') async def pull_model( request: Request, form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user), ): + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + form_data = form_data.model_dump(exclude_none=True) - form_data["model"] = form_data.get("model", form_data.get("name")) + form_data['model'] = form_data.get('model', form_data.get('name')) url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - log.info(f"url: {url}") + log.info(f'url: {url}') - # Admin should be able to pull models from any source - payload = {**form_data, "insecure": True} - - return await send_post_request( - url=f"{url}/api/pull", - payload=json.dumps(payload), + # Admins may pull from any registry + return await send_request( + f'{url}/api/pull', + payload=json.dumps({**form_data, 'insecure': True}), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), user=user, + stream=True, ) class PushModelForm(BaseModel): + """Payload for pushing a model to a registry.""" + model: str - insecure: Optional[bool] = None - stream: Optional[bool] = None + insecure: bool | None = None + stream: bool | None = None -@router.delete("/api/push") -@router.delete("/api/push/{url_idx}") +@router.delete('/api/push') +@router.delete('/api/push/{url_idx}') async def push_model( request: Request, form_data: PushModelForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): + """Push a local model to a remote registry.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + if url_idx is None: await get_all_models(request, user=user) models = request.app.state.OLLAMA_MODELS - - if form_data.model in models: - url_idx = models[form_data.model]["urls"][0] - else: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), - ) + if form_data.model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = models[form_data.model]['urls'][0] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - log.debug(f"url: {url}") + log.debug(f'url: {url}') - return await send_post_request( - url=f"{url}/api/push", + return await send_request( + f'{url}/api/push', payload=form_data.model_dump_json(exclude_none=True).encode(), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), user=user, + stream=True, ) class CreateModelForm(BaseModel): - model: Optional[str] = None - stream: Optional[bool] = None - path: Optional[str] = None + """Payload for creating a new model via Modelfile.""" - model_config = ConfigDict(extra="allow") + model: str | None = None + stream: bool | None = None + path: str | None = None + model_config = ConfigDict(extra='allow') -@router.post("/api/create") -@router.post("/api/create/{url_idx}") +@router.post('/api/create') +@router.post('/api/create/{url_idx}') async def create_model( request: Request, form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user), ): - log.debug(f"form_data: {form_data}") + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + log.debug(f'form_data: {form_data}') url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - return await send_post_request( - url=f"{url}/api/create", + return await send_request( + f'{url}/api/create', payload=form_data.model_dump_json(exclude_none=True).encode(), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), user=user, + stream=True, ) class CopyModelForm(BaseModel): + """Payload for duplicating an existing model under a new name.""" + source: str destination: str -@router.post("/api/copy") -@router.post("/api/copy/{url_idx}") +@router.post('/api/copy') +@router.post('/api/copy/{url_idx}') async def copy_model( request: Request, form_data: CopyModelForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): + """Duplicate an existing model under a new name.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + if url_idx is None: await get_all_models(request, user=user) models = request.app.state.OLLAMA_MODELS - - if form_data.source in models: - url_idx = models[form_data.source]["urls"][0] - else: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source), - ) + if form_data.source not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source)) + url_idx = models[form_data.source]['urls'][0] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - try: - headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), - } - - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.request( - method="POST", - url=f"{url}/api/copy", - headers=headers, - data=form_data.model_dump_json(exclude_none=True).encode(), - ) - r.raise_for_status() - - log.debug(f"r.text: {r.text}") - return True - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" - - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) + await send_request( + f'{url}/api/copy', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=key, + user=user, + ) + return True -@router.delete("/api/delete") -@router.delete("/api/delete/{url_idx}") +@router.delete('/api/delete') +@router.delete('/api/delete/{url_idx}') async def delete_model( request: Request, form_data: ModelNameForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): - form_data = form_data.model_dump(exclude_none=True) - form_data["model"] = form_data.get("model", form_data.get("name")) + """Remove a model from an Ollama backend.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) - model = form_data.get("model") + payload = form_data.model_dump(exclude_none=True) + payload['model'] = payload.get('model', payload.get('name')) + model = payload.get('model') if url_idx is None: await get_all_models(request, user=user) models = request.app.state.OLLAMA_MODELS - - if model in models: - url_idx = models[model]["urls"][0] - else: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model), - ) + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model)) + url_idx = models[model]['urls'][0] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - r = None - try: - headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), - } - - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.request( - method="DELETE", - url=f"{url}/api/delete", - headers=headers, - json=form_data, - ) - r.raise_for_status() - - log.debug(f"r.text: {r.text}") - return True - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" - - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) + await send_request( + f'{url}/api/delete', + 'DELETE', + payload=json.dumps(payload), + key=key, + user=user, + ) + return True -@router.post("/api/show") +@router.post('/api/show') async def show_model_info( - request: Request, form_data: ModelNameForm, user=Depends(get_verified_user) + request: Request, + form_data: ModelNameForm, + user=Depends(get_verified_user), ): - form_data = form_data.model_dump(exclude_none=True) - form_data["model"] = form_data.get("model", form_data.get("name")) + """Retrieve model metadata from the Ollama backend.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + payload = form_data.model_dump(exclude_none=True) + payload['model'] = payload.get('model', payload.get('name')) + model = payload.get('model') + + await check_model_access(user, await Models.get_model_by_id(model), BYPASS_MODEL_ACCESS_CONTROL) await get_all_models(request, user=user) models = request.app.state.OLLAMA_MODELS - model = form_data.get("model") - if model not in models: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model), - ) - - url_idx = random.choice(models[model]["urls"]) + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model)) + url_idx = random.choice(models[model]['urls']) url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - try: - headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), - } - - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.request( - method="POST", url=f"{url}/api/show", headers=headers, json=form_data - ) - r.raise_for_status() - - return r.json() - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" - - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - -class GenerateEmbedForm(BaseModel): - model: str - input: list[str] | str - truncate: Optional[bool] = None - options: Optional[dict] = None - keep_alive: Optional[Union[int, str]] = None - - model_config = ConfigDict( - extra="allow", - ) - - -@router.post("/api/embed") -@router.post("/api/embed/{url_idx}") -async def embed( - request: Request, - form_data: GenerateEmbedForm, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), -): - log.info(f"generate_ollama_batch_embeddings {form_data}") - - if url_idx is None: - await get_all_models(request, user=user) - models = request.app.state.OLLAMA_MODELS - - model = form_data.model - - if ":" not in model: - model = f"{model}:latest" - - if model in models: - url_idx = random.choice(models[model]["urls"]) - else: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), - ) - - url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(url_idx), - request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support - ) - key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - - prefix_id = api_config.get("prefix_id", None) - if prefix_id: - form_data.model = form_data.model.replace(f"{prefix_id}.", "") - - try: - headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), - } - - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.request( - method="POST", - url=f"{url}/api/embed", - headers=headers, - data=form_data.model_dump_json(exclude_none=True).encode(), - ) - r.raise_for_status() - - data = r.json() - return data - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" - - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - -class GenerateEmbeddingsForm(BaseModel): - model: str - prompt: str - options: Optional[dict] = None - keep_alive: Optional[Union[int, str]] = None - - -@router.post("/api/embeddings") -@router.post("/api/embeddings/{url_idx}") -async def embeddings( - request: Request, - form_data: GenerateEmbeddingsForm, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), -): - log.info(f"generate_ollama_embeddings {form_data}") - - if url_idx is None: - await get_all_models(request, user=user) - models = request.app.state.OLLAMA_MODELS - - model = form_data.model - - if ":" not in model: - model = f"{model}:latest" - - if model in models: - url_idx = random.choice(models[model]["urls"]) - else: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), - ) - - url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(url_idx), - request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support - ) - key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - - prefix_id = api_config.get("prefix_id", None) - if prefix_id: - form_data.model = form_data.model.replace(f"{prefix_id}.", "") - - try: - headers = { - "Content-Type": "application/json", - **({"Authorization": f"Bearer {key}"} if key else {}), - } - - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) - - r = requests.request( - method="POST", - url=f"{url}/api/embeddings", - headers=headers, - data=form_data.model_dump_json(exclude_none=True).encode(), - ) - r.raise_for_status() - - data = r.json() - return data - except Exception as e: - log.exception(e) - - detail = None - if r is not None: - try: - res = r.json() - if "error" in res: - detail = f"Ollama: {res['error']}" - except Exception: - detail = f"Ollama: {e}" - - raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", - ) - - -class GenerateCompletionForm(BaseModel): - model: str - prompt: str - suffix: Optional[str] = None - images: Optional[list[str]] = None - format: Optional[Union[dict, str]] = None - options: Optional[dict] = None - system: Optional[str] = None - template: Optional[str] = None - context: Optional[list[int]] = None - stream: Optional[bool] = True - raw: Optional[bool] = None - keep_alive: Optional[Union[int, str]] = None - - -@router.post("/api/generate") -@router.post("/api/generate/{url_idx}") -async def generate_completion( - request: Request, - form_data: GenerateCompletionForm, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), -): - if url_idx is None: - await get_all_models(request, user=user) - models = request.app.state.OLLAMA_MODELS - - model = form_data.model - - if ":" not in model: - model = f"{model}:latest" - - if model in models: - url_idx = random.choice(models[model]["urls"]) - else: - raise HTTPException( - status_code=400, - detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), - ) - - url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(url_idx), - request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support - ) - - prefix_id = api_config.get("prefix_id", None) - if prefix_id: - form_data.model = form_data.model.replace(f"{prefix_id}.", "") - - return await send_post_request( - url=f"{url}/api/generate", - payload=form_data.model_dump_json(exclude_none=True).encode(), - key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + return await send_request( + f'{url}/api/show', + payload=json.dumps(payload), + key=key, user=user, ) -class ChatMessage(BaseModel): - role: str - content: Optional[str] = None - tool_calls: Optional[list[dict]] = None - images: Optional[list[str]] = None +class GenerateEmbedForm(BaseModel): + """Payload for the newer /api/embed endpoint (batch-capable).""" - @validator("content", pre=True) + model: str + input: list[str] | str + truncate: bool | None = None + options: dict | None = None + keep_alive: Union[int, str | None] = None + model_config = ConfigDict(extra='allow') + + +@router.post('/api/embed') +@router.post('/api/embed/{url_idx}') +async def embed( + request: Request, + form_data: GenerateEmbedForm, + url_idx: int | None = None, + user=Depends(get_verified_user), +): + """Generate embeddings via the Ollama /api/embed endpoint.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + log.info(f'generate_ollama_batch_embeddings {form_data}') + await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) + await validate_ollama_backend_idx(request, form_data.model, url_idx, user) + + if url_idx is None: + model = form_data.model + models = request.app.state.OLLAMA_MODELS + if not models or model not in models: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = random.choice(models[model]['urls']) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), + ) + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + prefix_id = api_config.get('prefix_id') + if prefix_id: + form_data.model = form_data.model.replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/embed', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=key, + user=user, + ) + + +class GenerateEmbeddingsForm(BaseModel): + """Payload for the legacy /api/embeddings endpoint (single-prompt).""" + + model: str + prompt: str + options: dict | None = None + keep_alive: Union[int, str | None] = None + + +@router.post('/api/embeddings') +@router.post('/api/embeddings/{url_idx}') +async def embeddings( + request: Request, + form_data: GenerateEmbeddingsForm, + url_idx: int | None = None, + user=Depends(get_verified_user), +): + """Generate embeddings via the legacy Ollama /api/embeddings endpoint.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + log.info(f'generate_ollama_embeddings {form_data}') + await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) + await validate_ollama_backend_idx(request, form_data.model, url_idx, user) + + if url_idx is None: + model = form_data.model + models = request.app.state.OLLAMA_MODELS + if not models or model not in models: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = random.choice(models[model]['urls']) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), + ) + key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) + + prefix_id = api_config.get('prefix_id') + if prefix_id: + form_data.model = form_data.model.replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/embeddings', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=key, + user=user, + ) + + +class GenerateCompletionForm(BaseModel): + """Payload for the Ollama /api/generate endpoint.""" + + model: str + prompt: str | None = None + suffix: str | None = None + images: list[str | None] = None + format: Union[dict, str | None] = None + options: dict | None = None + system: str | None = None + template: str | None = None + context: list[int | None] = None + stream: bool | None = True + raw: bool | None = None + keep_alive: Union[int, str | None] = None + + +@router.post('/api/generate') +@router.post('/api/generate/{url_idx}') +async def generate_completion( + request: Request, + form_data: GenerateCompletionForm, + url_idx: int | None = None, + user=Depends(get_verified_user), +): + """Run text completion via Ollama /api/generate.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) + await validate_ollama_backend_idx(request, form_data.model, url_idx, user) + + if url_idx is None: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + model = form_data.model + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = random.choice(models[model]['urls']) + + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), + ) + + prefix_id = api_config.get('prefix_id') + if prefix_id: + form_data.model = form_data.model.replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/api/generate', + payload=form_data.model_dump_json(exclude_none=True).encode(), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=True, + ) + + +class ChatMessage(BaseModel): + """A single message in an Ollama chat conversation.""" + + role: str + content: str | None = None + tool_calls: list[dict | None] = None + images: list[str | None] = None + model_config = ConfigDict(extra='allow') + + @validator('content', pre=True) @classmethod def check_at_least_one_field(cls, field_value, values, **kwargs): - # Raise an error if both 'content' and 'tool_calls' are None - if field_value is None and ( - "tool_calls" not in values or values["tool_calls"] is None - ): - raise ValueError( - "At least one of 'content' or 'tool_calls' must be provided" - ) - + if field_value is None and ('tool_calls' not in values or values['tool_calls'] is None): + raise ValueError("At least one of 'content' or 'tool_calls' must be provided") return field_value class GenerateChatCompletionForm(BaseModel): + """Payload for the Ollama /api/chat endpoint.""" + model: str messages: list[ChatMessage] - format: Optional[Union[dict, str]] = None - options: Optional[dict] = None - template: Optional[str] = None - stream: Optional[bool] = True - keep_alive: Optional[Union[int, str]] = None - tools: Optional[list[dict]] = None - model_config = ConfigDict( - extra="allow", - ) + format: Union[dict, str | None] = None + options: dict | None = None + template: str | None = None + stream: bool | None = True + keep_alive: Union[int, str | None] = None + tools: list[dict | None] = None + model_config = ConfigDict(extra='allow') -async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = None): +async def validate_ollama_backend_idx(request: Request, model: str, url_idx: int | None, user) -> None: + # A caller-supplied url_idx must point to a backend the model is actually + # served from; the None path is already constrained to that allow-list. + if url_idx is None or user is None or getattr(user, 'role', None) == 'admin' or BYPASS_MODEL_ACCESS_CONTROL: + return + models = request.app.state.OLLAMA_MODELS + if not models or model not in models: + await get_all_models(request, user=user) + models = request.app.state.OLLAMA_MODELS + if url_idx not in (models.get(model) or {}).get('urls', []): + raise HTTPException(status_code=403, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +async def get_ollama_url(request: Request, model: str, url_idx: int | None = None, user=None): + await validate_ollama_backend_idx(request, model, url_idx, user) if url_idx is None: models = request.app.state.OLLAMA_MODELS if model not in models: @@ -1243,513 +979,505 @@ async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model), ) - url_idx = random.choice(models[model].get("urls", [])) + url_idx = random.choice(models[model].get('urls', [])) url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] return url, url_idx -@router.post("/api/chat") -@router.post("/api/chat/{url_idx}") +@router.post('/api/chat') +@router.post('/api/chat/{url_idx}') async def generate_chat_completion( request: Request, form_data: dict, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), - bypass_filter: Optional[bool] = False, - bypass_system_prompt: bool = False, - db: Session = Depends(get_session), + url_idx: int | None = None, + user=Depends(get_verified_user), # noqa: B008 ): + """Forward a chat completion request to an Ollama backend.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. + # This prevents holding a connection during the entire LLM call (30-60+ seconds), + # which would exhaust the connection pool under concurrent load. + + # bypass_filter and bypass_system_prompt are read from request.state to prevent + # external clients from setting them via query parameter. Only internal + # server-side callers (e.g. utils/chat.py) should set + # request.state.bypass_filter / request.state.bypass_system_prompt = True. + bypass_filter = getattr(request.state, 'bypass_filter', False) if BYPASS_MODEL_ACCESS_CONTROL: bypass_filter = True + bypass_system_prompt = getattr(request.state, 'bypass_system_prompt', False) - metadata = form_data.pop("metadata", None) + metadata = form_data.pop('metadata', None) try: form_data = GenerateChatCompletionForm(**form_data) - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=400, - detail=str(e), - ) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail=str(exc)) if isinstance(form_data, BaseModel): payload = {**form_data.model_dump(exclude_none=True)} - if "metadata" in payload: - del payload["metadata"] + payload.pop('metadata', None) - model_id = payload["model"] - model_info = Models.get_model_by_id(model_id, db=db) + model_id = payload['model'] + model_info = await Models.get_model_by_id(model_id) - if model_info: + if model_info is not None: if model_info.base_model_id: base_model_id = ( - request.base_model_id - if hasattr(request, "base_model_id") - else model_info.base_model_id - ) # Use request's base_model_id if available - payload["model"] = base_model_id + request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id + ) + payload['model'] = base_model_id params = model_info.params.model_dump() - if params: - system = params.pop("system", None) - + system = params.pop('system', None) payload = apply_model_params_to_body_ollama(params, payload) if not bypass_system_prompt: - payload = apply_system_prompt_to_body(system, payload, metadata, user) + payload = await apply_system_prompt_to_body(system, payload, metadata, user) - # Check if user has access to the model - if not bypass_filter and user.role == "user": - if not ( - user.id == model_info.user_id - or has_access( - user.id, - type="read", - access_control=model_info.access_control, - db=db, - ) - ): - raise HTTPException( - status_code=403, - detail="Model not found", - ) - elif not bypass_filter: - if user.role != "admin": - raise HTTPException( - status_code=403, - detail="Model not found", - ) + await check_model_access(user, model_info, bypass_filter) + else: + await check_model_access(user, None, bypass_filter) - if ":" not in payload["model"]: - payload["model"] = f"{payload['model']}:latest" + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = _resolve_api_config(request, url_idx, url) - url, url_idx = await get_ollama_url(request, payload["model"], url_idx) - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(url_idx), - request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support - ) - - prefix_id = api_config.get("prefix_id", None) + prefix_id = api_config.get('prefix_id') if prefix_id: - payload["model"] = payload["model"].replace(f"{prefix_id}.", "") + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') - return await send_post_request( - url=f"{url}/api/chat", + return await send_request( + f'{url}/api/chat', payload=json.dumps(payload), - stream=form_data.stream, key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), - content_type="application/x-ndjson", user=user, + stream=form_data.stream, + content_type='application/x-ndjson', metadata=metadata, ) # TODO: we should update this part once Ollama supports other types class OpenAIChatMessageContent(BaseModel): + """Content block within an OpenAI-style chat message.""" + type: str - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class OpenAIChatMessage(BaseModel): - role: str - content: Union[Optional[str], list[OpenAIChatMessageContent]] + """A single message in an OpenAI-compatible chat request.""" - model_config = ConfigDict(extra="allow") + role: str + content: Union[str | None, list[OpenAIChatMessageContent]] + model_config = ConfigDict(extra='allow') class OpenAIChatCompletionForm(BaseModel): + """Payload for the OpenAI-compatible /v1/chat/completions proxy.""" + model: str messages: list[OpenAIChatMessage] - - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') class OpenAICompletionForm(BaseModel): + """Payload for the OpenAI-compatible /v1/completions proxy.""" + model: str prompt: str - - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') -@router.post("/v1/completions") -@router.post("/v1/completions/{url_idx}") +@router.post('/v1/completions') +@router.post('/v1/completions/{url_idx}') async def generate_openai_completion( request: Request, form_data: dict, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + url_idx: int | None = None, + user=Depends(get_verified_user), # noqa: B008 ): - metadata = form_data.pop("metadata", None) + """Forward a text completion request via the OpenAI-compatible proxy.""" + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. + # This prevents holding a connection during the entire LLM call (30-60+ seconds), + # which would exhaust the connection pool under concurrent load. + metadata = form_data.pop('metadata', None) try: form_data = OpenAICompletionForm(**form_data) - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=400, - detail=str(e), - ) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail=str(exc)) - payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])} - if "metadata" in payload: - del payload["metadata"] + payload = {**form_data.model_dump(exclude_none=True, exclude=['metadata'])} + payload.pop('metadata', None) model_id = form_data.model - if ":" not in model_id: - model_id = f"{model_id}:latest" - - model_info = Models.get_model_by_id(model_id, db=db) - if model_info: + model_info = await Models.get_model_by_id(model_id) + if model_info is not None: if model_info.base_model_id: - payload["model"] = model_info.base_model_id + payload['model'] = model_info.base_model_id params = model_info.params.model_dump() - if params: payload = apply_model_params_to_body_openai(params, payload) - - # Check if user has access to the model - if user.role == "user": - if not ( - user.id == model_info.user_id - or has_access( - user.id, - type="read", - access_control=model_info.access_control, - db=db, - ) - ): - raise HTTPException( - status_code=403, - detail="Model not found", - ) + await check_model_access(user, model_info) else: - if user.role != "admin": - raise HTTPException( - status_code=403, - detail="Model not found", - ) + await check_model_access(user, None) - if ":" not in payload["model"]: - payload["model"] = f"{payload['model']}:latest" - - url, url_idx = await get_ollama_url(request, payload["model"], url_idx) - api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( - str(url_idx), - request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support - ) - - prefix_id = api_config.get("prefix_id", None) + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = _resolve_api_config(request, url_idx, url) + prefix_id = api_config.get('prefix_id') if prefix_id: - payload["model"] = payload["model"].replace(f"{prefix_id}.", "") + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') - return await send_post_request( - url=f"{url}/v1/completions", + return await send_request( + f'{url}/v1/completions', payload=json.dumps(payload), - stream=payload.get("stream", False), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), user=user, + stream=payload.get('stream', False), metadata=metadata, ) -@router.post("/v1/chat/completions") -@router.post("/v1/chat/completions/{url_idx}") +@router.post('/v1/chat/completions') +@router.post('/v1/chat/completions/{url_idx}') async def generate_openai_chat_completion( request: Request, form_data: dict, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + url_idx: int | None = None, + user=Depends(get_verified_user), # noqa: B008 ): - metadata = form_data.pop("metadata", None) + """Forward a chat completion request via the OpenAI-compatible proxy.""" + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. + # This prevents holding a connection during the entire LLM call (30-60+ seconds), + # which would exhaust the connection pool under concurrent load. + metadata = form_data.pop('metadata', None) try: - completion_form = OpenAIChatCompletionForm(**form_data) - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=400, - detail=str(e), - ) + form_data = OpenAIChatCompletionForm(**form_data) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail=str(exc)) - payload = {**completion_form.model_dump(exclude_none=True, exclude=["metadata"])} - if "metadata" in payload: - del payload["metadata"] + payload = {**form_data.model_dump(exclude_none=True, exclude=['metadata'])} + payload.pop('metadata', None) - model_id = completion_form.model - if ":" not in model_id: - model_id = f"{model_id}:latest" - - model_info = Models.get_model_by_id(model_id, db=db) - if model_info: + model_id = form_data.model + model_info = await Models.get_model_by_id(model_id) + if model_info is not None: if model_info.base_model_id: - payload["model"] = model_info.base_model_id + payload['model'] = model_info.base_model_id params = model_info.params.model_dump() - if params: - system = params.pop("system", None) - + system = params.pop('system', None) payload = apply_model_params_to_body_openai(params, payload) - payload = apply_system_prompt_to_body(system, payload, metadata, user) + payload = await apply_system_prompt_to_body(system, payload, metadata, user) - # Check if user has access to the model - if user.role == "user": - if not ( - user.id == model_info.user_id - or has_access( - user.id, - type="read", - access_control=model_info.access_control, - db=db, - ) - ): - raise HTTPException( - status_code=403, - detail="Model not found", - ) + await check_model_access(user, model_info) else: - if user.role != "admin": - raise HTTPException( - status_code=403, - detail="Model not found", - ) + await check_model_access(user, None) - if ":" not in payload["model"]: - payload["model"] = f"{payload['model']}:latest" + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = _resolve_api_config(request, url_idx, url) - url, url_idx = await get_ollama_url(request, payload["model"], url_idx) + prefix_id = api_config.get('prefix_id') + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/v1/chat/completions', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=payload.get('stream', False), + metadata=metadata, + ) + + +@router.post('/v1/messages') +@router.post('/v1/messages/{url_idx}') +async def generate_anthropic_messages( + request: Request, + form_data: dict, + url_idx: int | None = None, + user=Depends(get_verified_user), +): + """ + Proxy for Ollama's Anthropic-compatible /v1/messages endpoint. + + Forwards the request as-is to the Ollama backend, applying the same + model resolution, access control, and prefix_id handling used by + the OpenAI-compatible /v1/chat/completions proxy. + + See https://docs.ollama.com/api/anthropic-compatibility + """ + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + payload = {**form_data} + model_id = payload.get('model', '') + + model_info = await Models.get_model_by_id(model_id) + if model_info: + if model_info.base_model_id: + payload['model'] = model_info.base_model_id + + await check_model_access(user, model_info) + else: + await check_model_access(user, None) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support ) - prefix_id = api_config.get("prefix_id", None) + prefix_id = api_config.get('prefix_id', None) if prefix_id: - payload["model"] = payload["model"].replace(f"{prefix_id}.", "") + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') - return await send_post_request( - url=f"{url}/v1/chat/completions", + return await send_request( + f'{url}/v1/messages', payload=json.dumps(payload), - stream=payload.get("stream", False), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), user=user, - metadata=metadata, + stream=payload.get('stream', False), + content_type='text/event-stream' if payload.get('stream', False) else None, ) -@router.get("/v1/models") -@router.get("/v1/models/{url_idx}") +class ResponsesForm(BaseModel): + model: str + + model_config = ConfigDict(extra='allow') + + +@router.post('/v1/responses') +@router.post('/v1/responses/{url_idx}') +async def generate_responses( + request: Request, + form_data: ResponsesForm, + url_idx: int | None = None, + user=Depends(get_verified_user), +): + """ + Proxy for Ollama's OpenAI-compatible /v1/responses endpoint. + + Forwards the request as-is to the Ollama backend, applying the same + model resolution, access control, and prefix_id handling used by + the OpenAI-compatible /v1/chat/completions proxy. + + See https://ollama.com/blog/responses-api + """ + if not request.app.state.config.ENABLE_OLLAMA_API: + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) + + payload = form_data.model_dump() + model_id = form_data.model + + model_info = await Models.get_model_by_id(model_id) + if model_info: + if model_info.base_model_id: + payload['model'] = model_info.base_model_id + + await check_model_access(user, model_info) + else: + await check_model_access(user, None) + + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( + str(url_idx), + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support + ) + + prefix_id = api_config.get('prefix_id', None) + if prefix_id: + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') + + return await send_request( + f'{url}/v1/responses', + payload=json.dumps(payload), + key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), + user=user, + stream=payload.get('stream', False), + content_type='text/event-stream' if payload.get('stream', False) else None, + ) + + +@router.get('/v1/models') +@router.get('/v1/models/{url_idx}') async def get_openai_models( request: Request, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), - db: Session = Depends(get_session), -): - - models = [] + db: AsyncSession = Depends(get_async_session), +) -> dict: + """List models in the OpenAI-compatible format.""" if url_idx is None: model_list = await get_all_models(request, user=user) - models = [ - { - "id": model["model"], - "object": "model", - "created": int(time.time()), - "owned_by": "openai", - } - for model in model_list["models"] - ] - + raw_models = model_list['models'] else: url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - try: - r = requests.request(method="GET", url=f"{url}/api/tags") - r.raise_for_status() + model_list = await send_request(f'{url}/api/tags', 'GET') + raw_models = model_list.get('models', []) - model_list = r.json() + now_ts = int(time.time()) + models = [ + {'id': m['model'], 'object': 'model', 'created': now_ts, 'owned_by': 'openai'} + for m in raw_models + ] - models = [ - { - "id": model["model"], - "object": "model", - "created": int(time.time()), - "owned_by": "openai", - } - for model in models["models"] - ] - except Exception as e: - log.exception(e) - error_detail = "Open WebUI: Server Connection Error" - if r is not None: - try: - res = r.json() - if "error" in res: - error_detail = f"Ollama: {res['error']}" - except Exception: - error_detail = f"Ollama: {e}" + if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL: + model_ids = [m['id'] for m in models] + model_infos = { + mi.id: mi for mi in await Models.get_models_by_ids(model_ids, db=db) + } + user_group_ids = { + g.id for g in await Groups.get_groups_by_member_id(user.id, db=db) + } + accessible_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + models = [ + m for m in models + if (mi := model_infos.get(m['id'])) and (user.id == mi.user_id or mi.id in accessible_ids) + ] - raise HTTPException( - status_code=r.status_code if r else 500, - detail=error_detail, - ) - - if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL: - # Filter models based on user access control - filtered_models = [] - for model in models: - model_info = Models.get_model_by_id(model["id"], db=db) - if model_info: - if user.id == model_info.user_id or has_access( - user.id, - type="read", - access_control=model_info.access_control, - db=db, - ): - filtered_models.append(model) - models = filtered_models - - return { - "data": models, - "object": "list", - } + return {'data': models, 'object': 'list'} class UrlForm(BaseModel): + """Form carrying a single URL string.""" + url: str class UploadBlobForm(BaseModel): + """Form carrying a filename for blob uploads.""" + filename: str -def parse_huggingface_url(hf_url): +def parse_huggingface_url(hf_url: str) -> str | None: + """Extract the filename from a HuggingFace download URL.""" try: - # Parse the URL - parsed_url = urlparse(hf_url) - - # Get the path and split it into components - path_components = parsed_url.path.split("/") - - # Extract the desired output - model_file = path_components[-1] - - return model_file - except ValueError: + return urlparse(hf_url).path.split('/')[-1] + except (ValueError, IndexError): return None async def download_file_stream( - ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024 + ollama_url: str, + file_url: str, + file_path: str, + file_name: str, + chunk_size: int = 1024 * 1024, ): - done = False + """Stream a model file download from *file_url*, then push the blob to Ollama.""" + current_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0 + headers = {'Range': f'bytes={current_size}-'} if current_size > 0 else {} - if os.path.exists(file_path): - current_size = os.path.getsize(file_path) - else: - current_size = 0 + session = await get_session() + async with session.get( + file_url, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=600), + ) as response: + total_size = int(response.headers.get('content-length', 0)) + current_size - headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {} + with open(file_path, 'ab+') as f: + async for data in response.content.iter_chunked(chunk_size): + current_size += len(data) + f.write(data) - timeout = aiohttp.ClientTimeout(total=600) # Set the timeout + done = current_size == total_size + progress = round((current_size / total_size) * 100, 2) + yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' - async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - async with session.get( - file_url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL - ) as response: - total_size = int(response.headers.get("content-length", 0)) + current_size + if done: + f.close() + hashed = calculate_sha256(file_path, chunk_size) - with open(file_path, "ab+") as file: - async for data in response.content.iter_chunked(chunk_size): - current_size += len(data) - file.write(data) + with open(file_path, 'rb') as blob_f: + blob_data = blob_f.read() - done = current_size == total_size - progress = round((current_size / total_size) * 100, 2) - - yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' - - if done: - file.close() - hashed = calculate_sha256(file_path, chunk_size) - - with open(file_path, "rb") as file: - chunk_size = 1024 * 1024 * 2 - url = f"{ollama_url}/api/blobs/sha256:{hashed}" - with requests.Session() as session: - response = session.post(url, data=file, timeout=30) - - if response.ok: - res = { - "done": done, - "blob": f"sha256:{hashed}", - "name": file_name, - } - os.remove(file_path) - - yield f"data: {json.dumps(res)}\n\n" - else: - raise "Ollama: Could not create blob, Please try again." + blob_url = f'{ollama_url}/api/blobs/sha256:{hashed}' + async with session.post( + blob_url, + data=blob_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=30), + ) as blob_resp: + if blob_resp.ok: + os.remove(file_path) + yield f'data: {json.dumps({"done": done, "blob": f"sha256:{hashed}", "name": file_name})}\n\n' + else: + raise RuntimeError('Ollama: Could not create blob, Please try again.') -# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" -@router.post("/models/download") -@router.post("/models/download/{url_idx}") +@router.post('/models/download') +@router.post('/models/download/{url_idx}') async def download_model( request: Request, form_data: UrlForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): - allowed_hosts = ["https://huggingface.co/", "https://github.com/"] - + """Download a GGUF model from HuggingFace or GitHub and register it with Ollama.""" + allowed_hosts = ['https://huggingface.co/', 'https://github.com/'] if not any(form_data.url.startswith(host) for host in allowed_hosts): raise HTTPException( status_code=400, - detail="Invalid file_url. Only URLs from allowed hosts are permitted.", + detail='Invalid file_url. Only URLs from allowed hosts are permitted.', ) - if url_idx is None: - url_idx = 0 - url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] - + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx if url_idx is not None else 0] file_name = parse_huggingface_url(form_data.url) - if file_name: - file_path = f"{UPLOAD_DIR}/{file_name}" - - return StreamingResponse( - download_file_stream(url, form_data.url, file_path, file_name), - ) - else: + if not file_name: return None + file_path = os.path.join(UPLOAD_DIR, file_name) + return StreamingResponse( + download_file_stream(url, form_data.url, file_path, file_name), + ) -# TODO: Progress bar does not reflect size & duration of upload. -@router.post("/models/upload") -@router.post("/models/upload/{url_idx}") + +@router.post('/models/upload') +@router.post('/models/upload/{url_idx}') async def upload_model( request: Request, file: UploadFile = File(...), - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): - if url_idx is None: - url_idx = 0 - ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + """Upload a local model file, push it as a blob, and create the model in Ollama.""" + ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx if url_idx is not None else 0] filename = os.path.basename(file.filename) file_path = os.path.join(UPLOAD_DIR, filename) os.makedirs(UPLOAD_DIR, exist_ok=True) - # --- P1: save file locally --- - chunk_size = 1024 * 1024 * 2 # 2 MB chunks - with open(file_path, "wb") as out_f: + # Stage 1: persist the uploaded file to disk + chunk_size = 1024 * 1024 * 2 # 2 MiB + with open(file_path, 'wb') as out_f: while True: chunk = file.file.read(chunk_size) - # log.info(f"Chunk: {str(chunk)}") # DEBUG if not chunk: break out_f.write(chunk) @@ -1757,72 +1485,63 @@ async def upload_model( async def file_process_stream(): nonlocal ollama_url total_size = os.path.getsize(file_path) - log.info(f"Total Model Size: {str(total_size)}") # DEBUG + log.info(f'Total Model Size: {total_size}') - # --- P2: SSE progress + calculate sha256 hash --- + # Stage 2: hash the file and emit SSE progress file_hash = calculate_sha256(file_path, chunk_size) - log.info(f"Model Hash: {str(file_hash)}") # DEBUG + log.info(f'Model Hash: {file_hash}') + try: - with open(file_path, "rb") as f: + with open(file_path, 'rb') as f: bytes_read = 0 while chunk := f.read(chunk_size): bytes_read += len(chunk) progress = round(bytes_read / total_size * 100, 2) - data_msg = { - "progress": progress, - "total": total_size, - "completed": bytes_read, - } - yield f"data: {json.dumps(data_msg)}\n\n" + yield f'data: {json.dumps({"progress": progress, "total": total_size, "completed": bytes_read})}\n\n' - # --- P3: Upload to ollama /api/blobs --- - with open(file_path, "rb") as f: - url = f"{ollama_url}/api/blobs/sha256:{file_hash}" - response = requests.post(url, data=f) + # Stage 3: push blob to Ollama + with open(file_path, 'rb') as f: + blob_data = f.read() - if response.ok: - log.info(f"Uploaded to /api/blobs") # DEBUG - # Remove local file - os.remove(file_path) + session = await get_session() + blob_url = f'{ollama_url}/api/blobs/sha256:{file_hash}' + async with session.post( + blob_url, + data=blob_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as resp: + if not resp.ok: + raise Exception('Ollama: Could not create blob, Please try again.') - # Create model in ollama - model_name, ext = os.path.splitext(filename) - log.info(f"Created Model: {model_name}") # DEBUG + log.info('Uploaded to /api/blobs') + os.remove(file_path) - create_payload = { - "model": model_name, - # Reference the file by its original name => the uploaded blob's digest - "files": {filename: f"sha256:{file_hash}"}, - } - log.info(f"Model Payload: {create_payload}") # DEBUG + # Stage 4: create the model + model, _ext = os.path.splitext(filename) + log.info(f'Created Model: {model}') - # Call ollama /api/create - # https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model - create_resp = requests.post( - url=f"{ollama_url}/api/create", - headers={"Content-Type": "application/json"}, - data=json.dumps(create_payload), - ) + create_payload = { + 'model': model, + 'files': {filename: f'sha256:{file_hash}'}, + } + log.info(f'Model Payload: {create_payload}') + async with session.post( + f'{ollama_url}/api/create', + headers={'Content-Type': 'application/json'}, + data=json.dumps(create_payload), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as create_resp: if create_resp.ok: - log.info(f"API SUCCESS!") # DEBUG - done_msg = { - "done": True, - "blob": f"sha256:{file_hash}", - "name": filename, - "model_created": model_name, - } - yield f"data: {json.dumps(done_msg)}\n\n" + log.info('API SUCCESS!') + yield f'data: {json.dumps({"done": True, "blob": f"sha256:{file_hash}", "name": filename, "model_created": model})}\n\n' else: - raise Exception( - f"Failed to create model in Ollama. {create_resp.text}" - ) + resp_text = await create_resp.text() + raise Exception(f'Failed to create model in Ollama. {resp_text}') - else: - raise Exception("Ollama: Could not create blob, Please try again.") + except Exception as exc: + yield f'data: {json.dumps({"error": str(exc)})}\n\n' - except Exception as e: - res = {"error": str(e)} - yield f"data: {json.dumps(res)}\n\n" - - return StreamingResponse(file_process_stream(), media_type="text/event-stream") + return StreamingResponse(file_process_stream(), media_type='text/event-stream') diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index ec4ce2f4a8..b2ada1c8a3 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -1,58 +1,61 @@ +from __future__ import annotations + import asyncio import hashlib import json import logging +import re from typing import Optional +from urllib.parse import quote, urlparse import aiohttp from aiocache import cached -import requests - from azure.identity import DefaultAzureCredential, get_bearer_token_provider - -from fastapi import Depends, HTTPException, Request, APIRouter +from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import ( FileResponse, - StreamingResponse, JSONResponse, PlainTextResponse, + StreamingResponse, ) -from pydantic import BaseModel -from starlette.background import BackgroundTask -from sqlalchemy.orm import Session - -from open_webui.internal.db import get_session - -from open_webui.models.models import Models from open_webui.config import ( CACHE_DIR, ) +from open_webui.constants import ERROR_MESSAGES from open_webui.env import ( - MODELS_CACHE_TTL, AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, - ENABLE_FORWARD_USER_INFO_HEADERS, BYPASS_MODEL_ACCESS_CONTROL, + ENABLE_FORWARD_USER_INFO_HEADERS, + ENABLE_OPENAI_API_PASSTHROUGH, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + MODELS_CACHE_TTL, ) +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.models import Models from open_webui.models.users import UserModel - -from open_webui.constants import ERROR_MESSAGES - - -from open_webui.utils.payload import ( - apply_model_params_to_body_openai, - apply_system_prompt_to_body, -) +from open_webui.utils.access_control import check_model_access, has_connection_access +from open_webui.utils.anthropic import get_anthropic_models, is_anthropic_url +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import get_custom_headers, include_user_info_headers from open_webui.utils.misc import ( convert_logit_bias_input_to_json, stream_chunks_handler, ) - -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access -from open_webui.utils.headers import include_user_info_headers - +from open_webui.utils.payload import ( + apply_model_params_to_body_openai, + apply_system_prompt_to_body, +) +from open_webui.utils.session_pool import ( + cleanup_response, + get_session, + stream_wrapper, +) +from pydantic import BaseModel, ConfigDict +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -60,60 +63,86 @@ log = logging.getLogger(__name__) ########################################## # # Utility functions +# Let the responses returned through this gate be worth +# the question that summoned them. # ########################################## +# Headers that become stale after aiohttp auto-decompresses the upstream +# response body. Forwarding them verbatim causes desktop / programmatic +# clients to attempt decompression of an already-decoded payload, resulting +# in ZlibError. See https://github.com/aio-libs/aiohttp/issues/4462. +_STRIP_PROXY_HEADERS = frozenset({'Content-Encoding', 'Content-Length', 'Transfer-Encoding'}) -async def send_get_request(url, key=None, user: UserModel = None): + +def _clean_proxy_headers(raw_headers) -> dict: + """Return a copy of *raw_headers* with stale encoding headers removed.""" + return {k: v for k, v in raw_headers.items() if k not in _STRIP_PROXY_HEADERS} + + +async def send_get_request( + request: Request = None, + url=None, + key=None, + user: UserModel = None, + config=None, +): timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) try: async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - headers = { - **({"Authorization": f"Bearer {key}"} if key else {}), - } + if request and config: + headers, cookies = await get_headers_and_cookies(request, url, key, config, user=user) + else: + headers = { + **({'Authorization': f'Bearer {key}'} if key else {}), + } + cookies = None - if ENABLE_FORWARD_USER_INFO_HEADERS and user: - headers = include_user_info_headers(headers, user) + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) async with session.get( url, headers=headers, + cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: return await response.json() except Exception as e: # Handle connection error here - log.error(f"Connection error: {e}") + log.error(f'Connection error: {e}') return None -async def cleanup_response( - response: Optional[aiohttp.ClientResponse], - session: Optional[aiohttp.ClientSession], +async def get_models_request( + request: Request = None, + url=None, + key=None, + user: UserModel = None, + config=None, ): - if response: - response.close() - if session: - await session.close() + if is_anthropic_url(url): + return await get_anthropic_models(url, key, user=user) + return await send_get_request(request, f'{url}/models', key, user=user, config=config) def openai_reasoning_model_handler(payload): """ Handle reasoning model specific parameters """ - if "max_tokens" in payload: + if 'max_tokens' in payload: # Convert "max_tokens" to "max_completion_tokens" for all reasoning models - payload["max_completion_tokens"] = payload["max_tokens"] - del payload["max_tokens"] + payload['max_completion_tokens'] = payload['max_tokens'] + del payload['max_tokens'] # Handle system role conversion based on model type - if payload["messages"][0]["role"] == "system": - model_lower = payload["model"].lower() + if payload['messages'][0]['role'] == 'system': + model_lower = payload['model'].lower() # Legacy models use "user" role instead of "system" - if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"): - payload["messages"][0]["role"] = "user" + if model_lower.startswith('o1-mini') or model_lower.startswith('o1-preview'): + payload['messages'][0]['role'] = 'user' else: - payload["messages"][0]["role"] = "developer" + payload['messages'][0]['role'] = 'developer' return payload @@ -123,62 +152,63 @@ async def get_headers_and_cookies( url, key=None, config=None, - metadata: Optional[dict] = None, + metadata: dict | None = None, user: UserModel = None, ): cookies = {} headers = { - "Content-Type": "application/json", + 'Content-Type': 'application/json', **( { - "HTTP-Referer": "https://openwebui.com/", - "X-Title": "Open WebUI", + 'HTTP-Referer': 'https://openwebui.com/', + 'X-Title': 'Open WebUI', } - if "openrouter.ai" in url + if 'openrouter.ai' in url else {} ), } if ENABLE_FORWARD_USER_INFO_HEADERS and user: headers = include_user_info_headers(headers, user) - if metadata and metadata.get("chat_id"): - headers["X-OpenWebUI-Chat-Id"] = metadata.get("chat_id") + if metadata and metadata.get('chat_id'): + headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id') token = None - auth_type = config.get("auth_type") + auth_type = config.get('auth_type') - if auth_type == "bearer" or auth_type is None: + if auth_type == 'bearer' or auth_type is None: # Default to bearer if not specified - token = f"{key}" - elif auth_type == "none": + token = f'{key}' + elif auth_type == 'none': token = None - elif auth_type == "session": + elif auth_type == 'session': cookies = request.cookies token = request.state.token.credentials - elif auth_type == "system_oauth": + elif auth_type == 'system_oauth': cookies = request.cookies oauth_token = None try: - if request.cookies.get("oauth_session_id", None): + if request.cookies.get('oauth_session_id', None): oauth_token = await request.app.state.oauth_manager.get_oauth_token( user.id, - request.cookies.get("oauth_session_id", None), + request.cookies.get('oauth_session_id', None), ) except Exception as e: - log.error(f"Error getting OAuth token: {e}") + log.error(f'Error getting OAuth token: {e}') if oauth_token: - token = f"{oauth_token.get('access_token', '')}" + token = f'{oauth_token.get("access_token", "")}' - elif auth_type in ("azure_ad", "microsoft_entra_id"): + elif auth_type in ('azure_ad', 'microsoft_entra_id'): token = get_microsoft_entra_id_access_token() if token: - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f'Bearer {token}' - if config.get("headers") and isinstance(config.get("headers"), dict): - headers = {**headers, **config.get("headers")} + if config.get('headers') and isinstance(config.get('headers'), dict): + custom_headers = get_custom_headers(config.get('headers'), user, metadata) + headers.update(custom_headers) return headers, cookies @@ -190,11 +220,11 @@ def get_microsoft_entra_id_access_token(): """ try: token_provider = get_bearer_token_provider( - DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" + DefaultAzureCredential(), 'https://cognitiveservices.azure.com/.default' ) return token_provider() except Exception as e: - log.error(f"Error getting Microsoft Entra ID access token: {e}") + log.error(f'Error getting Microsoft Entra ID access token: {e}') return None @@ -207,47 +237,38 @@ def get_microsoft_entra_id_access_token(): router = APIRouter() -@router.get("/config") +@router.get('/config') async def get_config(request: Request, user=Depends(get_admin_user)): return { - "ENABLE_OPENAI_API": request.app.state.config.ENABLE_OPENAI_API, - "OPENAI_API_BASE_URLS": request.app.state.config.OPENAI_API_BASE_URLS, - "OPENAI_API_KEYS": request.app.state.config.OPENAI_API_KEYS, - "OPENAI_API_CONFIGS": request.app.state.config.OPENAI_API_CONFIGS, + 'ENABLE_OPENAI_API': request.app.state.config.ENABLE_OPENAI_API, + 'OPENAI_API_BASE_URLS': request.app.state.config.OPENAI_API_BASE_URLS, + 'OPENAI_API_KEYS': request.app.state.config.OPENAI_API_KEYS, + 'OPENAI_API_CONFIGS': request.app.state.config.OPENAI_API_CONFIGS, } class OpenAIConfigForm(BaseModel): - ENABLE_OPENAI_API: Optional[bool] = None + ENABLE_OPENAI_API: bool | None = None OPENAI_API_BASE_URLS: list[str] OPENAI_API_KEYS: list[str] OPENAI_API_CONFIGS: dict -@router.post("/config/update") -async def update_config( - request: Request, form_data: OpenAIConfigForm, user=Depends(get_admin_user) -): +@router.post('/config/update') +async def update_config(request: Request, form_data: OpenAIConfigForm, user=Depends(get_admin_user)): request.app.state.config.ENABLE_OPENAI_API = form_data.ENABLE_OPENAI_API request.app.state.config.OPENAI_API_BASE_URLS = form_data.OPENAI_API_BASE_URLS request.app.state.config.OPENAI_API_KEYS = form_data.OPENAI_API_KEYS # Check if API KEYS length is same than API URLS length - if len(request.app.state.config.OPENAI_API_KEYS) != len( - request.app.state.config.OPENAI_API_BASE_URLS - ): - if len(request.app.state.config.OPENAI_API_KEYS) > len( - request.app.state.config.OPENAI_API_BASE_URLS - ): - request.app.state.config.OPENAI_API_KEYS = ( - request.app.state.config.OPENAI_API_KEYS[ - : len(request.app.state.config.OPENAI_API_BASE_URLS) - ] - ) + if len(request.app.state.config.OPENAI_API_KEYS) != len(request.app.state.config.OPENAI_API_BASE_URLS): + if len(request.app.state.config.OPENAI_API_KEYS) > len(request.app.state.config.OPENAI_API_BASE_URLS): + request.app.state.config.OPENAI_API_KEYS = request.app.state.config.OPENAI_API_KEYS[ + : len(request.app.state.config.OPENAI_API_BASE_URLS) + ] else: - request.app.state.config.OPENAI_API_KEYS += [""] * ( - len(request.app.state.config.OPENAI_API_BASE_URLS) - - len(request.app.state.config.OPENAI_API_KEYS) + request.app.state.config.OPENAI_API_KEYS += [''] * ( + len(request.app.state.config.OPENAI_API_BASE_URLS) - len(request.app.state.config.OPENAI_API_KEYS) ) request.app.state.config.OPENAI_API_CONFIGS = form_data.OPENAI_API_CONFIGS @@ -255,34 +276,30 @@ async def update_config( # Remove the API configs that are not in the API URLS keys = list(map(str, range(len(request.app.state.config.OPENAI_API_BASE_URLS)))) request.app.state.config.OPENAI_API_CONFIGS = { - key: value - for key, value in request.app.state.config.OPENAI_API_CONFIGS.items() - if key in keys + key: value for key, value in request.app.state.config.OPENAI_API_CONFIGS.items() if key in keys } return { - "ENABLE_OPENAI_API": request.app.state.config.ENABLE_OPENAI_API, - "OPENAI_API_BASE_URLS": request.app.state.config.OPENAI_API_BASE_URLS, - "OPENAI_API_KEYS": request.app.state.config.OPENAI_API_KEYS, - "OPENAI_API_CONFIGS": request.app.state.config.OPENAI_API_CONFIGS, + 'ENABLE_OPENAI_API': request.app.state.config.ENABLE_OPENAI_API, + 'OPENAI_API_BASE_URLS': request.app.state.config.OPENAI_API_BASE_URLS, + 'OPENAI_API_KEYS': request.app.state.config.OPENAI_API_KEYS, + 'OPENAI_API_CONFIGS': request.app.state.config.OPENAI_API_CONFIGS, } -@router.post("/audio/speech") +@router.post('/audio/speech') async def speech(request: Request, user=Depends(get_verified_user)): idx = None try: - idx = request.app.state.config.OPENAI_API_BASE_URLS.index( - "https://api.openai.com/v1" - ) + idx = request.app.state.config.OPENAI_API_BASE_URLS.index('https://api.openai.com/v1') body = await request.body() name = hashlib.sha256(body).hexdigest() - SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech" + SPEECH_CACHE_DIR = CACHE_DIR / 'audio' / 'speech' SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) - file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") - file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") + file_path = SPEECH_CACHE_DIR.joinpath(f'{name}.mp3') + file_body_path = SPEECH_CACHE_DIR.joinpath(f'{name}.json') # Check if the file already exists in the cache if file_path.is_file(): @@ -295,29 +312,28 @@ async def speech(request: Request, user=Depends(get_verified_user)): request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support ) - headers, cookies = await get_headers_and_cookies( - request, url, key, api_config, user=user - ) + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) r = None try: - r = requests.post( - url=f"{url}/audio/speech", + session = await get_session() + r = await session.post( + url=f'{url}/audio/speech', data=body, headers=headers, cookies=cookies, - stream=True, + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) r.raise_for_status() # Save the streaming content to a file - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): + with open(file_path, 'wb') as f: + async for chunk in r.content.iter_chunked(8192): f.write(chunk) - with open(file_body_path, "w") as f: - json.dump(json.loads(body.decode("utf-8")), f) + with open(file_body_path, 'w') as f: + json.dump(json.loads(body.decode('utf-8')), f) # Return the saved file return FileResponse(file_path) @@ -328,15 +344,15 @@ async def speech(request: Request, user=Depends(get_verified_user)): detail = None if r is not None: try: - res = r.json() - if "error" in res: - detail = f"External: {res['error']}" + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"]}' except Exception: - detail = f"External: {e}" + detail = f'External: {e}' raise HTTPException( - status_code=r.status_code if r else 500, - detail=detail if detail else "Open WebUI: Server Connection Error", + status_code=r.status if r else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', ) except ValueError: @@ -347,69 +363,59 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: if not request.app.state.config.ENABLE_OPENAI_API: return [] + # Cache config values locally to avoid repeated Redis lookups. + # Each access to request.app.state.config. triggers a Redis GET; + # caching here avoids hundreds of redundant round-trips. + api_base_urls = request.app.state.config.OPENAI_API_BASE_URLS + api_keys = list(request.app.state.config.OPENAI_API_KEYS) + api_configs = request.app.state.config.OPENAI_API_CONFIGS + # Check if API KEYS length is same than API URLS length - num_urls = len(request.app.state.config.OPENAI_API_BASE_URLS) - num_keys = len(request.app.state.config.OPENAI_API_KEYS) + num_urls = len(api_base_urls) + num_keys = len(api_keys) if num_keys != num_urls: # if there are more keys than urls, remove the extra keys if num_keys > num_urls: - new_keys = request.app.state.config.OPENAI_API_KEYS[:num_urls] - request.app.state.config.OPENAI_API_KEYS = new_keys + api_keys = api_keys[:num_urls] + request.app.state.config.OPENAI_API_KEYS = api_keys # if there are more urls than keys, add empty keys else: - request.app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys) + api_keys += [''] * (num_urls - num_keys) + request.app.state.config.OPENAI_API_KEYS = api_keys request_tasks = [] - for idx, url in enumerate(request.app.state.config.OPENAI_API_BASE_URLS): - if (str(idx) not in request.app.state.config.OPENAI_API_CONFIGS) and ( - url not in request.app.state.config.OPENAI_API_CONFIGS # Legacy support - ): - request_tasks.append( - send_get_request( - f"{url}/models", - request.app.state.config.OPENAI_API_KEYS[idx], - user=user, - ) - ) + for idx, url in enumerate(api_base_urls): + if (str(idx) not in api_configs) and (url not in api_configs): # Legacy support + request_tasks.append(get_models_request(request, url, api_keys[idx], user=user)) else: - api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + api_config = api_configs.get( str(idx), - request.app.state.config.OPENAI_API_CONFIGS.get( - url, {} - ), # Legacy support + api_configs.get(url, {}), # Legacy support ) - enable = api_config.get("enable", True) - model_ids = api_config.get("model_ids", []) + enable = api_config.get('enable', True) + model_ids = api_config.get('model_ids', []) if enable: if len(model_ids) == 0: - request_tasks.append( - send_get_request( - f"{url}/models", - request.app.state.config.OPENAI_API_KEYS[idx], - user=user, - ) - ) + request_tasks.append(get_models_request(request, url, api_keys[idx], user=user, config=api_config)) else: model_list = { - "object": "list", - "data": [ + 'object': 'list', + 'data': [ { - "id": model_id, - "name": model_id, - "owned_by": "openai", - "openai": {"id": model_id}, - "urlIdx": idx, + 'id': model_id, + 'name': model_id, + 'owned_by': 'openai', + 'openai': {'id': model_id}, + 'urlIdx': idx, } for model_id in model_ids ], } - request_tasks.append( - asyncio.ensure_future(asyncio.sleep(0, model_list)) - ) + request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, model_list))) else: request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) @@ -417,73 +423,87 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list: for idx, response in enumerate(responses): if response: - url = request.app.state.config.OPENAI_API_BASE_URLS[idx] - api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + url = api_base_urls[idx] + api_config = api_configs.get( str(idx), - request.app.state.config.OPENAI_API_CONFIGS.get( - url, {} - ), # Legacy support + api_configs.get(url, {}), # Legacy support ) - connection_type = api_config.get("connection_type", "external") - prefix_id = api_config.get("prefix_id", None) - tags = api_config.get("tags", []) + connection_type = api_config.get('connection_type', 'external') + prefix_id = api_config.get('prefix_id', None) + tags = api_config.get('tags', []) + provider = api_config.get('provider', '') - model_list = ( - response if isinstance(response, list) else response.get("data", []) - ) + model_list = response if isinstance(response, list) else response.get('data', []) if not isinstance(model_list, list): # Catch non-list responses model_list = [] for model in model_list: # Remove name key if its value is None #16689 - if "name" in model and model["name"] is None: - del model["name"] + if 'name' in model and model['name'] is None: + del model['name'] if prefix_id: - model["id"] = ( - f"{prefix_id}.{model.get('id', model.get('name', ''))}" - ) + model['id'] = f'{prefix_id}.{model.get("id", model.get("name", ""))}' if tags: - model["tags"] = tags + model['tags'] = tags if connection_type: - model["connection_type"] = connection_type + model['connection_type'] = connection_type - log.debug(f"get_all_models:responses() {responses}") + if provider: + model['provider'] = provider + + log.debug(f'get_all_models:responses() {responses}') return responses async def get_filtered_models(models, user, db=None): # Filter models based on user access control + model_ids = [model['id'] for model in models.get('data', [])] + model_infos = {model_info.id: model_info for model_info in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + # Batch-fetch accessible resource IDs in a single query instead of N has_access calls + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='model', + resource_ids=list(model_infos.keys()), + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + filtered_models = [] - for model in models.get("data", []): - model_info = Models.get_model_by_id(model["id"], db=db) + for model in models.get('data', []): + model_info = model_infos.get(model['id']) if model_info: - if user.id == model_info.user_id or has_access( - user.id, type="read", access_control=model_info.access_control, db=db - ): + if user.id == model_info.user_id or model_info.id in accessible_model_ids: filtered_models.append(model) return filtered_models @cached( ttl=MODELS_CACHE_TTL, - key=lambda _, user: f"openai_all_models_{user.id}" if user else "openai_all_models", + key=lambda _, user: f'openai_all_models_{user.id}' if user else 'openai_all_models', ) async def get_all_models(request: Request, user: UserModel) -> dict[str, list]: - log.info("get_all_models()") + log.info('get_all_models()') if not request.app.state.config.ENABLE_OPENAI_API: - return {"data": []} + return {'data': []} + + # Cache config value locally to avoid repeated Redis lookups inside + # the nested loop in get_merged_models (one GET per model otherwise). + api_base_urls = request.app.state.config.OPENAI_API_BASE_URLS responses = await get_all_models_responses(request, user=user) def extract_data(response): - if response and "data" in response: - return response["data"] + if response and 'data' in response: + return response['data'] if isinstance(response, list): return response return None @@ -492,60 +512,68 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]: if any( name in model_id for name in [ - "babbage", - "dall-e", - "davinci", - "embedding", - "tts", - "whisper", + 'babbage', + 'dall-e', + 'davinci', + 'embedding', + 'tts', + 'whisper', ] ): return False return True def get_merged_models(model_lists): - log.debug(f"merge_models_lists {model_lists}") + log.debug(f'merge_models_lists {model_lists}') models = {} for idx, model_list in enumerate(model_lists): - if model_list is not None and "error" not in model_list: + if model_list is not None and 'error' not in model_list: for model in model_list: - model_id = model.get("id") or model.get("name") + model_id = model.get('id') or model.get('name') - if ( - "api.openai.com" - in request.app.state.config.OPENAI_API_BASE_URLS[idx] - and not is_supported_openai_models(model_id) - ): + base_url = api_base_urls[idx] + hostname = urlparse(base_url).hostname if base_url else None + if hostname == 'api.openai.com' and not is_supported_openai_models(model_id): # Skip unwanted OpenAI models continue if model_id and model_id not in models: - models[model_id] = { + merged = { **model, - "name": model.get("name", model_id), - "owned_by": "openai", - "openai": model, - "connection_type": model.get("connection_type", "external"), - "urlIdx": idx, + 'name': model.get('name', model_id), + 'owned_by': 'openai', + 'openai': model, + 'connection_type': model.get('connection_type', 'external'), + 'provider': model.get('provider', ''), + 'urlIdx': idx, } + # llama.cpp router mode: derive loaded state from + # the status object returned by GET /v1/models. + status = model.get('status') + if isinstance(status, dict) and 'value' in status: + merged['loaded'] = status['value'] in ('loaded', 'sleeping') + + models[model_id] = merged + return models models = get_merged_models(map(extract_data, responses)) - log.debug(f"models: {models}") + log.debug(f'models: {models}') request.app.state.OPENAI_MODELS = models - return {"data": list(models.values())} + return {'data': list(models.values())} -@router.get("/models") -@router.get("/models/{url_idx}") -async def get_models( - request: Request, url_idx: Optional[int] = None, user=Depends(get_verified_user) -): +@router.get('/models') +@router.get('/models/{url_idx}') +async def get_models(request: Request, url_idx: int | None = None, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_OPENAI_API: + raise HTTPException(status_code=503, detail='OpenAI API is disabled') + models = { - "data": [], + 'data': [], } if url_idx is None: @@ -565,47 +593,49 @@ async def get_models( timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), ) as session: try: - headers, cookies = await get_headers_and_cookies( - request, url, key, api_config, user=user - ) + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) - if api_config.get("azure", False): + if api_config.get('azure') or api_config.get('provider') == 'azure': models = { - "data": api_config.get("model_ids", []) or [], - "object": "list", + 'data': api_config.get('model_ids', []) or [], + 'object': 'list', } + elif is_anthropic_url(url): + models = await get_anthropic_models(url, key, user=user) + if models is None: + raise Exception('Failed to connect to Anthropic API') else: async with session.get( - f"{url}/models", + f'{url}/models', headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: if r.status != 200: - # Extract response error details if available - error_detail = f"HTTP Error: {r.status}" - res = await r.json() - if "error" in res: - error_detail = f"External Error: {res['error']}" + error_detail = f'HTTP Error: {r.status}' + try: + res = await r.json() + if 'error' in res: + error_detail = f'External Error: {res["error"]}' + except Exception: + pass raise Exception(error_detail) response_data = await r.json() - # Check if we're calling OpenAI API based on the URL - if "api.openai.com" in url: - # Filter models according to the specified conditions - response_data["data"] = [ + if 'api.openai.com' in url: + response_data['data'] = [ model - for model in response_data.get("data", []) + for model in response_data.get('data', []) if not any( - name in model["id"] + name in model['id'] for name in [ - "babbage", - "dall-e", - "davinci", - "embedding", - "tts", - "whisper", + 'babbage', + 'dall-e', + 'davinci', + 'embedding', + 'tts', + 'whisper', ] ) ] @@ -613,17 +643,15 @@ async def get_models( models = response_data except aiohttp.ClientError as e: # ClientError covers all aiohttp requests issues - log.exception(f"Client error: {str(e)}") - raise HTTPException( - status_code=500, detail="Open WebUI: Server Connection Error" - ) + log.exception(f'Client error: {str(e)}') + raise HTTPException(status_code=500, detail='Open WebUI: Server Connection Error') except Exception as e: - log.exception(f"Unexpected error: {e}") - error_detail = f"Unexpected error: {str(e)}" + log.exception(f'Unexpected error: {e}') + error_detail = f'Unexpected error: {str(e)}' raise HTTPException(status_code=500, detail=error_detail) - if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL: - models["data"] = await get_filtered_models(models, user) + if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL: + models['data'] = await get_filtered_models(models, user) return models @@ -632,10 +660,10 @@ class ConnectionVerificationForm(BaseModel): url: str key: str - config: Optional[dict] = None + config: dict | None = None -@router.post("/verify") +@router.post('/verify') async def verify_connection( request: Request, form_data: ConnectionVerificationForm, @@ -651,19 +679,17 @@ async def verify_connection( timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), ) as session: try: - headers, cookies = await get_headers_and_cookies( - request, url, key, api_config, user=user - ) + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) - if api_config.get("azure", False): + if api_config.get('azure') or api_config.get('provider') == 'azure': # Only set api-key header if not using Azure Entra ID authentication - auth_type = api_config.get("auth_type", "bearer") - if auth_type not in ("azure_ad", "microsoft_entra_id"): - headers["api-key"] = key + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key - api_version = api_config.get("api_version", "") or "2023-03-15-preview" + api_version = api_config.get('api_version', '') or '2023-03-15-preview' async with session.get( - url=f"{url}/openai/models?api-version={api_version}", + url=f'{url}/openai/models?api-version={api_version}', headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, @@ -675,18 +701,21 @@ async def verify_connection( if r.status != 200: if isinstance(response_data, (dict, list)): - return JSONResponse( - status_code=r.status, content=response_data - ) + return JSONResponse(status_code=r.status, content=response_data) else: - return PlainTextResponse( - status_code=r.status, content=response_data - ) + return PlainTextResponse(status_code=r.status, content=response_data) return response_data + elif is_anthropic_url(url): + result = await get_anthropic_models(url, key) + if result is None: + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + if 'error' in result: + raise HTTPException(status_code=500, detail=result['error']) + return result else: async with session.get( - f"{url}/models", + f'{url}/models', headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, @@ -698,172 +727,382 @@ async def verify_connection( if r.status != 200: if isinstance(response_data, (dict, list)): - return JSONResponse( - status_code=r.status, content=response_data - ) + return JSONResponse(status_code=r.status, content=response_data) else: - return PlainTextResponse( - status_code=r.status, content=response_data - ) + return PlainTextResponse(status_code=r.status, content=response_data) return response_data except aiohttp.ClientError as e: # ClientError covers all aiohttp requests issues - log.exception(f"Client error: {str(e)}") - raise HTTPException( - status_code=500, detail="Open WebUI: Server Connection Error" - ) + log.exception(f'Client error: {str(e)}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) except Exception as e: - log.exception(f"Unexpected error: {e}") - raise HTTPException( - status_code=500, detail="Open WebUI: Server Connection Error" - ) + log.exception(f'Unexpected error: {e}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) def get_azure_allowed_params(api_version: str) -> set[str]: allowed_params = { - "messages", - "temperature", - "role", - "content", - "contentPart", - "contentPartImage", - "enhancements", - "dataSources", - "n", - "stream", - "stop", - "max_tokens", - "presence_penalty", - "frequency_penalty", - "logit_bias", - "user", - "function_call", - "functions", - "tools", - "tool_choice", - "top_p", - "log_probs", - "top_logprobs", - "response_format", - "seed", - "max_completion_tokens", - "reasoning_effort", + 'messages', + 'temperature', + 'role', + 'content', + 'contentPart', + 'contentPartImage', + 'enhancements', + 'dataSources', + 'n', + 'stream', + 'stop', + 'max_tokens', + 'presence_penalty', + 'frequency_penalty', + 'logit_bias', + 'user', + 'function_call', + 'functions', + 'tools', + 'tool_choice', + 'top_p', + 'log_probs', + 'top_logprobs', + 'response_format', + 'seed', + 'max_completion_tokens', + 'reasoning_effort', } try: - if api_version >= "2024-09-01-preview": - allowed_params.add("stream_options") + if api_version >= '2024-09-01-preview': + allowed_params.add('stream_options') except ValueError: - log.debug( - f"Invalid API version {api_version} for Azure OpenAI. Defaulting to allowed parameters." - ) + log.debug(f'Invalid API version {api_version} for Azure OpenAI. Defaulting to allowed parameters.') return allowed_params -def is_openai_reasoning_model(model: str) -> bool: - return model.lower().startswith(("o1", "o3", "o4", "gpt-5")) +def is_openai_new_model(model: str) -> bool: + model_lower = model.lower() + # o-series models (o1, o3, o4, o5, ...) + if re.match(r'^o\d+', model_lower): + return True + # gpt-N where N >= 5 (gpt-5, gpt-5.2, gpt-6, ...) + m = re.match(r'^gpt-(\d+)', model_lower) + if m and int(m.group(1)) >= 5: + return True + return False + + +def _sanitize_model_for_url(model: str) -> str: + """Sanitize a model name before interpolating it into a URL path. + + Rejects path traversal attempts (../, /, \\) and percent-encodes + the name so it is safe to use as a single URL path segment + (e.g. Azure deployment name). + """ + if not model or '..' in model or '/' in model or '\\' in model: + raise HTTPException( + status_code=400, + detail='Invalid model name: must not be empty or contain path separators or traversal sequences', + ) + return quote(model, safe='') def convert_to_azure_payload(url, payload: dict, api_version: str): - model = payload.get("model", "") + model = payload.get('model', '') # Filter allowed parameters based on Azure OpenAI API allowed_params = get_azure_allowed_params(api_version) # Special handling for o-series models - if is_openai_reasoning_model(model): + if is_openai_new_model(model): # Convert max_tokens to max_completion_tokens for o-series models - if "max_tokens" in payload: - payload["max_completion_tokens"] = payload["max_tokens"] - del payload["max_tokens"] + if 'max_tokens' in payload: + payload['max_completion_tokens'] = payload['max_tokens'] + del payload['max_tokens'] # Remove temperature if not 1 for o-series models - if "temperature" in payload and payload["temperature"] != 1: + if 'temperature' in payload and payload['temperature'] != 1: log.debug( - f"Removing temperature parameter for o-series model {model} as only default value (1) is supported" + f'Removing temperature parameter for o-series model {model} as only default value (1) is supported' ) - del payload["temperature"] + del payload['temperature'] # Filter out unsupported parameters payload = {k: v for k, v in payload.items() if k in allowed_params} - url = f"{url}/openai/deployments/{model}" + # Sanitize model name to prevent path traversal in the deployment URL + model = _sanitize_model_for_url(model) + + url = f'{url}/openai/deployments/{model}' return url, payload -@router.post("/chat/completions") +# Fields accepted by the Responses API for each input item type. +RESPONSES_ALLOWED_FIELDS: dict[str, set[str]] = { + 'message': {'type', 'role', 'content'}, + 'function_call': {'type', 'call_id', 'name', 'arguments', 'id'}, + 'function_call_output': {'type', 'call_id', 'output'}, +} + + +def _normalize_stored_item(item: dict) -> dict: + """Strip local-only fields from a stored output item before replaying it. + + Open WebUI stores extra bookkeeping fields (``id``, ``status``, + ``started_at``, ``ended_at``, ``duration``, ``_tag_type``, + ``attributes``, ``summary``, etc.) that the Responses API does + not accept. This helper returns a copy containing only the + fields the API understands. + """ + item_type = item.get('type', '') + allowed = RESPONSES_ALLOWED_FIELDS.get(item_type) + if allowed is None: + # Unknown type — pass through as-is (e.g. reasoning, extension items). + return item + return {k: v for k, v in item.items() if k in allowed} + + +def convert_to_responses_payload(payload: dict) -> dict: + """ + Convert Chat Completions payload to Responses API format. + + Chat Completions: { messages: [{role, content}], ... } + Responses API: { input: [{type: "message", role, content: [...]}], instructions: "system" } + """ + messages = payload.pop('messages', []) + + system_content = '' + input_items = [] + + for msg in messages: + role = msg.get('role', 'user') + content = msg.get('content', '') + + # Check for stored output items (from previous Responses API turn) + stored_output = msg.get('output') + if stored_output and isinstance(stored_output, list): + input_items.extend(_normalize_stored_item(item) for item in stored_output) + continue + + if role == 'system': + if isinstance(content, str): + system_content = content + elif isinstance(content, list): + system_content = '\n'.join(p.get('text', '') for p in content if p.get('type') == 'text') + continue + + # Handle assistant messages with tool_calls (from convert_output_to_messages) + if role == 'assistant' and msg.get('tool_calls'): + # Add text content as message if present + if content: + text = ( + content + if isinstance(content, str) + else '\n'.join(p.get('text', '') for p in content if p.get('type') == 'text') + ) + if text.strip(): + input_items.append( + { + 'type': 'message', + 'role': 'assistant', + 'content': [{'type': 'output_text', 'text': text}], + } + ) + # Convert each tool_call to a function_call input item + for tool_call in msg['tool_calls']: + func = tool_call.get('function', {}) + input_items.append( + { + 'type': 'function_call', + 'call_id': tool_call.get('id', ''), + 'name': func.get('name', ''), + 'arguments': func.get('arguments', '{}'), + } + ) + continue + + # Handle tool result messages + if role == 'tool': + input_items.append( + { + 'type': 'function_call_output', + 'call_id': msg.get('tool_call_id', ''), + 'output': msg.get('content', ''), + } + ) + continue + + # Convert content format + text_type = 'output_text' if role == 'assistant' else 'input_text' + + if isinstance(content, str): + content_parts = [{'type': text_type, 'text': content}] + elif isinstance(content, list): + content_parts = [] + for part in content: + if part.get('type') == 'text': + content_parts.append({'type': text_type, 'text': part.get('text', '')}) + elif part.get('type') == 'image_url': + url_data = part.get('image_url', {}) + url = url_data.get('url', '') if isinstance(url_data, dict) else url_data + content_parts.append({'type': 'input_image', 'image_url': url}) + else: + content_parts = [{'type': text_type, 'text': str(content)}] + + input_items.append({'type': 'message', 'role': role, 'content': content_parts}) + + responses_payload = {**payload, 'input': input_items} + + # Forward previous_response_id when the middleware has set it + # (only used when ENABLE_RESPONSES_API_STATEFUL is enabled). + previous_response_id = responses_payload.pop('previous_response_id', None) + if previous_response_id: + responses_payload['previous_response_id'] = previous_response_id + + if system_content: + responses_payload['instructions'] = system_content + + if 'max_tokens' in responses_payload: + responses_payload['max_output_tokens'] = responses_payload.pop('max_tokens') + + if 'max_completion_tokens' in responses_payload: + responses_payload['max_output_tokens'] = responses_payload.pop('max_completion_tokens') + + # Remove Chat Completions-only parameters not supported by the Responses API + for unsupported_key in ( + 'stream_options', + 'logit_bias', + 'frequency_penalty', + 'presence_penalty', + 'stop', + ): + responses_payload.pop(unsupported_key, None) + + # Convert Chat Completions tools format to Responses API format + # Chat Completions: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}} + # Responses API: {"type": "function", "name": ..., "description": ..., "parameters": ...} + if 'tools' in responses_payload and isinstance(responses_payload['tools'], list): + converted_tools = [] + for tool in responses_payload['tools']: + if isinstance(tool, dict) and 'function' in tool: + func = tool['function'] + converted_tool = {'type': tool.get('type', 'function')} + if isinstance(func, dict): + converted_tool['name'] = func.get('name', '') + if 'description' in func: + converted_tool['description'] = func['description'] + if 'parameters' in func: + converted_tool['parameters'] = func['parameters'] + if 'strict' in func: + converted_tool['strict'] = func['strict'] + converted_tools.append(converted_tool) + else: + # Already in correct format or unknown format, pass through + converted_tools.append(tool) + responses_payload['tools'] = converted_tools + + return responses_payload + + +def convert_responses_result(response: dict) -> dict: + """ + Convert non-streaming Responses API result to Chat Completions format. + + Extracts text from message output items so all downstream consumers + (frontend tasks, get_content_from_response) work without modification. + """ + output_items = response.get('output', []) + + content = '' + for item in output_items: + if item.get('type') == 'message': + for part in item.get('content', []): + if part.get('type') == 'output_text': + content += part.get('text', '') + + return { + 'id': response.get('id', ''), + 'object': 'chat.completion', + 'model': response.get('model', ''), + 'choices': [ + { + 'index': 0, + 'message': { + 'role': 'assistant', + 'content': content, + }, + 'finish_reason': 'stop', + } + ], + 'usage': response.get('usage', {}), + } + + +@router.post('/chat/completions') async def generate_chat_completion( request: Request, form_data: dict, user=Depends(get_verified_user), - bypass_filter: Optional[bool] = False, - bypass_system_prompt: bool = False, - db: Session = Depends(get_session), ): + # NOTE: We intentionally do NOT use Depends(get_async_session) here. + # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. + # This prevents holding a connection during the entire LLM call (30-60+ seconds), + # which would exhaust the connection pool under concurrent load. + + # bypass_filter and bypass_system_prompt are read from request.state to prevent + # external clients from setting them via query parameter. Only internal + # server-side callers (e.g. utils/chat.py) should set + # request.state.bypass_filter / request.state.bypass_system_prompt = True. + bypass_filter = getattr(request.state, 'bypass_filter', False) if BYPASS_MODEL_ACCESS_CONTROL: bypass_filter = True + bypass_system_prompt = getattr(request.state, 'bypass_system_prompt', False) idx = 0 payload = {**form_data} - metadata = payload.pop("metadata", None) + metadata = payload.pop('metadata', None) - model_id = form_data.get("model") - model_info = Models.get_model_by_id(model_id, db=db) + model_id = form_data.get('model') + model_info = await Models.get_model_by_id(model_id) # Check model info and override the payload if model_info: if model_info.base_model_id: base_model_id = ( - request.base_model_id - if hasattr(request, "base_model_id") - else model_info.base_model_id + request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id ) # Use request's base_model_id if available - payload["model"] = base_model_id + payload['model'] = base_model_id model_id = base_model_id params = model_info.params.model_dump() if params: - system = params.pop("system", None) + system = params.pop('system', None) payload = apply_model_params_to_body_openai(params, payload) if not bypass_system_prompt: - payload = apply_system_prompt_to_body(system, payload, metadata, user) + payload = await apply_system_prompt_to_body(system, payload, metadata, user) - # Check if user has access to the model - if not bypass_filter and user.role == "user": - if not ( - user.id == model_info.user_id - or has_access( - user.id, - type="read", - access_control=model_info.access_control, - db=db, - ) - ): - raise HTTPException( - status_code=403, - detail="Model not found", - ) - elif not bypass_filter: - if user.role != "admin": - raise HTTPException( - status_code=403, - detail="Model not found", - ) + await check_model_access(user, model_info, bypass_filter) + else: + await check_model_access(user, None, bypass_filter) + + # Check if model is already in app state cache to avoid expensive get_all_models() call + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + model = models.get(model_id) - await get_all_models(request, user=user) - model = request.app.state.OPENAI_MODELS.get(model_id) if model: - idx = model["urlIdx"] + idx = model['urlIdx'] else: raise HTTPException( status_code=404, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Get the API config for the model @@ -874,90 +1113,131 @@ async def generate_chat_completion( ), # Legacy support ) - prefix_id = api_config.get("prefix_id", None) + prefix_id = api_config.get('prefix_id', None) if prefix_id: - payload["model"] = payload["model"].replace(f"{prefix_id}.", "") + payload['model'] = payload['model'].replace(f'{prefix_id}.', '') # Add user info to the payload if the model is a pipeline - if "pipeline" in model and model.get("pipeline"): - payload["user"] = { - "name": user.name, - "id": user.id, - "email": user.email, - "role": user.role, + if 'pipeline' in model and model.get('pipeline'): + payload['user'] = { + 'name': user.name, + 'id': user.id, + 'email': user.email, + 'role': user.role, } url = request.app.state.config.OPENAI_API_BASE_URLS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx] # Check if model is a reasoning model that needs special handling - if is_openai_reasoning_model(payload["model"]): + if is_openai_new_model(payload['model']): payload = openai_reasoning_model_handler(payload) - elif "api.openai.com" not in url: + elif 'api.openai.com' not in url: # Remove "max_completion_tokens" from the payload for backward compatibility - if "max_completion_tokens" in payload: - payload["max_tokens"] = payload["max_completion_tokens"] - del payload["max_completion_tokens"] + if 'max_completion_tokens' in payload: + payload['max_tokens'] = payload['max_completion_tokens'] + del payload['max_completion_tokens'] - if "max_tokens" in payload and "max_completion_tokens" in payload: - del payload["max_tokens"] + if 'max_tokens' in payload and 'max_completion_tokens' in payload: + del payload['max_tokens'] # Convert the modified body back to JSON - if "logit_bias" in payload and payload["logit_bias"]: - logit_bias = convert_logit_bias_input_to_json(payload["logit_bias"]) + if 'logit_bias' in payload and payload['logit_bias']: + logit_bias = convert_logit_bias_input_to_json(payload['logit_bias']) if logit_bias: - payload["logit_bias"] = json.loads(logit_bias) + payload['logit_bias'] = json.loads(logit_bias) - headers, cookies = await get_headers_and_cookies( - request, url, key, api_config, metadata, user=user - ) + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, metadata, user=user) - if api_config.get("azure", False): - api_version = api_config.get("api_version", "2023-03-15-preview") - request_url, payload = convert_to_azure_payload(url, payload, api_version) + is_responses = api_config.get('api_type') == 'responses' + if api_config.get('azure') or api_config.get('provider') == 'azure': # Only set api-key header if not using Azure Entra ID authentication - auth_type = api_config.get("auth_type", "bearer") - if auth_type not in ("azure_ad", "microsoft_entra_id"): - headers["api-key"] = key + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key - headers["api-version"] = api_version - request_url = f"{request_url}/chat/completions?api-version={api_version}" + # Azure v1 format: base URL already ends with /openai/v1, + # model stays in the payload, no deployment URL rewriting. + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) + + if is_azure_v1: + if is_responses: + payload = convert_to_responses_payload(payload) + request_url = f'{url.rstrip("/")}/responses' + else: + request_url = f'{url.rstrip("/")}/chat/completions' + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + request_url, payload = convert_to_azure_payload(url, payload, api_version) + headers['api-version'] = api_version + + if is_responses: + payload = convert_to_responses_payload(payload) + request_url = f'{request_url}/responses?api-version={api_version}' + else: + request_url = f'{request_url}/chat/completions?api-version={api_version}' else: - request_url = f"{url}/chat/completions" + if is_responses: + payload = convert_to_responses_payload(payload) + request_url = f'{url}/responses' + else: + request_url = f'{url}/chat/completions' + # For Chat Completions, strip image parts from multimodal tool messages + # (Chat Completions doesn't support images in tool content). + if not is_responses and 'messages' in payload: + for message in payload['messages']: + if message.get('role') == 'tool' and isinstance(message.get('content'), list): + message['content'] = ''.join( + part.get('text', '') for part in message['content'] if part.get('type') in ('input_text', 'text') + ) payload = json.dumps(payload) r = None - session = None streaming = False response = None try: - session = aiohttp.ClientSession( - trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) - ) + session = await get_session() r = await session.request( - method="POST", + method='POST', url=request_url, data=payload, headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), ) # Check if response is SSE - if "text/event-stream" in r.headers.get("Content-Type", ""): + if 'text/event-stream' in r.headers.get('Content-Type', ''): + # If the provider returned an error status with SSE content-type, + # read the body and return a proper error response instead of + # streaming the error back (which hides the error from logs). + if r.status >= 400: + error_body = await r.text() + log.error( + 'Provider returned HTTP %d with SSE content-type: %s', + r.status, + error_body[:1000], + ) + try: + error_json = json.loads(error_body) + return JSONResponse(status_code=r.status, content=error_json) + except json.JSONDecodeError: + return JSONResponse( + status_code=r.status, + content={'error': {'message': error_body, 'code': r.status}}, + ) + streaming = True return StreamingResponse( - stream_chunks_handler(r.content), + stream_wrapper(r, content_handler=stream_chunks_handler), status_code=r.status, - headers=dict(r.headers), - background=BackgroundTask( - cleanup_response, response=r, session=session - ), + headers=_clean_proxy_headers(r.headers), ) else: try: @@ -972,17 +1252,21 @@ async def generate_chat_completion( else: return PlainTextResponse(status_code=r.status, content=response) + # Convert Responses API result to simple format + if is_responses and isinstance(response, dict): + response = convert_responses_result(response) + return response except Exception as e: log.exception(e) raise HTTPException( status_code=r.status if r else 500, - detail="Open WebUI: Server Connection Error", + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: if not streaming: - await cleanup_response(r, session) + await cleanup_response(r) async def embeddings(request: Request, form_data: dict, user): @@ -1001,11 +1285,14 @@ async def embeddings(request: Request, form_data: dict, user): # Prepare payload/body body = json.dumps(form_data) # Find correct backend url/key based on model - await get_all_models(request, user=user) - model_id = form_data.get("model") + model_id = form_data.get('model') + # Check if model is already in app state cache to avoid expensive get_all_models() call models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS if model_id in models: - idx = models[model_id]["urlIdx"] + idx = models[model_id]['urlIdx'] url = request.app.state.config.OPENAI_API_BASE_URLS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx] @@ -1015,31 +1302,27 @@ async def embeddings(request: Request, form_data: dict, user): ) r = None - session = None streaming = False - headers, cookies = await get_headers_and_cookies( - request, url, key, api_config, user=user - ) + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) try: - session = aiohttp.ClientSession(trust_env=True) + session = await get_session() r = await session.request( - method="POST", - url=f"{url}/embeddings", + method='POST', + url=f'{url}/embeddings', data=body, headers=headers, cookies=cookies, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) - if "text/event-stream" in r.headers.get("Content-Type", ""): + if 'text/event-stream' in r.headers.get('Content-Type', ''): streaming = True return StreamingResponse( - r.content, + stream_wrapper(r), status_code=r.status, - headers=dict(r.headers), - background=BackgroundTask( - cleanup_response, response=r, session=session - ), + headers=_clean_proxy_headers(r.headers), ) else: try: @@ -1051,31 +1334,177 @@ async def embeddings(request: Request, form_data: dict, user): if isinstance(response_data, (dict, list)): return JSONResponse(status_code=r.status, content=response_data) else: - return PlainTextResponse( - status_code=r.status, content=response_data - ) + return PlainTextResponse(status_code=r.status, content=response_data) return response_data except Exception as e: log.exception(e) raise HTTPException( status_code=r.status if r else 500, - detail="Open WebUI: Server Connection Error", + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: if not streaming: - await cleanup_response(r, session) + await cleanup_response(r) -@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +class ResponsesForm(BaseModel): + model_config = ConfigDict(extra='allow') + + model: str + input: list | str | None = None + instructions: str | None = None + stream: bool | None = None + temperature: float | None = None + max_output_tokens: int | None = None + top_p: float | None = None + tools: list | None = None + tool_choice: str | dict | None = None + text: dict | None = None + truncation: str | None = None + metadata: dict | None = None + store: bool | None = None + reasoning: dict | None = None + previous_response_id: str | None = None + + +@router.post('/responses') +async def responses( + request: Request, + form_data: ResponsesForm, + user=Depends(get_verified_user), +): + """ + Forward requests to the OpenAI Responses API endpoint. + Routes to the correct upstream backend based on the model field. + """ + payload = form_data.model_dump(exclude_none=True) + + idx = 0 + model_id = form_data.model + + # Enforce per-model access control + await check_model_access(user, await Models.get_model_by_id(model_id), BYPASS_MODEL_ACCESS_CONTROL) + + body = json.dumps(payload) + + if model_id: + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + if model_id in models: + idx = models[model_id]['urlIdx'] + + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] + key = request.app.state.config.OPENAI_API_KEYS[idx] + api_config = request.app.state.config.OPENAI_API_CONFIGS.get( + str(idx), + request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support + ) + + r = None + streaming = False + + try: + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + if api_config.get('azure') or api_config.get('provider') == 'azure': + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key + + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) + + if is_azure_v1: + request_url = f'{url.rstrip("/")}/responses' + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + headers['api-version'] = api_version + model = _sanitize_model_for_url(payload.get('model', '')) + request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}' + else: + request_url = f'{url}/responses' + + session = await get_session() + r = await session.request( + method='POST', + url=request_url, + data=body, + headers=headers, + cookies=cookies, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) + + # Check if response is SSE + if 'text/event-stream' in r.headers.get('Content-Type', ''): + streaming = True + return StreamingResponse( + stream_wrapper(r), + status_code=r.status, + headers=_clean_proxy_headers(r.headers), + ) + else: + try: + response_data = await r.json() + except Exception: + response_data = await r.text() + + if r.status >= 400: + if isinstance(response_data, (dict, list)): + return JSONResponse(status_code=r.status, content=response_data) + else: + return PlainTextResponse(status_code=r.status, content=response_data) + + return response_data + + except HTTPException: + raise + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=r.status if r else 500, + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, + ) + finally: + if not streaming: + await cleanup_response(r) + + +@router.api_route('/{path:path}', methods=['GET', 'POST', 'PUT', 'DELETE']) async def proxy(path: str, request: Request, user=Depends(get_verified_user)): """ - Deprecated: proxy all requests to OpenAI API + Deprecated: proxy all requests to OpenAI API. + Disabled by default. Set ENABLE_OPENAI_API_PASSTHROUGH=True to enable. """ + if not ENABLE_OPENAI_API_PASSTHROUGH: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail='Direct API passthrough is disabled. Set ENABLE_OPENAI_API_PASSTHROUGH=True to enable.', + ) + body = await request.body() + # Parse JSON body to resolve model-based routing + payload = None + if body: + try: + payload = json.loads(body) + except (json.JSONDecodeError, ValueError): + payload = None + idx = 0 + model_id = payload.get('model') if isinstance(payload, dict) else None + if model_id: + models = request.app.state.OPENAI_MODELS + if not models or model_id not in models: + await get_all_models(request, user=user) + models = request.app.state.OPENAI_MODELS + if model_id in models: + idx = models[model_id]['urlIdx'] + url = request.app.state.config.OPENAI_API_BASE_URLS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx] api_config = request.app.state.config.OPENAI_API_CONFIGS.get( @@ -1086,33 +1515,35 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): ) r = None - session = None streaming = False try: - headers, cookies = await get_headers_and_cookies( - request, url, key, api_config, user=user - ) - - if api_config.get("azure", False): - api_version = api_config.get("api_version", "2023-03-15-preview") + headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + if api_config.get('azure') or api_config.get('provider') == 'azure': # Only set api-key header if not using Azure Entra ID authentication - auth_type = api_config.get("auth_type", "bearer") - if auth_type not in ("azure_ad", "microsoft_entra_id"): - headers["api-key"] = key + auth_type = api_config.get('auth_type', 'bearer') + if auth_type not in ('azure_ad', 'microsoft_entra_id'): + headers['api-key'] = key - headers["api-version"] = api_version + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) - payload = json.loads(body) - url, payload = convert_to_azure_payload(url, payload, api_version) - body = json.dumps(payload).encode() + if is_azure_v1: + qs = request.url.query + request_url = f'{url.rstrip("/")}/{path}' + (f'?{qs}' if qs else '') + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + headers['api-version'] = api_version - request_url = f"{url}/{path}?api-version={api_version}" + payload = json.loads(body) + url, payload = convert_to_azure_payload(url, payload, api_version) + body = json.dumps(payload).encode() + + request_url = f'{url}/{path}?api-version={api_version}' else: - request_url = f"{url}/{path}" + request_url = f'{url}/{path}' - session = aiohttp.ClientSession(trust_env=True) + session = await get_session() r = await session.request( method=request.method, url=request_url, @@ -1120,18 +1551,16 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), ) # Check if response is SSE - if "text/event-stream" in r.headers.get("Content-Type", ""): + if 'text/event-stream' in r.headers.get('Content-Type', ''): streaming = True return StreamingResponse( - r.content, + stream_wrapper(r), status_code=r.status, - headers=dict(r.headers), - background=BackgroundTask( - cleanup_response, response=r, session=session - ), + headers=_clean_proxy_headers(r.headers), ) else: try: @@ -1143,18 +1572,18 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): if isinstance(response_data, (dict, list)): return JSONResponse(status_code=r.status, content=response_data) else: - return PlainTextResponse( - status_code=r.status, content=response_data - ) + return PlainTextResponse(status_code=r.status, content=response_data) return response_data + except HTTPException: + raise except Exception as e: log.exception(e) raise HTTPException( status_code=r.status if r else 500, - detail="Open WebUI: Server Connection Error", + detail='Open WebUI: Server Connection Error', ) finally: if not streaming: - await cleanup_response(r, session) + await cleanup_response(r) diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py index 7a42acffc1..5e0d4dc199 100644 --- a/backend/open_webui/routers/pipelines.py +++ b/backend/open_webui/routers/pipelines.py @@ -1,4 +1,11 @@ +import logging +import os +import shutil +from typing import Optional + +import aiohttp from fastapi import ( + APIRouter, Depends, FastAPI, File, @@ -7,25 +14,14 @@ from fastapi import ( Request, UploadFile, status, - APIRouter, ) -import aiohttp -import os -import logging -import shutil -import requests -from pydantic import BaseModel -from starlette.responses import FileResponse -from typing import Optional - -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES - - +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL from open_webui.routers.openai import get_all_models_responses - from open_webui.utils.auth import get_admin_user +from pydantic import BaseModel +from starlette.responses import FileResponse log = logging.getLogger(__name__) @@ -33,6 +29,8 @@ log = logging.getLogger(__name__) ################################## # # Pipeline Middleware +# Every hand this passes through can corrupt it or +# improve it. Let each stage leave it better than it found. # ################################## @@ -41,37 +39,34 @@ def get_sorted_filters(model_id, models): filters = [ model for model in models.values() - if "pipeline" in model - and "type" in model["pipeline"] - and model["pipeline"]["type"] == "filter" + if 'pipeline' in model + and 'type' in model['pipeline'] + and model['pipeline']['type'] == 'filter' and ( - model["pipeline"]["pipelines"] == ["*"] - or any( - model_id == target_model_id - for target_model_id in model["pipeline"]["pipelines"] - ) + model['pipeline']['pipelines'] == ['*'] + or any(model_id == target_model_id for target_model_id in model['pipeline']['pipelines']) ) ] - sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"]) + sorted_filters = sorted(filters, key=lambda x: x['pipeline']['priority']) return sorted_filters async def process_pipeline_inlet_filter(request, payload, user, models): - user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} - model_id = payload["model"] + user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role} + model_id = payload['model'] sorted_filters = get_sorted_filters(model_id, models) model = models[model_id] - if "pipeline" in model: + if 'pipeline' in model: sorted_filters.append(model) async with aiohttp.ClientSession(trust_env=True) as session: for filter in sorted_filters: - urlIdx = filter.get("urlIdx") + urlIdx = filter.get('urlIdx') try: urlIdx = int(urlIdx) - except: + except Exception: continue url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] @@ -80,51 +75,62 @@ async def process_pipeline_inlet_filter(request, payload, user, models): if not key: continue - headers = {"Authorization": f"Bearer {key}"} + headers = {'Authorization': f'Bearer {key}'} request_data = { - "user": user, - "body": payload, + 'user': user, + 'body': payload, } try: async with session.post( - f"{url}/{filter['id']}/filter/inlet", + f'{url}/{filter["id"]}/filter/inlet', headers=headers, json=request_data, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: - payload = await response.json() response.raise_for_status() + payload = await response.json() except aiohttp.ClientResponseError as e: - res = ( - await response.json() - if response.content_type == "application/json" - else {} + try: + res = await response.json() if 'application/json' in response.content_type else {} + if 'detail' in res: + raise HTTPException( + status_code=response.status, + detail=res['detail'], + ) + except HTTPException: + raise + except Exception: + pass + + raise HTTPException( + status_code=response.status, + detail=e.message, ) - if "detail" in res: - raise Exception(response.status, res["detail"]) + except HTTPException: + raise except Exception as e: - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') return payload async def process_pipeline_outlet_filter(request, payload, user, models): - user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} - model_id = payload["model"] + user = {'id': user.id, 'email': user.email, 'name': user.name, 'role': user.role} + model_id = payload['model'] sorted_filters = get_sorted_filters(model_id, models) model = models[model_id] - if "pipeline" in model: + if 'pipeline' in model: sorted_filters = [model] + sorted_filters async with aiohttp.ClientSession(trust_env=True) as session: for filter in sorted_filters: - urlIdx = filter.get("urlIdx") + urlIdx = filter.get('urlIdx') try: urlIdx = int(urlIdx) - except: + except Exception: continue url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] @@ -133,34 +139,42 @@ async def process_pipeline_outlet_filter(request, payload, user, models): if not key: continue - headers = {"Authorization": f"Bearer {key}"} + headers = {'Authorization': f'Bearer {key}'} request_data = { - "user": user, - "body": payload, + 'user': user, + 'body': payload, } try: async with session.post( - f"{url}/{filter['id']}/filter/outlet", + f'{url}/{filter["id"]}/filter/outlet', headers=headers, json=request_data, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: - payload = await response.json() response.raise_for_status() + payload = await response.json() except aiohttp.ClientResponseError as e: try: - res = ( - await response.json() - if "application/json" in response.content_type - else {} - ) - if "detail" in res: - raise Exception(response.status, res) + res = await response.json() if 'application/json' in response.content_type else {} + if 'detail' in res: + raise HTTPException( + status_code=response.status, + detail=res['detail'], + ) + except HTTPException: + raise except Exception: pass + + raise HTTPException( + status_code=response.status, + detail=e.message, + ) + except HTTPException: + raise except Exception as e: - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') return payload @@ -174,88 +188,94 @@ async def process_pipeline_outlet_filter(request, payload, user, models): router = APIRouter() -@router.get("/list") +@router.get('/list') async def get_pipelines_list(request: Request, user=Depends(get_admin_user)): responses = await get_all_models_responses(request, user) - log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}") + log.debug(f'get_pipelines_list: get_openai_models_responses returned {responses}') - urlIdxs = [ - idx - for idx, response in enumerate(responses) - if response is not None and "pipelines" in response - ] + urlIdxs = [idx for idx, response in enumerate(responses) if response is not None and 'pipelines' in response] return { - "data": [ + 'data': [ { - "url": request.app.state.config.OPENAI_API_BASE_URLS[urlIdx], - "idx": urlIdx, + 'url': request.app.state.config.OPENAI_API_BASE_URLS[urlIdx], + 'idx': urlIdx, } for urlIdx in urlIdxs ] } -@router.post("/upload") +@router.post('/upload') async def upload_pipeline( request: Request, urlIdx: int = Form(...), file: UploadFile = File(...), user=Depends(get_admin_user), ): - log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}") + log.info(f'upload_pipeline: urlIdx={urlIdx}, filename={file.filename}') filename = os.path.basename(file.filename) # Check if the uploaded file is a python file - if not (filename and filename.endswith(".py")): + if not (filename and filename.endswith('.py')): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Only Python (.py) files are allowed.", + detail='Only Python (.py) files are allowed.', ) - upload_folder = f"{CACHE_DIR}/pipelines" + upload_folder = f'{CACHE_DIR}/pipelines' os.makedirs(upload_folder, exist_ok=True) file_path = os.path.join(upload_folder, filename) - r = None + response = None try: # Save the uploaded file - with open(file_path, "wb") as buffer: + with open(file_path, 'wb') as buffer: shutil.copyfileobj(file.file, buffer) url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - with open(file_path, "rb") as f: - files = {"file": f} - r = requests.post( - f"{url}/pipelines/upload", - headers={"Authorization": f"Bearer {key}"}, - files=files, - ) + headers = {'Authorization': f'Bearer {key}'} - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + with open(file_path, 'rb') as f: + form_data = aiohttp.FormData() + form_data.add_field( + 'file', + f, + filename=filename, + content_type='application/octet-stream', + ) + + async with session.post( + f'{url}/pipelines/upload', + headers=headers, + data=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None status_code = status.HTTP_404_NOT_FOUND - if r is not None: - status_code = r.status_code + if response is not None: + status_code = response.status try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( status_code=status_code, - detail=detail if detail else "Pipeline not found", + detail=detail if detail else 'Pipeline not found', ) finally: # Ensure the file is deleted after the upload is completed or on failure @@ -268,43 +288,42 @@ class AddPipelineForm(BaseModel): urlIdx: int -@router.post("/add") -async def add_pipeline( - request: Request, form_data: AddPipelineForm, user=Depends(get_admin_user) -): - r = None +@router.post('/add') +async def add_pipeline(request: Request, form_data: AddPipelineForm, user=Depends(get_admin_user)): + response = None try: urlIdx = form_data.urlIdx url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - r = requests.post( - f"{url}/pipelines/add", - headers={"Authorization": f"Bearer {key}"}, - json={"url": form_data.url}, - ) - - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + f'{url}/pipelines/add', + headers={'Authorization': f'Bearer {key}'}, + json={'url': form_data.url}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None - if r is not None: + if response is not None: try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( - status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), - detail=detail if detail else "Pipeline not found", + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', ) @@ -313,160 +332,164 @@ class DeletePipelineForm(BaseModel): urlIdx: int -@router.delete("/delete") -async def delete_pipeline( - request: Request, form_data: DeletePipelineForm, user=Depends(get_admin_user) -): - r = None +@router.delete('/delete') +async def delete_pipeline(request: Request, form_data: DeletePipelineForm, user=Depends(get_admin_user)): + response = None try: urlIdx = form_data.urlIdx url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - r = requests.delete( - f"{url}/pipelines/delete", - headers={"Authorization": f"Bearer {key}"}, - json={"id": form_data.id}, - ) - - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.delete( + f'{url}/pipelines/delete', + headers={'Authorization': f'Bearer {key}'}, + json={'id': form_data.id}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None - if r is not None: + if response is not None: try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( - status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), - detail=detail if detail else "Pipeline not found", + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', ) -@router.get("/") -async def get_pipelines( - request: Request, urlIdx: Optional[int] = None, user=Depends(get_admin_user) -): - r = None +@router.get('/') +async def get_pipelines(request: Request, urlIdx: Optional[int] = None, user=Depends(get_admin_user)): + response = None try: url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - r = requests.get(f"{url}/pipelines", headers={"Authorization": f"Bearer {key}"}) - - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + f'{url}/pipelines', + headers={'Authorization': f'Bearer {key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None - if r is not None: + if response is not None: try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( - status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), - detail=detail if detail else "Pipeline not found", + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', ) -@router.get("/{pipeline_id}/valves") +@router.get('/{pipeline_id}/valves') async def get_pipeline_valves( request: Request, urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user), ): - r = None + response = None try: url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - r = requests.get( - f"{url}/{pipeline_id}/valves", headers={"Authorization": f"Bearer {key}"} - ) - - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + f'{url}/{pipeline_id}/valves', + headers={'Authorization': f'Bearer {key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None - if r is not None: + if response is not None: try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( - status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), - detail=detail if detail else "Pipeline not found", + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', ) -@router.get("/{pipeline_id}/valves/spec") +@router.get('/{pipeline_id}/valves/spec') async def get_pipeline_valves_spec( request: Request, urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user), ): - r = None + response = None try: url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - r = requests.get( - f"{url}/{pipeline_id}/valves/spec", - headers={"Authorization": f"Bearer {key}"}, - ) - - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + f'{url}/{pipeline_id}/valves/spec', + headers={'Authorization': f'Bearer {key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None - if r is not None: + if response is not None: try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( - status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), - detail=detail if detail else "Pipeline not found", + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', ) -@router.post("/{pipeline_id}/valves/update") +@router.post('/{pipeline_id}/valves/update') async def update_pipeline_valves( request: Request, urlIdx: Optional[int], @@ -474,36 +497,37 @@ async def update_pipeline_valves( form_data: dict, user=Depends(get_admin_user), ): - r = None + response = None try: url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - r = requests.post( - f"{url}/{pipeline_id}/valves/update", - headers={"Authorization": f"Bearer {key}"}, - json={**form_data}, - ) - - r.raise_for_status() - data = r.json() + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + f'{url}/{pipeline_id}/valves/update', + headers={'Authorization': f'Bearer {key}'}, + json={**form_data}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as response: + response.raise_for_status() + data = await response.json() return {**data} except Exception as e: # Handle connection error here - log.exception(f"Connection error: {e}") + log.exception(f'Connection error: {e}') detail = None - if r is not None: + if response is not None: try: - res = r.json() - if "detail" in res: - detail = res["detail"] + res = await response.json() + if 'detail' in res: + detail = res['detail'] except Exception: pass raise HTTPException( - status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), - detail=detail if detail else "Pipeline not found", + status_code=(response.status if response is not None else status.HTTP_404_NOT_FOUND), + detail=detail if detail else 'Pipeline not found', ) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 19d25685ad..94f18e499f 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -1,59 +1,136 @@ -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, status, Request +from __future__ import annotations +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.prompt_history import ( + PromptHistories, + PromptHistoryModel, + PromptHistoryResponse, +) from open_webui.models.prompts import ( - PromptForm, - PromptUserResponse, + PromptAccessListResponse, PromptAccessResponse, + PromptForm, PromptModel, Prompts, + PromptUserResponse, ) -from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access, has_permission -from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + + +class PromptVersionUpdateForm(BaseModel): + version_id: str + + +class PromptMetadataForm(BaseModel): + name: str + command: str + tags: list[str | None] = None + router = APIRouter() +PAGE_ITEM_COUNT = 30 + + ############################ # GetPrompts +# The hardest part is knowing what to ask. Let the right +# question already be here when it is needed. ############################ -@router.get("/", response_model=list[PromptModel]) -async def get_prompts( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: - prompts = Prompts.get_prompts(db=db) +@router.get('/', response_model=list[PromptModel]) +async def get_prompts(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + prompts = await Prompts.get_prompts(db=db) else: - prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db) + prompts = await Prompts.get_prompts_by_user_id(user.id, 'read', db=db) return prompts -@router.get("/list", response_model=list[PromptAccessResponse]) -async def get_prompt_list( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: - prompts = Prompts.get_prompts(db=db) - else: - prompts = Prompts.get_prompts_by_user_id(user.id, "read", db=db) +@router.get('/tags', response_model=list[str]) +async def get_prompt_tags(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Prompts.get_tags(db=db) + return await Prompts.get_tags_by_user_id(user.id, db=db) - return [ - PromptAccessResponse( - **prompt.model_dump(), - write_access=( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == prompt.user_id - or has_access(user.id, "write", prompt.access_control, db=db) - ), - ) - for prompt in prompts - ] + +@router.get('/list', response_model=PromptAccessListResponse) +async def get_prompt_list( + query: str | None = None, + view_option: str | None = None, + tag: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + if tag: + filter['tag'] = tag + if order_by: + filter['order_by'] = order_by + if direction: + filter['direction'] = direction + + # Pre-fetch user group IDs once - used for both filter and write_access check + groups = await Groups.get_groups_by_member_id(user.id, db=db) + user_group_ids = {group.id for group in groups} + + if not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL): + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Prompts.search_prompts(user.id, filter=filter, skip=skip, limit=limit, db=db) + + # Batch-fetch writable prompt IDs in a single query instead of N has_access calls + prompt_ids = [prompt.id for prompt in result.items] + writable_prompt_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user.id, + resource_type='prompt', + resource_ids=prompt_ids, + permission='write', + user_group_ids=user_group_ids, + db=db, + ) + + return PromptAccessListResponse( + items=[ + PromptAccessResponse( + **prompt.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == prompt.user_id + or prompt.id in writable_prompt_ids + ), + ) + for prompt in result.items + ], + total=result.total, + ) ############################ @@ -61,23 +138,23 @@ async def get_prompt_list( ############################ -@router.post("/create", response_model=Optional[PromptModel]) +@router.post('/create', response_model=PromptModel | None) async def create_new_prompt( request: Request, form_data: PromptForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not ( - has_permission( + if user.role != 'admin' and not ( + await has_permission( user.id, - "workspace.prompts", + 'workspace.prompts', request.app.state.config.USER_PERMISSIONS, db=db, ) - or has_permission( + or await has_permission( user.id, - "workspace.prompts_import", + 'workspace.prompts_import', request.app.state.config.USER_PERMISSIONS, db=db, ) @@ -87,9 +164,17 @@ async def create_new_prompt( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - prompt = Prompts.get_prompt_by_command(form_data.command, db=db) + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_prompts', + ) + + prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) if prompt is None: - prompt = Prompts.insert_new_prompt(user.id, form_data, db=db) + prompt = await Prompts.insert_new_prompt(user.id, form_data, db=db) if prompt: return prompt @@ -104,102 +189,519 @@ async def create_new_prompt( ############################ -# GetPromptByCommand +# GetPromptById ############################ -@router.get("/command/{command}", response_model=Optional[PromptAccessResponse]) -async def get_prompt_by_command( - command: str, user=Depends(get_verified_user), db: Session = Depends(get_session) +@router.get('/id/{prompt_id}', response_model=PromptAccessResponse | None) +async def get_prompt_by_id( + prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - prompt = Prompts.get_prompt_by_command(f"/{command}", db=db) + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) if prompt: if ( - user.role == "admin" + user.role == 'admin' or prompt.user_id == user.id - or has_access(user.id, "read", prompt.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) ): return PromptAccessResponse( **prompt.model_dump(), write_access=( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) or user.id == prompt.user_id - or has_access(user.id, "write", prompt.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) ), ) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, - ) + + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) ############################ -# UpdatePromptByCommand +# UpdatePromptById ############################ -@router.post("/command/{command}/update", response_model=Optional[PromptModel]) -async def update_prompt_by_command( - command: str, - form_data: PromptForm, - user=Depends(get_verified_user), - db: Session = Depends(get_session), +@router.post('/id/{prompt_id}/update', response_model=PromptModel | None) +async def update_prompt_by_id( + request: Request, prompt_id: str, form_data: PromptForm, + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - prompt = Prompts.get_prompt_by_command(f"/{command}", db=db) + """Update a prompt's content, creating a new history entry if changed.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + if not prompt: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) # Is the user the original creator, in a group with write access, or an admin if ( prompt.user_id != user.id - and not has_access(user.id, "write", prompt.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - prompt = Prompts.update_prompt_by_command(f"/{command}", form_data, db=db) - if prompt: - return prompt + # Check for command collision if command is being changed + if form_data.command != prompt.command: + existing_prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) + if existing_prompt and existing_prompt.id != prompt.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.COMMAND_TAKEN, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_prompts', + ) + + # Use the ID from the found prompt + updated_prompt = await Prompts.update_prompt_by_id(prompt.id, form_data, user.id, db=db) + if updated_prompt: + return updated_prompt else: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), ) ############################ -# DeletePromptByCommand +# UpdatePromptMetadata ############################ -@router.delete("/command/{command}/delete", response_model=bool) -async def delete_prompt_by_command( - command: str, user=Depends(get_verified_user), db: Session = Depends(get_session) +@router.post('/id/{prompt_id}/update/meta', response_model=PromptModel | None) +async def update_prompt_metadata( + prompt_id: str, + form_data: PromptMetadataForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), ): - prompt = Prompts.get_prompt_by_command(f"/{command}", db=db) + """Update prompt name and command only (no history created).""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + if not prompt: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) if ( prompt.user_id != user.id - and not has_access(user.id, "write", prompt.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - result = Prompts.delete_prompt_by_command(f"/{command}", db=db) + # Check for command collision if command is being changed + if form_data.command != prompt.command: + existing_prompt = await Prompts.get_prompt_by_command(form_data.command, db=db) + if existing_prompt and existing_prompt.id != prompt.id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.COMMAND_TAKEN, + ) + + updated_prompt = await Prompts.update_prompt_metadata( + prompt.id, form_data.name, form_data.command, form_data.tags, db=db + ) + if updated_prompt: + return updated_prompt + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +@router.post('/id/{prompt_id}/update/version', response_model=PromptModel | None) +async def set_prompt_version( + prompt_id: str, + form_data: PromptVersionUpdateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + updated_prompt = await Prompts.update_prompt_version(prompt.id, form_data.version_id, db=db) + if updated_prompt: + return updated_prompt + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# UpdatePromptAccessById +############################ + + +class PromptAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/id/{prompt_id}/access/update', response_model=PromptModel | None) +async def update_prompt_access_by_id( + request: Request, + prompt_id: str, + form_data: PromptAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_prompts', + ) + + await AccessGrants.set_access_grants('prompt', prompt_id, form_data.access_grants, db=db) + + return await Prompts.get_prompt_by_id(prompt_id, db=db) + + +############################ +# TogglePromptActiveById +############################ + + +@router.post('/id/{prompt_id}/toggle', response_model=PromptModel | None) +async def toggle_prompt_active( + prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Prompts.toggle_prompt_active(prompt.id, db=db) + if result: + return result + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# DeletePromptById +############################ + + +@router.delete('/id/{prompt_id}/delete', response_model=bool) +async def delete_prompt_by_id( + prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + prompt.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = await Prompts.delete_prompt_by_id(prompt.id, db=db) return result + + +############################ +# Prompt History Endpoints +############################ + + +@router.get('/id/{prompt_id}/history', response_model=list[PromptHistoryResponse]) +async def get_prompt_history( + prompt_id: str, + page: int = 0, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get version history for a prompt.""" + PAGE_SIZE = 20 + + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + history = await PromptHistories.get_history_by_prompt_id(prompt.id, limit=PAGE_SIZE, offset=page * PAGE_SIZE, db=db) + return history + + +@router.get('/id/{prompt_id}/history/{history_id}', response_model=PromptHistoryModel) +async def get_prompt_history_entry( + prompt_id: str, + history_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get a specific version from history.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + history_entry = await PromptHistories.get_history_entry_by_id(history_id, db=db) + if not history_entry or history_entry.prompt_id != prompt.id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return history_entry + + +@router.delete('/id/{prompt_id}/history/{history_id}', response_model=bool) +async def delete_prompt_history_entry( + prompt_id: str, + history_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Delete a history entry. Cannot delete the active production version.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check write access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='write', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + # Cannot delete active production version + if prompt.version_id == history_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Cannot delete the active production version', + ) + + success = await PromptHistories.delete_history_entry(history_id, prompt.id, db=db) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return success + + +@router.get('/id/{prompt_id}/history/diff') +async def get_prompt_diff( + prompt_id: str, + from_id: str, + to_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Get diff between two versions.""" + prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) + + if not prompt: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # Check read access + if not ( + user.role == 'admin' + or prompt.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='prompt', + resource_id=prompt.id, + permission='read', + db=db, + ) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + diff = await PromptHistories.compute_diff(from_id, to_id, prompt.id, db=db) + if not diff: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + return diff diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index db3f80f149..646ee554e1 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1,128 +1,131 @@ +from __future__ import annotations + +import asyncio import json import logging import mimetypes import os -import shutil -import asyncio - import re +import shutil import uuid from datetime import datetime from pathlib import Path -from typing import Iterator, List, Optional, Sequence, Union +from typing import Callable, Iterator, Optional, Sequence, Union +import tiktoken from fastapi import ( + APIRouter, Depends, FastAPI, - Query, File, Form, HTTPException, - UploadFile, + Query, Request, + UploadFile, status, - APIRouter, ) -from fastapi.middleware.cors import CORSMiddleware from fastapi.concurrency import run_in_threadpool -from pydantic import BaseModel -import tiktoken - - +from fastapi.middleware.cors import CORSMiddleware +from langchain_core.documents import Document from langchain_text_splitters import ( + MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter, TokenTextSplitter, - MarkdownHeaderTextSplitter, ) -from langchain_core.documents import Document - -from open_webui.models.files import FileModel, FileUpdateForm, Files +from open_webui.config import ( + DEFAULT_LOCALE, + ENV, + RAG_EMBEDDING_CONTENT_PREFIX, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_EMBEDDING_QUERY_PREFIX, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + UPLOAD_DIR, +) +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ( + DEVICE_TYPE, + DOCKER, + RAG_EMBEDDING_TIMEOUT, + SENTENCE_TRANSFORMERS_BACKEND, + SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND, + SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS, + SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION, + SENTENCE_TRANSFORMERS_MODEL_KWARGS, +) +from open_webui.internal.db import get_async_db, get_async_session +from open_webui.models.files import FileModel, Files, FileUpdateForm from open_webui.models.knowledge import Knowledges -from open_webui.storage.provider import Storage -from open_webui.internal.db import get_session -from sqlalchemy.orm import Session - - -from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT # Document loaders -from open_webui.retrieval.loaders.main import Loader from open_webui.retrieval.loaders.youtube import YoutubeLoader - -# Web search engines -from open_webui.retrieval.web.main import SearchResult -from open_webui.retrieval.web.utils import get_web_loader -from open_webui.retrieval.web.ollama import search_ollama_cloud -from open_webui.retrieval.web.perplexity_search import search_perplexity_search -from open_webui.retrieval.web.brave import search_brave -from open_webui.retrieval.web.kagi import search_kagi -from open_webui.retrieval.web.mojeek import search_mojeek -from open_webui.retrieval.web.bocha import search_bocha -from open_webui.retrieval.web.duckduckgo import search_duckduckgo -from open_webui.retrieval.web.google_pse import search_google_pse -from open_webui.retrieval.web.jina_search import search_jina -from open_webui.retrieval.web.searchapi import search_searchapi -from open_webui.retrieval.web.serpapi import search_serpapi -from open_webui.retrieval.web.searxng import search_searxng -from open_webui.retrieval.web.yacy import search_yacy -from open_webui.retrieval.web.serper import search_serper -from open_webui.retrieval.web.serply import search_serply -from open_webui.retrieval.web.serpstack import search_serpstack -from open_webui.retrieval.web.tavily import search_tavily -from open_webui.retrieval.web.bing import search_bing -from open_webui.retrieval.web.azure import search_azure -from open_webui.retrieval.web.exa import search_exa -from open_webui.retrieval.web.perplexity import search_perplexity -from open_webui.retrieval.web.sougou import search_sougou -from open_webui.retrieval.web.firecrawl import search_firecrawl -from open_webui.retrieval.web.external import search_external - from open_webui.retrieval.utils import ( + build_loader_from_config, + filter_accessible_collections, get_content_from_url, get_embedding_function, - get_reranking_function, get_model_path, + get_reranking_function, query_collection, query_collection_with_hybrid_search, query_doc, query_doc_with_hybrid_search, ) +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT +from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT from open_webui.retrieval.vector.utils import filter_metadata +from open_webui.retrieval.web.azure import search_azure +from open_webui.retrieval.web.bing import search_bing +from open_webui.retrieval.web.bocha import search_bocha +from open_webui.retrieval.web.brave import search_brave +from open_webui.retrieval.web.brave_llm_context import search_brave_llm_context +from open_webui.retrieval.web.duckduckgo import search_duckduckgo +from open_webui.retrieval.web.exa import search_exa +from open_webui.retrieval.web.external import search_external +from open_webui.retrieval.web.firecrawl import search_firecrawl +from open_webui.retrieval.web.google_pse import search_google_pse +from open_webui.retrieval.web.jina_search import search_jina +from open_webui.retrieval.web.kagi import search_kagi + +# Web search engines +from open_webui.retrieval.web.main import SearchResult +from open_webui.retrieval.web.mojeek import search_mojeek +from open_webui.retrieval.web.ollama import search_ollama_cloud +from open_webui.retrieval.web.perplexity import search_perplexity +from open_webui.retrieval.web.perplexity_search import search_perplexity_search +from open_webui.retrieval.web.searchapi import search_searchapi +from open_webui.retrieval.web.searxng import search_searxng +from open_webui.retrieval.web.serpapi import search_serpapi +from open_webui.retrieval.web.serper import search_serper +from open_webui.retrieval.web.serply import search_serply +from open_webui.retrieval.web.serpstack import search_serpstack +from open_webui.retrieval.web.sougou import search_sougou +from open_webui.retrieval.web.tavily import search_tavily +from open_webui.retrieval.web.utils import get_web_loader +from open_webui.retrieval.web.yacy import search_yacy +from open_webui.retrieval.web.yandex import search_yandex +from open_webui.retrieval.web.ydc import search_youcom +from open_webui.retrieval.web.linkup import search_linkup +from open_webui.storage.provider import Storage +from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.misc import ( calculate_sha256_string, sanitize_text_for_db, ) -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_permission - -from open_webui.config import ( - ENV, - RAG_EMBEDDING_MODEL_AUTO_UPDATE, - RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, - RAG_RERANKING_MODEL_AUTO_UPDATE, - RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, - UPLOAD_DIR, - DEFAULT_LOCALE, - RAG_EMBEDDING_CONTENT_PREFIX, - RAG_EMBEDDING_QUERY_PREFIX, -) -from open_webui.env import ( - DEVICE_TYPE, - DOCKER, - SENTENCE_TRANSFORMERS_BACKEND, - SENTENCE_TRANSFORMERS_MODEL_KWARGS, - SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND, - SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS, - SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION, -) - -from open_webui.constants import ERROR_MESSAGES +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) ########################################## # # Utility functions +# Give us this day our relevant chunks, and lead us +# not into hallucination, but deliver us from noise. # ########################################## @@ -133,7 +136,7 @@ def get_ef( auto_update: bool = RAG_EMBEDDING_MODEL_AUTO_UPDATE, ): ef = None - if embedding_model and engine == "": + if embedding_model and engine == '': from sentence_transformers import SentenceTransformer try: @@ -145,39 +148,37 @@ def get_ef( model_kwargs=SENTENCE_TRANSFORMERS_MODEL_KWARGS, ) except Exception as e: - log.debug(f"Error loading SentenceTransformer: {e}") + log.error(f'Error loading SentenceTransformer: {e}') return ef def get_rf( - engine: str = "", - reranking_model: Optional[str] = None, - external_reranker_url: str = "", - external_reranker_api_key: str = "", - external_reranker_timeout: str = "", + engine: str = '', + reranking_model: str | None = None, + external_reranker_url: str = '', + external_reranker_api_key: str = '', + external_reranker_timeout: str = '', auto_update: bool = RAG_RERANKING_MODEL_AUTO_UPDATE, ): rf = None # Convert timeout string to int or None (system default) - timeout_value = ( - int(external_reranker_timeout) if external_reranker_timeout else None - ) + timeout_value = int(external_reranker_timeout) if external_reranker_timeout else None if reranking_model: - if any(model in reranking_model for model in ["jinaai/jina-colbert-v2"]): + if any(model in reranking_model for model in ['jinaai/jina-colbert-v2']): try: from open_webui.retrieval.models.colbert import ColBERT rf = ColBERT( get_model_path(reranking_model, auto_update), - env="docker" if DOCKER else None, + env='docker' if DOCKER else None, ) except Exception as e: - log.error(f"ColBERT: {e}") + log.error(f'ColBERT: {e}') raise Exception(ERROR_MESSAGES.DEFAULT(e)) else: - if engine == "external": + if engine == 'external': try: from open_webui.retrieval.models.external import ExternalReranker @@ -188,7 +189,7 @@ def get_rf( timeout=timeout_value, ) except Exception as e: - log.error(f"ExternalReranking: {e}") + log.error(f'ExternalReranking: {e}') raise Exception(ERROR_MESSAGES.DEFAULT(e)) else: import sentence_transformers @@ -208,28 +209,24 @@ def get_rf( ), ) except Exception as e: - log.error(f"CrossEncoder: {e}") - raise Exception(ERROR_MESSAGES.DEFAULT("CrossEncoder error")) + log.error(f'CrossEncoder: {e}') + raise Exception(ERROR_MESSAGES.DEFAULT('CrossEncoder error')) # Safely adjust pad_token_id if missing as some models do not have this in config try: - model_cfg = getattr(rf, "model", None) - if model_cfg and hasattr(model_cfg, "config"): + model_cfg = getattr(rf, 'model', None) + if model_cfg and hasattr(model_cfg, 'config'): cfg = model_cfg.config - if getattr(cfg, "pad_token_id", None) is None: + if getattr(cfg, 'pad_token_id', None) is None: # Fallback to eos_token_id when available - eos = getattr(cfg, "eos_token_id", None) + eos = getattr(cfg, 'eos_token_id', None) if eos is not None: cfg.pad_token_id = eos - log.debug( - f"Missing pad_token_id detected; set to eos_token_id={eos}" - ) + log.debug(f'Missing pad_token_id detected; set to eos_token_id={eos}') else: - log.warning( - "Neither pad_token_id nor eos_token_id present in model config" - ) + log.warning('Neither pad_token_id nor eos_token_id present in model config') except Exception as e2: - log.warning(f"Failed to adjust pad_token_id on CrossEncoder: {e2}") + log.warning(f'Failed to adjust pad_token_id on CrossEncoder: {e2}') return rf @@ -245,7 +242,7 @@ router = APIRouter() class CollectionNameForm(BaseModel): - collection_name: Optional[str] = None + collection_name: str | None = None class ProcessUrlForm(CollectionNameForm): @@ -253,44 +250,30 @@ class ProcessUrlForm(CollectionNameForm): class SearchForm(BaseModel): - queries: List[str] + queries: list[str] -@router.get("/") -async def get_status(request: Request): - return { - "status": True, - "CHUNK_SIZE": request.app.state.config.CHUNK_SIZE, - "CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP, - "RAG_TEMPLATE": request.app.state.config.RAG_TEMPLATE, - "RAG_EMBEDDING_ENGINE": request.app.state.config.RAG_EMBEDDING_ENGINE, - "RAG_EMBEDDING_MODEL": request.app.state.config.RAG_EMBEDDING_MODEL, - "RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL, - "RAG_EMBEDDING_BATCH_SIZE": request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, - "ENABLE_ASYNC_EMBEDDING": request.app.state.config.ENABLE_ASYNC_EMBEDDING, - } - - -@router.get("/embedding") +@router.get('/embedding') async def get_embedding_config(request: Request, user=Depends(get_admin_user)): return { - "status": True, - "RAG_EMBEDDING_ENGINE": request.app.state.config.RAG_EMBEDDING_ENGINE, - "RAG_EMBEDDING_MODEL": request.app.state.config.RAG_EMBEDDING_MODEL, - "RAG_EMBEDDING_BATCH_SIZE": request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, - "ENABLE_ASYNC_EMBEDDING": request.app.state.config.ENABLE_ASYNC_EMBEDDING, - "openai_config": { - "url": request.app.state.config.RAG_OPENAI_API_BASE_URL, - "key": request.app.state.config.RAG_OPENAI_API_KEY, + 'status': True, + 'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL, + 'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + 'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING, + 'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + 'openai_config': { + 'url': request.app.state.config.RAG_OPENAI_API_BASE_URL, + 'key': request.app.state.config.RAG_OPENAI_API_KEY, }, - "ollama_config": { - "url": request.app.state.config.RAG_OLLAMA_BASE_URL, - "key": request.app.state.config.RAG_OLLAMA_API_KEY, + 'ollama_config': { + 'url': request.app.state.config.RAG_OLLAMA_BASE_URL, + 'key': request.app.state.config.RAG_OLLAMA_API_KEY, }, - "azure_openai_config": { - "url": request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, - "key": request.app.state.config.RAG_AZURE_OPENAI_API_KEY, - "version": request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, + 'azure_openai_config': { + 'url': request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, + 'key': request.app.state.config.RAG_AZURE_OPENAI_API_KEY, + 'version': request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, }, } @@ -312,79 +295,61 @@ class AzureOpenAIConfigForm(BaseModel): class EmbeddingModelUpdateForm(BaseModel): - openai_config: Optional[OpenAIConfigForm] = None - ollama_config: Optional[OllamaConfigForm] = None - azure_openai_config: Optional[AzureOpenAIConfigForm] = None + openai_config: OpenAIConfigForm | None = None + ollama_config: OllamaConfigForm | None = None + azure_openai_config: AzureOpenAIConfigForm | None = None RAG_EMBEDDING_ENGINE: str RAG_EMBEDDING_MODEL: str - RAG_EMBEDDING_BATCH_SIZE: Optional[int] = 1 - ENABLE_ASYNC_EMBEDDING: Optional[bool] = True + RAG_EMBEDDING_BATCH_SIZE: int | None = 1 + ENABLE_ASYNC_EMBEDDING: bool | None = True + RAG_EMBEDDING_CONCURRENT_REQUESTS: int | None = 0 def unload_embedding_model(request: Request): - if request.app.state.config.RAG_EMBEDDING_ENGINE == "": + if request.app.state.config.RAG_EMBEDDING_ENGINE == '': # unloads current internal embedding model and clears VRAM cache request.app.state.ef = None request.app.state.EMBEDDING_FUNCTION = None import gc gc.collect() - if DEVICE_TYPE == "cuda": + if DEVICE_TYPE == 'cuda': import torch if torch.cuda.is_available(): torch.cuda.empty_cache() -@router.post("/embedding/update") -async def update_embedding_config( - request: Request, form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user) -): +@router.post('/embedding/update') +async def update_embedding_config(request: Request, form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)): log.info( - f"Updating embedding model: {request.app.state.config.RAG_EMBEDDING_MODEL} to {form_data.RAG_EMBEDDING_MODEL}" + f'Updating embedding model: {request.app.state.config.RAG_EMBEDDING_MODEL} to {form_data.RAG_EMBEDDING_MODEL}' ) unload_embedding_model(request) try: request.app.state.config.RAG_EMBEDDING_ENGINE = form_data.RAG_EMBEDDING_ENGINE - request.app.state.config.RAG_EMBEDDING_MODEL = form_data.RAG_EMBEDDING_MODEL - request.app.state.config.RAG_EMBEDDING_BATCH_SIZE = ( - form_data.RAG_EMBEDDING_BATCH_SIZE - ) - request.app.state.config.ENABLE_ASYNC_EMBEDDING = ( - form_data.ENABLE_ASYNC_EMBEDDING - ) + request.app.state.config.RAG_EMBEDDING_MODEL = form_data.RAG_EMBEDDING_MODEL.strip() + request.app.state.config.RAG_EMBEDDING_BATCH_SIZE = form_data.RAG_EMBEDDING_BATCH_SIZE + request.app.state.config.ENABLE_ASYNC_EMBEDDING = form_data.ENABLE_ASYNC_EMBEDDING + request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS = form_data.RAG_EMBEDDING_CONCURRENT_REQUESTS if request.app.state.config.RAG_EMBEDDING_ENGINE in [ - "ollama", - "openai", - "azure_openai", + 'ollama', + 'openai', + 'azure_openai', ]: if form_data.openai_config is not None: - request.app.state.config.RAG_OPENAI_API_BASE_URL = ( - form_data.openai_config.url - ) - request.app.state.config.RAG_OPENAI_API_KEY = ( - form_data.openai_config.key - ) + request.app.state.config.RAG_OPENAI_API_BASE_URL = form_data.openai_config.url + request.app.state.config.RAG_OPENAI_API_KEY = form_data.openai_config.key if form_data.ollama_config is not None: - request.app.state.config.RAG_OLLAMA_BASE_URL = ( - form_data.ollama_config.url - ) - request.app.state.config.RAG_OLLAMA_API_KEY = ( - form_data.ollama_config.key - ) + request.app.state.config.RAG_OLLAMA_BASE_URL = form_data.ollama_config.url + request.app.state.config.RAG_OLLAMA_API_KEY = form_data.ollama_config.key if form_data.azure_openai_config is not None: - request.app.state.config.RAG_AZURE_OPENAI_BASE_URL = ( - form_data.azure_openai_config.url - ) - request.app.state.config.RAG_AZURE_OPENAI_API_KEY = ( - form_data.azure_openai_config.key - ) - request.app.state.config.RAG_AZURE_OPENAI_API_VERSION = ( - form_data.azure_openai_config.version - ) + request.app.state.config.RAG_AZURE_OPENAI_BASE_URL = form_data.azure_openai_config.url + request.app.state.config.RAG_AZURE_OPENAI_API_KEY = form_data.azure_openai_config.key + request.app.state.config.RAG_AZURE_OPENAI_API_VERSION = form_data.azure_openai_config.version request.app.state.ef = get_ef( request.app.state.config.RAG_EMBEDDING_ENGINE, @@ -397,345 +362,365 @@ async def update_embedding_config( request.app.state.ef, ( request.app.state.config.RAG_OPENAI_API_BASE_URL - if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' else ( request.app.state.config.RAG_OLLAMA_BASE_URL - if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' else request.app.state.config.RAG_AZURE_OPENAI_BASE_URL ) ), ( request.app.state.config.RAG_OPENAI_API_KEY - if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' else ( request.app.state.config.RAG_OLLAMA_API_KEY - if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' else request.app.state.config.RAG_AZURE_OPENAI_API_KEY ) ), request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, azure_api_version=( request.app.state.config.RAG_AZURE_OPENAI_API_VERSION - if request.app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' else None ), enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, ) return { - "status": True, - "RAG_EMBEDDING_ENGINE": request.app.state.config.RAG_EMBEDDING_ENGINE, - "RAG_EMBEDDING_MODEL": request.app.state.config.RAG_EMBEDDING_MODEL, - "RAG_EMBEDDING_BATCH_SIZE": request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, - "ENABLE_ASYNC_EMBEDDING": request.app.state.config.ENABLE_ASYNC_EMBEDDING, - "openai_config": { - "url": request.app.state.config.RAG_OPENAI_API_BASE_URL, - "key": request.app.state.config.RAG_OPENAI_API_KEY, + 'status': True, + 'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL, + 'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, + 'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING, + 'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, + 'openai_config': { + 'url': request.app.state.config.RAG_OPENAI_API_BASE_URL, + 'key': request.app.state.config.RAG_OPENAI_API_KEY, }, - "ollama_config": { - "url": request.app.state.config.RAG_OLLAMA_BASE_URL, - "key": request.app.state.config.RAG_OLLAMA_API_KEY, + 'ollama_config': { + 'url': request.app.state.config.RAG_OLLAMA_BASE_URL, + 'key': request.app.state.config.RAG_OLLAMA_API_KEY, }, - "azure_openai_config": { - "url": request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, - "key": request.app.state.config.RAG_AZURE_OPENAI_API_KEY, - "version": request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, + 'azure_openai_config': { + 'url': request.app.state.config.RAG_AZURE_OPENAI_BASE_URL, + 'key': request.app.state.config.RAG_AZURE_OPENAI_API_KEY, + 'version': request.app.state.config.RAG_AZURE_OPENAI_API_VERSION, }, } except Exception as e: - log.exception(f"Problem updating embedding model: {e}") + log.exception(f'Problem updating embedding model: {e}') raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DEFAULT(e), ) -@router.get("/config") +@router.get('/config') async def get_rag_config(request: Request, user=Depends(get_admin_user)): return { - "status": True, + 'status': True, # RAG settings - "RAG_TEMPLATE": request.app.state.config.RAG_TEMPLATE, - "TOP_K": request.app.state.config.TOP_K, - "BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL, - "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT, + 'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE, + 'TOP_K': request.app.state.config.TOP_K, + 'BYPASS_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL, + 'RAG_FULL_CONTEXT': request.app.state.config.RAG_FULL_CONTEXT, # Hybrid search settings - "ENABLE_RAG_HYBRID_SEARCH": request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, - "ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS": request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, - "TOP_K_RERANKER": request.app.state.config.TOP_K_RERANKER, - "RELEVANCE_THRESHOLD": request.app.state.config.RELEVANCE_THRESHOLD, - "HYBRID_BM25_WEIGHT": request.app.state.config.HYBRID_BM25_WEIGHT, + 'ENABLE_RAG_HYBRID_SEARCH': request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, + 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS': request.app.state.config.ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + 'TOP_K_RERANKER': request.app.state.config.TOP_K_RERANKER, + 'RELEVANCE_THRESHOLD': request.app.state.config.RELEVANCE_THRESHOLD, + 'HYBRID_BM25_WEIGHT': request.app.state.config.HYBRID_BM25_WEIGHT, # Content extraction settings - "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, - "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, - "DATALAB_MARKER_API_KEY": request.app.state.config.DATALAB_MARKER_API_KEY, - "DATALAB_MARKER_API_BASE_URL": request.app.state.config.DATALAB_MARKER_API_BASE_URL, - "DATALAB_MARKER_ADDITIONAL_CONFIG": request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, - "DATALAB_MARKER_SKIP_CACHE": request.app.state.config.DATALAB_MARKER_SKIP_CACHE, - "DATALAB_MARKER_FORCE_OCR": request.app.state.config.DATALAB_MARKER_FORCE_OCR, - "DATALAB_MARKER_PAGINATE": request.app.state.config.DATALAB_MARKER_PAGINATE, - "DATALAB_MARKER_STRIP_EXISTING_OCR": request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, - "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION": request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, - "DATALAB_MARKER_FORMAT_LINES": request.app.state.config.DATALAB_MARKER_FORMAT_LINES, - "DATALAB_MARKER_USE_LLM": request.app.state.config.DATALAB_MARKER_USE_LLM, - "DATALAB_MARKER_OUTPUT_FORMAT": request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, - "EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, - "EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, - "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, - "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL, - "DOCLING_API_KEY": request.app.state.config.DOCLING_API_KEY, - "DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS, - "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, - "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, - "DOCUMENT_INTELLIGENCE_MODEL": request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, - "MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL, - "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, + 'CONTENT_EXTRACTION_ENGINE': request.app.state.config.CONTENT_EXTRACTION_ENGINE, + 'PDF_EXTRACT_IMAGES': request.app.state.config.PDF_EXTRACT_IMAGES, + 'PDF_LOADER_MODE': request.app.state.config.PDF_LOADER_MODE, + 'DATALAB_MARKER_API_KEY': request.app.state.config.DATALAB_MARKER_API_KEY, + 'DATALAB_MARKER_API_BASE_URL': request.app.state.config.DATALAB_MARKER_API_BASE_URL, + 'DATALAB_MARKER_ADDITIONAL_CONFIG': request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, + 'DATALAB_MARKER_SKIP_CACHE': request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + 'DATALAB_MARKER_FORCE_OCR': request.app.state.config.DATALAB_MARKER_FORCE_OCR, + 'DATALAB_MARKER_PAGINATE': request.app.state.config.DATALAB_MARKER_PAGINATE, + 'DATALAB_MARKER_STRIP_EXISTING_OCR': request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION': request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + 'DATALAB_MARKER_FORMAT_LINES': request.app.state.config.DATALAB_MARKER_FORMAT_LINES, + 'DATALAB_MARKER_USE_LLM': request.app.state.config.DATALAB_MARKER_USE_LLM, + 'DATALAB_MARKER_OUTPUT_FORMAT': request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, + 'EXTERNAL_DOCUMENT_LOADER_URL': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, + 'EXTERNAL_DOCUMENT_LOADER_API_KEY': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, + 'TIKA_SERVER_URL': request.app.state.config.TIKA_SERVER_URL, + 'DOCLING_SERVER_URL': request.app.state.config.DOCLING_SERVER_URL, + 'DOCLING_API_KEY': request.app.state.config.DOCLING_API_KEY, + 'DOCLING_PARAMS': request.app.state.config.DOCLING_PARAMS, + 'DOCUMENT_INTELLIGENCE_ENDPOINT': request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + 'DOCUMENT_INTELLIGENCE_KEY': request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + 'DOCUMENT_INTELLIGENCE_MODEL': request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, + 'MISTRAL_OCR_API_BASE_URL': request.app.state.config.MISTRAL_OCR_API_BASE_URL, + 'MISTRAL_OCR_API_KEY': request.app.state.config.MISTRAL_OCR_API_KEY, + 'PADDLEOCR_VL_BASE_URL': request.app.state.config.PADDLEOCR_VL_BASE_URL, + 'PADDLEOCR_VL_TOKEN': request.app.state.config.PADDLEOCR_VL_TOKEN, # MinerU settings - "MINERU_API_MODE": request.app.state.config.MINERU_API_MODE, - "MINERU_API_URL": request.app.state.config.MINERU_API_URL, - "MINERU_API_KEY": request.app.state.config.MINERU_API_KEY, - "MINERU_API_TIMEOUT": request.app.state.config.MINERU_API_TIMEOUT, - "MINERU_PARAMS": request.app.state.config.MINERU_PARAMS, + 'MINERU_API_MODE': request.app.state.config.MINERU_API_MODE, + 'MINERU_API_URL': request.app.state.config.MINERU_API_URL, + 'MINERU_API_KEY': request.app.state.config.MINERU_API_KEY, + 'MINERU_API_TIMEOUT': request.app.state.config.MINERU_API_TIMEOUT, + 'MINERU_PARAMS': request.app.state.config.MINERU_PARAMS, + 'MINERU_FILE_EXTENSIONS': request.app.state.config.MINERU_FILE_EXTENSIONS, # Reranking settings - "RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL, - "RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE, - "RAG_EXTERNAL_RERANKER_URL": request.app.state.config.RAG_EXTERNAL_RERANKER_URL, - "RAG_EXTERNAL_RERANKER_API_KEY": request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, - "RAG_EXTERNAL_RERANKER_TIMEOUT": request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, + 'RAG_RERANKING_ENGINE': request.app.state.config.RAG_RERANKING_ENGINE, + 'RAG_RERANKING_BATCH_SIZE': request.app.state.config.RAG_RERANKING_BATCH_SIZE, + 'RAG_EXTERNAL_RERANKER_URL': request.app.state.config.RAG_EXTERNAL_RERANKER_URL, + 'RAG_EXTERNAL_RERANKER_API_KEY': request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + 'RAG_EXTERNAL_RERANKER_TIMEOUT': request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, # Chunking settings - "TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER, - "ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER": request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, - "CHUNK_SIZE": request.app.state.config.CHUNK_SIZE, - "CHUNK_MIN_SIZE_TARGET": request.app.state.config.CHUNK_MIN_SIZE_TARGET, - "CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP, + 'TEXT_SPLITTER': request.app.state.config.TEXT_SPLITTER, + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER': request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + 'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE, + 'CHUNK_MIN_SIZE_TARGET': request.app.state.config.CHUNK_MIN_SIZE_TARGET, + 'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP, # File upload settings - "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE, - "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT, - "FILE_IMAGE_COMPRESSION_WIDTH": request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, - "FILE_IMAGE_COMPRESSION_HEIGHT": request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, - "ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS, + 'FILE_MAX_SIZE': request.app.state.config.FILE_MAX_SIZE, + 'FILE_MAX_COUNT': request.app.state.config.FILE_MAX_COUNT, + 'FILE_IMAGE_COMPRESSION_WIDTH': request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'FILE_IMAGE_COMPRESSION_HEIGHT': request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + 'ALLOWED_FILE_EXTENSIONS': request.app.state.config.ALLOWED_FILE_EXTENSIONS, # Integration settings - "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, - "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + 'ENABLE_GOOGLE_DRIVE_INTEGRATION': request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'ENABLE_ONEDRIVE_INTEGRATION': request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, # Web search settings - "web": { - "ENABLE_WEB_SEARCH": request.app.state.config.ENABLE_WEB_SEARCH, - "WEB_SEARCH_ENGINE": request.app.state.config.WEB_SEARCH_ENGINE, - "WEB_SEARCH_TRUST_ENV": request.app.state.config.WEB_SEARCH_TRUST_ENV, - "WEB_SEARCH_RESULT_COUNT": request.app.state.config.WEB_SEARCH_RESULT_COUNT, - "WEB_SEARCH_CONCURRENT_REQUESTS": request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, - "WEB_LOADER_CONCURRENT_REQUESTS": request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, - "WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, - "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, - "BYPASS_WEB_SEARCH_WEB_LOADER": request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER, - "OLLAMA_CLOUD_WEB_SEARCH_API_KEY": request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, - "SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL, - "SEARXNG_LANGUAGE": request.app.state.config.SEARXNG_LANGUAGE, - "YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL, - "YACY_USERNAME": request.app.state.config.YACY_USERNAME, - "YACY_PASSWORD": request.app.state.config.YACY_PASSWORD, - "GOOGLE_PSE_API_KEY": request.app.state.config.GOOGLE_PSE_API_KEY, - "GOOGLE_PSE_ENGINE_ID": request.app.state.config.GOOGLE_PSE_ENGINE_ID, - "BRAVE_SEARCH_API_KEY": request.app.state.config.BRAVE_SEARCH_API_KEY, - "KAGI_SEARCH_API_KEY": request.app.state.config.KAGI_SEARCH_API_KEY, - "MOJEEK_SEARCH_API_KEY": request.app.state.config.MOJEEK_SEARCH_API_KEY, - "BOCHA_SEARCH_API_KEY": request.app.state.config.BOCHA_SEARCH_API_KEY, - "SERPSTACK_API_KEY": request.app.state.config.SERPSTACK_API_KEY, - "SERPSTACK_HTTPS": request.app.state.config.SERPSTACK_HTTPS, - "SERPER_API_KEY": request.app.state.config.SERPER_API_KEY, - "SERPLY_API_KEY": request.app.state.config.SERPLY_API_KEY, - "DDGS_BACKEND": request.app.state.config.DDGS_BACKEND, - "TAVILY_API_KEY": request.app.state.config.TAVILY_API_KEY, - "SEARCHAPI_API_KEY": request.app.state.config.SEARCHAPI_API_KEY, - "SEARCHAPI_ENGINE": request.app.state.config.SEARCHAPI_ENGINE, - "SERPAPI_API_KEY": request.app.state.config.SERPAPI_API_KEY, - "SERPAPI_ENGINE": request.app.state.config.SERPAPI_ENGINE, - "JINA_API_KEY": request.app.state.config.JINA_API_KEY, - "JINA_API_BASE_URL": request.app.state.config.JINA_API_BASE_URL, - "BING_SEARCH_V7_ENDPOINT": request.app.state.config.BING_SEARCH_V7_ENDPOINT, - "BING_SEARCH_V7_SUBSCRIPTION_KEY": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, - "EXA_API_KEY": request.app.state.config.EXA_API_KEY, - "PERPLEXITY_API_KEY": request.app.state.config.PERPLEXITY_API_KEY, - "PERPLEXITY_MODEL": request.app.state.config.PERPLEXITY_MODEL, - "PERPLEXITY_SEARCH_CONTEXT_USAGE": request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, - "PERPLEXITY_SEARCH_API_URL": request.app.state.config.PERPLEXITY_SEARCH_API_URL, - "SOUGOU_API_SID": request.app.state.config.SOUGOU_API_SID, - "SOUGOU_API_SK": request.app.state.config.SOUGOU_API_SK, - "WEB_LOADER_ENGINE": request.app.state.config.WEB_LOADER_ENGINE, - "WEB_LOADER_TIMEOUT": request.app.state.config.WEB_LOADER_TIMEOUT, - "ENABLE_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, - "PLAYWRIGHT_WS_URL": request.app.state.config.PLAYWRIGHT_WS_URL, - "PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT, - "FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY, - "FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL, - "FIRECRAWL_TIMEOUT": request.app.state.config.FIRECRAWL_TIMEOUT, - "TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH, - "EXTERNAL_WEB_SEARCH_URL": request.app.state.config.EXTERNAL_WEB_SEARCH_URL, - "EXTERNAL_WEB_SEARCH_API_KEY": request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, - "EXTERNAL_WEB_LOADER_URL": request.app.state.config.EXTERNAL_WEB_LOADER_URL, - "EXTERNAL_WEB_LOADER_API_KEY": request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY, - "YOUTUBE_LOADER_LANGUAGE": request.app.state.config.YOUTUBE_LOADER_LANGUAGE, - "YOUTUBE_LOADER_PROXY_URL": request.app.state.config.YOUTUBE_LOADER_PROXY_URL, - "YOUTUBE_LOADER_TRANSLATION": request.app.state.YOUTUBE_LOADER_TRANSLATION, + 'web': { + 'ENABLE_WEB_SEARCH': request.app.state.config.ENABLE_WEB_SEARCH, + 'WEB_SEARCH_ENGINE': request.app.state.config.WEB_SEARCH_ENGINE, + 'WEB_SEARCH_TRUST_ENV': request.app.state.config.WEB_SEARCH_TRUST_ENV, + 'WEB_SEARCH_RESULT_COUNT': request.app.state.config.WEB_SEARCH_RESULT_COUNT, + 'WEB_SEARCH_CONCURRENT_REQUESTS': request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, + 'WEB_FETCH_MAX_CONTENT_LENGTH': request.app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH, + 'WEB_LOADER_CONCURRENT_REQUESTS': request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + 'WEB_SEARCH_DOMAIN_FILTER_LIST': request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + 'BYPASS_WEB_SEARCH_WEB_LOADER': request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER, + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY': request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + 'SEARXNG_QUERY_URL': request.app.state.config.SEARXNG_QUERY_URL, + 'SEARXNG_LANGUAGE': request.app.state.config.SEARXNG_LANGUAGE, + 'YACY_QUERY_URL': request.app.state.config.YACY_QUERY_URL, + 'YACY_USERNAME': request.app.state.config.YACY_USERNAME, + 'YACY_PASSWORD': request.app.state.config.YACY_PASSWORD, + 'GOOGLE_PSE_API_KEY': request.app.state.config.GOOGLE_PSE_API_KEY, + 'GOOGLE_PSE_ENGINE_ID': request.app.state.config.GOOGLE_PSE_ENGINE_ID, + 'BRAVE_SEARCH_API_KEY': request.app.state.config.BRAVE_SEARCH_API_KEY, + 'BRAVE_SEARCH_CONTEXT_TOKENS': request.app.state.config.BRAVE_SEARCH_CONTEXT_TOKENS, + 'KAGI_SEARCH_API_KEY': request.app.state.config.KAGI_SEARCH_API_KEY, + 'MOJEEK_SEARCH_API_KEY': request.app.state.config.MOJEEK_SEARCH_API_KEY, + 'BOCHA_SEARCH_API_KEY': request.app.state.config.BOCHA_SEARCH_API_KEY, + 'SERPSTACK_API_KEY': request.app.state.config.SERPSTACK_API_KEY, + 'SERPSTACK_HTTPS': request.app.state.config.SERPSTACK_HTTPS, + 'SERPER_API_KEY': request.app.state.config.SERPER_API_KEY, + 'SERPLY_API_KEY': request.app.state.config.SERPLY_API_KEY, + 'DDGS_BACKEND': request.app.state.config.DDGS_BACKEND, + 'TAVILY_API_KEY': request.app.state.config.TAVILY_API_KEY, + 'SEARCHAPI_API_KEY': request.app.state.config.SEARCHAPI_API_KEY, + 'SEARCHAPI_ENGINE': request.app.state.config.SEARCHAPI_ENGINE, + 'SERPAPI_API_KEY': request.app.state.config.SERPAPI_API_KEY, + 'SERPAPI_ENGINE': request.app.state.config.SERPAPI_ENGINE, + 'JINA_API_KEY': request.app.state.config.JINA_API_KEY, + 'JINA_API_BASE_URL': request.app.state.config.JINA_API_BASE_URL, + 'BING_SEARCH_V7_ENDPOINT': request.app.state.config.BING_SEARCH_V7_ENDPOINT, + 'BING_SEARCH_V7_SUBSCRIPTION_KEY': request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, + 'EXA_API_KEY': request.app.state.config.EXA_API_KEY, + 'PERPLEXITY_API_KEY': request.app.state.config.PERPLEXITY_API_KEY, + 'PERPLEXITY_MODEL': request.app.state.config.PERPLEXITY_MODEL, + 'PERPLEXITY_SEARCH_CONTEXT_USAGE': request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, + 'PERPLEXITY_SEARCH_API_URL': request.app.state.config.PERPLEXITY_SEARCH_API_URL, + 'SOUGOU_API_SID': request.app.state.config.SOUGOU_API_SID, + 'SOUGOU_API_SK': request.app.state.config.SOUGOU_API_SK, + 'WEB_LOADER_ENGINE': request.app.state.config.WEB_LOADER_ENGINE, + 'WEB_LOADER_TIMEOUT': request.app.state.config.WEB_LOADER_TIMEOUT, + 'ENABLE_WEB_LOADER_SSL_VERIFICATION': request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, + 'PLAYWRIGHT_WS_URL': request.app.state.config.PLAYWRIGHT_WS_URL, + 'PLAYWRIGHT_TIMEOUT': request.app.state.config.PLAYWRIGHT_TIMEOUT, + 'FIRECRAWL_API_KEY': request.app.state.config.FIRECRAWL_API_KEY, + 'FIRECRAWL_API_BASE_URL': request.app.state.config.FIRECRAWL_API_BASE_URL, + 'FIRECRAWL_TIMEOUT': request.app.state.config.FIRECRAWL_TIMEOUT, + 'TAVILY_EXTRACT_DEPTH': request.app.state.config.TAVILY_EXTRACT_DEPTH, + 'EXTERNAL_WEB_SEARCH_URL': request.app.state.config.EXTERNAL_WEB_SEARCH_URL, + 'EXTERNAL_WEB_SEARCH_API_KEY': request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, + 'EXTERNAL_WEB_LOADER_URL': request.app.state.config.EXTERNAL_WEB_LOADER_URL, + 'EXTERNAL_WEB_LOADER_API_KEY': request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY, + 'YOUTUBE_LOADER_LANGUAGE': request.app.state.config.YOUTUBE_LOADER_LANGUAGE, + 'YOUTUBE_LOADER_PROXY_URL': request.app.state.config.YOUTUBE_LOADER_PROXY_URL, + 'YOUTUBE_LOADER_TRANSLATION': request.app.state.YOUTUBE_LOADER_TRANSLATION, + 'YANDEX_WEB_SEARCH_URL': request.app.state.config.YANDEX_WEB_SEARCH_URL, + 'YANDEX_WEB_SEARCH_API_KEY': request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, + 'YANDEX_WEB_SEARCH_CONFIG': request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + 'YOUCOM_API_KEY': request.app.state.config.YOUCOM_API_KEY, + 'LINKUP_API_KEY': request.app.state.config.LINKUP_API_KEY, + 'LINKUP_SEARCH_PARAMS': request.app.state.config.LINKUP_SEARCH_PARAMS, }, } class WebConfig(BaseModel): - ENABLE_WEB_SEARCH: Optional[bool] = None - WEB_SEARCH_ENGINE: Optional[str] = None - WEB_SEARCH_TRUST_ENV: Optional[bool] = None - WEB_SEARCH_RESULT_COUNT: Optional[int] = None - WEB_SEARCH_CONCURRENT_REQUESTS: Optional[int] = None - WEB_LOADER_CONCURRENT_REQUESTS: Optional[int] = None - WEB_SEARCH_DOMAIN_FILTER_LIST: Optional[List[str]] = [] - BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None - BYPASS_WEB_SEARCH_WEB_LOADER: Optional[bool] = None - OLLAMA_CLOUD_WEB_SEARCH_API_KEY: Optional[str] = None - SEARXNG_QUERY_URL: Optional[str] = None - SEARXNG_LANGUAGE: Optional[str] = None - YACY_QUERY_URL: Optional[str] = None - YACY_USERNAME: Optional[str] = None - YACY_PASSWORD: Optional[str] = None - GOOGLE_PSE_API_KEY: Optional[str] = None - GOOGLE_PSE_ENGINE_ID: Optional[str] = None - BRAVE_SEARCH_API_KEY: Optional[str] = None - KAGI_SEARCH_API_KEY: Optional[str] = None - MOJEEK_SEARCH_API_KEY: Optional[str] = None - BOCHA_SEARCH_API_KEY: Optional[str] = None - SERPSTACK_API_KEY: Optional[str] = None - SERPSTACK_HTTPS: Optional[bool] = None - SERPER_API_KEY: Optional[str] = None - SERPLY_API_KEY: Optional[str] = None - DDGS_BACKEND: Optional[str] = None - TAVILY_API_KEY: Optional[str] = None - SEARCHAPI_API_KEY: Optional[str] = None - SEARCHAPI_ENGINE: Optional[str] = None - SERPAPI_API_KEY: Optional[str] = None - SERPAPI_ENGINE: Optional[str] = None - JINA_API_KEY: Optional[str] = None - JINA_API_BASE_URL: Optional[str] = None - BING_SEARCH_V7_ENDPOINT: Optional[str] = None - BING_SEARCH_V7_SUBSCRIPTION_KEY: Optional[str] = None - EXA_API_KEY: Optional[str] = None - PERPLEXITY_API_KEY: Optional[str] = None - PERPLEXITY_MODEL: Optional[str] = None - PERPLEXITY_SEARCH_CONTEXT_USAGE: Optional[str] = None - PERPLEXITY_SEARCH_API_URL: Optional[str] = None - SOUGOU_API_SID: Optional[str] = None - SOUGOU_API_SK: Optional[str] = None - WEB_LOADER_ENGINE: Optional[str] = None - WEB_LOADER_TIMEOUT: Optional[str] = None - ENABLE_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None - PLAYWRIGHT_WS_URL: Optional[str] = None - PLAYWRIGHT_TIMEOUT: Optional[int] = None - FIRECRAWL_API_KEY: Optional[str] = None - FIRECRAWL_API_BASE_URL: Optional[str] = None - FIRECRAWL_TIMEOUT: Optional[str] = None - TAVILY_EXTRACT_DEPTH: Optional[str] = None - EXTERNAL_WEB_SEARCH_URL: Optional[str] = None - EXTERNAL_WEB_SEARCH_API_KEY: Optional[str] = None - EXTERNAL_WEB_LOADER_URL: Optional[str] = None - EXTERNAL_WEB_LOADER_API_KEY: Optional[str] = None - YOUTUBE_LOADER_LANGUAGE: Optional[List[str]] = None - YOUTUBE_LOADER_PROXY_URL: Optional[str] = None - YOUTUBE_LOADER_TRANSLATION: Optional[str] = None + ENABLE_WEB_SEARCH: bool | None = None + WEB_SEARCH_ENGINE: str | None = None + WEB_SEARCH_TRUST_ENV: bool | None = None + WEB_SEARCH_RESULT_COUNT: int | None = None + WEB_SEARCH_CONCURRENT_REQUESTS: int | None = None + WEB_SEARCH_DOMAIN_FILTER_LIST: list[str | None] = [] + WEB_FETCH_MAX_CONTENT_LENGTH: int | None = None + WEB_LOADER_CONCURRENT_REQUESTS: int | None = None + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: bool | None = None + BYPASS_WEB_SEARCH_WEB_LOADER: bool | None = None + OLLAMA_CLOUD_WEB_SEARCH_API_KEY: str | None = None + SEARXNG_QUERY_URL: str | None = None + SEARXNG_LANGUAGE: str | None = None + YACY_QUERY_URL: str | None = None + YACY_USERNAME: str | None = None + YACY_PASSWORD: str | None = None + GOOGLE_PSE_API_KEY: str | None = None + GOOGLE_PSE_ENGINE_ID: str | None = None + BRAVE_SEARCH_API_KEY: str | None = None + BRAVE_SEARCH_CONTEXT_TOKENS: int | None = None + KAGI_SEARCH_API_KEY: str | None = None + MOJEEK_SEARCH_API_KEY: str | None = None + BOCHA_SEARCH_API_KEY: str | None = None + SERPSTACK_API_KEY: str | None = None + SERPSTACK_HTTPS: bool | None = None + SERPER_API_KEY: str | None = None + SERPLY_API_KEY: str | None = None + DDGS_BACKEND: str | None = None + TAVILY_API_KEY: str | None = None + SEARCHAPI_API_KEY: str | None = None + SEARCHAPI_ENGINE: str | None = None + SERPAPI_API_KEY: str | None = None + SERPAPI_ENGINE: str | None = None + JINA_API_KEY: str | None = None + JINA_API_BASE_URL: str | None = None + BING_SEARCH_V7_ENDPOINT: str | None = None + BING_SEARCH_V7_SUBSCRIPTION_KEY: str | None = None + EXA_API_KEY: str | None = None + PERPLEXITY_API_KEY: str | None = None + PERPLEXITY_MODEL: str | None = None + PERPLEXITY_SEARCH_CONTEXT_USAGE: str | None = None + PERPLEXITY_SEARCH_API_URL: str | None = None + SOUGOU_API_SID: str | None = None + SOUGOU_API_SK: str | None = None + WEB_LOADER_ENGINE: str | None = None + WEB_LOADER_TIMEOUT: str | None = None + ENABLE_WEB_LOADER_SSL_VERIFICATION: bool | None = None + PLAYWRIGHT_WS_URL: str | None = None + PLAYWRIGHT_TIMEOUT: int | None = None + FIRECRAWL_API_KEY: str | None = None + FIRECRAWL_API_BASE_URL: str | None = None + FIRECRAWL_TIMEOUT: str | None = None + TAVILY_EXTRACT_DEPTH: str | None = None + EXTERNAL_WEB_SEARCH_URL: str | None = None + EXTERNAL_WEB_SEARCH_API_KEY: str | None = None + EXTERNAL_WEB_LOADER_URL: str | None = None + EXTERNAL_WEB_LOADER_API_KEY: str | None = None + YOUTUBE_LOADER_LANGUAGE: list[str | None] = None + YOUTUBE_LOADER_PROXY_URL: str | None = None + YOUTUBE_LOADER_TRANSLATION: str | None = None + YANDEX_WEB_SEARCH_URL: str | None = None + YANDEX_WEB_SEARCH_API_KEY: str | None = None + YANDEX_WEB_SEARCH_CONFIG: str | None = None + YOUCOM_API_KEY: str | None = None + LINKUP_API_KEY: str | None = None + LINKUP_SEARCH_PARAMS: dict | None = None class ConfigForm(BaseModel): # RAG settings - RAG_TEMPLATE: Optional[str] = None - TOP_K: Optional[int] = None - BYPASS_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None - RAG_FULL_CONTEXT: Optional[bool] = None + RAG_TEMPLATE: str | None = None + TOP_K: int | None = None + BYPASS_EMBEDDING_AND_RETRIEVAL: bool | None = None + RAG_FULL_CONTEXT: bool | None = None # Hybrid search settings - ENABLE_RAG_HYBRID_SEARCH: Optional[bool] = None - ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS: Optional[bool] = None - TOP_K_RERANKER: Optional[int] = None - RELEVANCE_THRESHOLD: Optional[float] = None - HYBRID_BM25_WEIGHT: Optional[float] = None + ENABLE_RAG_HYBRID_SEARCH: bool | None = None + ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS: bool | None = None + TOP_K_RERANKER: int | None = None + RELEVANCE_THRESHOLD: float | None = None + HYBRID_BM25_WEIGHT: float | None = None # Content extraction settings - CONTENT_EXTRACTION_ENGINE: Optional[str] = None - PDF_EXTRACT_IMAGES: Optional[bool] = None + CONTENT_EXTRACTION_ENGINE: str | None = None + PDF_EXTRACT_IMAGES: bool | None = None + PDF_LOADER_MODE: str | None = None - DATALAB_MARKER_API_KEY: Optional[str] = None - DATALAB_MARKER_API_BASE_URL: Optional[str] = None - DATALAB_MARKER_ADDITIONAL_CONFIG: Optional[str] = None - DATALAB_MARKER_SKIP_CACHE: Optional[bool] = None - DATALAB_MARKER_FORCE_OCR: Optional[bool] = None - DATALAB_MARKER_PAGINATE: Optional[bool] = None - DATALAB_MARKER_STRIP_EXISTING_OCR: Optional[bool] = None - DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION: Optional[bool] = None - DATALAB_MARKER_FORMAT_LINES: Optional[bool] = None - DATALAB_MARKER_USE_LLM: Optional[bool] = None - DATALAB_MARKER_OUTPUT_FORMAT: Optional[str] = None + DATALAB_MARKER_API_KEY: str | None = None + DATALAB_MARKER_API_BASE_URL: str | None = None + DATALAB_MARKER_ADDITIONAL_CONFIG: str | None = None + DATALAB_MARKER_SKIP_CACHE: bool | None = None + DATALAB_MARKER_FORCE_OCR: bool | None = None + DATALAB_MARKER_PAGINATE: bool | None = None + DATALAB_MARKER_STRIP_EXISTING_OCR: bool | None = None + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION: bool | None = None + DATALAB_MARKER_FORMAT_LINES: bool | None = None + DATALAB_MARKER_USE_LLM: bool | None = None + DATALAB_MARKER_OUTPUT_FORMAT: str | None = None - EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None - EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None + EXTERNAL_DOCUMENT_LOADER_URL: str | None = None + EXTERNAL_DOCUMENT_LOADER_API_KEY: str | None = None - TIKA_SERVER_URL: Optional[str] = None - DOCLING_SERVER_URL: Optional[str] = None - DOCLING_API_KEY: Optional[str] = None - DOCLING_PARAMS: Optional[dict] = None - DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None - DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None - DOCUMENT_INTELLIGENCE_MODEL: Optional[str] = None - MISTRAL_OCR_API_BASE_URL: Optional[str] = None - MISTRAL_OCR_API_KEY: Optional[str] = None + TIKA_SERVER_URL: str | None = None + DOCLING_SERVER_URL: str | None = None + DOCLING_API_KEY: str | None = None + DOCLING_PARAMS: dict | None = None + DOCUMENT_INTELLIGENCE_ENDPOINT: str | None = None + DOCUMENT_INTELLIGENCE_KEY: str | None = None + DOCUMENT_INTELLIGENCE_MODEL: str | None = None + MISTRAL_OCR_API_BASE_URL: str | None = None + MISTRAL_OCR_API_KEY: str | None = None + PADDLEOCR_VL_BASE_URL: str | None = None + PADDLEOCR_VL_TOKEN: str | None = None # MinerU settings - MINERU_API_MODE: Optional[str] = None - MINERU_API_URL: Optional[str] = None - MINERU_API_KEY: Optional[str] = None - MINERU_API_TIMEOUT: Optional[str] = None - MINERU_PARAMS: Optional[dict] = None + MINERU_API_MODE: str | None = None + MINERU_API_URL: str | None = None + MINERU_API_KEY: str | None = None + MINERU_API_TIMEOUT: str | None = None + MINERU_PARAMS: dict | None = None + MINERU_FILE_EXTENSIONS: list[str] | None = None # Reranking settings - RAG_RERANKING_MODEL: Optional[str] = None - RAG_RERANKING_ENGINE: Optional[str] = None - RAG_EXTERNAL_RERANKER_URL: Optional[str] = None - RAG_EXTERNAL_RERANKER_API_KEY: Optional[str] = None - RAG_EXTERNAL_RERANKER_TIMEOUT: Optional[str] = None + RAG_RERANKING_MODEL: str | None = None + RAG_RERANKING_ENGINE: str | None = None + RAG_RERANKING_BATCH_SIZE: int | None = None + RAG_EXTERNAL_RERANKER_URL: str | None = None + RAG_EXTERNAL_RERANKER_API_KEY: str | None = None + RAG_EXTERNAL_RERANKER_TIMEOUT: str | None = None # Chunking settings - TEXT_SPLITTER: Optional[str] = None - ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: Optional[bool] = None - CHUNK_SIZE: Optional[int] = None - CHUNK_MIN_SIZE_TARGET: Optional[int] = None - CHUNK_OVERLAP: Optional[int] = None + TEXT_SPLITTER: str | None = None + ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: bool | None = None + CHUNK_SIZE: int | None = None + CHUNK_MIN_SIZE_TARGET: int | None = None + CHUNK_OVERLAP: int | None = None # File upload settings - FILE_MAX_SIZE: Optional[int] = None - FILE_MAX_COUNT: Optional[int] = None - FILE_IMAGE_COMPRESSION_WIDTH: Optional[int] = None - FILE_IMAGE_COMPRESSION_HEIGHT: Optional[int] = None - ALLOWED_FILE_EXTENSIONS: Optional[List[str]] = None + FILE_MAX_SIZE: Union[int, str | None] = None + FILE_MAX_COUNT: Union[int, str | None] = None + FILE_IMAGE_COMPRESSION_WIDTH: Union[int, str | None] = None + FILE_IMAGE_COMPRESSION_HEIGHT: Union[int, str | None] = None + ALLOWED_FILE_EXTENSIONS: list[str | None] = None # Integration settings - ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None - ENABLE_ONEDRIVE_INTEGRATION: Optional[bool] = None + ENABLE_GOOGLE_DRIVE_INTEGRATION: bool | None = None + ENABLE_ONEDRIVE_INTEGRATION: bool | None = None # Web search settings - web: Optional[WebConfig] = None + web: WebConfig | None = None -@router.post("/config/update") -async def update_rag_config( - request: Request, form_data: ConfigForm, user=Depends(get_admin_user) -): +@router.post('/config/update') +async def update_rag_config(request: Request, form_data: ConfigForm, user=Depends(get_admin_user)): # RAG settings request.app.state.config.RAG_TEMPLATE = ( - form_data.RAG_TEMPLATE - if form_data.RAG_TEMPLATE is not None - else request.app.state.config.RAG_TEMPLATE - ) - request.app.state.config.TOP_K = ( - form_data.TOP_K - if form_data.TOP_K is not None - else request.app.state.config.TOP_K + form_data.RAG_TEMPLATE if form_data.RAG_TEMPLATE is not None else request.app.state.config.RAG_TEMPLATE ) + request.app.state.config.TOP_K = form_data.TOP_K if form_data.TOP_K is not None else request.app.state.config.TOP_K request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = ( form_data.BYPASS_EMBEDDING_AND_RETRIEVAL if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None @@ -760,9 +745,7 @@ async def update_rag_config( ) request.app.state.config.TOP_K_RERANKER = ( - form_data.TOP_K_RERANKER - if form_data.TOP_K_RERANKER is not None - else request.app.state.config.TOP_K_RERANKER + form_data.TOP_K_RERANKER if form_data.TOP_K_RERANKER is not None else request.app.state.config.TOP_K_RERANKER ) request.app.state.config.RELEVANCE_THRESHOLD = ( form_data.RELEVANCE_THRESHOLD @@ -786,6 +769,9 @@ async def update_rag_config( if form_data.PDF_EXTRACT_IMAGES is not None else request.app.state.config.PDF_EXTRACT_IMAGES ) + request.app.state.config.PDF_LOADER_MODE = ( + form_data.PDF_LOADER_MODE if form_data.PDF_LOADER_MODE is not None else request.app.state.config.PDF_LOADER_MODE + ) request.app.state.config.DATALAB_MARKER_API_KEY = ( form_data.DATALAB_MARKER_API_KEY if form_data.DATALAB_MARKER_API_KEY is not None @@ -852,9 +838,7 @@ async def update_rag_config( else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY ) request.app.state.config.TIKA_SERVER_URL = ( - form_data.TIKA_SERVER_URL - if form_data.TIKA_SERVER_URL is not None - else request.app.state.config.TIKA_SERVER_URL + form_data.TIKA_SERVER_URL if form_data.TIKA_SERVER_URL is not None else request.app.state.config.TIKA_SERVER_URL ) request.app.state.config.DOCLING_SERVER_URL = ( form_data.DOCLING_SERVER_URL @@ -862,14 +846,10 @@ async def update_rag_config( else request.app.state.config.DOCLING_SERVER_URL ) request.app.state.config.DOCLING_API_KEY = ( - form_data.DOCLING_API_KEY - if form_data.DOCLING_API_KEY is not None - else request.app.state.config.DOCLING_API_KEY + form_data.DOCLING_API_KEY if form_data.DOCLING_API_KEY is not None else request.app.state.config.DOCLING_API_KEY ) request.app.state.config.DOCLING_PARAMS = ( - form_data.DOCLING_PARAMS - if form_data.DOCLING_PARAMS is not None - else request.app.state.config.DOCLING_PARAMS + form_data.DOCLING_PARAMS if form_data.DOCLING_PARAMS is not None else request.app.state.config.DOCLING_PARAMS ) request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = ( form_data.DOCUMENT_INTELLIGENCE_ENDPOINT @@ -897,22 +877,26 @@ async def update_rag_config( if form_data.MISTRAL_OCR_API_KEY is not None else request.app.state.config.MISTRAL_OCR_API_KEY ) + request.app.state.config.PADDLEOCR_VL_BASE_URL = ( + form_data.PADDLEOCR_VL_BASE_URL + if form_data.PADDLEOCR_VL_BASE_URL is not None + else request.app.state.config.PADDLEOCR_VL_BASE_URL + ) + request.app.state.config.PADDLEOCR_VL_TOKEN = ( + form_data.PADDLEOCR_VL_TOKEN + if form_data.PADDLEOCR_VL_TOKEN is not None + else request.app.state.config.PADDLEOCR_VL_TOKEN + ) # MinerU settings request.app.state.config.MINERU_API_MODE = ( - form_data.MINERU_API_MODE - if form_data.MINERU_API_MODE is not None - else request.app.state.config.MINERU_API_MODE + form_data.MINERU_API_MODE if form_data.MINERU_API_MODE is not None else request.app.state.config.MINERU_API_MODE ) request.app.state.config.MINERU_API_URL = ( - form_data.MINERU_API_URL - if form_data.MINERU_API_URL is not None - else request.app.state.config.MINERU_API_URL + form_data.MINERU_API_URL if form_data.MINERU_API_URL is not None else request.app.state.config.MINERU_API_URL ) request.app.state.config.MINERU_API_KEY = ( - form_data.MINERU_API_KEY - if form_data.MINERU_API_KEY is not None - else request.app.state.config.MINERU_API_KEY + form_data.MINERU_API_KEY if form_data.MINERU_API_KEY is not None else request.app.state.config.MINERU_API_KEY ) request.app.state.config.MINERU_API_TIMEOUT = ( form_data.MINERU_API_TIMEOUT @@ -920,20 +904,23 @@ async def update_rag_config( else request.app.state.config.MINERU_API_TIMEOUT ) request.app.state.config.MINERU_PARAMS = ( - form_data.MINERU_PARAMS - if form_data.MINERU_PARAMS is not None - else request.app.state.config.MINERU_PARAMS + form_data.MINERU_PARAMS if form_data.MINERU_PARAMS is not None else request.app.state.config.MINERU_PARAMS + ) + request.app.state.config.MINERU_FILE_EXTENSIONS = ( + form_data.MINERU_FILE_EXTENSIONS + if form_data.MINERU_FILE_EXTENSIONS is not None + else request.app.state.config.MINERU_FILE_EXTENSIONS ) # Reranking settings - if request.app.state.config.RAG_RERANKING_ENGINE == "": + if request.app.state.config.RAG_RERANKING_ENGINE == '': # Unloading the internal reranker and clear VRAM memory request.app.state.rf = None request.app.state.RERANKING_FUNCTION = None import gc gc.collect() - if DEVICE_TYPE == "cuda": + if DEVICE_TYPE == 'cuda': import torch if torch.cuda.is_available(): @@ -962,8 +949,14 @@ async def update_rag_config( else request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT ) + request.app.state.config.RAG_RERANKING_BATCH_SIZE = ( + form_data.RAG_RERANKING_BATCH_SIZE + if form_data.RAG_RERANKING_BATCH_SIZE is not None + else request.app.state.config.RAG_RERANKING_BATCH_SIZE + ) + log.info( - f"Updating reranking model: {request.app.state.config.RAG_RERANKING_MODEL} to {form_data.RAG_RERANKING_MODEL}" + f'Updating reranking model: {request.app.state.config.RAG_RERANKING_MODEL} to {form_data.RAG_RERANKING_MODEL}' ) try: request.app.state.config.RAG_RERANKING_MODEL = ( @@ -989,12 +982,13 @@ async def update_rag_config( request.app.state.config.RAG_RERANKING_ENGINE, request.app.state.config.RAG_RERANKING_MODEL, request.app.state.rf, + reranking_batch_size=request.app.state.config.RAG_RERANKING_BATCH_SIZE, ) except Exception as e: - log.error(f"Error loading reranking model: {e}") + log.error(f'Error loading reranking model: {e}') request.app.state.config.ENABLE_RAG_HYBRID_SEARCH = False except Exception as e: - log.exception(f"Problem updating reranking model: {e}") + log.exception(f'Problem updating reranking model: {e}') raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DEFAULT(e), @@ -1002,14 +996,15 @@ async def update_rag_config( # Chunking settings request.app.state.config.TEXT_SPLITTER = ( - form_data.TEXT_SPLITTER - if form_data.TEXT_SPLITTER is not None - else request.app.state.config.TEXT_SPLITTER + form_data.TEXT_SPLITTER if form_data.TEXT_SPLITTER is not None else request.app.state.config.TEXT_SPLITTER + ) + request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ( + form_data.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER + if form_data.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER is not None + else request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER ) request.app.state.config.CHUNK_SIZE = ( - form_data.CHUNK_SIZE - if form_data.CHUNK_SIZE is not None - else request.app.state.config.CHUNK_SIZE + form_data.CHUNK_SIZE if form_data.CHUNK_SIZE is not None else request.app.state.config.CHUNK_SIZE ) request.app.state.config.CHUNK_MIN_SIZE_TARGET = ( form_data.CHUNK_MIN_SIZE_TARGET @@ -1017,20 +1012,25 @@ async def update_rag_config( else request.app.state.config.CHUNK_MIN_SIZE_TARGET ) request.app.state.config.CHUNK_OVERLAP = ( - form_data.CHUNK_OVERLAP - if form_data.CHUNK_OVERLAP is not None - else request.app.state.config.CHUNK_OVERLAP + form_data.CHUNK_OVERLAP if form_data.CHUNK_OVERLAP is not None else request.app.state.config.CHUNK_OVERLAP ) # File upload settings - request.app.state.config.FILE_MAX_SIZE = form_data.FILE_MAX_SIZE - request.app.state.config.FILE_MAX_COUNT = form_data.FILE_MAX_COUNT - request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = ( - form_data.FILE_IMAGE_COMPRESSION_WIDTH - ) - request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = ( - form_data.FILE_IMAGE_COMPRESSION_HEIGHT - ) + # Empty string means "clear to None" (unlimited/no compression), + # None means "don't change", int means "set to this value" + if form_data.FILE_MAX_SIZE is not None: + request.app.state.config.FILE_MAX_SIZE = None if form_data.FILE_MAX_SIZE == '' else form_data.FILE_MAX_SIZE + if form_data.FILE_MAX_COUNT is not None: + request.app.state.config.FILE_MAX_COUNT = None if form_data.FILE_MAX_COUNT == '' else form_data.FILE_MAX_COUNT + if form_data.FILE_IMAGE_COMPRESSION_WIDTH is not None: + request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH = ( + None if form_data.FILE_IMAGE_COMPRESSION_WIDTH == '' else form_data.FILE_IMAGE_COMPRESSION_WIDTH + ) + if form_data.FILE_IMAGE_COMPRESSION_HEIGHT is not None: + request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT = ( + None if form_data.FILE_IMAGE_COMPRESSION_HEIGHT == '' else form_data.FILE_IMAGE_COMPRESSION_HEIGHT + ) + request.app.state.config.ALLOWED_FILE_EXTENSIONS = ( form_data.ALLOWED_FILE_EXTENSIONS if form_data.ALLOWED_FILE_EXTENSIONS is not None @@ -1053,49 +1053,30 @@ async def update_rag_config( # Web search settings request.app.state.config.ENABLE_WEB_SEARCH = form_data.web.ENABLE_WEB_SEARCH request.app.state.config.WEB_SEARCH_ENGINE = form_data.web.WEB_SEARCH_ENGINE - request.app.state.config.WEB_SEARCH_TRUST_ENV = ( - form_data.web.WEB_SEARCH_TRUST_ENV - ) - request.app.state.config.WEB_SEARCH_RESULT_COUNT = ( - form_data.web.WEB_SEARCH_RESULT_COUNT - ) - request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = ( - form_data.web.WEB_SEARCH_CONCURRENT_REQUESTS - ) - request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = ( - form_data.web.WEB_LOADER_CONCURRENT_REQUESTS - ) - request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = ( - form_data.web.WEB_SEARCH_DOMAIN_FILTER_LIST - ) + request.app.state.config.WEB_SEARCH_TRUST_ENV = form_data.web.WEB_SEARCH_TRUST_ENV + request.app.state.config.WEB_SEARCH_RESULT_COUNT = form_data.web.WEB_SEARCH_RESULT_COUNT + request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS = form_data.web.WEB_SEARCH_CONCURRENT_REQUESTS + request.app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH = form_data.web.WEB_FETCH_MAX_CONTENT_LENGTH + request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS = form_data.web.WEB_LOADER_CONCURRENT_REQUESTS + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST = form_data.web.WEB_SEARCH_DOMAIN_FILTER_LIST request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = ( form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL ) - request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = ( - form_data.web.BYPASS_WEB_SEARCH_WEB_LOADER - ) - request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = ( - form_data.web.OLLAMA_CLOUD_WEB_SEARCH_API_KEY - ) + request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER = form_data.web.BYPASS_WEB_SEARCH_WEB_LOADER + request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY = form_data.web.OLLAMA_CLOUD_WEB_SEARCH_API_KEY request.app.state.config.SEARXNG_QUERY_URL = form_data.web.SEARXNG_QUERY_URL request.app.state.config.SEARXNG_LANGUAGE = form_data.web.SEARXNG_LANGUAGE request.app.state.config.YACY_QUERY_URL = form_data.web.YACY_QUERY_URL request.app.state.config.YACY_USERNAME = form_data.web.YACY_USERNAME request.app.state.config.YACY_PASSWORD = form_data.web.YACY_PASSWORD request.app.state.config.GOOGLE_PSE_API_KEY = form_data.web.GOOGLE_PSE_API_KEY - request.app.state.config.GOOGLE_PSE_ENGINE_ID = ( - form_data.web.GOOGLE_PSE_ENGINE_ID - ) - request.app.state.config.BRAVE_SEARCH_API_KEY = ( - form_data.web.BRAVE_SEARCH_API_KEY - ) + request.app.state.config.GOOGLE_PSE_ENGINE_ID = form_data.web.GOOGLE_PSE_ENGINE_ID + request.app.state.config.BRAVE_SEARCH_API_KEY = form_data.web.BRAVE_SEARCH_API_KEY + if form_data.web.BRAVE_SEARCH_CONTEXT_TOKENS is not None: + request.app.state.config.BRAVE_SEARCH_CONTEXT_TOKENS = form_data.web.BRAVE_SEARCH_CONTEXT_TOKENS request.app.state.config.KAGI_SEARCH_API_KEY = form_data.web.KAGI_SEARCH_API_KEY - request.app.state.config.MOJEEK_SEARCH_API_KEY = ( - form_data.web.MOJEEK_SEARCH_API_KEY - ) - request.app.state.config.BOCHA_SEARCH_API_KEY = ( - form_data.web.BOCHA_SEARCH_API_KEY - ) + request.app.state.config.MOJEEK_SEARCH_API_KEY = form_data.web.MOJEEK_SEARCH_API_KEY + request.app.state.config.BOCHA_SEARCH_API_KEY = form_data.web.BOCHA_SEARCH_API_KEY request.app.state.config.SERPSTACK_API_KEY = form_data.web.SERPSTACK_API_KEY request.app.state.config.SERPSTACK_HTTPS = form_data.web.SERPSTACK_HTTPS request.app.state.config.SERPER_API_KEY = form_data.web.SERPER_API_KEY @@ -1108,21 +1089,13 @@ async def update_rag_config( request.app.state.config.SERPAPI_ENGINE = form_data.web.SERPAPI_ENGINE request.app.state.config.JINA_API_KEY = form_data.web.JINA_API_KEY request.app.state.config.JINA_API_BASE_URL = form_data.web.JINA_API_BASE_URL - request.app.state.config.BING_SEARCH_V7_ENDPOINT = ( - form_data.web.BING_SEARCH_V7_ENDPOINT - ) - request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = ( - form_data.web.BING_SEARCH_V7_SUBSCRIPTION_KEY - ) + request.app.state.config.BING_SEARCH_V7_ENDPOINT = form_data.web.BING_SEARCH_V7_ENDPOINT + request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = form_data.web.BING_SEARCH_V7_SUBSCRIPTION_KEY request.app.state.config.EXA_API_KEY = form_data.web.EXA_API_KEY request.app.state.config.PERPLEXITY_API_KEY = form_data.web.PERPLEXITY_API_KEY request.app.state.config.PERPLEXITY_MODEL = form_data.web.PERPLEXITY_MODEL - request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = ( - form_data.web.PERPLEXITY_SEARCH_CONTEXT_USAGE - ) - request.app.state.config.PERPLEXITY_SEARCH_API_URL = ( - form_data.web.PERPLEXITY_SEARCH_API_URL - ) + request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE = form_data.web.PERPLEXITY_SEARCH_CONTEXT_USAGE + request.app.state.config.PERPLEXITY_SEARCH_API_URL = form_data.web.PERPLEXITY_SEARCH_API_URL request.app.state.config.SOUGOU_API_SID = form_data.web.SOUGOU_API_SID request.app.state.config.SOUGOU_API_SK = form_data.web.SOUGOU_API_SK @@ -1130,163 +1103,160 @@ async def update_rag_config( request.app.state.config.WEB_LOADER_ENGINE = form_data.web.WEB_LOADER_ENGINE request.app.state.config.WEB_LOADER_TIMEOUT = form_data.web.WEB_LOADER_TIMEOUT - request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ( - form_data.web.ENABLE_WEB_LOADER_SSL_VERIFICATION - ) + request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = form_data.web.ENABLE_WEB_LOADER_SSL_VERIFICATION request.app.state.config.PLAYWRIGHT_WS_URL = form_data.web.PLAYWRIGHT_WS_URL request.app.state.config.PLAYWRIGHT_TIMEOUT = form_data.web.PLAYWRIGHT_TIMEOUT request.app.state.config.FIRECRAWL_API_KEY = form_data.web.FIRECRAWL_API_KEY - request.app.state.config.FIRECRAWL_API_BASE_URL = ( - form_data.web.FIRECRAWL_API_BASE_URL - ) + request.app.state.config.FIRECRAWL_API_BASE_URL = form_data.web.FIRECRAWL_API_BASE_URL request.app.state.config.FIRECRAWL_TIMEOUT = form_data.web.FIRECRAWL_TIMEOUT - request.app.state.config.EXTERNAL_WEB_SEARCH_URL = ( - form_data.web.EXTERNAL_WEB_SEARCH_URL - ) - request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = ( - form_data.web.EXTERNAL_WEB_SEARCH_API_KEY - ) - request.app.state.config.EXTERNAL_WEB_LOADER_URL = ( - form_data.web.EXTERNAL_WEB_LOADER_URL - ) - request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY = ( - form_data.web.EXTERNAL_WEB_LOADER_API_KEY - ) - request.app.state.config.TAVILY_EXTRACT_DEPTH = ( - form_data.web.TAVILY_EXTRACT_DEPTH - ) - request.app.state.config.YOUTUBE_LOADER_LANGUAGE = ( - form_data.web.YOUTUBE_LOADER_LANGUAGE - ) - request.app.state.config.YOUTUBE_LOADER_PROXY_URL = ( - form_data.web.YOUTUBE_LOADER_PROXY_URL - ) - request.app.state.YOUTUBE_LOADER_TRANSLATION = ( - form_data.web.YOUTUBE_LOADER_TRANSLATION - ) + request.app.state.config.EXTERNAL_WEB_SEARCH_URL = form_data.web.EXTERNAL_WEB_SEARCH_URL + request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = form_data.web.EXTERNAL_WEB_SEARCH_API_KEY + request.app.state.config.EXTERNAL_WEB_LOADER_URL = form_data.web.EXTERNAL_WEB_LOADER_URL + request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY = form_data.web.EXTERNAL_WEB_LOADER_API_KEY + request.app.state.config.TAVILY_EXTRACT_DEPTH = form_data.web.TAVILY_EXTRACT_DEPTH + request.app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.web.YOUTUBE_LOADER_LANGUAGE + request.app.state.config.YOUTUBE_LOADER_PROXY_URL = form_data.web.YOUTUBE_LOADER_PROXY_URL + request.app.state.YOUTUBE_LOADER_TRANSLATION = form_data.web.YOUTUBE_LOADER_TRANSLATION + request.app.state.config.YANDEX_WEB_SEARCH_URL = form_data.web.YANDEX_WEB_SEARCH_URL + request.app.state.config.YANDEX_WEB_SEARCH_API_KEY = form_data.web.YANDEX_WEB_SEARCH_API_KEY + request.app.state.config.YANDEX_WEB_SEARCH_CONFIG = form_data.web.YANDEX_WEB_SEARCH_CONFIG + request.app.state.config.YOUCOM_API_KEY = form_data.web.YOUCOM_API_KEY + request.app.state.config.LINKUP_API_KEY = form_data.web.LINKUP_API_KEY + request.app.state.config.LINKUP_SEARCH_PARAMS = form_data.web.LINKUP_SEARCH_PARAMS return { - "status": True, + 'status': True, # RAG settings - "RAG_TEMPLATE": request.app.state.config.RAG_TEMPLATE, - "TOP_K": request.app.state.config.TOP_K, - "BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL, - "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT, + 'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE, + 'TOP_K': request.app.state.config.TOP_K, + 'BYPASS_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL, + 'RAG_FULL_CONTEXT': request.app.state.config.RAG_FULL_CONTEXT, # Hybrid search settings - "ENABLE_RAG_HYBRID_SEARCH": request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, - "TOP_K_RERANKER": request.app.state.config.TOP_K_RERANKER, - "RELEVANCE_THRESHOLD": request.app.state.config.RELEVANCE_THRESHOLD, - "HYBRID_BM25_WEIGHT": request.app.state.config.HYBRID_BM25_WEIGHT, + 'ENABLE_RAG_HYBRID_SEARCH': request.app.state.config.ENABLE_RAG_HYBRID_SEARCH, + 'TOP_K_RERANKER': request.app.state.config.TOP_K_RERANKER, + 'RELEVANCE_THRESHOLD': request.app.state.config.RELEVANCE_THRESHOLD, + 'HYBRID_BM25_WEIGHT': request.app.state.config.HYBRID_BM25_WEIGHT, # Content extraction settings - "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, - "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, - "DATALAB_MARKER_API_KEY": request.app.state.config.DATALAB_MARKER_API_KEY, - "DATALAB_MARKER_API_BASE_URL": request.app.state.config.DATALAB_MARKER_API_BASE_URL, - "DATALAB_MARKER_ADDITIONAL_CONFIG": request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, - "DATALAB_MARKER_SKIP_CACHE": request.app.state.config.DATALAB_MARKER_SKIP_CACHE, - "DATALAB_MARKER_FORCE_OCR": request.app.state.config.DATALAB_MARKER_FORCE_OCR, - "DATALAB_MARKER_PAGINATE": request.app.state.config.DATALAB_MARKER_PAGINATE, - "DATALAB_MARKER_STRIP_EXISTING_OCR": request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, - "DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION": request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, - "DATALAB_MARKER_USE_LLM": request.app.state.config.DATALAB_MARKER_USE_LLM, - "DATALAB_MARKER_OUTPUT_FORMAT": request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, - "EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, - "EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, - "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, - "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL, - "DOCLING_API_KEY": request.app.state.config.DOCLING_API_KEY, - "DOCLING_PARAMS": request.app.state.config.DOCLING_PARAMS, - "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, - "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, - "DOCUMENT_INTELLIGENCE_MODEL": request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, - "MISTRAL_OCR_API_BASE_URL": request.app.state.config.MISTRAL_OCR_API_BASE_URL, - "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, + 'CONTENT_EXTRACTION_ENGINE': request.app.state.config.CONTENT_EXTRACTION_ENGINE, + 'PDF_EXTRACT_IMAGES': request.app.state.config.PDF_EXTRACT_IMAGES, + 'PDF_LOADER_MODE': request.app.state.config.PDF_LOADER_MODE, + 'DATALAB_MARKER_API_KEY': request.app.state.config.DATALAB_MARKER_API_KEY, + 'DATALAB_MARKER_API_BASE_URL': request.app.state.config.DATALAB_MARKER_API_BASE_URL, + 'DATALAB_MARKER_ADDITIONAL_CONFIG': request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, + 'DATALAB_MARKER_SKIP_CACHE': request.app.state.config.DATALAB_MARKER_SKIP_CACHE, + 'DATALAB_MARKER_FORCE_OCR': request.app.state.config.DATALAB_MARKER_FORCE_OCR, + 'DATALAB_MARKER_PAGINATE': request.app.state.config.DATALAB_MARKER_PAGINATE, + 'DATALAB_MARKER_STRIP_EXISTING_OCR': request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION': request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + 'DATALAB_MARKER_USE_LLM': request.app.state.config.DATALAB_MARKER_USE_LLM, + 'DATALAB_MARKER_OUTPUT_FORMAT': request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, + 'EXTERNAL_DOCUMENT_LOADER_URL': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, + 'EXTERNAL_DOCUMENT_LOADER_API_KEY': request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, + 'TIKA_SERVER_URL': request.app.state.config.TIKA_SERVER_URL, + 'DOCLING_SERVER_URL': request.app.state.config.DOCLING_SERVER_URL, + 'DOCLING_API_KEY': request.app.state.config.DOCLING_API_KEY, + 'DOCLING_PARAMS': request.app.state.config.DOCLING_PARAMS, + 'DOCUMENT_INTELLIGENCE_ENDPOINT': request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, + 'DOCUMENT_INTELLIGENCE_KEY': request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, + 'DOCUMENT_INTELLIGENCE_MODEL': request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, + 'MISTRAL_OCR_API_BASE_URL': request.app.state.config.MISTRAL_OCR_API_BASE_URL, + 'MISTRAL_OCR_API_KEY': request.app.state.config.MISTRAL_OCR_API_KEY, + 'PADDLEOCR_VL_BASE_URL': request.app.state.config.PADDLEOCR_VL_BASE_URL, + 'PADDLEOCR_VL_TOKEN': request.app.state.config.PADDLEOCR_VL_TOKEN, # MinerU settings - "MINERU_API_MODE": request.app.state.config.MINERU_API_MODE, - "MINERU_API_URL": request.app.state.config.MINERU_API_URL, - "MINERU_API_KEY": request.app.state.config.MINERU_API_KEY, - "MINERU_API_TIMEOUT": request.app.state.config.MINERU_API_TIMEOUT, - "MINERU_PARAMS": request.app.state.config.MINERU_PARAMS, + 'MINERU_API_MODE': request.app.state.config.MINERU_API_MODE, + 'MINERU_API_URL': request.app.state.config.MINERU_API_URL, + 'MINERU_API_KEY': request.app.state.config.MINERU_API_KEY, + 'MINERU_API_TIMEOUT': request.app.state.config.MINERU_API_TIMEOUT, + 'MINERU_PARAMS': request.app.state.config.MINERU_PARAMS, # Reranking settings - "RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL, - "RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE, - "RAG_EXTERNAL_RERANKER_URL": request.app.state.config.RAG_EXTERNAL_RERANKER_URL, - "RAG_EXTERNAL_RERANKER_API_KEY": request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, - "RAG_EXTERNAL_RERANKER_TIMEOUT": request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, + 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, + 'RAG_RERANKING_ENGINE': request.app.state.config.RAG_RERANKING_ENGINE, + 'RAG_EXTERNAL_RERANKER_URL': request.app.state.config.RAG_EXTERNAL_RERANKER_URL, + 'RAG_EXTERNAL_RERANKER_API_KEY': request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY, + 'RAG_EXTERNAL_RERANKER_TIMEOUT': request.app.state.config.RAG_EXTERNAL_RERANKER_TIMEOUT, # Chunking settings - "TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER, - "CHUNK_SIZE": request.app.state.config.CHUNK_SIZE, - "CHUNK_MIN_SIZE_TARGET": request.app.state.config.CHUNK_MIN_SIZE_TARGET, - "ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER": request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, - "CHUNK_OVERLAP": request.app.state.config.CHUNK_OVERLAP, + 'TEXT_SPLITTER': request.app.state.config.TEXT_SPLITTER, + 'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE, + 'CHUNK_MIN_SIZE_TARGET': request.app.state.config.CHUNK_MIN_SIZE_TARGET, + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER': request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + 'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP, # File upload settings - "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE, - "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT, - "FILE_IMAGE_COMPRESSION_WIDTH": request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, - "FILE_IMAGE_COMPRESSION_HEIGHT": request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, - "ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS, + 'FILE_MAX_SIZE': request.app.state.config.FILE_MAX_SIZE, + 'FILE_MAX_COUNT': request.app.state.config.FILE_MAX_COUNT, + 'FILE_IMAGE_COMPRESSION_WIDTH': request.app.state.config.FILE_IMAGE_COMPRESSION_WIDTH, + 'FILE_IMAGE_COMPRESSION_HEIGHT': request.app.state.config.FILE_IMAGE_COMPRESSION_HEIGHT, + 'ALLOWED_FILE_EXTENSIONS': request.app.state.config.ALLOWED_FILE_EXTENSIONS, # Integration settings - "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, - "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + 'ENABLE_GOOGLE_DRIVE_INTEGRATION': request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + 'ENABLE_ONEDRIVE_INTEGRATION': request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, # Web search settings - "web": { - "ENABLE_WEB_SEARCH": request.app.state.config.ENABLE_WEB_SEARCH, - "WEB_SEARCH_ENGINE": request.app.state.config.WEB_SEARCH_ENGINE, - "WEB_SEARCH_TRUST_ENV": request.app.state.config.WEB_SEARCH_TRUST_ENV, - "WEB_SEARCH_RESULT_COUNT": request.app.state.config.WEB_SEARCH_RESULT_COUNT, - "WEB_SEARCH_CONCURRENT_REQUESTS": request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, - "WEB_LOADER_CONCURRENT_REQUESTS": request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, - "WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, - "BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, - "BYPASS_WEB_SEARCH_WEB_LOADER": request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER, - "OLLAMA_CLOUD_WEB_SEARCH_API_KEY": request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, - "SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL, - "SEARXNG_LANGUAGE": request.app.state.config.SEARXNG_LANGUAGE, - "YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL, - "YACY_USERNAME": request.app.state.config.YACY_USERNAME, - "YACY_PASSWORD": request.app.state.config.YACY_PASSWORD, - "GOOGLE_PSE_API_KEY": request.app.state.config.GOOGLE_PSE_API_KEY, - "GOOGLE_PSE_ENGINE_ID": request.app.state.config.GOOGLE_PSE_ENGINE_ID, - "BRAVE_SEARCH_API_KEY": request.app.state.config.BRAVE_SEARCH_API_KEY, - "KAGI_SEARCH_API_KEY": request.app.state.config.KAGI_SEARCH_API_KEY, - "MOJEEK_SEARCH_API_KEY": request.app.state.config.MOJEEK_SEARCH_API_KEY, - "BOCHA_SEARCH_API_KEY": request.app.state.config.BOCHA_SEARCH_API_KEY, - "SERPSTACK_API_KEY": request.app.state.config.SERPSTACK_API_KEY, - "SERPSTACK_HTTPS": request.app.state.config.SERPSTACK_HTTPS, - "SERPER_API_KEY": request.app.state.config.SERPER_API_KEY, - "SERPLY_API_KEY": request.app.state.config.SERPLY_API_KEY, - "TAVILY_API_KEY": request.app.state.config.TAVILY_API_KEY, - "SEARCHAPI_API_KEY": request.app.state.config.SEARCHAPI_API_KEY, - "SEARCHAPI_ENGINE": request.app.state.config.SEARCHAPI_ENGINE, - "SERPAPI_API_KEY": request.app.state.config.SERPAPI_API_KEY, - "SERPAPI_ENGINE": request.app.state.config.SERPAPI_ENGINE, - "JINA_API_KEY": request.app.state.config.JINA_API_KEY, - "JINA_API_BASE_URL": request.app.state.config.JINA_API_BASE_URL, - "BING_SEARCH_V7_ENDPOINT": request.app.state.config.BING_SEARCH_V7_ENDPOINT, - "BING_SEARCH_V7_SUBSCRIPTION_KEY": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, - "EXA_API_KEY": request.app.state.config.EXA_API_KEY, - "PERPLEXITY_API_KEY": request.app.state.config.PERPLEXITY_API_KEY, - "PERPLEXITY_MODEL": request.app.state.config.PERPLEXITY_MODEL, - "PERPLEXITY_SEARCH_CONTEXT_USAGE": request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, - "PERPLEXITY_SEARCH_API_URL": request.app.state.config.PERPLEXITY_SEARCH_API_URL, - "SOUGOU_API_SID": request.app.state.config.SOUGOU_API_SID, - "SOUGOU_API_SK": request.app.state.config.SOUGOU_API_SK, - "WEB_LOADER_ENGINE": request.app.state.config.WEB_LOADER_ENGINE, - "WEB_LOADER_TIMEOUT": request.app.state.config.WEB_LOADER_TIMEOUT, - "ENABLE_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, - "PLAYWRIGHT_WS_URL": request.app.state.config.PLAYWRIGHT_WS_URL, - "PLAYWRIGHT_TIMEOUT": request.app.state.config.PLAYWRIGHT_TIMEOUT, - "FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY, - "FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL, - "FIRECRAWL_TIMEOUT": request.app.state.config.FIRECRAWL_TIMEOUT, - "TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH, - "EXTERNAL_WEB_SEARCH_URL": request.app.state.config.EXTERNAL_WEB_SEARCH_URL, - "EXTERNAL_WEB_SEARCH_API_KEY": request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, - "EXTERNAL_WEB_LOADER_URL": request.app.state.config.EXTERNAL_WEB_LOADER_URL, - "EXTERNAL_WEB_LOADER_API_KEY": request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY, - "YOUTUBE_LOADER_LANGUAGE": request.app.state.config.YOUTUBE_LOADER_LANGUAGE, - "YOUTUBE_LOADER_PROXY_URL": request.app.state.config.YOUTUBE_LOADER_PROXY_URL, - "YOUTUBE_LOADER_TRANSLATION": request.app.state.YOUTUBE_LOADER_TRANSLATION, + 'web': { + 'ENABLE_WEB_SEARCH': request.app.state.config.ENABLE_WEB_SEARCH, + 'WEB_SEARCH_ENGINE': request.app.state.config.WEB_SEARCH_ENGINE, + 'WEB_SEARCH_TRUST_ENV': request.app.state.config.WEB_SEARCH_TRUST_ENV, + 'WEB_SEARCH_RESULT_COUNT': request.app.state.config.WEB_SEARCH_RESULT_COUNT, + 'WEB_SEARCH_CONCURRENT_REQUESTS': request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, + 'WEB_FETCH_MAX_CONTENT_LENGTH': request.app.state.config.WEB_FETCH_MAX_CONTENT_LENGTH, + 'WEB_LOADER_CONCURRENT_REQUESTS': request.app.state.config.WEB_LOADER_CONCURRENT_REQUESTS, + 'WEB_SEARCH_DOMAIN_FILTER_LIST': request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL': request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + 'BYPASS_WEB_SEARCH_WEB_LOADER': request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER, + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY': request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + 'SEARXNG_QUERY_URL': request.app.state.config.SEARXNG_QUERY_URL, + 'SEARXNG_LANGUAGE': request.app.state.config.SEARXNG_LANGUAGE, + 'YACY_QUERY_URL': request.app.state.config.YACY_QUERY_URL, + 'YACY_USERNAME': request.app.state.config.YACY_USERNAME, + 'YACY_PASSWORD': request.app.state.config.YACY_PASSWORD, + 'GOOGLE_PSE_API_KEY': request.app.state.config.GOOGLE_PSE_API_KEY, + 'GOOGLE_PSE_ENGINE_ID': request.app.state.config.GOOGLE_PSE_ENGINE_ID, + 'BRAVE_SEARCH_API_KEY': request.app.state.config.BRAVE_SEARCH_API_KEY, + 'BRAVE_SEARCH_CONTEXT_TOKENS': request.app.state.config.BRAVE_SEARCH_CONTEXT_TOKENS, + 'KAGI_SEARCH_API_KEY': request.app.state.config.KAGI_SEARCH_API_KEY, + 'MOJEEK_SEARCH_API_KEY': request.app.state.config.MOJEEK_SEARCH_API_KEY, + 'BOCHA_SEARCH_API_KEY': request.app.state.config.BOCHA_SEARCH_API_KEY, + 'SERPSTACK_API_KEY': request.app.state.config.SERPSTACK_API_KEY, + 'SERPSTACK_HTTPS': request.app.state.config.SERPSTACK_HTTPS, + 'SERPER_API_KEY': request.app.state.config.SERPER_API_KEY, + 'SERPLY_API_KEY': request.app.state.config.SERPLY_API_KEY, + 'TAVILY_API_KEY': request.app.state.config.TAVILY_API_KEY, + 'SEARCHAPI_API_KEY': request.app.state.config.SEARCHAPI_API_KEY, + 'SEARCHAPI_ENGINE': request.app.state.config.SEARCHAPI_ENGINE, + 'SERPAPI_API_KEY': request.app.state.config.SERPAPI_API_KEY, + 'SERPAPI_ENGINE': request.app.state.config.SERPAPI_ENGINE, + 'JINA_API_KEY': request.app.state.config.JINA_API_KEY, + 'JINA_API_BASE_URL': request.app.state.config.JINA_API_BASE_URL, + 'BING_SEARCH_V7_ENDPOINT': request.app.state.config.BING_SEARCH_V7_ENDPOINT, + 'BING_SEARCH_V7_SUBSCRIPTION_KEY': request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, + 'EXA_API_KEY': request.app.state.config.EXA_API_KEY, + 'PERPLEXITY_API_KEY': request.app.state.config.PERPLEXITY_API_KEY, + 'PERPLEXITY_MODEL': request.app.state.config.PERPLEXITY_MODEL, + 'PERPLEXITY_SEARCH_CONTEXT_USAGE': request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, + 'PERPLEXITY_SEARCH_API_URL': request.app.state.config.PERPLEXITY_SEARCH_API_URL, + 'SOUGOU_API_SID': request.app.state.config.SOUGOU_API_SID, + 'SOUGOU_API_SK': request.app.state.config.SOUGOU_API_SK, + 'WEB_LOADER_ENGINE': request.app.state.config.WEB_LOADER_ENGINE, + 'WEB_LOADER_TIMEOUT': request.app.state.config.WEB_LOADER_TIMEOUT, + 'ENABLE_WEB_LOADER_SSL_VERIFICATION': request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION, + 'PLAYWRIGHT_WS_URL': request.app.state.config.PLAYWRIGHT_WS_URL, + 'PLAYWRIGHT_TIMEOUT': request.app.state.config.PLAYWRIGHT_TIMEOUT, + 'FIRECRAWL_API_KEY': request.app.state.config.FIRECRAWL_API_KEY, + 'FIRECRAWL_API_BASE_URL': request.app.state.config.FIRECRAWL_API_BASE_URL, + 'FIRECRAWL_TIMEOUT': request.app.state.config.FIRECRAWL_TIMEOUT, + 'TAVILY_EXTRACT_DEPTH': request.app.state.config.TAVILY_EXTRACT_DEPTH, + 'EXTERNAL_WEB_SEARCH_URL': request.app.state.config.EXTERNAL_WEB_SEARCH_URL, + 'EXTERNAL_WEB_SEARCH_API_KEY': request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, + 'EXTERNAL_WEB_LOADER_URL': request.app.state.config.EXTERNAL_WEB_LOADER_URL, + 'EXTERNAL_WEB_LOADER_API_KEY': request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY, + 'YOUTUBE_LOADER_LANGUAGE': request.app.state.config.YOUTUBE_LOADER_LANGUAGE, + 'YOUTUBE_LOADER_PROXY_URL': request.app.state.config.YOUTUBE_LOADER_PROXY_URL, + 'YOUTUBE_LOADER_TRANSLATION': request.app.state.YOUTUBE_LOADER_TRANSLATION, + 'YANDEX_WEB_SEARCH_URL': request.app.state.config.YANDEX_WEB_SEARCH_URL, + 'YANDEX_WEB_SEARCH_API_KEY': request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, + 'YANDEX_WEB_SEARCH_CONFIG': request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + 'YOUCOM_API_KEY': request.app.state.config.YOUCOM_API_KEY, + 'LINKUP_API_KEY': request.app.state.config.LINKUP_API_KEY, + 'LINKUP_SEARCH_PARAMS': request.app.state.config.LINKUP_SEARCH_PARAMS, }, } @@ -1299,11 +1269,11 @@ async def update_rag_config( def can_merge_chunks(a: Document, b: Document) -> bool: - if a.metadata.get("source") != b.metadata.get("source"): + if a.metadata.get('source') != b.metadata.get('source'): return False - a_file_id = a.metadata.get("file_id") - b_file_id = b.metadata.get("file_id") + a_file_id = a.metadata.get('file_id') + b_file_id = b.metadata.get('file_id') if a_file_id is not None and b_file_id is not None: return a_file_id == b_file_id @@ -1321,67 +1291,78 @@ def merge_docs_to_target_size( Attempts to grow small chunks up to a desired minimum size, without exceeding the maximum size or crossing source/file boundaries. - """ - min_chunk_size_target = request.app.state.config.CHUNK_MIN_SIZE_TARGET - max_chunk_size = request.app.state.config.CHUNK_SIZE - if min_chunk_size_target <= 0: + Uses forward merging first (absorb the next chunk), then + backward merging (append into the previous emitted chunk) + for undersized chunks that can't grow forward. + """ + min_size = request.app.state.config.CHUNK_MIN_SIZE_TARGET + max_size = request.app.state.config.CHUNK_SIZE + + if min_size <= 0: return chunks - measure_chunk_size = len - if request.app.state.config.TEXT_SPLITTER == "token": - encoding = tiktoken.get_encoding( - str(request.app.state.config.TIKTOKEN_ENCODING_NAME) - ) - measure_chunk_size = lambda text: len(encoding.encode(text)) + measure: Callable[[str], int] = len + if request.app.state.config.TEXT_SPLITTER == 'token': + encoding = tiktoken.get_encoding(str(request.app.state.config.TIKTOKEN_ENCODING_NAME)) + measure = lambda text: len(encoding.encode(text)) - processed_chunks: list[Document] = [] + def _merge_backward(result: list[Document], content: str, chunk: Document) -> bool: + """Try to append content into the last emitted chunk. Returns True on success.""" + if not result: + return False + prev = result[-1] + if not can_merge_chunks(prev, chunk): + return False + merged = f"{prev.page_content}\n\n{content}" + if measure(merged) > max_size: + return False + result[-1] = Document(page_content=merged, metadata={**prev.metadata}) + return True + def _emit(result: list[Document], content: str, chunk: Document) -> None: + """Emit a chunk, trying backward merge first if it's undersized.""" + is_undersized = measure(content) < min_size + if is_undersized and _merge_backward(result, content, chunk): + return + result.append(Document(page_content=content, metadata={**chunk.metadata})) + + result: list[Document] = [] current_chunk: Document | None = None - current_content: str = "" + current_content: str = '' for next_chunk in chunks: if current_chunk is None: current_chunk = next_chunk current_content = next_chunk.page_content - continue # First chunk initialization + continue - proposed_content = f"{current_content}\n\n{next_chunk.page_content}" - - can_merge = ( + # Forward merge: absorb next chunk into current if undersized and fits + merged_content = f'{current_content}\n\n{next_chunk.page_content}' + can_merge_forward = ( can_merge_chunks(current_chunk, next_chunk) - and measure_chunk_size(current_content) < min_chunk_size_target - and measure_chunk_size(proposed_content) <= max_chunk_size + and measure(current_content) < min_size + and measure(merged_content) <= max_size ) - if can_merge: - current_content = proposed_content + if can_merge_forward: + current_content = merged_content else: - processed_chunks.append( - Document( - page_content=current_content, - metadata={**current_chunk.metadata}, - ) - ) + _emit(result, current_content, current_chunk) current_chunk = next_chunk current_content = next_chunk.page_content if current_chunk is not None: - processed_chunks.append( - Document( - page_content=current_content, - metadata={**current_chunk.metadata}, - ) - ) + _emit(result, current_content, current_chunk) - return processed_chunks + return result def save_docs_to_vector_db( request: Request, docs, collection_name, - metadata: Optional[dict] = None, + metadata: dict | None = None, overwrite: bool = False, split: bool = True, add: bool = False, @@ -1392,46 +1373,52 @@ def save_docs_to_vector_db( # Trying to select relevant metadata identifying the document. for doc in docs: - metadata = getattr(doc, "metadata", {}) - doc_name = metadata.get("name", "") + metadata = getattr(doc, 'metadata', {}) + doc_name = metadata.get('name', '') if not doc_name: - doc_name = metadata.get("title", "") + doc_name = metadata.get('title', '') if not doc_name: - doc_name = metadata.get("source", "") + doc_name = metadata.get('source', '') if doc_name: docs_info.add(doc_name) - return ", ".join(docs_info) + return ', '.join(docs_info) - log.debug( - f"save_docs_to_vector_db: document {_get_docs_info(docs)} {collection_name}" - ) + log.debug(f'save_docs_to_vector_db: document {_get_docs_info(docs)} {collection_name}') # Check if entries with the same hash (metadata.hash) already exist - if metadata and "hash" in metadata: + if metadata and 'hash' in metadata: result = VECTOR_DB_CLIENT.query( collection_name=collection_name, - filter={"hash": metadata["hash"]}, + filter={'hash': metadata['hash']}, ) if result is not None and result.ids and len(result.ids) > 0: existing_doc_ids = result.ids[0] if existing_doc_ids: - log.info(f"Document with hash {metadata['hash']} already exists") - raise ValueError(ERROR_MESSAGES.DUPLICATE_CONTENT) + # Check if the existing document belongs to the same file + # If same file_id, this is a re-add/reindex - allow it + # If different file_id, this is a duplicate - block it + existing_file_id = None + if result.metadatas and result.metadatas[0]: + existing_file_id = result.metadatas[0][0].get('file_id') + + if existing_file_id != metadata.get('file_id'): + log.info(f'Document with hash {metadata["hash"]} already exists') + raise ValueError(ERROR_MESSAGES.DUPLICATE_CONTENT) if split: if request.app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: - log.info("Using markdown header text splitter") + log.info('Using markdown header text splitter') # Define headers to split on - covering most common markdown header levels markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[ - ("#", "Header 1"), - ("##", "Header 2"), - ("###", "Header 3"), - ("####", "Header 4"), - ("#####", "Header 5"), - ("######", "Header 6"), + ('#', 'Header 1'), + ('##', 'Header 2'), + ('###', 'Header 3'), + ('####', 'Header 4'), + ('#####', 'Header 5'), + ('######', 'Header 6'), ], strip_headers=False, # Keep headers in content for context ) @@ -1444,9 +1431,7 @@ def save_docs_to_vector_db( page_content=split_chunk.page_content, metadata={**doc.metadata}, ) - for split_chunk in markdown_splitter.split_text( - doc.page_content - ) + for split_chunk in markdown_splitter.split_text(doc.page_content) ] ) @@ -1454,17 +1439,15 @@ def save_docs_to_vector_db( if request.app.state.config.CHUNK_MIN_SIZE_TARGET > 0: docs = merge_docs_to_target_size(request, docs) - if request.app.state.config.TEXT_SPLITTER in ["", "character"]: + if request.app.state.config.TEXT_SPLITTER in ['', 'character']: text_splitter = RecursiveCharacterTextSplitter( chunk_size=request.app.state.config.CHUNK_SIZE, chunk_overlap=request.app.state.config.CHUNK_OVERLAP, add_start_index=True, ) docs = text_splitter.split_documents(docs) - elif request.app.state.config.TEXT_SPLITTER == "token": - log.info( - f"Using token text splitter: {request.app.state.config.TIKTOKEN_ENCODING_NAME}" - ) + elif request.app.state.config.TEXT_SPLITTER == 'token': + log.info(f'Using token text splitter: {request.app.state.config.TIKTOKEN_ENCODING_NAME}') tiktoken.get_encoding(str(request.app.state.config.TIKTOKEN_ENCODING_NAME)) text_splitter = TokenTextSplitter( @@ -1475,7 +1458,7 @@ def save_docs_to_vector_db( ) docs = text_splitter.split_documents(docs) else: - raise ValueError(ERROR_MESSAGES.DEFAULT("Invalid text splitter")) + raise ValueError(ERROR_MESSAGES.DEFAULT('Invalid text splitter')) if len(docs) == 0: raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) @@ -1485,9 +1468,9 @@ def save_docs_to_vector_db( { **doc.metadata, **(metadata if metadata else {}), - "embedding_config": { - "engine": request.app.state.config.RAG_EMBEDDING_ENGINE, - "model": request.app.state.config.RAG_EMBEDDING_MODEL, + 'embedding_config': { + 'engine': request.app.state.config.RAG_EMBEDDING_ENGINE, + 'model': request.app.state.config.RAG_EMBEDDING_MODEL, }, } for doc in docs @@ -1495,76 +1478,80 @@ def save_docs_to_vector_db( try: if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name): - log.info(f"collection {collection_name} already exists") + log.info(f'collection {collection_name} already exists') if overwrite: VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name) - log.info(f"deleting existing collection {collection_name}") + log.info(f'deleting existing collection {collection_name}') elif add is False: - log.info( - f"collection {collection_name} already exists, overwrite is False and add is False" - ) + log.info(f'collection {collection_name} already exists, overwrite is False and add is False') return True - log.info(f"generating embeddings for {collection_name}") + log.info(f'generating embeddings for {collection_name}') embedding_function = get_embedding_function( request.app.state.config.RAG_EMBEDDING_ENGINE, request.app.state.config.RAG_EMBEDDING_MODEL, request.app.state.ef, ( request.app.state.config.RAG_OPENAI_API_BASE_URL - if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' else ( request.app.state.config.RAG_OLLAMA_BASE_URL - if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' else request.app.state.config.RAG_AZURE_OPENAI_BASE_URL ) ), ( request.app.state.config.RAG_OPENAI_API_KEY - if request.app.state.config.RAG_EMBEDDING_ENGINE == "openai" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'openai' else ( request.app.state.config.RAG_OLLAMA_API_KEY - if request.app.state.config.RAG_EMBEDDING_ENGINE == "ollama" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'ollama' else request.app.state.config.RAG_AZURE_OPENAI_API_KEY ) ), request.app.state.config.RAG_EMBEDDING_BATCH_SIZE, azure_api_version=( request.app.state.config.RAG_AZURE_OPENAI_API_VERSION - if request.app.state.config.RAG_EMBEDDING_ENGINE == "azure_openai" + if request.app.state.config.RAG_EMBEDDING_ENGINE == 'azure_openai' else None ), enable_async=request.app.state.config.ENABLE_ASYNC_EMBEDDING, + concurrent_requests=request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS, ) - # Run async embedding in sync context - embeddings = asyncio.run( + # Run async embedding in sync context using the main event loop + # This allows the main loop to stay responsive to health checks during long operations + embedding_timeout = RAG_EMBEDDING_TIMEOUT + + future = asyncio.run_coroutine_threadsafe( embedding_function( - list(map(lambda x: x.replace("\n", " "), texts)), + list(map(lambda x: x.replace('\n', ' '), texts)), prefix=RAG_EMBEDDING_CONTENT_PREFIX, user=user, - ) + ), + request.app.state.main_loop, ) - log.info(f"embeddings generated {len(embeddings)} for {len(texts)} items") + embeddings = future.result(timeout=embedding_timeout) + log.info(f'embeddings generated {len(embeddings)} for {len(texts)} items') items = [ { - "id": str(uuid.uuid4()), - "text": text, - "vector": embeddings[idx], - "metadata": metadatas[idx], + 'id': str(uuid.uuid4()), + 'text': text, + 'vector': embeddings[idx], + 'metadata': metadatas[idx], } for idx, text in enumerate(texts) ] - log.info(f"adding to collection {collection_name}") + log.info(f'adding to collection {collection_name}') VECTOR_DB_CLIENT.insert( collection_name=collection_name, items=items, ) - log.info(f"added {len(items)} items to collection {collection_name}") + log.info(f'added {len(items)} items to collection {collection_name}') return True except Exception as e: log.exception(e) @@ -1573,32 +1560,36 @@ def save_docs_to_vector_db( class ProcessFileForm(BaseModel): file_id: str - content: Optional[str] = None - collection_name: Optional[str] = None + content: str | None = None + collection_name: str | None = None -@router.post("/process/file") -def process_file( +@router.post('/process/file') +async def process_file( request: Request, form_data: ProcessFileForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """ Process a file and save its content to the vector database. + Process a file and save its content to the vector database. + Note: granular session management is used to prevent connection pool exhaustion. + The session is committed before external API calls, and updates use a fresh session. """ - if user.role == "admin": - file = Files.get_file_by_id(form_data.file_id, db=db) + if user.role == 'admin': + file = await Files.get_file_by_id(form_data.file_id, db=db) else: - file = Files.get_file_by_id_and_user_id(form_data.file_id, user.id, db=db) + file = await Files.get_file_by_id_and_user_id(form_data.file_id, user.id, db=db) if file: try: - collection_name = form_data.collection_name if collection_name is None: - collection_name = f"file-{file.id}" + collection_name = f'file-{file.id}' + else: + await _validate_collection_access([collection_name], user, access_type='write') if form_data.content: # Update the content in the file @@ -1606,22 +1597,20 @@ def process_file( try: # /files/{file_id}/data/content/update - VECTOR_DB_CLIENT.delete_collection( - collection_name=f"file-{file.id}" - ) - except: + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name=f'file-{file.id}') + except Exception: # Audio file upload pipeline pass docs = [ Document( - page_content=form_data.content.replace("
", "\n"), + page_content=form_data.content.replace('
', '\n'), metadata={ **file.meta, - "name": file.filename, - "created_by": file.user_id, - "file_id": file.id, - "source": file.filename, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, }, ) ] @@ -1631,8 +1620,8 @@ def process_file( # Check if the file has already been processed and save the content # Usage: /knowledge/{id}/file/add, /knowledge/{id}/file/update - result = VECTOR_DB_CLIENT.query( - collection_name=f"file-{file.id}", filter={"file_id": file.id} + result = await ASYNC_VECTOR_DB_CLIENT.query( + collection_name=f'file-{file.id}', filter={'file_id': file.id} ) if result is not None and len(result.ids[0]) > 0: @@ -1646,69 +1635,37 @@ def process_file( else: docs = [ Document( - page_content=file.data.get("content", ""), + page_content=file.data.get('content', ''), metadata={ **file.meta, - "name": file.filename, - "created_by": file.user_id, - "file_id": file.id, - "source": file.filename, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, }, ) ] - text_content = file.data.get("content", "") + text_content = file.data.get('content', '') else: # Process the file and save the content # Usage: /files/ file_path = file.path if file_path: - file_path = Storage.get_file(file_path) - loader = Loader( - engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE, - user=user, - DATALAB_MARKER_API_KEY=request.app.state.config.DATALAB_MARKER_API_KEY, - DATALAB_MARKER_API_BASE_URL=request.app.state.config.DATALAB_MARKER_API_BASE_URL, - DATALAB_MARKER_ADDITIONAL_CONFIG=request.app.state.config.DATALAB_MARKER_ADDITIONAL_CONFIG, - DATALAB_MARKER_SKIP_CACHE=request.app.state.config.DATALAB_MARKER_SKIP_CACHE, - DATALAB_MARKER_FORCE_OCR=request.app.state.config.DATALAB_MARKER_FORCE_OCR, - DATALAB_MARKER_PAGINATE=request.app.state.config.DATALAB_MARKER_PAGINATE, - DATALAB_MARKER_STRIP_EXISTING_OCR=request.app.state.config.DATALAB_MARKER_STRIP_EXISTING_OCR, - DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION=request.app.state.config.DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, - DATALAB_MARKER_FORMAT_LINES=request.app.state.config.DATALAB_MARKER_FORMAT_LINES, - DATALAB_MARKER_USE_LLM=request.app.state.config.DATALAB_MARKER_USE_LLM, - DATALAB_MARKER_OUTPUT_FORMAT=request.app.state.config.DATALAB_MARKER_OUTPUT_FORMAT, - EXTERNAL_DOCUMENT_LOADER_URL=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL, - EXTERNAL_DOCUMENT_LOADER_API_KEY=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY, - TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL, - DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL, - DOCLING_API_KEY=request.app.state.config.DOCLING_API_KEY, - DOCLING_PARAMS=request.app.state.config.DOCLING_PARAMS, - PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, - DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, - DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, - DOCUMENT_INTELLIGENCE_MODEL=request.app.state.config.DOCUMENT_INTELLIGENCE_MODEL, - MISTRAL_OCR_API_BASE_URL=request.app.state.config.MISTRAL_OCR_API_BASE_URL, - MISTRAL_OCR_API_KEY=request.app.state.config.MISTRAL_OCR_API_KEY, - MINERU_API_MODE=request.app.state.config.MINERU_API_MODE, - MINERU_API_URL=request.app.state.config.MINERU_API_URL, - MINERU_API_KEY=request.app.state.config.MINERU_API_KEY, - MINERU_API_TIMEOUT=request.app.state.config.MINERU_API_TIMEOUT, - MINERU_PARAMS=request.app.state.config.MINERU_PARAMS, - ) - docs = loader.load( - file.filename, file.meta.get("content_type"), file_path - ) + file_path = await asyncio.to_thread(Storage.get_file, file_path) + loader = build_loader_from_config(request) + loader.user = user + docs = await loader.aload(file.filename, file.meta.get('content_type'), file_path) docs = [ Document( page_content=doc.page_content, metadata={ **filter_metadata(doc.metadata), - "name": file.filename, - "created_by": file.user_id, - "file_id": file.id, - "source": file.filename, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, }, ) for doc in docs @@ -1716,89 +1673,104 @@ def process_file( else: docs = [ Document( - page_content=file.data.get("content", ""), + page_content=file.data.get('content', ''), metadata={ **file.meta, - "name": file.filename, - "created_by": file.user_id, - "file_id": file.id, - "source": file.filename, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, }, ) ] - text_content = " ".join([doc.page_content for doc in docs]) + text_content = ' '.join([doc.page_content for doc in docs]) - log.debug(f"text_content: {text_content}") - Files.update_file_data_by_id( + log.debug(f'text_content: {text_content}') + await Files.update_file_data_by_id( file.id, - {"content": text_content}, + {'content': text_content}, db=db, ) hash = calculate_sha256_string(text_content) if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: - Files.update_file_data_by_id(file.id, {"status": "completed"}, db=db) - Files.update_file_hash_by_id(file.id, hash, db=db) + await Files.update_file_data_by_id(file.id, {'status': 'completed'}, db=db) + await Files.update_file_hash_by_id(file.id, hash, db=db) return { - "status": True, - "collection_name": None, - "filename": file.filename, - "content": text_content, + 'status': True, + 'collection_name': None, + 'filename': file.filename, + 'content': text_content, } else: try: - result = save_docs_to_vector_db( + # Commit any pending changes before the slow embedding step. + # Note: file is already a Pydantic model (not ORM), so no expunge needed. + await db.commit() + + # External embedding API takes time (5-60s+). + # Subsequent updates use fresh async sessions. + # NOTE: save_docs_to_vector_db is a sync function that + # calls asyncio.run_coroutine_threadsafe(..., main_loop).result() + # which blocks the calling thread. We MUST run it in a + # worker thread to avoid deadlocking the event loop. + result = await run_in_threadpool( + save_docs_to_vector_db, request, docs=docs, collection_name=collection_name, metadata={ - "file_id": file.id, - "name": file.filename, - "hash": hash, + 'file_id': file.id, + 'name': file.filename, + 'hash': hash, }, add=(True if form_data.collection_name else False), user=user, ) - log.info(f"added {len(docs)} items to collection {collection_name}") + log.info(f'added {len(docs)} items to collection {collection_name}') if result: - Files.update_file_metadata_by_id( - file.id, - { - "collection_name": collection_name, - }, - db=db, - ) + # Fresh session for the final update. + async with get_async_db() as session: + await Files.update_file_metadata_by_id( + file.id, + { + 'collection_name': collection_name, + }, + db=session, + ) - Files.update_file_data_by_id( - file.id, - {"status": "completed"}, - db=db, - ) - Files.update_file_hash_by_id(file.id, hash, db=db) + await Files.update_file_data_by_id( + file.id, + {'status': 'completed'}, + db=session, + ) + await Files.update_file_hash_by_id(file.id, hash, db=session) - return { - "status": True, - "collection_name": collection_name, - "filename": file.filename, - "content": text_content, - } + return { + 'status': True, + 'collection_name': collection_name, + 'filename': file.filename, + 'content': text_content, + } else: - raise Exception("Error saving document to vector database") + raise Exception('Error saving document to vector database') except Exception as e: raise e except Exception as e: log.exception(e) - Files.update_file_data_by_id( - file.id, - {"status": "failed"}, - db=db, - ) - # Clear the hash so the file can be re-uploaded after fixing the issue - Files.update_file_hash_by_id(file.id, None, db=db) + # Fresh session for error status update. + async with get_async_db() as session: + await Files.update_file_data_by_id( + file.id, + {'status': 'failed'}, + db=session, + ) + # Clear the hash so the file can be re-uploaded after fixing the issue + await Files.update_file_hash_by_id(file.id, None, db=session) - if "No pandoc was found" in str(e): + if 'No pandoc was found' in str(e): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED, @@ -1810,18 +1782,16 @@ def process_file( ) else: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND) class ProcessTextForm(BaseModel): name: str content: str - collection_name: Optional[str] = None + collection_name: str | None = None -@router.post("/process/text") +@router.post('/process/text') async def process_text( request: Request, form_data: ProcessTextForm, @@ -1830,24 +1800,24 @@ async def process_text( collection_name = form_data.collection_name if collection_name is None: collection_name = calculate_sha256_string(form_data.content) + else: + await _validate_collection_access([collection_name], user, access_type='write') docs = [ Document( page_content=form_data.content, - metadata={"name": form_data.name, "created_by": user.id}, + metadata={'name': form_data.name, 'created_by': user.id}, ) ] text_content = form_data.content - log.debug(f"text_content: {text_content}") + log.debug(f'text_content: {text_content}') - result = await run_in_threadpool( - save_docs_to_vector_db, request, docs, collection_name, user=user - ) + result = await run_in_threadpool(save_docs_to_vector_db, request, docs, collection_name, user=user) if result: return { - "status": True, - "collection_name": collection_name, - "content": text_content, + 'status': True, + 'collection_name': collection_name, + 'content': text_content, } else: raise HTTPException( @@ -1856,24 +1826,25 @@ async def process_text( ) -@router.post("/process/youtube") -@router.post("/process/web") +@router.post('/process/youtube') +@router.post('/process/web') async def process_web( request: Request, form_data: ProcessUrlForm, - process: bool = Query(True, description="Whether to process and save the content"), + process: bool = Query(True, description='Whether to process and save the content'), + overwrite: bool = Query(True, description='Whether to overwrite existing collection'), user=Depends(get_verified_user), ): try: - content, docs = await run_in_threadpool( - get_content_from_url, request, form_data.url - ) - log.debug(f"text_content: {content}") + content, docs = await run_in_threadpool(get_content_from_url, request, form_data.url) + log.debug(f'text_content: {content}') if process: collection_name = form_data.collection_name if not collection_name: collection_name = calculate_sha256_string(form_data.url)[:63] + else: + await _validate_collection_access([collection_name], user, access_type='write') if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: await run_in_threadpool( @@ -1881,30 +1852,31 @@ async def process_web( request, docs, collection_name, - overwrite=True, + overwrite=overwrite, + add=(not overwrite), user=user, ) else: collection_name = None return { - "status": True, - "collection_name": collection_name, - "filename": form_data.url, - "file": { - "data": { - "content": content, + 'status': True, + 'collection_name': collection_name, + 'filename': form_data.url, + 'file': { + 'data': { + 'content': content, }, - "meta": { - "name": form_data.url, - "source": form_data.url, + 'meta': { + 'name': form_data.url, + 'source': form_data.url, }, }, } else: return { - "status": True, - "content": content, + 'status': True, + 'content': content, } except Exception as e: log.exception(e) @@ -1914,43 +1886,28 @@ async def process_web( ) -def search_web( - request: Request, engine: str, query: str, user=None -) -> list[SearchResult]: - """Search the web using a search engine and return the results as a list of SearchResult objects. - Will look for a search engine API key in environment variables in the following order: - - SEARXNG_QUERY_URL - - YACY_QUERY_URL + YACY_USERNAME + YACY_PASSWORD - - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID - - BRAVE_SEARCH_API_KEY - - KAGI_SEARCH_API_KEY - - MOJEEK_SEARCH_API_KEY - - BOCHA_SEARCH_API_KEY - - SERPSTACK_API_KEY - - SERPER_API_KEY - - SERPLY_API_KEY - - TAVILY_API_KEY - - EXA_API_KEY - - PERPLEXITY_API_KEY - - SOUGOU_API_SID + SOUGOU_API_SK - - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`) - - SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`) - Args: - query (str): The query to search for +async def search_web(request: Request, engine: str, query: str, user=None) -> list[SearchResult]: + """Dispatch a web search query to the configured engine and return results. + + Providers that have been migrated to async (aiohttp) are awaited natively. + Legacy sync providers are offloaded via ``asyncio.to_thread`` to avoid + blocking the event loop. """ # TODO: add playwright to search the web - if engine == "ollama_cloud": - return search_ollama_cloud( - "https://ollama.com", + if engine == 'ollama_cloud': + return await asyncio.to_thread( + search_ollama_cloud, + 'https://ollama.com', request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) - elif engine == "perplexity_search": + elif engine == 'perplexity_search': if request.app.state.config.PERPLEXITY_API_KEY: - return search_perplexity_search( + return await asyncio.to_thread( + search_perplexity_search, request.app.state.config.PERPLEXITY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1959,11 +1916,11 @@ def search_web( user, ) else: - raise Exception("No PERPLEXITY_API_KEY found in environment variables") - elif engine == "searxng": + raise Exception('No PERPLEXITY_API_KEY found in environment variables') + elif engine == 'searxng': if request.app.state.config.SEARXNG_QUERY_URL: - searxng_kwargs = {"language": request.app.state.config.SEARXNG_LANGUAGE} - return search_searxng( + searxng_kwargs = {'language': request.app.state.config.SEARXNG_LANGUAGE} + return await search_searxng( request.app.state.config.SEARXNG_QUERY_URL, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1971,10 +1928,11 @@ def search_web( **searxng_kwargs, ) else: - raise Exception("No SEARXNG_QUERY_URL found in environment variables") - elif engine == "yacy": + raise Exception('No SEARXNG_QUERY_URL found in environment variables') + elif engine == 'yacy': if request.app.state.config.YACY_QUERY_URL: - return search_yacy( + return await asyncio.to_thread( + search_yacy, request.app.state.config.YACY_QUERY_URL, request.app.state.config.YACY_USERNAME, request.app.state.config.YACY_PASSWORD, @@ -1983,13 +1941,10 @@ def search_web( request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No YACY_QUERY_URL found in environment variables") - elif engine == "google_pse": - if ( - request.app.state.config.GOOGLE_PSE_API_KEY - and request.app.state.config.GOOGLE_PSE_ENGINE_ID - ): - return search_google_pse( + raise Exception('No YACY_QUERY_URL found in environment variables') + elif engine == 'google_pse': + if request.app.state.config.GOOGLE_PSE_API_KEY and request.app.state.config.GOOGLE_PSE_ENGINE_ID: + return await search_google_pse( request.app.state.config.GOOGLE_PSE_API_KEY, request.app.state.config.GOOGLE_PSE_ENGINE_ID, query, @@ -1998,52 +1953,65 @@ def search_web( referer=request.app.state.config.WEBUI_URL, ) else: - raise Exception( - "No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables" - ) - elif engine == "brave": + raise Exception('No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables') + elif engine == 'brave': if request.app.state.config.BRAVE_SEARCH_API_KEY: - return search_brave( + return await search_brave( request.app.state.config.BRAVE_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables") - elif engine == "kagi": + raise Exception('No BRAVE_SEARCH_API_KEY found in environment variables') + elif engine == 'brave_llm_context': + if request.app.state.config.BRAVE_SEARCH_API_KEY: + return await asyncio.to_thread( + search_brave_llm_context, + request.app.state.config.BRAVE_SEARCH_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + request.app.state.config.BRAVE_SEARCH_CONTEXT_TOKENS, + ) + else: + raise Exception('No BRAVE_SEARCH_API_KEY found in environment variables') + elif engine == 'kagi': if request.app.state.config.KAGI_SEARCH_API_KEY: - return search_kagi( + return await asyncio.to_thread( + search_kagi, request.app.state.config.KAGI_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No KAGI_SEARCH_API_KEY found in environment variables") - elif engine == "mojeek": + raise Exception('No KAGI_SEARCH_API_KEY found in environment variables') + elif engine == 'mojeek': if request.app.state.config.MOJEEK_SEARCH_API_KEY: - return search_mojeek( + return await asyncio.to_thread( + search_mojeek, request.app.state.config.MOJEEK_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No MOJEEK_SEARCH_API_KEY found in environment variables") - elif engine == "bocha": + raise Exception('No MOJEEK_SEARCH_API_KEY found in environment variables') + elif engine == 'bocha': if request.app.state.config.BOCHA_SEARCH_API_KEY: - return search_bocha( + return await asyncio.to_thread( + search_bocha, request.app.state.config.BOCHA_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No BOCHA_SEARCH_API_KEY found in environment variables") - elif engine == "serpstack": + raise Exception('No BOCHA_SEARCH_API_KEY found in environment variables') + elif engine == 'serpstack': if request.app.state.config.SERPSTACK_API_KEY: - return search_serpstack( + return await search_serpstack( request.app.state.config.SERPSTACK_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2051,58 +2019,63 @@ def search_web( https_enabled=request.app.state.config.SERPSTACK_HTTPS, ) else: - raise Exception("No SERPSTACK_API_KEY found in environment variables") - elif engine == "serper": + raise Exception('No SERPSTACK_API_KEY found in environment variables') + elif engine == 'serper': if request.app.state.config.SERPER_API_KEY: - return search_serper( + return await search_serper( request.app.state.config.SERPER_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No SERPER_API_KEY found in environment variables") - elif engine == "serply": + raise Exception('No SERPER_API_KEY found in environment variables') + elif engine == 'serply': if request.app.state.config.SERPLY_API_KEY: - return search_serply( + return await asyncio.to_thread( + search_serply, request.app.state.config.SERPLY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, filter_list=request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No SERPLY_API_KEY found in environment variables") - elif engine == "duckduckgo": - return search_duckduckgo( + raise Exception('No SERPLY_API_KEY found in environment variables') + elif engine == 'duckduckgo': + return await asyncio.to_thread( + search_duckduckgo, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, concurrent_requests=request.app.state.config.WEB_SEARCH_CONCURRENT_REQUESTS, backend=request.app.state.config.DDGS_BACKEND, ) - elif engine == "tavily": + elif engine == 'tavily': if request.app.state.config.TAVILY_API_KEY: - return search_tavily( + return await asyncio.to_thread( + search_tavily, request.app.state.config.TAVILY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No TAVILY_API_KEY found in environment variables") - elif engine == "exa": + raise Exception('No TAVILY_API_KEY found in environment variables') + elif engine == 'exa': if request.app.state.config.EXA_API_KEY: - return search_exa( + return await asyncio.to_thread( + search_exa, request.app.state.config.EXA_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No EXA_API_KEY found in environment variables") - elif engine == "searchapi": + raise Exception('No EXA_API_KEY found in environment variables') + elif engine == 'searchapi': if request.app.state.config.SEARCHAPI_API_KEY: - return search_searchapi( + return await asyncio.to_thread( + search_searchapi, request.app.state.config.SEARCHAPI_API_KEY, request.app.state.config.SEARCHAPI_ENGINE, query, @@ -2110,10 +2083,11 @@ def search_web( request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No SEARCHAPI_API_KEY found in environment variables") - elif engine == "serpapi": + raise Exception('No SEARCHAPI_API_KEY found in environment variables') + elif engine == 'serpapi': if request.app.state.config.SERPAPI_API_KEY: - return search_serpapi( + return await asyncio.to_thread( + search_serpapi, request.app.state.config.SERPAPI_API_KEY, request.app.state.config.SERPAPI_ENGINE, query, @@ -2121,16 +2095,18 @@ def search_web( request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception("No SERPAPI_API_KEY found in environment variables") - elif engine == "jina": - return search_jina( + raise Exception('No SERPAPI_API_KEY found in environment variables') + elif engine == 'jina': + return await asyncio.to_thread( + search_jina, request.app.state.config.JINA_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.JINA_API_BASE_URL, ) - elif engine == "bing": - return search_bing( + elif engine == 'bing': + return await asyncio.to_thread( + search_bing, request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, request.app.state.config.BING_SEARCH_V7_ENDPOINT, str(DEFAULT_LOCALE), @@ -2138,13 +2114,14 @@ def search_web( request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) - elif engine == "azure": + elif engine == 'azure': if ( request.app.state.config.AZURE_AI_SEARCH_API_KEY and request.app.state.config.AZURE_AI_SEARCH_ENDPOINT and request.app.state.config.AZURE_AI_SEARCH_INDEX_NAME ): - return search_azure( + return await asyncio.to_thread( + search_azure, request.app.state.config.AZURE_AI_SEARCH_API_KEY, request.app.state.config.AZURE_AI_SEARCH_ENDPOINT, request.app.state.config.AZURE_AI_SEARCH_INDEX_NAME, @@ -2154,17 +2131,11 @@ def search_web( ) else: raise Exception( - "AZURE_AI_SEARCH_API_KEY, AZURE_AI_SEARCH_ENDPOINT, and AZURE_AI_SEARCH_INDEX_NAME are required for Azure AI Search" + 'AZURE_AI_SEARCH_API_KEY, AZURE_AI_SEARCH_ENDPOINT, and AZURE_AI_SEARCH_INDEX_NAME are required for Azure AI Search' ) - elif engine == "exa": - return search_exa( - request.app.state.config.EXA_API_KEY, - query, - request.app.state.config.WEB_SEARCH_RESULT_COUNT, - request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, - ) - elif engine == "perplexity": - return search_perplexity( + elif engine == 'perplexity': + return await asyncio.to_thread( + search_perplexity, request.app.state.config.PERPLEXITY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2172,12 +2143,10 @@ def search_web( model=request.app.state.config.PERPLEXITY_MODEL, search_context_usage=request.app.state.config.PERPLEXITY_SEARCH_CONTEXT_USAGE, ) - elif engine == "sougou": - if ( - request.app.state.config.SOUGOU_API_SID - and request.app.state.config.SOUGOU_API_SK - ): - return search_sougou( + elif engine == 'sougou': + if request.app.state.config.SOUGOU_API_SID and request.app.state.config.SOUGOU_API_SK: + return await asyncio.to_thread( + search_sougou, request.app.state.config.SOUGOU_API_SID, request.app.state.config.SOUGOU_API_SK, query, @@ -2185,19 +2154,19 @@ def search_web( request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: - raise Exception( - "No SOUGOU_API_SID or SOUGOU_API_SK found in environment variables" - ) - elif engine == "firecrawl": - return search_firecrawl( + raise Exception('No SOUGOU_API_SID or SOUGOU_API_SK found in environment variables') + elif engine == 'firecrawl': + return await asyncio.to_thread( + search_firecrawl, request.app.state.config.FIRECRAWL_API_BASE_URL, request.app.state.config.FIRECRAWL_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) - elif engine == "external": - return search_external( + elif engine == 'external': + return await asyncio.to_thread( + search_external, request, request.app.state.config.EXTERNAL_WEB_SEARCH_URL, request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, @@ -2206,22 +2175,53 @@ def search_web( request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, user=user, ) + elif engine == 'yandex': + return await asyncio.to_thread( + search_yandex, + request, + request.app.state.config.YANDEX_WEB_SEARCH_URL, + request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, + request.app.state.config.YANDEX_WEB_SEARCH_CONFIG, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + user=user, + ) + elif engine == 'youcom': + return await asyncio.to_thread( + search_youcom, + request.app.state.config.YOUCOM_API_KEY, + query, + request.app.state.config.WEB_SEARCH_RESULT_COUNT, + request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + elif engine == 'linkup': + if request.app.state.config.LINKUP_API_KEY: + return await asyncio.to_thread( + search_linkup, + api_key=request.app.state.config.LINKUP_API_KEY, + query=query, + count=request.app.state.config.WEB_SEARCH_RESULT_COUNT, + filter_list=request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + params=request.app.state.config.LINKUP_SEARCH_PARAMS, + ) + else: + raise Exception('No LINKUP_API_KEY found in environment variables') else: - raise Exception("No search engine API key found in environment variables") + raise Exception('No search engine API key found in environment variables') -@router.post("/process/web/search") -async def process_web_search( - request: Request, form_data: SearchForm, user=Depends(get_verified_user) -): + +@router.post('/process/web/search') +async def process_web_search(request: Request, form_data: SearchForm, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_WEB_SEARCH: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - if user.role != "admin" and not has_permission( - user.id, "features.web_search", request.app.state.config.USER_PERMISSIONS + if user.role != 'admin' and not await has_permission( + user.id, 'features.web_search', request.app.state.config.USER_PERMISSIONS ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -2232,9 +2232,7 @@ async def process_web_search( result_items = [] try: - logging.debug( - f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}" - ) + logging.debug(f'trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}') # Use semaphore to limit concurrent requests based on WEB_SEARCH_CONCURRENT_REQUESTS # 0 or None = unlimited (previous behavior), positive number = limited concurrency @@ -2245,22 +2243,20 @@ async def process_web_search( # Limited concurrency with semaphore semaphore = asyncio.Semaphore(concurrent_limit) - async def search_with_limit(query): + async def search_query_with_semaphore(query): async with semaphore: - return await run_in_threadpool( - search_web, + return await search_web( request, request.app.state.config.WEB_SEARCH_ENGINE, query, user, ) - search_tasks = [search_with_limit(query) for query in form_data.queries] + search_tasks = [search_query_with_semaphore(query) for query in form_data.queries] else: - # Unlimited parallel execution (previous behavior) + # Unlimited parallel execution search_tasks = [ - run_in_threadpool( - search_web, + search_web( request, request.app.state.config.WEB_SEARCH_ENGINE, query, @@ -2279,40 +2275,34 @@ async def process_web_search( urls.append(item.link) urls = list(dict.fromkeys(urls)) - log.debug(f"urls: {urls}") + log.debug(f'urls: {urls}') except Exception as e: - log.exception(e) - - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e), - ) + log.exception('Web search failed') + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e)) if len(urls) == 0: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=ERROR_MESSAGES.DEFAULT("No results found from web search"), + detail=ERROR_MESSAGES.DEFAULT('No results found from web search'), ) try: if request.app.state.config.BYPASS_WEB_SEARCH_WEB_LOADER: - search_results = [ - item for result in search_results for item in result if result - ] + search_results = [item for result in search_results for item in result if result] docs = [ Document( page_content=result.snippet, metadata={ - "source": result.link, - "title": result.title, - "snippet": result.snippet, - "link": result.link, + 'source': result.link, + 'title': result.title, + 'snippet': result.snippet, + 'link': result.link, }, ) for result in search_results - if hasattr(result, "snippet") and result.snippet is not None + if hasattr(result, 'snippet') and result.snippet is not None ] else: loader = get_web_loader( @@ -2324,7 +2314,7 @@ async def process_web_search( docs = await loader.aload() urls = [ - doc.metadata.get("source") for doc in docs if doc.metadata.get("source") + doc.metadata.get('source') for doc in docs if doc.metadata.get('source') ] # only keep the urls returned by the loader result_items = [ dict(item) for item in result_items if item.link in urls @@ -2332,26 +2322,22 @@ async def process_web_search( if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: return { - "status": True, - "collection_name": None, - "filenames": urls, - "items": result_items, - "docs": [ + 'status': True, + 'collection_name': None, + 'filenames': urls, + 'items': result_items, + 'docs': [ { - "content": doc.page_content, - "metadata": doc.metadata, + 'content': doc.page_content, + 'metadata': doc.metadata, } for doc in docs ], - "loaded_count": len(docs), + 'loaded_count': len(docs), } else: # Create a single collection for all documents - collection_name = ( - f"web-search-{calculate_sha256_string('-'.join(form_data.queries))}"[ - :63 - ] - ) + collection_name = f'web-search-{calculate_sha256_string("-".join(form_data.queries))}'[:63] try: await run_in_threadpool( @@ -2363,44 +2349,57 @@ async def process_web_search( user=user, ) except Exception as e: - log.debug(f"error saving docs: {e}") + log.debug(f'error saving docs: {e}') return { - "status": True, - "collection_names": [collection_name], - "items": result_items, - "filenames": urls, - "loaded_count": len(docs), + 'status': True, + 'collection_names': [collection_name], + 'items': result_items, + 'filenames': urls, + 'loaded_count': len(docs), } except Exception as e: - log.exception(e) + log.exception('Web search content loading failed') + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e)) + + +async def _validate_collection_access(collection_names: list[str], user, access_type: str = 'read') -> None: + """ + Raise 403 if the user lacks access to any of the requested collections. + Delegates to the shared filter_accessible_collections utility so the + access rules stay in one place. + """ + requested = set(collection_names) + allowed = await filter_accessible_collections(requested, user, access_type=access_type) + denied = requested - allowed + if denied: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(e), + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) class QueryDocForm(BaseModel): collection_name: str query: str - k: Optional[int] = None - k_reranker: Optional[int] = None - r: Optional[float] = None - hybrid: Optional[bool] = None + k: int | None = None + k_reranker: int | None = None + r: float | None = None + hybrid: bool | None = None -@router.post("/query/doc") +@router.post('/query/doc') async def query_doc_handler( request: Request, form_data: QueryDocForm, user=Depends(get_verified_user), ): + await _validate_collection_access([form_data.collection_name], user) + try: - if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and ( - form_data.hybrid is None or form_data.hybrid - ): + if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and (form_data.hybrid is None or form_data.hybrid): collection_results = {} - collection_results[form_data.collection_name] = VECTOR_DB_CLIENT.get( + collection_results[form_data.collection_name] = await ASYNC_VECTOR_DB_CLIENT.get( collection_name=form_data.collection_name ) return await query_doc_with_hybrid_search( @@ -2412,21 +2411,12 @@ async def query_doc_handler( ), k=form_data.k if form_data.k else request.app.state.config.TOP_K, reranking_function=( - ( - lambda query, documents: request.app.state.RERANKING_FUNCTION( - query, documents, user=user - ) - ) + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user)) if request.app.state.RERANKING_FUNCTION else None ), - k_reranker=form_data.k_reranker - or request.app.state.config.TOP_K_RERANKER, - r=( - form_data.r - if form_data.r - else request.app.state.config.RELEVANCE_THRESHOLD - ), + k_reranker=form_data.k_reranker or request.app.state.config.TOP_K_RERANKER, + r=(form_data.r if form_data.r else request.app.state.config.RELEVANCE_THRESHOLD), hybrid_bm25_weight=( form_data.hybrid_bm25_weight if form_data.hybrid_bm25_weight @@ -2438,7 +2428,10 @@ async def query_doc_handler( query_embedding = await request.app.state.EMBEDDING_FUNCTION( form_data.query, prefix=RAG_EMBEDDING_QUERY_PREFIX, user=user ) - return query_doc( + # query_doc wraps a blocking VECTOR_DB_CLIENT.search call; + # offload so the request's event loop stays responsive. + return await asyncio.to_thread( + query_doc, collection_name=form_data.collection_name, query_embedding=query_embedding, k=form_data.k if form_data.k else request.app.state.config.TOP_K, @@ -2455,24 +2448,24 @@ async def query_doc_handler( class QueryCollectionsForm(BaseModel): collection_names: list[str] query: str - k: Optional[int] = None - k_reranker: Optional[int] = None - r: Optional[float] = None - hybrid: Optional[bool] = None - hybrid_bm25_weight: Optional[float] = None - enable_enriched_texts: Optional[bool] = None + k: int | None = None + k_reranker: int | None = None + r: float | None = None + hybrid: bool | None = None + hybrid_bm25_weight: float | None = None + enable_enriched_texts: bool | None = None -@router.post("/query/collection") +@router.post('/query/collection') async def query_collection_handler( request: Request, form_data: QueryCollectionsForm, user=Depends(get_verified_user), ): + await _validate_collection_access(form_data.collection_names, user) + try: - if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and ( - form_data.hybrid is None or form_data.hybrid - ): + if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and (form_data.hybrid is None or form_data.hybrid): return await query_collection_with_hybrid_search( collection_names=form_data.collection_names, queries=[form_data.query], @@ -2481,21 +2474,12 @@ async def query_collection_handler( ), k=form_data.k if form_data.k else request.app.state.config.TOP_K, reranking_function=( - ( - lambda query, documents: request.app.state.RERANKING_FUNCTION( - query, documents, user=user - ) - ) + (lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user)) if request.app.state.RERANKING_FUNCTION else None ), - k_reranker=form_data.k_reranker - or request.app.state.config.TOP_K_RERANKER, - r=( - form_data.r - if form_data.r - else request.app.state.config.RELEVANCE_THRESHOLD - ), + k_reranker=form_data.k_reranker or request.app.state.config.TOP_K_RERANKER, + r=(form_data.r if form_data.r else request.app.state.config.RELEVANCE_THRESHOLD), hybrid_bm25_weight=( form_data.hybrid_bm25_weight if form_data.hybrid_bm25_weight @@ -2509,6 +2493,7 @@ async def query_collection_handler( ) else: return await query_collection( + request, collection_names=form_data.collection_names, queries=[form_data.query], embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION( @@ -2537,15 +2522,15 @@ class DeleteForm(BaseModel): file_id: str -@router.post("/delete") -def delete_entries_from_collection( +@router.post('/delete') +async def delete_entries_from_collection( form_data: DeleteForm, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): try: - if VECTOR_DB_CLIENT.has_collection(collection_name=form_data.collection_name): - file = Files.get_file_by_id(form_data.file_id, db=db) + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name=form_data.collection_name): + file = await Files.get_file_by_id(form_data.file_id, db=db) if not file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -2553,27 +2538,51 @@ def delete_entries_from_collection( ) hash = file.hash - VECTOR_DB_CLIENT.delete( + # Refuse to issue a `filter={'hash': None}` query — the + # match semantics of a null filter value are + # backend-dependent (some backends ignore the key, some + # match every row whose metadata lacks `hash`) and risk + # deleting unrelated entries. Files without a hash are + # typically unprocessed / failed / legacy records that + # can't be targeted by hash anyway. + if hash is None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('File has no hash; cannot delete vector entries by hash.'), + ) + + # Pre-existing bug: this used `metadata=` which is not a + # parameter on `VectorDBBase.delete` nor on any backend + # implementation, so the call always raised TypeError that + # was silently swallowed by the surrounding `except + # Exception` and the endpoint reported `{'status': False}` + # for every request. Use `filter` to actually do what the + # endpoint name promises. + await ASYNC_VECTOR_DB_CLIENT.delete( collection_name=form_data.collection_name, - metadata={"hash": hash}, + filter={'hash': hash}, ) - return {"status": True} + return {'status': True} else: - return {"status": False} + return {'status': False} + except HTTPException: + # Caller-meaningful errors (404/400 above) must not be + # swallowed and re-shaped as `{'status': False}`. + raise except Exception as e: log.exception(e) - return {"status": False} + return {'status': False} -@router.post("/reset/db") -def reset_vector_db(user=Depends(get_admin_user), db: Session = Depends(get_session)): - VECTOR_DB_CLIENT.reset() - Knowledges.delete_all_knowledge(db=db) +@router.post('/reset/db') +async def reset_vector_db(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + await ASYNC_VECTOR_DB_CLIENT.reset() + await Knowledges.delete_all_knowledge(db=db) -@router.post("/reset/uploads") -def reset_upload_dir(user=Depends(get_admin_user)) -> bool: - folder = f"{UPLOAD_DIR}" +@router.post('/reset/uploads') +async def reset_upload_dir(user=Depends(get_admin_user)) -> bool: + folder = f'{UPLOAD_DIR}' try: # Check if the directory exists if os.path.exists(folder): @@ -2586,73 +2595,98 @@ def reset_upload_dir(user=Depends(get_admin_user)) -> bool: elif os.path.isdir(file_path): shutil.rmtree(file_path) # Remove the directory except Exception as e: - log.exception(f"Failed to delete {file_path}. Reason: {e}") + log.exception(f'Failed to delete {file_path}. Reason: {e}') else: - log.warning(f"The directory {folder} does not exist") + log.warning(f'The directory {folder} does not exist') except Exception as e: - log.exception(f"Failed to process the directory {folder}. Reason: {e}") + log.exception(f'Failed to process the directory {folder}. Reason: {e}') return True -if ENV == "dev": +if ENV == 'dev': - @router.get("/ef/{text}") - async def get_embeddings(request: Request, text: Optional[str] = "Hello World!"): - return { - "result": await request.app.state.EMBEDDING_FUNCTION( - text, prefix=RAG_EMBEDDING_QUERY_PREFIX - ) - } + @router.get('/ef/{text}') + async def get_embeddings(request: Request, text: str | None = 'Hello World!'): + return {'result': await request.app.state.EMBEDDING_FUNCTION(text, prefix=RAG_EMBEDDING_QUERY_PREFIX)} class BatchProcessFilesForm(BaseModel): - files: List[FileModel] + files: list[FileModel] collection_name: str class BatchProcessFilesResult(BaseModel): file_id: str status: str - error: Optional[str] = None + error: str | None = None class BatchProcessFilesResponse(BaseModel): - results: List[BatchProcessFilesResult] - errors: List[BatchProcessFilesResult] + results: list[BatchProcessFilesResult] + errors: list[BatchProcessFilesResult] -@router.post("/process/files/batch") +@router.post('/process/files/batch') async def process_files_batch( request: Request, form_data: BatchProcessFilesForm, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db=None, ) -> BatchProcessFilesResponse: """ Process a batch of files and save them to the vector database. + + NOTE: We intentionally do NOT use Depends(get_async_session) here. + The save_docs_to_vector_db() call makes external embedding API calls which + can take 5-60+ seconds for batch operations. Database operations after + embedding (Files.update_file_by_id) manage their own short-lived sessions. """ collection_name = form_data.collection_name - file_results: List[BatchProcessFilesResult] = [] - file_errors: List[BatchProcessFilesResult] = [] - file_updates: List[FileUpdateForm] = [] + if collection_name: + await _validate_collection_access([collection_name], user, access_type='write') + + file_results: list[BatchProcessFilesResult] = [] + file_errors: list[BatchProcessFilesResult] = [] + file_updates: list[FileUpdateForm] = [] # Prepare all documents first - all_docs: List[Document] = [] + all_docs: list[Document] = [] for file in form_data.files: try: - text_content = file.data.get("content", "") - docs: List[Document] = [ + # Ownership check: verify the requesting user owns the file or is an admin + db_file = await Files.get_file_by_id(file.id, db=db) + if not db_file: + file_errors.append( + BatchProcessFilesResult( + file_id=file.id, + status='failed', + error='File not found', + ) + ) + continue + if db_file.user_id != user.id and user.role != 'admin': + file_errors.append( + BatchProcessFilesResult( + file_id=file.id, + status='failed', + error='Permission denied: not file owner', + ) + ) + continue + + text_content = file.data.get('content', '') + docs: list[Document] = [ Document( - page_content=text_content.replace("
", "\n"), + page_content=text_content.replace('
', '\n'), metadata={ **file.meta, - "name": file.filename, - "created_by": file.user_id, - "file_id": file.id, - "source": file.filename, + 'name': file.filename, + 'created_by': file.user_id, + 'file_id': file.id, + 'source': file.filename, }, ) ] @@ -2662,18 +2696,14 @@ async def process_files_batch( file_updates.append( FileUpdateForm( hash=calculate_sha256_string(text_content), - data={"content": text_content}, + data={'content': text_content}, ) ) - file_results.append( - BatchProcessFilesResult(file_id=file.id, status="prepared") - ) + file_results.append(BatchProcessFilesResult(file_id=file.id, status='prepared')) except Exception as e: - log.error(f"process_files_batch: Error processing file {file.id}: {str(e)}") - file_errors.append( - BatchProcessFilesResult(file_id=file.id, status="failed", error=str(e)) - ) + log.error(f'process_files_batch: Error processing file {file.id}: {str(e)}') + file_errors.append(BatchProcessFilesResult(file_id=file.id, status='failed', error=str(e))) # Save all documents in one batch if all_docs: @@ -2689,19 +2719,13 @@ async def process_files_batch( # Update all files with collection name for file_update, file_result in zip(file_updates, file_results): - Files.update_file_by_id( - id=file_result.file_id, form_data=file_update, db=db - ) - file_result.status = "completed" + await Files.update_file_by_id(id=file_result.file_id, form_data=file_update, db=db) + file_result.status = 'completed' except Exception as e: - log.error( - f"process_files_batch: Error saving documents to vector DB: {str(e)}" - ) + log.error(f'process_files_batch: Error saving documents to vector DB: {str(e)}') for file_result in file_results: - file_result.status = "failed" - file_errors.append( - BatchProcessFilesResult(file_id=file_result.file_id, error=str(e)) - ) + file_result.status = 'failed' + file_errors.append(BatchProcessFilesResult(file_id=file_result.file_id, status='failed', error=str(e))) return BatchProcessFilesResponse(results=file_results, errors=file_errors) diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index 9070256770..9292523adc 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -5,61 +5,61 @@ Provides System for Cross-domain Identity Management endpoints for users and gro NOTE: This is an experimental implementation and may not fully comply with SCIM 2.0 standards, and is subject to change. """ +import hmac import logging -import uuid import time -from typing import Optional, List, Dict, Any +import uuid from datetime import datetime, timezone +from typing import Any, Dict, List, Optional -from fastapi import APIRouter, Depends, HTTPException, Request, Query, Header, status +from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status from fastapi.responses import JSONResponse -from pydantic import BaseModel, Field, ConfigDict - -from open_webui.models.users import Users, UserModel -from open_webui.models.groups import Groups, GroupModel +from open_webui.config import OAUTH_PROVIDERS +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SCIM_AUTH_PROVIDER +from open_webui.internal.db import get_async_session +from open_webui.models.groups import GroupModel, Groups +from open_webui.models.users import UserModel, Users from open_webui.utils.auth import ( + decode_token, get_admin_user, get_current_user, - decode_token, get_verified_user, ) -from open_webui.constants import ERROR_MESSAGES - - -from sqlalchemy.orm import Session -from open_webui.internal.db import get_session +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) router = APIRouter() # SCIM 2.0 Schema URIs -SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User" -SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group" -SCIM_LIST_RESPONSE_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse" -SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error" +SCIM_USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User' +SCIM_GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group' +SCIM_LIST_RESPONSE_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse' +SCIM_ERROR_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error' # SCIM Resource Types -SCIM_RESOURCE_TYPE_USER = "User" -SCIM_RESOURCE_TYPE_GROUP = "Group" +SCIM_RESOURCE_TYPE_USER = 'User' +SCIM_RESOURCE_TYPE_GROUP = 'Group' def scim_error(status_code: int, detail: str, scim_type: Optional[str] = None): """Create a SCIM-compliant error response""" error_body = { - "schemas": [SCIM_ERROR_SCHEMA], - "status": str(status_code), - "detail": detail, + 'schemas': [SCIM_ERROR_SCHEMA], + 'status': str(status_code), + 'detail': detail, } if scim_type: - error_body["scimType"] = scim_type + error_body['scimType'] = scim_type elif status_code == 404: - error_body["scimType"] = "invalidValue" + error_body['scimType'] = 'invalidValue' elif status_code == 409: - error_body["scimType"] = "uniqueness" + error_body['scimType'] = 'uniqueness' elif status_code == 400: - error_body["scimType"] = "invalidSyntax" + error_body['scimType'] = 'invalidSyntax' return JSONResponse(status_code=status_code, content=error_body) @@ -98,7 +98,7 @@ class SCIMEmail(BaseModel): """SCIM Email""" value: str - type: Optional[str] = "work" + type: Optional[str] = 'work' primary: bool = True display: Optional[str] = None @@ -107,7 +107,7 @@ class SCIMPhoto(BaseModel): """SCIM Photo""" value: str - type: Optional[str] = "photo" + type: Optional[str] = 'photo' primary: bool = True display: Optional[str] = None @@ -116,8 +116,8 @@ class SCIMGroupMember(BaseModel): """SCIM Group Member""" value: str # User ID - ref: Optional[str] = Field(None, alias="$ref") - type: Optional[str] = "User" + ref: Optional[str] = Field(None, alias='$ref') + type: Optional[str] = 'User' display: Optional[str] = None @@ -224,13 +224,11 @@ class SCIMPatchOperation(BaseModel): class SCIMPatchRequest(BaseModel): """SCIM Patch Request""" - schemas: List[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] + schemas: List[str] = ['urn:ietf:params:scim:api:messages:2.0:PatchOp'] Operations: List[SCIMPatchOperation] -def get_scim_auth( - request: Request, authorization: Optional[str] = Header(None) -) -> bool: +def get_scim_auth(request: Request, authorization: Optional[str] = Header(None)) -> bool: """ Verify SCIM authentication Checks for SCIM-specific bearer token configured in the system @@ -238,8 +236,8 @@ def get_scim_auth( if not authorization: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Authorization header required", - headers={"WWW-Authenticate": "Bearer"}, + detail='Authorization header required', + headers={'WWW-Authenticate': 'Bearer'}, ) try: @@ -247,42 +245,40 @@ def get_scim_auth( if len(parts) != 2: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authorization format. Expected: Bearer ", + detail='Invalid authorization format. Expected: Bearer ', ) scheme, token = parts - if scheme.lower() != "bearer": + if scheme.lower() != 'bearer': raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid authentication scheme", + detail='Invalid authentication scheme', ) # Check if SCIM is enabled - enable_scim = getattr(request.app.state, "ENABLE_SCIM", False) - log.info( - f"SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}" - ) + enable_scim = getattr(request.app.state, 'ENABLE_SCIM', False) + log.info(f'SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}') - # Handle both PersistentConfig and direct value - if hasattr(enable_scim, "value"): + # Handle both ConfigVar and direct value + if hasattr(enable_scim, 'value'): enable_scim = enable_scim.value if not enable_scim: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail="SCIM is not enabled", + detail='SCIM is not enabled', ) # Verify the SCIM token - scim_token = getattr(request.app.state, "SCIM_TOKEN", None) - # Handle both PersistentConfig and direct value - if hasattr(scim_token, "value"): + scim_token = getattr(request.app.state, 'SCIM_TOKEN', None) + # Handle both ConfigVar and direct value + if hasattr(scim_token, 'value'): scim_token = scim_token.value - log.debug(f"SCIM token configured: {bool(scim_token)}") - if not scim_token or token != scim_token: + log.debug(f'SCIM token configured: {bool(scim_token)}') + if not scim_token or not hmac.compare_digest(token, scim_token): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid SCIM token", + detail='Invalid SCIM token', ) return True @@ -290,37 +286,75 @@ def get_scim_auth( # Re-raise HTTP exceptions as-is raise except Exception as e: - log.error(f"SCIM authentication error: {e}") + log.error(f'SCIM authentication error: {e}') import traceback - log.error(f"Traceback: {traceback.format_exc()}") + log.error(f'Traceback: {traceback.format_exc()}') raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Authentication failed", + detail='Authentication failed', ) -def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser: +def get_external_id(user: UserModel) -> Optional[str]: + """Extract externalId from a user's scim data. + + Checks all stored provider entries and returns the first external_id found. + """ + if not user.scim: + return None + for provider_data in user.scim.values(): + if isinstance(provider_data, dict) and 'external_id' in provider_data: + return provider_data['external_id'] + return None + + +def get_scim_provider() -> str: + """Return the configured SCIM auth provider. + + Requires SCIM_AUTH_PROVIDER env var to be set (e.g. 'microsoft', 'oidc'). + """ + if not SCIM_AUTH_PROVIDER: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled', + ) + return SCIM_AUTH_PROVIDER + + +async def find_user_by_external_id(external_id: str, db=None) -> Optional[UserModel]: + """Find a user by SCIM externalId, falling back to OAuth sub match.""" + provider = get_scim_provider() + user = await Users.get_user_by_scim_external_id(provider, external_id, db=db) + if user: + return user + + # Fallback: check if externalId matches an existing OAuth sub (account linking) + return await Users.get_user_by_oauth_sub(provider, external_id, db=db) + + +async def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser: """Convert internal User model to SCIM User""" # Parse display name into name components - name_parts = user.name.split(" ", 1) if user.name else ["", ""] - given_name = name_parts[0] if name_parts else "" - family_name = name_parts[1] if len(name_parts) > 1 else "" + name_parts = user.name.split(' ', 1) if user.name else ['', ''] + given_name = name_parts[0] if name_parts else '' + family_name = name_parts[1] if len(name_parts) > 1 else '' # Get user's groups - user_groups = Groups.get_groups_by_member_id(user.id, db=db) + user_groups = await Groups.get_groups_by_member_id(user.id, db=db) groups = [ { - "value": group.id, - "display": group.name, - "$ref": f"{request.base_url}api/v1/scim/v2/Groups/{group.id}", - "type": "direct", + 'value': group.id, + 'display': group.name, + '$ref': f'{request.base_url}api/v1/scim/v2/Groups/{group.id}', + 'type': 'direct', } for group in user_groups ] return SCIMUser( id=user.id, + externalId=get_external_id(user), userName=user.email, name=SCIMName( formatted=user.name, @@ -329,41 +363,32 @@ def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser: ), displayName=user.name, emails=[SCIMEmail(value=user.email)], - active=user.role != "pending", - photos=( - [SCIMPhoto(value=user.profile_image_url)] - if user.profile_image_url - else None - ), + active=user.role != 'pending', + photos=([SCIMPhoto(value=user.profile_image_url)] if user.profile_image_url else None), groups=groups if groups else None, meta=SCIMMeta( resourceType=SCIM_RESOURCE_TYPE_USER, - created=datetime.fromtimestamp( - user.created_at, tz=timezone.utc - ).isoformat(), - lastModified=datetime.fromtimestamp( - user.updated_at, tz=timezone.utc - ).isoformat(), - location=f"{request.base_url}api/v1/scim/v2/Users/{user.id}", + created=datetime.fromtimestamp(user.created_at, tz=timezone.utc).isoformat(), + lastModified=datetime.fromtimestamp(user.updated_at, tz=timezone.utc).isoformat(), + location=f'{request.base_url}api/v1/scim/v2/Users/{user.id}', ), ) -def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup: +async def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup: """Convert internal Group model to SCIM Group""" - member_ids = Groups.get_group_user_ids_by_id(group.id, db) or [] - members = [] + member_ids = await Groups.get_group_user_ids_by_id(group.id, db) or [] - for user_id in member_ids: - user = Users.get_user_by_id(user_id, db=db) - if user: - members.append( - SCIMGroupMember( - value=user.id, - ref=f"{request.base_url}api/v1/scim/v2/Users/{user.id}", - display=user.name, - ) - ) + # Batch-fetch all users to avoid N+1 queries + users = await Users.get_users_by_user_ids(member_ids, db=db) if member_ids else [] + members = [ + SCIMGroupMember( + value=user.id, + ref=f'{request.base_url}api/v1/scim/v2/Users/{user.id}', + display=user.name, + ) + for user in users + ] return SCIMGroup( id=group.id, @@ -371,108 +396,104 @@ def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup: members=members, meta=SCIMMeta( resourceType=SCIM_RESOURCE_TYPE_GROUP, - created=datetime.fromtimestamp( - group.created_at, tz=timezone.utc - ).isoformat(), - lastModified=datetime.fromtimestamp( - group.updated_at, tz=timezone.utc - ).isoformat(), - location=f"{request.base_url}api/v1/scim/v2/Groups/{group.id}", + created=datetime.fromtimestamp(group.created_at, tz=timezone.utc).isoformat(), + lastModified=datetime.fromtimestamp(group.updated_at, tz=timezone.utc).isoformat(), + location=f'{request.base_url}api/v1/scim/v2/Groups/{group.id}', ), ) # SCIM Service Provider Config -@router.get("/ServiceProviderConfig") +@router.get('/ServiceProviderConfig') async def get_service_provider_config(): """Get SCIM Service Provider Configuration""" return { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], - "patch": {"supported": True}, - "bulk": {"supported": False, "maxOperations": 1000, "maxPayloadSize": 1048576}, - "filter": {"supported": True, "maxResults": 200}, - "changePassword": {"supported": False}, - "sort": {"supported": False}, - "etag": {"supported": False}, - "authenticationSchemes": [ + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'], + 'patch': {'supported': True}, + 'bulk': {'supported': False, 'maxOperations': 1000, 'maxPayloadSize': 1048576}, + 'filter': {'supported': True, 'maxResults': 200}, + 'changePassword': {'supported': False}, + 'sort': {'supported': False}, + 'etag': {'supported': False}, + 'authenticationSchemes': [ { - "type": "oauthbearertoken", - "name": "OAuth Bearer Token", - "description": "Authentication using OAuth 2.0 Bearer Token", + 'type': 'oauthbearertoken', + 'name': 'OAuth Bearer Token', + 'description': 'Authentication using OAuth 2.0 Bearer Token', } ], } # SCIM Resource Types -@router.get("/ResourceTypes") +@router.get('/ResourceTypes') async def get_resource_types(request: Request): """Get SCIM Resource Types""" return [ { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"], - "id": "User", - "name": "User", - "endpoint": "/Users", - "schema": SCIM_USER_SCHEMA, - "meta": { - "location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/User", - "resourceType": "ResourceType", + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'], + 'id': 'User', + 'name': 'User', + 'endpoint': '/Users', + 'schema': SCIM_USER_SCHEMA, + 'meta': { + 'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/User', + 'resourceType': 'ResourceType', }, }, { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"], - "id": "Group", - "name": "Group", - "endpoint": "/Groups", - "schema": SCIM_GROUP_SCHEMA, - "meta": { - "location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/Group", - "resourceType": "ResourceType", + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'], + 'id': 'Group', + 'name': 'Group', + 'endpoint': '/Groups', + 'schema': SCIM_GROUP_SCHEMA, + 'meta': { + 'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/Group', + 'resourceType': 'ResourceType', }, }, ] # SCIM Schemas -@router.get("/Schemas") +@router.get('/Schemas') async def get_schemas(): """Get SCIM Schemas""" return [ { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], - "id": SCIM_USER_SCHEMA, - "name": "User", - "description": "User Account", - "attributes": [ + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'], + 'id': SCIM_USER_SCHEMA, + 'name': 'User', + 'description': 'User Account', + 'attributes': [ { - "name": "userName", - "type": "string", - "required": True, - "uniqueness": "server", + 'name': 'userName', + 'type': 'string', + 'required': True, + 'uniqueness': 'server', }, - {"name": "displayName", "type": "string", "required": True}, + {'name': 'displayName', 'type': 'string', 'required': True}, { - "name": "emails", - "type": "complex", - "multiValued": True, - "required": True, + 'name': 'emails', + 'type': 'complex', + 'multiValued': True, + 'required': True, }, - {"name": "active", "type": "boolean", "required": False}, + {'name': 'active', 'type': 'boolean', 'required': False}, ], }, { - "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"], - "id": SCIM_GROUP_SCHEMA, - "name": "Group", - "description": "Group", - "attributes": [ - {"name": "displayName", "type": "string", "required": True}, + 'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'], + 'id': SCIM_GROUP_SCHEMA, + 'name': 'Group', + 'description': 'Group', + 'attributes': [ + {'name': 'displayName', 'type': 'string', 'required': True}, { - "name": "members", - "type": "complex", - "multiValued": True, - "required": False, + 'name': 'members', + 'type': 'complex', + 'multiValued': True, + 'required': False, }, ], }, @@ -480,39 +501,47 @@ async def get_schemas(): # Users endpoints -@router.get("/Users", response_model=SCIMListResponse) +@router.get('/Users', response_model=SCIMListResponse) async def get_users( request: Request, - startIndex: int = Query(1, ge=1), - count: int = Query(20, ge=1, le=100), + startIndex: int = Query(1), + count: int = Query(20), filter: Optional[str] = None, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """List SCIM Users""" + # Clamp per SCIM 2.0 spec (RFC 7644 §3.4.2.4): + # startIndex < 1 SHALL be treated as 1; count < 0 SHALL be treated as 0. + startIndex = max(1, startIndex) + count = max(0, min(100, count)) skip = startIndex - 1 limit = count # Get users from database if filter: - # Simple filter parsing - supports userName eq "email" - # In production, you'd want a more robust filter parser - if "userName eq" in filter: + # Simple filter parsing - supports userName eq, externalId eq + if 'userName eq' in filter: email = filter.split('"')[1] - user = Users.get_user_by_email(email, db=db) + user = await Users.get_user_by_email(email, db=db) + users_list = [user] if user else [] + total = 1 if user else 0 + elif 'externalId eq' in filter: + external_id = filter.split('"')[1] + user = await find_user_by_external_id(external_id, db=db) users_list = [user] if user else [] total = 1 if user else 0 else: - response = Users.get_users(skip=skip, limit=limit, db=db) - users_list = response["users"] - total = response["total"] + response = await Users.get_users(skip=skip, limit=limit, db=db) + users_list = response['users'] + total = response['total'] else: - response = Users.get_users(skip=skip, limit=limit, db=db) - users_list = response["users"] - total = response["total"] + response = await Users.get_users(skip=skip, limit=limit, db=db) + users_list = response['users'] + total = response['total'] # Convert to SCIM format - scim_users = [user_to_scim(user, request, db=db) for user in users_list] + scim_users = [await user_to_scim(user, request, db=db) for user in users_list] return SCIMListResponse( totalResults=total, @@ -522,42 +551,56 @@ async def get_users( ) -@router.get("/Users/{user_id}", response_model=SCIMUser) +@router.get('/Users/{user_id}', response_model=SCIMUser) async def get_user( user_id: str, request: Request, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Get SCIM User by ID""" - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) if not user: - return scim_error( - status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found" - ) + return scim_error(status_code=status.HTTP_404_NOT_FOUND, detail=f'User {user_id} not found') - return user_to_scim(user, request, db=db) + return await user_to_scim(user, request, db=db) -@router.post("/Users", response_model=SCIMUser, status_code=status.HTTP_201_CREATED) +@router.post('/Users', response_model=SCIMUser, status_code=status.HTTP_201_CREATED) async def create_user( request: Request, user_data: SCIMUserCreateRequest, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Create SCIM User""" - # Check if user already exists - existing_user = Users.get_user_by_email(user_data.userName, db=db) + # Check for duplicate by externalId + if user_data.externalId: + existing_user = await find_user_by_external_id(user_data.externalId, db=db) + if existing_user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f'User with externalId {user_data.externalId} already exists', + ) + + # Determine primary email (lowercased per RFC 5321) + email = user_data.userName + for entry in user_data.emails: + if entry.primary: + email = entry.value + break + email = email.lower() + + # Check for duplicate by email + existing_user = await Users.get_user_by_email(email, db=db) if existing_user: raise HTTPException( status_code=status.HTTP_409_CONFLICT, - detail=f"User with email {user_data.userName} already exists", + detail=f'User with email {email} already exists', ) # Create user user_id = str(uuid.uuid4()) - email = user_data.emails[0].value if user_data.emails else user_data.userName # Parse name if provided name = user_data.displayName @@ -565,98 +608,106 @@ async def create_user( if user_data.name.formatted: name = user_data.name.formatted elif user_data.name.givenName or user_data.name.familyName: - name = f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip() + name = f'{user_data.name.givenName or ""} {user_data.name.familyName or ""}'.strip() # Get profile image if provided - profile_image = "/user.png" + profile_image = '/user.png' if user_data.photos and len(user_data.photos) > 0: profile_image = user_data.photos[0].value - # Create user - new_user = Users.insert_new_user( + new_user = await Users.insert_new_user( id=user_id, name=name, email=email, profile_image_url=profile_image, - role="user" if user_data.active else "pending", + role='user' if user_data.active else 'pending', db=db, ) if not new_user: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to create user", + detail='Failed to create user', ) - return user_to_scim(new_user, request, db=db) + # Store externalId in the scim field + if user_data.externalId: + provider = get_scim_provider() + await Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db) + new_user = await Users.get_user_by_id(user_id, db=db) + + return await user_to_scim(new_user, request, db=db) -@router.put("/Users/{user_id}", response_model=SCIMUser) +@router.put('/Users/{user_id}', response_model=SCIMUser) async def update_user( user_id: str, request: Request, user_data: SCIMUserUpdateRequest, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Update SCIM User (full update)""" - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"User {user_id} not found", + detail=f'User {user_id} not found', ) # Build update dict update_data = {} if user_data.userName: - update_data["email"] = user_data.userName + update_data['email'] = user_data.userName if user_data.displayName: - update_data["name"] = user_data.displayName + update_data['name'] = user_data.displayName elif user_data.name: if user_data.name.formatted: - update_data["name"] = user_data.name.formatted + update_data['name'] = user_data.name.formatted elif user_data.name.givenName or user_data.name.familyName: - update_data["name"] = ( - f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip() - ) + update_data['name'] = f'{user_data.name.givenName or ""} {user_data.name.familyName or ""}'.strip() if user_data.emails and len(user_data.emails) > 0: - update_data["email"] = user_data.emails[0].value + update_data['email'] = user_data.emails[0].value if user_data.active is not None: - update_data["role"] = "user" if user_data.active else "pending" + update_data['role'] = 'user' if user_data.active else 'pending' if user_data.photos and len(user_data.photos) > 0: - update_data["profile_image_url"] = user_data.photos[0].value + update_data['profile_image_url'] = user_data.photos[0].value - # Update user - updated_user = Users.update_user_by_id(user_id, update_data, db=db) + updated_user = await Users.update_user_by_id(user_id, update_data, db=db) if not updated_user: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update user", + detail='Failed to update user', ) - return user_to_scim(updated_user, request, db=db) + # Update externalId in the scim field + if user_data.externalId: + provider = get_scim_provider() + await Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db) + updated_user = await Users.get_user_by_id(user_id, db=db) + + return await user_to_scim(updated_user, request, db=db) -@router.patch("/Users/{user_id}", response_model=SCIMUser) +@router.patch('/Users/{user_id}', response_model=SCIMUser) async def patch_user( user_id: str, request: Request, patch_data: SCIMPatchRequest, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Update SCIM User (partial update)""" - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"User {user_id} not found", + detail=f'User {user_id} not found', ) update_data = {} @@ -666,70 +717,87 @@ async def patch_user( path = operation.path value = operation.value - if op == "replace": - if path == "active": - update_data["role"] = "user" if value else "pending" - elif path == "userName": - update_data["email"] = value - elif path == "displayName": - update_data["name"] = value - elif path == "emails[primary eq true].value": - update_data["email"] = value - elif path == "name.formatted": - update_data["name"] = value + if op == 'replace': + if path == 'active': + update_data['role'] = 'user' if value else 'pending' + elif path == 'userName': + update_data['email'] = value + elif path == 'displayName': + update_data['name'] = value + elif path == 'emails[primary eq true].value': + update_data['email'] = value + elif path == 'name.formatted': + update_data['name'] = value + elif path == 'externalId': + provider = get_scim_provider() + await Users.update_user_scim_by_id(user_id, provider, value, db=db) # Update user if update_data: - updated_user = Users.update_user_by_id(user_id, update_data, db=db) + updated_user = await Users.update_user_by_id(user_id, update_data, db=db) if not updated_user: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update user", + detail='Failed to update user', ) else: updated_user = user - return user_to_scim(updated_user, request, db=db) + return await user_to_scim(updated_user, request, db=db) -@router.delete("/Users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete('/Users/{user_id}', status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: str, request: Request, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Delete SCIM User""" - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"User {user_id} not found", + detail=f'User {user_id} not found', ) - success = Users.delete_user_by_id(user_id, db=db) + success = await Users.delete_user_by_id(user_id, db=db) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to delete user", + detail='Failed to delete user', ) return None # Groups endpoints -@router.get("/Groups", response_model=SCIMListResponse) +@router.get('/Groups', response_model=SCIMListResponse) async def get_groups( request: Request, - startIndex: int = Query(1, ge=1), - count: int = Query(20, ge=1, le=100), + startIndex: int = Query(1), + count: int = Query(20), filter: Optional[str] = None, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """List SCIM Groups""" - # Get all groups - groups_list = Groups.get_all_groups(db=db) + # Clamp per SCIM 2.0 spec (RFC 7644 §3.4.2.4): + # startIndex < 1 SHALL be treated as 1; count < 0 SHALL be treated as 0. + startIndex = max(1, startIndex) + count = max(0, min(100, count)) + + # Get groups, applying filter if provided + if filter: + if 'displayName eq' in filter: + display_name = filter.split('"')[1] + group = await Groups.get_group_by_name(display_name, db=db) + groups_list = [group] if group else [] + else: + # Unrecognized filter — fall back to all groups + groups_list = await Groups.get_all_groups(db=db) + else: + groups_list = await Groups.get_all_groups(db=db) # Apply pagination total = len(groups_list) @@ -738,7 +806,7 @@ async def get_groups( paginated_groups = groups_list[start:end] # Convert to SCIM format - scim_groups = [group_to_scim(group, request, db=db) for group in paginated_groups] + scim_groups = [await group_to_scim(group, request, db=db) for group in paginated_groups] return SCIMListResponse( totalResults=total, @@ -748,30 +816,30 @@ async def get_groups( ) -@router.get("/Groups/{group_id}", response_model=SCIMGroup) +@router.get('/Groups/{group_id}', response_model=SCIMGroup) async def get_group( group_id: str, request: Request, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Get SCIM Group by ID""" - group = Groups.get_group_by_id(group_id, db=db) + group = await Groups.get_group_by_id(group_id, db=db) if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Group {group_id} not found", + detail=f'Group {group_id} not found', ) - return group_to_scim(group, request, db=db) + return await group_to_scim(group, request, db=db) -@router.post("/Groups", response_model=SCIMGroup, status_code=status.HTTP_201_CREATED) +@router.post('/Groups', response_model=SCIMGroup, status_code=status.HTTP_201_CREATED) async def create_group( request: Request, group_data: SCIMGroupCreateRequest, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Create SCIM Group""" # Extract member IDs @@ -785,22 +853,22 @@ async def create_group( form = GroupForm( name=group_data.displayName, - description="", + description='', ) # Need to get the creating user's ID - we'll use the first admin - admin_user = Users.get_super_admin_user(db=db) + admin_user = await Users.get_super_admin_user(db=db) if not admin_user: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="No admin user found", + detail='No admin user found', ) - new_group = Groups.insert_new_group(admin_user.id, form, db=db) + new_group = await Groups.insert_new_group(admin_user.id, form, db=db) if not new_group: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to create group", + detail='Failed to create group', ) # Add members if provided @@ -812,28 +880,28 @@ async def create_group( description=new_group.description, ) - Groups.update_group_by_id(new_group.id, update_form, db=db) - Groups.set_group_user_ids_by_id(new_group.id, member_ids, db=db) + await Groups.update_group_by_id(new_group.id, update_form, db=db) + await Groups.set_group_user_ids_by_id(new_group.id, member_ids, db=db) - new_group = Groups.get_group_by_id(new_group.id, db=db) + new_group = await Groups.get_group_by_id(new_group.id, db=db) - return group_to_scim(new_group, request, db=db) + return await group_to_scim(new_group, request, db=db) -@router.put("/Groups/{group_id}", response_model=SCIMGroup) +@router.put('/Groups/{group_id}', response_model=SCIMGroup) async def update_group( group_id: str, request: Request, group_data: SCIMGroupUpdateRequest, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Update SCIM Group (full update)""" - group = Groups.get_group_by_id(group_id, db=db) + group = await Groups.get_group_by_id(group_id, db=db) if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Group {group_id} not found", + detail=f'Group {group_id} not found', ) # Build update form @@ -847,33 +915,33 @@ async def update_group( # Handle members if provided if group_data.members is not None: member_ids = [member.value for member in group_data.members] - Groups.set_group_user_ids_by_id(group_id, member_ids, db=db) + await Groups.set_group_user_ids_by_id(group_id, member_ids, db=db) # Update group - updated_group = Groups.update_group_by_id(group_id, update_form, db=db) + updated_group = await Groups.update_group_by_id(group_id, update_form, db=db) if not updated_group: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update group", + detail='Failed to update group', ) - return group_to_scim(updated_group, request, db=db) + return await group_to_scim(updated_group, request, db=db) -@router.patch("/Groups/{group_id}", response_model=SCIMGroup) +@router.patch('/Groups/{group_id}', response_model=SCIMGroup) async def patch_group( group_id: str, request: Request, patch_data: SCIMPatchRequest, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Update SCIM Group (partial update)""" - group = Groups.get_group_by_id(group_id, db=db) + group = await Groups.get_group_by_id(group_id, db=db) if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Group {group_id} not found", + detail=f'Group {group_id} not found', ) from open_webui.models.groups import GroupUpdateForm @@ -888,61 +956,57 @@ async def patch_group( path = operation.path value = operation.value - if op == "replace": - if path == "displayName": + if op == 'replace': + if path == 'displayName': update_form.name = value - elif path == "members": + elif path == 'members': # Replace all members - Groups.set_group_user_ids_by_id( - group_id, [member["value"] for member in value], db=db - ) + await Groups.set_group_user_ids_by_id(group_id, [member['value'] for member in value], db=db) - elif op == "add": - if path == "members": + elif op == 'add': + if path == 'members': # Add members if isinstance(value, list): for member in value: - if isinstance(member, dict) and "value" in member: - Groups.add_users_to_group( - group_id, [member["value"]], db=db - ) - elif op == "remove": - if path and path.startswith("members[value eq"): + if isinstance(member, dict) and 'value' in member: + await Groups.add_users_to_group(group_id, [member['value']], db=db) + elif op == 'remove': + if path and path.startswith('members[value eq'): # Remove specific member member_id = path.split('"')[1] - Groups.remove_users_from_group(group_id, [member_id], db=db) + await Groups.remove_users_from_group(group_id, [member_id], db=db) # Update group - updated_group = Groups.update_group_by_id(group_id, update_form, db=db) + updated_group = await Groups.update_group_by_id(group_id, update_form, db=db) if not updated_group: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to update group", + detail='Failed to update group', ) - return group_to_scim(updated_group, request, db=db) + return await group_to_scim(updated_group, request, db=db) -@router.delete("/Groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete('/Groups/{group_id}', status_code=status.HTTP_204_NO_CONTENT) async def delete_group( group_id: str, request: Request, _: bool = Depends(get_scim_auth), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): """Delete SCIM Group""" - group = Groups.get_group_by_id(group_id, db=db) + group = await Groups.get_group_by_id(group_id, db=db) if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Group {group_id} not found", + detail=f'Group {group_id} not found', ) - success = Groups.delete_group_by_id(group_id, db=db) + success = await Groups.delete_group_by_id(group_id, db=db) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Failed to delete group", + detail='Failed to delete group', ) return None diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py new file mode 100644 index 0000000000..55aa351ff0 --- /dev/null +++ b/backend/open_webui/routers/skills.py @@ -0,0 +1,453 @@ +import logging +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.skills import ( + SkillAccessListResponse, + SkillAccessResponse, + SkillForm, + SkillModel, + SkillResponse, + Skills, + SkillUserResponse, +) +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession + +log = logging.getLogger(__name__) + +PAGE_ITEM_COUNT = 30 + +router = APIRouter() + + +############################ +# GetSkills +############################ + + +@router.get('/', response_model=list[SkillUserResponse]) +async def get_skills( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + skills = await Skills.get_skills(db=db) + else: + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + all_skills = await Skills.get_skills(db=db) + skills = [ + skill + for skill in all_skills + if skill.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + ] + + return skills + + +############################ +# GetSkillList +############################ + + +@router.get('/list', response_model=SkillAccessListResponse) +async def get_skill_list( + query: Optional[str] = None, + view_option: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if query: + filter['query'] = query + if view_option: + filter['view_option'] = view_option + + if not (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL): + groups = await Groups.get_groups_by_member_id(user.id, db=db) + if groups: + filter['group_ids'] = [group.id for group in groups] + + filter['user_id'] = user.id + + result = await Skills.search_skills(user.id, filter=filter, skip=skip, limit=limit, db=db) + + return SkillAccessListResponse( + items=[ + SkillAccessResponse( + **skill.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == skill.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + ), + ) + for skill in result.items + ], + total=result.total, + ) + + +############################ +# ExportSkills +############################ + + +@router.get('/export', response_model=list[SkillModel]) +async def export_skills( + request: Request, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, + 'workspace.skills', + request.app.state.config.USER_PERMISSIONS, + db=db, + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Skills.get_skills(db=db) + else: + return await Skills.get_skills_by_user_id(user.id, 'read', db=db) + + +############################ +# CreateNewSkill +############################ + + +@router.post('/create', response_model=Optional[SkillResponse]) +async def create_new_skill( + request: Request, + form_data: SkillForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + if user.role != 'admin' and not await has_permission( + user.id, 'workspace.skills', request.app.state.config.USER_PERMISSIONS, db=db + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.id = form_data.id.lower().replace(' ', '-') + + existing = await Skills.get_skill_by_id(form_data.id, db=db) + if existing is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/calendar pattern). Without this, a user with + # workspace.skills permission could attach principal_id='*' read/write + # grants in the create payload, bypassing the sharing.public_skills gate + # that the dedicated /access/update endpoint already enforces. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_skills', + ) + + try: + skill = await Skills.insert_new_skill(user.id, form_data, db=db) + if skill: + return skill + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error creating skill'), + ) + except Exception as e: + log.exception(f'Failed to create skill: {e}') + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# GetSkillById +############################ + + +@router.get('/id/{id}', response_model=Optional[SkillAccessResponse]) +async def get_skill_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + skill = await Skills.get_skill_by_id(id, db=db) + + if skill: + if ( + user.role == 'admin' + or skill.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='read', + db=db, + ) + ): + return SkillAccessResponse( + **skill.model_dump(), + write_access=( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == skill.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + ), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateSkillById +############################ + + +@router.post('/id/{id}/update', response_model=Optional[SkillModel]) +async def update_skill_by_id( + request: Request, + id: str, + form_data: SkillForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + skill = await Skills.get_skill_by_id(id, db=db) + if not skill: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + skill.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + # Strip public/user grants the requesting user is not permitted to assign + # (matches the channel/notes/calendar pattern). The access check above only + # restricts WHO can write to the skill; this filter restricts WHICH grants + # they may set, so a non-admin owner cannot make their own skill publicly + # readable/writable without sharing.public_skills permission. + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_skills', + ) + + try: + updated = { + **form_data.model_dump(exclude={'id'}), + } + + skill = await Skills.update_skill_by_id(id, updated, db=db) + + if skill: + return skill + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error updating skill'), + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +############################ +# UpdateSkillAccessById +############################ + + +class SkillAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/id/{id}/access/update', response_model=Optional[SkillModel]) +async def update_skill_access_by_id( + request: Request, + id: str, + form_data: SkillAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + skill = await Skills.get_skill_by_id(id, db=db) + if not skill: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + skill.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_skills', + ) + + await AccessGrants.set_access_grants('skill', id, form_data.access_grants, db=db) + + return await Skills.get_skill_by_id(id, db=db) + + +############################ +# ToggleSkillById +############################ + + +@router.post('/id/{id}/toggle', response_model=Optional[SkillModel]) +async def toggle_skill_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + skill = await Skills.get_skill_by_id(id, db=db) + if skill: + if ( + user.role == 'admin' + or skill.user_id == user.id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + ): + skill = await Skills.toggle_skill_by_id(id, db=db) + + if skill: + return skill + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error toggling skill'), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# DeleteSkillById +############################ + + +@router.delete('/id/{id}/delete', response_model=bool) +async def delete_skill_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + skill = await Skills.get_skill_by_id(id, db=db) + if not skill: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + skill.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='skill', + resource_id=skill.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + result = await Skills.delete_skill_by_id(id, db=db) + return result diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 37a80e2ce0..0e88f8594a 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -1,41 +1,36 @@ -from fastapi import APIRouter, Depends, HTTPException, Response, status, Request -from fastapi.responses import JSONResponse, RedirectResponse - -from pydantic import BaseModel -from typing import Optional import logging import re +from typing import Optional -from open_webui.utils.chat import generate_chat_completion -from open_webui.utils.task import ( - title_generation_template, - follow_up_generation_template, - query_generation_template, - image_prompt_generation_template, - autocomplete_generation_template, - tags_generation_template, - emoji_generation_template, - moa_response_generation_template, -) -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.constants import TASKS - -from open_webui.routers.pipelines import process_pipeline_inlet_filter - -from open_webui.utils.task import get_task_model_id - +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi.responses import JSONResponse, RedirectResponse from open_webui.config import ( - DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE, - DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, - DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE, - DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, - DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE, DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE, + DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE, + DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE, + DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE, + DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE, DEFAULT_VOICE_MODE_PROMPT_TEMPLATE, ) - +from open_webui.constants import ERROR_MESSAGES, TASKS +from open_webui.routers.pipelines import process_pipeline_inlet_filter +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.chat import generate_chat_completion +from open_webui.utils.task import ( + autocomplete_generation_template, + emoji_generation_template, + follow_up_generation_template, + get_task_model_id, + image_prompt_generation_template, + moa_response_generation_template, + query_generation_template, + tags_generation_template, + title_generation_template, +) +from pydantic import BaseModel log = logging.getLogger(__name__) @@ -49,25 +44,39 @@ router = APIRouter() ################################## -@router.get("/config") +class ActiveChatsForm(BaseModel): + chat_ids: list[str] + + +@router.post('/active/chats') +async def check_active_chats(request: Request, form_data: ActiveChatsForm, user=Depends(get_verified_user)): + """Check which chat IDs have active tasks.""" + from open_webui.tasks import get_active_chat_ids + + active = await get_active_chat_ids(request.app.state.redis, form_data.chat_ids) + return {'active_chat_ids': active} + + +@router.get('/config') async def get_task_config(request: Request, user=Depends(get_verified_user)): return { - "TASK_MODEL": request.app.state.config.TASK_MODEL, - "TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL, - "TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, - "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, - "ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, - "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, - "TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, - "FOLLOW_UP_GENERATION_PROMPT_TEMPLATE": request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, - "ENABLE_FOLLOW_UP_GENERATION": request.app.state.config.ENABLE_FOLLOW_UP_GENERATION, - "ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION, - "ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION, - "ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, - "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, - "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, - "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, - "VOICE_MODE_PROMPT_TEMPLATE": request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, + 'TASK_MODEL': request.app.state.config.TASK_MODEL, + 'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL, + 'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + 'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION, + 'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION, + 'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION, + 'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, + 'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, + 'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + 'ENABLE_VOICE_MODE_PROMPT': request.app.state.config.ENABLE_VOICE_MODE_PROMPT, + 'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, } @@ -87,103 +96,85 @@ class TaskConfigForm(BaseModel): ENABLE_RETRIEVAL_QUERY_GENERATION: bool QUERY_GENERATION_PROMPT_TEMPLATE: str TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str + ENABLE_VOICE_MODE_PROMPT: bool VOICE_MODE_PROMPT_TEMPLATE: Optional[str] -@router.post("/config/update") -async def update_task_config( - request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user) -): +@router.post('/config/update') +async def update_task_config(request: Request, form_data: TaskConfigForm, user=Depends(get_admin_user)): request.app.state.config.TASK_MODEL = form_data.TASK_MODEL request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION - request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = ( - form_data.TITLE_GENERATION_PROMPT_TEMPLATE - ) + request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = form_data.TITLE_GENERATION_PROMPT_TEMPLATE - request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = ( - form_data.ENABLE_FOLLOW_UP_GENERATION - ) - request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = ( - form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE - ) + request.app.state.config.ENABLE_FOLLOW_UP_GENERATION = form_data.ENABLE_FOLLOW_UP_GENERATION + request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = form_data.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE - request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = ( - form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE - ) + request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = form_data.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE - request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ( - form_data.ENABLE_AUTOCOMPLETE_GENERATION - ) + request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = form_data.ENABLE_AUTOCOMPLETE_GENERATION request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = ( form_data.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH ) - request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = ( - form_data.TAGS_GENERATION_PROMPT_TEMPLATE - ) + request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = form_data.TAGS_GENERATION_PROMPT_TEMPLATE request.app.state.config.ENABLE_TAGS_GENERATION = form_data.ENABLE_TAGS_GENERATION - request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ( - form_data.ENABLE_SEARCH_QUERY_GENERATION - ) - request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ( - form_data.ENABLE_RETRIEVAL_QUERY_GENERATION - ) + request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION = form_data.ENABLE_SEARCH_QUERY_GENERATION + request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = form_data.ENABLE_RETRIEVAL_QUERY_GENERATION - request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = ( - form_data.QUERY_GENERATION_PROMPT_TEMPLATE - ) - request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ( - form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE - ) + request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = form_data.QUERY_GENERATION_PROMPT_TEMPLATE + request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE - request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = ( - form_data.VOICE_MODE_PROMPT_TEMPLATE - ) + request.app.state.config.ENABLE_VOICE_MODE_PROMPT = form_data.ENABLE_VOICE_MODE_PROMPT + request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE = form_data.VOICE_MODE_PROMPT_TEMPLATE return { - "TASK_MODEL": request.app.state.config.TASK_MODEL, - "TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL, - "ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION, - "TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, - "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, - "ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, - "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, - "TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, - "ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION, - "ENABLE_FOLLOW_UP_GENERATION": request.app.state.config.ENABLE_FOLLOW_UP_GENERATION, - "FOLLOW_UP_GENERATION_PROMPT_TEMPLATE": request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, - "ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, - "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, - "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, - "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, - "VOICE_MODE_PROMPT_TEMPLATE": request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, + 'TASK_MODEL': request.app.state.config.TASK_MODEL, + 'TASK_MODEL_EXTERNAL': request.app.state.config.TASK_MODEL_EXTERNAL, + 'ENABLE_TITLE_GENERATION': request.app.state.config.ENABLE_TITLE_GENERATION, + 'TITLE_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE': request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_AUTOCOMPLETE_GENERATION': request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH': request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + 'TAGS_GENERATION_PROMPT_TEMPLATE': request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_TAGS_GENERATION': request.app.state.config.ENABLE_TAGS_GENERATION, + 'ENABLE_FOLLOW_UP_GENERATION': request.app.state.config.ENABLE_FOLLOW_UP_GENERATION, + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE': request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + 'ENABLE_SEARCH_QUERY_GENERATION': request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION, + 'ENABLE_RETRIEVAL_QUERY_GENERATION': request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION, + 'QUERY_GENERATION_PROMPT_TEMPLATE': request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE, + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE': request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + 'ENABLE_VOICE_MODE_PROMPT': request.app.state.config.ENABLE_VOICE_MODE_PROMPT, + 'VOICE_MODE_PROMPT_TEMPLATE': request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE, } -@router.post("/title/completions") -async def generate_title( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - +@router.post('/title/completions') +async def generate_title(request: Request, form_data: dict, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_TITLE_GENERATION: return JSONResponse( status_code=status.HTTP_200_OK, - content={"detail": "Title generation is disabled"}, + content={'detail': 'Title generation is disabled'}, ) - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] + if not model_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='No model specified for title generation. Please ensure a model is selected for this chat.', + ) if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -195,37 +186,33 @@ async def generate_title( models, ) - log.debug( - f"generating chat title using model {task_model_id} for user {user.email} " - ) + log.debug(f'generating chat title using model {task_model_id} for user {user.email} ') - if request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "": + if request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != '': template = request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE else: template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE - content = title_generation_template(template, form_data["messages"], user) + content = await title_generation_template(template, form_data['messages'], user) - max_tokens = ( - models[task_model_id].get("info", {}).get("params", {}).get("max_tokens", 1000) - ) + max_tokens = models[task_model_id].get('info', {}).get('params', {}).get('max_tokens', 1000) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, **( - {"max_tokens": max_tokens} - if models[task_model_id].get("owned_by") == "ollama" + {'max_tokens': max_tokens} + if models[task_model_id].get('owned_by') == 'ollama' else { - "max_completion_tokens": max_tokens, + 'max_completion_tokens': max_tokens, } ), - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.TITLE_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.TITLE_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -238,36 +225,34 @@ async def generate_title( try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: - log.error("Exception occurred", exc_info=True) + log.error('Exception occurred', exc_info=True) return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": "An internal error has occurred."}, + content={'detail': 'An internal error has occurred.'}, ) -@router.post("/follow_up/completions") -async def generate_follow_ups( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - +@router.post('/follow_up/completions') +async def generate_follow_ups(request: Request, form_data: dict, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_FOLLOW_UP_GENERATION: return JSONResponse( status_code=status.HTTP_200_OK, - content={"detail": "Follow-up generation is disabled"}, + content={'detail': 'Follow-up generation is disabled'}, ) - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -279,26 +264,24 @@ async def generate_follow_ups( models, ) - log.debug( - f"generating chat title using model {task_model_id} for user {user.email} " - ) + log.debug(f'generating chat title using model {task_model_id} for user {user.email} ') - if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != "": + if request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE != '': template = request.app.state.config.FOLLOW_UP_GENERATION_PROMPT_TEMPLATE else: template = DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE - content = follow_up_generation_template(template, form_data["messages"], user) + content = await follow_up_generation_template(template, form_data['messages'], user) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.FOLLOW_UP_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.FOLLOW_UP_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -311,36 +294,34 @@ async def generate_follow_ups( try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: - log.error("Exception occurred", exc_info=True) + log.error('Exception occurred', exc_info=True) return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": "An internal error has occurred."}, + content={'detail': 'An internal error has occurred.'}, ) -@router.post("/tags/completions") -async def generate_chat_tags( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - +@router.post('/tags/completions') +async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_TAGS_GENERATION: return JSONResponse( status_code=status.HTTP_200_OK, - content={"detail": "Tags generation is disabled"}, + content={'detail': 'Tags generation is disabled'}, ) - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -352,26 +333,24 @@ async def generate_chat_tags( models, ) - log.debug( - f"generating chat tags using model {task_model_id} for user {user.email} " - ) + log.debug(f'generating chat tags using model {task_model_id} for user {user.email} ') - if request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != "": + if request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE != '': template = request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE else: template = DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE - content = tags_generation_template(template, form_data["messages"], user) + content = await tags_generation_template(template, form_data['messages'], user) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.TAGS_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.TAGS_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -384,29 +363,28 @@ async def generate_chat_tags( try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: - log.error(f"Error generating chat completion: {e}") + log.error(f'Error generating chat completion: {e}') return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={"detail": "An internal error has occurred."}, + content={'detail': 'An internal error has occurred.'}, ) -@router.post("/image_prompt/completions") -async def generate_image_prompt( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): +@router.post('/image_prompt/completions') +async def generate_image_prompt(request: Request, form_data: dict, user=Depends(get_verified_user)): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -418,26 +396,24 @@ async def generate_image_prompt( models, ) - log.debug( - f"generating image prompt using model {task_model_id} for user {user.email} " - ) + log.debug(f'generating image prompt using model {task_model_id} for user {user.email} ') - if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != "": + if request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE != '': template = request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE else: template = DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE - content = image_prompt_generation_template(template, form_data["messages"], user) + content = await image_prompt_generation_template(template, form_data['messages'], user) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.IMAGE_PROMPT_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.IMAGE_PROMPT_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -450,48 +426,46 @@ async def generate_image_prompt( try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: - log.error("Exception occurred", exc_info=True) + log.error('Exception occurred', exc_info=True) return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": "An internal error has occurred."}, + content={'detail': 'An internal error has occurred.'}, ) -@router.post("/queries/completions") -async def generate_queries( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - - type = form_data.get("type") - if type == "web_search": +@router.post('/queries/completions') +async def generate_queries(request: Request, form_data: dict, user=Depends(get_verified_user)): + type = form_data.get('type') + if type == 'web_search': if not request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Search query generation is disabled", + detail=ERROR_MESSAGES.FEATURE_DISABLED('Search query generation'), ) - elif type == "retrieval": + elif type == 'retrieval': if not request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Query generation is disabled", + detail=ERROR_MESSAGES.FEATURE_DISABLED('Query generation'), ) - if getattr(request.state, "cached_queries", None): - log.info(f"Reusing cached queries: {request.state.cached_queries}") + if getattr(request.state, 'cached_queries', None): + log.info(f'Reusing cached queries: {request.state.cached_queries}') return request.state.cached_queries - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -503,26 +477,24 @@ async def generate_queries( models, ) - log.debug( - f"generating {type} queries using model {task_model_id} for user {user.email}" - ) + log.debug(f'generating {type} queries using model {task_model_id} for user {user.email}') - if (request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != "": + if (request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE).strip() != '': template = request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE else: template = DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE - content = query_generation_template(template, form_data["messages"], user) + content = await query_generation_template(template, form_data['messages'], user) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.QUERY_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.QUERY_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -537,46 +509,42 @@ async def generate_queries( except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": str(e)}, + content={'detail': str(e)}, ) -@router.post("/auto/completions") -async def generate_autocompletion( - request: Request, form_data: dict, user=Depends(get_verified_user) -): +@router.post('/auto/completions') +async def generate_autocompletion(request: Request, form_data: dict, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Autocompletion generation is disabled", + detail=ERROR_MESSAGES.FEATURE_DISABLED('Autocompletion generation'), ) - type = form_data.get("type") - prompt = form_data.get("prompt") - messages = form_data.get("messages") + type = form_data.get('type') + prompt = form_data.get('prompt') + messages = form_data.get('messages') if request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH > 0: - if ( - len(prompt) - > request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH - ): + if len(prompt) > request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Input prompt exceeds maximum length of {request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}", + detail=ERROR_MESSAGES.INPUT_TOO_LONG(request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH), ) - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -588,26 +556,24 @@ async def generate_autocompletion( models, ) - log.debug( - f"generating autocompletion using model {task_model_id} for user {user.email}" - ) + log.debug(f'generating autocompletion using model {task_model_id} for user {user.email}') - if (request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != "": + if (request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE).strip() != '': template = request.app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE else: template = DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE - content = autocomplete_generation_template(template, prompt, messages, type, user) + content = await autocomplete_generation_template(template, prompt, messages, type, user) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.AUTOCOMPLETE_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.AUTOCOMPLETE_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -620,30 +586,28 @@ async def generate_autocompletion( try: return await generate_chat_completion(request, form_data=payload, user=user) except Exception as e: - log.error(f"Error generating chat completion: {e}") + log.error(f'Error generating chat completion: {e}') return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={"detail": "An internal error has occurred."}, + content={'detail': 'An internal error has occurred.'}, ) -@router.post("/emoji/completions") -async def generate_emoji( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): +@router.post('/emoji/completions') +async def generate_emoji(request: Request, form_data: dict, user=Depends(get_verified_user)): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -655,28 +619,28 @@ async def generate_emoji( models, ) - log.debug(f"generating emoji using model {task_model_id} for user {user.email} ") + log.debug(f'generating emoji using model {task_model_id} for user {user.email} ') template = DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE - content = emoji_generation_template(template, form_data["prompt"], user) + content = await emoji_generation_template(template, form_data['prompt'], user) payload = { - "model": task_model_id, - "messages": [{"role": "user", "content": content}], - "stream": False, + 'model': task_model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': False, **( - {"max_tokens": 4} - if models[task_model_id].get("owned_by") == "ollama" + {'max_tokens': 4} + if models[task_model_id].get('owned_by') == 'ollama' else { - "max_completion_tokens": 4, + 'max_completion_tokens': 4, } ), - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "task": str(TASKS.EMOJI_GENERATION), - "task_body": form_data, - "chat_id": form_data.get("chat_id", None), + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'task': str(TASKS.EMOJI_GENERATION), + 'task_body': form_data, + 'chat_id': form_data.get('chat_id', None), }, } @@ -691,47 +655,45 @@ async def generate_emoji( except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": str(e)}, + content={'detail': str(e)}, ) -@router.post("/moa/completions") -async def generate_moa_response( - request: Request, form_data: dict, user=Depends(get_verified_user) -): - - if getattr(request.state, "direct", False) and hasattr(request.state, "model"): +@router.post('/moa/completions') +async def generate_moa_response(request: Request, form_data: dict, user=Depends(get_verified_user)): + if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): models = { - request.state.model["id"]: request.state.model, + **request.app.state.MODELS, + request.state.model['id']: request.state.model, } else: models = request.app.state.MODELS - model_id = form_data["model"] + model_id = form_data['model'] if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail="Model not found", + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE content = moa_response_generation_template( template, - form_data["prompt"], - form_data["responses"], + form_data['prompt'], + form_data['responses'], ) payload = { - "model": model_id, - "messages": [{"role": "user", "content": content}], - "stream": form_data.get("stream", False), - "metadata": { - **(request.state.metadata if hasattr(request.state, "metadata") else {}), - "chat_id": form_data.get("chat_id", None), - "task": str(TASKS.MOA_RESPONSE_GENERATION), - "task_body": form_data, + 'model': model_id, + 'messages': [{'role': 'user', 'content': content}], + 'stream': form_data.get('stream', False), + 'metadata': { + **(request.state.metadata if hasattr(request.state, 'metadata') else {}), + 'chat_id': form_data.get('chat_id', None), + 'task': str(TASKS.MOA_RESPONSE_GENERATION), + 'task_body': form_data, }, } @@ -746,5 +708,5 @@ async def generate_moa_response( except Exception as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content={"detail": str(e)}, + content={'detail': str(e)}, ) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py new file mode 100644 index 0000000000..6a942cf9b2 --- /dev/null +++ b/backend/open_webui/routers/terminals.py @@ -0,0 +1,355 @@ +"""Reverse proxy for admin-configured terminal servers. + +Routes: + GET / — list terminals the user has access to + * /{server_id}/{path:path} — proxy request to terminal server +""" + +import logging +import posixpath +from urllib.parse import unquote + +import aiohttp +from fastapi import APIRouter, Depends, Request, Response, WebSocket +from fastapi.responses import JSONResponse, StreamingResponse +from open_webui.config import TERMINAL_PROXY_HEADERS +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL +from open_webui.models.groups import Groups +from open_webui.models.users import Users +from open_webui.utils.access_control import has_connection_access +from open_webui.utils.auth import get_verified_user +from starlette.background import BackgroundTask + +log = logging.getLogger(__name__) + +router = APIRouter() + +STREAMING_CONTENT_TYPES = ('application/octet-stream', 'image/', 'application/pdf') +STRIPPED_RESPONSE_HEADERS = frozenset(('transfer-encoding', 'connection', 'content-encoding', 'content-length')) + + +def _sanitize_proxy_path(path: str) -> str | None: + """Sanitize a proxy path to prevent directory traversal / SSRF. + + Returns the cleaned path, or None if the path is invalid. + Trailing slashes are preserved — many upstream frameworks treat + ``/path`` and ``/path/`` differently. + """ + # Decode until stable: a single unquote pass leaves %252e%252e as %2e%2e, + # which the upstream then re-decodes into '..', bypassing the check below. + decoded = path + for _ in range(8): + once = unquote(decoded) + if once == decoded: + break + decoded = once + had_trailing_slash = decoded.endswith('/') + normalized = posixpath.normpath(decoded) + # Remove any leading slashes that would reset the base + cleaned = normalized.lstrip('/') + # Reject if normpath resolved to parent traversal or current-dir only + if cleaned.startswith('..') or cleaned == '.': + return None + # Restore trailing slash if the original path had one + if had_trailing_slash and cleaned and not cleaned.endswith('/'): + cleaned += '/' + return cleaned + + +@router.get('/') +async def list_terminal_servers(request: Request, user=Depends(get_verified_user)): + """Return terminal servers the authenticated user has access to.""" + connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + + return [ + { + 'id': connection.get('id', ''), + 'url': connection.get('url', ''), + 'name': connection.get('name', ''), + } + for connection in connections + if connection.get('enabled', True) and await has_connection_access(user, connection, user_group_ids) + ] + + +PROXY_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] + + +@router.api_route('/{server_id}/{path:path}', methods=PROXY_METHODS) +async def proxy_terminal( + server_id: str, + path: str, + request: Request, + user=Depends(get_verified_user), +): + """Proxy a request to the admin terminal server identified by *server_id*.""" + connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + connection = next((c for c in connections if c.get('id') == server_id), None) + + if connection is None: + return JSONResponse({'error': f"Terminal server '{server_id}' not found"}, status_code=404) + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + if not await has_connection_access(user, connection, user_group_ids): + return JSONResponse({'error': 'Access denied'}, status_code=403) + + base_url = (connection.get('url') or '').rstrip('/') + if not base_url: + return JSONResponse({'error': 'Terminal server URL not configured'}, status_code=503) + + safe_path = _sanitize_proxy_path(path) + if safe_path is None: + return JSONResponse({'error': 'Invalid path'}, status_code=400) + + target_url = f'{base_url}/{safe_path}' + + # Route through orchestrator policy endpoint if policy_id is set + policy_id = connection.get('policy_id') + if policy_id: + target_url = f'{base_url}/p/{policy_id}/{safe_path}' + + if request.query_params: + target_url += f'?{request.query_params}' + + headers = {'X-User-Id': user.id} + # Forward per-session cwd tracking header + session_id = request.headers.get('x-session-id') + if session_id: + headers['X-Session-Id'] = session_id + cookies = {} + auth_type = connection.get('auth_type', 'bearer') + + if auth_type == 'bearer': + headers['Authorization'] = f'Bearer {connection.get("key", "")}' + elif auth_type == 'session': + cookies = request.cookies + headers['Authorization'] = f'Bearer {request.state.token.credentials}' + elif auth_type == 'system_oauth': + cookies = request.cookies + oauth_token = request.headers.get('x-oauth-access-token', '') + if oauth_token: + headers['Authorization'] = f'Bearer {oauth_token}' + # auth_type == "none": no Authorization header + + content_type = request.headers.get('content-type') + if content_type: + headers['Content-Type'] = content_type + + body = await request.body() + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=300, connect=10), + trust_env=True, + ) + + try: + upstream_response = await session.request( + method=request.method, + url=target_url, + headers=headers, + cookies=cookies, + data=body or None, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + + upstream_content_type = upstream_response.headers.get('content-type', '') + filtered_headers = { + key: value + for key, value in upstream_response.headers.items() + if key.lower() not in STRIPPED_RESPONSE_HEADERS + } + if TERMINAL_PROXY_HEADERS: + filtered_headers.update(TERMINAL_PROXY_HEADERS) + + # Stream binary responses directly + if any(t in upstream_content_type for t in STREAMING_CONTENT_TYPES): + + async def cleanup(): + await upstream_response.release() + await session.close() + + return StreamingResponse( + content=upstream_response.content.iter_any(), + status_code=upstream_response.status, + headers=filtered_headers, + background=BackgroundTask(cleanup), + ) + + # Buffer text/JSON responses + response_body = await upstream_response.read() + status_code = upstream_response.status + await upstream_response.release() + await session.close() + + return Response(content=response_body, status_code=status_code, headers=filtered_headers) + + except Exception as error: + await session.close() + log.exception('Terminal proxy error: %s', error) + return JSONResponse({'error': f'Terminal proxy error: {error}'}, status_code=502) + + +# --------------------------------------------------------------------------- +# WebSocket proxy for interactive terminal sessions +# --------------------------------------------------------------------------- + + +async def _resolve_authenticated_connection(ws: WebSocket, server_id: str): + """Authenticate a WebSocket via first-message auth and resolve the terminal server. + + The client must send ``{"type": "auth", "token": ""}`` as its first + message after connecting. + + Returns ``(user, connection)`` on success, or ``None`` after closing *ws* + with an appropriate error code. + """ + import asyncio + import json + + from open_webui.utils.auth import decode_token + + # First-message authentication + try: + raw = await asyncio.wait_for(ws.receive_text(), timeout=10.0) + payload = json.loads(raw) + if payload.get('type') != 'auth': + await ws.close(code=4001, reason='Expected auth message') + return None + token = payload.get('token', '') + data = decode_token(token) + if data is None or 'id' not in data: + await ws.close(code=4001, reason='Invalid token') + return None + user = await Users.get_user_by_id(data['id']) + if user is None: + await ws.close(code=4001, reason='User not found') + return None + except (asyncio.TimeoutError, json.JSONDecodeError): + await ws.close(code=4001, reason='Auth timeout or invalid payload') + return None + except Exception: + await ws.close(code=4001, reason='Invalid token') + return None + + # Resolve terminal server + connections = ws.app.state.config.TERMINAL_SERVER_CONNECTIONS or [] + connection = next((c for c in connections if c.get('id') == server_id), None) + + if connection is None: + await ws.close(code=4004, reason='Terminal server not found') + return None + + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id)} + if not await has_connection_access(user, connection, user_group_ids): + await ws.close(code=4003, reason='Access denied') + return None + + return user, connection + + +@router.websocket('/{server_id}/api/terminals/{session_id}') +async def ws_terminal( + ws: WebSocket, + server_id: str, + session_id: str, +): + """Proxy an interactive WebSocket terminal session to a terminal server. + + Uses first-message auth: the client sends ``{"type": "auth", "token": ""}`` + as its first message. The proxy validates the JWT, then connects to the + upstream terminal server and authenticates with the server's API key. + """ + await ws.accept() + + result = await _resolve_authenticated_connection(ws, server_id) + if result is None: + return + user, connection = result + + base_url = (connection.get('url') or '').rstrip('/') + if not base_url: + await ws.close(code=4003, reason='Terminal server URL not configured') + return + + # Build upstream WebSocket URL (no token in URL) + ws_base = base_url.replace('https://', 'wss://').replace('http://', 'ws://') + + # Route through orchestrator policy endpoint if policy_id is set + policy_id = connection.get('policy_id') + upstream_params = {} + # For orchestrator-backed servers, pass user_id + upstream_params['user_id'] = user.id + + import urllib.parse + + if policy_id: + upstream_url = f'{ws_base}/p/{policy_id}/api/terminals/{session_id}' + else: + upstream_url = f'{ws_base}/api/terminals/{session_id}' + if upstream_params: + upstream_url += f'?{urllib.parse.urlencode(upstream_params)}' + + session = aiohttp.ClientSession() + try: + async with session.ws_connect(upstream_url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as upstream: + import asyncio + import json as _json + + # First-message auth to upstream terminal server + auth_type = connection.get('auth_type', 'bearer') + if auth_type == 'bearer': + key = connection.get('key', '') + await upstream.send_str(_json.dumps({'type': 'auth', 'token': key})) + + async def _client_to_upstream(): + """Forward client → upstream.""" + try: + while True: + msg = await ws.receive() + if msg['type'] == 'websocket.disconnect': + break + elif 'bytes' in msg and msg['bytes']: + await upstream.send_bytes(msg['bytes']) + elif 'text' in msg and msg['text']: + await upstream.send_str(msg['text']) + except Exception: + pass + + async def _upstream_to_client(): + """Forward upstream → client.""" + try: + async for msg in upstream: + if msg.type == aiohttp.WSMsgType.BINARY: + await ws.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.TEXT: + await ws.send_text(msg.data) + elif msg.type in ( + aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.ERROR, + ): + break + except Exception: + pass + + # End the proxy as soon as either direction finishes (e.g. a + # graceful upstream CLOSE) and cancel the sibling, which would + # otherwise hang on a blocked ws.receive() until the browser leaves. + tasks = [ + asyncio.create_task(_client_to_upstream()), + asyncio.create_task(_upstream_to_client()), + ] + _done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception as e: + log.exception('Terminal WebSocket proxy error: %s', e) + finally: + await session.close() + try: + await ws.close() + except Exception: + pass diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 03018d24a1..b922a9c0d6 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -1,39 +1,43 @@ +from __future__ import annotations + import logging +import re +import time from pathlib import Path from typing import Optional -import time -import re + import aiohttp -from open_webui.env import AIOHTTP_CLIENT_TIMEOUT -from open_webui.models.groups import Groups -from pydantic import BaseModel, HttpUrl from fastapi import APIRouter, Depends, HTTPException, Request, status -from sqlalchemy.orm import Session -from open_webui.internal.db import get_session - - +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, CACHE_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups from open_webui.models.oauth_sessions import OAuthSessions from open_webui.models.tools import ( + ToolAccessResponse, ToolForm, ToolModel, ToolResponse, - ToolUserResponse, - ToolAccessResponse, Tools, + ToolUserResponse, ) +from open_webui.utils.access_control import ( + filter_allowed_access_grants, + has_access, + has_permission, +) +from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.plugin import ( + get_tool_module_from_cache, load_tool_module_by_id, replace_imports, - get_tool_module_from_cache, + resolve_valves_schema_options, ) -from open_webui.utils.tools import get_tool_specs -from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access, has_permission -from open_webui.utils.tools import get_tool_servers - -from open_webui.config import CACHE_DIR, BYPASS_ADMIN_ACCESS_CONTROL -from open_webui.constants import ERROR_MESSAGES - +from open_webui.utils.tools import get_tool_servers, get_tool_specs +from pydantic import BaseModel, HttpUrl +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -41,123 +45,143 @@ log = logging.getLogger(__name__) router = APIRouter() -def get_tool_module(request, tool_id, load_from_db=True): +async def get_tool_module(request, tool_id, load_from_db=True): """ Get the tool module by its ID. """ - tool_module, _ = get_tool_module_from_cache(request, tool_id, load_from_db) + tool_module, _ = await get_tool_module_from_cache(request, tool_id, load_from_db) return tool_module ############################ # GetTools +# The danger is not in having tools, but in reaching +# for the wrong one. Let the choice here be deliberate. ############################ -@router.get("/", response_model=list[ToolUserResponse]) +@router.get('/', response_model=list[ToolUserResponse]) async def get_tools( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): tools = [] # Local Tools - for tool in Tools.get_tools(db=db): - tool_module = get_tool_module(request, tool.id) + for tool in await Tools.get_tools(defer_content=True, db=db): + tool_module = request.app.state.TOOLS.get(tool.id) if hasattr(request.app.state, 'TOOLS') else None tools.append( ToolUserResponse( **{ **tool.model_dump(), - "has_user_valves": hasattr(tool_module, "UserValves"), + 'has_user_valves': (hasattr(tool_module, 'UserValves') if tool_module else False), } ) ) # OpenAPI Tool Servers + server_access_grants = {} for server in await get_tool_servers(request): + server_idx = server.get('idx', 0) + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + if server_idx >= len(connections): + log.warning( + f'Tool server index {server_idx} out of range ' + f'(have {len(connections)} connections), skipping server {server.get("id")}' + ) + continue + connection = connections[server_idx] + server_config = connection.get('config', {}) + + server_id = f'server:{server.get("id")}' + server_access_grants[server_id] = server_config.get('access_grants', []) + tools.append( ToolUserResponse( **{ - "id": f"server:{server.get('id')}", - "user_id": f"server:{server.get('id')}", - "name": server.get("openapi", {}) - .get("info", {}) - .get("title", "Tool Server"), - "meta": { - "description": server.get("openapi", {}) - .get("info", {}) - .get("description", ""), + 'id': server_id, + 'user_id': server_id, + 'name': server.get('openapi', {}).get('info', {}).get('title', 'Tool Server'), + 'meta': { + 'description': server.get('openapi', {}).get('info', {}).get('description', ''), }, - "access_control": request.app.state.config.TOOL_SERVER_CONNECTIONS[ - server.get("idx", 0) - ] - .get("config", {}) - .get("access_control", None), - "updated_at": int(time.time()), - "created_at": int(time.time()), + 'updated_at': int(time.time()), + 'created_at': int(time.time()), } ) ) # MCP Tool Servers for server in request.app.state.config.TOOL_SERVER_CONNECTIONS: - if server.get("type", "openapi") == "mcp": - server_id = server.get("info", {}).get("id") - auth_type = server.get("auth_type", "none") + if server.get('type', 'openapi') == 'mcp' and server.get('config', {}).get('enable'): + server_id = server.get('info', {}).get('id') + auth_type = server.get('auth_type', 'none') session_token = None - if auth_type == "oauth_2.1": - splits = server_id.split(":") + if auth_type in ('oauth_2.1', 'oauth_2.1_static'): + splits = server_id.split(':') server_id = splits[-1] if len(splits) > 1 else server_id - session_token = ( - await request.app.state.oauth_client_manager.get_oauth_token( - user.id, f"mcp:{server_id}" - ) + session_token = await request.app.state.oauth_client_manager.get_oauth_token( + user.id, f'mcp:{server_id}' ) + server_config = server.get('config', {}) + + tool_id = f'server:mcp:{server.get("info", {}).get("id")}' + server_access_grants[tool_id] = server_config.get('access_grants', []) + tools.append( ToolUserResponse( **{ - "id": f"server:mcp:{server.get('info', {}).get('id')}", - "user_id": f"server:mcp:{server.get('info', {}).get('id')}", - "name": server.get("info", {}).get("name", "MCP Tool Server"), - "meta": { - "description": server.get("info", {}).get( - "description", "" - ), + 'id': tool_id, + 'user_id': tool_id, + 'name': server.get('info', {}).get('name', 'MCP Tool Server'), + 'meta': { + 'description': server.get('info', {}).get('description', ''), }, - "access_control": server.get("config", {}).get( - "access_control", None - ), - "updated_at": int(time.time()), - "created_at": int(time.time()), + 'updated_at': int(time.time()), + 'created_at': int(time.time()), **( { - "authenticated": session_token is not None, + 'authenticated': session_token is not None, } - if auth_type == "oauth_2.1" + if auth_type in ('oauth_2.1', 'oauth_2.1_static') else {} ), } ) ) - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: # Admin can see all tools return tools else: - user_group_ids = { - group.id for group in Groups.get_groups_by_member_id(user.id, db=db) - } - tools = [ - tool - for tool in tools - if tool.user_id == user.id - or has_access(user.id, "read", tool.access_control, user_group_ids, db=db) - ] - return tools + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + filtered_tools = [] + for tool in tools: + if tool.user_id == user.id: + filtered_tools.append(tool) + elif str(tool.id).startswith('server:'): + if await has_access( + user.id, + 'read', + server_access_grants.get(str(tool.id), []), + user_group_ids, + db=db, + ): + filtered_tools.append(tool) + elif await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tool.id, + permission='read', + user_group_ids=user_group_ids, + db=db, + ): + filtered_tools.append(tool) + return filtered_tools ############################ @@ -165,26 +189,36 @@ async def get_tools( ############################ -@router.get("/list", response_model=list[ToolAccessResponse]) -async def get_tool_list( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: - tools = Tools.get_tools(db=db) +@router.get('/list', response_model=list[ToolAccessResponse]) +async def get_tool_list(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + tools = await Tools.get_tools(defer_content=True, db=db) else: - tools = Tools.get_tools_by_user_id(user.id, "read", db=db) + tools = await Tools.get_tools_by_user_id(user.id, 'read', defer_content=True, db=db) - return [ - ToolAccessResponse( - **tool.model_dump(), - write_access=( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == tool.user_id - or has_access(user.id, "write", tool.access_control, db=db) - ), + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + + result = [] + for tool in tools: + has_write = ( + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == tool.user_id + or any( + g.permission == 'write' + and ( + (g.principal_type == 'user' and (g.principal_id == user.id or g.principal_id == '*')) + or (g.principal_type == 'group' and g.principal_id in user_group_ids) + ) + for g in tool.access_grants + ) ) - for tool in tools - ] + result.append( + ToolAccessResponse( + **tool.model_dump(), + write_access=has_write, + ) + ) + return result ############################ @@ -198,46 +232,41 @@ class LoadUrlForm(BaseModel): def github_url_to_raw_url(url: str) -> str: # Handle 'tree' (folder) URLs (add main.py at the end) - m1 = re.match(r"https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)", url) + m1 = re.match(r'https://github\.com/([^/]+)/([^/]+)/tree/([^/]+)/(.*)', url) if m1: org, repo, branch, path = m1.groups() - return f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip('/')}/main.py" + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path.rstrip("/")}/main.py' # Handle 'blob' (file) URLs - m2 = re.match(r"https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)", url) + m2 = re.match(r'https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)', url) if m2: org, repo, branch, path = m2.groups() - return ( - f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}" - ) + return f'https://raw.githubusercontent.com/{org}/{repo}/refs/heads/{branch}/{path}' # No match; return as-is return url -@router.post("/load/url", response_model=Optional[dict]) -async def load_tool_from_url( - request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user) -): +@router.post('/load/url', response_model=dict | None) +async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depends(get_admin_user)): # NOTE: This is NOT a SSRF vulnerability: # This endpoint is admin-only (see get_admin_user), meant for *trusted* internal use, # and does NOT accept untrusted user input. Access is enforced by authentication. url = str(form_data.url) if not url: - raise HTTPException(status_code=400, detail="Please enter a valid URL") + raise HTTPException(status_code=400, detail='Please enter a valid URL') url = github_url_to_raw_url(url) - url_parts = url.rstrip("/").split("/") + url_parts = url.rstrip('/').split('/') file_name = url_parts[-1] tool_name = ( file_name[:-3] - if ( - file_name.endswith(".py") - and (not file_name.startswith(("main.py", "index.py", "__init__.py"))) - ) - else url_parts[-2] if len(url_parts) > 1 else "function" + if (file_name.endswith('.py') and (not file_name.startswith(('main.py', 'index.py', '__init__.py')))) + else url_parts[-2] + if len(url_parts) > 1 + else 'function' ) try: @@ -245,23 +274,19 @@ async def load_tool_from_url( trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) ) as session: async with session.get( - url, headers={"Content-Type": "application/json"} + url, headers={'Content-Type': 'application/json'}, ssl=AIOHTTP_CLIENT_SESSION_SSL ) as resp: if resp.status != 200: - raise HTTPException( - status_code=resp.status, detail="Failed to fetch the tool" - ) + raise HTTPException(status_code=resp.status, detail='Failed to fetch the tool') data = await resp.text() if not data: - raise HTTPException( - status_code=400, detail="No data received from the URL" - ) + raise HTTPException(status_code=400, detail='No data received from the URL') return { - "name": tool_name, - "content": data, + 'name': tool_name, + 'content': data, } except Exception as e: - raise HTTPException(status_code=500, detail=f"Error importing tool: {e}") + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.DEFAULT(e)) ############################ @@ -269,15 +294,15 @@ async def load_tool_from_url( ############################ -@router.get("/export", response_model=list[ToolModel]) +@router.get('/export', response_model=list[ToolModel]) async def export_tools( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not has_permission( + if user.role != 'admin' and not await has_permission( user.id, - "workspace.tools_export", + 'workspace.tools_export', request.app.state.config.USER_PERMISSIONS, db=db, ): @@ -286,10 +311,10 @@ async def export_tools( detail=ERROR_MESSAGES.UNAUTHORIZED, ) - if user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL: - return Tools.get_tools(db=db) + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: + return await Tools.get_tools(db=db) else: - return Tools.get_tools_by_user_id(user.id, "read", db=db) + return await Tools.get_tools_by_user_id(user.id, 'read', db=db) ############################ @@ -297,20 +322,17 @@ async def export_tools( ############################ -@router.post("/create", response_model=Optional[ToolResponse]) +@router.post('/create', response_model=ToolResponse | None) async def create_new_tools( - request: Request, - form_data: ToolForm, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + request: Request, form_data: ToolForm, + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - if user.role != "admin" and not ( - has_permission( - user.id, "workspace.tools", request.app.state.config.USER_PERMISSIONS, db=db - ) - or has_permission( + """Create a new tool from user-supplied Python source code.""" + if user.role != 'admin' and not ( + await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) + or await has_permission( user.id, - "workspace.tools_import", + 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db, ) @@ -323,27 +345,33 @@ async def create_new_tools( if not form_data.id.isidentifier(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Only alphanumeric characters and underscores are allowed in the id", + detail='Only alphanumeric characters and underscores are allowed in the id', ) form_data.id = form_data.id.lower() - tools = Tools.get_tool_by_id(form_data.id, db=db) + tools = await Tools.get_tool_by_id(form_data.id, db=db) if tools is None: try: - form_data.content = replace_imports(form_data.content) - tool_module, frontmatter = load_tool_module_by_id( - form_data.id, content=form_data.content + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_tools', ) + + form_data.content = replace_imports(form_data.content) + tool_module, frontmatter = await load_tool_module_by_id(form_data.id, content=form_data.content) form_data.meta.manifest = frontmatter TOOLS = request.app.state.TOOLS TOOLS[form_data.id] = tool_module specs = get_tool_specs(TOOLS[form_data.id]) - tools = Tools.insert_new_tool(user.id, form_data, specs, db=db) + tools = await Tools.insert_new_tool(user.id, form_data, specs, db=db) - tool_cache_dir = CACHE_DIR / "tools" / form_data.id + tool_cache_dir = CACHE_DIR / 'tools' / form_data.id tool_cache_dir.mkdir(parents=True, exist_ok=True) if tools: @@ -351,10 +379,10 @@ async def create_new_tools( else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error creating tools"), + detail=ERROR_MESSAGES.DEFAULT('Error creating tools'), ) except Exception as e: - log.exception(f"Failed to load the tool by id {form_data.id}: {e}") + log.exception(f'Failed to load the tool by id {form_data.id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(str(e)), @@ -371,24 +399,34 @@ async def create_new_tools( ############################ -@router.get("/id/{id}", response_model=Optional[ToolAccessResponse]) -async def get_tools_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - tools = Tools.get_tool_by_id(id, db=db) +@router.get('/id/{id}', response_model=ToolAccessResponse | None) +async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + tools = await Tools.get_tool_by_id(id, db=db) if tools: if ( - user.role == "admin" + user.role == 'admin' or tools.user_id == user.id - or has_access(user.id, "read", tools.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) ): return ToolAccessResponse( **tools.model_dump(), write_access=( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) or user.id == tools.user_id - or has_access(user.id, "write", tools.access_control, db=db) + or await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) ), ) else: @@ -408,35 +446,49 @@ async def get_tools_by_id( ############################ -@router.post("/id/{id}/update", response_model=Optional[ToolModel]) +@router.post('/id/{id}/update', response_model=ToolModel | None) async def update_tools_by_id( - request: Request, - id: str, - form_data: ToolForm, - user=Depends(get_verified_user), - db: Session = Depends(get_session), + request: Request, id: str, form_data: ToolForm, + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - tools = Tools.get_tool_by_id(id, db=db) + """Update an existing tool's source code and metadata.""" + tools = await Tools.get_tool_by_id(id, db=db) if not tools: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, ) # Is the user the original creator, in a group with write access, or an admin if ( tools.user_id != user.id - and not has_access(user.id, "write", tools.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) + # Content edits trigger exec on load — gate them behind workspace.tools (matches /create). + if form_data.content != tools.content: + if user.role != 'admin' and not ( + await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db) + or await has_permission(user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db) + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + try: form_data.content = replace_imports(form_data.content) - tool_module, frontmatter = load_tool_module_by_id(id, content=form_data.content) + tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content) form_data.meta.manifest = frontmatter TOOLS = request.app.state.TOOLS @@ -444,20 +496,28 @@ async def update_tools_by_id( specs = get_tool_specs(TOOLS[id]) + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_tools', + ) + updated = { - **form_data.model_dump(exclude={"id"}), - "specs": specs, + **form_data.model_dump(exclude={'id'}), + 'specs': specs, } log.debug(updated) - tools = Tools.update_tool_by_id(id, updated, db=db) + tools = await Tools.update_tool_by_id(id, updated, db=db) if tools: return tools else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT("Error updating tools"), + detail=ERROR_MESSAGES.DEFAULT('Error updating tools'), ) except Exception as e: @@ -467,19 +527,72 @@ async def update_tools_by_id( ) +############################ +# UpdateToolAccessById +############################ + + +class ToolAccessGrantsForm(BaseModel): + access_grants: list[dict] + + +@router.post('/id/{id}/access/update', response_model=ToolModel | None) +async def update_tool_access_by_id( + request: Request, + id: str, + form_data: ToolAccessGrantsForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + form_data.access_grants = await filter_allowed_access_grants( + request.app.state.config.USER_PERMISSIONS, + user.id, + user.role, + form_data.access_grants, + 'sharing.public_tools', + ) + + await AccessGrants.set_access_grants('tool', id, form_data.access_grants, db=db) + + return await Tools.get_tool_by_id(id, db=db) + + ############################ # DeleteToolsById ############################ -@router.delete("/id/{id}/delete", response_model=bool) +@router.delete('/id/{id}/delete', response_model=bool) async def delete_tools_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - tools = Tools.get_tool_by_id(id, db=db) + tools = await Tools.get_tool_by_id(id, db=db) if not tools: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -488,15 +601,21 @@ async def delete_tools_by_id( if ( tools.user_id != user.id - and not has_access(user.id, "write", tools.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED, ) - result = Tools.delete_tool_by_id(id, db=db) + result = await Tools.delete_tool_by_id(id, db=db) if result: TOOLS = request.app.state.TOOLS if id in TOOLS: @@ -510,24 +629,40 @@ async def delete_tools_by_id( ############################ -@router.get("/id/{id}/valves", response_model=Optional[dict]) +@router.get('/id/{id}/valves', response_model=dict | None) async def get_tools_valves_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - tools = Tools.get_tool_by_id(id, db=db) - if tools: - try: - valves = Tools.get_tool_valves_by_id(id, db=db) - return valves - except Exception as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(str(e)), - ) - else: + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + valves = await Tools.get_tool_valves_by_id(id, db=db) + return valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), ) @@ -536,46 +671,65 @@ async def get_tools_valves_by_id( ############################ -@router.get("/id/{id}/valves/spec", response_model=Optional[dict]) +@router.get('/id/{id}/valves/spec', response_model=dict | None) async def get_tools_valves_spec_by_id( request: Request, id: str, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - tools = Tools.get_tool_by_id(id, db=db) - if tools: - if id in request.app.state.TOOLS: - tools_module = request.app.state.TOOLS[id] - else: - tools_module, _ = load_tool_module_by_id(id) - request.app.state.TOOLS[id] = tools_module - - if hasattr(tools_module, "Valves"): - Valves = tools_module.Valves - return Valves.schema() - return None - else: + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if hasattr(tools_module, 'Valves'): + Valves = tools_module.Valves + schema = Valves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(Valves, schema, user) + return schema + return None + ############################ # UpdateToolValves ############################ -@router.post("/id/{id}/valves/update", response_model=Optional[dict]) +@router.post('/id/{id}/valves/update', response_model=dict | None) async def update_tools_valves_by_id( request: Request, id: str, form_data: dict, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - tools = Tools.get_tool_by_id(id, db=db) + tools = await Tools.get_tool_by_id(id, db=db) if not tools: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -584,8 +738,14 @@ async def update_tools_valves_by_id( if ( tools.user_id != user.id - and not has_access(user.id, "write", tools.access_control, db=db) - and user.role != "admin" + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='write', + db=db, + ) + and user.role != 'admin' ): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -595,10 +755,10 @@ async def update_tools_valves_by_id( if id in request.app.state.TOOLS: tools_module = request.app.state.TOOLS[id] else: - tools_module, _ = load_tool_module_by_id(id) + tools_module, _ = await load_tool_module_by_id(id) request.app.state.TOOLS[id] = tools_module - if not hasattr(tools_module, "Valves"): + if not hasattr(tools_module, 'Valves'): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, @@ -609,10 +769,10 @@ async def update_tools_valves_by_id( form_data = {k: v for k, v in form_data.items() if v is not None} valves = Valves(**form_data) valves_dict = valves.model_dump(exclude_unset=True) - Tools.update_tool_valves_by_id(id, valves_dict, db=db) + await Tools.update_tool_valves_by_id(id, valves_dict, db=db) return valves_dict except Exception as e: - log.exception(f"Failed to update tool valves by id {id}: {e}") + log.exception(f'Failed to update tool valves by id {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(str(e)), @@ -624,16 +784,136 @@ async def update_tools_valves_by_id( ############################ -@router.get("/id/{id}/valves/user", response_model=Optional[dict]) +@router.get('/id/{id}/valves/user', response_model=dict | None) async def get_tools_user_valves_by_id( - id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - tools = Tools.get_tool_by_id(id, db=db) - if tools: + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + try: + user_valves = await Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db) + return user_valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(str(e)), + ) + + +@router.get('/id/{id}/valves/user/spec', response_model=dict | None) +async def get_tools_user_valves_spec_by_id( + request: Request, + id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if hasattr(tools_module, 'UserValves'): + UserValves = tools_module.UserValves + schema = UserValves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(UserValves, schema, user) + return schema + return None + + +@router.post('/id/{id}/valves/user/update', response_model=dict | None) +async def update_tools_user_valves_by_id( + request: Request, + id: str, + form_data: dict, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + tools = await Tools.get_tool_by_id(id, db=db) + if not tools: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + if ( + tools.user_id != user.id + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='tool', + resource_id=tools.id, + permission='read', + db=db, + ) + and user.role != 'admin' + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + if id in request.app.state.TOOLS: + tools_module = request.app.state.TOOLS[id] + else: + tools_module, _ = await load_tool_module_by_id(id) + request.app.state.TOOLS[id] = tools_module + + if hasattr(tools_module, 'UserValves'): + UserValves = tools_module.UserValves + try: - user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id, db=db) - return user_valves + form_data = {k: v for k, v in form_data.items() if v is not None} + user_valves = UserValves(**form_data) + user_valves_dict = user_valves.model_dump(exclude_unset=True) + await Tools.update_user_valves_by_id_and_user_id(id, user.id, user_valves_dict, db=db) + return user_valves_dict except Exception as e: + log.exception(f'Failed to update user valves by id {id}: {e}') raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(str(e)), @@ -643,75 +923,3 @@ async def get_tools_user_valves_by_id( status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND, ) - - -@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict]) -async def get_tools_user_valves_spec_by_id( - request: Request, - id: str, - user=Depends(get_verified_user), - db: Session = Depends(get_session), -): - tools = Tools.get_tool_by_id(id, db=db) - if tools: - if id in request.app.state.TOOLS: - tools_module = request.app.state.TOOLS[id] - else: - tools_module, _ = load_tool_module_by_id(id) - request.app.state.TOOLS[id] = tools_module - - if hasattr(tools_module, "UserValves"): - UserValves = tools_module.UserValves - return UserValves.schema() - return None - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, - ) - - -@router.post("/id/{id}/valves/user/update", response_model=Optional[dict]) -async def update_tools_user_valves_by_id( - request: Request, - id: str, - form_data: dict, - user=Depends(get_verified_user), - db: Session = Depends(get_session), -): - tools = Tools.get_tool_by_id(id, db=db) - - if tools: - if id in request.app.state.TOOLS: - tools_module = request.app.state.TOOLS[id] - else: - tools_module, _ = load_tool_module_by_id(id) - request.app.state.TOOLS[id] = tools_module - - if hasattr(tools_module, "UserValves"): - UserValves = tools_module.UserValves - - try: - form_data = {k: v for k, v in form_data.items() if v is not None} - user_valves = UserValves(**form_data) - user_valves_dict = user_valves.model_dump(exclude_unset=True) - Tools.update_user_valves_by_id_and_user_id( - id, user.id, user_valves_dict, db=db - ) - return user_valves_dict - except Exception as e: - log.exception(f"Failed to update user valves by id {id}: {e}") - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DEFAULT(str(e)), - ) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, - ) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.NOT_FOUND, - ) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 8eb4b5d199..c9fca5a12f 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -1,46 +1,45 @@ -import logging -from typing import Optional -from sqlalchemy.orm import Session +from __future__ import annotations + import base64 import io - +import logging +import time +from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status -from fastapi.responses import Response, StreamingResponse, FileResponse -from pydantic import BaseModel, ConfigDict - - +from fastapi.responses import FileResponse, Response, StreamingResponse +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR +from open_webui.internal.db import get_async_session from open_webui.models.auths import Auths -from open_webui.models.oauth_sessions import OAuthSessions - from open_webui.models.groups import Groups -from open_webui.models.chats import Chats +from open_webui.models.oauth_sessions import OAuthSessions from open_webui.models.users import ( - UserModel, - UserGroupIdsModel, UserGroupIdsListResponse, + UserGroupIdsModel, UserInfoListResponse, - UserInfoListResponse, + UserInfoResponse, + UserModel, UserRoleUpdateForm, - UserStatus, Users, UserSettings, + UserStatus, UserUpdateForm, ) - -from open_webui.constants import ERROR_MESSAGES -from open_webui.env import STATIC_DIR -from open_webui.internal.db import get_session - - +from open_webui.models.access_grants import AccessGrants +from open_webui.models.knowledge import Knowledges +from open_webui.models.models import Models +from open_webui.models.tools import Tools +from open_webui.socket.main import disconnect_user_sessions +from open_webui.utils.access_control import get_permissions, has_permission from open_webui.utils.auth import ( get_admin_user, get_password_hash, get_verified_user, validate_password, ) -from open_webui.utils.access_control import get_permissions, has_permission - +from pydantic import BaseModel, ConfigDict +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -49,20 +48,22 @@ router = APIRouter() ############################ # GetUsers +# A house is only as strong as its care for the least of +# its members. Let none here be counted without being served. ############################ PAGE_ITEM_COUNT = 30 -@router.get("/", response_model=UserGroupIdsListResponse) +@router.get('/', response_model=UserGroupIdsListResponse) async def get_users( - query: Optional[str] = None, - order_by: Optional[str] = None, - direction: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): limit = PAGE_ITEM_COUNT @@ -71,53 +72,53 @@ async def get_users( filter = {} if query: - filter["query"] = query + filter['query'] = query if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - filter["direction"] = direction + filter['direction'] = direction - result = Users.get_users(filter=filter, skip=skip, limit=limit, db=db) + result = await Users.get_users(filter=filter, skip=skip, limit=limit, db=db) - users = result["users"] - total = result["total"] + users = result['users'] + total = result['total'] # Fetch groups for all users in a single query to avoid N+1 user_ids = [user.id for user in users] - user_groups = Groups.get_groups_by_member_ids(user_ids, db=db) + user_groups = await Groups.get_groups_by_member_ids(user_ids, db=db) return { - "users": [ + 'users': [ UserGroupIdsModel( **{ **user.model_dump(), - "group_ids": [group.id for group in user_groups.get(user.id, [])], + 'group_ids': [group.id for group in user_groups.get(user.id, [])], } ) for user in users ], - "total": total, + 'total': total, } -@router.get("/all", response_model=UserInfoListResponse) +@router.get('/all', response_model=UserInfoListResponse) async def get_all_users( user=Depends(get_admin_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - return Users.get_users(db=db) + return await Users.get_users(db=db) -@router.get("/search", response_model=UserInfoListResponse) +@router.get('/search', response_model=UserInfoListResponse) async def search_users( - query: Optional[str] = None, - order_by: Optional[str] = None, - direction: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): limit = PAGE_ITEM_COUNT @@ -126,13 +127,13 @@ async def search_users( filter = {} if query: - filter["query"] = query + filter['query'] = query if order_by: - filter["order_by"] = order_by + filter['order_by'] = order_by if direction: - filter["direction"] = direction + filter['direction'] = direction - return Users.get_users(filter=filter, skip=skip, limit=limit, db=db) + return await Users.get_users(filter=filter, skip=skip, limit=limit, db=db) ############################ @@ -140,11 +141,9 @@ async def search_users( ############################ -@router.get("/groups") -async def get_user_groups( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - return Groups.get_groups_by_member_id(user.id, db=db) +@router.get('/groups') +async def get_user_groups(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + return await Groups.get_groups_by_member_id(user.id, db=db) ############################ @@ -152,15 +151,13 @@ async def get_user_groups( ############################ -@router.get("/permissions") +@router.get('/permissions') async def get_user_permissisions( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): - user_permissions = get_permissions( - user.id, request.app.state.config.USER_PERMISSIONS, db=db - ) + user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) return user_permissions @@ -173,6 +170,7 @@ class WorkspacePermissions(BaseModel): knowledge: bool = False prompts: bool = False tools: bool = False + skills: bool = False models_import: bool = False models_export: bool = False prompts_import: bool = False @@ -190,8 +188,16 @@ class SharingPermissions(BaseModel): public_prompts: bool = False tools: bool = False public_tools: bool = True + skills: bool = False + public_skills: bool = False notes: bool = False public_notes: bool = True + public_chats: bool = False + public_calendars: bool = False + + +class AccessGrantsPermissions(BaseModel): + allow_users: bool = True class ChatPermissions(BaseModel): @@ -200,6 +206,7 @@ class ChatPermissions(BaseModel): system_prompt: bool = True params: bool = True file_upload: bool = True + web_upload: bool = True delete: bool = True delete_message: bool = True continue_response: bool = True @@ -227,6 +234,8 @@ class FeaturesPermissions(BaseModel): image_generation: bool = True code_interpreter: bool = True memories: bool = True + automations: bool = False + calendar: bool = True class SettingsPermissions(BaseModel): @@ -236,36 +245,26 @@ class SettingsPermissions(BaseModel): class UserPermissions(BaseModel): workspace: WorkspacePermissions sharing: SharingPermissions + access_grants: AccessGrantsPermissions chat: ChatPermissions features: FeaturesPermissions settings: SettingsPermissions -@router.get("/default/permissions", response_model=UserPermissions) +@router.get('/default/permissions', response_model=UserPermissions) async def get_default_user_permissions(request: Request, user=Depends(get_admin_user)): return { - "workspace": WorkspacePermissions( - **request.app.state.config.USER_PERMISSIONS.get("workspace", {}) - ), - "sharing": SharingPermissions( - **request.app.state.config.USER_PERMISSIONS.get("sharing", {}) - ), - "chat": ChatPermissions( - **request.app.state.config.USER_PERMISSIONS.get("chat", {}) - ), - "features": FeaturesPermissions( - **request.app.state.config.USER_PERMISSIONS.get("features", {}) - ), - "settings": SettingsPermissions( - **request.app.state.config.USER_PERMISSIONS.get("settings", {}) - ), + 'workspace': WorkspacePermissions(**request.app.state.config.USER_PERMISSIONS.get('workspace', {})), + 'sharing': SharingPermissions(**request.app.state.config.USER_PERMISSIONS.get('sharing', {})), + 'access_grants': AccessGrantsPermissions(**request.app.state.config.USER_PERMISSIONS.get('access_grants', {})), + 'chat': ChatPermissions(**request.app.state.config.USER_PERMISSIONS.get('chat', {})), + 'features': FeaturesPermissions(**request.app.state.config.USER_PERMISSIONS.get('features', {})), + 'settings': SettingsPermissions(**request.app.state.config.USER_PERMISSIONS.get('settings', {})), } -@router.post("/default/permissions") -async def update_default_user_permissions( - request: Request, form_data: UserPermissions, user=Depends(get_admin_user) -): +@router.post('/default/permissions') +async def update_default_user_permissions(request: Request, form_data: UserPermissions, user=Depends(get_admin_user)): request.app.state.config.USER_PERMISSIONS = form_data.model_dump() return request.app.state.config.USER_PERMISSIONS @@ -275,18 +274,12 @@ async def update_default_user_permissions( ############################ -@router.get("/user/settings", response_model=Optional[UserSettings]) +@router.get('/user/settings', response_model=UserSettings | None) async def get_user_settings_by_session_user( - user=Depends(get_verified_user), db: Session = Depends(get_session) + user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - user = Users.get_user_by_id(user.id, db=db) - if user: - return user.settings - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.USER_NOT_FOUND, - ) + # user already fetched by get_verified_user — no need to refetch + return user.settings ############################ @@ -294,29 +287,29 @@ async def get_user_settings_by_session_user( ############################ -@router.post("/user/settings/update", response_model=UserSettings) +@router.post('/user/settings/update', response_model=UserSettings) async def update_user_settings_by_session_user( request: Request, form_data: UserSettings, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): updated_user_settings = form_data.model_dump() - ui_settings = updated_user_settings.get("ui") + ui_settings = updated_user_settings.get('ui') if ( - user.role != "admin" + user.role != 'admin' and ui_settings is not None - and "toolServers" in ui_settings.keys() - and not has_permission( + and 'toolServers' in ui_settings.keys() + and not await has_permission( user.id, - "features.direct_tool_servers", + 'features.direct_tool_servers', request.app.state.config.USER_PERMISSIONS, ) ): # If the user is not an admin and does not have permission to use tool servers, remove the key - updated_user_settings["ui"].pop("toolServers", None) + updated_user_settings['ui'].pop('toolServers', None) - user = Users.update_user_settings_by_id(user.id, updated_user_settings, db=db) + user = await Users.update_user_settings_by_id(user.id, updated_user_settings, db=db) if user: return user.settings else: @@ -331,25 +324,19 @@ async def update_user_settings_by_session_user( ############################ -@router.get("/user/status") +@router.get('/user/status') async def get_user_status_by_session_user( request: Request, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not request.app.state.config.ENABLE_USER_STATUS: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) - user = Users.get_user_by_id(user.id, db=db) - if user: - return user - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.USER_NOT_FOUND, - ) + # user already fetched by get_verified_user — no need to refetch + return user ############################ @@ -357,27 +344,26 @@ async def get_user_status_by_session_user( ############################ -@router.post("/user/status/update") +@router.post('/user/status/update') async def update_user_status_by_session_user( request: Request, form_data: UserStatus, user=Depends(get_verified_user), - db: Session = Depends(get_session), + db: AsyncSession = Depends(get_async_session), ): if not request.app.state.config.ENABLE_USER_STATUS: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) - user = Users.get_user_by_id(user.id, db=db) - if user: - user = Users.update_user_status_by_id(user.id, form_data, db=db) - return user - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.USER_NOT_FOUND, - ) + # user already fetched by get_verified_user — no need to refetch + updated = await Users.update_user_status_by_id(user.id, form_data, db=db) + if updated: + return updated + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) ############################ @@ -385,18 +371,10 @@ async def update_user_status_by_session_user( ############################ -@router.get("/user/info", response_model=Optional[dict]) -async def get_user_info_by_session_user( - user=Depends(get_verified_user), db: Session = Depends(get_session) -): - user = Users.get_user_by_id(user.id, db=db) - if user: - return user.info - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.USER_NOT_FOUND, - ) +@router.get('/user/info', response_model=dict | None) +async def get_user_info_by_session_user(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + # user already fetched by get_verified_user — no need to refetch + return user.info ############################ @@ -404,31 +382,24 @@ async def get_user_info_by_session_user( ############################ -@router.post("/user/info/update", response_model=Optional[dict]) -async def update_user_info_by_session_user( - form_data: dict, user=Depends(get_verified_user), db: Session = Depends(get_session) +@router.post('/user/info/update', response_model=dict | None) +async def update_user_info_by_session_user( # PATCH-style merge + form_data: dict, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): - user = Users.get_user_by_id(user.id, db=db) - if user: - if user.info is None: - user.info = {} + """Merge caller-supplied fields into the current user's info dict. - user = Users.update_user_by_id( - user.id, {"info": {**user.info, **form_data}}, db=db - ) - if user: - return user.info - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.USER_NOT_FOUND, - ) - else: + Uses the auth-time snapshot of ``user.info`` as the merge base. This does + NOT eliminate lost-update races on concurrent same-user writes; real safety + would need row locking or an optimistic-concurrency version column. + """ + merged_info = {**(user.info or {}), **form_data} + updated = await Users.update_user_by_id(user.id, {'info': merged_info}, db=db) + if not updated: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.USER_NOT_FOUND, ) - + return updated.info ############################ # GetUserById @@ -437,38 +408,24 @@ async def update_user_info_by_session_user( class UserActiveResponse(UserStatus): name: str - profile_image_url: Optional[str] = None - groups: Optional[list] = [] + profile_image_url: str | None = None + groups: list | None = [] is_active: bool - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra='allow') -@router.get("/{user_id}", response_model=UserActiveResponse) -async def get_user_by_id( - user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - # Check if user_id is a shared chat - # If it is, get the user_id from the chat - if user_id.startswith("shared-"): - chat_id = user_id.replace("shared-", "") - chat = Chats.get_chat_by_id(chat_id) - if chat: - user_id = chat.user_id - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.USER_NOT_FOUND, - ) +@router.get('/{user_id}', response_model=UserActiveResponse) +async def get_user_by_id(user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) if user: - groups = Groups.get_groups_by_member_id(user_id, db=db) + groups = await Groups.get_groups_by_member_id(user_id, db=db) return UserActiveResponse( **{ **user.model_dump(), - "groups": [{"id": group.id, "name": group.name} for group in groups], - "is_active": Users.is_user_active(user_id, db=db), + 'groups': [{'id': group.id, 'name': group.name} for group in groups], + 'is_active': await Users.is_user_active(user_id, db=db), } ) else: @@ -478,11 +435,32 @@ async def get_user_by_id( ) -@router.get("/{user_id}/oauth/sessions") -async def get_user_oauth_sessions_by_id( - user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) +@router.get('/{user_id}/info', response_model=UserInfoResponse) +async def get_user_info_by_id( + user_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): - sessions = OAuthSessions.get_sessions_by_user_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) + if user: + groups = await Groups.get_groups_by_member_id(user_id, db=db) + return UserInfoResponse( + **{ + **user.model_dump(), + 'groups': [{'id': group.id, 'name': group.name} for group in groups], + 'is_active': await Users.is_user_active(user_id, db=db), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +@router.get('/{user_id}/oauth/sessions') +async def get_user_oauth_sessions_by_id( + user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) +): + sessions = await OAuthSessions.get_sessions_by_user_id(user_id, db=db) if sessions and len(sessions) > 0: return sessions else: @@ -497,34 +475,41 @@ async def get_user_oauth_sessions_by_id( ############################ -@router.get("/{user_id}/profile/image") -async def get_user_profile_image_by_id( - user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) -): - user = Users.get_user_by_id(user_id, db=db) +@router.get('/{user_id}/profile/image') +async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_user)): + user = await Users.get_user_by_id(user_id) if user: if user.profile_image_url: - # check if it's url or base64 - if user.profile_image_url.startswith("http"): - return Response( - status_code=status.HTTP_302_FOUND, - headers={"Location": user.profile_image_url}, - ) - elif user.profile_image_url.startswith("data:image"): + if user.profile_image_url.startswith('http'): + if ENABLE_PROFILE_IMAGE_URL_FORWARDING: + return Response( + status_code=status.HTTP_302_FOUND, + headers={'Location': user.profile_image_url}, + ) + # When forwarding is disabled, fall through to the + # default image to prevent client-side IP/UA/Referer + # leaks via 302 redirect to external origins. + elif user.profile_image_url.startswith('data:image'): try: - header, base64_data = user.profile_image_url.split(",", 1) + header, base64_data = user.profile_image_url.split(',', 1) image_data = base64.b64decode(base64_data) image_buffer = io.BytesIO(image_data) - media_type = header.split(";")[0].lstrip("data:") + media_type = header.split(';')[0].lstrip('data:').lower() + + if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: + return FileResponse(f'{STATIC_DIR}/user.png') return StreamingResponse( image_buffer, media_type=media_type, - headers={"Content-Disposition": "inline"}, + headers={ + 'Content-Disposition': 'inline', + 'X-Content-Type-Options': 'nosniff', + }, ) except Exception as e: pass - return FileResponse(f"{STATIC_DIR}/user.png") + return FileResponse(f'{STATIC_DIR}/user.png') else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -537,12 +522,12 @@ async def get_user_profile_image_by_id( ############################ -@router.get("/{user_id}/active", response_model=dict) +@router.get('/{user_id}/active', response_model=dict) async def get_user_active_status_by_id( - user_id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) + user_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): return { - "active": Users.is_user_active(user_id, db=db), + 'active': await Users.is_user_active(user_id, db=db), } @@ -551,16 +536,15 @@ async def get_user_active_status_by_id( ############################ -@router.post("/{user_id}/update", response_model=Optional[UserModel]) +@router.post('/{user_id}/update', response_model=UserModel | None) async def update_user_by_id( - user_id: str, - form_data: UserUpdateForm, - session_user=Depends(get_admin_user), - db: Session = Depends(get_session), + user_id: str, form_data: UserUpdateForm, + session_user: UserModel = Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), ): # Prevent modification of the primary admin user by other admins try: - first_user = Users.get_first_user(db=db) + first_user = await Users.get_first_user(db=db) if first_user: if user_id == first_user.id: if session_user.id != user_id: @@ -570,25 +554,27 @@ async def update_user_by_id( detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) - if form_data.role != "admin": + if form_data.role is not None and form_data.role != 'admin': # If the primary admin is trying to change their own role, prevent it raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) + except HTTPException: + raise except Exception as e: - log.error(f"Error checking primary admin status: {e}") + log.error(f'Error checking primary admin status: {e}') raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Could not verify primary admin status.", + detail='Could not verify primary admin status.', ) - user = Users.get_user_by_id(user_id, db=db) + user = await Users.get_user_by_id(user_id, db=db) if user: - if form_data.email.lower() != user.email: - email_user = Users.get_user_by_email(form_data.email.lower(), db=db) + if form_data.email is not None and form_data.email.lower() != user.email: + email_user = await Users.get_user_by_email(form_data.email.lower(), db=db) if email_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -602,21 +588,34 @@ async def update_user_by_id( raise HTTPException(400, detail=str(e)) hashed = get_password_hash(form_data.password) - Auths.update_user_password_by_id(user_id, hashed, db=db) + await Auths.update_user_password_by_id(user_id, hashed, db=db) - Auths.update_email_by_id(user_id, form_data.email.lower(), db=db) - updated_user = Users.update_user_by_id( - user_id, - { - "role": form_data.role, - "name": form_data.name, - "email": form_data.email.lower(), - "profile_image_url": form_data.profile_image_url, - }, - db=db, - ) + # Build update dict from only the provided fields + update_data = {} + if form_data.role is not None: + update_data['role'] = form_data.role + if form_data.name is not None: + update_data['name'] = form_data.name + if form_data.email is not None: + update_data['email'] = form_data.email.lower() + await Auths.update_email_by_id(user_id, form_data.email.lower(), db=db) + if form_data.profile_image_url is not None: + update_data['profile_image_url'] = form_data.profile_image_url + + if update_data: + updated_user = await Users.update_user_by_id( + user_id, + update_data, + db=db, + ) + else: + updated_user = user if updated_user: + # If the role changed, disconnect all socket sessions so stale + # privileges cached in SESSION_POOL are invalidated. + if updated_user.role != user.role: + await disconnect_user_sessions(user_id) return updated_user raise HTTPException( @@ -635,29 +634,30 @@ async def update_user_by_id( ############################ -@router.delete("/{user_id}", response_model=bool) -async def delete_user_by_id( - user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) -): +@router.delete('/{user_id}', response_model=bool) +async def delete_user_by_id(user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): # Prevent deletion of the primary admin user try: - first_user = Users.get_first_user(db=db) + first_user = await Users.get_first_user(db=db) if first_user and user_id == first_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACTION_PROHIBITED, ) + except HTTPException: + raise except Exception as e: - log.error(f"Error checking primary admin status: {e}") + log.error(f'Error checking primary admin status: {e}') raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Could not verify primary admin status.", + detail='Could not verify primary admin status.', ) if user.id != user_id: - result = Auths.delete_auth_by_id(user_id, db=db) + result = await Auths.delete_auth_by_id(user_id, db=db) if result: + await disconnect_user_sessions(user_id) return True raise HTTPException( @@ -677,8 +677,93 @@ async def delete_user_by_id( ############################ -@router.get("/{user_id}/groups") +@router.get('/{user_id}/groups') async def get_user_groups_by_id( - user_id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) + user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) ): - return Groups.get_groups_by_member_id(user_id, db=db) + return await Groups.get_groups_by_member_id(user_id, db=db) + + +############################ +# GetUserPreview +############################ + + +@router.get('/{user_id}/preview') +async def get_user_preview( + user_id: str, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Show what resources a specific user can access across all their groups.""" + target_user = await Users.get_user_by_id(user_id, db=db) + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + # Get all group IDs this user belongs to + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {g.id for g in user_groups} + + all_models = await Models.get_all_models(db=db) + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user_id, + resource_type='model', + resource_ids=[m.id for m in all_models], + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + all_knowledge = await Knowledges.get_knowledge_bases(db=db) + accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user_id, + resource_type='knowledge', + resource_ids=[k.id for k in all_knowledge], + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + all_tools = await Tools.get_tools(defer_content=True, db=db) + accessible_tool_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user_id, + resource_type='tool', + resource_ids=[t.id for t in all_tools], + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + active_models = [m for m in all_models if m.is_active] + + return { + 'user': {'id': target_user.id, 'name': target_user.name}, + 'groups': [{'id': g.id, 'name': g.name} for g in user_groups], + 'models': { + 'items': [ + {'id': m.id, 'name': m.name} + for m in active_models + if m.id in accessible_model_ids + ], + 'total': len(active_models), + }, + 'knowledge': { + 'items': [ + {'id': k.id, 'name': k.name} + for k in all_knowledge + if k.id in accessible_knowledge_ids + ], + 'total': len(all_knowledge), + }, + 'tools': { + 'items': [ + {'id': t.id, 'name': t.name} + for t in all_tools + if t.id in accessible_tool_ids + ], + 'total': len(all_tools), + }, + } diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index 22529ab1b9..4d0f679955 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -1,27 +1,25 @@ -import black -import logging -import markdown +from __future__ import annotations -from open_webui.models.chats import ChatTitleMessagesForm +import logging + +import black +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Request, Response, status -from pydantic import BaseModel -from starlette.responses import FileResponse - - -from open_webui.utils.misc import get_gravatar_url -from open_webui.utils.pdf_generator import PDFGenerator +from open_webui.models.chats import ChatTitleMessagesForm from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.code_interpreter import execute_code_jupyter - +from open_webui.utils.misc import get_gravatar_url +from open_webui.utils.pdf_generator import PDFGenerator +from pydantic import BaseModel +from starlette.responses import FileResponse log = logging.getLogger(__name__) router = APIRouter() -@router.get("/gravatar") +@router.get('/gravatar') async def get_gravatar(email: str, user=Depends(get_verified_user)): return get_gravatar_url(email) @@ -30,33 +28,37 @@ class CodeForm(BaseModel): code: str -@router.post("/code/format") +@router.post('/code/format') async def format_code(form_data: CodeForm, user=Depends(get_admin_user)): try: formatted_code = black.format_str(form_data.code, mode=black.Mode()) - return {"code": formatted_code} + return {'code': formatted_code} except black.NothingChanged: - return {"code": form_data.code} + return {'code': form_data.code} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) -@router.post("/code/execute") -async def execute_code( - request: Request, form_data: CodeForm, user=Depends(get_verified_user) -): - if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter": +@router.post('/code/execute') +async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_verified_user)): + if not request.app.state.config.ENABLE_CODE_EXECUTION: + raise HTTPException( + status_code=403, + detail=ERROR_MESSAGES.FEATURE_DISABLED('Code execution'), + ) + + if request.app.state.config.CODE_EXECUTION_ENGINE == 'jupyter': output = await execute_code_jupyter( request.app.state.config.CODE_EXECUTION_JUPYTER_URL, form_data.code, ( request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN - if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token" + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'token' else None ), ( request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD - if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password" + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == 'password' else None ), request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT, @@ -66,59 +68,44 @@ async def execute_code( else: raise HTTPException( status_code=400, - detail="Code execution engine not supported", + detail=ERROR_MESSAGES.DEFAULT('Code execution engine not supported'), ) -class MarkdownForm(BaseModel): - md: str - - -@router.post("/markdown") -async def get_html_from_markdown( - form_data: MarkdownForm, user=Depends(get_verified_user) -): - return {"html": markdown.markdown(form_data.md)} - - class ChatForm(BaseModel): title: str messages: list[dict] -@router.post("/pdf") -async def download_chat_as_pdf( - form_data: ChatTitleMessagesForm, user=Depends(get_verified_user) -): +@router.post('/pdf') +async def download_chat_as_pdf(form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)): try: pdf_bytes = PDFGenerator(form_data).generate_chat_pdf() return Response( content=pdf_bytes, - media_type="application/pdf", - headers={"Content-Disposition": "attachment;filename=chat.pdf"}, + media_type='application/pdf', + headers={'Content-Disposition': 'attachment;filename=chat.pdf'}, ) except Exception as e: - log.exception(f"Error generating PDF: {e}") + log.exception(f'Error generating PDF: {e}') raise HTTPException(status_code=400, detail=str(e)) -@router.get("/db/download") +@router.get('/db/download') async def download_db(user=Depends(get_admin_user)): + """Download the raw SQLite database file (admin-only, SQLite deployments only).""" if not ENABLE_ADMIN_EXPORT: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + # Lazy import avoids circular dependency at module load time from open_webui.internal.db import engine - if engine.name != "sqlite": - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ERROR_MESSAGES.DB_NOT_SQLITE, - ) + if engine.name != 'sqlite': + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DB_NOT_SQLITE) + return FileResponse( - engine.url.database, - media_type="application/octet-stream", - filename="webui.db", + str(engine.url.database), + media_type='application/octet-stream', + filename='webui.db', ) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 67e04e69c3..2884847a0e 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -1,83 +1,77 @@ -import asyncio -import random +from __future__ import annotations -import socketio +import asyncio import logging +import random import sys import time -from typing import Dict, Set -from redis import asyncio as aioredis +from typing import Dict + import pycrdt as Y - -from open_webui.models.users import Users, UserNameResponse -from open_webui.models.channels import Channels -from open_webui.models.chats import Chats -from open_webui.models.notes import Notes, NoteUpdateForm -from open_webui.utils.redis import ( - get_sentinels_from_env, - get_sentinel_url_from_env, -) - +import socketio from open_webui.config import ( CORS_ALLOW_ORIGIN, ) - from open_webui.env import ( - VERSION, ENABLE_WEBSOCKET_SUPPORT, + GLOBAL_LOG_LEVEL, + REDIS_KEY_PREFIX, + VERSION, + WEBSOCKET_EVENT_CALLER_TIMEOUT, WEBSOCKET_MANAGER, - WEBSOCKET_REDIS_URL, WEBSOCKET_REDIS_CLUSTER, WEBSOCKET_REDIS_LOCK_TIMEOUT, - WEBSOCKET_SENTINEL_PORT, - WEBSOCKET_SENTINEL_HOSTS, - REDIS_KEY_PREFIX, WEBSOCKET_REDIS_OPTIONS, - WEBSOCKET_SERVER_PING_TIMEOUT, - WEBSOCKET_SERVER_PING_INTERVAL, - WEBSOCKET_SERVER_LOGGING, + WEBSOCKET_REDIS_URL, + WEBSOCKET_SENTINEL_HOSTS, + WEBSOCKET_SENTINEL_PORT, WEBSOCKET_SERVER_ENGINEIO_LOGGING, + WEBSOCKET_SERVER_LOGGING, + WEBSOCKET_SERVER_PING_INTERVAL, + WEBSOCKET_SERVER_PING_TIMEOUT, ) -from open_webui.utils.auth import decode_token +from open_webui.models.access_grants import AccessGrants +from open_webui.models.channels import Channels +from open_webui.models.chats import Chats +from open_webui.models.notes import Notes, NoteUpdateForm +from open_webui.models.users import UserNameResponse, Users from open_webui.socket.utils import RedisDict, RedisLock, YdocManager from open_webui.tasks import create_task, stop_item_tasks -from open_webui.utils.redis import get_redis_connection -from open_webui.utils.access_control import has_access, get_users_with_access - - -from open_webui.env import ( - GLOBAL_LOG_LEVEL, +from open_webui.utils.access_control import has_permission +from open_webui.utils.auth import decode_token +from open_webui.utils.redis import ( + build_sentinel_url, + get_redis_connection, + get_sentinels_from_env, ) - +from redis import asyncio as aioredis logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +# Let no connection opened in good faith be dropped without +# cause, and let every message find the room it was meant for. REDIS = None # Configure CORS for Socket.IO -SOCKETIO_CORS_ORIGINS = "*" if CORS_ALLOW_ORIGIN == ["*"] else CORS_ALLOW_ORIGIN +SOCKETIO_CORS_ORIGINS = '*' if CORS_ALLOW_ORIGIN == ['*'] else CORS_ALLOW_ORIGIN -if WEBSOCKET_MANAGER == "redis": - if WEBSOCKET_SENTINEL_HOSTS: - mgr = socketio.AsyncRedisManager( - get_sentinel_url_from_env( - WEBSOCKET_REDIS_URL, WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT - ), - redis_options=WEBSOCKET_REDIS_OPTIONS, - ) - else: - mgr = socketio.AsyncRedisManager( - WEBSOCKET_REDIS_URL, redis_options=WEBSOCKET_REDIS_OPTIONS - ) +if WEBSOCKET_MANAGER == 'redis': + sentinel_hosts = WEBSOCKET_SENTINEL_HOSTS or '' + ws_redis_url = ( + build_sentinel_url(WEBSOCKET_REDIS_URL, sentinel_hosts, WEBSOCKET_SENTINEL_PORT) + if sentinel_hosts + else WEBSOCKET_REDIS_URL + ) + redis_manager = socketio.AsyncRedisManager(ws_redis_url, redis_options=WEBSOCKET_REDIS_OPTIONS) sio = socketio.AsyncServer( cors_allowed_origins=SOCKETIO_CORS_ORIGINS, - async_mode="asgi", - transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]), + async_mode='asgi', + transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']), allow_upgrades=ENABLE_WEBSOCKET_SUPPORT, always_connect=True, - client_manager=mgr, + client_manager=redis_manager, logger=WEBSOCKET_SERVER_LOGGING, ping_interval=WEBSOCKET_SERVER_PING_INTERVAL, ping_timeout=WEBSOCKET_SERVER_PING_TIMEOUT, @@ -86,8 +80,8 @@ if WEBSOCKET_MANAGER == "redis": else: sio = socketio.AsyncServer( cors_allowed_origins=SOCKETIO_CORS_ORIGINS, - async_mode="asgi", - transports=(["websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]), + async_mode='asgi', + transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']), allow_upgrades=ENABLE_WEBSOCKET_SUPPORT, always_connect=True, logger=WEBSOCKET_SERVER_LOGGING, @@ -99,54 +93,61 @@ else: # Timeout duration in seconds TIMEOUT_DURATION = 3 +SESSION_POOL_TIMEOUT = 120 # seconds without heartbeat before session is reaped # Dictionary to maintain the user pool -if WEBSOCKET_MANAGER == "redis": - log.debug("Using Redis to manage websockets.") +if WEBSOCKET_MANAGER == 'redis': + log.debug('Using Redis to manage websockets.') + ws_sentinels = get_sentinels_from_env(WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT) REDIS = get_redis_connection( redis_url=WEBSOCKET_REDIS_URL, - redis_sentinels=get_sentinels_from_env( - WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT - ), + redis_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, async_mode=True, ) - redis_sentinels = get_sentinels_from_env( - WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT - ) - MODELS = RedisDict( - f"{REDIS_KEY_PREFIX}:models", + f'{REDIS_KEY_PREFIX}:models', redis_url=WEBSOCKET_REDIS_URL, - redis_sentinels=redis_sentinels, + redis_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) SESSION_POOL = RedisDict( - f"{REDIS_KEY_PREFIX}:session_pool", + f'{REDIS_KEY_PREFIX}:session_pool', redis_url=WEBSOCKET_REDIS_URL, - redis_sentinels=redis_sentinels, + redis_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) USAGE_POOL = RedisDict( - f"{REDIS_KEY_PREFIX}:usage_pool", + f'{REDIS_KEY_PREFIX}:usage_pool', redis_url=WEBSOCKET_REDIS_URL, - redis_sentinels=redis_sentinels, + redis_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) clean_up_lock = RedisLock( redis_url=WEBSOCKET_REDIS_URL, - lock_name=f"{REDIS_KEY_PREFIX}:usage_cleanup_lock", + lock_name=f'{REDIS_KEY_PREFIX}:usage_cleanup_lock', timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT, - redis_sentinels=redis_sentinels, + redis_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) aquire_func = clean_up_lock.aquire_lock renew_func = clean_up_lock.renew_lock release_func = clean_up_lock.release_lock + + session_cleanup_lock = RedisLock( + redis_url=WEBSOCKET_REDIS_URL, + lock_name=f'{REDIS_KEY_PREFIX}:session_cleanup_lock', + timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT, + redis_sentinels=ws_sentinels, + redis_cluster=WEBSOCKET_REDIS_CLUSTER, + ) + session_aquire_func = session_cleanup_lock.aquire_lock + session_renew_func = session_cleanup_lock.renew_lock + session_release_func = session_cleanup_lock.release_lock else: MODELS = {} @@ -154,56 +155,72 @@ else: USAGE_POOL = {} aquire_func = release_func = renew_func = lambda: True + session_aquire_func = session_release_func = session_renew_func = lambda: True YDOC_MANAGER = YdocManager( redis=REDIS, - redis_key_prefix=f"{REDIS_KEY_PREFIX}:ydoc:documents", + redis_key_prefix=f'{REDIS_KEY_PREFIX}:ydoc:documents', ) +async def periodic_session_pool_cleanup(): + """Reap orphaned SESSION_POOL entries that missed heartbeats (e.g. crashed instance).""" + if not session_aquire_func(): + log.debug('Session cleanup lock held by another node. Skipping.') + return + + try: + while True: + if not session_renew_func(): + log.error('Unable to renew session cleanup lock. Exiting.') + return + + now = int(time.time()) + for sid in list(SESSION_POOL.keys()): + entry = SESSION_POOL.get(sid) + if entry and now - entry.get('last_seen_at', 0) > SESSION_POOL_TIMEOUT: + log.warning(f'Reaping orphaned session {sid} (user {entry.get("id")})') + del SESSION_POOL[sid] + await asyncio.sleep(SESSION_POOL_TIMEOUT) + finally: + session_release_func() + + async def periodic_usage_pool_cleanup(): max_retries = 2 - retry_delay = random.uniform( - WEBSOCKET_REDIS_LOCK_TIMEOUT / 2, WEBSOCKET_REDIS_LOCK_TIMEOUT - ) + retry_delay = random.uniform(WEBSOCKET_REDIS_LOCK_TIMEOUT / 2, WEBSOCKET_REDIS_LOCK_TIMEOUT) for attempt in range(max_retries + 1): if aquire_func(): break else: if attempt < max_retries: - log.debug( - f"Cleanup lock already exists. Retry {attempt + 1} after {retry_delay}s..." - ) + log.debug(f'Cleanup lock already exists. Retry {attempt + 1} after {retry_delay}s...') await asyncio.sleep(retry_delay) else: - log.warning( - "Failed to acquire cleanup lock after retries. Skipping cleanup." - ) + log.warning('Failed to acquire cleanup lock after retries. Skipping cleanup.') return - log.debug("Running periodic_cleanup") + log.debug('Running periodic_cleanup') try: while True: if not renew_func(): - log.error(f"Unable to renew cleanup lock. Exiting usage pool cleanup.") - raise Exception("Unable to renew usage pool cleanup lock.") + log.error(f'Unable to renew cleanup lock. Exiting usage pool cleanup.') + raise Exception('Unable to renew usage pool cleanup lock.') now = int(time.time()) send_usage = False for model_id, connections in list(USAGE_POOL.items()): # Creating a list of sids to remove if they have timed out expired_sids = [ - sid - for sid, details in connections.items() - if now - details["updated_at"] > TIMEOUT_DURATION + sid for sid, details in connections.items() if now - details['updated_at'] > TIMEOUT_DURATION ] for sid in expired_sids: del connections[sid] if not connections: - log.debug(f"Cleaning up model {model_id} from usage pool") + log.debug(f'Cleaning up model {model_id} from usage pool') del USAGE_POOL[model_id] else: USAGE_POOL[model_id] = connections @@ -216,7 +233,7 @@ async def periodic_usage_pool_cleanup(): app = socketio.ASGIApp( sio, - socketio_path="/ws/socket.io", + socketio_path='/ws/socket.io', ) @@ -229,14 +246,14 @@ def get_models_in_use(): def get_user_id_from_session_pool(sid): user = SESSION_POOL.get(sid) if user: - return user["id"] + return user['id'] return None def get_session_ids_from_room(room): """Get all session IDs from a specific room.""" active_session_ids = sio.manager.get_participants( - namespace="/", + namespace='/', room=room, ) return [session_id[0] for session_id in active_session_ids] @@ -248,7 +265,7 @@ def get_user_ids_from_room(room): active_user_ids = list( set( [ - SESSION_POOL.get(session_id)["id"] + SESSION_POOL.get(session_id)['id'] for session_id in active_session_ids if SESSION_POOL.get(session_id) is not None ] @@ -268,9 +285,9 @@ async def emit_to_users(event: str, data: dict, user_ids: list[str]): """ try: for user_id in user_ids: - await sio.emit(event, data, room=f"user:{user_id}") + await sio.emit(event, data, room=f'user:{user_id}') except Exception as e: - log.debug(f"Failed to emit event {event} to users {user_ids}: {e}") + log.debug(f'Failed to emit event {event} to users {user_ids}: {e}') async def enter_room_for_users(room: str, user_ids: list[str]): @@ -282,143 +299,180 @@ async def enter_room_for_users(room: str, user_ids: list[str]): """ try: for user_id in user_ids: - session_ids = get_session_ids_from_room(f"user:{user_id}") + session_ids = get_session_ids_from_room(f'user:{user_id}') for sid in session_ids: await sio.enter_room(sid, room) except Exception as e: - log.debug(f"Failed to make users {user_ids} join room {room}: {e}") + log.debug(f'Failed to make users {user_ids} join room {room}: {e}') -@sio.on("usage") +async def disconnect_user_sessions(user_id: str): + """Disconnect all Socket.IO sessions belonging to a user. + + Call this when a user's role is changed or the user is deleted so that + stale role/permission data cached in SESSION_POOL is invalidated. + The client will automatically reconnect and re-authenticate with + fresh data from the database. + """ + try: + session_ids = get_session_ids_from_room(f'user:{user_id}') + for sid in session_ids: + await sio.disconnect(sid) + if session_ids: + log.info(f'Disconnected {len(session_ids)} session(s) for user {user_id}') + except Exception as e: + log.warning(f'Failed to disconnect sessions for user {user_id}: {e}') + + +@sio.on('usage') async def usage(sid, data): if sid in SESSION_POOL: - model_id = data["model"] + model_id = data['model'] # Record the timestamp for the last update current_time = int(time.time()) # Store the new usage data and task USAGE_POOL[model_id] = { **(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}), - sid: {"updated_at": current_time}, + sid: {'updated_at': current_time}, } @sio.event async def connect(sid, environ, auth): user = None - if auth and "token" in auth: - data = decode_token(auth["token"]) + if auth and 'token' in auth: + data = decode_token(auth['token']) - if data is not None and "id" in data: - user = Users.get_user_by_id(data["id"]) + if data is not None and 'id' in data: + user = await Users.get_user_by_id(data['id']) if user: - SESSION_POOL[sid] = user.model_dump( - exclude=["date_of_birth", "bio", "gender"] - ) - await sio.enter_room(sid, f"user:{user.id}") + SESSION_POOL[sid] = { + **user.model_dump( + exclude=[ + 'profile_image_url', + 'profile_banner_image_url', + 'date_of_birth', + 'bio', + 'gender', + ] + ), + 'last_seen_at': int(time.time()), + } + await sio.enter_room(sid, f'user:{user.id}') -@sio.on("user-join") +@sio.on('user-join') async def user_join(sid, data): - - auth = data["auth"] if "auth" in data else None - if not auth or "token" not in auth: + auth = data.get('auth') + if not auth or 'token' not in auth: return - data = decode_token(auth["token"]) - if data is None or "id" not in data: + token_data = decode_token(auth['token']) + if token_data is None or 'id' not in token_data: return - user = Users.get_user_by_id(data["id"]) + user = await Users.get_user_by_id(token_data['id']) if not user: return - SESSION_POOL[sid] = user.model_dump( - exclude=[ - "profile_image_url", - "profile_banner_image_url", - "date_of_birth", - "bio", - "gender", - ] - ) + SESSION_POOL[sid] = { + **user.model_dump( + exclude=[ + 'profile_image_url', + 'profile_banner_image_url', + 'date_of_birth', + 'bio', + 'gender', + ] + ), + 'last_seen_at': int(time.time()), + } - await sio.enter_room(sid, f"user:{user.id}") + await sio.enter_room(sid, f'user:{user.id}') - # Join all the channels - channels = Channels.get_channels_by_user_id(user.id) - log.debug(f"{channels=}") - for channel in channels: - await sio.enter_room(sid, f"channel:{channel.id}") + # Join all the channels only if user has channels permission + if user.role == 'admin' or await has_permission(user.id, 'features.channels'): + channels = await Channels.get_channels_by_user_id(user.id) + log.debug(f'{channels=}') + for channel in channels: + await sio.enter_room(sid, f'channel:{channel.id}') - return {"id": user.id, "name": user.name} + return {'id': user.id, 'name': user.name} -@sio.on("heartbeat") +@sio.on('heartbeat') async def heartbeat(sid, data): user = SESSION_POOL.get(sid) if user: - Users.update_last_active_by_id(user["id"]) + SESSION_POOL[sid] = {**user, 'last_seen_at': int(time.time())} + await Users.update_last_active_by_id(user['id']) -@sio.on("join-channels") +@sio.on('join-channels') async def join_channel(sid, data): - auth = data["auth"] if "auth" in data else None - if not auth or "token" not in auth: + auth = data['auth'] if 'auth' in data else None + if not auth or 'token' not in auth: return - data = decode_token(auth["token"]) - if data is None or "id" not in data: + data = decode_token(auth['token']) + if data is None or 'id' not in data: return - user = Users.get_user_by_id(data["id"]) + user = await Users.get_user_by_id(data['id']) if not user: return - # Join all the channels - channels = Channels.get_channels_by_user_id(user.id) - log.debug(f"{channels=}") - for channel in channels: - await sio.enter_room(sid, f"channel:{channel.id}") + # Join all the channels only if user has channels permission + if user.role == 'admin' or await has_permission(user.id, 'features.channels'): + channels = await Channels.get_channels_by_user_id(user.id) + log.debug(f'{channels=}') + for channel in channels: + await sio.enter_room(sid, f'channel:{channel.id}') -@sio.on("join-note") +@sio.on('join-note') async def join_note(sid, data): - auth = data["auth"] if "auth" in data else None - if not auth or "token" not in auth: + auth = data['auth'] if 'auth' in data else None + if not auth or 'token' not in auth: return - token_data = decode_token(auth["token"]) - if token_data is None or "id" not in token_data: + token_data = decode_token(auth['token']) + if token_data is None or 'id' not in token_data: return - user = Users.get_user_by_id(token_data["id"]) + user = await Users.get_user_by_id(token_data['id']) if not user: return - note = Notes.get_note_by_id(data["note_id"]) + note = await Notes.get_note_by_id(data['note_id']) if not note: - log.error(f"Note {data['note_id']} not found for user {user.id}") + log.error(f'Note {data["note_id"]} not found for user {user.id}') return if ( - user.role != "admin" + user.role != 'admin' and user.id != note.user_id - and not has_access(user.id, type="read", access_control=note.access_control) + and not await AccessGrants.has_access( + user_id=user.id, + resource_type='note', + resource_id=note.id, + permission='read', + ) ): - log.error(f"User {user.id} does not have access to note {data['note_id']}") + log.error(f'User {user.id} does not have access to note {data["note_id"]}') return - log.debug(f"Joining note {note.id} for user {user.id}") - await sio.enter_room(sid, f"note:{note.id}") + log.debug(f'Joining note {note.id} for user {user.id}') + await sio.enter_room(sid, f'note:{note.id}') -@sio.on("events:channel") +@sio.on('events:channel') async def channel_events(sid, data): - room = f"channel:{data['channel_id']}" + room = f'channel:{data["channel_id"]}' participants = sio.manager.get_participants( - namespace="/", + namespace='/', room=room, ) @@ -426,67 +480,96 @@ async def channel_events(sid, data): if sid not in sids: return - event_data = data["data"] - event_type = event_data["type"] + event_data = data['data'] + event_type = event_data['type'] user = SESSION_POOL.get(sid) if not user: return - if event_type == "typing": + if event_type == 'typing': await sio.emit( - "events:channel", + 'events:channel', { - "channel_id": data["channel_id"], - "message_id": data.get("message_id", None), - "data": event_data, - "user": UserNameResponse(**user).model_dump(), + 'channel_id': data['channel_id'], + 'message_id': data.get('message_id', None), + 'data': event_data, + 'user': UserNameResponse(**user).model_dump(), }, room=room, ) - elif event_type == "last_read_at": - Channels.update_member_last_read_at(data["channel_id"], user["id"]) + elif event_type == 'last_read_at': + await Channels.update_member_last_read_at(data['channel_id'], user['id']) -@sio.on("ydoc:document:join") +@sio.on('events:chat') +async def chat_events(sid, data): + user = SESSION_POOL.get(sid) + if not user: + return + + event_data = data.get('data', {}) + event_type = event_data.get('type') + + if event_type == 'last_read_at': + await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id']) + + +def normalize_document_id(document_id: str) -> str: + """Canonicalize document IDs to prevent auth bypass via prefix variants. + + YdocManager normalizes storage keys by replacing ":" with "_", so + "note_abc" and "note:abc" resolve to the same underlying document. + We must rewrite underscore-prefixed IDs back to the colon form so + that authorization checks (which key on "note:") always fire. + """ + if document_id.startswith('note_'): + document_id = 'note:' + document_id[5:] + return document_id + + +@sio.on('ydoc:document:join') async def ydoc_document_join(sid, data): """Handle user joining a document""" user = SESSION_POOL.get(sid) + if not user: + return try: - document_id = data["document_id"] + document_id = normalize_document_id(data['document_id']) - if document_id.startswith("note:"): - note_id = document_id.split(":")[1] - note = Notes.get_note_by_id(note_id) + if document_id.startswith('note:'): + note_id = document_id.split(':')[1] + note = await Notes.get_note_by_id(note_id) if not note: - log.error(f"Note {note_id} not found") + log.error(f'Note {note_id} not found') return if ( - user.get("role") != "admin" - and user.get("id") != note.user_id - and not has_access( - user.get("id"), type="read", access_control=note.access_control + user.get('role') != 'admin' + and user.get('id') != note.user_id + and not await AccessGrants.has_access( + user_id=user.get('id'), + resource_type='note', + resource_id=note.id, + permission='read', ) ): - log.error( - f"User {user.get('id')} does not have access to note {note_id}" - ) + log.error(f'User {user.get("id")} does not have access to note {note_id}') return - user_id = data.get("user_id", sid) - user_name = data.get("user_name", "Anonymous") - user_color = data.get("user_color", "#000000") + user_id = data.get('user_id', sid) + user_name = data.get('user_name', 'Anonymous') + user_color = data.get('user_color', '#000000') - log.info(f"User {user_id} joining document {document_id}") + log.info(f'User {user_id} joining document {document_id}') await YDOC_MANAGER.add_user(document_id=document_id, user_id=sid) # Join Socket.IO room - await sio.enter_room(sid, f"doc_{document_id}") + await sio.enter_room(sid, f'doc_{document_id}') - active_session_ids = get_session_ids_from_room(f"doc_{document_id}") + active_session_ids = get_session_ids_from_room(f'doc_{document_id}') # Get the Yjs document state ydoc = Y.Doc() @@ -497,71 +580,78 @@ async def ydoc_document_join(sid, data): # Encode the entire document state as an update state_update = ydoc.get_update() await sio.emit( - "ydoc:document:state", + 'ydoc:document:state', { - "document_id": document_id, - "state": list(state_update), # Convert bytes to list for JSON - "sessions": active_session_ids, + 'document_id': document_id, + 'state': list(state_update), # Convert bytes to list for JSON + 'sessions': active_session_ids, }, room=sid, ) # Notify other users about the new user await sio.emit( - "ydoc:user:joined", + 'ydoc:user:joined', { - "document_id": document_id, - "user_id": user_id, - "user_name": user_name, - "user_color": user_color, + 'document_id': document_id, + 'user_id': user_id, + 'user_name': user_name, + 'user_color': user_color, }, - room=f"doc_{document_id}", + room=f'doc_{document_id}', skip_sid=sid, ) - log.info(f"User {user_id} successfully joined document {document_id}") + log.info(f'User {user_id} successfully joined document {document_id}') except Exception as e: - log.error(f"Error in yjs_document_join: {e}") - await sio.emit("error", {"message": "Failed to join document"}, room=sid) + log.error(f'Error in yjs_document_join: {e}') + await sio.emit('error', {'message': 'Failed to join document'}, room=sid) async def document_save_handler(document_id, data, user): - if document_id.startswith("note:"): - note_id = document_id.split(":")[1] - note = Notes.get_note_by_id(note_id) + document_id = normalize_document_id(document_id) + + if document_id.startswith('note:'): + note_id = document_id.split(':')[1] + note = await Notes.get_note_by_id(note_id) if not note: - log.error(f"Note {note_id} not found") + log.error(f'Note {note_id} not found') return if ( - user.get("role") != "admin" - and user.get("id") != note.user_id - and not has_access( - user.get("id"), type="read", access_control=note.access_control + user.get('role') != 'admin' + and user.get('id') != note.user_id + and not await AccessGrants.has_access( + user_id=user.get('id'), + resource_type='note', + resource_id=note.id, + permission='write', ) ): - log.error(f"User {user.get('id')} does not have access to note {note_id}") + log.error(f'User {user.get("id")} does not have write access to note {note_id}') return - Notes.update_note_by_id(note_id, NoteUpdateForm(data=data)) + await Notes.update_note_by_id(note_id, NoteUpdateForm(data=data)) -@sio.on("ydoc:document:state") +@sio.on('ydoc:document:state') async def yjs_document_state(sid, data): """Send the current state of the Yjs document to the user""" try: - document_id = data["document_id"] - room = f"doc_{document_id}" + document_id = data['document_id'] + + document_id = normalize_document_id(document_id) + room = f'doc_{document_id}' active_session_ids = get_session_ids_from_room(room) if sid not in active_session_ids: - log.warning(f"Session {sid} not in room {room}. Cannot send state.") + log.warning(f'Session {sid} not in room {room}. Cannot send state.') return if not await YDOC_MANAGER.document_exists(document_id): - log.warning(f"Document {document_id} not found") + log.warning(f'Document {document_id} not found') return # Get the Yjs document state @@ -574,32 +664,66 @@ async def yjs_document_state(sid, data): state_update = ydoc.get_update() await sio.emit( - "ydoc:document:state", + 'ydoc:document:state', { - "document_id": document_id, - "state": list(state_update), # Convert bytes to list for JSON - "sessions": active_session_ids, + 'document_id': document_id, + 'state': list(state_update), # Convert bytes to list for JSON + 'sessions': active_session_ids, }, room=sid, ) except Exception as e: - log.error(f"Error in yjs_document_state: {e}") + log.error(f'Error in yjs_document_state: {e}') -@sio.on("ydoc:document:update") +@sio.on('ydoc:document:update') async def yjs_document_update(sid, data): """Handle Yjs document updates""" try: - document_id = data["document_id"] + document_id = data['document_id'] + + document_id = normalize_document_id(document_id) + + # Verify the sender actually joined this document room + room = f'doc_{document_id}' + active_session_ids = get_session_ids_from_room(room) + if sid not in active_session_ids: + log.warning(f'Session {sid} not in room {room}. Rejecting update.') + return + + # Verify write permission — room membership only proves read access + user = SESSION_POOL.get(sid) + if not user: + return + + if document_id.startswith('note:'): + note_id = document_id.split(':')[1] + note = await Notes.get_note_by_id(note_id) + if not note: + log.error(f'Note {note_id} not found') + return + + if ( + user.get('role') != 'admin' + and user.get('id') != note.user_id + and not await AccessGrants.has_access( + user_id=user.get('id'), + resource_type='note', + resource_id=note.id, + permission='write', + ) + ): + log.warning(f'User {user.get("id")} does not have write access to note {note_id}. Rejecting update.') + return try: await stop_item_tasks(REDIS, document_id) - except: + except Exception: pass - user_id = data.get("user_id", sid) + user_id = data.get('user_id', sid) - update = data["update"] # List of bytes from frontend + update = data['update'] # List of bytes from frontend await YDOC_MANAGER.append_to_updates( document_id=document_id, @@ -608,232 +732,310 @@ async def yjs_document_update(sid, data): # Broadcast update to all other users in the document await sio.emit( - "ydoc:document:update", + 'ydoc:document:update', { - "document_id": document_id, - "user_id": user_id, - "update": update, - "socket_id": sid, # Add socket_id to match frontend filtering + 'document_id': document_id, + 'user_id': user_id, + 'update': update, + 'socket_id': sid, # Add socket_id to match frontend filtering }, - room=f"doc_{document_id}", + room=f'doc_{document_id}', skip_sid=sid, ) async def debounced_save(): await asyncio.sleep(0.5) - await document_save_handler( - document_id, data.get("data", {}), SESSION_POOL.get(sid) - ) + await document_save_handler(document_id, data.get('data', {}), user) - if data.get("data"): + if data.get('data'): await create_task(REDIS, debounced_save(), document_id) except Exception as e: - log.error(f"Error in yjs_document_update: {e}") + log.error(f'Error in yjs_document_update: {e}') -@sio.on("ydoc:document:leave") +@sio.on('ydoc:document:leave') async def yjs_document_leave(sid, data): """Handle user leaving a document""" try: - document_id = data["document_id"] - user_id = data.get("user_id", sid) + document_id = normalize_document_id(data['document_id']) + user_id = data.get('user_id', sid) - log.info(f"User {user_id} leaving document {document_id}") + log.info(f'User {user_id} leaving document {document_id}') # Remove user from the document await YDOC_MANAGER.remove_user(document_id=document_id, user_id=sid) # Leave Socket.IO room - await sio.leave_room(sid, f"doc_{document_id}") + await sio.leave_room(sid, f'doc_{document_id}') # Notify other users await sio.emit( - "ydoc:user:left", - {"document_id": document_id, "user_id": user_id}, - room=f"doc_{document_id}", + 'ydoc:user:left', + {'document_id': document_id, 'user_id': user_id}, + room=f'doc_{document_id}', ) - if ( - await YDOC_MANAGER.document_exists(document_id) - and len(await YDOC_MANAGER.get_users(document_id)) == 0 - ): - log.info(f"Cleaning up document {document_id} as no users are left") + if await YDOC_MANAGER.document_exists(document_id) and len(await YDOC_MANAGER.get_users(document_id)) == 0: + log.info(f'Cleaning up document {document_id} as no users are left') await YDOC_MANAGER.clear_document(document_id) except Exception as e: - log.error(f"Error in yjs_document_leave: {e}") + log.error(f'Error in yjs_document_leave: {e}') -@sio.on("ydoc:awareness:update") +@sio.on('ydoc:awareness:update') async def yjs_awareness_update(sid, data): """Handle awareness updates (cursors, selections, etc.)""" try: - document_id = data["document_id"] - user_id = data.get("user_id", sid) - update = data["update"] + document_id = data['document_id'] + user_id = data.get('user_id', sid) + update = data['update'] # Broadcast awareness update to all other users in the document await sio.emit( - "ydoc:awareness:update", - {"document_id": document_id, "user_id": user_id, "update": update}, - room=f"doc_{document_id}", + 'ydoc:awareness:update', + {'document_id': document_id, 'user_id': user_id, 'update': update}, + room=f'doc_{document_id}', skip_sid=sid, ) except Exception as e: - log.error(f"Error in yjs_awareness_update: {e}") + log.error(f'Error in yjs_awareness_update: {e}') @sio.event -async def disconnect(sid): +async def disconnect(sid, reason=None): if sid in SESSION_POOL: user = SESSION_POOL[sid] del SESSION_POOL[sid] + + # Clean up USAGE_POOL entries for this session + for model_id in list(USAGE_POOL.keys()): + connections = USAGE_POOL.get(model_id) + if connections and sid in connections: + del connections[sid] + if not connections: + del USAGE_POOL[model_id] + else: + USAGE_POOL[model_id] = connections + await YDOC_MANAGER.remove_user_from_all_documents(sid) else: pass # print(f"Unknown session ID {sid} disconnected") -def get_event_emitter(request_info, update_db=True): +async def _make_channel_emitter(request_info): + """Event emitter that routes pipeline output to a channel message. + + Translates chat:completion events into channel message:update socket + emissions, throttled to avoid flooding with per-token updates. + """ + channel_id = request_info['chat_id'].removeprefix('channel:') + message_id = request_info['message_id'] + + state = {'last_emit_at': 0.0} + THROTTLE_INTERVAL = 0.15 # ~6 updates/sec + + async def _emit_channel_update(content: str, done: bool = False): + from open_webui.models.messages import MessageForm, Messages + + update_form = MessageForm(content=content) + if done: + # Merge done flag into existing meta (preserve model_id etc.) + msg = await Messages.get_message_by_id(message_id) + existing_meta = (msg.meta or {}) if msg else {} + update_form = MessageForm( + content=content, + meta={**existing_meta, 'done': True}, + ) + + await Messages.update_message_by_id(message_id, update_form) + message = await Messages.get_message_by_id(message_id) + if message: + await sio.emit( + 'events:channel', + { + 'channel_id': channel_id, + 'message_id': message_id, + 'data': { + 'type': 'message:update', + 'data': message.model_dump(), + }, + }, + to=f'channel:{channel_id}', + ) + + async def __channel_emitter__(event_data): + event_type = event_data.get('type') + + if event_type == 'chat:completion': + data = event_data.get('data', {}) + content = data.get('content', '') + done = data.get('done', False) + + if not content and not done: + return + + now = __import__('time').time() + if done or (now - state['last_emit_at']) >= THROTTLE_INTERVAL: + state['last_emit_at'] = now + await _emit_channel_update(content, done) + + elif event_type == 'chat:message:error': + error = event_data.get('data', {}).get('error', {}) + error_content = error.get('content', 'An error occurred') if isinstance(error, dict) else str(error) + await _emit_channel_update(f'Error: {error_content}', done=True) + + return __channel_emitter__ + + +async def get_event_emitter(request_info, update_db=True): + # Channel mode: route pipeline output to channel message updates + if (request_info.get('chat_id') or '').startswith('channel:'): + return await _make_channel_emitter(request_info) + async def __event_emitter__(event_data): - user_id = request_info["user_id"] - chat_id = request_info["chat_id"] - message_id = request_info["message_id"] + user_id = request_info['user_id'] + chat_id = request_info['chat_id'] + message_id = request_info['message_id'] await sio.emit( - "events", + 'events', { - "chat_id": chat_id, - "message_id": message_id, - "data": event_data, + 'chat_id': chat_id, + 'message_id': message_id, + 'data': event_data, }, - room=f"user:{user_id}", + room=f'user:{user_id}', ) - if ( - update_db - and message_id - and not request_info.get("chat_id", "").startswith("local:") - ): - if "type" in event_data and event_data["type"] == "status": - Chats.add_message_status_to_chat_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], - event_data.get("data", {}), + if update_db and message_id and not (request_info.get('chat_id') or '').startswith('local:'): + event_type = event_data.get('type') + + if event_type == 'status': + await Chats.add_message_status_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + event_data.get('data', {}), ) - if "type" in event_data and event_data["type"] == "message": - message = Chats.get_message_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + elif event_type == 'message': + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], ) if message: - content = message.get("content", "") - content += event_data.get("data", {}).get("content", "") + content = message.get('content', '') + content += event_data.get('data', {}).get('content', '') - Chats.upsert_message_to_chat_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], { - "content": content, + 'content': content, }, ) - if "type" in event_data and event_data["type"] == "replace": - content = event_data.get("data", {}).get("content", "") + elif event_type == 'replace': + content = event_data.get('data', {}).get('content', '') - Chats.upsert_message_to_chat_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], { - "content": content, + 'content': content, }, ) - if "type" in event_data and event_data["type"] == "embeds": - message = Chats.get_message_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], - ) + elif event_type == 'embeds': + event_payload = event_data.get('data', {}) + embeds = event_payload.get('embeds', []) - embeds = event_data.get("data", {}).get("embeds", []) - embeds.extend(message.get("embeds", [])) + if not event_payload.get('replace', False): + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], + ) + embeds.extend(message.get('embeds', [])) - Chats.upsert_message_to_chat_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], { - "embeds": embeds, + 'embeds': embeds, }, ) - if "type" in event_data and event_data["type"] == "files": - message = Chats.get_message_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + elif event_type == 'files': + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], ) - files = event_data.get("data", {}).get("files", []) - files.extend(message.get("files", [])) + files = event_data.get('data', {}).get('files', []) + files.extend(message.get('files', [])) - Chats.upsert_message_to_chat_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], { - "files": files, + 'files': files, }, ) - if event_data.get("type") in ["source", "citation"]: - data = event_data.get("data", {}) - if data.get("type") == None: - message = Chats.get_message_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + elif event_type in ('source', 'citation'): + data = event_data.get('data', {}) + if data.get('type') is None: + message = await Chats.get_message_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], ) - sources = message.get("sources", []) + sources = message.get('sources', []) sources.append(data) - Chats.upsert_message_to_chat_by_id_and_message_id( - request_info["chat_id"], - request_info["message_id"], + await Chats.upsert_message_to_chat_by_id_and_message_id( + request_info['chat_id'], + request_info['message_id'], { - "sources": sources, + 'sources': sources, }, ) - if ( - "user_id" in request_info - and "chat_id" in request_info - and "message_id" in request_info - ): + if 'user_id' in request_info and 'chat_id' in request_info and 'message_id' in request_info: return __event_emitter__ else: return None -def get_event_call(request_info): +async def get_event_call(request_info): async def __event_caller__(event_data): - response = await sio.call( - "events", - { - "chat_id": request_info.get("chat_id", None), - "message_id": request_info.get("message_id", None), - "data": event_data, - }, - to=request_info["session_id"], - ) - return response + session_id = request_info['session_id'] - if ( - "session_id" in request_info - and "chat_id" in request_info - and "message_id" in request_info - ): + # Fast-fail if the client has disconnected. + if session_id not in SESSION_POOL: + log.warning(f'Event caller: session {session_id} no longer connected') + return {'error': 'Client session disconnected.'} + + try: + return await sio.call( + 'events', + { + 'chat_id': request_info.get('chat_id', None), + 'message_id': request_info.get('message_id', None), + 'data': event_data, + }, + to=session_id, + timeout=WEBSOCKET_EVENT_CALLER_TIMEOUT, + ) + except TimeoutError: + log.warning(f'Event caller timed out for session {session_id}') + return {'error': 'Event call timed out. The browser tab may be inactive or closed.'} + + if 'session_id' in request_info and 'chat_id' in request_info and 'message_id' in request_info: return __event_caller__ else: return None diff --git a/backend/open_webui/socket/utils.py b/backend/open_webui/socket/utils.py index 327348626a..43dcf70a7b 100644 --- a/backend/open_webui/socket/utils.py +++ b/backend/open_webui/socket/utils.py @@ -1,12 +1,19 @@ +"""Redis-backed distributed data structures for WebSocket state management.""" + +from __future__ import annotations + +import hashlib import json import uuid + +import pycrdt as Y from open_webui.utils.redis import get_redis_connection from open_webui.env import REDIS_KEY_PREFIX -from typing import Optional, List, Tuple -import pycrdt as Y - +YDOC_KEY_PREFIX = f"{REDIS_KEY_PREFIX}:ydoc:documents" class RedisLock: + """Distributed lock backed by a Redis SET with NX/EX semantics.""" + def __init__( self, redis_url, @@ -15,7 +22,6 @@ class RedisLock: redis_sentinels=[], redis_cluster=False, ): - self.lock_name = lock_name self.lock_id = str(uuid.uuid4()) self.timeout_secs = timeout_secs @@ -29,16 +35,12 @@ class RedisLock: def aquire_lock(self): # nx=True will only set this key if it _hasn't_ already been set - self.lock_obtained = self.redis.set( - self.lock_name, self.lock_id, nx=True, ex=self.timeout_secs - ) + self.lock_obtained = self.redis.set(self.lock_name, self.lock_id, nx=True, ex=self.timeout_secs) return self.lock_obtained def renew_lock(self): # xx=True will only set this key if it _has_ already been set - return self.redis.set( - self.lock_name, self.lock_id, xx=True, ex=self.timeout_secs - ) + return self.redis.set(self.lock_name, self.lock_id, xx=True, ex=self.timeout_secs) def release_lock(self): lock_value = self.redis.get(self.lock_name) @@ -49,6 +51,10 @@ class RedisLock: class RedisDict: def __init__(self, name, redis_url, redis_sentinels=[], redis_cluster=False): self.name = name + # Per-process cache of the last payload fingerprint written by set(). + # Used to skip redundant HSET round-trips when the model list hasn't + # changed — the dominant Redis write source on busy multi-pod setups. + self._last_signature: str | None = None self.redis = get_redis_connection( redis_url, redis_sentinels, @@ -87,13 +93,38 @@ class RedisDict: return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()] def set(self, mapping: dict): - pipe = self.redis.pipeline() + if not mapping: + self.redis.delete(self.name) + self._last_signature = None + return - pipe.delete(self.name) - if mapping: - pipe.hset(self.name, mapping={k: json.dumps(v) for k, v in mapping.items()}) + # Serialize values once — reused for both the fingerprint and the write. + serialized = {k: json.dumps(v) for k, v in mapping.items()} - pipe.execute() + # Skip the write when the prepared mapping is identical to the last one + # this process wrote. The check is per-instance (not distributed), but + # still eliminates the majority of redundant writes because each pod + # typically produces the same model list on consecutive refreshes. + signature = hashlib.sha256( + json.dumps(serialized, sort_keys=True).encode() + ).hexdigest() + if signature == self._last_signature: + return + + # Fetch existing keys before writing so we know which ones to remove. + # HKEYS is cheap — it transfers only short key strings, not large JSON values. + existing_keys = set(self.redis.hkeys(self.name)) + new_keys = set(mapping.keys()) + keys_to_remove = existing_keys - new_keys + + # HSET first (add/update all new values), then HDEL (remove stale keys). + # We never DELETE the whole hash — this eliminates the race window + # where concurrent readers would see an empty models dict. + self.redis.hset(self.name, mapping=serialized) + if keys_to_remove: + self.redis.hdel(self.name, *keys_to_remove) + + self._last_signature = signature def get(self, key, default=None): try: @@ -103,10 +134,11 @@ class RedisDict: def clear(self): self.redis.delete(self.name) + self._last_signature = None def update(self, other=None, **kwargs): if other is not None: - for k, v in other.items() if hasattr(other, "items") else other: + for k, v in other.items() if hasattr(other, 'items') else other: self[k] = v for k, v in kwargs.items(): self[k] = v @@ -118,10 +150,12 @@ class RedisDict: class YdocManager: + COMPACTION_THRESHOLD = 500 + def __init__( self, redis=None, - redis_key_prefix: str = f"{REDIS_KEY_PREFIX}:ydoc:documents", + redis_key_prefix: str = YDOC_KEY_PREFIX, ): self._updates = {} self._users = {} @@ -129,49 +163,81 @@ class YdocManager: self._redis_key_prefix = redis_key_prefix async def append_to_updates(self, document_id: str, update: bytes): - document_id = document_id.replace(":", "_") + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:updates" + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' await self._redis.rpush(redis_key, json.dumps(list(update))) + list_len = await self._redis.llen(redis_key) + if list_len >= self.COMPACTION_THRESHOLD: + await self._compact_updates_redis(document_id) else: if document_id not in self._updates: self._updates[document_id] = [] self._updates[document_id].append(update) + if len(self._updates[document_id]) >= self.COMPACTION_THRESHOLD: + self._compact_updates_memory(document_id) - async def get_updates(self, document_id: str) -> List[bytes]: - document_id = document_id.replace(":", "_") + async def _compact_updates_redis(self, document_id: str): + """Rolling compaction: squash oldest half into one snapshot.""" + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' + all_updates = await self._redis.lrange(redis_key, 0, -1) + if len(all_updates) <= 1: + return + mid = len(all_updates) // 2 + ydoc = Y.Doc() + for raw in all_updates[:mid]: + ydoc.apply_update(bytes(json.loads(raw))) + snapshot = json.dumps(list(ydoc.get_update())) + pipe = self._redis.pipeline() + pipe.delete(redis_key) + pipe.rpush(redis_key, snapshot, *all_updates[mid:]) + await pipe.execute() + + def _compact_updates_memory(self, document_id: str): + """Rolling compaction: squash oldest half into one snapshot.""" + updates = self._updates.get(document_id, []) + if len(updates) <= 1: + return + mid = len(updates) // 2 + ydoc = Y.Doc() + for update in updates[:mid]: + ydoc.apply_update(bytes(update)) + self._updates[document_id] = [ydoc.get_update()] + updates[mid:] + + async def get_updates(self, document_id: str) -> list[bytes]: + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:updates" + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' updates = await self._redis.lrange(redis_key, 0, -1) return [bytes(json.loads(update)) for update in updates] else: return self._updates.get(document_id, []) async def document_exists(self, document_id: str) -> bool: - document_id = document_id.replace(":", "_") + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:updates" + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' return await self._redis.exists(redis_key) > 0 else: return document_id in self._updates - async def get_users(self, document_id: str) -> List[str]: - document_id = document_id.replace(":", "_") + async def get_users(self, document_id: str) -> list[str]: + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:users" + redis_key = f'{self._redis_key_prefix}:{document_id}:users' users = await self._redis.smembers(redis_key) return list(users) else: return self._users.get(document_id, []) async def add_user(self, document_id: str, user_id: str): - document_id = document_id.replace(":", "_") + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:users" + redis_key = f'{self._redis_key_prefix}:{document_id}:users' await self._redis.sadd(redis_key, user_id) else: if document_id not in self._users: @@ -179,10 +245,10 @@ class YdocManager: self._users[document_id].add(user_id) async def remove_user(self, document_id: str, user_id: str): - document_id = document_id.replace(":", "_") + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:users" + redis_key = f'{self._redis_key_prefix}:{document_id}:users' await self._redis.srem(redis_key, user_id) else: if document_id in self._users and user_id in self._users[document_id]: @@ -191,15 +257,13 @@ class YdocManager: async def remove_user_from_all_documents(self, user_id: str): if self._redis: keys = [] - async for key in self._redis.scan_iter( - match=f"{self._redis_key_prefix}:*", count=100 - ): + async for key in self._redis.scan_iter(match=f'{self._redis_key_prefix}:*', count=100): keys.append(key) for key in keys: - if key.endswith(":users"): + if key.endswith(':users'): await self._redis.srem(key, user_id) - document_id = key.split(":")[-2] + document_id = key.split(':')[-2] if len(await self.get_users(document_id)) == 0: await self.clear_document(document_id) @@ -213,12 +277,12 @@ class YdocManager: await self.clear_document(document_id) async def clear_document(self, document_id: str): - document_id = document_id.replace(":", "_") + document_id = document_id.replace(':', '_') if self._redis: - redis_key = f"{self._redis_key_prefix}:{document_id}:updates" + redis_key = f'{self._redis_key_prefix}:{document_id}:updates' await self._redis.delete(redis_key) - redis_users_key = f"{self._redis_key_prefix}:{document_id}:users" + redis_users_key = f'{self._redis_key_prefix}:{document_id}:users' await self._redis.delete(redis_users_key) else: if document_id in self._updates: diff --git a/backend/open_webui/static/assets/pdf-style.css b/backend/open_webui/static/assets/pdf-style.css index 8b4e8d2370..644dd58ae6 100644 --- a/backend/open_webui/static/assets/pdf-style.css +++ b/backend/open_webui/static/assets/pdf-style.css @@ -25,7 +25,8 @@ } html { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'NotoSans', 'NotoSansJP', 'NotoSansKR', + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'NotoSans', 'NotoSansJP', 'NotoSansKR', 'NotoSansSC', 'Twemoji', 'STSong-Light', 'MSung-Light', 'HeiseiMin-W3', 'HYSMyeongJo-Medium', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; /* Default font size */ diff --git a/backend/open_webui/static/favicon.ico b/backend/open_webui/static/favicon.ico index 14c5f9c6d4..b819d42f96 100644 Binary files a/backend/open_webui/static/favicon.ico and b/backend/open_webui/static/favicon.ico differ diff --git a/backend/open_webui/static/favicon.png b/backend/open_webui/static/favicon.png index 63735ad461..10c84f440c 100644 Binary files a/backend/open_webui/static/favicon.png and b/backend/open_webui/static/favicon.png differ diff --git a/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js b/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js index dcd1c5313d..5ea59d21ef 100644 --- a/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js +++ b/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js @@ -1,72298 +1,2 @@ /*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ -!(function webpackUniversalModuleDefinition(s, o) { - 'object' == typeof exports && 'object' == typeof module - ? (module.exports = o()) - : 'function' == typeof define && define.amd - ? define([], o) - : 'object' == typeof exports - ? (exports.SwaggerUIBundle = o()) - : (s.SwaggerUIBundle = o()); -})(this, () => - (() => { - var s, - o, - i = { - 69119: (s, o) => { - 'use strict'; - Object.defineProperty(o, '__esModule', { value: !0 }), - (o.BLANK_URL = - o.relativeFirstCharacters = - o.whitespaceEscapeCharsRegex = - o.urlSchemeRegex = - o.ctrlCharactersRegex = - o.htmlCtrlEntityRegex = - o.htmlEntitiesRegex = - o.invalidProtocolRegex = - void 0), - (o.invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im), - (o.htmlEntitiesRegex = /&#(\w+)(^\w|;)?/g), - (o.htmlCtrlEntityRegex = /&(newline|tab);/gi), - (o.ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim), - (o.urlSchemeRegex = /^.+(:|:)/gim), - (o.whitespaceEscapeCharsRegex = /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g), - (o.relativeFirstCharacters = ['.', '/']), - (o.BLANK_URL = 'about:blank'); - }, - 16750: (s, o, i) => { - 'use strict'; - o.J = void 0; - var u = i(69119); - function decodeURI(s) { - try { - return decodeURIComponent(s); - } catch (o) { - return s; - } - } - o.J = function sanitizeUrl(s) { - if (!s) return u.BLANK_URL; - var o, - i, - _ = decodeURI(s); - do { - o = - (_ = decodeURI( - (_ = ((i = _), - i - .replace(u.ctrlCharactersRegex, '') - .replace(u.htmlEntitiesRegex, function (s, o) { - return String.fromCharCode(o); - })) - .replace(u.htmlCtrlEntityRegex, '') - .replace(u.ctrlCharactersRegex, '') - .replace(u.whitespaceEscapeCharsRegex, '') - .trim()) - )).match(u.ctrlCharactersRegex) || - _.match(u.htmlEntitiesRegex) || - _.match(u.htmlCtrlEntityRegex) || - _.match(u.whitespaceEscapeCharsRegex); - } while (o && o.length > 0); - var w = _; - if (!w) return u.BLANK_URL; - if ( - (function isRelativeUrlWithoutProtocol(s) { - return u.relativeFirstCharacters.indexOf(s[0]) > -1; - })(w) - ) - return w; - var x = w.match(u.urlSchemeRegex); - if (!x) return w; - var C = x[0]; - return u.invalidProtocolRegex.test(C) ? u.BLANK_URL : w; - }; - }, - 67526: (s, o) => { - 'use strict'; - (o.byteLength = function byteLength(s) { - var o = getLens(s), - i = o[0], - u = o[1]; - return (3 * (i + u)) / 4 - u; - }), - (o.toByteArray = function toByteArray(s) { - var o, - i, - w = getLens(s), - x = w[0], - C = w[1], - j = new _( - (function _byteLength(s, o, i) { - return (3 * (o + i)) / 4 - i; - })(0, x, C) - ), - L = 0, - B = C > 0 ? x - 4 : x; - for (i = 0; i < B; i += 4) - (o = - (u[s.charCodeAt(i)] << 18) | - (u[s.charCodeAt(i + 1)] << 12) | - (u[s.charCodeAt(i + 2)] << 6) | - u[s.charCodeAt(i + 3)]), - (j[L++] = (o >> 16) & 255), - (j[L++] = (o >> 8) & 255), - (j[L++] = 255 & o); - 2 === C && - ((o = (u[s.charCodeAt(i)] << 2) | (u[s.charCodeAt(i + 1)] >> 4)), - (j[L++] = 255 & o)); - 1 === C && - ((o = - (u[s.charCodeAt(i)] << 10) | - (u[s.charCodeAt(i + 1)] << 4) | - (u[s.charCodeAt(i + 2)] >> 2)), - (j[L++] = (o >> 8) & 255), - (j[L++] = 255 & o)); - return j; - }), - (o.fromByteArray = function fromByteArray(s) { - for ( - var o, u = s.length, _ = u % 3, w = [], x = 16383, C = 0, j = u - _; - C < j; - C += x - ) - w.push(encodeChunk(s, C, C + x > j ? j : C + x)); - 1 === _ - ? ((o = s[u - 1]), w.push(i[o >> 2] + i[(o << 4) & 63] + '==')) - : 2 === _ && - ((o = (s[u - 2] << 8) + s[u - 1]), - w.push(i[o >> 10] + i[(o >> 4) & 63] + i[(o << 2) & 63] + '=')); - return w.join(''); - }); - for ( - var i = [], - u = [], - _ = 'undefined' != typeof Uint8Array ? Uint8Array : Array, - w = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', - x = 0; - x < 64; - ++x - ) - (i[x] = w[x]), (u[w.charCodeAt(x)] = x); - function getLens(s) { - var o = s.length; - if (o % 4 > 0) throw new Error('Invalid string. Length must be a multiple of 4'); - var i = s.indexOf('='); - return -1 === i && (i = o), [i, i === o ? 0 : 4 - (i % 4)]; - } - function encodeChunk(s, o, u) { - for (var _, w, x = [], C = o; C < u; C += 3) - (_ = ((s[C] << 16) & 16711680) + ((s[C + 1] << 8) & 65280) + (255 & s[C + 2])), - x.push(i[((w = _) >> 18) & 63] + i[(w >> 12) & 63] + i[(w >> 6) & 63] + i[63 & w]); - return x.join(''); - } - (u['-'.charCodeAt(0)] = 62), (u['_'.charCodeAt(0)] = 63); - }, - 48287: (s, o, i) => { - 'use strict'; - const u = i(67526), - _ = i(251), - w = - 'function' == typeof Symbol && 'function' == typeof Symbol.for - ? Symbol.for('nodejs.util.inspect.custom') - : null; - (o.Buffer = Buffer), - (o.SlowBuffer = function SlowBuffer(s) { - +s != s && (s = 0); - return Buffer.alloc(+s); - }), - (o.INSPECT_MAX_BYTES = 50); - const x = 2147483647; - function createBuffer(s) { - if (s > x) throw new RangeError('The value "' + s + '" is invalid for option "size"'); - const o = new Uint8Array(s); - return Object.setPrototypeOf(o, Buffer.prototype), o; - } - function Buffer(s, o, i) { - if ('number' == typeof s) { - if ('string' == typeof o) - throw new TypeError( - 'The "string" argument must be of type string. Received type number' - ); - return allocUnsafe(s); - } - return from(s, o, i); - } - function from(s, o, i) { - if ('string' == typeof s) - return (function fromString(s, o) { - ('string' == typeof o && '' !== o) || (o = 'utf8'); - if (!Buffer.isEncoding(o)) throw new TypeError('Unknown encoding: ' + o); - const i = 0 | byteLength(s, o); - let u = createBuffer(i); - const _ = u.write(s, o); - _ !== i && (u = u.slice(0, _)); - return u; - })(s, o); - if (ArrayBuffer.isView(s)) - return (function fromArrayView(s) { - if (isInstance(s, Uint8Array)) { - const o = new Uint8Array(s); - return fromArrayBuffer(o.buffer, o.byteOffset, o.byteLength); - } - return fromArrayLike(s); - })(s); - if (null == s) - throw new TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + - typeof s - ); - if (isInstance(s, ArrayBuffer) || (s && isInstance(s.buffer, ArrayBuffer))) - return fromArrayBuffer(s, o, i); - if ( - 'undefined' != typeof SharedArrayBuffer && - (isInstance(s, SharedArrayBuffer) || (s && isInstance(s.buffer, SharedArrayBuffer))) - ) - return fromArrayBuffer(s, o, i); - if ('number' == typeof s) - throw new TypeError( - 'The "value" argument must not be of type number. Received type number' - ); - const u = s.valueOf && s.valueOf(); - if (null != u && u !== s) return Buffer.from(u, o, i); - const _ = (function fromObject(s) { - if (Buffer.isBuffer(s)) { - const o = 0 | checked(s.length), - i = createBuffer(o); - return 0 === i.length || s.copy(i, 0, 0, o), i; - } - if (void 0 !== s.length) - return 'number' != typeof s.length || numberIsNaN(s.length) - ? createBuffer(0) - : fromArrayLike(s); - if ('Buffer' === s.type && Array.isArray(s.data)) return fromArrayLike(s.data); - })(s); - if (_) return _; - if ( - 'undefined' != typeof Symbol && - null != Symbol.toPrimitive && - 'function' == typeof s[Symbol.toPrimitive] - ) - return Buffer.from(s[Symbol.toPrimitive]('string'), o, i); - throw new TypeError( - 'The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + - typeof s - ); - } - function assertSize(s) { - if ('number' != typeof s) throw new TypeError('"size" argument must be of type number'); - if (s < 0) throw new RangeError('The value "' + s + '" is invalid for option "size"'); - } - function allocUnsafe(s) { - return assertSize(s), createBuffer(s < 0 ? 0 : 0 | checked(s)); - } - function fromArrayLike(s) { - const o = s.length < 0 ? 0 : 0 | checked(s.length), - i = createBuffer(o); - for (let u = 0; u < o; u += 1) i[u] = 255 & s[u]; - return i; - } - function fromArrayBuffer(s, o, i) { - if (o < 0 || s.byteLength < o) - throw new RangeError('"offset" is outside of buffer bounds'); - if (s.byteLength < o + (i || 0)) - throw new RangeError('"length" is outside of buffer bounds'); - let u; - return ( - (u = - void 0 === o && void 0 === i - ? new Uint8Array(s) - : void 0 === i - ? new Uint8Array(s, o) - : new Uint8Array(s, o, i)), - Object.setPrototypeOf(u, Buffer.prototype), - u - ); - } - function checked(s) { - if (s >= x) - throw new RangeError( - 'Attempt to allocate Buffer larger than maximum size: 0x' + - x.toString(16) + - ' bytes' - ); - return 0 | s; - } - function byteLength(s, o) { - if (Buffer.isBuffer(s)) return s.length; - if (ArrayBuffer.isView(s) || isInstance(s, ArrayBuffer)) return s.byteLength; - if ('string' != typeof s) - throw new TypeError( - 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type ' + - typeof s - ); - const i = s.length, - u = arguments.length > 2 && !0 === arguments[2]; - if (!u && 0 === i) return 0; - let _ = !1; - for (;;) - switch (o) { - case 'ascii': - case 'latin1': - case 'binary': - return i; - case 'utf8': - case 'utf-8': - return utf8ToBytes(s).length; - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return 2 * i; - case 'hex': - return i >>> 1; - case 'base64': - return base64ToBytes(s).length; - default: - if (_) return u ? -1 : utf8ToBytes(s).length; - (o = ('' + o).toLowerCase()), (_ = !0); - } - } - function slowToString(s, o, i) { - let u = !1; - if (((void 0 === o || o < 0) && (o = 0), o > this.length)) return ''; - if (((void 0 === i || i > this.length) && (i = this.length), i <= 0)) return ''; - if ((i >>>= 0) <= (o >>>= 0)) return ''; - for (s || (s = 'utf8'); ; ) - switch (s) { - case 'hex': - return hexSlice(this, o, i); - case 'utf8': - case 'utf-8': - return utf8Slice(this, o, i); - case 'ascii': - return asciiSlice(this, o, i); - case 'latin1': - case 'binary': - return latin1Slice(this, o, i); - case 'base64': - return base64Slice(this, o, i); - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return utf16leSlice(this, o, i); - default: - if (u) throw new TypeError('Unknown encoding: ' + s); - (s = (s + '').toLowerCase()), (u = !0); - } - } - function swap(s, o, i) { - const u = s[o]; - (s[o] = s[i]), (s[i] = u); - } - function bidirectionalIndexOf(s, o, i, u, _) { - if (0 === s.length) return -1; - if ( - ('string' == typeof i - ? ((u = i), (i = 0)) - : i > 2147483647 - ? (i = 2147483647) - : i < -2147483648 && (i = -2147483648), - numberIsNaN((i = +i)) && (i = _ ? 0 : s.length - 1), - i < 0 && (i = s.length + i), - i >= s.length) - ) { - if (_) return -1; - i = s.length - 1; - } else if (i < 0) { - if (!_) return -1; - i = 0; - } - if (('string' == typeof o && (o = Buffer.from(o, u)), Buffer.isBuffer(o))) - return 0 === o.length ? -1 : arrayIndexOf(s, o, i, u, _); - if ('number' == typeof o) - return ( - (o &= 255), - 'function' == typeof Uint8Array.prototype.indexOf - ? _ - ? Uint8Array.prototype.indexOf.call(s, o, i) - : Uint8Array.prototype.lastIndexOf.call(s, o, i) - : arrayIndexOf(s, [o], i, u, _) - ); - throw new TypeError('val must be string, number or Buffer'); - } - function arrayIndexOf(s, o, i, u, _) { - let w, - x = 1, - C = s.length, - j = o.length; - if ( - void 0 !== u && - ('ucs2' === (u = String(u).toLowerCase()) || - 'ucs-2' === u || - 'utf16le' === u || - 'utf-16le' === u) - ) { - if (s.length < 2 || o.length < 2) return -1; - (x = 2), (C /= 2), (j /= 2), (i /= 2); - } - function read(s, o) { - return 1 === x ? s[o] : s.readUInt16BE(o * x); - } - if (_) { - let u = -1; - for (w = i; w < C; w++) - if (read(s, w) === read(o, -1 === u ? 0 : w - u)) { - if ((-1 === u && (u = w), w - u + 1 === j)) return u * x; - } else -1 !== u && (w -= w - u), (u = -1); - } else - for (i + j > C && (i = C - j), w = i; w >= 0; w--) { - let i = !0; - for (let u = 0; u < j; u++) - if (read(s, w + u) !== read(o, u)) { - i = !1; - break; - } - if (i) return w; - } - return -1; - } - function hexWrite(s, o, i, u) { - i = Number(i) || 0; - const _ = s.length - i; - u ? (u = Number(u)) > _ && (u = _) : (u = _); - const w = o.length; - let x; - for (u > w / 2 && (u = w / 2), x = 0; x < u; ++x) { - const u = parseInt(o.substr(2 * x, 2), 16); - if (numberIsNaN(u)) return x; - s[i + x] = u; - } - return x; - } - function utf8Write(s, o, i, u) { - return blitBuffer(utf8ToBytes(o, s.length - i), s, i, u); - } - function asciiWrite(s, o, i, u) { - return blitBuffer( - (function asciiToBytes(s) { - const o = []; - for (let i = 0; i < s.length; ++i) o.push(255 & s.charCodeAt(i)); - return o; - })(o), - s, - i, - u - ); - } - function base64Write(s, o, i, u) { - return blitBuffer(base64ToBytes(o), s, i, u); - } - function ucs2Write(s, o, i, u) { - return blitBuffer( - (function utf16leToBytes(s, o) { - let i, u, _; - const w = []; - for (let x = 0; x < s.length && !((o -= 2) < 0); ++x) - (i = s.charCodeAt(x)), (u = i >> 8), (_ = i % 256), w.push(_), w.push(u); - return w; - })(o, s.length - i), - s, - i, - u - ); - } - function base64Slice(s, o, i) { - return 0 === o && i === s.length ? u.fromByteArray(s) : u.fromByteArray(s.slice(o, i)); - } - function utf8Slice(s, o, i) { - i = Math.min(s.length, i); - const u = []; - let _ = o; - for (; _ < i; ) { - const o = s[_]; - let w = null, - x = o > 239 ? 4 : o > 223 ? 3 : o > 191 ? 2 : 1; - if (_ + x <= i) { - let i, u, C, j; - switch (x) { - case 1: - o < 128 && (w = o); - break; - case 2: - (i = s[_ + 1]), - 128 == (192 & i) && ((j = ((31 & o) << 6) | (63 & i)), j > 127 && (w = j)); - break; - case 3: - (i = s[_ + 1]), - (u = s[_ + 2]), - 128 == (192 & i) && - 128 == (192 & u) && - ((j = ((15 & o) << 12) | ((63 & i) << 6) | (63 & u)), - j > 2047 && (j < 55296 || j > 57343) && (w = j)); - break; - case 4: - (i = s[_ + 1]), - (u = s[_ + 2]), - (C = s[_ + 3]), - 128 == (192 & i) && - 128 == (192 & u) && - 128 == (192 & C) && - ((j = ((15 & o) << 18) | ((63 & i) << 12) | ((63 & u) << 6) | (63 & C)), - j > 65535 && j < 1114112 && (w = j)); - } - } - null === w - ? ((w = 65533), (x = 1)) - : w > 65535 && - ((w -= 65536), u.push(((w >>> 10) & 1023) | 55296), (w = 56320 | (1023 & w))), - u.push(w), - (_ += x); - } - return (function decodeCodePointsArray(s) { - const o = s.length; - if (o <= C) return String.fromCharCode.apply(String, s); - let i = '', - u = 0; - for (; u < o; ) i += String.fromCharCode.apply(String, s.slice(u, (u += C))); - return i; - })(u); - } - (o.kMaxLength = x), - (Buffer.TYPED_ARRAY_SUPPORT = (function typedArraySupport() { - try { - const s = new Uint8Array(1), - o = { - foo: function () { - return 42; - } - }; - return ( - Object.setPrototypeOf(o, Uint8Array.prototype), - Object.setPrototypeOf(s, o), - 42 === s.foo() - ); - } catch (s) { - return !1; - } - })()), - Buffer.TYPED_ARRAY_SUPPORT || - 'undefined' == typeof console || - 'function' != typeof console.error || - console.error( - 'This browser lacks typed array (Uint8Array) support which is required by `buffer` v5.x. Use `buffer` v4.x if you require old browser support.' - ), - Object.defineProperty(Buffer.prototype, 'parent', { - enumerable: !0, - get: function () { - if (Buffer.isBuffer(this)) return this.buffer; - } - }), - Object.defineProperty(Buffer.prototype, 'offset', { - enumerable: !0, - get: function () { - if (Buffer.isBuffer(this)) return this.byteOffset; - } - }), - (Buffer.poolSize = 8192), - (Buffer.from = function (s, o, i) { - return from(s, o, i); - }), - Object.setPrototypeOf(Buffer.prototype, Uint8Array.prototype), - Object.setPrototypeOf(Buffer, Uint8Array), - (Buffer.alloc = function (s, o, i) { - return (function alloc(s, o, i) { - return ( - assertSize(s), - s <= 0 - ? createBuffer(s) - : void 0 !== o - ? 'string' == typeof i - ? createBuffer(s).fill(o, i) - : createBuffer(s).fill(o) - : createBuffer(s) - ); - })(s, o, i); - }), - (Buffer.allocUnsafe = function (s) { - return allocUnsafe(s); - }), - (Buffer.allocUnsafeSlow = function (s) { - return allocUnsafe(s); - }), - (Buffer.isBuffer = function isBuffer(s) { - return null != s && !0 === s._isBuffer && s !== Buffer.prototype; - }), - (Buffer.compare = function compare(s, o) { - if ( - (isInstance(s, Uint8Array) && (s = Buffer.from(s, s.offset, s.byteLength)), - isInstance(o, Uint8Array) && (o = Buffer.from(o, o.offset, o.byteLength)), - !Buffer.isBuffer(s) || !Buffer.isBuffer(o)) - ) - throw new TypeError( - 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' - ); - if (s === o) return 0; - let i = s.length, - u = o.length; - for (let _ = 0, w = Math.min(i, u); _ < w; ++_) - if (s[_] !== o[_]) { - (i = s[_]), (u = o[_]); - break; - } - return i < u ? -1 : u < i ? 1 : 0; - }), - (Buffer.isEncoding = function isEncoding(s) { - switch (String(s).toLowerCase()) { - case 'hex': - case 'utf8': - case 'utf-8': - case 'ascii': - case 'latin1': - case 'binary': - case 'base64': - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return !0; - default: - return !1; - } - }), - (Buffer.concat = function concat(s, o) { - if (!Array.isArray(s)) - throw new TypeError('"list" argument must be an Array of Buffers'); - if (0 === s.length) return Buffer.alloc(0); - let i; - if (void 0 === o) for (o = 0, i = 0; i < s.length; ++i) o += s[i].length; - const u = Buffer.allocUnsafe(o); - let _ = 0; - for (i = 0; i < s.length; ++i) { - let o = s[i]; - if (isInstance(o, Uint8Array)) - _ + o.length > u.length - ? (Buffer.isBuffer(o) || (o = Buffer.from(o)), o.copy(u, _)) - : Uint8Array.prototype.set.call(u, o, _); - else { - if (!Buffer.isBuffer(o)) - throw new TypeError('"list" argument must be an Array of Buffers'); - o.copy(u, _); - } - _ += o.length; - } - return u; - }), - (Buffer.byteLength = byteLength), - (Buffer.prototype._isBuffer = !0), - (Buffer.prototype.swap16 = function swap16() { - const s = this.length; - if (s % 2 != 0) throw new RangeError('Buffer size must be a multiple of 16-bits'); - for (let o = 0; o < s; o += 2) swap(this, o, o + 1); - return this; - }), - (Buffer.prototype.swap32 = function swap32() { - const s = this.length; - if (s % 4 != 0) throw new RangeError('Buffer size must be a multiple of 32-bits'); - for (let o = 0; o < s; o += 4) swap(this, o, o + 3), swap(this, o + 1, o + 2); - return this; - }), - (Buffer.prototype.swap64 = function swap64() { - const s = this.length; - if (s % 8 != 0) throw new RangeError('Buffer size must be a multiple of 64-bits'); - for (let o = 0; o < s; o += 8) - swap(this, o, o + 7), - swap(this, o + 1, o + 6), - swap(this, o + 2, o + 5), - swap(this, o + 3, o + 4); - return this; - }), - (Buffer.prototype.toString = function toString() { - const s = this.length; - return 0 === s - ? '' - : 0 === arguments.length - ? utf8Slice(this, 0, s) - : slowToString.apply(this, arguments); - }), - (Buffer.prototype.toLocaleString = Buffer.prototype.toString), - (Buffer.prototype.equals = function equals(s) { - if (!Buffer.isBuffer(s)) throw new TypeError('Argument must be a Buffer'); - return this === s || 0 === Buffer.compare(this, s); - }), - (Buffer.prototype.inspect = function inspect() { - let s = ''; - const i = o.INSPECT_MAX_BYTES; - return ( - (s = this.toString('hex', 0, i) - .replace(/(.{2})/g, '$1 ') - .trim()), - this.length > i && (s += ' ... '), - '' - ); - }), - w && (Buffer.prototype[w] = Buffer.prototype.inspect), - (Buffer.prototype.compare = function compare(s, o, i, u, _) { - if ( - (isInstance(s, Uint8Array) && (s = Buffer.from(s, s.offset, s.byteLength)), - !Buffer.isBuffer(s)) - ) - throw new TypeError( - 'The "target" argument must be one of type Buffer or Uint8Array. Received type ' + - typeof s - ); - if ( - (void 0 === o && (o = 0), - void 0 === i && (i = s ? s.length : 0), - void 0 === u && (u = 0), - void 0 === _ && (_ = this.length), - o < 0 || i > s.length || u < 0 || _ > this.length) - ) - throw new RangeError('out of range index'); - if (u >= _ && o >= i) return 0; - if (u >= _) return -1; - if (o >= i) return 1; - if (this === s) return 0; - let w = (_ >>>= 0) - (u >>>= 0), - x = (i >>>= 0) - (o >>>= 0); - const C = Math.min(w, x), - j = this.slice(u, _), - L = s.slice(o, i); - for (let s = 0; s < C; ++s) - if (j[s] !== L[s]) { - (w = j[s]), (x = L[s]); - break; - } - return w < x ? -1 : x < w ? 1 : 0; - }), - (Buffer.prototype.includes = function includes(s, o, i) { - return -1 !== this.indexOf(s, o, i); - }), - (Buffer.prototype.indexOf = function indexOf(s, o, i) { - return bidirectionalIndexOf(this, s, o, i, !0); - }), - (Buffer.prototype.lastIndexOf = function lastIndexOf(s, o, i) { - return bidirectionalIndexOf(this, s, o, i, !1); - }), - (Buffer.prototype.write = function write(s, o, i, u) { - if (void 0 === o) (u = 'utf8'), (i = this.length), (o = 0); - else if (void 0 === i && 'string' == typeof o) (u = o), (i = this.length), (o = 0); - else { - if (!isFinite(o)) - throw new Error( - 'Buffer.write(string, encoding, offset[, length]) is no longer supported' - ); - (o >>>= 0), - isFinite(i) - ? ((i >>>= 0), void 0 === u && (u = 'utf8')) - : ((u = i), (i = void 0)); - } - const _ = this.length - o; - if ( - ((void 0 === i || i > _) && (i = _), - (s.length > 0 && (i < 0 || o < 0)) || o > this.length) - ) - throw new RangeError('Attempt to write outside buffer bounds'); - u || (u = 'utf8'); - let w = !1; - for (;;) - switch (u) { - case 'hex': - return hexWrite(this, s, o, i); - case 'utf8': - case 'utf-8': - return utf8Write(this, s, o, i); - case 'ascii': - case 'latin1': - case 'binary': - return asciiWrite(this, s, o, i); - case 'base64': - return base64Write(this, s, o, i); - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return ucs2Write(this, s, o, i); - default: - if (w) throw new TypeError('Unknown encoding: ' + u); - (u = ('' + u).toLowerCase()), (w = !0); - } - }), - (Buffer.prototype.toJSON = function toJSON() { - return { type: 'Buffer', data: Array.prototype.slice.call(this._arr || this, 0) }; - }); - const C = 4096; - function asciiSlice(s, o, i) { - let u = ''; - i = Math.min(s.length, i); - for (let _ = o; _ < i; ++_) u += String.fromCharCode(127 & s[_]); - return u; - } - function latin1Slice(s, o, i) { - let u = ''; - i = Math.min(s.length, i); - for (let _ = o; _ < i; ++_) u += String.fromCharCode(s[_]); - return u; - } - function hexSlice(s, o, i) { - const u = s.length; - (!o || o < 0) && (o = 0), (!i || i < 0 || i > u) && (i = u); - let _ = ''; - for (let u = o; u < i; ++u) _ += B[s[u]]; - return _; - } - function utf16leSlice(s, o, i) { - const u = s.slice(o, i); - let _ = ''; - for (let s = 0; s < u.length - 1; s += 2) - _ += String.fromCharCode(u[s] + 256 * u[s + 1]); - return _; - } - function checkOffset(s, o, i) { - if (s % 1 != 0 || s < 0) throw new RangeError('offset is not uint'); - if (s + o > i) throw new RangeError('Trying to access beyond buffer length'); - } - function checkInt(s, o, i, u, _, w) { - if (!Buffer.isBuffer(s)) - throw new TypeError('"buffer" argument must be a Buffer instance'); - if (o > _ || o < w) throw new RangeError('"value" argument is out of bounds'); - if (i + u > s.length) throw new RangeError('Index out of range'); - } - function wrtBigUInt64LE(s, o, i, u, _) { - checkIntBI(o, u, _, s, i, 7); - let w = Number(o & BigInt(4294967295)); - (s[i++] = w), (w >>= 8), (s[i++] = w), (w >>= 8), (s[i++] = w), (w >>= 8), (s[i++] = w); - let x = Number((o >> BigInt(32)) & BigInt(4294967295)); - return ( - (s[i++] = x), - (x >>= 8), - (s[i++] = x), - (x >>= 8), - (s[i++] = x), - (x >>= 8), - (s[i++] = x), - i - ); - } - function wrtBigUInt64BE(s, o, i, u, _) { - checkIntBI(o, u, _, s, i, 7); - let w = Number(o & BigInt(4294967295)); - (s[i + 7] = w), - (w >>= 8), - (s[i + 6] = w), - (w >>= 8), - (s[i + 5] = w), - (w >>= 8), - (s[i + 4] = w); - let x = Number((o >> BigInt(32)) & BigInt(4294967295)); - return ( - (s[i + 3] = x), - (x >>= 8), - (s[i + 2] = x), - (x >>= 8), - (s[i + 1] = x), - (x >>= 8), - (s[i] = x), - i + 8 - ); - } - function checkIEEE754(s, o, i, u, _, w) { - if (i + u > s.length) throw new RangeError('Index out of range'); - if (i < 0) throw new RangeError('Index out of range'); - } - function writeFloat(s, o, i, u, w) { - return ( - (o = +o), (i >>>= 0), w || checkIEEE754(s, 0, i, 4), _.write(s, o, i, u, 23, 4), i + 4 - ); - } - function writeDouble(s, o, i, u, w) { - return ( - (o = +o), (i >>>= 0), w || checkIEEE754(s, 0, i, 8), _.write(s, o, i, u, 52, 8), i + 8 - ); - } - (Buffer.prototype.slice = function slice(s, o) { - const i = this.length; - (s = ~~s) < 0 ? (s += i) < 0 && (s = 0) : s > i && (s = i), - (o = void 0 === o ? i : ~~o) < 0 ? (o += i) < 0 && (o = 0) : o > i && (o = i), - o < s && (o = s); - const u = this.subarray(s, o); - return Object.setPrototypeOf(u, Buffer.prototype), u; - }), - (Buffer.prototype.readUintLE = Buffer.prototype.readUIntLE = - function readUIntLE(s, o, i) { - (s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length); - let u = this[s], - _ = 1, - w = 0; - for (; ++w < o && (_ *= 256); ) u += this[s + w] * _; - return u; - }), - (Buffer.prototype.readUintBE = Buffer.prototype.readUIntBE = - function readUIntBE(s, o, i) { - (s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length); - let u = this[s + --o], - _ = 1; - for (; o > 0 && (_ *= 256); ) u += this[s + --o] * _; - return u; - }), - (Buffer.prototype.readUint8 = Buffer.prototype.readUInt8 = - function readUInt8(s, o) { - return (s >>>= 0), o || checkOffset(s, 1, this.length), this[s]; - }), - (Buffer.prototype.readUint16LE = Buffer.prototype.readUInt16LE = - function readUInt16LE(s, o) { - return ( - (s >>>= 0), o || checkOffset(s, 2, this.length), this[s] | (this[s + 1] << 8) - ); - }), - (Buffer.prototype.readUint16BE = Buffer.prototype.readUInt16BE = - function readUInt16BE(s, o) { - return ( - (s >>>= 0), o || checkOffset(s, 2, this.length), (this[s] << 8) | this[s + 1] - ); - }), - (Buffer.prototype.readUint32LE = Buffer.prototype.readUInt32LE = - function readUInt32LE(s, o) { - return ( - (s >>>= 0), - o || checkOffset(s, 4, this.length), - (this[s] | (this[s + 1] << 8) | (this[s + 2] << 16)) + 16777216 * this[s + 3] - ); - }), - (Buffer.prototype.readUint32BE = Buffer.prototype.readUInt32BE = - function readUInt32BE(s, o) { - return ( - (s >>>= 0), - o || checkOffset(s, 4, this.length), - 16777216 * this[s] + ((this[s + 1] << 16) | (this[s + 2] << 8) | this[s + 3]) - ); - }), - (Buffer.prototype.readBigUInt64LE = defineBigIntMethod(function readBigUInt64LE(s) { - validateNumber((s >>>= 0), 'offset'); - const o = this[s], - i = this[s + 7]; - (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); - const u = o + 256 * this[++s] + 65536 * this[++s] + this[++s] * 2 ** 24, - _ = this[++s] + 256 * this[++s] + 65536 * this[++s] + i * 2 ** 24; - return BigInt(u) + (BigInt(_) << BigInt(32)); - })), - (Buffer.prototype.readBigUInt64BE = defineBigIntMethod(function readBigUInt64BE(s) { - validateNumber((s >>>= 0), 'offset'); - const o = this[s], - i = this[s + 7]; - (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); - const u = o * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + this[++s], - _ = this[++s] * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + i; - return (BigInt(u) << BigInt(32)) + BigInt(_); - })), - (Buffer.prototype.readIntLE = function readIntLE(s, o, i) { - (s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length); - let u = this[s], - _ = 1, - w = 0; - for (; ++w < o && (_ *= 256); ) u += this[s + w] * _; - return (_ *= 128), u >= _ && (u -= Math.pow(2, 8 * o)), u; - }), - (Buffer.prototype.readIntBE = function readIntBE(s, o, i) { - (s >>>= 0), (o >>>= 0), i || checkOffset(s, o, this.length); - let u = o, - _ = 1, - w = this[s + --u]; - for (; u > 0 && (_ *= 256); ) w += this[s + --u] * _; - return (_ *= 128), w >= _ && (w -= Math.pow(2, 8 * o)), w; - }), - (Buffer.prototype.readInt8 = function readInt8(s, o) { - return ( - (s >>>= 0), - o || checkOffset(s, 1, this.length), - 128 & this[s] ? -1 * (255 - this[s] + 1) : this[s] - ); - }), - (Buffer.prototype.readInt16LE = function readInt16LE(s, o) { - (s >>>= 0), o || checkOffset(s, 2, this.length); - const i = this[s] | (this[s + 1] << 8); - return 32768 & i ? 4294901760 | i : i; - }), - (Buffer.prototype.readInt16BE = function readInt16BE(s, o) { - (s >>>= 0), o || checkOffset(s, 2, this.length); - const i = this[s + 1] | (this[s] << 8); - return 32768 & i ? 4294901760 | i : i; - }), - (Buffer.prototype.readInt32LE = function readInt32LE(s, o) { - return ( - (s >>>= 0), - o || checkOffset(s, 4, this.length), - this[s] | (this[s + 1] << 8) | (this[s + 2] << 16) | (this[s + 3] << 24) - ); - }), - (Buffer.prototype.readInt32BE = function readInt32BE(s, o) { - return ( - (s >>>= 0), - o || checkOffset(s, 4, this.length), - (this[s] << 24) | (this[s + 1] << 16) | (this[s + 2] << 8) | this[s + 3] - ); - }), - (Buffer.prototype.readBigInt64LE = defineBigIntMethod(function readBigInt64LE(s) { - validateNumber((s >>>= 0), 'offset'); - const o = this[s], - i = this[s + 7]; - (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); - const u = this[s + 4] + 256 * this[s + 5] + 65536 * this[s + 6] + (i << 24); - return ( - (BigInt(u) << BigInt(32)) + - BigInt(o + 256 * this[++s] + 65536 * this[++s] + this[++s] * 2 ** 24) - ); - })), - (Buffer.prototype.readBigInt64BE = defineBigIntMethod(function readBigInt64BE(s) { - validateNumber((s >>>= 0), 'offset'); - const o = this[s], - i = this[s + 7]; - (void 0 !== o && void 0 !== i) || boundsError(s, this.length - 8); - const u = (o << 24) + 65536 * this[++s] + 256 * this[++s] + this[++s]; - return ( - (BigInt(u) << BigInt(32)) + - BigInt(this[++s] * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + i) - ); - })), - (Buffer.prototype.readFloatLE = function readFloatLE(s, o) { - return (s >>>= 0), o || checkOffset(s, 4, this.length), _.read(this, s, !0, 23, 4); - }), - (Buffer.prototype.readFloatBE = function readFloatBE(s, o) { - return (s >>>= 0), o || checkOffset(s, 4, this.length), _.read(this, s, !1, 23, 4); - }), - (Buffer.prototype.readDoubleLE = function readDoubleLE(s, o) { - return (s >>>= 0), o || checkOffset(s, 8, this.length), _.read(this, s, !0, 52, 8); - }), - (Buffer.prototype.readDoubleBE = function readDoubleBE(s, o) { - return (s >>>= 0), o || checkOffset(s, 8, this.length), _.read(this, s, !1, 52, 8); - }), - (Buffer.prototype.writeUintLE = Buffer.prototype.writeUIntLE = - function writeUIntLE(s, o, i, u) { - if (((s = +s), (o >>>= 0), (i >>>= 0), !u)) { - checkInt(this, s, o, i, Math.pow(2, 8 * i) - 1, 0); - } - let _ = 1, - w = 0; - for (this[o] = 255 & s; ++w < i && (_ *= 256); ) this[o + w] = (s / _) & 255; - return o + i; - }), - (Buffer.prototype.writeUintBE = Buffer.prototype.writeUIntBE = - function writeUIntBE(s, o, i, u) { - if (((s = +s), (o >>>= 0), (i >>>= 0), !u)) { - checkInt(this, s, o, i, Math.pow(2, 8 * i) - 1, 0); - } - let _ = i - 1, - w = 1; - for (this[o + _] = 255 & s; --_ >= 0 && (w *= 256); ) this[o + _] = (s / w) & 255; - return o + i; - }), - (Buffer.prototype.writeUint8 = Buffer.prototype.writeUInt8 = - function writeUInt8(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 1, 255, 0), - (this[o] = 255 & s), - o + 1 - ); - }), - (Buffer.prototype.writeUint16LE = Buffer.prototype.writeUInt16LE = - function writeUInt16LE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 2, 65535, 0), - (this[o] = 255 & s), - (this[o + 1] = s >>> 8), - o + 2 - ); - }), - (Buffer.prototype.writeUint16BE = Buffer.prototype.writeUInt16BE = - function writeUInt16BE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 2, 65535, 0), - (this[o] = s >>> 8), - (this[o + 1] = 255 & s), - o + 2 - ); - }), - (Buffer.prototype.writeUint32LE = Buffer.prototype.writeUInt32LE = - function writeUInt32LE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 4, 4294967295, 0), - (this[o + 3] = s >>> 24), - (this[o + 2] = s >>> 16), - (this[o + 1] = s >>> 8), - (this[o] = 255 & s), - o + 4 - ); - }), - (Buffer.prototype.writeUint32BE = Buffer.prototype.writeUInt32BE = - function writeUInt32BE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 4, 4294967295, 0), - (this[o] = s >>> 24), - (this[o + 1] = s >>> 16), - (this[o + 2] = s >>> 8), - (this[o + 3] = 255 & s), - o + 4 - ); - }), - (Buffer.prototype.writeBigUInt64LE = defineBigIntMethod(function writeBigUInt64LE( - s, - o = 0 - ) { - return wrtBigUInt64LE(this, s, o, BigInt(0), BigInt('0xffffffffffffffff')); - })), - (Buffer.prototype.writeBigUInt64BE = defineBigIntMethod(function writeBigUInt64BE( - s, - o = 0 - ) { - return wrtBigUInt64BE(this, s, o, BigInt(0), BigInt('0xffffffffffffffff')); - })), - (Buffer.prototype.writeIntLE = function writeIntLE(s, o, i, u) { - if (((s = +s), (o >>>= 0), !u)) { - const u = Math.pow(2, 8 * i - 1); - checkInt(this, s, o, i, u - 1, -u); - } - let _ = 0, - w = 1, - x = 0; - for (this[o] = 255 & s; ++_ < i && (w *= 256); ) - s < 0 && 0 === x && 0 !== this[o + _ - 1] && (x = 1), - (this[o + _] = (((s / w) | 0) - x) & 255); - return o + i; - }), - (Buffer.prototype.writeIntBE = function writeIntBE(s, o, i, u) { - if (((s = +s), (o >>>= 0), !u)) { - const u = Math.pow(2, 8 * i - 1); - checkInt(this, s, o, i, u - 1, -u); - } - let _ = i - 1, - w = 1, - x = 0; - for (this[o + _] = 255 & s; --_ >= 0 && (w *= 256); ) - s < 0 && 0 === x && 0 !== this[o + _ + 1] && (x = 1), - (this[o + _] = (((s / w) | 0) - x) & 255); - return o + i; - }), - (Buffer.prototype.writeInt8 = function writeInt8(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 1, 127, -128), - s < 0 && (s = 255 + s + 1), - (this[o] = 255 & s), - o + 1 - ); - }), - (Buffer.prototype.writeInt16LE = function writeInt16LE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 2, 32767, -32768), - (this[o] = 255 & s), - (this[o + 1] = s >>> 8), - o + 2 - ); - }), - (Buffer.prototype.writeInt16BE = function writeInt16BE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 2, 32767, -32768), - (this[o] = s >>> 8), - (this[o + 1] = 255 & s), - o + 2 - ); - }), - (Buffer.prototype.writeInt32LE = function writeInt32LE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 4, 2147483647, -2147483648), - (this[o] = 255 & s), - (this[o + 1] = s >>> 8), - (this[o + 2] = s >>> 16), - (this[o + 3] = s >>> 24), - o + 4 - ); - }), - (Buffer.prototype.writeInt32BE = function writeInt32BE(s, o, i) { - return ( - (s = +s), - (o >>>= 0), - i || checkInt(this, s, o, 4, 2147483647, -2147483648), - s < 0 && (s = 4294967295 + s + 1), - (this[o] = s >>> 24), - (this[o + 1] = s >>> 16), - (this[o + 2] = s >>> 8), - (this[o + 3] = 255 & s), - o + 4 - ); - }), - (Buffer.prototype.writeBigInt64LE = defineBigIntMethod(function writeBigInt64LE( - s, - o = 0 - ) { - return wrtBigUInt64LE( - this, - s, - o, - -BigInt('0x8000000000000000'), - BigInt('0x7fffffffffffffff') - ); - })), - (Buffer.prototype.writeBigInt64BE = defineBigIntMethod(function writeBigInt64BE( - s, - o = 0 - ) { - return wrtBigUInt64BE( - this, - s, - o, - -BigInt('0x8000000000000000'), - BigInt('0x7fffffffffffffff') - ); - })), - (Buffer.prototype.writeFloatLE = function writeFloatLE(s, o, i) { - return writeFloat(this, s, o, !0, i); - }), - (Buffer.prototype.writeFloatBE = function writeFloatBE(s, o, i) { - return writeFloat(this, s, o, !1, i); - }), - (Buffer.prototype.writeDoubleLE = function writeDoubleLE(s, o, i) { - return writeDouble(this, s, o, !0, i); - }), - (Buffer.prototype.writeDoubleBE = function writeDoubleBE(s, o, i) { - return writeDouble(this, s, o, !1, i); - }), - (Buffer.prototype.copy = function copy(s, o, i, u) { - if (!Buffer.isBuffer(s)) throw new TypeError('argument should be a Buffer'); - if ( - (i || (i = 0), - u || 0 === u || (u = this.length), - o >= s.length && (o = s.length), - o || (o = 0), - u > 0 && u < i && (u = i), - u === i) - ) - return 0; - if (0 === s.length || 0 === this.length) return 0; - if (o < 0) throw new RangeError('targetStart out of bounds'); - if (i < 0 || i >= this.length) throw new RangeError('Index out of range'); - if (u < 0) throw new RangeError('sourceEnd out of bounds'); - u > this.length && (u = this.length), s.length - o < u - i && (u = s.length - o + i); - const _ = u - i; - return ( - this === s && 'function' == typeof Uint8Array.prototype.copyWithin - ? this.copyWithin(o, i, u) - : Uint8Array.prototype.set.call(s, this.subarray(i, u), o), - _ - ); - }), - (Buffer.prototype.fill = function fill(s, o, i, u) { - if ('string' == typeof s) { - if ( - ('string' == typeof o - ? ((u = o), (o = 0), (i = this.length)) - : 'string' == typeof i && ((u = i), (i = this.length)), - void 0 !== u && 'string' != typeof u) - ) - throw new TypeError('encoding must be a string'); - if ('string' == typeof u && !Buffer.isEncoding(u)) - throw new TypeError('Unknown encoding: ' + u); - if (1 === s.length) { - const o = s.charCodeAt(0); - (('utf8' === u && o < 128) || 'latin1' === u) && (s = o); - } - } else 'number' == typeof s ? (s &= 255) : 'boolean' == typeof s && (s = Number(s)); - if (o < 0 || this.length < o || this.length < i) - throw new RangeError('Out of range index'); - if (i <= o) return this; - let _; - if ( - ((o >>>= 0), - (i = void 0 === i ? this.length : i >>> 0), - s || (s = 0), - 'number' == typeof s) - ) - for (_ = o; _ < i; ++_) this[_] = s; - else { - const w = Buffer.isBuffer(s) ? s : Buffer.from(s, u), - x = w.length; - if (0 === x) - throw new TypeError('The value "' + s + '" is invalid for argument "value"'); - for (_ = 0; _ < i - o; ++_) this[_ + o] = w[_ % x]; - } - return this; - }); - const j = {}; - function E(s, o, i) { - j[s] = class NodeError extends i { - constructor() { - super(), - Object.defineProperty(this, 'message', { - value: o.apply(this, arguments), - writable: !0, - configurable: !0 - }), - (this.name = `${this.name} [${s}]`), - this.stack, - delete this.name; - } - get code() { - return s; - } - set code(s) { - Object.defineProperty(this, 'code', { - configurable: !0, - enumerable: !0, - value: s, - writable: !0 - }); - } - toString() { - return `${this.name} [${s}]: ${this.message}`; - } - }; - } - function addNumericalSeparator(s) { - let o = '', - i = s.length; - const u = '-' === s[0] ? 1 : 0; - for (; i >= u + 4; i -= 3) o = `_${s.slice(i - 3, i)}${o}`; - return `${s.slice(0, i)}${o}`; - } - function checkIntBI(s, o, i, u, _, w) { - if (s > i || s < o) { - const u = 'bigint' == typeof o ? 'n' : ''; - let _; - throw ( - ((_ = - w > 3 - ? 0 === o || o === BigInt(0) - ? `>= 0${u} and < 2${u} ** ${8 * (w + 1)}${u}` - : `>= -(2${u} ** ${8 * (w + 1) - 1}${u}) and < 2 ** ${8 * (w + 1) - 1}${u}` - : `>= ${o}${u} and <= ${i}${u}`), - new j.ERR_OUT_OF_RANGE('value', _, s)) - ); - } - !(function checkBounds(s, o, i) { - validateNumber(o, 'offset'), - (void 0 !== s[o] && void 0 !== s[o + i]) || boundsError(o, s.length - (i + 1)); - })(u, _, w); - } - function validateNumber(s, o) { - if ('number' != typeof s) throw new j.ERR_INVALID_ARG_TYPE(o, 'number', s); - } - function boundsError(s, o, i) { - if (Math.floor(s) !== s) - throw (validateNumber(s, i), new j.ERR_OUT_OF_RANGE(i || 'offset', 'an integer', s)); - if (o < 0) throw new j.ERR_BUFFER_OUT_OF_BOUNDS(); - throw new j.ERR_OUT_OF_RANGE(i || 'offset', `>= ${i ? 1 : 0} and <= ${o}`, s); - } - E( - 'ERR_BUFFER_OUT_OF_BOUNDS', - function (s) { - return s - ? `${s} is outside of buffer bounds` - : 'Attempt to access memory outside buffer bounds'; - }, - RangeError - ), - E( - 'ERR_INVALID_ARG_TYPE', - function (s, o) { - return `The "${s}" argument must be of type number. Received type ${typeof o}`; - }, - TypeError - ), - E( - 'ERR_OUT_OF_RANGE', - function (s, o, i) { - let u = `The value of "${s}" is out of range.`, - _ = i; - return ( - Number.isInteger(i) && Math.abs(i) > 2 ** 32 - ? (_ = addNumericalSeparator(String(i))) - : 'bigint' == typeof i && - ((_ = String(i)), - (i > BigInt(2) ** BigInt(32) || i < -(BigInt(2) ** BigInt(32))) && - (_ = addNumericalSeparator(_)), - (_ += 'n')), - (u += ` It must be ${o}. Received ${_}`), - u - ); - }, - RangeError - ); - const L = /[^+/0-9A-Za-z-_]/g; - function utf8ToBytes(s, o) { - let i; - o = o || 1 / 0; - const u = s.length; - let _ = null; - const w = []; - for (let x = 0; x < u; ++x) { - if (((i = s.charCodeAt(x)), i > 55295 && i < 57344)) { - if (!_) { - if (i > 56319) { - (o -= 3) > -1 && w.push(239, 191, 189); - continue; - } - if (x + 1 === u) { - (o -= 3) > -1 && w.push(239, 191, 189); - continue; - } - _ = i; - continue; - } - if (i < 56320) { - (o -= 3) > -1 && w.push(239, 191, 189), (_ = i); - continue; - } - i = 65536 + (((_ - 55296) << 10) | (i - 56320)); - } else _ && (o -= 3) > -1 && w.push(239, 191, 189); - if (((_ = null), i < 128)) { - if ((o -= 1) < 0) break; - w.push(i); - } else if (i < 2048) { - if ((o -= 2) < 0) break; - w.push((i >> 6) | 192, (63 & i) | 128); - } else if (i < 65536) { - if ((o -= 3) < 0) break; - w.push((i >> 12) | 224, ((i >> 6) & 63) | 128, (63 & i) | 128); - } else { - if (!(i < 1114112)) throw new Error('Invalid code point'); - if ((o -= 4) < 0) break; - w.push( - (i >> 18) | 240, - ((i >> 12) & 63) | 128, - ((i >> 6) & 63) | 128, - (63 & i) | 128 - ); - } - } - return w; - } - function base64ToBytes(s) { - return u.toByteArray( - (function base64clean(s) { - if ((s = (s = s.split('=')[0]).trim().replace(L, '')).length < 2) return ''; - for (; s.length % 4 != 0; ) s += '='; - return s; - })(s) - ); - } - function blitBuffer(s, o, i, u) { - let _; - for (_ = 0; _ < u && !(_ + i >= o.length || _ >= s.length); ++_) o[_ + i] = s[_]; - return _; - } - function isInstance(s, o) { - return ( - s instanceof o || - (null != s && - null != s.constructor && - null != s.constructor.name && - s.constructor.name === o.name) - ); - } - function numberIsNaN(s) { - return s != s; - } - const B = (function () { - const s = '0123456789abcdef', - o = new Array(256); - for (let i = 0; i < 16; ++i) { - const u = 16 * i; - for (let _ = 0; _ < 16; ++_) o[u + _] = s[i] + s[_]; - } - return o; - })(); - function defineBigIntMethod(s) { - return 'undefined' == typeof BigInt ? BufferBigIntNotDefined : s; - } - function BufferBigIntNotDefined() { - throw new Error('BigInt not supported'); - } - }, - 17965: (s, o, i) => { - 'use strict'; - var u = i(16426), - _ = { 'text/plain': 'Text', 'text/html': 'Url', default: 'Text' }; - s.exports = function copy(s, o) { - var i, - w, - x, - C, - j, - L, - B = !1; - o || (o = {}), (i = o.debug || !1); - try { - if ( - ((x = u()), - (C = document.createRange()), - (j = document.getSelection()), - ((L = document.createElement('span')).textContent = s), - (L.ariaHidden = 'true'), - (L.style.all = 'unset'), - (L.style.position = 'fixed'), - (L.style.top = 0), - (L.style.clip = 'rect(0, 0, 0, 0)'), - (L.style.whiteSpace = 'pre'), - (L.style.webkitUserSelect = 'text'), - (L.style.MozUserSelect = 'text'), - (L.style.msUserSelect = 'text'), - (L.style.userSelect = 'text'), - L.addEventListener('copy', function (u) { - if ((u.stopPropagation(), o.format)) - if ((u.preventDefault(), void 0 === u.clipboardData)) { - i && console.warn('unable to use e.clipboardData'), - i && console.warn('trying IE specific stuff'), - window.clipboardData.clearData(); - var w = _[o.format] || _.default; - window.clipboardData.setData(w, s); - } else u.clipboardData.clearData(), u.clipboardData.setData(o.format, s); - o.onCopy && (u.preventDefault(), o.onCopy(u.clipboardData)); - }), - document.body.appendChild(L), - C.selectNodeContents(L), - j.addRange(C), - !document.execCommand('copy')) - ) - throw new Error('copy command was unsuccessful'); - B = !0; - } catch (u) { - i && console.error('unable to copy using execCommand: ', u), - i && console.warn('trying IE specific stuff'); - try { - window.clipboardData.setData(o.format || 'text', s), - o.onCopy && o.onCopy(window.clipboardData), - (B = !0); - } catch (u) { - i && console.error('unable to copy using clipboardData: ', u), - i && console.error('falling back to prompt'), - (w = (function format(s) { - var o = (/mac os x/i.test(navigator.userAgent) ? '⌘' : 'Ctrl') + '+C'; - return s.replace(/#{\s*key\s*}/g, o); - })('message' in o ? o.message : 'Copy to clipboard: #{key}, Enter')), - window.prompt(w, s); - } - } finally { - j && ('function' == typeof j.removeRange ? j.removeRange(C) : j.removeAllRanges()), - L && document.body.removeChild(L), - x(); - } - return B; - }; - }, - 2205: function (s, o, i) { - var u; - (u = void 0 !== i.g ? i.g : this), - (s.exports = (function (s) { - if (s.CSS && s.CSS.escape) return s.CSS.escape; - var cssEscape = function (s) { - if (0 == arguments.length) - throw new TypeError('`CSS.escape` requires an argument.'); - for ( - var o, i = String(s), u = i.length, _ = -1, w = '', x = i.charCodeAt(0); - ++_ < u; - - ) - 0 != (o = i.charCodeAt(_)) - ? (w += - (o >= 1 && o <= 31) || - 127 == o || - (0 == _ && o >= 48 && o <= 57) || - (1 == _ && o >= 48 && o <= 57 && 45 == x) - ? '\\' + o.toString(16) + ' ' - : (0 == _ && 1 == u && 45 == o) || - !( - o >= 128 || - 45 == o || - 95 == o || - (o >= 48 && o <= 57) || - (o >= 65 && o <= 90) || - (o >= 97 && o <= 122) - ) - ? '\\' + i.charAt(_) - : i.charAt(_)) - : (w += '�'); - return w; - }; - return s.CSS || (s.CSS = {}), (s.CSS.escape = cssEscape), cssEscape; - })(u)); - }, - 81919: (s, o, i) => { - 'use strict'; - var u = i(48287).Buffer; - function isSpecificValue(s) { - return s instanceof u || s instanceof Date || s instanceof RegExp; - } - function cloneSpecificValue(s) { - if (s instanceof u) { - var o = u.alloc ? u.alloc(s.length) : new u(s.length); - return s.copy(o), o; - } - if (s instanceof Date) return new Date(s.getTime()); - if (s instanceof RegExp) return new RegExp(s); - throw new Error('Unexpected situation'); - } - function deepCloneArray(s) { - var o = []; - return ( - s.forEach(function (s, i) { - 'object' == typeof s && null !== s - ? Array.isArray(s) - ? (o[i] = deepCloneArray(s)) - : isSpecificValue(s) - ? (o[i] = cloneSpecificValue(s)) - : (o[i] = _({}, s)) - : (o[i] = s); - }), - o - ); - } - function safeGetProperty(s, o) { - return '__proto__' === o ? void 0 : s[o]; - } - var _ = (s.exports = function () { - if (arguments.length < 1 || 'object' != typeof arguments[0]) return !1; - if (arguments.length < 2) return arguments[0]; - var s, - o, - i = arguments[0]; - return ( - Array.prototype.slice.call(arguments, 1).forEach(function (u) { - 'object' != typeof u || - null === u || - Array.isArray(u) || - Object.keys(u).forEach(function (w) { - return ( - (o = safeGetProperty(i, w)), - (s = safeGetProperty(u, w)) === i - ? void 0 - : 'object' != typeof s || null === s - ? void (i[w] = s) - : Array.isArray(s) - ? void (i[w] = deepCloneArray(s)) - : isSpecificValue(s) - ? void (i[w] = cloneSpecificValue(s)) - : 'object' != typeof o || null === o || Array.isArray(o) - ? void (i[w] = _({}, s)) - : void (i[w] = _(o, s)) - ); - }); - }), - i - ); - }); - }, - 14744: (s) => { - 'use strict'; - var o = function isMergeableObject(s) { - return ( - (function isNonNullObject(s) { - return !!s && 'object' == typeof s; - })(s) && - !(function isSpecial(s) { - var o = Object.prototype.toString.call(s); - return ( - '[object RegExp]' === o || - '[object Date]' === o || - (function isReactElement(s) { - return s.$$typeof === i; - })(s) - ); - })(s) - ); - }; - var i = 'function' == typeof Symbol && Symbol.for ? Symbol.for('react.element') : 60103; - function cloneUnlessOtherwiseSpecified(s, o) { - return !1 !== o.clone && o.isMergeableObject(s) - ? deepmerge( - (function emptyTarget(s) { - return Array.isArray(s) ? [] : {}; - })(s), - s, - o - ) - : s; - } - function defaultArrayMerge(s, o, i) { - return s.concat(o).map(function (s) { - return cloneUnlessOtherwiseSpecified(s, i); - }); - } - function getKeys(s) { - return Object.keys(s).concat( - (function getEnumerableOwnPropertySymbols(s) { - return Object.getOwnPropertySymbols - ? Object.getOwnPropertySymbols(s).filter(function (o) { - return Object.propertyIsEnumerable.call(s, o); - }) - : []; - })(s) - ); - } - function propertyIsOnObject(s, o) { - try { - return o in s; - } catch (s) { - return !1; - } - } - function mergeObject(s, o, i) { - var u = {}; - return ( - i.isMergeableObject(s) && - getKeys(s).forEach(function (o) { - u[o] = cloneUnlessOtherwiseSpecified(s[o], i); - }), - getKeys(o).forEach(function (_) { - (function propertyIsUnsafe(s, o) { - return ( - propertyIsOnObject(s, o) && - !(Object.hasOwnProperty.call(s, o) && Object.propertyIsEnumerable.call(s, o)) - ); - })(s, _) || - (propertyIsOnObject(s, _) && i.isMergeableObject(o[_]) - ? (u[_] = (function getMergeFunction(s, o) { - if (!o.customMerge) return deepmerge; - var i = o.customMerge(s); - return 'function' == typeof i ? i : deepmerge; - })(_, i)(s[_], o[_], i)) - : (u[_] = cloneUnlessOtherwiseSpecified(o[_], i))); - }), - u - ); - } - function deepmerge(s, i, u) { - ((u = u || {}).arrayMerge = u.arrayMerge || defaultArrayMerge), - (u.isMergeableObject = u.isMergeableObject || o), - (u.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified); - var _ = Array.isArray(i); - return _ === Array.isArray(s) - ? _ - ? u.arrayMerge(s, i, u) - : mergeObject(s, i, u) - : cloneUnlessOtherwiseSpecified(i, u); - } - deepmerge.all = function deepmergeAll(s, o) { - if (!Array.isArray(s)) throw new Error('first argument should be an array'); - return s.reduce(function (s, i) { - return deepmerge(s, i, o); - }, {}); - }; - var u = deepmerge; - s.exports = u; - }, - 42838: function (s) { - s.exports = (function () { - 'use strict'; - const { - entries: s, - setPrototypeOf: o, - isFrozen: i, - getPrototypeOf: u, - getOwnPropertyDescriptor: _ - } = Object; - let { freeze: w, seal: x, create: C } = Object, - { apply: j, construct: L } = 'undefined' != typeof Reflect && Reflect; - w || - (w = function freeze(s) { - return s; - }), - x || - (x = function seal(s) { - return s; - }), - j || - (j = function apply(s, o, i) { - return s.apply(o, i); - }), - L || - (L = function construct(s, o) { - return new s(...o); - }); - const B = unapply(Array.prototype.forEach), - $ = unapply(Array.prototype.pop), - V = unapply(Array.prototype.push), - U = unapply(String.prototype.toLowerCase), - z = unapply(String.prototype.toString), - Y = unapply(String.prototype.match), - Z = unapply(String.prototype.replace), - ee = unapply(String.prototype.indexOf), - ie = unapply(String.prototype.trim), - ae = unapply(Object.prototype.hasOwnProperty), - le = unapply(RegExp.prototype.test), - ce = unconstruct(TypeError); - function unapply(s) { - return function (o) { - for (var i = arguments.length, u = new Array(i > 1 ? i - 1 : 0), _ = 1; _ < i; _++) - u[_ - 1] = arguments[_]; - return j(s, o, u); - }; - } - function unconstruct(s) { - return function () { - for (var o = arguments.length, i = new Array(o), u = 0; u < o; u++) - i[u] = arguments[u]; - return L(s, i); - }; - } - function addToSet(s, u) { - let _ = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : U; - o && o(s, null); - let w = u.length; - for (; w--; ) { - let o = u[w]; - if ('string' == typeof o) { - const s = _(o); - s !== o && (i(u) || (u[w] = s), (o = s)); - } - s[o] = !0; - } - return s; - } - function cleanArray(s) { - for (let o = 0; o < s.length; o++) ae(s, o) || (s[o] = null); - return s; - } - function clone(o) { - const i = C(null); - for (const [u, _] of s(o)) - ae(o, u) && - (Array.isArray(_) - ? (i[u] = cleanArray(_)) - : _ && 'object' == typeof _ && _.constructor === Object - ? (i[u] = clone(_)) - : (i[u] = _)); - return i; - } - function lookupGetter(s, o) { - for (; null !== s; ) { - const i = _(s, o); - if (i) { - if (i.get) return unapply(i.get); - if ('function' == typeof i.value) return unapply(i.value); - } - s = u(s); - } - function fallbackValue() { - return null; - } - return fallbackValue; - } - const pe = w([ - 'a', - 'abbr', - 'acronym', - 'address', - 'area', - 'article', - 'aside', - 'audio', - 'b', - 'bdi', - 'bdo', - 'big', - 'blink', - 'blockquote', - 'body', - 'br', - 'button', - 'canvas', - 'caption', - 'center', - 'cite', - 'code', - 'col', - 'colgroup', - 'content', - 'data', - 'datalist', - 'dd', - 'decorator', - 'del', - 'details', - 'dfn', - 'dialog', - 'dir', - 'div', - 'dl', - 'dt', - 'element', - 'em', - 'fieldset', - 'figcaption', - 'figure', - 'font', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'header', - 'hgroup', - 'hr', - 'html', - 'i', - 'img', - 'input', - 'ins', - 'kbd', - 'label', - 'legend', - 'li', - 'main', - 'map', - 'mark', - 'marquee', - 'menu', - 'menuitem', - 'meter', - 'nav', - 'nobr', - 'ol', - 'optgroup', - 'option', - 'output', - 'p', - 'picture', - 'pre', - 'progress', - 'q', - 'rp', - 'rt', - 'ruby', - 's', - 'samp', - 'section', - 'select', - 'shadow', - 'small', - 'source', - 'spacer', - 'span', - 'strike', - 'strong', - 'style', - 'sub', - 'summary', - 'sup', - 'table', - 'tbody', - 'td', - 'template', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'time', - 'tr', - 'track', - 'tt', - 'u', - 'ul', - 'var', - 'video', - 'wbr' - ]), - de = w([ - 'svg', - 'a', - 'altglyph', - 'altglyphdef', - 'altglyphitem', - 'animatecolor', - 'animatemotion', - 'animatetransform', - 'circle', - 'clippath', - 'defs', - 'desc', - 'ellipse', - 'filter', - 'font', - 'g', - 'glyph', - 'glyphref', - 'hkern', - 'image', - 'line', - 'lineargradient', - 'marker', - 'mask', - 'metadata', - 'mpath', - 'path', - 'pattern', - 'polygon', - 'polyline', - 'radialgradient', - 'rect', - 'stop', - 'style', - 'switch', - 'symbol', - 'text', - 'textpath', - 'title', - 'tref', - 'tspan', - 'view', - 'vkern' - ]), - fe = w([ - 'feBlend', - 'feColorMatrix', - 'feComponentTransfer', - 'feComposite', - 'feConvolveMatrix', - 'feDiffuseLighting', - 'feDisplacementMap', - 'feDistantLight', - 'feDropShadow', - 'feFlood', - 'feFuncA', - 'feFuncB', - 'feFuncG', - 'feFuncR', - 'feGaussianBlur', - 'feImage', - 'feMerge', - 'feMergeNode', - 'feMorphology', - 'feOffset', - 'fePointLight', - 'feSpecularLighting', - 'feSpotLight', - 'feTile', - 'feTurbulence' - ]), - ye = w([ - 'animate', - 'color-profile', - 'cursor', - 'discard', - 'font-face', - 'font-face-format', - 'font-face-name', - 'font-face-src', - 'font-face-uri', - 'foreignobject', - 'hatch', - 'hatchpath', - 'mesh', - 'meshgradient', - 'meshpatch', - 'meshrow', - 'missing-glyph', - 'script', - 'set', - 'solidcolor', - 'unknown', - 'use' - ]), - be = w([ - 'math', - 'menclose', - 'merror', - 'mfenced', - 'mfrac', - 'mglyph', - 'mi', - 'mlabeledtr', - 'mmultiscripts', - 'mn', - 'mo', - 'mover', - 'mpadded', - 'mphantom', - 'mroot', - 'mrow', - 'ms', - 'mspace', - 'msqrt', - 'mstyle', - 'msub', - 'msup', - 'msubsup', - 'mtable', - 'mtd', - 'mtext', - 'mtr', - 'munder', - 'munderover', - 'mprescripts' - ]), - _e = w([ - 'maction', - 'maligngroup', - 'malignmark', - 'mlongdiv', - 'mscarries', - 'mscarry', - 'msgroup', - 'mstack', - 'msline', - 'msrow', - 'semantics', - 'annotation', - 'annotation-xml', - 'mprescripts', - 'none' - ]), - we = w(['#text']), - Se = w([ - 'accept', - 'action', - 'align', - 'alt', - 'autocapitalize', - 'autocomplete', - 'autopictureinpicture', - 'autoplay', - 'background', - 'bgcolor', - 'border', - 'capture', - 'cellpadding', - 'cellspacing', - 'checked', - 'cite', - 'class', - 'clear', - 'color', - 'cols', - 'colspan', - 'controls', - 'controlslist', - 'coords', - 'crossorigin', - 'datetime', - 'decoding', - 'default', - 'dir', - 'disabled', - 'disablepictureinpicture', - 'disableremoteplayback', - 'download', - 'draggable', - 'enctype', - 'enterkeyhint', - 'face', - 'for', - 'headers', - 'height', - 'hidden', - 'high', - 'href', - 'hreflang', - 'id', - 'inputmode', - 'integrity', - 'ismap', - 'kind', - 'label', - 'lang', - 'list', - 'loading', - 'loop', - 'low', - 'max', - 'maxlength', - 'media', - 'method', - 'min', - 'minlength', - 'multiple', - 'muted', - 'name', - 'nonce', - 'noshade', - 'novalidate', - 'nowrap', - 'open', - 'optimum', - 'pattern', - 'placeholder', - 'playsinline', - 'popover', - 'popovertarget', - 'popovertargetaction', - 'poster', - 'preload', - 'pubdate', - 'radiogroup', - 'readonly', - 'rel', - 'required', - 'rev', - 'reversed', - 'role', - 'rows', - 'rowspan', - 'spellcheck', - 'scope', - 'selected', - 'shape', - 'size', - 'sizes', - 'span', - 'srclang', - 'start', - 'src', - 'srcset', - 'step', - 'style', - 'summary', - 'tabindex', - 'title', - 'translate', - 'type', - 'usemap', - 'valign', - 'value', - 'width', - 'wrap', - 'xmlns', - 'slot' - ]), - xe = w([ - 'accent-height', - 'accumulate', - 'additive', - 'alignment-baseline', - 'ascent', - 'attributename', - 'attributetype', - 'azimuth', - 'basefrequency', - 'baseline-shift', - 'begin', - 'bias', - 'by', - 'class', - 'clip', - 'clippathunits', - 'clip-path', - 'clip-rule', - 'color', - 'color-interpolation', - 'color-interpolation-filters', - 'color-profile', - 'color-rendering', - 'cx', - 'cy', - 'd', - 'dx', - 'dy', - 'diffuseconstant', - 'direction', - 'display', - 'divisor', - 'dur', - 'edgemode', - 'elevation', - 'end', - 'fill', - 'fill-opacity', - 'fill-rule', - 'filter', - 'filterunits', - 'flood-color', - 'flood-opacity', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'fx', - 'fy', - 'g1', - 'g2', - 'glyph-name', - 'glyphref', - 'gradientunits', - 'gradienttransform', - 'height', - 'href', - 'id', - 'image-rendering', - 'in', - 'in2', - 'k', - 'k1', - 'k2', - 'k3', - 'k4', - 'kerning', - 'keypoints', - 'keysplines', - 'keytimes', - 'lang', - 'lengthadjust', - 'letter-spacing', - 'kernelmatrix', - 'kernelunitlength', - 'lighting-color', - 'local', - 'marker-end', - 'marker-mid', - 'marker-start', - 'markerheight', - 'markerunits', - 'markerwidth', - 'maskcontentunits', - 'maskunits', - 'max', - 'mask', - 'media', - 'method', - 'mode', - 'min', - 'name', - 'numoctaves', - 'offset', - 'operator', - 'opacity', - 'order', - 'orient', - 'orientation', - 'origin', - 'overflow', - 'paint-order', - 'path', - 'pathlength', - 'patterncontentunits', - 'patterntransform', - 'patternunits', - 'points', - 'preservealpha', - 'preserveaspectratio', - 'primitiveunits', - 'r', - 'rx', - 'ry', - 'radius', - 'refx', - 'refy', - 'repeatcount', - 'repeatdur', - 'restart', - 'result', - 'rotate', - 'scale', - 'seed', - 'shape-rendering', - 'specularconstant', - 'specularexponent', - 'spreadmethod', - 'startoffset', - 'stddeviation', - 'stitchtiles', - 'stop-color', - 'stop-opacity', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke', - 'stroke-width', - 'style', - 'surfacescale', - 'systemlanguage', - 'tabindex', - 'targetx', - 'targety', - 'transform', - 'transform-origin', - 'text-anchor', - 'text-decoration', - 'text-rendering', - 'textlength', - 'type', - 'u1', - 'u2', - 'unicode', - 'values', - 'viewbox', - 'visibility', - 'version', - 'vert-adv-y', - 'vert-origin-x', - 'vert-origin-y', - 'width', - 'word-spacing', - 'wrap', - 'writing-mode', - 'xchannelselector', - 'ychannelselector', - 'x', - 'x1', - 'x2', - 'xmlns', - 'y', - 'y1', - 'y2', - 'z', - 'zoomandpan' - ]), - Pe = w([ - 'accent', - 'accentunder', - 'align', - 'bevelled', - 'close', - 'columnsalign', - 'columnlines', - 'columnspan', - 'denomalign', - 'depth', - 'dir', - 'display', - 'displaystyle', - 'encoding', - 'fence', - 'frame', - 'height', - 'href', - 'id', - 'largeop', - 'length', - 'linethickness', - 'lspace', - 'lquote', - 'mathbackground', - 'mathcolor', - 'mathsize', - 'mathvariant', - 'maxsize', - 'minsize', - 'movablelimits', - 'notation', - 'numalign', - 'open', - 'rowalign', - 'rowlines', - 'rowspacing', - 'rowspan', - 'rspace', - 'rquote', - 'scriptlevel', - 'scriptminsize', - 'scriptsizemultiplier', - 'selection', - 'separator', - 'separators', - 'stretchy', - 'subscriptshift', - 'supscriptshift', - 'symmetric', - 'voffset', - 'width', - 'xmlns' - ]), - Te = w(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']), - Re = x(/\{\{[\w\W]*|[\w\W]*\}\}/gm), - qe = x(/<%[\w\W]*|[\w\W]*%>/gm), - $e = x(/\${[\w\W]*}/gm), - ze = x(/^data-[\-\w.\u00B7-\uFFFF]/), - We = x(/^aria-[\-\w]+$/), - He = x( - /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i - ), - Ye = x(/^(?:\w+script|data):/i), - Xe = x(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g), - Qe = x(/^html$/i), - et = x(/^[a-z][.\w]*(-[.\w]+)+$/i); - var tt = Object.freeze({ - __proto__: null, - MUSTACHE_EXPR: Re, - ERB_EXPR: qe, - TMPLIT_EXPR: $e, - DATA_ATTR: ze, - ARIA_ATTR: We, - IS_ALLOWED_URI: He, - IS_SCRIPT_OR_DATA: Ye, - ATTR_WHITESPACE: Xe, - DOCTYPE_NAME: Qe, - CUSTOM_ELEMENT: et - }); - const rt = { - element: 1, - attribute: 2, - text: 3, - cdataSection: 4, - entityReference: 5, - entityNode: 6, - progressingInstruction: 7, - comment: 8, - document: 9, - documentType: 10, - documentFragment: 11, - notation: 12 - }, - nt = function getGlobal() { - return 'undefined' == typeof window ? null : window; - }, - st = function _createTrustedTypesPolicy(s, o) { - if ('object' != typeof s || 'function' != typeof s.createPolicy) return null; - let i = null; - const u = 'data-tt-policy-suffix'; - o && o.hasAttribute(u) && (i = o.getAttribute(u)); - const _ = 'dompurify' + (i ? '#' + i : ''); - try { - return s.createPolicy(_, { createHTML: (s) => s, createScriptURL: (s) => s }); - } catch (s) { - return console.warn('TrustedTypes policy ' + _ + ' could not be created.'), null; - } - }; - function createDOMPurify() { - let o = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : nt(); - const DOMPurify = (s) => createDOMPurify(s); - if ( - ((DOMPurify.version = '3.1.6'), - (DOMPurify.removed = []), - !o || !o.document || o.document.nodeType !== rt.document) - ) - return (DOMPurify.isSupported = !1), DOMPurify; - let { document: i } = o; - const u = i, - _ = u.currentScript, - { - DocumentFragment: x, - HTMLTemplateElement: j, - Node: L, - Element: Re, - NodeFilter: qe, - NamedNodeMap: $e = o.NamedNodeMap || o.MozNamedAttrMap, - HTMLFormElement: ze, - DOMParser: We, - trustedTypes: Ye - } = o, - Xe = Re.prototype, - et = lookupGetter(Xe, 'cloneNode'), - ot = lookupGetter(Xe, 'remove'), - it = lookupGetter(Xe, 'nextSibling'), - at = lookupGetter(Xe, 'childNodes'), - lt = lookupGetter(Xe, 'parentNode'); - if ('function' == typeof j) { - const s = i.createElement('template'); - s.content && s.content.ownerDocument && (i = s.content.ownerDocument); - } - let ct, - ut = ''; - const { - implementation: pt, - createNodeIterator: ht, - createDocumentFragment: dt, - getElementsByTagName: mt - } = i, - { importNode: gt } = u; - let yt = {}; - DOMPurify.isSupported = - 'function' == typeof s && - 'function' == typeof lt && - pt && - void 0 !== pt.createHTMLDocument; - const { - MUSTACHE_EXPR: vt, - ERB_EXPR: bt, - TMPLIT_EXPR: _t, - DATA_ATTR: Et, - ARIA_ATTR: wt, - IS_SCRIPT_OR_DATA: St, - ATTR_WHITESPACE: xt, - CUSTOM_ELEMENT: kt - } = tt; - let { IS_ALLOWED_URI: Ct } = tt, - Ot = null; - const At = addToSet({}, [...pe, ...de, ...fe, ...be, ...we]); - let jt = null; - const It = addToSet({}, [...Se, ...xe, ...Pe, ...Te]); - let Pt = Object.seal( - C(null, { - tagNameCheck: { writable: !0, configurable: !1, enumerable: !0, value: null }, - attributeNameCheck: { - writable: !0, - configurable: !1, - enumerable: !0, - value: null - }, - allowCustomizedBuiltInElements: { - writable: !0, - configurable: !1, - enumerable: !0, - value: !1 - } - }) - ), - Mt = null, - Tt = null, - Nt = !0, - Rt = !0, - Dt = !1, - Lt = !0, - Bt = !1, - Ft = !0, - qt = !1, - $t = !1, - Vt = !1, - Ut = !1, - zt = !1, - Wt = !1, - Kt = !0, - Ht = !1; - const Jt = 'user-content-'; - let Gt = !0, - Yt = !1, - Xt = {}, - Zt = null; - const Qt = addToSet({}, [ - 'annotation-xml', - 'audio', - 'colgroup', - 'desc', - 'foreignobject', - 'head', - 'iframe', - 'math', - 'mi', - 'mn', - 'mo', - 'ms', - 'mtext', - 'noembed', - 'noframes', - 'noscript', - 'plaintext', - 'script', - 'style', - 'svg', - 'template', - 'thead', - 'title', - 'video', - 'xmp' - ]); - let er = null; - const tr = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); - let rr = null; - const nr = addToSet({}, [ - 'alt', - 'class', - 'for', - 'id', - 'label', - 'name', - 'pattern', - 'placeholder', - 'role', - 'summary', - 'title', - 'value', - 'style', - 'xmlns' - ]), - sr = 'http://www.w3.org/1998/Math/MathML', - ir = 'http://www.w3.org/2000/svg', - ar = 'http://www.w3.org/1999/xhtml'; - let lr = ar, - cr = !1, - ur = null; - const pr = addToSet({}, [sr, ir, ar], z); - let dr = null; - const fr = ['application/xhtml+xml', 'text/html'], - mr = 'text/html'; - let gr = null, - yr = null; - const vr = i.createElement('form'), - br = function isRegexOrFunction(s) { - return s instanceof RegExp || s instanceof Function; - }, - _r = function _parseConfig() { - let s = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; - if (!yr || yr !== s) { - if ( - ((s && 'object' == typeof s) || (s = {}), - (s = clone(s)), - (dr = -1 === fr.indexOf(s.PARSER_MEDIA_TYPE) ? mr : s.PARSER_MEDIA_TYPE), - (gr = 'application/xhtml+xml' === dr ? z : U), - (Ot = ae(s, 'ALLOWED_TAGS') ? addToSet({}, s.ALLOWED_TAGS, gr) : At), - (jt = ae(s, 'ALLOWED_ATTR') ? addToSet({}, s.ALLOWED_ATTR, gr) : It), - (ur = ae(s, 'ALLOWED_NAMESPACES') - ? addToSet({}, s.ALLOWED_NAMESPACES, z) - : pr), - (rr = ae(s, 'ADD_URI_SAFE_ATTR') - ? addToSet(clone(nr), s.ADD_URI_SAFE_ATTR, gr) - : nr), - (er = ae(s, 'ADD_DATA_URI_TAGS') - ? addToSet(clone(tr), s.ADD_DATA_URI_TAGS, gr) - : tr), - (Zt = ae(s, 'FORBID_CONTENTS') ? addToSet({}, s.FORBID_CONTENTS, gr) : Qt), - (Mt = ae(s, 'FORBID_TAGS') ? addToSet({}, s.FORBID_TAGS, gr) : {}), - (Tt = ae(s, 'FORBID_ATTR') ? addToSet({}, s.FORBID_ATTR, gr) : {}), - (Xt = !!ae(s, 'USE_PROFILES') && s.USE_PROFILES), - (Nt = !1 !== s.ALLOW_ARIA_ATTR), - (Rt = !1 !== s.ALLOW_DATA_ATTR), - (Dt = s.ALLOW_UNKNOWN_PROTOCOLS || !1), - (Lt = !1 !== s.ALLOW_SELF_CLOSE_IN_ATTR), - (Bt = s.SAFE_FOR_TEMPLATES || !1), - (Ft = !1 !== s.SAFE_FOR_XML), - (qt = s.WHOLE_DOCUMENT || !1), - (Ut = s.RETURN_DOM || !1), - (zt = s.RETURN_DOM_FRAGMENT || !1), - (Wt = s.RETURN_TRUSTED_TYPE || !1), - (Vt = s.FORCE_BODY || !1), - (Kt = !1 !== s.SANITIZE_DOM), - (Ht = s.SANITIZE_NAMED_PROPS || !1), - (Gt = !1 !== s.KEEP_CONTENT), - (Yt = s.IN_PLACE || !1), - (Ct = s.ALLOWED_URI_REGEXP || He), - (lr = s.NAMESPACE || ar), - (Pt = s.CUSTOM_ELEMENT_HANDLING || {}), - s.CUSTOM_ELEMENT_HANDLING && - br(s.CUSTOM_ELEMENT_HANDLING.tagNameCheck) && - (Pt.tagNameCheck = s.CUSTOM_ELEMENT_HANDLING.tagNameCheck), - s.CUSTOM_ELEMENT_HANDLING && - br(s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck) && - (Pt.attributeNameCheck = s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck), - s.CUSTOM_ELEMENT_HANDLING && - 'boolean' == - typeof s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && - (Pt.allowCustomizedBuiltInElements = - s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements), - Bt && (Rt = !1), - zt && (Ut = !0), - Xt && - ((Ot = addToSet({}, we)), - (jt = []), - !0 === Xt.html && (addToSet(Ot, pe), addToSet(jt, Se)), - !0 === Xt.svg && (addToSet(Ot, de), addToSet(jt, xe), addToSet(jt, Te)), - !0 === Xt.svgFilters && - (addToSet(Ot, fe), addToSet(jt, xe), addToSet(jt, Te)), - !0 === Xt.mathMl && (addToSet(Ot, be), addToSet(jt, Pe), addToSet(jt, Te))), - s.ADD_TAGS && (Ot === At && (Ot = clone(Ot)), addToSet(Ot, s.ADD_TAGS, gr)), - s.ADD_ATTR && (jt === It && (jt = clone(jt)), addToSet(jt, s.ADD_ATTR, gr)), - s.ADD_URI_SAFE_ATTR && addToSet(rr, s.ADD_URI_SAFE_ATTR, gr), - s.FORBID_CONTENTS && - (Zt === Qt && (Zt = clone(Zt)), addToSet(Zt, s.FORBID_CONTENTS, gr)), - Gt && (Ot['#text'] = !0), - qt && addToSet(Ot, ['html', 'head', 'body']), - Ot.table && (addToSet(Ot, ['tbody']), delete Mt.tbody), - s.TRUSTED_TYPES_POLICY) - ) { - if ('function' != typeof s.TRUSTED_TYPES_POLICY.createHTML) - throw ce( - 'TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.' - ); - if ('function' != typeof s.TRUSTED_TYPES_POLICY.createScriptURL) - throw ce( - 'TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.' - ); - (ct = s.TRUSTED_TYPES_POLICY), (ut = ct.createHTML('')); - } else - void 0 === ct && (ct = st(Ye, _)), - null !== ct && 'string' == typeof ut && (ut = ct.createHTML('')); - w && w(s), (yr = s); - } - }, - Er = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']), - wr = addToSet({}, ['foreignobject', 'annotation-xml']), - Sr = addToSet({}, ['title', 'style', 'font', 'a', 'script']), - xr = addToSet({}, [...de, ...fe, ...ye]), - kr = addToSet({}, [...be, ..._e]), - Cr = function _checkValidNamespace(s) { - let o = lt(s); - (o && o.tagName) || (o = { namespaceURI: lr, tagName: 'template' }); - const i = U(s.tagName), - u = U(o.tagName); - return ( - !!ur[s.namespaceURI] && - (s.namespaceURI === ir - ? o.namespaceURI === ar - ? 'svg' === i - : o.namespaceURI === sr - ? 'svg' === i && ('annotation-xml' === u || Er[u]) - : Boolean(xr[i]) - : s.namespaceURI === sr - ? o.namespaceURI === ar - ? 'math' === i - : o.namespaceURI === ir - ? 'math' === i && wr[u] - : Boolean(kr[i]) - : s.namespaceURI === ar - ? !(o.namespaceURI === ir && !wr[u]) && - !(o.namespaceURI === sr && !Er[u]) && - !kr[i] && - (Sr[i] || !xr[i]) - : !('application/xhtml+xml' !== dr || !ur[s.namespaceURI])) - ); - }, - Or = function _forceRemove(s) { - V(DOMPurify.removed, { element: s }); - try { - lt(s).removeChild(s); - } catch (o) { - ot(s); - } - }, - Ar = function _removeAttribute(s, o) { - try { - V(DOMPurify.removed, { attribute: o.getAttributeNode(s), from: o }); - } catch (s) { - V(DOMPurify.removed, { attribute: null, from: o }); - } - if ((o.removeAttribute(s), 'is' === s && !jt[s])) - if (Ut || zt) - try { - Or(o); - } catch (s) {} - else - try { - o.setAttribute(s, ''); - } catch (s) {} - }, - jr = function _initDocument(s) { - let o = null, - u = null; - if (Vt) s = '' + s; - else { - const o = Y(s, /^[\r\n\t ]+/); - u = o && o[0]; - } - 'application/xhtml+xml' === dr && - lr === ar && - (s = - '' + - s + - ''); - const _ = ct ? ct.createHTML(s) : s; - if (lr === ar) - try { - o = new We().parseFromString(_, dr); - } catch (s) {} - if (!o || !o.documentElement) { - o = pt.createDocument(lr, 'template', null); - try { - o.documentElement.innerHTML = cr ? ut : _; - } catch (s) {} - } - const w = o.body || o.documentElement; - return ( - s && u && w.insertBefore(i.createTextNode(u), w.childNodes[0] || null), - lr === ar ? mt.call(o, qt ? 'html' : 'body')[0] : qt ? o.documentElement : w - ); - }, - Ir = function _createNodeIterator(s) { - return ht.call( - s.ownerDocument || s, - s, - qe.SHOW_ELEMENT | - qe.SHOW_COMMENT | - qe.SHOW_TEXT | - qe.SHOW_PROCESSING_INSTRUCTION | - qe.SHOW_CDATA_SECTION, - null - ); - }, - Pr = function _isClobbered(s) { - return ( - s instanceof ze && - ('string' != typeof s.nodeName || - 'string' != typeof s.textContent || - 'function' != typeof s.removeChild || - !(s.attributes instanceof $e) || - 'function' != typeof s.removeAttribute || - 'function' != typeof s.setAttribute || - 'string' != typeof s.namespaceURI || - 'function' != typeof s.insertBefore || - 'function' != typeof s.hasChildNodes) - ); - }, - Mr = function _isNode(s) { - return 'function' == typeof L && s instanceof L; - }, - Tr = function _executeHook(s, o, i) { - yt[s] && - B(yt[s], (s) => { - s.call(DOMPurify, o, i, yr); - }); - }, - Nr = function _sanitizeElements(s) { - let o = null; - if ((Tr('beforeSanitizeElements', s, null), Pr(s))) return Or(s), !0; - const i = gr(s.nodeName); - if ( - (Tr('uponSanitizeElement', s, { tagName: i, allowedTags: Ot }), - s.hasChildNodes() && - !Mr(s.firstElementChild) && - le(/<[/\w]/g, s.innerHTML) && - le(/<[/\w]/g, s.textContent)) - ) - return Or(s), !0; - if (s.nodeType === rt.progressingInstruction) return Or(s), !0; - if (Ft && s.nodeType === rt.comment && le(/<[/\w]/g, s.data)) return Or(s), !0; - if (!Ot[i] || Mt[i]) { - if (!Mt[i] && Dr(i)) { - if (Pt.tagNameCheck instanceof RegExp && le(Pt.tagNameCheck, i)) return !1; - if (Pt.tagNameCheck instanceof Function && Pt.tagNameCheck(i)) return !1; - } - if (Gt && !Zt[i]) { - const o = lt(s) || s.parentNode, - i = at(s) || s.childNodes; - if (i && o) - for (let u = i.length - 1; u >= 0; --u) { - const _ = et(i[u], !0); - (_.__removalCount = (s.__removalCount || 0) + 1), - o.insertBefore(_, it(s)); - } - } - return Or(s), !0; - } - return s instanceof Re && !Cr(s) - ? (Or(s), !0) - : ('noscript' !== i && 'noembed' !== i && 'noframes' !== i) || - !le(/<\/no(script|embed|frames)/i, s.innerHTML) - ? (Bt && - s.nodeType === rt.text && - ((o = s.textContent), - B([vt, bt, _t], (s) => { - o = Z(o, s, ' '); - }), - s.textContent !== o && - (V(DOMPurify.removed, { element: s.cloneNode() }), - (s.textContent = o))), - Tr('afterSanitizeElements', s, null), - !1) - : (Or(s), !0); - }, - Rr = function _isValidAttribute(s, o, u) { - if (Kt && ('id' === o || 'name' === o) && (u in i || u in vr)) return !1; - if (Rt && !Tt[o] && le(Et, o)); - else if (Nt && le(wt, o)); - else if (!jt[o] || Tt[o]) { - if ( - !( - (Dr(s) && - ((Pt.tagNameCheck instanceof RegExp && le(Pt.tagNameCheck, s)) || - (Pt.tagNameCheck instanceof Function && Pt.tagNameCheck(s))) && - ((Pt.attributeNameCheck instanceof RegExp && - le(Pt.attributeNameCheck, o)) || - (Pt.attributeNameCheck instanceof Function && - Pt.attributeNameCheck(o)))) || - ('is' === o && - Pt.allowCustomizedBuiltInElements && - ((Pt.tagNameCheck instanceof RegExp && le(Pt.tagNameCheck, u)) || - (Pt.tagNameCheck instanceof Function && Pt.tagNameCheck(u)))) - ) - ) - return !1; - } else if (rr[o]); - else if (le(Ct, Z(u, xt, ''))); - else if ( - ('src' !== o && 'xlink:href' !== o && 'href' !== o) || - 'script' === s || - 0 !== ee(u, 'data:') || - !er[s] - ) - if (Dt && !le(St, Z(u, xt, ''))); - else if (u) return !1; - return !0; - }, - Dr = function _isBasicCustomElement(s) { - return 'annotation-xml' !== s && Y(s, kt); - }, - Lr = function _sanitizeAttributes(s) { - Tr('beforeSanitizeAttributes', s, null); - const { attributes: o } = s; - if (!o) return; - const i = { attrName: '', attrValue: '', keepAttr: !0, allowedAttributes: jt }; - let u = o.length; - for (; u--; ) { - const _ = o[u], - { name: w, namespaceURI: x, value: C } = _, - j = gr(w); - let L = 'value' === w ? C : ie(C); - if ( - ((i.attrName = j), - (i.attrValue = L), - (i.keepAttr = !0), - (i.forceKeepAttr = void 0), - Tr('uponSanitizeAttribute', s, i), - (L = i.attrValue), - Ft && le(/((--!?|])>)|<\/(style|title)/i, L)) - ) { - Ar(w, s); - continue; - } - if (i.forceKeepAttr) continue; - if ((Ar(w, s), !i.keepAttr)) continue; - if (!Lt && le(/\/>/i, L)) { - Ar(w, s); - continue; - } - Bt && - B([vt, bt, _t], (s) => { - L = Z(L, s, ' '); - }); - const V = gr(s.nodeName); - if (Rr(V, j, L)) { - if ( - (!Ht || ('id' !== j && 'name' !== j) || (Ar(w, s), (L = Jt + L)), - ct && 'object' == typeof Ye && 'function' == typeof Ye.getAttributeType) - ) - if (x); - else - switch (Ye.getAttributeType(V, j)) { - case 'TrustedHTML': - L = ct.createHTML(L); - break; - case 'TrustedScriptURL': - L = ct.createScriptURL(L); - } - try { - x ? s.setAttributeNS(x, w, L) : s.setAttribute(w, L), - Pr(s) ? Or(s) : $(DOMPurify.removed); - } catch (s) {} - } - } - Tr('afterSanitizeAttributes', s, null); - }, - Br = function _sanitizeShadowDOM(s) { - let o = null; - const i = Ir(s); - for (Tr('beforeSanitizeShadowDOM', s, null); (o = i.nextNode()); ) - Tr('uponSanitizeShadowNode', o, null), - Nr(o) || (o.content instanceof x && _sanitizeShadowDOM(o.content), Lr(o)); - Tr('afterSanitizeShadowDOM', s, null); - }; - return ( - (DOMPurify.sanitize = function (s) { - let o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, - i = null, - _ = null, - w = null, - C = null; - if (((cr = !s), cr && (s = '\x3c!--\x3e'), 'string' != typeof s && !Mr(s))) { - if ('function' != typeof s.toString) throw ce('toString is not a function'); - if ('string' != typeof (s = s.toString())) - throw ce('dirty is not a string, aborting'); - } - if (!DOMPurify.isSupported) return s; - if ( - ($t || _r(o), (DOMPurify.removed = []), 'string' == typeof s && (Yt = !1), Yt) - ) { - if (s.nodeName) { - const o = gr(s.nodeName); - if (!Ot[o] || Mt[o]) - throw ce('root node is forbidden and cannot be sanitized in-place'); - } - } else if (s instanceof L) - (i = jr('\x3c!----\x3e')), - (_ = i.ownerDocument.importNode(s, !0)), - (_.nodeType === rt.element && 'BODY' === _.nodeName) || 'HTML' === _.nodeName - ? (i = _) - : i.appendChild(_); - else { - if (!Ut && !Bt && !qt && -1 === s.indexOf('<')) - return ct && Wt ? ct.createHTML(s) : s; - if (((i = jr(s)), !i)) return Ut ? null : Wt ? ut : ''; - } - i && Vt && Or(i.firstChild); - const j = Ir(Yt ? s : i); - for (; (w = j.nextNode()); ) - Nr(w) || (w.content instanceof x && Br(w.content), Lr(w)); - if (Yt) return s; - if (Ut) { - if (zt) - for (C = dt.call(i.ownerDocument); i.firstChild; ) - C.appendChild(i.firstChild); - else C = i; - return (jt.shadowroot || jt.shadowrootmode) && (C = gt.call(u, C, !0)), C; - } - let $ = qt ? i.outerHTML : i.innerHTML; - return ( - qt && - Ot['!doctype'] && - i.ownerDocument && - i.ownerDocument.doctype && - i.ownerDocument.doctype.name && - le(Qe, i.ownerDocument.doctype.name) && - ($ = '\n' + $), - Bt && - B([vt, bt, _t], (s) => { - $ = Z($, s, ' '); - }), - ct && Wt ? ct.createHTML($) : $ - ); - }), - (DOMPurify.setConfig = function () { - _r(arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}), - ($t = !0); - }), - (DOMPurify.clearConfig = function () { - (yr = null), ($t = !1); - }), - (DOMPurify.isValidAttribute = function (s, o, i) { - yr || _r({}); - const u = gr(s), - _ = gr(o); - return Rr(u, _, i); - }), - (DOMPurify.addHook = function (s, o) { - 'function' == typeof o && ((yt[s] = yt[s] || []), V(yt[s], o)); - }), - (DOMPurify.removeHook = function (s) { - if (yt[s]) return $(yt[s]); - }), - (DOMPurify.removeHooks = function (s) { - yt[s] && (yt[s] = []); - }), - (DOMPurify.removeAllHooks = function () { - yt = {}; - }), - DOMPurify - ); - } - return createDOMPurify(); - })(); - }, - 78004: (s) => { - 'use strict'; - class SubRange { - constructor(s, o) { - (this.low = s), (this.high = o), (this.length = 1 + o - s); - } - overlaps(s) { - return !(this.high < s.low || this.low > s.high); - } - touches(s) { - return !(this.high + 1 < s.low || this.low - 1 > s.high); - } - add(s) { - return new SubRange(Math.min(this.low, s.low), Math.max(this.high, s.high)); - } - subtract(s) { - return s.low <= this.low && s.high >= this.high - ? [] - : s.low > this.low && s.high < this.high - ? [new SubRange(this.low, s.low - 1), new SubRange(s.high + 1, this.high)] - : s.low <= this.low - ? [new SubRange(s.high + 1, this.high)] - : [new SubRange(this.low, s.low - 1)]; - } - toString() { - return this.low == this.high ? this.low.toString() : this.low + '-' + this.high; - } - } - class DRange { - constructor(s, o) { - (this.ranges = []), (this.length = 0), null != s && this.add(s, o); - } - _update_length() { - this.length = this.ranges.reduce((s, o) => s + o.length, 0); - } - add(s, o) { - var _add = (s) => { - for (var o = 0; o < this.ranges.length && !s.touches(this.ranges[o]); ) o++; - for ( - var i = this.ranges.slice(0, o); - o < this.ranges.length && s.touches(this.ranges[o]); - - ) - (s = s.add(this.ranges[o])), o++; - i.push(s), (this.ranges = i.concat(this.ranges.slice(o))), this._update_length(); - }; - return ( - s instanceof DRange - ? s.ranges.forEach(_add) - : (null == o && (o = s), _add(new SubRange(s, o))), - this - ); - } - subtract(s, o) { - var _subtract = (s) => { - for (var o = 0; o < this.ranges.length && !s.overlaps(this.ranges[o]); ) o++; - for ( - var i = this.ranges.slice(0, o); - o < this.ranges.length && s.overlaps(this.ranges[o]); - - ) - (i = i.concat(this.ranges[o].subtract(s))), o++; - (this.ranges = i.concat(this.ranges.slice(o))), this._update_length(); - }; - return ( - s instanceof DRange - ? s.ranges.forEach(_subtract) - : (null == o && (o = s), _subtract(new SubRange(s, o))), - this - ); - } - intersect(s, o) { - var i = [], - _intersect = (s) => { - for (var o = 0; o < this.ranges.length && !s.overlaps(this.ranges[o]); ) o++; - for (; o < this.ranges.length && s.overlaps(this.ranges[o]); ) { - var u = Math.max(this.ranges[o].low, s.low), - _ = Math.min(this.ranges[o].high, s.high); - i.push(new SubRange(u, _)), o++; - } - }; - return ( - s instanceof DRange - ? s.ranges.forEach(_intersect) - : (null == o && (o = s), _intersect(new SubRange(s, o))), - (this.ranges = i), - this._update_length(), - this - ); - } - index(s) { - for (var o = 0; o < this.ranges.length && this.ranges[o].length <= s; ) - (s -= this.ranges[o].length), o++; - return this.ranges[o].low + s; - } - toString() { - return '[ ' + this.ranges.join(', ') + ' ]'; - } - clone() { - return new DRange(this); - } - numbers() { - return this.ranges.reduce((s, o) => { - for (var i = o.low; i <= o.high; ) s.push(i), i++; - return s; - }, []); - } - subranges() { - return this.ranges.map((s) => ({ - low: s.low, - high: s.high, - length: 1 + s.high - s.low - })); - } - } - s.exports = DRange; - }, - 37007: (s) => { - 'use strict'; - var o, - i = 'object' == typeof Reflect ? Reflect : null, - u = - i && 'function' == typeof i.apply - ? i.apply - : function ReflectApply(s, o, i) { - return Function.prototype.apply.call(s, o, i); - }; - o = - i && 'function' == typeof i.ownKeys - ? i.ownKeys - : Object.getOwnPropertySymbols - ? function ReflectOwnKeys(s) { - return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s)); - } - : function ReflectOwnKeys(s) { - return Object.getOwnPropertyNames(s); - }; - var _ = - Number.isNaN || - function NumberIsNaN(s) { - return s != s; - }; - function EventEmitter() { - EventEmitter.init.call(this); - } - (s.exports = EventEmitter), - (s.exports.once = function once(s, o) { - return new Promise(function (i, u) { - function errorListener(i) { - s.removeListener(o, resolver), u(i); - } - function resolver() { - 'function' == typeof s.removeListener && s.removeListener('error', errorListener), - i([].slice.call(arguments)); - } - eventTargetAgnosticAddListener(s, o, resolver, { once: !0 }), - 'error' !== o && - (function addErrorHandlerIfEventEmitter(s, o, i) { - 'function' == typeof s.on && eventTargetAgnosticAddListener(s, 'error', o, i); - })(s, errorListener, { once: !0 }); - }); - }), - (EventEmitter.EventEmitter = EventEmitter), - (EventEmitter.prototype._events = void 0), - (EventEmitter.prototype._eventsCount = 0), - (EventEmitter.prototype._maxListeners = void 0); - var w = 10; - function checkListener(s) { - if ('function' != typeof s) - throw new TypeError( - 'The "listener" argument must be of type Function. Received type ' + typeof s - ); - } - function _getMaxListeners(s) { - return void 0 === s._maxListeners ? EventEmitter.defaultMaxListeners : s._maxListeners; - } - function _addListener(s, o, i, u) { - var _, w, x; - if ( - (checkListener(i), - void 0 === (w = s._events) - ? ((w = s._events = Object.create(null)), (s._eventsCount = 0)) - : (void 0 !== w.newListener && - (s.emit('newListener', o, i.listener ? i.listener : i), (w = s._events)), - (x = w[o])), - void 0 === x) - ) - (x = w[o] = i), ++s._eventsCount; - else if ( - ('function' == typeof x - ? (x = w[o] = u ? [i, x] : [x, i]) - : u - ? x.unshift(i) - : x.push(i), - (_ = _getMaxListeners(s)) > 0 && x.length > _ && !x.warned) - ) { - x.warned = !0; - var C = new Error( - 'Possible EventEmitter memory leak detected. ' + - x.length + - ' ' + - String(o) + - ' listeners added. Use emitter.setMaxListeners() to increase limit' - ); - (C.name = 'MaxListenersExceededWarning'), - (C.emitter = s), - (C.type = o), - (C.count = x.length), - (function ProcessEmitWarning(s) { - console && console.warn && console.warn(s); - })(C); - } - return s; - } - function onceWrapper() { - if (!this.fired) - return ( - this.target.removeListener(this.type, this.wrapFn), - (this.fired = !0), - 0 === arguments.length - ? this.listener.call(this.target) - : this.listener.apply(this.target, arguments) - ); - } - function _onceWrap(s, o, i) { - var u = { fired: !1, wrapFn: void 0, target: s, type: o, listener: i }, - _ = onceWrapper.bind(u); - return (_.listener = i), (u.wrapFn = _), _; - } - function _listeners(s, o, i) { - var u = s._events; - if (void 0 === u) return []; - var _ = u[o]; - return void 0 === _ - ? [] - : 'function' == typeof _ - ? i - ? [_.listener || _] - : [_] - : i - ? (function unwrapListeners(s) { - for (var o = new Array(s.length), i = 0; i < o.length; ++i) - o[i] = s[i].listener || s[i]; - return o; - })(_) - : arrayClone(_, _.length); - } - function listenerCount(s) { - var o = this._events; - if (void 0 !== o) { - var i = o[s]; - if ('function' == typeof i) return 1; - if (void 0 !== i) return i.length; - } - return 0; - } - function arrayClone(s, o) { - for (var i = new Array(o), u = 0; u < o; ++u) i[u] = s[u]; - return i; - } - function eventTargetAgnosticAddListener(s, o, i, u) { - if ('function' == typeof s.on) u.once ? s.once(o, i) : s.on(o, i); - else { - if ('function' != typeof s.addEventListener) - throw new TypeError( - 'The "emitter" argument must be of type EventEmitter. Received type ' + typeof s - ); - s.addEventListener(o, function wrapListener(_) { - u.once && s.removeEventListener(o, wrapListener), i(_); - }); - } - } - Object.defineProperty(EventEmitter, 'defaultMaxListeners', { - enumerable: !0, - get: function () { - return w; - }, - set: function (s) { - if ('number' != typeof s || s < 0 || _(s)) - throw new RangeError( - 'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + - s + - '.' - ); - w = s; - } - }), - (EventEmitter.init = function () { - (void 0 !== this._events && this._events !== Object.getPrototypeOf(this)._events) || - ((this._events = Object.create(null)), (this._eventsCount = 0)), - (this._maxListeners = this._maxListeners || void 0); - }), - (EventEmitter.prototype.setMaxListeners = function setMaxListeners(s) { - if ('number' != typeof s || s < 0 || _(s)) - throw new RangeError( - 'The value of "n" is out of range. It must be a non-negative number. Received ' + - s + - '.' - ); - return (this._maxListeners = s), this; - }), - (EventEmitter.prototype.getMaxListeners = function getMaxListeners() { - return _getMaxListeners(this); - }), - (EventEmitter.prototype.emit = function emit(s) { - for (var o = [], i = 1; i < arguments.length; i++) o.push(arguments[i]); - var _ = 'error' === s, - w = this._events; - if (void 0 !== w) _ = _ && void 0 === w.error; - else if (!_) return !1; - if (_) { - var x; - if ((o.length > 0 && (x = o[0]), x instanceof Error)) throw x; - var C = new Error('Unhandled error.' + (x ? ' (' + x.message + ')' : '')); - throw ((C.context = x), C); - } - var j = w[s]; - if (void 0 === j) return !1; - if ('function' == typeof j) u(j, this, o); - else { - var L = j.length, - B = arrayClone(j, L); - for (i = 0; i < L; ++i) u(B[i], this, o); - } - return !0; - }), - (EventEmitter.prototype.addListener = function addListener(s, o) { - return _addListener(this, s, o, !1); - }), - (EventEmitter.prototype.on = EventEmitter.prototype.addListener), - (EventEmitter.prototype.prependListener = function prependListener(s, o) { - return _addListener(this, s, o, !0); - }), - (EventEmitter.prototype.once = function once(s, o) { - return checkListener(o), this.on(s, _onceWrap(this, s, o)), this; - }), - (EventEmitter.prototype.prependOnceListener = function prependOnceListener(s, o) { - return checkListener(o), this.prependListener(s, _onceWrap(this, s, o)), this; - }), - (EventEmitter.prototype.removeListener = function removeListener(s, o) { - var i, u, _, w, x; - if ((checkListener(o), void 0 === (u = this._events))) return this; - if (void 0 === (i = u[s])) return this; - if (i === o || i.listener === o) - 0 == --this._eventsCount - ? (this._events = Object.create(null)) - : (delete u[s], - u.removeListener && this.emit('removeListener', s, i.listener || o)); - else if ('function' != typeof i) { - for (_ = -1, w = i.length - 1; w >= 0; w--) - if (i[w] === o || i[w].listener === o) { - (x = i[w].listener), (_ = w); - break; - } - if (_ < 0) return this; - 0 === _ - ? i.shift() - : (function spliceOne(s, o) { - for (; o + 1 < s.length; o++) s[o] = s[o + 1]; - s.pop(); - })(i, _), - 1 === i.length && (u[s] = i[0]), - void 0 !== u.removeListener && this.emit('removeListener', s, x || o); - } - return this; - }), - (EventEmitter.prototype.off = EventEmitter.prototype.removeListener), - (EventEmitter.prototype.removeAllListeners = function removeAllListeners(s) { - var o, i, u; - if (void 0 === (i = this._events)) return this; - if (void 0 === i.removeListener) - return ( - 0 === arguments.length - ? ((this._events = Object.create(null)), (this._eventsCount = 0)) - : void 0 !== i[s] && - (0 == --this._eventsCount - ? (this._events = Object.create(null)) - : delete i[s]), - this - ); - if (0 === arguments.length) { - var _, - w = Object.keys(i); - for (u = 0; u < w.length; ++u) - 'removeListener' !== (_ = w[u]) && this.removeAllListeners(_); - return ( - this.removeAllListeners('removeListener'), - (this._events = Object.create(null)), - (this._eventsCount = 0), - this - ); - } - if ('function' == typeof (o = i[s])) this.removeListener(s, o); - else if (void 0 !== o) - for (u = o.length - 1; u >= 0; u--) this.removeListener(s, o[u]); - return this; - }), - (EventEmitter.prototype.listeners = function listeners(s) { - return _listeners(this, s, !0); - }), - (EventEmitter.prototype.rawListeners = function rawListeners(s) { - return _listeners(this, s, !1); - }), - (EventEmitter.listenerCount = function (s, o) { - return 'function' == typeof s.listenerCount - ? s.listenerCount(o) - : listenerCount.call(s, o); - }), - (EventEmitter.prototype.listenerCount = listenerCount), - (EventEmitter.prototype.eventNames = function eventNames() { - return this._eventsCount > 0 ? o(this._events) : []; - }); - }, - 85587: (s, o, i) => { - 'use strict'; - var u = i(26311), - _ = create(Error); - function create(s) { - return (FormattedError.displayName = s.displayName || s.name), FormattedError; - function FormattedError(o) { - return o && (o = u.apply(null, arguments)), new s(o); - } - } - (s.exports = _), - (_.eval = create(EvalError)), - (_.range = create(RangeError)), - (_.reference = create(ReferenceError)), - (_.syntax = create(SyntaxError)), - (_.type = create(TypeError)), - (_.uri = create(URIError)), - (_.create = create); - }, - 26311: (s) => { - !(function () { - var o; - function format(s) { - for ( - var o, - i, - u, - _, - w = 1, - x = [].slice.call(arguments), - C = 0, - j = s.length, - L = '', - B = !1, - $ = !1, - nextArg = function () { - return x[w++]; - }, - slurpNumber = function () { - for (var i = ''; /\d/.test(s[C]); ) (i += s[C++]), (o = s[C]); - return i.length > 0 ? parseInt(i) : null; - }; - C < j; - ++C - ) - if (((o = s[C]), B)) - switch ( - ((B = !1), - '.' == o - ? (($ = !1), (o = s[++C])) - : '0' == o && '.' == s[C + 1] - ? (($ = !0), (o = s[(C += 2)])) - : ($ = !0), - (_ = slurpNumber()), - o) - ) { - case 'b': - L += parseInt(nextArg(), 10).toString(2); - break; - case 'c': - L += - 'string' == typeof (i = nextArg()) || i instanceof String - ? i - : String.fromCharCode(parseInt(i, 10)); - break; - case 'd': - L += parseInt(nextArg(), 10); - break; - case 'f': - (u = String(parseFloat(nextArg()).toFixed(_ || 6))), - (L += $ ? u : u.replace(/^0/, '')); - break; - case 'j': - L += JSON.stringify(nextArg()); - break; - case 'o': - L += '0' + parseInt(nextArg(), 10).toString(8); - break; - case 's': - L += nextArg(); - break; - case 'x': - L += '0x' + parseInt(nextArg(), 10).toString(16); - break; - case 'X': - L += '0x' + parseInt(nextArg(), 10).toString(16).toUpperCase(); - break; - default: - L += o; - } - else '%' === o ? (B = !0) : (L += o); - return L; - } - ((o = s.exports = format).format = format), - (o.vsprintf = function vsprintf(s, o) { - return format.apply(null, [s].concat(o)); - }), - 'undefined' != typeof console && - 'function' == typeof console.log && - (o.printf = function printf() { - console.log(format.apply(null, arguments)); - }); - })(); - }, - 45981: (s) => { - function deepFreeze(s) { - return ( - s instanceof Map - ? (s.clear = - s.delete = - s.set = - function () { - throw new Error('map is read-only'); - }) - : s instanceof Set && - (s.add = - s.clear = - s.delete = - function () { - throw new Error('set is read-only'); - }), - Object.freeze(s), - Object.getOwnPropertyNames(s).forEach(function (o) { - var i = s[o]; - 'object' != typeof i || Object.isFrozen(i) || deepFreeze(i); - }), - s - ); - } - var o = deepFreeze, - i = deepFreeze; - o.default = i; - class Response { - constructor(s) { - void 0 === s.data && (s.data = {}), (this.data = s.data), (this.isMatchIgnored = !1); - } - ignoreMatch() { - this.isMatchIgnored = !0; - } - } - function escapeHTML(s) { - return s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - function inherit(s, ...o) { - const i = Object.create(null); - for (const o in s) i[o] = s[o]; - return ( - o.forEach(function (s) { - for (const o in s) i[o] = s[o]; - }), - i - ); - } - const emitsWrappingTags = (s) => !!s.kind; - class HTMLRenderer { - constructor(s, o) { - (this.buffer = ''), (this.classPrefix = o.classPrefix), s.walk(this); - } - addText(s) { - this.buffer += escapeHTML(s); - } - openNode(s) { - if (!emitsWrappingTags(s)) return; - let o = s.kind; - s.sublanguage || (o = `${this.classPrefix}${o}`), this.span(o); - } - closeNode(s) { - emitsWrappingTags(s) && (this.buffer += ''); - } - value() { - return this.buffer; - } - span(s) { - this.buffer += ``; - } - } - class TokenTree { - constructor() { - (this.rootNode = { children: [] }), (this.stack = [this.rootNode]); - } - get top() { - return this.stack[this.stack.length - 1]; - } - get root() { - return this.rootNode; - } - add(s) { - this.top.children.push(s); - } - openNode(s) { - const o = { kind: s, children: [] }; - this.add(o), this.stack.push(o); - } - closeNode() { - if (this.stack.length > 1) return this.stack.pop(); - } - closeAllNodes() { - for (; this.closeNode(); ); - } - toJSON() { - return JSON.stringify(this.rootNode, null, 4); - } - walk(s) { - return this.constructor._walk(s, this.rootNode); - } - static _walk(s, o) { - return ( - 'string' == typeof o - ? s.addText(o) - : o.children && - (s.openNode(o), o.children.forEach((o) => this._walk(s, o)), s.closeNode(o)), - s - ); - } - static _collapse(s) { - 'string' != typeof s && - s.children && - (s.children.every((s) => 'string' == typeof s) - ? (s.children = [s.children.join('')]) - : s.children.forEach((s) => { - TokenTree._collapse(s); - })); - } - } - class TokenTreeEmitter extends TokenTree { - constructor(s) { - super(), (this.options = s); - } - addKeyword(s, o) { - '' !== s && (this.openNode(o), this.addText(s), this.closeNode()); - } - addText(s) { - '' !== s && this.add(s); - } - addSublanguage(s, o) { - const i = s.root; - (i.kind = o), (i.sublanguage = !0), this.add(i); - } - toHTML() { - return new HTMLRenderer(this, this.options).value(); - } - finalize() { - return !0; - } - } - function source(s) { - return s ? ('string' == typeof s ? s : s.source) : null; - } - const u = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; - const _ = '[a-zA-Z]\\w*', - w = '[a-zA-Z_]\\w*', - x = '\\b\\d+(\\.\\d+)?', - C = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)', - j = '\\b(0b[01]+)', - L = { begin: '\\\\[\\s\\S]', relevance: 0 }, - B = { className: 'string', begin: "'", end: "'", illegal: '\\n', contains: [L] }, - $ = { className: 'string', begin: '"', end: '"', illegal: '\\n', contains: [L] }, - V = { - begin: - /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ - }, - COMMENT = function (s, o, i = {}) { - const u = inherit({ className: 'comment', begin: s, end: o, contains: [] }, i); - return ( - u.contains.push(V), - u.contains.push({ - className: 'doctag', - begin: '(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):', - relevance: 0 - }), - u - ); - }, - U = COMMENT('//', '$'), - z = COMMENT('/\\*', '\\*/'), - Y = COMMENT('#', '$'), - Z = { className: 'number', begin: x, relevance: 0 }, - ee = { className: 'number', begin: C, relevance: 0 }, - ie = { className: 'number', begin: j, relevance: 0 }, - ae = { - className: 'number', - begin: - x + - '(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?', - relevance: 0 - }, - le = { - begin: /(?=\/[^/\n]*\/)/, - contains: [ - { - className: 'regexp', - begin: /\//, - end: /\/[gimuy]*/, - illegal: /\n/, - contains: [L, { begin: /\[/, end: /\]/, relevance: 0, contains: [L] }] - } - ] - }, - ce = { className: 'title', begin: _, relevance: 0 }, - pe = { className: 'title', begin: w, relevance: 0 }, - de = { begin: '\\.\\s*' + w, relevance: 0 }; - var fe = Object.freeze({ - __proto__: null, - MATCH_NOTHING_RE: /\b\B/, - IDENT_RE: _, - UNDERSCORE_IDENT_RE: w, - NUMBER_RE: x, - C_NUMBER_RE: C, - BINARY_NUMBER_RE: j, - RE_STARTERS_RE: - '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~', - SHEBANG: (s = {}) => { - const o = /^#![ ]*\//; - return ( - s.binary && - (s.begin = (function concat(...s) { - return s.map((s) => source(s)).join(''); - })(o, /.*\b/, s.binary, /\b.*/)), - inherit( - { - className: 'meta', - begin: o, - end: /$/, - relevance: 0, - 'on:begin': (s, o) => { - 0 !== s.index && o.ignoreMatch(); - } - }, - s - ) - ); - }, - BACKSLASH_ESCAPE: L, - APOS_STRING_MODE: B, - QUOTE_STRING_MODE: $, - PHRASAL_WORDS_MODE: V, - COMMENT, - C_LINE_COMMENT_MODE: U, - C_BLOCK_COMMENT_MODE: z, - HASH_COMMENT_MODE: Y, - NUMBER_MODE: Z, - C_NUMBER_MODE: ee, - BINARY_NUMBER_MODE: ie, - CSS_NUMBER_MODE: ae, - REGEXP_MODE: le, - TITLE_MODE: ce, - UNDERSCORE_TITLE_MODE: pe, - METHOD_GUARD: de, - END_SAME_AS_BEGIN: function (s) { - return Object.assign(s, { - 'on:begin': (s, o) => { - o.data._beginMatch = s[1]; - }, - 'on:end': (s, o) => { - o.data._beginMatch !== s[1] && o.ignoreMatch(); - } - }); - } - }); - function skipIfhasPrecedingDot(s, o) { - '.' === s.input[s.index - 1] && o.ignoreMatch(); - } - function beginKeywords(s, o) { - o && - s.beginKeywords && - ((s.begin = '\\b(' + s.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)'), - (s.__beforeBegin = skipIfhasPrecedingDot), - (s.keywords = s.keywords || s.beginKeywords), - delete s.beginKeywords, - void 0 === s.relevance && (s.relevance = 0)); - } - function compileIllegal(s, o) { - Array.isArray(s.illegal) && - (s.illegal = (function either(...s) { - return '(' + s.map((s) => source(s)).join('|') + ')'; - })(...s.illegal)); - } - function compileMatch(s, o) { - if (s.match) { - if (s.begin || s.end) throw new Error('begin & end are not supported with match'); - (s.begin = s.match), delete s.match; - } - } - function compileRelevance(s, o) { - void 0 === s.relevance && (s.relevance = 1); - } - const ye = [ - 'of', - 'and', - 'for', - 'in', - 'not', - 'or', - 'if', - 'then', - 'parent', - 'list', - 'value' - ]; - function compileKeywords(s, o, i = 'keyword') { - const u = {}; - return ( - 'string' == typeof s - ? compileList(i, s.split(' ')) - : Array.isArray(s) - ? compileList(i, s) - : Object.keys(s).forEach(function (i) { - Object.assign(u, compileKeywords(s[i], o, i)); - }), - u - ); - function compileList(s, i) { - o && (i = i.map((s) => s.toLowerCase())), - i.forEach(function (o) { - const i = o.split('|'); - u[i[0]] = [s, scoreForKeyword(i[0], i[1])]; - }); - } - } - function scoreForKeyword(s, o) { - return o - ? Number(o) - : (function commonKeyword(s) { - return ye.includes(s.toLowerCase()); - })(s) - ? 0 - : 1; - } - function compileLanguage(s, { plugins: o }) { - function langRe(o, i) { - return new RegExp(source(o), 'm' + (s.case_insensitive ? 'i' : '') + (i ? 'g' : '')); - } - class MultiRegex { - constructor() { - (this.matchIndexes = {}), - (this.regexes = []), - (this.matchAt = 1), - (this.position = 0); - } - addRule(s, o) { - (o.position = this.position++), - (this.matchIndexes[this.matchAt] = o), - this.regexes.push([o, s]), - (this.matchAt += - (function countMatchGroups(s) { - return new RegExp(s.toString() + '|').exec('').length - 1; - })(s) + 1); - } - compile() { - 0 === this.regexes.length && (this.exec = () => null); - const s = this.regexes.map((s) => s[1]); - (this.matcherRe = langRe( - (function join(s, o = '|') { - let i = 0; - return s - .map((s) => { - i += 1; - const o = i; - let _ = source(s), - w = ''; - for (; _.length > 0; ) { - const s = u.exec(_); - if (!s) { - w += _; - break; - } - (w += _.substring(0, s.index)), - (_ = _.substring(s.index + s[0].length)), - '\\' === s[0][0] && s[1] - ? (w += '\\' + String(Number(s[1]) + o)) - : ((w += s[0]), '(' === s[0] && i++); - } - return w; - }) - .map((s) => `(${s})`) - .join(o); - })(s), - !0 - )), - (this.lastIndex = 0); - } - exec(s) { - this.matcherRe.lastIndex = this.lastIndex; - const o = this.matcherRe.exec(s); - if (!o) return null; - const i = o.findIndex((s, o) => o > 0 && void 0 !== s), - u = this.matchIndexes[i]; - return o.splice(0, i), Object.assign(o, u); - } - } - class ResumableMultiRegex { - constructor() { - (this.rules = []), - (this.multiRegexes = []), - (this.count = 0), - (this.lastIndex = 0), - (this.regexIndex = 0); - } - getMatcher(s) { - if (this.multiRegexes[s]) return this.multiRegexes[s]; - const o = new MultiRegex(); - return ( - this.rules.slice(s).forEach(([s, i]) => o.addRule(s, i)), - o.compile(), - (this.multiRegexes[s] = o), - o - ); - } - resumingScanAtSamePosition() { - return 0 !== this.regexIndex; - } - considerAll() { - this.regexIndex = 0; - } - addRule(s, o) { - this.rules.push([s, o]), 'begin' === o.type && this.count++; - } - exec(s) { - const o = this.getMatcher(this.regexIndex); - o.lastIndex = this.lastIndex; - let i = o.exec(s); - if (this.resumingScanAtSamePosition()) - if (i && i.index === this.lastIndex); - else { - const o = this.getMatcher(0); - (o.lastIndex = this.lastIndex + 1), (i = o.exec(s)); - } - return ( - i && - ((this.regexIndex += i.position + 1), - this.regexIndex === this.count && this.considerAll()), - i - ); - } - } - if ( - (s.compilerExtensions || (s.compilerExtensions = []), - s.contains && s.contains.includes('self')) - ) - throw new Error( - 'ERR: contains `self` is not supported at the top-level of a language. See documentation.' - ); - return ( - (s.classNameAliases = inherit(s.classNameAliases || {})), - (function compileMode(o, i) { - const u = o; - if (o.isCompiled) return u; - [compileMatch].forEach((s) => s(o, i)), - s.compilerExtensions.forEach((s) => s(o, i)), - (o.__beforeBegin = null), - [beginKeywords, compileIllegal, compileRelevance].forEach((s) => s(o, i)), - (o.isCompiled = !0); - let _ = null; - if ( - ('object' == typeof o.keywords && - ((_ = o.keywords.$pattern), delete o.keywords.$pattern), - o.keywords && (o.keywords = compileKeywords(o.keywords, s.case_insensitive)), - o.lexemes && _) - ) - throw new Error( - 'ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ' - ); - return ( - (_ = _ || o.lexemes || /\w+/), - (u.keywordPatternRe = langRe(_, !0)), - i && - (o.begin || (o.begin = /\B|\b/), - (u.beginRe = langRe(o.begin)), - o.endSameAsBegin && (o.end = o.begin), - o.end || o.endsWithParent || (o.end = /\B|\b/), - o.end && (u.endRe = langRe(o.end)), - (u.terminatorEnd = source(o.end) || ''), - o.endsWithParent && - i.terminatorEnd && - (u.terminatorEnd += (o.end ? '|' : '') + i.terminatorEnd)), - o.illegal && (u.illegalRe = langRe(o.illegal)), - o.contains || (o.contains = []), - (o.contains = [].concat( - ...o.contains.map(function (s) { - return (function expandOrCloneMode(s) { - s.variants && - !s.cachedVariants && - (s.cachedVariants = s.variants.map(function (o) { - return inherit(s, { variants: null }, o); - })); - if (s.cachedVariants) return s.cachedVariants; - if (dependencyOnParent(s)) - return inherit(s, { starts: s.starts ? inherit(s.starts) : null }); - if (Object.isFrozen(s)) return inherit(s); - return s; - })('self' === s ? o : s); - }) - )), - o.contains.forEach(function (s) { - compileMode(s, u); - }), - o.starts && compileMode(o.starts, i), - (u.matcher = (function buildModeRegex(s) { - const o = new ResumableMultiRegex(); - return ( - s.contains.forEach((s) => o.addRule(s.begin, { rule: s, type: 'begin' })), - s.terminatorEnd && o.addRule(s.terminatorEnd, { type: 'end' }), - s.illegal && o.addRule(s.illegal, { type: 'illegal' }), - o - ); - })(u)), - u - ); - })(s) - ); - } - function dependencyOnParent(s) { - return !!s && (s.endsWithParent || dependencyOnParent(s.starts)); - } - function BuildVuePlugin(s) { - const o = { - props: ['language', 'code', 'autodetect'], - data: function () { - return { detectedLanguage: '', unknownLanguage: !1 }; - }, - computed: { - className() { - return this.unknownLanguage ? '' : 'hljs ' + this.detectedLanguage; - }, - highlighted() { - if (!this.autoDetect && !s.getLanguage(this.language)) - return ( - console.warn( - `The language "${this.language}" you specified could not be found.` - ), - (this.unknownLanguage = !0), - escapeHTML(this.code) - ); - let o = {}; - return ( - this.autoDetect - ? ((o = s.highlightAuto(this.code)), (this.detectedLanguage = o.language)) - : ((o = s.highlight(this.language, this.code, this.ignoreIllegals)), - (this.detectedLanguage = this.language)), - o.value - ); - }, - autoDetect() { - return ( - !this.language || - (function hasValueOrEmptyAttribute(s) { - return Boolean(s || '' === s); - })(this.autodetect) - ); - }, - ignoreIllegals: () => !0 - }, - render(s) { - return s('pre', {}, [ - s('code', { class: this.className, domProps: { innerHTML: this.highlighted } }) - ]); - } - }; - return { - Component: o, - VuePlugin: { - install(s) { - s.component('highlightjs', o); - } - } - }; - } - const be = { - 'after:highlightElement': ({ el: s, result: o, text: i }) => { - const u = nodeStream(s); - if (!u.length) return; - const _ = document.createElement('div'); - (_.innerHTML = o.value), - (o.value = (function mergeStreams(s, o, i) { - let u = 0, - _ = ''; - const w = []; - function selectStream() { - return s.length && o.length - ? s[0].offset !== o[0].offset - ? s[0].offset < o[0].offset - ? s - : o - : 'start' === o[0].event - ? s - : o - : s.length - ? s - : o; - } - function open(s) { - function attributeString(s) { - return ' ' + s.nodeName + '="' + escapeHTML(s.value) + '"'; - } - _ += '<' + tag(s) + [].map.call(s.attributes, attributeString).join('') + '>'; - } - function close(s) { - _ += ''; - } - function render(s) { - ('start' === s.event ? open : close)(s.node); - } - for (; s.length || o.length; ) { - let o = selectStream(); - if ( - ((_ += escapeHTML(i.substring(u, o[0].offset))), (u = o[0].offset), o === s) - ) { - w.reverse().forEach(close); - do { - render(o.splice(0, 1)[0]), (o = selectStream()); - } while (o === s && o.length && o[0].offset === u); - w.reverse().forEach(open); - } else - 'start' === o[0].event ? w.push(o[0].node) : w.pop(), - render(o.splice(0, 1)[0]); - } - return _ + escapeHTML(i.substr(u)); - })(u, nodeStream(_), i)); - } - }; - function tag(s) { - return s.nodeName.toLowerCase(); - } - function nodeStream(s) { - const o = []; - return ( - (function _nodeStream(s, i) { - for (let u = s.firstChild; u; u = u.nextSibling) - 3 === u.nodeType - ? (i += u.nodeValue.length) - : 1 === u.nodeType && - (o.push({ event: 'start', offset: i, node: u }), - (i = _nodeStream(u, i)), - tag(u).match(/br|hr|img|input/) || - o.push({ event: 'stop', offset: i, node: u })); - return i; - })(s, 0), - o - ); - } - const _e = {}, - error = (s) => { - console.error(s); - }, - warn = (s, ...o) => { - console.log(`WARN: ${s}`, ...o); - }, - deprecated = (s, o) => { - _e[`${s}/${o}`] || - (console.log(`Deprecated as of ${s}. ${o}`), (_e[`${s}/${o}`] = !0)); - }, - we = escapeHTML, - Se = inherit, - xe = Symbol('nomatch'); - var Pe = (function (s) { - const i = Object.create(null), - u = Object.create(null), - _ = []; - let w = !0; - const x = /(^(<[^>]+>|\t|)+|\n)/gm, - C = - "Could not find the language '{}', did you forget to load/include a language module?", - j = { disableAutodetect: !0, name: 'Plain text', contains: [] }; - let L = { - noHighlightRe: /^(no-?highlight)$/i, - languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, - classPrefix: 'hljs-', - tabReplace: null, - useBR: !1, - languages: null, - __emitter: TokenTreeEmitter - }; - function shouldNotHighlight(s) { - return L.noHighlightRe.test(s); - } - function highlight(s, o, i, u) { - let _ = '', - w = ''; - 'object' == typeof o - ? ((_ = s), (i = o.ignoreIllegals), (w = o.language), (u = void 0)) - : (deprecated('10.7.0', 'highlight(lang, code, ...args) has been deprecated.'), - deprecated( - '10.7.0', - 'Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277' - ), - (w = s), - (_ = o)); - const x = { code: _, language: w }; - fire('before:highlight', x); - const C = x.result ? x.result : _highlight(x.language, x.code, i, u); - return (C.code = x.code), fire('after:highlight', C), C; - } - function _highlight(s, o, u, x) { - function keywordData(s, o) { - const i = B.case_insensitive ? o[0].toLowerCase() : o[0]; - return Object.prototype.hasOwnProperty.call(s.keywords, i) && s.keywords[i]; - } - function processBuffer() { - null != U.subLanguage - ? (function processSubLanguage() { - if ('' === Z) return; - let s = null; - if ('string' == typeof U.subLanguage) { - if (!i[U.subLanguage]) return void Y.addText(Z); - (s = _highlight(U.subLanguage, Z, !0, z[U.subLanguage])), - (z[U.subLanguage] = s.top); - } else s = highlightAuto(Z, U.subLanguage.length ? U.subLanguage : null); - U.relevance > 0 && (ee += s.relevance), - Y.addSublanguage(s.emitter, s.language); - })() - : (function processKeywords() { - if (!U.keywords) return void Y.addText(Z); - let s = 0; - U.keywordPatternRe.lastIndex = 0; - let o = U.keywordPatternRe.exec(Z), - i = ''; - for (; o; ) { - i += Z.substring(s, o.index); - const u = keywordData(U, o); - if (u) { - const [s, _] = u; - if ((Y.addText(i), (i = ''), (ee += _), s.startsWith('_'))) i += o[0]; - else { - const i = B.classNameAliases[s] || s; - Y.addKeyword(o[0], i); - } - } else i += o[0]; - (s = U.keywordPatternRe.lastIndex), (o = U.keywordPatternRe.exec(Z)); - } - (i += Z.substr(s)), Y.addText(i); - })(), - (Z = ''); - } - function startNewMode(s) { - return ( - s.className && Y.openNode(B.classNameAliases[s.className] || s.className), - (U = Object.create(s, { parent: { value: U } })), - U - ); - } - function endOfMode(s, o, i) { - let u = (function startsWith(s, o) { - const i = s && s.exec(o); - return i && 0 === i.index; - })(s.endRe, i); - if (u) { - if (s['on:end']) { - const i = new Response(s); - s['on:end'](o, i), i.isMatchIgnored && (u = !1); - } - if (u) { - for (; s.endsParent && s.parent; ) s = s.parent; - return s; - } - } - if (s.endsWithParent) return endOfMode(s.parent, o, i); - } - function doIgnore(s) { - return 0 === U.matcher.regexIndex ? ((Z += s[0]), 1) : ((le = !0), 0); - } - function doBeginMatch(s) { - const o = s[0], - i = s.rule, - u = new Response(i), - _ = [i.__beforeBegin, i['on:begin']]; - for (const i of _) if (i && (i(s, u), u.isMatchIgnored)) return doIgnore(o); - return ( - i && - i.endSameAsBegin && - (i.endRe = (function escape(s) { - return new RegExp(s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); - })(o)), - i.skip - ? (Z += o) - : (i.excludeBegin && (Z += o), - processBuffer(), - i.returnBegin || i.excludeBegin || (Z = o)), - startNewMode(i), - i.returnBegin ? 0 : o.length - ); - } - function doEndMatch(s) { - const i = s[0], - u = o.substr(s.index), - _ = endOfMode(U, s, u); - if (!_) return xe; - const w = U; - w.skip - ? (Z += i) - : (w.returnEnd || w.excludeEnd || (Z += i), - processBuffer(), - w.excludeEnd && (Z = i)); - do { - U.className && Y.closeNode(), - U.skip || U.subLanguage || (ee += U.relevance), - (U = U.parent); - } while (U !== _.parent); - return ( - _.starts && - (_.endSameAsBegin && (_.starts.endRe = _.endRe), startNewMode(_.starts)), - w.returnEnd ? 0 : i.length - ); - } - let j = {}; - function processLexeme(i, _) { - const x = _ && _[0]; - if (((Z += i), null == x)) return processBuffer(), 0; - if ('begin' === j.type && 'end' === _.type && j.index === _.index && '' === x) { - if (((Z += o.slice(_.index, _.index + 1)), !w)) { - const o = new Error('0 width match regex'); - throw ((o.languageName = s), (o.badRule = j.rule), o); - } - return 1; - } - if (((j = _), 'begin' === _.type)) return doBeginMatch(_); - if ('illegal' === _.type && !u) { - const s = new Error( - 'Illegal lexeme "' + x + '" for mode "' + (U.className || '') + '"' - ); - throw ((s.mode = U), s); - } - if ('end' === _.type) { - const s = doEndMatch(_); - if (s !== xe) return s; - } - if ('illegal' === _.type && '' === x) return 1; - if (ae > 1e5 && ae > 3 * _.index) { - throw new Error('potential infinite loop, way more iterations than matches'); - } - return (Z += x), x.length; - } - const B = getLanguage(s); - if (!B) throw (error(C.replace('{}', s)), new Error('Unknown language: "' + s + '"')); - const $ = compileLanguage(B, { plugins: _ }); - let V = '', - U = x || $; - const z = {}, - Y = new L.__emitter(L); - !(function processContinuations() { - const s = []; - for (let o = U; o !== B; o = o.parent) o.className && s.unshift(o.className); - s.forEach((s) => Y.openNode(s)); - })(); - let Z = '', - ee = 0, - ie = 0, - ae = 0, - le = !1; - try { - for (U.matcher.considerAll(); ; ) { - ae++, le ? (le = !1) : U.matcher.considerAll(), (U.matcher.lastIndex = ie); - const s = U.matcher.exec(o); - if (!s) break; - const i = processLexeme(o.substring(ie, s.index), s); - ie = s.index + i; - } - return ( - processLexeme(o.substr(ie)), - Y.closeAllNodes(), - Y.finalize(), - (V = Y.toHTML()), - { - relevance: Math.floor(ee), - value: V, - language: s, - illegal: !1, - emitter: Y, - top: U - } - ); - } catch (i) { - if (i.message && i.message.includes('Illegal')) - return { - illegal: !0, - illegalBy: { - msg: i.message, - context: o.slice(ie - 100, ie + 100), - mode: i.mode - }, - sofar: V, - relevance: 0, - value: we(o), - emitter: Y - }; - if (w) - return { - illegal: !1, - relevance: 0, - value: we(o), - emitter: Y, - language: s, - top: U, - errorRaised: i - }; - throw i; - } - } - function highlightAuto(s, o) { - o = o || L.languages || Object.keys(i); - const u = (function justTextHighlightResult(s) { - const o = { - relevance: 0, - emitter: new L.__emitter(L), - value: we(s), - illegal: !1, - top: j - }; - return o.emitter.addText(s), o; - })(s), - _ = o - .filter(getLanguage) - .filter(autoDetection) - .map((o) => _highlight(o, s, !1)); - _.unshift(u); - const w = _.sort((s, o) => { - if (s.relevance !== o.relevance) return o.relevance - s.relevance; - if (s.language && o.language) { - if (getLanguage(s.language).supersetOf === o.language) return 1; - if (getLanguage(o.language).supersetOf === s.language) return -1; - } - return 0; - }), - [x, C] = w, - B = x; - return (B.second_best = C), B; - } - const B = { - 'before:highlightElement': ({ el: s }) => { - L.useBR && - (s.innerHTML = s.innerHTML.replace(/\n/g, '').replace(//g, '\n')); - }, - 'after:highlightElement': ({ result: s }) => { - L.useBR && (s.value = s.value.replace(/\n/g, '
')); - } - }, - $ = /^(<[^>]+>|\t)+/gm, - V = { - 'after:highlightElement': ({ result: s }) => { - L.tabReplace && - (s.value = s.value.replace($, (s) => s.replace(/\t/g, L.tabReplace))); - } - }; - function highlightElement(s) { - let o = null; - const i = (function blockLanguage(s) { - let o = s.className + ' '; - o += s.parentNode ? s.parentNode.className : ''; - const i = L.languageDetectRe.exec(o); - if (i) { - const o = getLanguage(i[1]); - return ( - o || - (warn(C.replace('{}', i[1])), - warn('Falling back to no-highlight mode for this block.', s)), - o ? i[1] : 'no-highlight' - ); - } - return o.split(/\s+/).find((s) => shouldNotHighlight(s) || getLanguage(s)); - })(s); - if (shouldNotHighlight(i)) return; - fire('before:highlightElement', { el: s, language: i }), (o = s); - const _ = o.textContent, - w = i ? highlight(_, { language: i, ignoreIllegals: !0 }) : highlightAuto(_); - fire('after:highlightElement', { el: s, result: w, text: _ }), - (s.innerHTML = w.value), - (function updateClassName(s, o, i) { - const _ = o ? u[o] : i; - s.classList.add('hljs'), _ && s.classList.add(_); - })(s, i, w.language), - (s.result = { language: w.language, re: w.relevance, relavance: w.relevance }), - w.second_best && - (s.second_best = { - language: w.second_best.language, - re: w.second_best.relevance, - relavance: w.second_best.relevance - }); - } - const initHighlighting = () => { - if (initHighlighting.called) return; - (initHighlighting.called = !0), - deprecated( - '10.6.0', - 'initHighlighting() is deprecated. Use highlightAll() instead.' - ); - document.querySelectorAll('pre code').forEach(highlightElement); - }; - let U = !1; - function highlightAll() { - if ('loading' === document.readyState) return void (U = !0); - document.querySelectorAll('pre code').forEach(highlightElement); - } - function getLanguage(s) { - return (s = (s || '').toLowerCase()), i[s] || i[u[s]]; - } - function registerAliases(s, { languageName: o }) { - 'string' == typeof s && (s = [s]), - s.forEach((s) => { - u[s.toLowerCase()] = o; - }); - } - function autoDetection(s) { - const o = getLanguage(s); - return o && !o.disableAutodetect; - } - function fire(s, o) { - const i = s; - _.forEach(function (s) { - s[i] && s[i](o); - }); - } - 'undefined' != typeof window && - window.addEventListener && - window.addEventListener( - 'DOMContentLoaded', - function boot() { - U && highlightAll(); - }, - !1 - ), - Object.assign(s, { - highlight, - highlightAuto, - highlightAll, - fixMarkup: function deprecateFixMarkup(s) { - return ( - deprecated('10.2.0', 'fixMarkup will be removed entirely in v11.0'), - deprecated( - '10.2.0', - 'Please see https://github.com/highlightjs/highlight.js/issues/2534' - ), - (function fixMarkup(s) { - return L.tabReplace || L.useBR - ? s.replace(x, (s) => - '\n' === s - ? L.useBR - ? '
' - : s - : L.tabReplace - ? s.replace(/\t/g, L.tabReplace) - : s - ) - : s; - })(s) - ); - }, - highlightElement, - highlightBlock: function deprecateHighlightBlock(s) { - return ( - deprecated('10.7.0', 'highlightBlock will be removed entirely in v12.0'), - deprecated('10.7.0', 'Please use highlightElement now.'), - highlightElement(s) - ); - }, - configure: function configure(s) { - s.useBR && - (deprecated('10.3.0', "'useBR' will be removed entirely in v11.0"), - deprecated( - '10.3.0', - 'Please see https://github.com/highlightjs/highlight.js/issues/2559' - )), - (L = Se(L, s)); - }, - initHighlighting, - initHighlightingOnLoad: function initHighlightingOnLoad() { - deprecated( - '10.6.0', - 'initHighlightingOnLoad() is deprecated. Use highlightAll() instead.' - ), - (U = !0); - }, - registerLanguage: function registerLanguage(o, u) { - let _ = null; - try { - _ = u(s); - } catch (s) { - if ( - (error( - "Language definition for '{}' could not be registered.".replace('{}', o) - ), - !w) - ) - throw s; - error(s), (_ = j); - } - _.name || (_.name = o), - (i[o] = _), - (_.rawDefinition = u.bind(null, s)), - _.aliases && registerAliases(_.aliases, { languageName: o }); - }, - unregisterLanguage: function unregisterLanguage(s) { - delete i[s]; - for (const o of Object.keys(u)) u[o] === s && delete u[o]; - }, - listLanguages: function listLanguages() { - return Object.keys(i); - }, - getLanguage, - registerAliases, - requireLanguage: function requireLanguage(s) { - deprecated('10.4.0', 'requireLanguage will be removed entirely in v11.'), - deprecated( - '10.4.0', - 'Please see https://github.com/highlightjs/highlight.js/pull/2844' - ); - const o = getLanguage(s); - if (o) return o; - throw new Error( - "The '{}' language is required, but not loaded.".replace('{}', s) - ); - }, - autoDetection, - inherit: Se, - addPlugin: function addPlugin(s) { - !(function upgradePluginAPI(s) { - s['before:highlightBlock'] && - !s['before:highlightElement'] && - (s['before:highlightElement'] = (o) => { - s['before:highlightBlock'](Object.assign({ block: o.el }, o)); - }), - s['after:highlightBlock'] && - !s['after:highlightElement'] && - (s['after:highlightElement'] = (o) => { - s['after:highlightBlock'](Object.assign({ block: o.el }, o)); - }); - })(s), - _.push(s); - }, - vuePlugin: BuildVuePlugin(s).VuePlugin - }), - (s.debugMode = function () { - w = !1; - }), - (s.safeMode = function () { - w = !0; - }), - (s.versionString = '10.7.3'); - for (const s in fe) 'object' == typeof fe[s] && o(fe[s]); - return Object.assign(s, fe), s.addPlugin(B), s.addPlugin(be), s.addPlugin(V), s; - })({}); - s.exports = Pe; - }, - 35344: (s) => { - function concat(...s) { - return s - .map((s) => - (function source(s) { - return s ? ('string' == typeof s ? s : s.source) : null; - })(s) - ) - .join(''); - } - s.exports = function bash(s) { - const o = {}, - i = { begin: /\$\{/, end: /\}/, contains: ['self', { begin: /:-/, contains: [o] }] }; - Object.assign(o, { - className: 'variable', - variants: [{ begin: concat(/\$[\w\d#@][\w\d_]*/, '(?![\\w\\d])(?![$])') }, i] - }); - const u = { - className: 'subst', - begin: /\$\(/, - end: /\)/, - contains: [s.BACKSLASH_ESCAPE] - }, - _ = { - begin: /<<-?\s*(?=\w+)/, - starts: { - contains: [ - s.END_SAME_AS_BEGIN({ begin: /(\w+)/, end: /(\w+)/, className: 'string' }) - ] - } - }, - w = { - className: 'string', - begin: /"/, - end: /"/, - contains: [s.BACKSLASH_ESCAPE, o, u] - }; - u.contains.push(w); - const x = { - begin: /\$\(\(/, - end: /\)\)/, - contains: [{ begin: /\d+#[0-9a-f]+/, className: 'number' }, s.NUMBER_MODE, o] - }, - C = s.SHEBANG({ - binary: `(${['fish', 'bash', 'zsh', 'sh', 'csh', 'ksh', 'tcsh', 'dash', 'scsh'].join('|')})`, - relevance: 10 - }), - j = { - className: 'function', - begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, - returnBegin: !0, - contains: [s.inherit(s.TITLE_MODE, { begin: /\w[\w\d_]*/ })], - relevance: 0 - }; - return { - name: 'Bash', - aliases: ['sh', 'zsh'], - keywords: { - $pattern: /\b[a-z._-]+\b/, - keyword: 'if then else elif fi for while in do done case esac function', - literal: 'true false', - built_in: - 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp' - }, - contains: [ - C, - s.SHEBANG(), - j, - x, - s.HASH_COMMENT_MODE, - _, - w, - { className: '', begin: /\\"/ }, - { className: 'string', begin: /'/, end: /'/ }, - o - ] - }; - }; - }, - 73402: (s) => { - function concat(...s) { - return s - .map((s) => - (function source(s) { - return s ? ('string' == typeof s ? s : s.source) : null; - })(s) - ) - .join(''); - } - s.exports = function http(s) { - const o = 'HTTP/(2|1\\.[01])', - i = { - className: 'attribute', - begin: concat('^', /[A-Za-z][A-Za-z0-9-]*/, '(?=\\:\\s)'), - starts: { - contains: [ - { - className: 'punctuation', - begin: /: /, - relevance: 0, - starts: { end: '$', relevance: 0 } - } - ] - } - }, - u = [i, { begin: '\\n\\n', starts: { subLanguage: [], endsWithParent: !0 } }]; - return { - name: 'HTTP', - aliases: ['https'], - illegal: /\S/, - contains: [ - { - begin: '^(?=' + o + ' \\d{3})', - end: /$/, - contains: [ - { className: 'meta', begin: o }, - { className: 'number', begin: '\\b\\d{3}\\b' } - ], - starts: { end: /\b\B/, illegal: /\S/, contains: u } - }, - { - begin: '(?=^[A-Z]+ (.*?) ' + o + '$)', - end: /$/, - contains: [ - { className: 'string', begin: ' ', end: ' ', excludeBegin: !0, excludeEnd: !0 }, - { className: 'meta', begin: o }, - { className: 'keyword', begin: '[A-Z]+' } - ], - starts: { end: /\b\B/, illegal: /\S/, contains: u } - }, - s.inherit(i, { relevance: 0 }) - ] - }; - }; - }, - 95089: (s) => { - const o = '[A-Za-z$_][0-9A-Za-z$_]*', - i = [ - 'as', - 'in', - 'of', - 'if', - 'for', - 'while', - 'finally', - 'var', - 'new', - 'function', - 'do', - 'return', - 'void', - 'else', - 'break', - 'catch', - 'instanceof', - 'with', - 'throw', - 'case', - 'default', - 'try', - 'switch', - 'continue', - 'typeof', - 'delete', - 'let', - 'yield', - 'const', - 'class', - 'debugger', - 'async', - 'await', - 'static', - 'import', - 'from', - 'export', - 'extends' - ], - u = ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'], - _ = [].concat( - [ - 'setInterval', - 'setTimeout', - 'clearInterval', - 'clearTimeout', - 'require', - 'exports', - 'eval', - 'isFinite', - 'isNaN', - 'parseFloat', - 'parseInt', - 'decodeURI', - 'decodeURIComponent', - 'encodeURI', - 'encodeURIComponent', - 'escape', - 'unescape' - ], - [ - 'arguments', - 'this', - 'super', - 'console', - 'window', - 'document', - 'localStorage', - 'module', - 'global' - ], - [ - 'Intl', - 'DataView', - 'Number', - 'Math', - 'Date', - 'String', - 'RegExp', - 'Object', - 'Function', - 'Boolean', - 'Error', - 'Symbol', - 'Set', - 'Map', - 'WeakSet', - 'WeakMap', - 'Proxy', - 'Reflect', - 'JSON', - 'Promise', - 'Float64Array', - 'Int16Array', - 'Int32Array', - 'Int8Array', - 'Uint16Array', - 'Uint32Array', - 'Float32Array', - 'Array', - 'Uint8Array', - 'Uint8ClampedArray', - 'ArrayBuffer', - 'BigInt64Array', - 'BigUint64Array', - 'BigInt' - ], - [ - 'EvalError', - 'InternalError', - 'RangeError', - 'ReferenceError', - 'SyntaxError', - 'TypeError', - 'URIError' - ] - ); - function lookahead(s) { - return concat('(?=', s, ')'); - } - function concat(...s) { - return s - .map((s) => - (function source(s) { - return s ? ('string' == typeof s ? s : s.source) : null; - })(s) - ) - .join(''); - } - s.exports = function javascript(s) { - const w = o, - x = '<>', - C = '', - j = { - begin: /<[A-Za-z0-9\\._:-]+/, - end: /\/[A-Za-z0-9\\._:-]+>|\/>/, - isTrulyOpeningTag: (s, o) => { - const i = s[0].length + s.index, - u = s.input[i]; - '<' !== u - ? '>' === u && - (((s, { after: o }) => { - const i = '', - returnBegin: !0, - end: '\\s*=>', - contains: [ - { - className: 'params', - variants: [ - { begin: s.UNDERSCORE_IDENT_RE, relevance: 0 }, - { className: null, begin: /\(\s*\)/, skip: !0 }, - { - begin: /\(/, - end: /\)/, - excludeBegin: !0, - excludeEnd: !0, - keywords: L, - contains: ce - } - ] - } - ] - }, - { begin: /,/, relevance: 0 }, - { className: '', begin: /\s/, end: /\s*/, skip: !0 }, - { - variants: [ - { begin: x, end: C }, - { begin: j.begin, 'on:begin': j.isTrulyOpeningTag, end: j.end } - ], - subLanguage: 'xml', - contains: [{ begin: j.begin, end: j.end, skip: !0, contains: ['self'] }] - } - ], - relevance: 0 - }, - { - className: 'function', - beginKeywords: 'function', - end: /[{;]/, - excludeEnd: !0, - keywords: L, - contains: ['self', s.inherit(s.TITLE_MODE, { begin: w }), pe], - illegal: /%/ - }, - { beginKeywords: 'while if switch catch for' }, - { - className: 'function', - begin: - s.UNDERSCORE_IDENT_RE + - '\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{', - returnBegin: !0, - contains: [pe, s.inherit(s.TITLE_MODE, { begin: w })] - }, - { variants: [{ begin: '\\.' + w }, { begin: '\\$' + w }], relevance: 0 }, - { - className: 'class', - beginKeywords: 'class', - end: /[{;=]/, - excludeEnd: !0, - illegal: /[:"[\]]/, - contains: [{ beginKeywords: 'extends' }, s.UNDERSCORE_TITLE_MODE] - }, - { - begin: /\b(?=constructor)/, - end: /[{;]/, - excludeEnd: !0, - contains: [s.inherit(s.TITLE_MODE, { begin: w }), 'self', pe] - }, - { - begin: '(get|set)\\s+(?=' + w + '\\()', - end: /\{/, - keywords: 'get set', - contains: [s.inherit(s.TITLE_MODE, { begin: w }), { begin: /\(\)/ }, pe] - }, - { begin: /\$[(.]/ } - ] - }; - }; - }, - 65772: (s) => { - s.exports = function json(s) { - const o = { literal: 'true false null' }, - i = [s.C_LINE_COMMENT_MODE, s.C_BLOCK_COMMENT_MODE], - u = [s.QUOTE_STRING_MODE, s.C_NUMBER_MODE], - _ = { end: ',', endsWithParent: !0, excludeEnd: !0, contains: u, keywords: o }, - w = { - begin: /\{/, - end: /\}/, - contains: [ - { - className: 'attr', - begin: /"/, - end: /"/, - contains: [s.BACKSLASH_ESCAPE], - illegal: '\\n' - }, - s.inherit(_, { begin: /:/ }) - ].concat(i), - illegal: '\\S' - }, - x = { begin: '\\[', end: '\\]', contains: [s.inherit(_)], illegal: '\\S' }; - return ( - u.push(w, x), - i.forEach(function (s) { - u.push(s); - }), - { name: 'JSON', contains: u, keywords: o, illegal: '\\S' } - ); - }; - }, - 26571: (s) => { - s.exports = function powershell(s) { - const o = { - $pattern: /-?[A-z\.\-]+\b/, - keyword: - 'if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter', - built_in: - 'ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write' - }, - i = { begin: '`[\\s\\S]', relevance: 0 }, - u = { - className: 'variable', - variants: [ - { begin: /\$\B/ }, - { className: 'keyword', begin: /\$this/ }, - { begin: /\$[\w\d][\w\d_:]*/ } - ] - }, - _ = { - className: 'string', - variants: [ - { begin: /"/, end: /"/ }, - { begin: /@"/, end: /^"@/ } - ], - contains: [i, u, { className: 'variable', begin: /\$[A-z]/, end: /[^A-z]/ }] - }, - w = { - className: 'string', - variants: [ - { begin: /'/, end: /'/ }, - { begin: /@'/, end: /^'@/ } - ] - }, - x = s.inherit(s.COMMENT(null, null), { - variants: [ - { begin: /#/, end: /$/ }, - { begin: /<#/, end: /#>/ } - ], - contains: [ - { - className: 'doctag', - variants: [ - { - begin: - /\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/ - }, - { - begin: - /\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/ - } - ] - } - ] - }), - C = { - className: 'built_in', - variants: [ - { - begin: '('.concat( - 'Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where', - ')+(-)[\\w\\d]+' - ) - } - ] - }, - j = { - className: 'class', - beginKeywords: 'class enum', - end: /\s*[{]/, - excludeEnd: !0, - relevance: 0, - contains: [s.TITLE_MODE] - }, - L = { - className: 'function', - begin: /function\s+/, - end: /\s*\{|$/, - excludeEnd: !0, - returnBegin: !0, - relevance: 0, - contains: [ - { begin: 'function', relevance: 0, className: 'keyword' }, - { className: 'title', begin: /\w[\w\d]*((-)[\w\d]+)*/, relevance: 0 }, - { begin: /\(/, end: /\)/, className: 'params', relevance: 0, contains: [u] } - ] - }, - B = { - begin: /using\s/, - end: /$/, - returnBegin: !0, - contains: [ - _, - w, - { className: 'keyword', begin: /(using|assembly|command|module|namespace|type)/ } - ] - }, - $ = { - variants: [ - { - className: 'operator', - begin: '('.concat( - '-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor', - ')\\b' - ) - }, - { className: 'literal', begin: /(-)[\w\d]+/, relevance: 0 } - ] - }, - V = { - className: 'function', - begin: /\[.*\]\s*[\w]+[ ]??\(/, - end: /$/, - returnBegin: !0, - relevance: 0, - contains: [ - { - className: 'keyword', - begin: '('.concat(o.keyword.toString().replace(/\s/g, '|'), ')\\b'), - endsParent: !0, - relevance: 0 - }, - s.inherit(s.TITLE_MODE, { endsParent: !0 }) - ] - }, - U = [ - V, - x, - i, - s.NUMBER_MODE, - _, - w, - C, - u, - { className: 'literal', begin: /\$(null|true|false)\b/ }, - { className: 'selector-tag', begin: /@\B/, relevance: 0 } - ], - z = { - begin: /\[/, - end: /\]/, - excludeBegin: !0, - excludeEnd: !0, - relevance: 0, - contains: [].concat( - 'self', - U, - { - begin: - '(' + - [ - 'string', - 'char', - 'byte', - 'int', - 'long', - 'bool', - 'decimal', - 'single', - 'double', - 'DateTime', - 'xml', - 'array', - 'hashtable', - 'void' - ].join('|') + - ')', - className: 'built_in', - relevance: 0 - }, - { className: 'type', begin: /[\.\w\d]+/, relevance: 0 } - ) - }; - return ( - V.contains.unshift(z), - { - name: 'PowerShell', - aliases: ['ps', 'ps1'], - case_insensitive: !0, - keywords: o, - contains: U.concat(j, L, B, $, z) - } - ); - }; - }, - 17285: (s) => { - function source(s) { - return s ? ('string' == typeof s ? s : s.source) : null; - } - function lookahead(s) { - return concat('(?=', s, ')'); - } - function concat(...s) { - return s.map((s) => source(s)).join(''); - } - function either(...s) { - return '(' + s.map((s) => source(s)).join('|') + ')'; - } - s.exports = function xml(s) { - const o = concat( - /[A-Z_]/, - (function optional(s) { - return concat('(', s, ')?'); - })(/[A-Z0-9_.-]*:/), - /[A-Z0-9_.-]*/ - ), - i = { className: 'symbol', begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/ }, - u = { - begin: /\s/, - contains: [ - { className: 'meta-keyword', begin: /#?[a-z_][a-z1-9_-]+/, illegal: /\n/ } - ] - }, - _ = s.inherit(u, { begin: /\(/, end: /\)/ }), - w = s.inherit(s.APOS_STRING_MODE, { className: 'meta-string' }), - x = s.inherit(s.QUOTE_STRING_MODE, { className: 'meta-string' }), - C = { - endsWithParent: !0, - illegal: /`]+/ } - ] - } - ] - } - ] - }; - return { - name: 'HTML, XML', - aliases: ['html', 'xhtml', 'rss', 'atom', 'xjb', 'xsd', 'xsl', 'plist', 'wsf', 'svg'], - case_insensitive: !0, - contains: [ - { - className: 'meta', - begin: //, - relevance: 10, - contains: [ - u, - x, - w, - _, - { - begin: /\[/, - end: /\]/, - contains: [ - { className: 'meta', begin: //, contains: [u, _, x, w] } - ] - } - ] - }, - s.COMMENT(//, { relevance: 10 }), - { begin: //, relevance: 10 }, - i, - { className: 'meta', begin: /<\?xml/, end: /\?>/, relevance: 10 }, - { - className: 'tag', - begin: /)/, - end: />/, - keywords: { name: 'style' }, - contains: [C], - starts: { end: /<\/style>/, returnEnd: !0, subLanguage: ['css', 'xml'] } - }, - { - className: 'tag', - begin: /)/, - end: />/, - keywords: { name: 'script' }, - contains: [C], - starts: { - end: /<\/script>/, - returnEnd: !0, - subLanguage: ['javascript', 'handlebars', 'xml'] - } - }, - { className: 'tag', begin: /<>|<\/>/ }, - { - className: 'tag', - begin: concat(//, />/, /\s/)))), - end: /\/?>/, - contains: [{ className: 'name', begin: o, relevance: 0, starts: C }] - }, - { - className: 'tag', - begin: concat(/<\//, lookahead(concat(o, />/))), - contains: [ - { className: 'name', begin: o, relevance: 0 }, - { begin: />/, relevance: 0, endsParent: !0 } - ] - } - ] - }; - }; - }, - 17533: (s) => { - s.exports = function yaml(s) { - var o = 'true false yes no null', - i = "[\\w#;/?:@&=+$,.~*'()[\\]]+", - u = { - className: 'string', - relevance: 0, - variants: [{ begin: /'/, end: /'/ }, { begin: /"/, end: /"/ }, { begin: /\S+/ }], - contains: [ - s.BACKSLASH_ESCAPE, - { - className: 'template-variable', - variants: [ - { begin: /\{\{/, end: /\}\}/ }, - { begin: /%\{/, end: /\}/ } - ] - } - ] - }, - _ = s.inherit(u, { - variants: [ - { begin: /'/, end: /'/ }, - { begin: /"/, end: /"/ }, - { begin: /[^\s,{}[\]]+/ } - ] - }), - w = { - className: 'number', - begin: - '\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b' - }, - x = { end: ',', endsWithParent: !0, excludeEnd: !0, keywords: o, relevance: 0 }, - C = { begin: /\{/, end: /\}/, contains: [x], illegal: '\\n', relevance: 0 }, - j = { begin: '\\[', end: '\\]', contains: [x], illegal: '\\n', relevance: 0 }, - L = [ - { - className: 'attr', - variants: [ - { begin: '\\w[\\w :\\/.-]*:(?=[ \t]|$)' }, - { begin: '"\\w[\\w :\\/.-]*":(?=[ \t]|$)' }, - { begin: "'\\w[\\w :\\/.-]*':(?=[ \t]|$)" } - ] - }, - { className: 'meta', begin: '^---\\s*$', relevance: 10 }, - { - className: 'string', - begin: '[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*' - }, - { - begin: '<%[%=-]?', - end: '[%-]?%>', - subLanguage: 'ruby', - excludeBegin: !0, - excludeEnd: !0, - relevance: 0 - }, - { className: 'type', begin: '!\\w+!' + i }, - { className: 'type', begin: '!<' + i + '>' }, - { className: 'type', begin: '!' + i }, - { className: 'type', begin: '!!' + i }, - { className: 'meta', begin: '&' + s.UNDERSCORE_IDENT_RE + '$' }, - { className: 'meta', begin: '\\*' + s.UNDERSCORE_IDENT_RE + '$' }, - { className: 'bullet', begin: '-(?=[ ]|$)', relevance: 0 }, - s.HASH_COMMENT_MODE, - { beginKeywords: o, keywords: { literal: o } }, - w, - { className: 'number', begin: s.C_NUMBER_RE + '\\b', relevance: 0 }, - C, - j, - u - ], - B = [...L]; - return ( - B.pop(), - B.push(_), - (x.contains = B), - { name: 'YAML', case_insensitive: !0, aliases: ['yml'], contains: L } - ); - }; - }, - 251: (s, o) => { - (o.read = function (s, o, i, u, _) { - var w, - x, - C = 8 * _ - u - 1, - j = (1 << C) - 1, - L = j >> 1, - B = -7, - $ = i ? _ - 1 : 0, - V = i ? -1 : 1, - U = s[o + $]; - for ( - $ += V, w = U & ((1 << -B) - 1), U >>= -B, B += C; - B > 0; - w = 256 * w + s[o + $], $ += V, B -= 8 - ); - for ( - x = w & ((1 << -B) - 1), w >>= -B, B += u; - B > 0; - x = 256 * x + s[o + $], $ += V, B -= 8 - ); - if (0 === w) w = 1 - L; - else { - if (w === j) return x ? NaN : (1 / 0) * (U ? -1 : 1); - (x += Math.pow(2, u)), (w -= L); - } - return (U ? -1 : 1) * x * Math.pow(2, w - u); - }), - (o.write = function (s, o, i, u, _, w) { - var x, - C, - j, - L = 8 * w - _ - 1, - B = (1 << L) - 1, - $ = B >> 1, - V = 23 === _ ? Math.pow(2, -24) - Math.pow(2, -77) : 0, - U = u ? 0 : w - 1, - z = u ? 1 : -1, - Y = o < 0 || (0 === o && 1 / o < 0) ? 1 : 0; - for ( - o = Math.abs(o), - isNaN(o) || o === 1 / 0 - ? ((C = isNaN(o) ? 1 : 0), (x = B)) - : ((x = Math.floor(Math.log(o) / Math.LN2)), - o * (j = Math.pow(2, -x)) < 1 && (x--, (j *= 2)), - (o += x + $ >= 1 ? V / j : V * Math.pow(2, 1 - $)) * j >= 2 && - (x++, (j /= 2)), - x + $ >= B - ? ((C = 0), (x = B)) - : x + $ >= 1 - ? ((C = (o * j - 1) * Math.pow(2, _)), (x += $)) - : ((C = o * Math.pow(2, $ - 1) * Math.pow(2, _)), (x = 0))); - _ >= 8; - s[i + U] = 255 & C, U += z, C /= 256, _ -= 8 - ); - for (x = (x << _) | C, L += _; L > 0; s[i + U] = 255 & x, U += z, x /= 256, L -= 8); - s[i + U - z] |= 128 * Y; - }); - }, - 9404: function (s) { - s.exports = (function () { - 'use strict'; - var s = Array.prototype.slice; - function createClass(s, o) { - o && (s.prototype = Object.create(o.prototype)), (s.prototype.constructor = s); - } - function Iterable(s) { - return isIterable(s) ? s : Seq(s); - } - function KeyedIterable(s) { - return isKeyed(s) ? s : KeyedSeq(s); - } - function IndexedIterable(s) { - return isIndexed(s) ? s : IndexedSeq(s); - } - function SetIterable(s) { - return isIterable(s) && !isAssociative(s) ? s : SetSeq(s); - } - function isIterable(s) { - return !(!s || !s[o]); - } - function isKeyed(s) { - return !(!s || !s[i]); - } - function isIndexed(s) { - return !(!s || !s[u]); - } - function isAssociative(s) { - return isKeyed(s) || isIndexed(s); - } - function isOrdered(s) { - return !(!s || !s[_]); - } - createClass(KeyedIterable, Iterable), - createClass(IndexedIterable, Iterable), - createClass(SetIterable, Iterable), - (Iterable.isIterable = isIterable), - (Iterable.isKeyed = isKeyed), - (Iterable.isIndexed = isIndexed), - (Iterable.isAssociative = isAssociative), - (Iterable.isOrdered = isOrdered), - (Iterable.Keyed = KeyedIterable), - (Iterable.Indexed = IndexedIterable), - (Iterable.Set = SetIterable); - var o = '@@__IMMUTABLE_ITERABLE__@@', - i = '@@__IMMUTABLE_KEYED__@@', - u = '@@__IMMUTABLE_INDEXED__@@', - _ = '@@__IMMUTABLE_ORDERED__@@', - w = 'delete', - x = 5, - C = 1 << x, - j = C - 1, - L = {}, - B = { value: !1 }, - $ = { value: !1 }; - function MakeRef(s) { - return (s.value = !1), s; - } - function SetRef(s) { - s && (s.value = !0); - } - function OwnerID() {} - function arrCopy(s, o) { - o = o || 0; - for (var i = Math.max(0, s.length - o), u = new Array(i), _ = 0; _ < i; _++) - u[_] = s[_ + o]; - return u; - } - function ensureSize(s) { - return void 0 === s.size && (s.size = s.__iterate(returnTrue)), s.size; - } - function wrapIndex(s, o) { - if ('number' != typeof o) { - var i = o >>> 0; - if ('' + i !== o || 4294967295 === i) return NaN; - o = i; - } - return o < 0 ? ensureSize(s) + o : o; - } - function returnTrue() { - return !0; - } - function wholeSlice(s, o, i) { - return ( - (0 === s || (void 0 !== i && s <= -i)) && (void 0 === o || (void 0 !== i && o >= i)) - ); - } - function resolveBegin(s, o) { - return resolveIndex(s, o, 0); - } - function resolveEnd(s, o) { - return resolveIndex(s, o, o); - } - function resolveIndex(s, o, i) { - return void 0 === s - ? i - : s < 0 - ? Math.max(0, o + s) - : void 0 === o - ? s - : Math.min(o, s); - } - var V = 0, - U = 1, - z = 2, - Y = 'function' == typeof Symbol && Symbol.iterator, - Z = '@@iterator', - ee = Y || Z; - function Iterator(s) { - this.next = s; - } - function iteratorValue(s, o, i, u) { - var _ = 0 === s ? o : 1 === s ? i : [o, i]; - return u ? (u.value = _) : (u = { value: _, done: !1 }), u; - } - function iteratorDone() { - return { value: void 0, done: !0 }; - } - function hasIterator(s) { - return !!getIteratorFn(s); - } - function isIterator(s) { - return s && 'function' == typeof s.next; - } - function getIterator(s) { - var o = getIteratorFn(s); - return o && o.call(s); - } - function getIteratorFn(s) { - var o = s && ((Y && s[Y]) || s[Z]); - if ('function' == typeof o) return o; - } - function isArrayLike(s) { - return s && 'number' == typeof s.length; - } - function Seq(s) { - return null == s ? emptySequence() : isIterable(s) ? s.toSeq() : seqFromValue(s); - } - function KeyedSeq(s) { - return null == s - ? emptySequence().toKeyedSeq() - : isIterable(s) - ? isKeyed(s) - ? s.toSeq() - : s.fromEntrySeq() - : keyedSeqFromValue(s); - } - function IndexedSeq(s) { - return null == s - ? emptySequence() - : isIterable(s) - ? isKeyed(s) - ? s.entrySeq() - : s.toIndexedSeq() - : indexedSeqFromValue(s); - } - function SetSeq(s) { - return ( - null == s - ? emptySequence() - : isIterable(s) - ? isKeyed(s) - ? s.entrySeq() - : s - : indexedSeqFromValue(s) - ).toSetSeq(); - } - (Iterator.prototype.toString = function () { - return '[Iterator]'; - }), - (Iterator.KEYS = V), - (Iterator.VALUES = U), - (Iterator.ENTRIES = z), - (Iterator.prototype.inspect = Iterator.prototype.toSource = - function () { - return this.toString(); - }), - (Iterator.prototype[ee] = function () { - return this; - }), - createClass(Seq, Iterable), - (Seq.of = function () { - return Seq(arguments); - }), - (Seq.prototype.toSeq = function () { - return this; - }), - (Seq.prototype.toString = function () { - return this.__toString('Seq {', '}'); - }), - (Seq.prototype.cacheResult = function () { - return ( - !this._cache && - this.__iterateUncached && - ((this._cache = this.entrySeq().toArray()), (this.size = this._cache.length)), - this - ); - }), - (Seq.prototype.__iterate = function (s, o) { - return seqIterate(this, s, o, !0); - }), - (Seq.prototype.__iterator = function (s, o) { - return seqIterator(this, s, o, !0); - }), - createClass(KeyedSeq, Seq), - (KeyedSeq.prototype.toKeyedSeq = function () { - return this; - }), - createClass(IndexedSeq, Seq), - (IndexedSeq.of = function () { - return IndexedSeq(arguments); - }), - (IndexedSeq.prototype.toIndexedSeq = function () { - return this; - }), - (IndexedSeq.prototype.toString = function () { - return this.__toString('Seq [', ']'); - }), - (IndexedSeq.prototype.__iterate = function (s, o) { - return seqIterate(this, s, o, !1); - }), - (IndexedSeq.prototype.__iterator = function (s, o) { - return seqIterator(this, s, o, !1); - }), - createClass(SetSeq, Seq), - (SetSeq.of = function () { - return SetSeq(arguments); - }), - (SetSeq.prototype.toSetSeq = function () { - return this; - }), - (Seq.isSeq = isSeq), - (Seq.Keyed = KeyedSeq), - (Seq.Set = SetSeq), - (Seq.Indexed = IndexedSeq); - var ie, - ae, - le, - ce = '@@__IMMUTABLE_SEQ__@@'; - function ArraySeq(s) { - (this._array = s), (this.size = s.length); - } - function ObjectSeq(s) { - var o = Object.keys(s); - (this._object = s), (this._keys = o), (this.size = o.length); - } - function IterableSeq(s) { - (this._iterable = s), (this.size = s.length || s.size); - } - function IteratorSeq(s) { - (this._iterator = s), (this._iteratorCache = []); - } - function isSeq(s) { - return !(!s || !s[ce]); - } - function emptySequence() { - return ie || (ie = new ArraySeq([])); - } - function keyedSeqFromValue(s) { - var o = Array.isArray(s) - ? new ArraySeq(s).fromEntrySeq() - : isIterator(s) - ? new IteratorSeq(s).fromEntrySeq() - : hasIterator(s) - ? new IterableSeq(s).fromEntrySeq() - : 'object' == typeof s - ? new ObjectSeq(s) - : void 0; - if (!o) - throw new TypeError( - 'Expected Array or iterable object of [k, v] entries, or keyed object: ' + s - ); - return o; - } - function indexedSeqFromValue(s) { - var o = maybeIndexedSeqFromValue(s); - if (!o) throw new TypeError('Expected Array or iterable object of values: ' + s); - return o; - } - function seqFromValue(s) { - var o = maybeIndexedSeqFromValue(s) || ('object' == typeof s && new ObjectSeq(s)); - if (!o) - throw new TypeError( - 'Expected Array or iterable object of values, or keyed object: ' + s - ); - return o; - } - function maybeIndexedSeqFromValue(s) { - return isArrayLike(s) - ? new ArraySeq(s) - : isIterator(s) - ? new IteratorSeq(s) - : hasIterator(s) - ? new IterableSeq(s) - : void 0; - } - function seqIterate(s, o, i, u) { - var _ = s._cache; - if (_) { - for (var w = _.length - 1, x = 0; x <= w; x++) { - var C = _[i ? w - x : x]; - if (!1 === o(C[1], u ? C[0] : x, s)) return x + 1; - } - return x; - } - return s.__iterateUncached(o, i); - } - function seqIterator(s, o, i, u) { - var _ = s._cache; - if (_) { - var w = _.length - 1, - x = 0; - return new Iterator(function () { - var s = _[i ? w - x : x]; - return x++ > w ? iteratorDone() : iteratorValue(o, u ? s[0] : x - 1, s[1]); - }); - } - return s.__iteratorUncached(o, i); - } - function fromJS(s, o) { - return o ? fromJSWith(o, s, '', { '': s }) : fromJSDefault(s); - } - function fromJSWith(s, o, i, u) { - return Array.isArray(o) - ? s.call( - u, - i, - IndexedSeq(o).map(function (i, u) { - return fromJSWith(s, i, u, o); - }) - ) - : isPlainObj(o) - ? s.call( - u, - i, - KeyedSeq(o).map(function (i, u) { - return fromJSWith(s, i, u, o); - }) - ) - : o; - } - function fromJSDefault(s) { - return Array.isArray(s) - ? IndexedSeq(s).map(fromJSDefault).toList() - : isPlainObj(s) - ? KeyedSeq(s).map(fromJSDefault).toMap() - : s; - } - function isPlainObj(s) { - return s && (s.constructor === Object || void 0 === s.constructor); - } - function is(s, o) { - if (s === o || (s != s && o != o)) return !0; - if (!s || !o) return !1; - if ('function' == typeof s.valueOf && 'function' == typeof o.valueOf) { - if ((s = s.valueOf()) === (o = o.valueOf()) || (s != s && o != o)) return !0; - if (!s || !o) return !1; - } - return !( - 'function' != typeof s.equals || - 'function' != typeof o.equals || - !s.equals(o) - ); - } - function deepEqual(s, o) { - if (s === o) return !0; - if ( - !isIterable(o) || - (void 0 !== s.size && void 0 !== o.size && s.size !== o.size) || - (void 0 !== s.__hash && void 0 !== o.__hash && s.__hash !== o.__hash) || - isKeyed(s) !== isKeyed(o) || - isIndexed(s) !== isIndexed(o) || - isOrdered(s) !== isOrdered(o) - ) - return !1; - if (0 === s.size && 0 === o.size) return !0; - var i = !isAssociative(s); - if (isOrdered(s)) { - var u = s.entries(); - return ( - o.every(function (s, o) { - var _ = u.next().value; - return _ && is(_[1], s) && (i || is(_[0], o)); - }) && u.next().done - ); - } - var _ = !1; - if (void 0 === s.size) - if (void 0 === o.size) 'function' == typeof s.cacheResult && s.cacheResult(); - else { - _ = !0; - var w = s; - (s = o), (o = w); - } - var x = !0, - C = o.__iterate(function (o, u) { - if (i ? !s.has(o) : _ ? !is(o, s.get(u, L)) : !is(s.get(u, L), o)) - return (x = !1), !1; - }); - return x && s.size === C; - } - function Repeat(s, o) { - if (!(this instanceof Repeat)) return new Repeat(s, o); - if ( - ((this._value = s), - (this.size = void 0 === o ? 1 / 0 : Math.max(0, o)), - 0 === this.size) - ) { - if (ae) return ae; - ae = this; - } - } - function invariant(s, o) { - if (!s) throw new Error(o); - } - function Range(s, o, i) { - if (!(this instanceof Range)) return new Range(s, o, i); - if ( - (invariant(0 !== i, 'Cannot step a Range by 0'), - (s = s || 0), - void 0 === o && (o = 1 / 0), - (i = void 0 === i ? 1 : Math.abs(i)), - o < s && (i = -i), - (this._start = s), - (this._end = o), - (this._step = i), - (this.size = Math.max(0, Math.ceil((o - s) / i - 1) + 1)), - 0 === this.size) - ) { - if (le) return le; - le = this; - } - } - function Collection() { - throw TypeError('Abstract'); - } - function KeyedCollection() {} - function IndexedCollection() {} - function SetCollection() {} - (Seq.prototype[ce] = !0), - createClass(ArraySeq, IndexedSeq), - (ArraySeq.prototype.get = function (s, o) { - return this.has(s) ? this._array[wrapIndex(this, s)] : o; - }), - (ArraySeq.prototype.__iterate = function (s, o) { - for (var i = this._array, u = i.length - 1, _ = 0; _ <= u; _++) - if (!1 === s(i[o ? u - _ : _], _, this)) return _ + 1; - return _; - }), - (ArraySeq.prototype.__iterator = function (s, o) { - var i = this._array, - u = i.length - 1, - _ = 0; - return new Iterator(function () { - return _ > u ? iteratorDone() : iteratorValue(s, _, i[o ? u - _++ : _++]); - }); - }), - createClass(ObjectSeq, KeyedSeq), - (ObjectSeq.prototype.get = function (s, o) { - return void 0 === o || this.has(s) ? this._object[s] : o; - }), - (ObjectSeq.prototype.has = function (s) { - return this._object.hasOwnProperty(s); - }), - (ObjectSeq.prototype.__iterate = function (s, o) { - for (var i = this._object, u = this._keys, _ = u.length - 1, w = 0; w <= _; w++) { - var x = u[o ? _ - w : w]; - if (!1 === s(i[x], x, this)) return w + 1; - } - return w; - }), - (ObjectSeq.prototype.__iterator = function (s, o) { - var i = this._object, - u = this._keys, - _ = u.length - 1, - w = 0; - return new Iterator(function () { - var x = u[o ? _ - w : w]; - return w++ > _ ? iteratorDone() : iteratorValue(s, x, i[x]); - }); - }), - (ObjectSeq.prototype[_] = !0), - createClass(IterableSeq, IndexedSeq), - (IterableSeq.prototype.__iterateUncached = function (s, o) { - if (o) return this.cacheResult().__iterate(s, o); - var i = getIterator(this._iterable), - u = 0; - if (isIterator(i)) - for (var _; !(_ = i.next()).done && !1 !== s(_.value, u++, this); ); - return u; - }), - (IterableSeq.prototype.__iteratorUncached = function (s, o) { - if (o) return this.cacheResult().__iterator(s, o); - var i = getIterator(this._iterable); - if (!isIterator(i)) return new Iterator(iteratorDone); - var u = 0; - return new Iterator(function () { - var o = i.next(); - return o.done ? o : iteratorValue(s, u++, o.value); - }); - }), - createClass(IteratorSeq, IndexedSeq), - (IteratorSeq.prototype.__iterateUncached = function (s, o) { - if (o) return this.cacheResult().__iterate(s, o); - for (var i, u = this._iterator, _ = this._iteratorCache, w = 0; w < _.length; ) - if (!1 === s(_[w], w++, this)) return w; - for (; !(i = u.next()).done; ) { - var x = i.value; - if (((_[w] = x), !1 === s(x, w++, this))) break; - } - return w; - }), - (IteratorSeq.prototype.__iteratorUncached = function (s, o) { - if (o) return this.cacheResult().__iterator(s, o); - var i = this._iterator, - u = this._iteratorCache, - _ = 0; - return new Iterator(function () { - if (_ >= u.length) { - var o = i.next(); - if (o.done) return o; - u[_] = o.value; - } - return iteratorValue(s, _, u[_++]); - }); - }), - createClass(Repeat, IndexedSeq), - (Repeat.prototype.toString = function () { - return 0 === this.size - ? 'Repeat []' - : 'Repeat [ ' + this._value + ' ' + this.size + ' times ]'; - }), - (Repeat.prototype.get = function (s, o) { - return this.has(s) ? this._value : o; - }), - (Repeat.prototype.includes = function (s) { - return is(this._value, s); - }), - (Repeat.prototype.slice = function (s, o) { - var i = this.size; - return wholeSlice(s, o, i) - ? this - : new Repeat(this._value, resolveEnd(o, i) - resolveBegin(s, i)); - }), - (Repeat.prototype.reverse = function () { - return this; - }), - (Repeat.prototype.indexOf = function (s) { - return is(this._value, s) ? 0 : -1; - }), - (Repeat.prototype.lastIndexOf = function (s) { - return is(this._value, s) ? this.size : -1; - }), - (Repeat.prototype.__iterate = function (s, o) { - for (var i = 0; i < this.size; i++) - if (!1 === s(this._value, i, this)) return i + 1; - return i; - }), - (Repeat.prototype.__iterator = function (s, o) { - var i = this, - u = 0; - return new Iterator(function () { - return u < i.size ? iteratorValue(s, u++, i._value) : iteratorDone(); - }); - }), - (Repeat.prototype.equals = function (s) { - return s instanceof Repeat ? is(this._value, s._value) : deepEqual(s); - }), - createClass(Range, IndexedSeq), - (Range.prototype.toString = function () { - return 0 === this.size - ? 'Range []' - : 'Range [ ' + - this._start + - '...' + - this._end + - (1 !== this._step ? ' by ' + this._step : '') + - ' ]'; - }), - (Range.prototype.get = function (s, o) { - return this.has(s) ? this._start + wrapIndex(this, s) * this._step : o; - }), - (Range.prototype.includes = function (s) { - var o = (s - this._start) / this._step; - return o >= 0 && o < this.size && o === Math.floor(o); - }), - (Range.prototype.slice = function (s, o) { - return wholeSlice(s, o, this.size) - ? this - : ((s = resolveBegin(s, this.size)), - (o = resolveEnd(o, this.size)) <= s - ? new Range(0, 0) - : new Range(this.get(s, this._end), this.get(o, this._end), this._step)); - }), - (Range.prototype.indexOf = function (s) { - var o = s - this._start; - if (o % this._step == 0) { - var i = o / this._step; - if (i >= 0 && i < this.size) return i; - } - return -1; - }), - (Range.prototype.lastIndexOf = function (s) { - return this.indexOf(s); - }), - (Range.prototype.__iterate = function (s, o) { - for ( - var i = this.size - 1, - u = this._step, - _ = o ? this._start + i * u : this._start, - w = 0; - w <= i; - w++ - ) { - if (!1 === s(_, w, this)) return w + 1; - _ += o ? -u : u; - } - return w; - }), - (Range.prototype.__iterator = function (s, o) { - var i = this.size - 1, - u = this._step, - _ = o ? this._start + i * u : this._start, - w = 0; - return new Iterator(function () { - var x = _; - return (_ += o ? -u : u), w > i ? iteratorDone() : iteratorValue(s, w++, x); - }); - }), - (Range.prototype.equals = function (s) { - return s instanceof Range - ? this._start === s._start && this._end === s._end && this._step === s._step - : deepEqual(this, s); - }), - createClass(Collection, Iterable), - createClass(KeyedCollection, Collection), - createClass(IndexedCollection, Collection), - createClass(SetCollection, Collection), - (Collection.Keyed = KeyedCollection), - (Collection.Indexed = IndexedCollection), - (Collection.Set = SetCollection); - var pe = - 'function' == typeof Math.imul && -2 === Math.imul(4294967295, 2) - ? Math.imul - : function imul(s, o) { - var i = 65535 & (s |= 0), - u = 65535 & (o |= 0); - return (i * u + ((((s >>> 16) * u + i * (o >>> 16)) << 16) >>> 0)) | 0; - }; - function smi(s) { - return ((s >>> 1) & 1073741824) | (3221225471 & s); - } - function hash(s) { - if (!1 === s || null == s) return 0; - if ('function' == typeof s.valueOf && (!1 === (s = s.valueOf()) || null == s)) - return 0; - if (!0 === s) return 1; - var o = typeof s; - if ('number' === o) { - if (s != s || s === 1 / 0) return 0; - var i = 0 | s; - for (i !== s && (i ^= 4294967295 * s); s > 4294967295; ) i ^= s /= 4294967295; - return smi(i); - } - if ('string' === o) return s.length > Se ? cachedHashString(s) : hashString(s); - if ('function' == typeof s.hashCode) return s.hashCode(); - if ('object' === o) return hashJSObj(s); - if ('function' == typeof s.toString) return hashString(s.toString()); - throw new Error('Value type ' + o + ' cannot be hashed.'); - } - function cachedHashString(s) { - var o = Te[s]; - return ( - void 0 === o && - ((o = hashString(s)), Pe === xe && ((Pe = 0), (Te = {})), Pe++, (Te[s] = o)), - o - ); - } - function hashString(s) { - for (var o = 0, i = 0; i < s.length; i++) o = (31 * o + s.charCodeAt(i)) | 0; - return smi(o); - } - function hashJSObj(s) { - var o; - if (be && void 0 !== (o = ye.get(s))) return o; - if (void 0 !== (o = s[we])) return o; - if (!fe) { - if (void 0 !== (o = s.propertyIsEnumerable && s.propertyIsEnumerable[we])) return o; - if (void 0 !== (o = getIENodeHash(s))) return o; - } - if (((o = ++_e), 1073741824 & _e && (_e = 0), be)) ye.set(s, o); - else { - if (void 0 !== de && !1 === de(s)) - throw new Error('Non-extensible objects are not allowed as keys.'); - if (fe) - Object.defineProperty(s, we, { - enumerable: !1, - configurable: !1, - writable: !1, - value: o - }); - else if ( - void 0 !== s.propertyIsEnumerable && - s.propertyIsEnumerable === s.constructor.prototype.propertyIsEnumerable - ) - (s.propertyIsEnumerable = function () { - return this.constructor.prototype.propertyIsEnumerable.apply(this, arguments); - }), - (s.propertyIsEnumerable[we] = o); - else { - if (void 0 === s.nodeType) - throw new Error('Unable to set a non-enumerable property on object.'); - s[we] = o; - } - } - return o; - } - var de = Object.isExtensible, - fe = (function () { - try { - return Object.defineProperty({}, '@', {}), !0; - } catch (s) { - return !1; - } - })(); - function getIENodeHash(s) { - if (s && s.nodeType > 0) - switch (s.nodeType) { - case 1: - return s.uniqueID; - case 9: - return s.documentElement && s.documentElement.uniqueID; - } - } - var ye, - be = 'function' == typeof WeakMap; - be && (ye = new WeakMap()); - var _e = 0, - we = '__immutablehash__'; - 'function' == typeof Symbol && (we = Symbol(we)); - var Se = 16, - xe = 255, - Pe = 0, - Te = {}; - function assertNotInfinite(s) { - invariant(s !== 1 / 0, 'Cannot perform this action with an infinite size.'); - } - function Map(s) { - return null == s - ? emptyMap() - : isMap(s) && !isOrdered(s) - ? s - : emptyMap().withMutations(function (o) { - var i = KeyedIterable(s); - assertNotInfinite(i.size), - i.forEach(function (s, i) { - return o.set(i, s); - }); - }); - } - function isMap(s) { - return !(!s || !s[qe]); - } - createClass(Map, KeyedCollection), - (Map.of = function () { - var o = s.call(arguments, 0); - return emptyMap().withMutations(function (s) { - for (var i = 0; i < o.length; i += 2) { - if (i + 1 >= o.length) throw new Error('Missing value for key: ' + o[i]); - s.set(o[i], o[i + 1]); - } - }); - }), - (Map.prototype.toString = function () { - return this.__toString('Map {', '}'); - }), - (Map.prototype.get = function (s, o) { - return this._root ? this._root.get(0, void 0, s, o) : o; - }), - (Map.prototype.set = function (s, o) { - return updateMap(this, s, o); - }), - (Map.prototype.setIn = function (s, o) { - return this.updateIn(s, L, function () { - return o; - }); - }), - (Map.prototype.remove = function (s) { - return updateMap(this, s, L); - }), - (Map.prototype.deleteIn = function (s) { - return this.updateIn(s, function () { - return L; - }); - }), - (Map.prototype.update = function (s, o, i) { - return 1 === arguments.length ? s(this) : this.updateIn([s], o, i); - }), - (Map.prototype.updateIn = function (s, o, i) { - i || ((i = o), (o = void 0)); - var u = updateInDeepMap(this, forceIterator(s), o, i); - return u === L ? void 0 : u; - }), - (Map.prototype.clear = function () { - return 0 === this.size - ? this - : this.__ownerID - ? ((this.size = 0), - (this._root = null), - (this.__hash = void 0), - (this.__altered = !0), - this) - : emptyMap(); - }), - (Map.prototype.merge = function () { - return mergeIntoMapWith(this, void 0, arguments); - }), - (Map.prototype.mergeWith = function (o) { - return mergeIntoMapWith(this, o, s.call(arguments, 1)); - }), - (Map.prototype.mergeIn = function (o) { - var i = s.call(arguments, 1); - return this.updateIn(o, emptyMap(), function (s) { - return 'function' == typeof s.merge ? s.merge.apply(s, i) : i[i.length - 1]; - }); - }), - (Map.prototype.mergeDeep = function () { - return mergeIntoMapWith(this, deepMerger, arguments); - }), - (Map.prototype.mergeDeepWith = function (o) { - var i = s.call(arguments, 1); - return mergeIntoMapWith(this, deepMergerWith(o), i); - }), - (Map.prototype.mergeDeepIn = function (o) { - var i = s.call(arguments, 1); - return this.updateIn(o, emptyMap(), function (s) { - return 'function' == typeof s.mergeDeep - ? s.mergeDeep.apply(s, i) - : i[i.length - 1]; - }); - }), - (Map.prototype.sort = function (s) { - return OrderedMap(sortFactory(this, s)); - }), - (Map.prototype.sortBy = function (s, o) { - return OrderedMap(sortFactory(this, o, s)); - }), - (Map.prototype.withMutations = function (s) { - var o = this.asMutable(); - return s(o), o.wasAltered() ? o.__ensureOwner(this.__ownerID) : this; - }), - (Map.prototype.asMutable = function () { - return this.__ownerID ? this : this.__ensureOwner(new OwnerID()); - }), - (Map.prototype.asImmutable = function () { - return this.__ensureOwner(); - }), - (Map.prototype.wasAltered = function () { - return this.__altered; - }), - (Map.prototype.__iterator = function (s, o) { - return new MapIterator(this, s, o); - }), - (Map.prototype.__iterate = function (s, o) { - var i = this, - u = 0; - return ( - this._root && - this._root.iterate(function (o) { - return u++, s(o[1], o[0], i); - }, o), - u - ); - }), - (Map.prototype.__ensureOwner = function (s) { - return s === this.__ownerID - ? this - : s - ? makeMap(this.size, this._root, s, this.__hash) - : ((this.__ownerID = s), (this.__altered = !1), this); - }), - (Map.isMap = isMap); - var Re, - qe = '@@__IMMUTABLE_MAP__@@', - $e = Map.prototype; - function ArrayMapNode(s, o) { - (this.ownerID = s), (this.entries = o); - } - function BitmapIndexedNode(s, o, i) { - (this.ownerID = s), (this.bitmap = o), (this.nodes = i); - } - function HashArrayMapNode(s, o, i) { - (this.ownerID = s), (this.count = o), (this.nodes = i); - } - function HashCollisionNode(s, o, i) { - (this.ownerID = s), (this.keyHash = o), (this.entries = i); - } - function ValueNode(s, o, i) { - (this.ownerID = s), (this.keyHash = o), (this.entry = i); - } - function MapIterator(s, o, i) { - (this._type = o), - (this._reverse = i), - (this._stack = s._root && mapIteratorFrame(s._root)); - } - function mapIteratorValue(s, o) { - return iteratorValue(s, o[0], o[1]); - } - function mapIteratorFrame(s, o) { - return { node: s, index: 0, __prev: o }; - } - function makeMap(s, o, i, u) { - var _ = Object.create($e); - return ( - (_.size = s), - (_._root = o), - (_.__ownerID = i), - (_.__hash = u), - (_.__altered = !1), - _ - ); - } - function emptyMap() { - return Re || (Re = makeMap(0)); - } - function updateMap(s, o, i) { - var u, _; - if (s._root) { - var w = MakeRef(B), - x = MakeRef($); - if (((u = updateNode(s._root, s.__ownerID, 0, void 0, o, i, w, x)), !x.value)) - return s; - _ = s.size + (w.value ? (i === L ? -1 : 1) : 0); - } else { - if (i === L) return s; - (_ = 1), (u = new ArrayMapNode(s.__ownerID, [[o, i]])); - } - return s.__ownerID - ? ((s.size = _), (s._root = u), (s.__hash = void 0), (s.__altered = !0), s) - : u - ? makeMap(_, u) - : emptyMap(); - } - function updateNode(s, o, i, u, _, w, x, C) { - return s - ? s.update(o, i, u, _, w, x, C) - : w === L - ? s - : (SetRef(C), SetRef(x), new ValueNode(o, u, [_, w])); - } - function isLeafNode(s) { - return s.constructor === ValueNode || s.constructor === HashCollisionNode; - } - function mergeIntoNode(s, o, i, u, _) { - if (s.keyHash === u) return new HashCollisionNode(o, u, [s.entry, _]); - var w, - C = (0 === i ? s.keyHash : s.keyHash >>> i) & j, - L = (0 === i ? u : u >>> i) & j; - return new BitmapIndexedNode( - o, - (1 << C) | (1 << L), - C === L - ? [mergeIntoNode(s, o, i + x, u, _)] - : ((w = new ValueNode(o, u, _)), C < L ? [s, w] : [w, s]) - ); - } - function createNodes(s, o, i, u) { - s || (s = new OwnerID()); - for (var _ = new ValueNode(s, hash(i), [i, u]), w = 0; w < o.length; w++) { - var x = o[w]; - _ = _.update(s, 0, void 0, x[0], x[1]); - } - return _; - } - function packNodes(s, o, i, u) { - for ( - var _ = 0, w = 0, x = new Array(i), C = 0, j = 1, L = o.length; - C < L; - C++, j <<= 1 - ) { - var B = o[C]; - void 0 !== B && C !== u && ((_ |= j), (x[w++] = B)); - } - return new BitmapIndexedNode(s, _, x); - } - function expandNodes(s, o, i, u, _) { - for (var w = 0, x = new Array(C), j = 0; 0 !== i; j++, i >>>= 1) - x[j] = 1 & i ? o[w++] : void 0; - return (x[u] = _), new HashArrayMapNode(s, w + 1, x); - } - function mergeIntoMapWith(s, o, i) { - for (var u = [], _ = 0; _ < i.length; _++) { - var w = i[_], - x = KeyedIterable(w); - isIterable(w) || - (x = x.map(function (s) { - return fromJS(s); - })), - u.push(x); - } - return mergeIntoCollectionWith(s, o, u); - } - function deepMerger(s, o, i) { - return s && s.mergeDeep && isIterable(o) ? s.mergeDeep(o) : is(s, o) ? s : o; - } - function deepMergerWith(s) { - return function (o, i, u) { - if (o && o.mergeDeepWith && isIterable(i)) return o.mergeDeepWith(s, i); - var _ = s(o, i, u); - return is(o, _) ? o : _; - }; - } - function mergeIntoCollectionWith(s, o, i) { - return 0 === - (i = i.filter(function (s) { - return 0 !== s.size; - })).length - ? s - : 0 !== s.size || s.__ownerID || 1 !== i.length - ? s.withMutations(function (s) { - for ( - var u = o - ? function (i, u) { - s.update(u, L, function (s) { - return s === L ? i : o(s, i, u); - }); - } - : function (o, i) { - s.set(i, o); - }, - _ = 0; - _ < i.length; - _++ - ) - i[_].forEach(u); - }) - : s.constructor(i[0]); - } - function updateInDeepMap(s, o, i, u) { - var _ = s === L, - w = o.next(); - if (w.done) { - var x = _ ? i : s, - C = u(x); - return C === x ? s : C; - } - invariant(_ || (s && s.set), 'invalid keyPath'); - var j = w.value, - B = _ ? L : s.get(j, L), - $ = updateInDeepMap(B, o, i, u); - return $ === B ? s : $ === L ? s.remove(j) : (_ ? emptyMap() : s).set(j, $); - } - function popCount(s) { - return ( - (s = - ((s = (858993459 & (s -= (s >> 1) & 1431655765)) + ((s >> 2) & 858993459)) + - (s >> 4)) & - 252645135), - (s += s >> 8), - 127 & (s += s >> 16) - ); - } - function setIn(s, o, i, u) { - var _ = u ? s : arrCopy(s); - return (_[o] = i), _; - } - function spliceIn(s, o, i, u) { - var _ = s.length + 1; - if (u && o + 1 === _) return (s[o] = i), s; - for (var w = new Array(_), x = 0, C = 0; C < _; C++) - C === o ? ((w[C] = i), (x = -1)) : (w[C] = s[C + x]); - return w; - } - function spliceOut(s, o, i) { - var u = s.length - 1; - if (i && o === u) return s.pop(), s; - for (var _ = new Array(u), w = 0, x = 0; x < u; x++) - x === o && (w = 1), (_[x] = s[x + w]); - return _; - } - ($e[qe] = !0), - ($e[w] = $e.remove), - ($e.removeIn = $e.deleteIn), - (ArrayMapNode.prototype.get = function (s, o, i, u) { - for (var _ = this.entries, w = 0, x = _.length; w < x; w++) - if (is(i, _[w][0])) return _[w][1]; - return u; - }), - (ArrayMapNode.prototype.update = function (s, o, i, u, _, w, x) { - for ( - var C = _ === L, j = this.entries, B = 0, $ = j.length; - B < $ && !is(u, j[B][0]); - B++ - ); - var V = B < $; - if (V ? j[B][1] === _ : C) return this; - if ((SetRef(x), (C || !V) && SetRef(w), !C || 1 !== j.length)) { - if (!V && !C && j.length >= ze) return createNodes(s, j, u, _); - var U = s && s === this.ownerID, - z = U ? j : arrCopy(j); - return ( - V - ? C - ? B === $ - 1 - ? z.pop() - : (z[B] = z.pop()) - : (z[B] = [u, _]) - : z.push([u, _]), - U ? ((this.entries = z), this) : new ArrayMapNode(s, z) - ); - } - }), - (BitmapIndexedNode.prototype.get = function (s, o, i, u) { - void 0 === o && (o = hash(i)); - var _ = 1 << ((0 === s ? o : o >>> s) & j), - w = this.bitmap; - return w & _ ? this.nodes[popCount(w & (_ - 1))].get(s + x, o, i, u) : u; - }), - (BitmapIndexedNode.prototype.update = function (s, o, i, u, _, w, C) { - void 0 === i && (i = hash(u)); - var B = (0 === o ? i : i >>> o) & j, - $ = 1 << B, - V = this.bitmap, - U = !!(V & $); - if (!U && _ === L) return this; - var z = popCount(V & ($ - 1)), - Y = this.nodes, - Z = U ? Y[z] : void 0, - ee = updateNode(Z, s, o + x, i, u, _, w, C); - if (ee === Z) return this; - if (!U && ee && Y.length >= We) return expandNodes(s, Y, V, B, ee); - if (U && !ee && 2 === Y.length && isLeafNode(Y[1 ^ z])) return Y[1 ^ z]; - if (U && ee && 1 === Y.length && isLeafNode(ee)) return ee; - var ie = s && s === this.ownerID, - ae = U ? (ee ? V : V ^ $) : V | $, - le = U - ? ee - ? setIn(Y, z, ee, ie) - : spliceOut(Y, z, ie) - : spliceIn(Y, z, ee, ie); - return ie - ? ((this.bitmap = ae), (this.nodes = le), this) - : new BitmapIndexedNode(s, ae, le); - }), - (HashArrayMapNode.prototype.get = function (s, o, i, u) { - void 0 === o && (o = hash(i)); - var _ = (0 === s ? o : o >>> s) & j, - w = this.nodes[_]; - return w ? w.get(s + x, o, i, u) : u; - }), - (HashArrayMapNode.prototype.update = function (s, o, i, u, _, w, C) { - void 0 === i && (i = hash(u)); - var B = (0 === o ? i : i >>> o) & j, - $ = _ === L, - V = this.nodes, - U = V[B]; - if ($ && !U) return this; - var z = updateNode(U, s, o + x, i, u, _, w, C); - if (z === U) return this; - var Y = this.count; - if (U) { - if (!z && --Y < He) return packNodes(s, V, Y, B); - } else Y++; - var Z = s && s === this.ownerID, - ee = setIn(V, B, z, Z); - return Z - ? ((this.count = Y), (this.nodes = ee), this) - : new HashArrayMapNode(s, Y, ee); - }), - (HashCollisionNode.prototype.get = function (s, o, i, u) { - for (var _ = this.entries, w = 0, x = _.length; w < x; w++) - if (is(i, _[w][0])) return _[w][1]; - return u; - }), - (HashCollisionNode.prototype.update = function (s, o, i, u, _, w, x) { - void 0 === i && (i = hash(u)); - var C = _ === L; - if (i !== this.keyHash) - return C ? this : (SetRef(x), SetRef(w), mergeIntoNode(this, s, o, i, [u, _])); - for (var j = this.entries, B = 0, $ = j.length; B < $ && !is(u, j[B][0]); B++); - var V = B < $; - if (V ? j[B][1] === _ : C) return this; - if ((SetRef(x), (C || !V) && SetRef(w), C && 2 === $)) - return new ValueNode(s, this.keyHash, j[1 ^ B]); - var U = s && s === this.ownerID, - z = U ? j : arrCopy(j); - return ( - V - ? C - ? B === $ - 1 - ? z.pop() - : (z[B] = z.pop()) - : (z[B] = [u, _]) - : z.push([u, _]), - U ? ((this.entries = z), this) : new HashCollisionNode(s, this.keyHash, z) - ); - }), - (ValueNode.prototype.get = function (s, o, i, u) { - return is(i, this.entry[0]) ? this.entry[1] : u; - }), - (ValueNode.prototype.update = function (s, o, i, u, _, w, x) { - var C = _ === L, - j = is(u, this.entry[0]); - return (j ? _ === this.entry[1] : C) - ? this - : (SetRef(x), - C - ? void SetRef(w) - : j - ? s && s === this.ownerID - ? ((this.entry[1] = _), this) - : new ValueNode(s, this.keyHash, [u, _]) - : (SetRef(w), mergeIntoNode(this, s, o, hash(u), [u, _]))); - }), - (ArrayMapNode.prototype.iterate = HashCollisionNode.prototype.iterate = - function (s, o) { - for (var i = this.entries, u = 0, _ = i.length - 1; u <= _; u++) - if (!1 === s(i[o ? _ - u : u])) return !1; - }), - (BitmapIndexedNode.prototype.iterate = HashArrayMapNode.prototype.iterate = - function (s, o) { - for (var i = this.nodes, u = 0, _ = i.length - 1; u <= _; u++) { - var w = i[o ? _ - u : u]; - if (w && !1 === w.iterate(s, o)) return !1; - } - }), - (ValueNode.prototype.iterate = function (s, o) { - return s(this.entry); - }), - createClass(MapIterator, Iterator), - (MapIterator.prototype.next = function () { - for (var s = this._type, o = this._stack; o; ) { - var i, - u = o.node, - _ = o.index++; - if (u.entry) { - if (0 === _) return mapIteratorValue(s, u.entry); - } else if (u.entries) { - if (_ <= (i = u.entries.length - 1)) - return mapIteratorValue(s, u.entries[this._reverse ? i - _ : _]); - } else if (_ <= (i = u.nodes.length - 1)) { - var w = u.nodes[this._reverse ? i - _ : _]; - if (w) { - if (w.entry) return mapIteratorValue(s, w.entry); - o = this._stack = mapIteratorFrame(w, o); - } - continue; - } - o = this._stack = this._stack.__prev; - } - return iteratorDone(); - }); - var ze = C / 4, - We = C / 2, - He = C / 4; - function List(s) { - var o = emptyList(); - if (null == s) return o; - if (isList(s)) return s; - var i = IndexedIterable(s), - u = i.size; - return 0 === u - ? o - : (assertNotInfinite(u), - u > 0 && u < C - ? makeList(0, u, x, null, new VNode(i.toArray())) - : o.withMutations(function (s) { - s.setSize(u), - i.forEach(function (o, i) { - return s.set(i, o); - }); - })); - } - function isList(s) { - return !(!s || !s[Ye]); - } - createClass(List, IndexedCollection), - (List.of = function () { - return this(arguments); - }), - (List.prototype.toString = function () { - return this.__toString('List [', ']'); - }), - (List.prototype.get = function (s, o) { - if ((s = wrapIndex(this, s)) >= 0 && s < this.size) { - var i = listNodeFor(this, (s += this._origin)); - return i && i.array[s & j]; - } - return o; - }), - (List.prototype.set = function (s, o) { - return updateList(this, s, o); - }), - (List.prototype.remove = function (s) { - return this.has(s) - ? 0 === s - ? this.shift() - : s === this.size - 1 - ? this.pop() - : this.splice(s, 1) - : this; - }), - (List.prototype.insert = function (s, o) { - return this.splice(s, 0, o); - }), - (List.prototype.clear = function () { - return 0 === this.size - ? this - : this.__ownerID - ? ((this.size = this._origin = this._capacity = 0), - (this._level = x), - (this._root = this._tail = null), - (this.__hash = void 0), - (this.__altered = !0), - this) - : emptyList(); - }), - (List.prototype.push = function () { - var s = arguments, - o = this.size; - return this.withMutations(function (i) { - setListBounds(i, 0, o + s.length); - for (var u = 0; u < s.length; u++) i.set(o + u, s[u]); - }); - }), - (List.prototype.pop = function () { - return setListBounds(this, 0, -1); - }), - (List.prototype.unshift = function () { - var s = arguments; - return this.withMutations(function (o) { - setListBounds(o, -s.length); - for (var i = 0; i < s.length; i++) o.set(i, s[i]); - }); - }), - (List.prototype.shift = function () { - return setListBounds(this, 1); - }), - (List.prototype.merge = function () { - return mergeIntoListWith(this, void 0, arguments); - }), - (List.prototype.mergeWith = function (o) { - return mergeIntoListWith(this, o, s.call(arguments, 1)); - }), - (List.prototype.mergeDeep = function () { - return mergeIntoListWith(this, deepMerger, arguments); - }), - (List.prototype.mergeDeepWith = function (o) { - var i = s.call(arguments, 1); - return mergeIntoListWith(this, deepMergerWith(o), i); - }), - (List.prototype.setSize = function (s) { - return setListBounds(this, 0, s); - }), - (List.prototype.slice = function (s, o) { - var i = this.size; - return wholeSlice(s, o, i) - ? this - : setListBounds(this, resolveBegin(s, i), resolveEnd(o, i)); - }), - (List.prototype.__iterator = function (s, o) { - var i = 0, - u = iterateList(this, o); - return new Iterator(function () { - var o = u(); - return o === tt ? iteratorDone() : iteratorValue(s, i++, o); - }); - }), - (List.prototype.__iterate = function (s, o) { - for ( - var i, u = 0, _ = iterateList(this, o); - (i = _()) !== tt && !1 !== s(i, u++, this); - - ); - return u; - }), - (List.prototype.__ensureOwner = function (s) { - return s === this.__ownerID - ? this - : s - ? makeList( - this._origin, - this._capacity, - this._level, - this._root, - this._tail, - s, - this.__hash - ) - : ((this.__ownerID = s), this); - }), - (List.isList = isList); - var Ye = '@@__IMMUTABLE_LIST__@@', - Xe = List.prototype; - function VNode(s, o) { - (this.array = s), (this.ownerID = o); - } - (Xe[Ye] = !0), - (Xe[w] = Xe.remove), - (Xe.setIn = $e.setIn), - (Xe.deleteIn = Xe.removeIn = $e.removeIn), - (Xe.update = $e.update), - (Xe.updateIn = $e.updateIn), - (Xe.mergeIn = $e.mergeIn), - (Xe.mergeDeepIn = $e.mergeDeepIn), - (Xe.withMutations = $e.withMutations), - (Xe.asMutable = $e.asMutable), - (Xe.asImmutable = $e.asImmutable), - (Xe.wasAltered = $e.wasAltered), - (VNode.prototype.removeBefore = function (s, o, i) { - if (i === o ? 1 << o : 0 === this.array.length) return this; - var u = (i >>> o) & j; - if (u >= this.array.length) return new VNode([], s); - var _, - w = 0 === u; - if (o > 0) { - var C = this.array[u]; - if ((_ = C && C.removeBefore(s, o - x, i)) === C && w) return this; - } - if (w && !_) return this; - var L = editableVNode(this, s); - if (!w) for (var B = 0; B < u; B++) L.array[B] = void 0; - return _ && (L.array[u] = _), L; - }), - (VNode.prototype.removeAfter = function (s, o, i) { - if (i === (o ? 1 << o : 0) || 0 === this.array.length) return this; - var u, - _ = ((i - 1) >>> o) & j; - if (_ >= this.array.length) return this; - if (o > 0) { - var w = this.array[_]; - if ((u = w && w.removeAfter(s, o - x, i)) === w && _ === this.array.length - 1) - return this; - } - var C = editableVNode(this, s); - return C.array.splice(_ + 1), u && (C.array[_] = u), C; - }); - var Qe, - et, - tt = {}; - function iterateList(s, o) { - var i = s._origin, - u = s._capacity, - _ = getTailOffset(u), - w = s._tail; - return iterateNodeOrLeaf(s._root, s._level, 0); - function iterateNodeOrLeaf(s, o, i) { - return 0 === o ? iterateLeaf(s, i) : iterateNode(s, o, i); - } - function iterateLeaf(s, x) { - var j = x === _ ? w && w.array : s && s.array, - L = x > i ? 0 : i - x, - B = u - x; - return ( - B > C && (B = C), - function () { - if (L === B) return tt; - var s = o ? --B : L++; - return j && j[s]; - } - ); - } - function iterateNode(s, _, w) { - var j, - L = s && s.array, - B = w > i ? 0 : (i - w) >> _, - $ = 1 + ((u - w) >> _); - return ( - $ > C && ($ = C), - function () { - for (;;) { - if (j) { - var s = j(); - if (s !== tt) return s; - j = null; - } - if (B === $) return tt; - var i = o ? --$ : B++; - j = iterateNodeOrLeaf(L && L[i], _ - x, w + (i << _)); - } - } - ); - } - } - function makeList(s, o, i, u, _, w, x) { - var C = Object.create(Xe); - return ( - (C.size = o - s), - (C._origin = s), - (C._capacity = o), - (C._level = i), - (C._root = u), - (C._tail = _), - (C.__ownerID = w), - (C.__hash = x), - (C.__altered = !1), - C - ); - } - function emptyList() { - return Qe || (Qe = makeList(0, 0, x)); - } - function updateList(s, o, i) { - if ((o = wrapIndex(s, o)) != o) return s; - if (o >= s.size || o < 0) - return s.withMutations(function (s) { - o < 0 ? setListBounds(s, o).set(0, i) : setListBounds(s, 0, o + 1).set(o, i); - }); - o += s._origin; - var u = s._tail, - _ = s._root, - w = MakeRef($); - return ( - o >= getTailOffset(s._capacity) - ? (u = updateVNode(u, s.__ownerID, 0, o, i, w)) - : (_ = updateVNode(_, s.__ownerID, s._level, o, i, w)), - w.value - ? s.__ownerID - ? ((s._root = _), (s._tail = u), (s.__hash = void 0), (s.__altered = !0), s) - : makeList(s._origin, s._capacity, s._level, _, u) - : s - ); - } - function updateVNode(s, o, i, u, _, w) { - var C, - L = (u >>> i) & j, - B = s && L < s.array.length; - if (!B && void 0 === _) return s; - if (i > 0) { - var $ = s && s.array[L], - V = updateVNode($, o, i - x, u, _, w); - return V === $ ? s : (((C = editableVNode(s, o)).array[L] = V), C); - } - return B && s.array[L] === _ - ? s - : (SetRef(w), - (C = editableVNode(s, o)), - void 0 === _ && L === C.array.length - 1 ? C.array.pop() : (C.array[L] = _), - C); - } - function editableVNode(s, o) { - return o && s && o === s.ownerID ? s : new VNode(s ? s.array.slice() : [], o); - } - function listNodeFor(s, o) { - if (o >= getTailOffset(s._capacity)) return s._tail; - if (o < 1 << (s._level + x)) { - for (var i = s._root, u = s._level; i && u > 0; ) - (i = i.array[(o >>> u) & j]), (u -= x); - return i; - } - } - function setListBounds(s, o, i) { - void 0 !== o && (o |= 0), void 0 !== i && (i |= 0); - var u = s.__ownerID || new OwnerID(), - _ = s._origin, - w = s._capacity, - C = _ + o, - L = void 0 === i ? w : i < 0 ? w + i : _ + i; - if (C === _ && L === w) return s; - if (C >= L) return s.clear(); - for (var B = s._level, $ = s._root, V = 0; C + V < 0; ) - ($ = new VNode($ && $.array.length ? [void 0, $] : [], u)), (V += 1 << (B += x)); - V && ((C += V), (_ += V), (L += V), (w += V)); - for (var U = getTailOffset(w), z = getTailOffset(L); z >= 1 << (B + x); ) - ($ = new VNode($ && $.array.length ? [$] : [], u)), (B += x); - var Y = s._tail, - Z = z < U ? listNodeFor(s, L - 1) : z > U ? new VNode([], u) : Y; - if (Y && z > U && C < w && Y.array.length) { - for (var ee = ($ = editableVNode($, u)), ie = B; ie > x; ie -= x) { - var ae = (U >>> ie) & j; - ee = ee.array[ae] = editableVNode(ee.array[ae], u); - } - ee.array[(U >>> x) & j] = Y; - } - if ((L < w && (Z = Z && Z.removeAfter(u, 0, L)), C >= z)) - (C -= z), (L -= z), (B = x), ($ = null), (Z = Z && Z.removeBefore(u, 0, C)); - else if (C > _ || z < U) { - for (V = 0; $; ) { - var le = (C >>> B) & j; - if ((le !== z >>> B) & j) break; - le && (V += (1 << B) * le), (B -= x), ($ = $.array[le]); - } - $ && C > _ && ($ = $.removeBefore(u, B, C - V)), - $ && z < U && ($ = $.removeAfter(u, B, z - V)), - V && ((C -= V), (L -= V)); - } - return s.__ownerID - ? ((s.size = L - C), - (s._origin = C), - (s._capacity = L), - (s._level = B), - (s._root = $), - (s._tail = Z), - (s.__hash = void 0), - (s.__altered = !0), - s) - : makeList(C, L, B, $, Z); - } - function mergeIntoListWith(s, o, i) { - for (var u = [], _ = 0, w = 0; w < i.length; w++) { - var x = i[w], - C = IndexedIterable(x); - C.size > _ && (_ = C.size), - isIterable(x) || - (C = C.map(function (s) { - return fromJS(s); - })), - u.push(C); - } - return _ > s.size && (s = s.setSize(_)), mergeIntoCollectionWith(s, o, u); - } - function getTailOffset(s) { - return s < C ? 0 : ((s - 1) >>> x) << x; - } - function OrderedMap(s) { - return null == s - ? emptyOrderedMap() - : isOrderedMap(s) - ? s - : emptyOrderedMap().withMutations(function (o) { - var i = KeyedIterable(s); - assertNotInfinite(i.size), - i.forEach(function (s, i) { - return o.set(i, s); - }); - }); - } - function isOrderedMap(s) { - return isMap(s) && isOrdered(s); - } - function makeOrderedMap(s, o, i, u) { - var _ = Object.create(OrderedMap.prototype); - return ( - (_.size = s ? s.size : 0), - (_._map = s), - (_._list = o), - (_.__ownerID = i), - (_.__hash = u), - _ - ); - } - function emptyOrderedMap() { - return et || (et = makeOrderedMap(emptyMap(), emptyList())); - } - function updateOrderedMap(s, o, i) { - var u, - _, - w = s._map, - x = s._list, - j = w.get(o), - B = void 0 !== j; - if (i === L) { - if (!B) return s; - x.size >= C && x.size >= 2 * w.size - ? ((u = (_ = x.filter(function (s, o) { - return void 0 !== s && j !== o; - })) - .toKeyedSeq() - .map(function (s) { - return s[0]; - }) - .flip() - .toMap()), - s.__ownerID && (u.__ownerID = _.__ownerID = s.__ownerID)) - : ((u = w.remove(o)), (_ = j === x.size - 1 ? x.pop() : x.set(j, void 0))); - } else if (B) { - if (i === x.get(j)[1]) return s; - (u = w), (_ = x.set(j, [o, i])); - } else (u = w.set(o, x.size)), (_ = x.set(x.size, [o, i])); - return s.__ownerID - ? ((s.size = u.size), (s._map = u), (s._list = _), (s.__hash = void 0), s) - : makeOrderedMap(u, _); - } - function ToKeyedSequence(s, o) { - (this._iter = s), (this._useKeys = o), (this.size = s.size); - } - function ToIndexedSequence(s) { - (this._iter = s), (this.size = s.size); - } - function ToSetSequence(s) { - (this._iter = s), (this.size = s.size); - } - function FromEntriesSequence(s) { - (this._iter = s), (this.size = s.size); - } - function flipFactory(s) { - var o = makeSequence(s); - return ( - (o._iter = s), - (o.size = s.size), - (o.flip = function () { - return s; - }), - (o.reverse = function () { - var o = s.reverse.apply(this); - return ( - (o.flip = function () { - return s.reverse(); - }), - o - ); - }), - (o.has = function (o) { - return s.includes(o); - }), - (o.includes = function (o) { - return s.has(o); - }), - (o.cacheResult = cacheResultThrough), - (o.__iterateUncached = function (o, i) { - var u = this; - return s.__iterate(function (s, i) { - return !1 !== o(i, s, u); - }, i); - }), - (o.__iteratorUncached = function (o, i) { - if (o === z) { - var u = s.__iterator(o, i); - return new Iterator(function () { - var s = u.next(); - if (!s.done) { - var o = s.value[0]; - (s.value[0] = s.value[1]), (s.value[1] = o); - } - return s; - }); - } - return s.__iterator(o === U ? V : U, i); - }), - o - ); - } - function mapFactory(s, o, i) { - var u = makeSequence(s); - return ( - (u.size = s.size), - (u.has = function (o) { - return s.has(o); - }), - (u.get = function (u, _) { - var w = s.get(u, L); - return w === L ? _ : o.call(i, w, u, s); - }), - (u.__iterateUncached = function (u, _) { - var w = this; - return s.__iterate(function (s, _, x) { - return !1 !== u(o.call(i, s, _, x), _, w); - }, _); - }), - (u.__iteratorUncached = function (u, _) { - var w = s.__iterator(z, _); - return new Iterator(function () { - var _ = w.next(); - if (_.done) return _; - var x = _.value, - C = x[0]; - return iteratorValue(u, C, o.call(i, x[1], C, s), _); - }); - }), - u - ); - } - function reverseFactory(s, o) { - var i = makeSequence(s); - return ( - (i._iter = s), - (i.size = s.size), - (i.reverse = function () { - return s; - }), - s.flip && - (i.flip = function () { - var o = flipFactory(s); - return ( - (o.reverse = function () { - return s.flip(); - }), - o - ); - }), - (i.get = function (i, u) { - return s.get(o ? i : -1 - i, u); - }), - (i.has = function (i) { - return s.has(o ? i : -1 - i); - }), - (i.includes = function (o) { - return s.includes(o); - }), - (i.cacheResult = cacheResultThrough), - (i.__iterate = function (o, i) { - var u = this; - return s.__iterate(function (s, i) { - return o(s, i, u); - }, !i); - }), - (i.__iterator = function (o, i) { - return s.__iterator(o, !i); - }), - i - ); - } - function filterFactory(s, o, i, u) { - var _ = makeSequence(s); - return ( - u && - ((_.has = function (u) { - var _ = s.get(u, L); - return _ !== L && !!o.call(i, _, u, s); - }), - (_.get = function (u, _) { - var w = s.get(u, L); - return w !== L && o.call(i, w, u, s) ? w : _; - })), - (_.__iterateUncached = function (_, w) { - var x = this, - C = 0; - return ( - s.__iterate(function (s, w, j) { - if (o.call(i, s, w, j)) return C++, _(s, u ? w : C - 1, x); - }, w), - C - ); - }), - (_.__iteratorUncached = function (_, w) { - var x = s.__iterator(z, w), - C = 0; - return new Iterator(function () { - for (;;) { - var w = x.next(); - if (w.done) return w; - var j = w.value, - L = j[0], - B = j[1]; - if (o.call(i, B, L, s)) return iteratorValue(_, u ? L : C++, B, w); - } - }); - }), - _ - ); - } - function countByFactory(s, o, i) { - var u = Map().asMutable(); - return ( - s.__iterate(function (_, w) { - u.update(o.call(i, _, w, s), 0, function (s) { - return s + 1; - }); - }), - u.asImmutable() - ); - } - function groupByFactory(s, o, i) { - var u = isKeyed(s), - _ = (isOrdered(s) ? OrderedMap() : Map()).asMutable(); - s.__iterate(function (w, x) { - _.update(o.call(i, w, x, s), function (s) { - return (s = s || []).push(u ? [x, w] : w), s; - }); - }); - var w = iterableClass(s); - return _.map(function (o) { - return reify(s, w(o)); - }); - } - function sliceFactory(s, o, i, u) { - var _ = s.size; - if ( - (void 0 !== o && (o |= 0), - void 0 !== i && (i === 1 / 0 ? (i = _) : (i |= 0)), - wholeSlice(o, i, _)) - ) - return s; - var w = resolveBegin(o, _), - x = resolveEnd(i, _); - if (w != w || x != x) return sliceFactory(s.toSeq().cacheResult(), o, i, u); - var C, - j = x - w; - j == j && (C = j < 0 ? 0 : j); - var L = makeSequence(s); - return ( - (L.size = 0 === C ? C : (s.size && C) || void 0), - !u && - isSeq(s) && - C >= 0 && - (L.get = function (o, i) { - return (o = wrapIndex(this, o)) >= 0 && o < C ? s.get(o + w, i) : i; - }), - (L.__iterateUncached = function (o, i) { - var _ = this; - if (0 === C) return 0; - if (i) return this.cacheResult().__iterate(o, i); - var x = 0, - j = !0, - L = 0; - return ( - s.__iterate(function (s, i) { - if (!j || !(j = x++ < w)) - return L++, !1 !== o(s, u ? i : L - 1, _) && L !== C; - }), - L - ); - }), - (L.__iteratorUncached = function (o, i) { - if (0 !== C && i) return this.cacheResult().__iterator(o, i); - var _ = 0 !== C && s.__iterator(o, i), - x = 0, - j = 0; - return new Iterator(function () { - for (; x++ < w; ) _.next(); - if (++j > C) return iteratorDone(); - var s = _.next(); - return u || o === U - ? s - : iteratorValue(o, j - 1, o === V ? void 0 : s.value[1], s); - }); - }), - L - ); - } - function takeWhileFactory(s, o, i) { - var u = makeSequence(s); - return ( - (u.__iterateUncached = function (u, _) { - var w = this; - if (_) return this.cacheResult().__iterate(u, _); - var x = 0; - return ( - s.__iterate(function (s, _, C) { - return o.call(i, s, _, C) && ++x && u(s, _, w); - }), - x - ); - }), - (u.__iteratorUncached = function (u, _) { - var w = this; - if (_) return this.cacheResult().__iterator(u, _); - var x = s.__iterator(z, _), - C = !0; - return new Iterator(function () { - if (!C) return iteratorDone(); - var s = x.next(); - if (s.done) return s; - var _ = s.value, - j = _[0], - L = _[1]; - return o.call(i, L, j, w) - ? u === z - ? s - : iteratorValue(u, j, L, s) - : ((C = !1), iteratorDone()); - }); - }), - u - ); - } - function skipWhileFactory(s, o, i, u) { - var _ = makeSequence(s); - return ( - (_.__iterateUncached = function (_, w) { - var x = this; - if (w) return this.cacheResult().__iterate(_, w); - var C = !0, - j = 0; - return ( - s.__iterate(function (s, w, L) { - if (!C || !(C = o.call(i, s, w, L))) return j++, _(s, u ? w : j - 1, x); - }), - j - ); - }), - (_.__iteratorUncached = function (_, w) { - var x = this; - if (w) return this.cacheResult().__iterator(_, w); - var C = s.__iterator(z, w), - j = !0, - L = 0; - return new Iterator(function () { - var s, w, B; - do { - if ((s = C.next()).done) - return u || _ === U - ? s - : iteratorValue(_, L++, _ === V ? void 0 : s.value[1], s); - var $ = s.value; - (w = $[0]), (B = $[1]), j && (j = o.call(i, B, w, x)); - } while (j); - return _ === z ? s : iteratorValue(_, w, B, s); - }); - }), - _ - ); - } - function concatFactory(s, o) { - var i = isKeyed(s), - u = [s] - .concat(o) - .map(function (s) { - return ( - isIterable(s) - ? i && (s = KeyedIterable(s)) - : (s = i - ? keyedSeqFromValue(s) - : indexedSeqFromValue(Array.isArray(s) ? s : [s])), - s - ); - }) - .filter(function (s) { - return 0 !== s.size; - }); - if (0 === u.length) return s; - if (1 === u.length) { - var _ = u[0]; - if (_ === s || (i && isKeyed(_)) || (isIndexed(s) && isIndexed(_))) return _; - } - var w = new ArraySeq(u); - return ( - i ? (w = w.toKeyedSeq()) : isIndexed(s) || (w = w.toSetSeq()), - ((w = w.flatten(!0)).size = u.reduce(function (s, o) { - if (void 0 !== s) { - var i = o.size; - if (void 0 !== i) return s + i; - } - }, 0)), - w - ); - } - function flattenFactory(s, o, i) { - var u = makeSequence(s); - return ( - (u.__iterateUncached = function (u, _) { - var w = 0, - x = !1; - function flatDeep(s, C) { - var j = this; - s.__iterate(function (s, _) { - return ( - (!o || C < o) && isIterable(s) - ? flatDeep(s, C + 1) - : !1 === u(s, i ? _ : w++, j) && (x = !0), - !x - ); - }, _); - } - return flatDeep(s, 0), w; - }), - (u.__iteratorUncached = function (u, _) { - var w = s.__iterator(u, _), - x = [], - C = 0; - return new Iterator(function () { - for (; w; ) { - var s = w.next(); - if (!1 === s.done) { - var j = s.value; - if ((u === z && (j = j[1]), (o && !(x.length < o)) || !isIterable(j))) - return i ? s : iteratorValue(u, C++, j, s); - x.push(w), (w = j.__iterator(u, _)); - } else w = x.pop(); - } - return iteratorDone(); - }); - }), - u - ); - } - function flatMapFactory(s, o, i) { - var u = iterableClass(s); - return s - .toSeq() - .map(function (_, w) { - return u(o.call(i, _, w, s)); - }) - .flatten(!0); - } - function interposeFactory(s, o) { - var i = makeSequence(s); - return ( - (i.size = s.size && 2 * s.size - 1), - (i.__iterateUncached = function (i, u) { - var _ = this, - w = 0; - return ( - s.__iterate(function (s, u) { - return (!w || !1 !== i(o, w++, _)) && !1 !== i(s, w++, _); - }, u), - w - ); - }), - (i.__iteratorUncached = function (i, u) { - var _, - w = s.__iterator(U, u), - x = 0; - return new Iterator(function () { - return (!_ || x % 2) && (_ = w.next()).done - ? _ - : x % 2 - ? iteratorValue(i, x++, o) - : iteratorValue(i, x++, _.value, _); - }); - }), - i - ); - } - function sortFactory(s, o, i) { - o || (o = defaultComparator); - var u = isKeyed(s), - _ = 0, - w = s - .toSeq() - .map(function (o, u) { - return [u, o, _++, i ? i(o, u, s) : o]; - }) - .toArray(); - return ( - w - .sort(function (s, i) { - return o(s[3], i[3]) || s[2] - i[2]; - }) - .forEach( - u - ? function (s, o) { - w[o].length = 2; - } - : function (s, o) { - w[o] = s[1]; - } - ), - u ? KeyedSeq(w) : isIndexed(s) ? IndexedSeq(w) : SetSeq(w) - ); - } - function maxFactory(s, o, i) { - if ((o || (o = defaultComparator), i)) { - var u = s - .toSeq() - .map(function (o, u) { - return [o, i(o, u, s)]; - }) - .reduce(function (s, i) { - return maxCompare(o, s[1], i[1]) ? i : s; - }); - return u && u[0]; - } - return s.reduce(function (s, i) { - return maxCompare(o, s, i) ? i : s; - }); - } - function maxCompare(s, o, i) { - var u = s(i, o); - return (0 === u && i !== o && (null == i || i != i)) || u > 0; - } - function zipWithFactory(s, o, i) { - var u = makeSequence(s); - return ( - (u.size = new ArraySeq(i) - .map(function (s) { - return s.size; - }) - .min()), - (u.__iterate = function (s, o) { - for ( - var i, u = this.__iterator(U, o), _ = 0; - !(i = u.next()).done && !1 !== s(i.value, _++, this); - - ); - return _; - }), - (u.__iteratorUncached = function (s, u) { - var _ = i.map(function (s) { - return (s = Iterable(s)), getIterator(u ? s.reverse() : s); - }), - w = 0, - x = !1; - return new Iterator(function () { - var i; - return ( - x || - ((i = _.map(function (s) { - return s.next(); - })), - (x = i.some(function (s) { - return s.done; - }))), - x - ? iteratorDone() - : iteratorValue( - s, - w++, - o.apply( - null, - i.map(function (s) { - return s.value; - }) - ) - ) - ); - }); - }), - u - ); - } - function reify(s, o) { - return isSeq(s) ? o : s.constructor(o); - } - function validateEntry(s) { - if (s !== Object(s)) throw new TypeError('Expected [K, V] tuple: ' + s); - } - function resolveSize(s) { - return assertNotInfinite(s.size), ensureSize(s); - } - function iterableClass(s) { - return isKeyed(s) ? KeyedIterable : isIndexed(s) ? IndexedIterable : SetIterable; - } - function makeSequence(s) { - return Object.create( - (isKeyed(s) ? KeyedSeq : isIndexed(s) ? IndexedSeq : SetSeq).prototype - ); - } - function cacheResultThrough() { - return this._iter.cacheResult - ? (this._iter.cacheResult(), (this.size = this._iter.size), this) - : Seq.prototype.cacheResult.call(this); - } - function defaultComparator(s, o) { - return s > o ? 1 : s < o ? -1 : 0; - } - function forceIterator(s) { - var o = getIterator(s); - if (!o) { - if (!isArrayLike(s)) throw new TypeError('Expected iterable or array-like: ' + s); - o = getIterator(Iterable(s)); - } - return o; - } - function Record(s, o) { - var i, - u = function Record(w) { - if (w instanceof u) return w; - if (!(this instanceof u)) return new u(w); - if (!i) { - i = !0; - var x = Object.keys(s); - setProps(_, x), - (_.size = x.length), - (_._name = o), - (_._keys = x), - (_._defaultValues = s); - } - this._map = Map(w); - }, - _ = (u.prototype = Object.create(rt)); - return (_.constructor = u), u; - } - createClass(OrderedMap, Map), - (OrderedMap.of = function () { - return this(arguments); - }), - (OrderedMap.prototype.toString = function () { - return this.__toString('OrderedMap {', '}'); - }), - (OrderedMap.prototype.get = function (s, o) { - var i = this._map.get(s); - return void 0 !== i ? this._list.get(i)[1] : o; - }), - (OrderedMap.prototype.clear = function () { - return 0 === this.size - ? this - : this.__ownerID - ? ((this.size = 0), this._map.clear(), this._list.clear(), this) - : emptyOrderedMap(); - }), - (OrderedMap.prototype.set = function (s, o) { - return updateOrderedMap(this, s, o); - }), - (OrderedMap.prototype.remove = function (s) { - return updateOrderedMap(this, s, L); - }), - (OrderedMap.prototype.wasAltered = function () { - return this._map.wasAltered() || this._list.wasAltered(); - }), - (OrderedMap.prototype.__iterate = function (s, o) { - var i = this; - return this._list.__iterate(function (o) { - return o && s(o[1], o[0], i); - }, o); - }), - (OrderedMap.prototype.__iterator = function (s, o) { - return this._list.fromEntrySeq().__iterator(s, o); - }), - (OrderedMap.prototype.__ensureOwner = function (s) { - if (s === this.__ownerID) return this; - var o = this._map.__ensureOwner(s), - i = this._list.__ensureOwner(s); - return s - ? makeOrderedMap(o, i, s, this.__hash) - : ((this.__ownerID = s), (this._map = o), (this._list = i), this); - }), - (OrderedMap.isOrderedMap = isOrderedMap), - (OrderedMap.prototype[_] = !0), - (OrderedMap.prototype[w] = OrderedMap.prototype.remove), - createClass(ToKeyedSequence, KeyedSeq), - (ToKeyedSequence.prototype.get = function (s, o) { - return this._iter.get(s, o); - }), - (ToKeyedSequence.prototype.has = function (s) { - return this._iter.has(s); - }), - (ToKeyedSequence.prototype.valueSeq = function () { - return this._iter.valueSeq(); - }), - (ToKeyedSequence.prototype.reverse = function () { - var s = this, - o = reverseFactory(this, !0); - return ( - this._useKeys || - (o.valueSeq = function () { - return s._iter.toSeq().reverse(); - }), - o - ); - }), - (ToKeyedSequence.prototype.map = function (s, o) { - var i = this, - u = mapFactory(this, s, o); - return ( - this._useKeys || - (u.valueSeq = function () { - return i._iter.toSeq().map(s, o); - }), - u - ); - }), - (ToKeyedSequence.prototype.__iterate = function (s, o) { - var i, - u = this; - return this._iter.__iterate( - this._useKeys - ? function (o, i) { - return s(o, i, u); - } - : ((i = o ? resolveSize(this) : 0), - function (_) { - return s(_, o ? --i : i++, u); - }), - o - ); - }), - (ToKeyedSequence.prototype.__iterator = function (s, o) { - if (this._useKeys) return this._iter.__iterator(s, o); - var i = this._iter.__iterator(U, o), - u = o ? resolveSize(this) : 0; - return new Iterator(function () { - var _ = i.next(); - return _.done ? _ : iteratorValue(s, o ? --u : u++, _.value, _); - }); - }), - (ToKeyedSequence.prototype[_] = !0), - createClass(ToIndexedSequence, IndexedSeq), - (ToIndexedSequence.prototype.includes = function (s) { - return this._iter.includes(s); - }), - (ToIndexedSequence.prototype.__iterate = function (s, o) { - var i = this, - u = 0; - return this._iter.__iterate(function (o) { - return s(o, u++, i); - }, o); - }), - (ToIndexedSequence.prototype.__iterator = function (s, o) { - var i = this._iter.__iterator(U, o), - u = 0; - return new Iterator(function () { - var o = i.next(); - return o.done ? o : iteratorValue(s, u++, o.value, o); - }); - }), - createClass(ToSetSequence, SetSeq), - (ToSetSequence.prototype.has = function (s) { - return this._iter.includes(s); - }), - (ToSetSequence.prototype.__iterate = function (s, o) { - var i = this; - return this._iter.__iterate(function (o) { - return s(o, o, i); - }, o); - }), - (ToSetSequence.prototype.__iterator = function (s, o) { - var i = this._iter.__iterator(U, o); - return new Iterator(function () { - var o = i.next(); - return o.done ? o : iteratorValue(s, o.value, o.value, o); - }); - }), - createClass(FromEntriesSequence, KeyedSeq), - (FromEntriesSequence.prototype.entrySeq = function () { - return this._iter.toSeq(); - }), - (FromEntriesSequence.prototype.__iterate = function (s, o) { - var i = this; - return this._iter.__iterate(function (o) { - if (o) { - validateEntry(o); - var u = isIterable(o); - return s(u ? o.get(1) : o[1], u ? o.get(0) : o[0], i); - } - }, o); - }), - (FromEntriesSequence.prototype.__iterator = function (s, o) { - var i = this._iter.__iterator(U, o); - return new Iterator(function () { - for (;;) { - var o = i.next(); - if (o.done) return o; - var u = o.value; - if (u) { - validateEntry(u); - var _ = isIterable(u); - return iteratorValue(s, _ ? u.get(0) : u[0], _ ? u.get(1) : u[1], o); - } - } - }); - }), - (ToIndexedSequence.prototype.cacheResult = - ToKeyedSequence.prototype.cacheResult = - ToSetSequence.prototype.cacheResult = - FromEntriesSequence.prototype.cacheResult = - cacheResultThrough), - createClass(Record, KeyedCollection), - (Record.prototype.toString = function () { - return this.__toString(recordName(this) + ' {', '}'); - }), - (Record.prototype.has = function (s) { - return this._defaultValues.hasOwnProperty(s); - }), - (Record.prototype.get = function (s, o) { - if (!this.has(s)) return o; - var i = this._defaultValues[s]; - return this._map ? this._map.get(s, i) : i; - }), - (Record.prototype.clear = function () { - if (this.__ownerID) return this._map && this._map.clear(), this; - var s = this.constructor; - return s._empty || (s._empty = makeRecord(this, emptyMap())); - }), - (Record.prototype.set = function (s, o) { - if (!this.has(s)) - throw new Error('Cannot set unknown key "' + s + '" on ' + recordName(this)); - if (this._map && !this._map.has(s) && o === this._defaultValues[s]) return this; - var i = this._map && this._map.set(s, o); - return this.__ownerID || i === this._map ? this : makeRecord(this, i); - }), - (Record.prototype.remove = function (s) { - if (!this.has(s)) return this; - var o = this._map && this._map.remove(s); - return this.__ownerID || o === this._map ? this : makeRecord(this, o); - }), - (Record.prototype.wasAltered = function () { - return this._map.wasAltered(); - }), - (Record.prototype.__iterator = function (s, o) { - var i = this; - return KeyedIterable(this._defaultValues) - .map(function (s, o) { - return i.get(o); - }) - .__iterator(s, o); - }), - (Record.prototype.__iterate = function (s, o) { - var i = this; - return KeyedIterable(this._defaultValues) - .map(function (s, o) { - return i.get(o); - }) - .__iterate(s, o); - }), - (Record.prototype.__ensureOwner = function (s) { - if (s === this.__ownerID) return this; - var o = this._map && this._map.__ensureOwner(s); - return s ? makeRecord(this, o, s) : ((this.__ownerID = s), (this._map = o), this); - }); - var rt = Record.prototype; - function makeRecord(s, o, i) { - var u = Object.create(Object.getPrototypeOf(s)); - return (u._map = o), (u.__ownerID = i), u; - } - function recordName(s) { - return s._name || s.constructor.name || 'Record'; - } - function setProps(s, o) { - try { - o.forEach(setProp.bind(void 0, s)); - } catch (s) {} - } - function setProp(s, o) { - Object.defineProperty(s, o, { - get: function () { - return this.get(o); - }, - set: function (s) { - invariant(this.__ownerID, 'Cannot set on an immutable record.'), this.set(o, s); - } - }); - } - function Set(s) { - return null == s - ? emptySet() - : isSet(s) && !isOrdered(s) - ? s - : emptySet().withMutations(function (o) { - var i = SetIterable(s); - assertNotInfinite(i.size), - i.forEach(function (s) { - return o.add(s); - }); - }); - } - function isSet(s) { - return !(!s || !s[st]); - } - (rt[w] = rt.remove), - (rt.deleteIn = rt.removeIn = $e.removeIn), - (rt.merge = $e.merge), - (rt.mergeWith = $e.mergeWith), - (rt.mergeIn = $e.mergeIn), - (rt.mergeDeep = $e.mergeDeep), - (rt.mergeDeepWith = $e.mergeDeepWith), - (rt.mergeDeepIn = $e.mergeDeepIn), - (rt.setIn = $e.setIn), - (rt.update = $e.update), - (rt.updateIn = $e.updateIn), - (rt.withMutations = $e.withMutations), - (rt.asMutable = $e.asMutable), - (rt.asImmutable = $e.asImmutable), - createClass(Set, SetCollection), - (Set.of = function () { - return this(arguments); - }), - (Set.fromKeys = function (s) { - return this(KeyedIterable(s).keySeq()); - }), - (Set.prototype.toString = function () { - return this.__toString('Set {', '}'); - }), - (Set.prototype.has = function (s) { - return this._map.has(s); - }), - (Set.prototype.add = function (s) { - return updateSet(this, this._map.set(s, !0)); - }), - (Set.prototype.remove = function (s) { - return updateSet(this, this._map.remove(s)); - }), - (Set.prototype.clear = function () { - return updateSet(this, this._map.clear()); - }), - (Set.prototype.union = function () { - var o = s.call(arguments, 0); - return 0 === - (o = o.filter(function (s) { - return 0 !== s.size; - })).length - ? this - : 0 !== this.size || this.__ownerID || 1 !== o.length - ? this.withMutations(function (s) { - for (var i = 0; i < o.length; i++) - SetIterable(o[i]).forEach(function (o) { - return s.add(o); - }); - }) - : this.constructor(o[0]); - }), - (Set.prototype.intersect = function () { - var o = s.call(arguments, 0); - if (0 === o.length) return this; - o = o.map(function (s) { - return SetIterable(s); - }); - var i = this; - return this.withMutations(function (s) { - i.forEach(function (i) { - o.every(function (s) { - return s.includes(i); - }) || s.remove(i); - }); - }); - }), - (Set.prototype.subtract = function () { - var o = s.call(arguments, 0); - if (0 === o.length) return this; - o = o.map(function (s) { - return SetIterable(s); - }); - var i = this; - return this.withMutations(function (s) { - i.forEach(function (i) { - o.some(function (s) { - return s.includes(i); - }) && s.remove(i); - }); - }); - }), - (Set.prototype.merge = function () { - return this.union.apply(this, arguments); - }), - (Set.prototype.mergeWith = function (o) { - var i = s.call(arguments, 1); - return this.union.apply(this, i); - }), - (Set.prototype.sort = function (s) { - return OrderedSet(sortFactory(this, s)); - }), - (Set.prototype.sortBy = function (s, o) { - return OrderedSet(sortFactory(this, o, s)); - }), - (Set.prototype.wasAltered = function () { - return this._map.wasAltered(); - }), - (Set.prototype.__iterate = function (s, o) { - var i = this; - return this._map.__iterate(function (o, u) { - return s(u, u, i); - }, o); - }), - (Set.prototype.__iterator = function (s, o) { - return this._map - .map(function (s, o) { - return o; - }) - .__iterator(s, o); - }), - (Set.prototype.__ensureOwner = function (s) { - if (s === this.__ownerID) return this; - var o = this._map.__ensureOwner(s); - return s ? this.__make(o, s) : ((this.__ownerID = s), (this._map = o), this); - }), - (Set.isSet = isSet); - var nt, - st = '@@__IMMUTABLE_SET__@@', - ot = Set.prototype; - function updateSet(s, o) { - return s.__ownerID - ? ((s.size = o.size), (s._map = o), s) - : o === s._map - ? s - : 0 === o.size - ? s.__empty() - : s.__make(o); - } - function makeSet(s, o) { - var i = Object.create(ot); - return (i.size = s ? s.size : 0), (i._map = s), (i.__ownerID = o), i; - } - function emptySet() { - return nt || (nt = makeSet(emptyMap())); - } - function OrderedSet(s) { - return null == s - ? emptyOrderedSet() - : isOrderedSet(s) - ? s - : emptyOrderedSet().withMutations(function (o) { - var i = SetIterable(s); - assertNotInfinite(i.size), - i.forEach(function (s) { - return o.add(s); - }); - }); - } - function isOrderedSet(s) { - return isSet(s) && isOrdered(s); - } - (ot[st] = !0), - (ot[w] = ot.remove), - (ot.mergeDeep = ot.merge), - (ot.mergeDeepWith = ot.mergeWith), - (ot.withMutations = $e.withMutations), - (ot.asMutable = $e.asMutable), - (ot.asImmutable = $e.asImmutable), - (ot.__empty = emptySet), - (ot.__make = makeSet), - createClass(OrderedSet, Set), - (OrderedSet.of = function () { - return this(arguments); - }), - (OrderedSet.fromKeys = function (s) { - return this(KeyedIterable(s).keySeq()); - }), - (OrderedSet.prototype.toString = function () { - return this.__toString('OrderedSet {', '}'); - }), - (OrderedSet.isOrderedSet = isOrderedSet); - var it, - at = OrderedSet.prototype; - function makeOrderedSet(s, o) { - var i = Object.create(at); - return (i.size = s ? s.size : 0), (i._map = s), (i.__ownerID = o), i; - } - function emptyOrderedSet() { - return it || (it = makeOrderedSet(emptyOrderedMap())); - } - function Stack(s) { - return null == s ? emptyStack() : isStack(s) ? s : emptyStack().unshiftAll(s); - } - function isStack(s) { - return !(!s || !s[ct]); - } - (at[_] = !0), - (at.__empty = emptyOrderedSet), - (at.__make = makeOrderedSet), - createClass(Stack, IndexedCollection), - (Stack.of = function () { - return this(arguments); - }), - (Stack.prototype.toString = function () { - return this.__toString('Stack [', ']'); - }), - (Stack.prototype.get = function (s, o) { - var i = this._head; - for (s = wrapIndex(this, s); i && s--; ) i = i.next; - return i ? i.value : o; - }), - (Stack.prototype.peek = function () { - return this._head && this._head.value; - }), - (Stack.prototype.push = function () { - if (0 === arguments.length) return this; - for ( - var s = this.size + arguments.length, o = this._head, i = arguments.length - 1; - i >= 0; - i-- - ) - o = { value: arguments[i], next: o }; - return this.__ownerID - ? ((this.size = s), - (this._head = o), - (this.__hash = void 0), - (this.__altered = !0), - this) - : makeStack(s, o); - }), - (Stack.prototype.pushAll = function (s) { - if (0 === (s = IndexedIterable(s)).size) return this; - assertNotInfinite(s.size); - var o = this.size, - i = this._head; - return ( - s.reverse().forEach(function (s) { - o++, (i = { value: s, next: i }); - }), - this.__ownerID - ? ((this.size = o), - (this._head = i), - (this.__hash = void 0), - (this.__altered = !0), - this) - : makeStack(o, i) - ); - }), - (Stack.prototype.pop = function () { - return this.slice(1); - }), - (Stack.prototype.unshift = function () { - return this.push.apply(this, arguments); - }), - (Stack.prototype.unshiftAll = function (s) { - return this.pushAll(s); - }), - (Stack.prototype.shift = function () { - return this.pop.apply(this, arguments); - }), - (Stack.prototype.clear = function () { - return 0 === this.size - ? this - : this.__ownerID - ? ((this.size = 0), - (this._head = void 0), - (this.__hash = void 0), - (this.__altered = !0), - this) - : emptyStack(); - }), - (Stack.prototype.slice = function (s, o) { - if (wholeSlice(s, o, this.size)) return this; - var i = resolveBegin(s, this.size); - if (resolveEnd(o, this.size) !== this.size) - return IndexedCollection.prototype.slice.call(this, s, o); - for (var u = this.size - i, _ = this._head; i--; ) _ = _.next; - return this.__ownerID - ? ((this.size = u), - (this._head = _), - (this.__hash = void 0), - (this.__altered = !0), - this) - : makeStack(u, _); - }), - (Stack.prototype.__ensureOwner = function (s) { - return s === this.__ownerID - ? this - : s - ? makeStack(this.size, this._head, s, this.__hash) - : ((this.__ownerID = s), (this.__altered = !1), this); - }), - (Stack.prototype.__iterate = function (s, o) { - if (o) return this.reverse().__iterate(s); - for (var i = 0, u = this._head; u && !1 !== s(u.value, i++, this); ) u = u.next; - return i; - }), - (Stack.prototype.__iterator = function (s, o) { - if (o) return this.reverse().__iterator(s); - var i = 0, - u = this._head; - return new Iterator(function () { - if (u) { - var o = u.value; - return (u = u.next), iteratorValue(s, i++, o); - } - return iteratorDone(); - }); - }), - (Stack.isStack = isStack); - var lt, - ct = '@@__IMMUTABLE_STACK__@@', - ut = Stack.prototype; - function makeStack(s, o, i, u) { - var _ = Object.create(ut); - return ( - (_.size = s), - (_._head = o), - (_.__ownerID = i), - (_.__hash = u), - (_.__altered = !1), - _ - ); - } - function emptyStack() { - return lt || (lt = makeStack(0)); - } - function mixin(s, o) { - var keyCopier = function (i) { - s.prototype[i] = o[i]; - }; - return ( - Object.keys(o).forEach(keyCopier), - Object.getOwnPropertySymbols && Object.getOwnPropertySymbols(o).forEach(keyCopier), - s - ); - } - (ut[ct] = !0), - (ut.withMutations = $e.withMutations), - (ut.asMutable = $e.asMutable), - (ut.asImmutable = $e.asImmutable), - (ut.wasAltered = $e.wasAltered), - (Iterable.Iterator = Iterator), - mixin(Iterable, { - toArray: function () { - assertNotInfinite(this.size); - var s = new Array(this.size || 0); - return ( - this.valueSeq().__iterate(function (o, i) { - s[i] = o; - }), - s - ); - }, - toIndexedSeq: function () { - return new ToIndexedSequence(this); - }, - toJS: function () { - return this.toSeq() - .map(function (s) { - return s && 'function' == typeof s.toJS ? s.toJS() : s; - }) - .__toJS(); - }, - toJSON: function () { - return this.toSeq() - .map(function (s) { - return s && 'function' == typeof s.toJSON ? s.toJSON() : s; - }) - .__toJS(); - }, - toKeyedSeq: function () { - return new ToKeyedSequence(this, !0); - }, - toMap: function () { - return Map(this.toKeyedSeq()); - }, - toObject: function () { - assertNotInfinite(this.size); - var s = {}; - return ( - this.__iterate(function (o, i) { - s[i] = o; - }), - s - ); - }, - toOrderedMap: function () { - return OrderedMap(this.toKeyedSeq()); - }, - toOrderedSet: function () { - return OrderedSet(isKeyed(this) ? this.valueSeq() : this); - }, - toSet: function () { - return Set(isKeyed(this) ? this.valueSeq() : this); - }, - toSetSeq: function () { - return new ToSetSequence(this); - }, - toSeq: function () { - return isIndexed(this) - ? this.toIndexedSeq() - : isKeyed(this) - ? this.toKeyedSeq() - : this.toSetSeq(); - }, - toStack: function () { - return Stack(isKeyed(this) ? this.valueSeq() : this); - }, - toList: function () { - return List(isKeyed(this) ? this.valueSeq() : this); - }, - toString: function () { - return '[Iterable]'; - }, - __toString: function (s, o) { - return 0 === this.size - ? s + o - : s + ' ' + this.toSeq().map(this.__toStringMapper).join(', ') + ' ' + o; - }, - concat: function () { - return reify(this, concatFactory(this, s.call(arguments, 0))); - }, - includes: function (s) { - return this.some(function (o) { - return is(o, s); - }); - }, - entries: function () { - return this.__iterator(z); - }, - every: function (s, o) { - assertNotInfinite(this.size); - var i = !0; - return ( - this.__iterate(function (u, _, w) { - if (!s.call(o, u, _, w)) return (i = !1), !1; - }), - i - ); - }, - filter: function (s, o) { - return reify(this, filterFactory(this, s, o, !0)); - }, - find: function (s, o, i) { - var u = this.findEntry(s, o); - return u ? u[1] : i; - }, - forEach: function (s, o) { - return assertNotInfinite(this.size), this.__iterate(o ? s.bind(o) : s); - }, - join: function (s) { - assertNotInfinite(this.size), (s = void 0 !== s ? '' + s : ','); - var o = '', - i = !0; - return ( - this.__iterate(function (u) { - i ? (i = !1) : (o += s), (o += null != u ? u.toString() : ''); - }), - o - ); - }, - keys: function () { - return this.__iterator(V); - }, - map: function (s, o) { - return reify(this, mapFactory(this, s, o)); - }, - reduce: function (s, o, i) { - var u, _; - return ( - assertNotInfinite(this.size), - arguments.length < 2 ? (_ = !0) : (u = o), - this.__iterate(function (o, w, x) { - _ ? ((_ = !1), (u = o)) : (u = s.call(i, u, o, w, x)); - }), - u - ); - }, - reduceRight: function (s, o, i) { - var u = this.toKeyedSeq().reverse(); - return u.reduce.apply(u, arguments); - }, - reverse: function () { - return reify(this, reverseFactory(this, !0)); - }, - slice: function (s, o) { - return reify(this, sliceFactory(this, s, o, !0)); - }, - some: function (s, o) { - return !this.every(not(s), o); - }, - sort: function (s) { - return reify(this, sortFactory(this, s)); - }, - values: function () { - return this.__iterator(U); - }, - butLast: function () { - return this.slice(0, -1); - }, - isEmpty: function () { - return void 0 !== this.size - ? 0 === this.size - : !this.some(function () { - return !0; - }); - }, - count: function (s, o) { - return ensureSize(s ? this.toSeq().filter(s, o) : this); - }, - countBy: function (s, o) { - return countByFactory(this, s, o); - }, - equals: function (s) { - return deepEqual(this, s); - }, - entrySeq: function () { - var s = this; - if (s._cache) return new ArraySeq(s._cache); - var o = s.toSeq().map(entryMapper).toIndexedSeq(); - return ( - (o.fromEntrySeq = function () { - return s.toSeq(); - }), - o - ); - }, - filterNot: function (s, o) { - return this.filter(not(s), o); - }, - findEntry: function (s, o, i) { - var u = i; - return ( - this.__iterate(function (i, _, w) { - if (s.call(o, i, _, w)) return (u = [_, i]), !1; - }), - u - ); - }, - findKey: function (s, o) { - var i = this.findEntry(s, o); - return i && i[0]; - }, - findLast: function (s, o, i) { - return this.toKeyedSeq().reverse().find(s, o, i); - }, - findLastEntry: function (s, o, i) { - return this.toKeyedSeq().reverse().findEntry(s, o, i); - }, - findLastKey: function (s, o) { - return this.toKeyedSeq().reverse().findKey(s, o); - }, - first: function () { - return this.find(returnTrue); - }, - flatMap: function (s, o) { - return reify(this, flatMapFactory(this, s, o)); - }, - flatten: function (s) { - return reify(this, flattenFactory(this, s, !0)); - }, - fromEntrySeq: function () { - return new FromEntriesSequence(this); - }, - get: function (s, o) { - return this.find( - function (o, i) { - return is(i, s); - }, - void 0, - o - ); - }, - getIn: function (s, o) { - for (var i, u = this, _ = forceIterator(s); !(i = _.next()).done; ) { - var w = i.value; - if ((u = u && u.get ? u.get(w, L) : L) === L) return o; - } - return u; - }, - groupBy: function (s, o) { - return groupByFactory(this, s, o); - }, - has: function (s) { - return this.get(s, L) !== L; - }, - hasIn: function (s) { - return this.getIn(s, L) !== L; - }, - isSubset: function (s) { - return ( - (s = 'function' == typeof s.includes ? s : Iterable(s)), - this.every(function (o) { - return s.includes(o); - }) - ); - }, - isSuperset: function (s) { - return (s = 'function' == typeof s.isSubset ? s : Iterable(s)).isSubset(this); - }, - keyOf: function (s) { - return this.findKey(function (o) { - return is(o, s); - }); - }, - keySeq: function () { - return this.toSeq().map(keyMapper).toIndexedSeq(); - }, - last: function () { - return this.toSeq().reverse().first(); - }, - lastKeyOf: function (s) { - return this.toKeyedSeq().reverse().keyOf(s); - }, - max: function (s) { - return maxFactory(this, s); - }, - maxBy: function (s, o) { - return maxFactory(this, o, s); - }, - min: function (s) { - return maxFactory(this, s ? neg(s) : defaultNegComparator); - }, - minBy: function (s, o) { - return maxFactory(this, o ? neg(o) : defaultNegComparator, s); - }, - rest: function () { - return this.slice(1); - }, - skip: function (s) { - return this.slice(Math.max(0, s)); - }, - skipLast: function (s) { - return reify(this, this.toSeq().reverse().skip(s).reverse()); - }, - skipWhile: function (s, o) { - return reify(this, skipWhileFactory(this, s, o, !0)); - }, - skipUntil: function (s, o) { - return this.skipWhile(not(s), o); - }, - sortBy: function (s, o) { - return reify(this, sortFactory(this, o, s)); - }, - take: function (s) { - return this.slice(0, Math.max(0, s)); - }, - takeLast: function (s) { - return reify(this, this.toSeq().reverse().take(s).reverse()); - }, - takeWhile: function (s, o) { - return reify(this, takeWhileFactory(this, s, o)); - }, - takeUntil: function (s, o) { - return this.takeWhile(not(s), o); - }, - valueSeq: function () { - return this.toIndexedSeq(); - }, - hashCode: function () { - return this.__hash || (this.__hash = hashIterable(this)); - } - }); - var pt = Iterable.prototype; - (pt[o] = !0), - (pt[ee] = pt.values), - (pt.__toJS = pt.toArray), - (pt.__toStringMapper = quoteString), - (pt.inspect = pt.toSource = - function () { - return this.toString(); - }), - (pt.chain = pt.flatMap), - (pt.contains = pt.includes), - mixin(KeyedIterable, { - flip: function () { - return reify(this, flipFactory(this)); - }, - mapEntries: function (s, o) { - var i = this, - u = 0; - return reify( - this, - this.toSeq() - .map(function (_, w) { - return s.call(o, [w, _], u++, i); - }) - .fromEntrySeq() - ); - }, - mapKeys: function (s, o) { - var i = this; - return reify( - this, - this.toSeq() - .flip() - .map(function (u, _) { - return s.call(o, u, _, i); - }) - .flip() - ); - } - }); - var ht = KeyedIterable.prototype; - function keyMapper(s, o) { - return o; - } - function entryMapper(s, o) { - return [o, s]; - } - function not(s) { - return function () { - return !s.apply(this, arguments); - }; - } - function neg(s) { - return function () { - return -s.apply(this, arguments); - }; - } - function quoteString(s) { - return 'string' == typeof s ? JSON.stringify(s) : String(s); - } - function defaultZipper() { - return arrCopy(arguments); - } - function defaultNegComparator(s, o) { - return s < o ? 1 : s > o ? -1 : 0; - } - function hashIterable(s) { - if (s.size === 1 / 0) return 0; - var o = isOrdered(s), - i = isKeyed(s), - u = o ? 1 : 0; - return murmurHashOfSize( - s.__iterate( - i - ? o - ? function (s, o) { - u = (31 * u + hashMerge(hash(s), hash(o))) | 0; - } - : function (s, o) { - u = (u + hashMerge(hash(s), hash(o))) | 0; - } - : o - ? function (s) { - u = (31 * u + hash(s)) | 0; - } - : function (s) { - u = (u + hash(s)) | 0; - } - ), - u - ); - } - function murmurHashOfSize(s, o) { - return ( - (o = pe(o, 3432918353)), - (o = pe((o << 15) | (o >>> -15), 461845907)), - (o = pe((o << 13) | (o >>> -13), 5)), - (o = pe((o = (o + 3864292196) ^ s) ^ (o >>> 16), 2246822507)), - (o = smi((o = pe(o ^ (o >>> 13), 3266489909)) ^ (o >>> 16))) - ); - } - function hashMerge(s, o) { - return s ^ (o + 2654435769 + (s << 6) + (s >> 2)); - } - return ( - (ht[i] = !0), - (ht[ee] = pt.entries), - (ht.__toJS = pt.toObject), - (ht.__toStringMapper = function (s, o) { - return JSON.stringify(o) + ': ' + quoteString(s); - }), - mixin(IndexedIterable, { - toKeyedSeq: function () { - return new ToKeyedSequence(this, !1); - }, - filter: function (s, o) { - return reify(this, filterFactory(this, s, o, !1)); - }, - findIndex: function (s, o) { - var i = this.findEntry(s, o); - return i ? i[0] : -1; - }, - indexOf: function (s) { - var o = this.keyOf(s); - return void 0 === o ? -1 : o; - }, - lastIndexOf: function (s) { - var o = this.lastKeyOf(s); - return void 0 === o ? -1 : o; - }, - reverse: function () { - return reify(this, reverseFactory(this, !1)); - }, - slice: function (s, o) { - return reify(this, sliceFactory(this, s, o, !1)); - }, - splice: function (s, o) { - var i = arguments.length; - if (((o = Math.max(0 | o, 0)), 0 === i || (2 === i && !o))) return this; - s = resolveBegin(s, s < 0 ? this.count() : this.size); - var u = this.slice(0, s); - return reify( - this, - 1 === i ? u : u.concat(arrCopy(arguments, 2), this.slice(s + o)) - ); - }, - findLastIndex: function (s, o) { - var i = this.findLastEntry(s, o); - return i ? i[0] : -1; - }, - first: function () { - return this.get(0); - }, - flatten: function (s) { - return reify(this, flattenFactory(this, s, !1)); - }, - get: function (s, o) { - return (s = wrapIndex(this, s)) < 0 || - this.size === 1 / 0 || - (void 0 !== this.size && s > this.size) - ? o - : this.find( - function (o, i) { - return i === s; - }, - void 0, - o - ); - }, - has: function (s) { - return ( - (s = wrapIndex(this, s)) >= 0 && - (void 0 !== this.size - ? this.size === 1 / 0 || s < this.size - : -1 !== this.indexOf(s)) - ); - }, - interpose: function (s) { - return reify(this, interposeFactory(this, s)); - }, - interleave: function () { - var s = [this].concat(arrCopy(arguments)), - o = zipWithFactory(this.toSeq(), IndexedSeq.of, s), - i = o.flatten(!0); - return o.size && (i.size = o.size * s.length), reify(this, i); - }, - keySeq: function () { - return Range(0, this.size); - }, - last: function () { - return this.get(-1); - }, - skipWhile: function (s, o) { - return reify(this, skipWhileFactory(this, s, o, !1)); - }, - zip: function () { - return reify( - this, - zipWithFactory(this, defaultZipper, [this].concat(arrCopy(arguments))) - ); - }, - zipWith: function (s) { - var o = arrCopy(arguments); - return (o[0] = this), reify(this, zipWithFactory(this, s, o)); - } - }), - (IndexedIterable.prototype[u] = !0), - (IndexedIterable.prototype[_] = !0), - mixin(SetIterable, { - get: function (s, o) { - return this.has(s) ? s : o; - }, - includes: function (s) { - return this.has(s); - }, - keySeq: function () { - return this.valueSeq(); - } - }), - (SetIterable.prototype.has = pt.includes), - (SetIterable.prototype.contains = SetIterable.prototype.includes), - mixin(KeyedSeq, KeyedIterable.prototype), - mixin(IndexedSeq, IndexedIterable.prototype), - mixin(SetSeq, SetIterable.prototype), - mixin(KeyedCollection, KeyedIterable.prototype), - mixin(IndexedCollection, IndexedIterable.prototype), - mixin(SetCollection, SetIterable.prototype), - { - Iterable, - Seq, - Collection, - Map, - OrderedMap, - List, - Stack, - Set, - OrderedSet, - Record, - Range, - Repeat, - is, - fromJS - } - ); - })(); - }, - 56698: (s) => { - 'function' == typeof Object.create - ? (s.exports = function inherits(s, o) { - o && - ((s.super_ = o), - (s.prototype = Object.create(o.prototype, { - constructor: { value: s, enumerable: !1, writable: !0, configurable: !0 } - }))); - }) - : (s.exports = function inherits(s, o) { - if (o) { - s.super_ = o; - var TempCtor = function () {}; - (TempCtor.prototype = o.prototype), - (s.prototype = new TempCtor()), - (s.prototype.constructor = s); - } - }); - }, - 5419: (s) => { - s.exports = function (s, o, i, u) { - var _ = new Blob(void 0 !== u ? [u, s] : [s], { - type: i || 'application/octet-stream' - }); - if (void 0 !== window.navigator.msSaveBlob) window.navigator.msSaveBlob(_, o); - else { - var w = - window.URL && window.URL.createObjectURL - ? window.URL.createObjectURL(_) - : window.webkitURL.createObjectURL(_), - x = document.createElement('a'); - (x.style.display = 'none'), - (x.href = w), - x.setAttribute('download', o), - void 0 === x.download && x.setAttribute('target', '_blank'), - document.body.appendChild(x), - x.click(), - setTimeout(function () { - document.body.removeChild(x), window.URL.revokeObjectURL(w); - }, 200); - } - }; - }, - 20181: (s, o, i) => { - var u = /^\s+|\s+$/g, - _ = /^[-+]0x[0-9a-f]+$/i, - w = /^0b[01]+$/i, - x = /^0o[0-7]+$/i, - C = parseInt, - j = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g, - L = 'object' == typeof self && self && self.Object === Object && self, - B = j || L || Function('return this')(), - $ = Object.prototype.toString, - V = Math.max, - U = Math.min, - now = function () { - return B.Date.now(); - }; - function isObject(s) { - var o = typeof s; - return !!s && ('object' == o || 'function' == o); - } - function toNumber(s) { - if ('number' == typeof s) return s; - if ( - (function isSymbol(s) { - return ( - 'symbol' == typeof s || - ((function isObjectLike(s) { - return !!s && 'object' == typeof s; - })(s) && - '[object Symbol]' == $.call(s)) - ); - })(s) - ) - return NaN; - if (isObject(s)) { - var o = 'function' == typeof s.valueOf ? s.valueOf() : s; - s = isObject(o) ? o + '' : o; - } - if ('string' != typeof s) return 0 === s ? s : +s; - s = s.replace(u, ''); - var i = w.test(s); - return i || x.test(s) ? C(s.slice(2), i ? 2 : 8) : _.test(s) ? NaN : +s; - } - s.exports = function debounce(s, o, i) { - var u, - _, - w, - x, - C, - j, - L = 0, - B = !1, - $ = !1, - z = !0; - if ('function' != typeof s) throw new TypeError('Expected a function'); - function invokeFunc(o) { - var i = u, - w = _; - return (u = _ = void 0), (L = o), (x = s.apply(w, i)); - } - function shouldInvoke(s) { - var i = s - j; - return void 0 === j || i >= o || i < 0 || ($ && s - L >= w); - } - function timerExpired() { - var s = now(); - if (shouldInvoke(s)) return trailingEdge(s); - C = setTimeout( - timerExpired, - (function remainingWait(s) { - var i = o - (s - j); - return $ ? U(i, w - (s - L)) : i; - })(s) - ); - } - function trailingEdge(s) { - return (C = void 0), z && u ? invokeFunc(s) : ((u = _ = void 0), x); - } - function debounced() { - var s = now(), - i = shouldInvoke(s); - if (((u = arguments), (_ = this), (j = s), i)) { - if (void 0 === C) - return (function leadingEdge(s) { - return (L = s), (C = setTimeout(timerExpired, o)), B ? invokeFunc(s) : x; - })(j); - if ($) return (C = setTimeout(timerExpired, o)), invokeFunc(j); - } - return void 0 === C && (C = setTimeout(timerExpired, o)), x; - } - return ( - (o = toNumber(o) || 0), - isObject(i) && - ((B = !!i.leading), - (w = ($ = 'maxWait' in i) ? V(toNumber(i.maxWait) || 0, o) : w), - (z = 'trailing' in i ? !!i.trailing : z)), - (debounced.cancel = function cancel() { - void 0 !== C && clearTimeout(C), (L = 0), (u = j = _ = C = void 0); - }), - (debounced.flush = function flush() { - return void 0 === C ? x : trailingEdge(now()); - }), - debounced - ); - }; - }, - 55580: (s, o, i) => { - var u = i(56110)(i(9325), 'DataView'); - s.exports = u; - }, - 21549: (s, o, i) => { - var u = i(22032), - _ = i(63862), - w = i(66721), - x = i(12749), - C = i(35749); - function Hash(s) { - var o = -1, - i = null == s ? 0 : s.length; - for (this.clear(); ++o < i; ) { - var u = s[o]; - this.set(u[0], u[1]); - } - } - (Hash.prototype.clear = u), - (Hash.prototype.delete = _), - (Hash.prototype.get = w), - (Hash.prototype.has = x), - (Hash.prototype.set = C), - (s.exports = Hash); - }, - 30980: (s, o, i) => { - var u = i(39344), - _ = i(94033); - function LazyWrapper(s) { - (this.__wrapped__ = s), - (this.__actions__ = []), - (this.__dir__ = 1), - (this.__filtered__ = !1), - (this.__iteratees__ = []), - (this.__takeCount__ = 4294967295), - (this.__views__ = []); - } - (LazyWrapper.prototype = u(_.prototype)), - (LazyWrapper.prototype.constructor = LazyWrapper), - (s.exports = LazyWrapper); - }, - 80079: (s, o, i) => { - var u = i(63702), - _ = i(70080), - w = i(24739), - x = i(48655), - C = i(31175); - function ListCache(s) { - var o = -1, - i = null == s ? 0 : s.length; - for (this.clear(); ++o < i; ) { - var u = s[o]; - this.set(u[0], u[1]); - } - } - (ListCache.prototype.clear = u), - (ListCache.prototype.delete = _), - (ListCache.prototype.get = w), - (ListCache.prototype.has = x), - (ListCache.prototype.set = C), - (s.exports = ListCache); - }, - 56017: (s, o, i) => { - var u = i(39344), - _ = i(94033); - function LodashWrapper(s, o) { - (this.__wrapped__ = s), - (this.__actions__ = []), - (this.__chain__ = !!o), - (this.__index__ = 0), - (this.__values__ = void 0); - } - (LodashWrapper.prototype = u(_.prototype)), - (LodashWrapper.prototype.constructor = LodashWrapper), - (s.exports = LodashWrapper); - }, - 68223: (s, o, i) => { - var u = i(56110)(i(9325), 'Map'); - s.exports = u; - }, - 53661: (s, o, i) => { - var u = i(63040), - _ = i(17670), - w = i(90289), - x = i(4509), - C = i(72949); - function MapCache(s) { - var o = -1, - i = null == s ? 0 : s.length; - for (this.clear(); ++o < i; ) { - var u = s[o]; - this.set(u[0], u[1]); - } - } - (MapCache.prototype.clear = u), - (MapCache.prototype.delete = _), - (MapCache.prototype.get = w), - (MapCache.prototype.has = x), - (MapCache.prototype.set = C), - (s.exports = MapCache); - }, - 32804: (s, o, i) => { - var u = i(56110)(i(9325), 'Promise'); - s.exports = u; - }, - 76545: (s, o, i) => { - var u = i(56110)(i(9325), 'Set'); - s.exports = u; - }, - 38859: (s, o, i) => { - var u = i(53661), - _ = i(31380), - w = i(51459); - function SetCache(s) { - var o = -1, - i = null == s ? 0 : s.length; - for (this.__data__ = new u(); ++o < i; ) this.add(s[o]); - } - (SetCache.prototype.add = SetCache.prototype.push = _), - (SetCache.prototype.has = w), - (s.exports = SetCache); - }, - 37217: (s, o, i) => { - var u = i(80079), - _ = i(51420), - w = i(90938), - x = i(63605), - C = i(29817), - j = i(80945); - function Stack(s) { - var o = (this.__data__ = new u(s)); - this.size = o.size; - } - (Stack.prototype.clear = _), - (Stack.prototype.delete = w), - (Stack.prototype.get = x), - (Stack.prototype.has = C), - (Stack.prototype.set = j), - (s.exports = Stack); - }, - 51873: (s, o, i) => { - var u = i(9325).Symbol; - s.exports = u; - }, - 37828: (s, o, i) => { - var u = i(9325).Uint8Array; - s.exports = u; - }, - 28303: (s, o, i) => { - var u = i(56110)(i(9325), 'WeakMap'); - s.exports = u; - }, - 91033: (s) => { - s.exports = function apply(s, o, i) { - switch (i.length) { - case 0: - return s.call(o); - case 1: - return s.call(o, i[0]); - case 2: - return s.call(o, i[0], i[1]); - case 3: - return s.call(o, i[0], i[1], i[2]); - } - return s.apply(o, i); - }; - }, - 83729: (s) => { - s.exports = function arrayEach(s, o) { - for (var i = -1, u = null == s ? 0 : s.length; ++i < u && !1 !== o(s[i], i, s); ); - return s; - }; - }, - 79770: (s) => { - s.exports = function arrayFilter(s, o) { - for (var i = -1, u = null == s ? 0 : s.length, _ = 0, w = []; ++i < u; ) { - var x = s[i]; - o(x, i, s) && (w[_++] = x); - } - return w; - }; - }, - 15325: (s, o, i) => { - var u = i(96131); - s.exports = function arrayIncludes(s, o) { - return !!(null == s ? 0 : s.length) && u(s, o, 0) > -1; - }; - }, - 70695: (s, o, i) => { - var u = i(78096), - _ = i(72428), - w = i(56449), - x = i(3656), - C = i(30361), - j = i(37167), - L = Object.prototype.hasOwnProperty; - s.exports = function arrayLikeKeys(s, o) { - var i = w(s), - B = !i && _(s), - $ = !i && !B && x(s), - V = !i && !B && !$ && j(s), - U = i || B || $ || V, - z = U ? u(s.length, String) : [], - Y = z.length; - for (var Z in s) - (!o && !L.call(s, Z)) || - (U && - ('length' == Z || - ($ && ('offset' == Z || 'parent' == Z)) || - (V && ('buffer' == Z || 'byteLength' == Z || 'byteOffset' == Z)) || - C(Z, Y))) || - z.push(Z); - return z; - }; - }, - 34932: (s) => { - s.exports = function arrayMap(s, o) { - for (var i = -1, u = null == s ? 0 : s.length, _ = Array(u); ++i < u; ) - _[i] = o(s[i], i, s); - return _; - }; - }, - 14528: (s) => { - s.exports = function arrayPush(s, o) { - for (var i = -1, u = o.length, _ = s.length; ++i < u; ) s[_ + i] = o[i]; - return s; - }; - }, - 40882: (s) => { - s.exports = function arrayReduce(s, o, i, u) { - var _ = -1, - w = null == s ? 0 : s.length; - for (u && w && (i = s[++_]); ++_ < w; ) i = o(i, s[_], _, s); - return i; - }; - }, - 14248: (s) => { - s.exports = function arraySome(s, o) { - for (var i = -1, u = null == s ? 0 : s.length; ++i < u; ) if (o(s[i], i, s)) return !0; - return !1; - }; - }, - 61074: (s) => { - s.exports = function asciiToArray(s) { - return s.split(''); - }; - }, - 1733: (s) => { - var o = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; - s.exports = function asciiWords(s) { - return s.match(o) || []; - }; - }, - 87805: (s, o, i) => { - var u = i(43360), - _ = i(75288); - s.exports = function assignMergeValue(s, o, i) { - ((void 0 !== i && !_(s[o], i)) || (void 0 === i && !(o in s))) && u(s, o, i); - }; - }, - 16547: (s, o, i) => { - var u = i(43360), - _ = i(75288), - w = Object.prototype.hasOwnProperty; - s.exports = function assignValue(s, o, i) { - var x = s[o]; - (w.call(s, o) && _(x, i) && (void 0 !== i || o in s)) || u(s, o, i); - }; - }, - 26025: (s, o, i) => { - var u = i(75288); - s.exports = function assocIndexOf(s, o) { - for (var i = s.length; i--; ) if (u(s[i][0], o)) return i; - return -1; - }; - }, - 74733: (s, o, i) => { - var u = i(21791), - _ = i(95950); - s.exports = function baseAssign(s, o) { - return s && u(o, _(o), s); - }; - }, - 43838: (s, o, i) => { - var u = i(21791), - _ = i(37241); - s.exports = function baseAssignIn(s, o) { - return s && u(o, _(o), s); - }; - }, - 43360: (s, o, i) => { - var u = i(93243); - s.exports = function baseAssignValue(s, o, i) { - '__proto__' == o && u - ? u(s, o, { configurable: !0, enumerable: !0, value: i, writable: !0 }) - : (s[o] = i); - }; - }, - 9999: (s, o, i) => { - var u = i(37217), - _ = i(83729), - w = i(16547), - x = i(74733), - C = i(43838), - j = i(93290), - L = i(23007), - B = i(92271), - $ = i(48948), - V = i(50002), - U = i(83349), - z = i(5861), - Y = i(76189), - Z = i(77199), - ee = i(35529), - ie = i(56449), - ae = i(3656), - le = i(87730), - ce = i(23805), - pe = i(38440), - de = i(95950), - fe = i(37241), - ye = '[object Arguments]', - be = '[object Function]', - _e = '[object Object]', - we = {}; - (we[ye] = - we['[object Array]'] = - we['[object ArrayBuffer]'] = - we['[object DataView]'] = - we['[object Boolean]'] = - we['[object Date]'] = - we['[object Float32Array]'] = - we['[object Float64Array]'] = - we['[object Int8Array]'] = - we['[object Int16Array]'] = - we['[object Int32Array]'] = - we['[object Map]'] = - we['[object Number]'] = - we[_e] = - we['[object RegExp]'] = - we['[object Set]'] = - we['[object String]'] = - we['[object Symbol]'] = - we['[object Uint8Array]'] = - we['[object Uint8ClampedArray]'] = - we['[object Uint16Array]'] = - we['[object Uint32Array]'] = - !0), - (we['[object Error]'] = we[be] = we['[object WeakMap]'] = !1), - (s.exports = function baseClone(s, o, i, Se, xe, Pe) { - var Te, - Re = 1 & o, - qe = 2 & o, - $e = 4 & o; - if ((i && (Te = xe ? i(s, Se, xe, Pe) : i(s)), void 0 !== Te)) return Te; - if (!ce(s)) return s; - var ze = ie(s); - if (ze) { - if (((Te = Y(s)), !Re)) return L(s, Te); - } else { - var We = z(s), - He = We == be || '[object GeneratorFunction]' == We; - if (ae(s)) return j(s, Re); - if (We == _e || We == ye || (He && !xe)) { - if (((Te = qe || He ? {} : ee(s)), !Re)) - return qe ? $(s, C(Te, s)) : B(s, x(Te, s)); - } else { - if (!we[We]) return xe ? s : {}; - Te = Z(s, We, Re); - } - } - Pe || (Pe = new u()); - var Ye = Pe.get(s); - if (Ye) return Ye; - Pe.set(s, Te), - pe(s) - ? s.forEach(function (u) { - Te.add(baseClone(u, o, i, u, s, Pe)); - }) - : le(s) && - s.forEach(function (u, _) { - Te.set(_, baseClone(u, o, i, _, s, Pe)); - }); - var Xe = ze ? void 0 : ($e ? (qe ? U : V) : qe ? fe : de)(s); - return ( - _(Xe || s, function (u, _) { - Xe && (u = s[(_ = u)]), w(Te, _, baseClone(u, o, i, _, s, Pe)); - }), - Te - ); - }); - }, - 39344: (s, o, i) => { - var u = i(23805), - _ = Object.create, - w = (function () { - function object() {} - return function (s) { - if (!u(s)) return {}; - if (_) return _(s); - object.prototype = s; - var o = new object(); - return (object.prototype = void 0), o; - }; - })(); - s.exports = w; - }, - 80909: (s, o, i) => { - var u = i(30641), - _ = i(38329)(u); - s.exports = _; - }, - 2523: (s) => { - s.exports = function baseFindIndex(s, o, i, u) { - for (var _ = s.length, w = i + (u ? 1 : -1); u ? w-- : ++w < _; ) - if (o(s[w], w, s)) return w; - return -1; - }; - }, - 83120: (s, o, i) => { - var u = i(14528), - _ = i(45891); - s.exports = function baseFlatten(s, o, i, w, x) { - var C = -1, - j = s.length; - for (i || (i = _), x || (x = []); ++C < j; ) { - var L = s[C]; - o > 0 && i(L) - ? o > 1 - ? baseFlatten(L, o - 1, i, w, x) - : u(x, L) - : w || (x[x.length] = L); - } - return x; - }; - }, - 86649: (s, o, i) => { - var u = i(83221)(); - s.exports = u; - }, - 30641: (s, o, i) => { - var u = i(86649), - _ = i(95950); - s.exports = function baseForOwn(s, o) { - return s && u(s, o, _); - }; - }, - 47422: (s, o, i) => { - var u = i(31769), - _ = i(77797); - s.exports = function baseGet(s, o) { - for (var i = 0, w = (o = u(o, s)).length; null != s && i < w; ) s = s[_(o[i++])]; - return i && i == w ? s : void 0; - }; - }, - 82199: (s, o, i) => { - var u = i(14528), - _ = i(56449); - s.exports = function baseGetAllKeys(s, o, i) { - var w = o(s); - return _(s) ? w : u(w, i(s)); - }; - }, - 72552: (s, o, i) => { - var u = i(51873), - _ = i(659), - w = i(59350), - x = u ? u.toStringTag : void 0; - s.exports = function baseGetTag(s) { - return null == s - ? void 0 === s - ? '[object Undefined]' - : '[object Null]' - : x && x in Object(s) - ? _(s) - : w(s); - }; - }, - 20426: (s) => { - var o = Object.prototype.hasOwnProperty; - s.exports = function baseHas(s, i) { - return null != s && o.call(s, i); - }; - }, - 28077: (s) => { - s.exports = function baseHasIn(s, o) { - return null != s && o in Object(s); - }; - }, - 96131: (s, o, i) => { - var u = i(2523), - _ = i(85463), - w = i(76959); - s.exports = function baseIndexOf(s, o, i) { - return o == o ? w(s, o, i) : u(s, _, i); - }; - }, - 27534: (s, o, i) => { - var u = i(72552), - _ = i(40346); - s.exports = function baseIsArguments(s) { - return _(s) && '[object Arguments]' == u(s); - }; - }, - 60270: (s, o, i) => { - var u = i(87068), - _ = i(40346); - s.exports = function baseIsEqual(s, o, i, w, x) { - return ( - s === o || - (null == s || null == o || (!_(s) && !_(o)) - ? s != s && o != o - : u(s, o, i, w, baseIsEqual, x)) - ); - }; - }, - 87068: (s, o, i) => { - var u = i(37217), - _ = i(25911), - w = i(21986), - x = i(50689), - C = i(5861), - j = i(56449), - L = i(3656), - B = i(37167), - $ = '[object Arguments]', - V = '[object Array]', - U = '[object Object]', - z = Object.prototype.hasOwnProperty; - s.exports = function baseIsEqualDeep(s, o, i, Y, Z, ee) { - var ie = j(s), - ae = j(o), - le = ie ? V : C(s), - ce = ae ? V : C(o), - pe = (le = le == $ ? U : le) == U, - de = (ce = ce == $ ? U : ce) == U, - fe = le == ce; - if (fe && L(s)) { - if (!L(o)) return !1; - (ie = !0), (pe = !1); - } - if (fe && !pe) - return ( - ee || (ee = new u()), ie || B(s) ? _(s, o, i, Y, Z, ee) : w(s, o, le, i, Y, Z, ee) - ); - if (!(1 & i)) { - var ye = pe && z.call(s, '__wrapped__'), - be = de && z.call(o, '__wrapped__'); - if (ye || be) { - var _e = ye ? s.value() : s, - we = be ? o.value() : o; - return ee || (ee = new u()), Z(_e, we, i, Y, ee); - } - } - return !!fe && (ee || (ee = new u()), x(s, o, i, Y, Z, ee)); - }; - }, - 29172: (s, o, i) => { - var u = i(5861), - _ = i(40346); - s.exports = function baseIsMap(s) { - return _(s) && '[object Map]' == u(s); - }; - }, - 41799: (s, o, i) => { - var u = i(37217), - _ = i(60270); - s.exports = function baseIsMatch(s, o, i, w) { - var x = i.length, - C = x, - j = !w; - if (null == s) return !C; - for (s = Object(s); x--; ) { - var L = i[x]; - if (j && L[2] ? L[1] !== s[L[0]] : !(L[0] in s)) return !1; - } - for (; ++x < C; ) { - var B = (L = i[x])[0], - $ = s[B], - V = L[1]; - if (j && L[2]) { - if (void 0 === $ && !(B in s)) return !1; - } else { - var U = new u(); - if (w) var z = w($, V, B, s, o, U); - if (!(void 0 === z ? _(V, $, 3, w, U) : z)) return !1; - } - } - return !0; - }; - }, - 85463: (s) => { - s.exports = function baseIsNaN(s) { - return s != s; - }; - }, - 45083: (s, o, i) => { - var u = i(1882), - _ = i(87296), - w = i(23805), - x = i(47473), - C = /^\[object .+?Constructor\]$/, - j = Function.prototype, - L = Object.prototype, - B = j.toString, - $ = L.hasOwnProperty, - V = RegExp( - '^' + - B.call($) - .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') - .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + - '$' - ); - s.exports = function baseIsNative(s) { - return !(!w(s) || _(s)) && (u(s) ? V : C).test(x(s)); - }; - }, - 16038: (s, o, i) => { - var u = i(5861), - _ = i(40346); - s.exports = function baseIsSet(s) { - return _(s) && '[object Set]' == u(s); - }; - }, - 4901: (s, o, i) => { - var u = i(72552), - _ = i(30294), - w = i(40346), - x = {}; - (x['[object Float32Array]'] = - x['[object Float64Array]'] = - x['[object Int8Array]'] = - x['[object Int16Array]'] = - x['[object Int32Array]'] = - x['[object Uint8Array]'] = - x['[object Uint8ClampedArray]'] = - x['[object Uint16Array]'] = - x['[object Uint32Array]'] = - !0), - (x['[object Arguments]'] = - x['[object Array]'] = - x['[object ArrayBuffer]'] = - x['[object Boolean]'] = - x['[object DataView]'] = - x['[object Date]'] = - x['[object Error]'] = - x['[object Function]'] = - x['[object Map]'] = - x['[object Number]'] = - x['[object Object]'] = - x['[object RegExp]'] = - x['[object Set]'] = - x['[object String]'] = - x['[object WeakMap]'] = - !1), - (s.exports = function baseIsTypedArray(s) { - return w(s) && _(s.length) && !!x[u(s)]; - }); - }, - 15389: (s, o, i) => { - var u = i(93663), - _ = i(87978), - w = i(83488), - x = i(56449), - C = i(50583); - s.exports = function baseIteratee(s) { - return 'function' == typeof s - ? s - : null == s - ? w - : 'object' == typeof s - ? x(s) - ? _(s[0], s[1]) - : u(s) - : C(s); - }; - }, - 88984: (s, o, i) => { - var u = i(55527), - _ = i(3650), - w = Object.prototype.hasOwnProperty; - s.exports = function baseKeys(s) { - if (!u(s)) return _(s); - var o = []; - for (var i in Object(s)) w.call(s, i) && 'constructor' != i && o.push(i); - return o; - }; - }, - 72903: (s, o, i) => { - var u = i(23805), - _ = i(55527), - w = i(90181), - x = Object.prototype.hasOwnProperty; - s.exports = function baseKeysIn(s) { - if (!u(s)) return w(s); - var o = _(s), - i = []; - for (var C in s) ('constructor' != C || (!o && x.call(s, C))) && i.push(C); - return i; - }; - }, - 94033: (s) => { - s.exports = function baseLodash() {}; - }, - 93663: (s, o, i) => { - var u = i(41799), - _ = i(10776), - w = i(67197); - s.exports = function baseMatches(s) { - var o = _(s); - return 1 == o.length && o[0][2] - ? w(o[0][0], o[0][1]) - : function (i) { - return i === s || u(i, s, o); - }; - }; - }, - 87978: (s, o, i) => { - var u = i(60270), - _ = i(58156), - w = i(80631), - x = i(28586), - C = i(30756), - j = i(67197), - L = i(77797); - s.exports = function baseMatchesProperty(s, o) { - return x(s) && C(o) - ? j(L(s), o) - : function (i) { - var x = _(i, s); - return void 0 === x && x === o ? w(i, s) : u(o, x, 3); - }; - }; - }, - 85250: (s, o, i) => { - var u = i(37217), - _ = i(87805), - w = i(86649), - x = i(42824), - C = i(23805), - j = i(37241), - L = i(14974); - s.exports = function baseMerge(s, o, i, B, $) { - s !== o && - w( - o, - function (w, j) { - if (($ || ($ = new u()), C(w))) x(s, o, j, i, baseMerge, B, $); - else { - var V = B ? B(L(s, j), w, j + '', s, o, $) : void 0; - void 0 === V && (V = w), _(s, j, V); - } - }, - j - ); - }; - }, - 42824: (s, o, i) => { - var u = i(87805), - _ = i(93290), - w = i(71961), - x = i(23007), - C = i(35529), - j = i(72428), - L = i(56449), - B = i(83693), - $ = i(3656), - V = i(1882), - U = i(23805), - z = i(11331), - Y = i(37167), - Z = i(14974), - ee = i(69884); - s.exports = function baseMergeDeep(s, o, i, ie, ae, le, ce) { - var pe = Z(s, i), - de = Z(o, i), - fe = ce.get(de); - if (fe) u(s, i, fe); - else { - var ye = le ? le(pe, de, i + '', s, o, ce) : void 0, - be = void 0 === ye; - if (be) { - var _e = L(de), - we = !_e && $(de), - Se = !_e && !we && Y(de); - (ye = de), - _e || we || Se - ? L(pe) - ? (ye = pe) - : B(pe) - ? (ye = x(pe)) - : we - ? ((be = !1), (ye = _(de, !0))) - : Se - ? ((be = !1), (ye = w(de, !0))) - : (ye = []) - : z(de) || j(de) - ? ((ye = pe), j(pe) ? (ye = ee(pe)) : (U(pe) && !V(pe)) || (ye = C(de))) - : (be = !1); - } - be && (ce.set(de, ye), ae(ye, de, ie, le, ce), ce.delete(de)), u(s, i, ye); - } - }; - }, - 47237: (s) => { - s.exports = function baseProperty(s) { - return function (o) { - return null == o ? void 0 : o[s]; - }; - }; - }, - 17255: (s, o, i) => { - var u = i(47422); - s.exports = function basePropertyDeep(s) { - return function (o) { - return u(o, s); - }; - }; - }, - 54552: (s) => { - s.exports = function basePropertyOf(s) { - return function (o) { - return null == s ? void 0 : s[o]; - }; - }; - }, - 85558: (s) => { - s.exports = function baseReduce(s, o, i, u, _) { - return ( - _(s, function (s, _, w) { - i = u ? ((u = !1), s) : o(i, s, _, w); - }), - i - ); - }; - }, - 69302: (s, o, i) => { - var u = i(83488), - _ = i(56757), - w = i(32865); - s.exports = function baseRest(s, o) { - return w(_(s, o, u), s + ''); - }; - }, - 73170: (s, o, i) => { - var u = i(16547), - _ = i(31769), - w = i(30361), - x = i(23805), - C = i(77797); - s.exports = function baseSet(s, o, i, j) { - if (!x(s)) return s; - for (var L = -1, B = (o = _(o, s)).length, $ = B - 1, V = s; null != V && ++L < B; ) { - var U = C(o[L]), - z = i; - if ('__proto__' === U || 'constructor' === U || 'prototype' === U) return s; - if (L != $) { - var Y = V[U]; - void 0 === (z = j ? j(Y, U, V) : void 0) && (z = x(Y) ? Y : w(o[L + 1]) ? [] : {}); - } - u(V, U, z), (V = V[U]); - } - return s; - }; - }, - 68882: (s, o, i) => { - var u = i(83488), - _ = i(48152), - w = _ - ? function (s, o) { - return _.set(s, o), s; - } - : u; - s.exports = w; - }, - 19570: (s, o, i) => { - var u = i(37334), - _ = i(93243), - w = i(83488), - x = _ - ? function (s, o) { - return _(s, 'toString', { - configurable: !0, - enumerable: !1, - value: u(o), - writable: !0 - }); - } - : w; - s.exports = x; - }, - 25160: (s) => { - s.exports = function baseSlice(s, o, i) { - var u = -1, - _ = s.length; - o < 0 && (o = -o > _ ? 0 : _ + o), - (i = i > _ ? _ : i) < 0 && (i += _), - (_ = o > i ? 0 : (i - o) >>> 0), - (o >>>= 0); - for (var w = Array(_); ++u < _; ) w[u] = s[u + o]; - return w; - }; - }, - 90916: (s, o, i) => { - var u = i(80909); - s.exports = function baseSome(s, o) { - var i; - return ( - u(s, function (s, u, _) { - return !(i = o(s, u, _)); - }), - !!i - ); - }; - }, - 78096: (s) => { - s.exports = function baseTimes(s, o) { - for (var i = -1, u = Array(s); ++i < s; ) u[i] = o(i); - return u; - }; - }, - 77556: (s, o, i) => { - var u = i(51873), - _ = i(34932), - w = i(56449), - x = i(44394), - C = u ? u.prototype : void 0, - j = C ? C.toString : void 0; - s.exports = function baseToString(s) { - if ('string' == typeof s) return s; - if (w(s)) return _(s, baseToString) + ''; - if (x(s)) return j ? j.call(s) : ''; - var o = s + ''; - return '0' == o && 1 / s == -1 / 0 ? '-0' : o; - }; - }, - 54128: (s, o, i) => { - var u = i(31800), - _ = /^\s+/; - s.exports = function baseTrim(s) { - return s ? s.slice(0, u(s) + 1).replace(_, '') : s; - }; - }, - 27301: (s) => { - s.exports = function baseUnary(s) { - return function (o) { - return s(o); - }; - }; - }, - 19931: (s, o, i) => { - var u = i(31769), - _ = i(68090), - w = i(68969), - x = i(77797); - s.exports = function baseUnset(s, o) { - return (o = u(o, s)), null == (s = w(s, o)) || delete s[x(_(o))]; - }; - }, - 51234: (s) => { - s.exports = function baseZipObject(s, o, i) { - for (var u = -1, _ = s.length, w = o.length, x = {}; ++u < _; ) { - var C = u < w ? o[u] : void 0; - i(x, s[u], C); - } - return x; - }; - }, - 19219: (s) => { - s.exports = function cacheHas(s, o) { - return s.has(o); - }; - }, - 31769: (s, o, i) => { - var u = i(56449), - _ = i(28586), - w = i(61802), - x = i(13222); - s.exports = function castPath(s, o) { - return u(s) ? s : _(s, o) ? [s] : w(x(s)); - }; - }, - 28754: (s, o, i) => { - var u = i(25160); - s.exports = function castSlice(s, o, i) { - var _ = s.length; - return (i = void 0 === i ? _ : i), !o && i >= _ ? s : u(s, o, i); - }; - }, - 49653: (s, o, i) => { - var u = i(37828); - s.exports = function cloneArrayBuffer(s) { - var o = new s.constructor(s.byteLength); - return new u(o).set(new u(s)), o; - }; - }, - 93290: (s, o, i) => { - s = i.nmd(s); - var u = i(9325), - _ = o && !o.nodeType && o, - w = _ && s && !s.nodeType && s, - x = w && w.exports === _ ? u.Buffer : void 0, - C = x ? x.allocUnsafe : void 0; - s.exports = function cloneBuffer(s, o) { - if (o) return s.slice(); - var i = s.length, - u = C ? C(i) : new s.constructor(i); - return s.copy(u), u; - }; - }, - 76169: (s, o, i) => { - var u = i(49653); - s.exports = function cloneDataView(s, o) { - var i = o ? u(s.buffer) : s.buffer; - return new s.constructor(i, s.byteOffset, s.byteLength); - }; - }, - 73201: (s) => { - var o = /\w*$/; - s.exports = function cloneRegExp(s) { - var i = new s.constructor(s.source, o.exec(s)); - return (i.lastIndex = s.lastIndex), i; - }; - }, - 93736: (s, o, i) => { - var u = i(51873), - _ = u ? u.prototype : void 0, - w = _ ? _.valueOf : void 0; - s.exports = function cloneSymbol(s) { - return w ? Object(w.call(s)) : {}; - }; - }, - 71961: (s, o, i) => { - var u = i(49653); - s.exports = function cloneTypedArray(s, o) { - var i = o ? u(s.buffer) : s.buffer; - return new s.constructor(i, s.byteOffset, s.length); - }; - }, - 91596: (s) => { - var o = Math.max; - s.exports = function composeArgs(s, i, u, _) { - for ( - var w = -1, - x = s.length, - C = u.length, - j = -1, - L = i.length, - B = o(x - C, 0), - $ = Array(L + B), - V = !_; - ++j < L; - - ) - $[j] = i[j]; - for (; ++w < C; ) (V || w < x) && ($[u[w]] = s[w]); - for (; B--; ) $[j++] = s[w++]; - return $; - }; - }, - 53320: (s) => { - var o = Math.max; - s.exports = function composeArgsRight(s, i, u, _) { - for ( - var w = -1, - x = s.length, - C = -1, - j = u.length, - L = -1, - B = i.length, - $ = o(x - j, 0), - V = Array($ + B), - U = !_; - ++w < $; - - ) - V[w] = s[w]; - for (var z = w; ++L < B; ) V[z + L] = i[L]; - for (; ++C < j; ) (U || w < x) && (V[z + u[C]] = s[w++]); - return V; - }; - }, - 23007: (s) => { - s.exports = function copyArray(s, o) { - var i = -1, - u = s.length; - for (o || (o = Array(u)); ++i < u; ) o[i] = s[i]; - return o; - }; - }, - 21791: (s, o, i) => { - var u = i(16547), - _ = i(43360); - s.exports = function copyObject(s, o, i, w) { - var x = !i; - i || (i = {}); - for (var C = -1, j = o.length; ++C < j; ) { - var L = o[C], - B = w ? w(i[L], s[L], L, i, s) : void 0; - void 0 === B && (B = s[L]), x ? _(i, L, B) : u(i, L, B); - } - return i; - }; - }, - 92271: (s, o, i) => { - var u = i(21791), - _ = i(4664); - s.exports = function copySymbols(s, o) { - return u(s, _(s), o); - }; - }, - 48948: (s, o, i) => { - var u = i(21791), - _ = i(86375); - s.exports = function copySymbolsIn(s, o) { - return u(s, _(s), o); - }; - }, - 55481: (s, o, i) => { - var u = i(9325)['__core-js_shared__']; - s.exports = u; - }, - 58523: (s) => { - s.exports = function countHolders(s, o) { - for (var i = s.length, u = 0; i--; ) s[i] === o && ++u; - return u; - }; - }, - 20999: (s, o, i) => { - var u = i(69302), - _ = i(36800); - s.exports = function createAssigner(s) { - return u(function (o, i) { - var u = -1, - w = i.length, - x = w > 1 ? i[w - 1] : void 0, - C = w > 2 ? i[2] : void 0; - for ( - x = s.length > 3 && 'function' == typeof x ? (w--, x) : void 0, - C && _(i[0], i[1], C) && ((x = w < 3 ? void 0 : x), (w = 1)), - o = Object(o); - ++u < w; - - ) { - var j = i[u]; - j && s(o, j, u, x); - } - return o; - }); - }; - }, - 38329: (s, o, i) => { - var u = i(64894); - s.exports = function createBaseEach(s, o) { - return function (i, _) { - if (null == i) return i; - if (!u(i)) return s(i, _); - for ( - var w = i.length, x = o ? w : -1, C = Object(i); - (o ? x-- : ++x < w) && !1 !== _(C[x], x, C); - - ); - return i; - }; - }; - }, - 83221: (s) => { - s.exports = function createBaseFor(s) { - return function (o, i, u) { - for (var _ = -1, w = Object(o), x = u(o), C = x.length; C--; ) { - var j = x[s ? C : ++_]; - if (!1 === i(w[j], j, w)) break; - } - return o; - }; - }; - }, - 11842: (s, o, i) => { - var u = i(82819), - _ = i(9325); - s.exports = function createBind(s, o, i) { - var w = 1 & o, - x = u(s); - return function wrapper() { - return (this && this !== _ && this instanceof wrapper ? x : s).apply( - w ? i : this, - arguments - ); - }; - }; - }, - 12507: (s, o, i) => { - var u = i(28754), - _ = i(49698), - w = i(63912), - x = i(13222); - s.exports = function createCaseFirst(s) { - return function (o) { - o = x(o); - var i = _(o) ? w(o) : void 0, - C = i ? i[0] : o.charAt(0), - j = i ? u(i, 1).join('') : o.slice(1); - return C[s]() + j; - }; - }; - }, - 45539: (s, o, i) => { - var u = i(40882), - _ = i(50828), - w = i(66645), - x = RegExp("['’]", 'g'); - s.exports = function createCompounder(s) { - return function (o) { - return u(w(_(o).replace(x, '')), s, ''); - }; - }; - }, - 82819: (s, o, i) => { - var u = i(39344), - _ = i(23805); - s.exports = function createCtor(s) { - return function () { - var o = arguments; - switch (o.length) { - case 0: - return new s(); - case 1: - return new s(o[0]); - case 2: - return new s(o[0], o[1]); - case 3: - return new s(o[0], o[1], o[2]); - case 4: - return new s(o[0], o[1], o[2], o[3]); - case 5: - return new s(o[0], o[1], o[2], o[3], o[4]); - case 6: - return new s(o[0], o[1], o[2], o[3], o[4], o[5]); - case 7: - return new s(o[0], o[1], o[2], o[3], o[4], o[5], o[6]); - } - var i = u(s.prototype), - w = s.apply(i, o); - return _(w) ? w : i; - }; - }; - }, - 77078: (s, o, i) => { - var u = i(91033), - _ = i(82819), - w = i(37471), - x = i(18073), - C = i(11287), - j = i(36306), - L = i(9325); - s.exports = function createCurry(s, o, i) { - var B = _(s); - return function wrapper() { - for (var _ = arguments.length, $ = Array(_), V = _, U = C(wrapper); V--; ) - $[V] = arguments[V]; - var z = _ < 3 && $[0] !== U && $[_ - 1] !== U ? [] : j($, U); - return (_ -= z.length) < i - ? x(s, o, w, wrapper.placeholder, void 0, $, z, void 0, void 0, i - _) - : u(this && this !== L && this instanceof wrapper ? B : s, this, $); - }; - }; - }, - 62006: (s, o, i) => { - var u = i(15389), - _ = i(64894), - w = i(95950); - s.exports = function createFind(s) { - return function (o, i, x) { - var C = Object(o); - if (!_(o)) { - var j = u(i, 3); - (o = w(o)), - (i = function (s) { - return j(C[s], s, C); - }); - } - var L = s(o, i, x); - return L > -1 ? C[j ? o[L] : L] : void 0; - }; - }; - }, - 37471: (s, o, i) => { - var u = i(91596), - _ = i(53320), - w = i(58523), - x = i(82819), - C = i(18073), - j = i(11287), - L = i(68294), - B = i(36306), - $ = i(9325); - s.exports = function createHybrid(s, o, i, V, U, z, Y, Z, ee, ie) { - var ae = 128 & o, - le = 1 & o, - ce = 2 & o, - pe = 24 & o, - de = 512 & o, - fe = ce ? void 0 : x(s); - return function wrapper() { - for (var ye = arguments.length, be = Array(ye), _e = ye; _e--; ) - be[_e] = arguments[_e]; - if (pe) - var we = j(wrapper), - Se = w(be, we); - if ( - (V && (be = u(be, V, U, pe)), - z && (be = _(be, z, Y, pe)), - (ye -= Se), - pe && ye < ie) - ) { - var xe = B(be, we); - return C(s, o, createHybrid, wrapper.placeholder, i, be, xe, Z, ee, ie - ye); - } - var Pe = le ? i : this, - Te = ce ? Pe[s] : s; - return ( - (ye = be.length), - Z ? (be = L(be, Z)) : de && ye > 1 && be.reverse(), - ae && ee < ye && (be.length = ee), - this && this !== $ && this instanceof wrapper && (Te = fe || x(Te)), - Te.apply(Pe, be) - ); - }; - }; - }, - 24168: (s, o, i) => { - var u = i(91033), - _ = i(82819), - w = i(9325); - s.exports = function createPartial(s, o, i, x) { - var C = 1 & o, - j = _(s); - return function wrapper() { - for ( - var o = -1, - _ = arguments.length, - L = -1, - B = x.length, - $ = Array(B + _), - V = this && this !== w && this instanceof wrapper ? j : s; - ++L < B; - - ) - $[L] = x[L]; - for (; _--; ) $[L++] = arguments[++o]; - return u(V, C ? i : this, $); - }; - }; - }, - 18073: (s, o, i) => { - var u = i(85087), - _ = i(54641), - w = i(70981); - s.exports = function createRecurry(s, o, i, x, C, j, L, B, $, V) { - var U = 8 & o; - (o |= U ? 32 : 64), 4 & (o &= ~(U ? 64 : 32)) || (o &= -4); - var z = [ - s, - o, - C, - U ? j : void 0, - U ? L : void 0, - U ? void 0 : j, - U ? void 0 : L, - B, - $, - V - ], - Y = i.apply(void 0, z); - return u(s) && _(Y, z), (Y.placeholder = x), w(Y, s, o); - }; - }, - 66977: (s, o, i) => { - var u = i(68882), - _ = i(11842), - w = i(77078), - x = i(37471), - C = i(24168), - j = i(37381), - L = i(3209), - B = i(54641), - $ = i(70981), - V = i(61489), - U = Math.max; - s.exports = function createWrap(s, o, i, z, Y, Z, ee, ie) { - var ae = 2 & o; - if (!ae && 'function' != typeof s) throw new TypeError('Expected a function'); - var le = z ? z.length : 0; - if ( - (le || ((o &= -97), (z = Y = void 0)), - (ee = void 0 === ee ? ee : U(V(ee), 0)), - (ie = void 0 === ie ? ie : V(ie)), - (le -= Y ? Y.length : 0), - 64 & o) - ) { - var ce = z, - pe = Y; - z = Y = void 0; - } - var de = ae ? void 0 : j(s), - fe = [s, o, i, z, Y, ce, pe, Z, ee, ie]; - if ( - (de && L(fe, de), - (s = fe[0]), - (o = fe[1]), - (i = fe[2]), - (z = fe[3]), - (Y = fe[4]), - !(ie = fe[9] = void 0 === fe[9] ? (ae ? 0 : s.length) : U(fe[9] - le, 0)) && - 24 & o && - (o &= -25), - o && 1 != o) - ) - ye = - 8 == o || 16 == o - ? w(s, o, ie) - : (32 != o && 33 != o) || Y.length - ? x.apply(void 0, fe) - : C(s, o, i, z); - else var ye = _(s, o, i); - return $((de ? u : B)(ye, fe), s, o); - }; - }, - 53138: (s, o, i) => { - var u = i(11331); - s.exports = function customOmitClone(s) { - return u(s) ? void 0 : s; - }; - }, - 24647: (s, o, i) => { - var u = i(54552)({ - À: 'A', - Á: 'A', - Â: 'A', - Ã: 'A', - Ä: 'A', - Å: 'A', - à: 'a', - á: 'a', - â: 'a', - ã: 'a', - ä: 'a', - å: 'a', - Ç: 'C', - ç: 'c', - Ð: 'D', - ð: 'd', - È: 'E', - É: 'E', - Ê: 'E', - Ë: 'E', - è: 'e', - é: 'e', - ê: 'e', - ë: 'e', - Ì: 'I', - Í: 'I', - Î: 'I', - Ï: 'I', - ì: 'i', - í: 'i', - î: 'i', - ï: 'i', - Ñ: 'N', - ñ: 'n', - Ò: 'O', - Ó: 'O', - Ô: 'O', - Õ: 'O', - Ö: 'O', - Ø: 'O', - ò: 'o', - ó: 'o', - ô: 'o', - õ: 'o', - ö: 'o', - ø: 'o', - Ù: 'U', - Ú: 'U', - Û: 'U', - Ü: 'U', - ù: 'u', - ú: 'u', - û: 'u', - ü: 'u', - Ý: 'Y', - ý: 'y', - ÿ: 'y', - Æ: 'Ae', - æ: 'ae', - Þ: 'Th', - þ: 'th', - ß: 'ss', - Ā: 'A', - Ă: 'A', - Ą: 'A', - ā: 'a', - ă: 'a', - ą: 'a', - Ć: 'C', - Ĉ: 'C', - Ċ: 'C', - Č: 'C', - ć: 'c', - ĉ: 'c', - ċ: 'c', - č: 'c', - Ď: 'D', - Đ: 'D', - ď: 'd', - đ: 'd', - Ē: 'E', - Ĕ: 'E', - Ė: 'E', - Ę: 'E', - Ě: 'E', - ē: 'e', - ĕ: 'e', - ė: 'e', - ę: 'e', - ě: 'e', - Ĝ: 'G', - Ğ: 'G', - Ġ: 'G', - Ģ: 'G', - ĝ: 'g', - ğ: 'g', - ġ: 'g', - ģ: 'g', - Ĥ: 'H', - Ħ: 'H', - ĥ: 'h', - ħ: 'h', - Ĩ: 'I', - Ī: 'I', - Ĭ: 'I', - Į: 'I', - İ: 'I', - ĩ: 'i', - ī: 'i', - ĭ: 'i', - į: 'i', - ı: 'i', - Ĵ: 'J', - ĵ: 'j', - Ķ: 'K', - ķ: 'k', - ĸ: 'k', - Ĺ: 'L', - Ļ: 'L', - Ľ: 'L', - Ŀ: 'L', - Ł: 'L', - ĺ: 'l', - ļ: 'l', - ľ: 'l', - ŀ: 'l', - ł: 'l', - Ń: 'N', - Ņ: 'N', - Ň: 'N', - Ŋ: 'N', - ń: 'n', - ņ: 'n', - ň: 'n', - ŋ: 'n', - Ō: 'O', - Ŏ: 'O', - Ő: 'O', - ō: 'o', - ŏ: 'o', - ő: 'o', - Ŕ: 'R', - Ŗ: 'R', - Ř: 'R', - ŕ: 'r', - ŗ: 'r', - ř: 'r', - Ś: 'S', - Ŝ: 'S', - Ş: 'S', - Š: 'S', - ś: 's', - ŝ: 's', - ş: 's', - š: 's', - Ţ: 'T', - Ť: 'T', - Ŧ: 'T', - ţ: 't', - ť: 't', - ŧ: 't', - Ũ: 'U', - Ū: 'U', - Ŭ: 'U', - Ů: 'U', - Ű: 'U', - Ų: 'U', - ũ: 'u', - ū: 'u', - ŭ: 'u', - ů: 'u', - ű: 'u', - ų: 'u', - Ŵ: 'W', - ŵ: 'w', - Ŷ: 'Y', - ŷ: 'y', - Ÿ: 'Y', - Ź: 'Z', - Ż: 'Z', - Ž: 'Z', - ź: 'z', - ż: 'z', - ž: 'z', - IJ: 'IJ', - ij: 'ij', - Œ: 'Oe', - œ: 'oe', - ʼn: "'n", - ſ: 's' - }); - s.exports = u; - }, - 93243: (s, o, i) => { - var u = i(56110), - _ = (function () { - try { - var s = u(Object, 'defineProperty'); - return s({}, '', {}), s; - } catch (s) {} - })(); - s.exports = _; - }, - 25911: (s, o, i) => { - var u = i(38859), - _ = i(14248), - w = i(19219); - s.exports = function equalArrays(s, o, i, x, C, j) { - var L = 1 & i, - B = s.length, - $ = o.length; - if (B != $ && !(L && $ > B)) return !1; - var V = j.get(s), - U = j.get(o); - if (V && U) return V == o && U == s; - var z = -1, - Y = !0, - Z = 2 & i ? new u() : void 0; - for (j.set(s, o), j.set(o, s); ++z < B; ) { - var ee = s[z], - ie = o[z]; - if (x) var ae = L ? x(ie, ee, z, o, s, j) : x(ee, ie, z, s, o, j); - if (void 0 !== ae) { - if (ae) continue; - Y = !1; - break; - } - if (Z) { - if ( - !_(o, function (s, o) { - if (!w(Z, o) && (ee === s || C(ee, s, i, x, j))) return Z.push(o); - }) - ) { - Y = !1; - break; - } - } else if (ee !== ie && !C(ee, ie, i, x, j)) { - Y = !1; - break; - } - } - return j.delete(s), j.delete(o), Y; - }; - }, - 21986: (s, o, i) => { - var u = i(51873), - _ = i(37828), - w = i(75288), - x = i(25911), - C = i(20317), - j = i(84247), - L = u ? u.prototype : void 0, - B = L ? L.valueOf : void 0; - s.exports = function equalByTag(s, o, i, u, L, $, V) { - switch (i) { - case '[object DataView]': - if (s.byteLength != o.byteLength || s.byteOffset != o.byteOffset) return !1; - (s = s.buffer), (o = o.buffer); - case '[object ArrayBuffer]': - return !(s.byteLength != o.byteLength || !$(new _(s), new _(o))); - case '[object Boolean]': - case '[object Date]': - case '[object Number]': - return w(+s, +o); - case '[object Error]': - return s.name == o.name && s.message == o.message; - case '[object RegExp]': - case '[object String]': - return s == o + ''; - case '[object Map]': - var U = C; - case '[object Set]': - var z = 1 & u; - if ((U || (U = j), s.size != o.size && !z)) return !1; - var Y = V.get(s); - if (Y) return Y == o; - (u |= 2), V.set(s, o); - var Z = x(U(s), U(o), u, L, $, V); - return V.delete(s), Z; - case '[object Symbol]': - if (B) return B.call(s) == B.call(o); - } - return !1; - }; - }, - 50689: (s, o, i) => { - var u = i(50002), - _ = Object.prototype.hasOwnProperty; - s.exports = function equalObjects(s, o, i, w, x, C) { - var j = 1 & i, - L = u(s), - B = L.length; - if (B != u(o).length && !j) return !1; - for (var $ = B; $--; ) { - var V = L[$]; - if (!(j ? V in o : _.call(o, V))) return !1; - } - var U = C.get(s), - z = C.get(o); - if (U && z) return U == o && z == s; - var Y = !0; - C.set(s, o), C.set(o, s); - for (var Z = j; ++$ < B; ) { - var ee = s[(V = L[$])], - ie = o[V]; - if (w) var ae = j ? w(ie, ee, V, o, s, C) : w(ee, ie, V, s, o, C); - if (!(void 0 === ae ? ee === ie || x(ee, ie, i, w, C) : ae)) { - Y = !1; - break; - } - Z || (Z = 'constructor' == V); - } - if (Y && !Z) { - var le = s.constructor, - ce = o.constructor; - le == ce || - !('constructor' in s) || - !('constructor' in o) || - ('function' == typeof le && - le instanceof le && - 'function' == typeof ce && - ce instanceof ce) || - (Y = !1); - } - return C.delete(s), C.delete(o), Y; - }; - }, - 38816: (s, o, i) => { - var u = i(35970), - _ = i(56757), - w = i(32865); - s.exports = function flatRest(s) { - return w(_(s, void 0, u), s + ''); - }; - }, - 34840: (s, o, i) => { - var u = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g; - s.exports = u; - }, - 50002: (s, o, i) => { - var u = i(82199), - _ = i(4664), - w = i(95950); - s.exports = function getAllKeys(s) { - return u(s, w, _); - }; - }, - 83349: (s, o, i) => { - var u = i(82199), - _ = i(86375), - w = i(37241); - s.exports = function getAllKeysIn(s) { - return u(s, w, _); - }; - }, - 37381: (s, o, i) => { - var u = i(48152), - _ = i(63950), - w = u - ? function (s) { - return u.get(s); - } - : _; - s.exports = w; - }, - 62284: (s, o, i) => { - var u = i(84629), - _ = Object.prototype.hasOwnProperty; - s.exports = function getFuncName(s) { - for (var o = s.name + '', i = u[o], w = _.call(u, o) ? i.length : 0; w--; ) { - var x = i[w], - C = x.func; - if (null == C || C == s) return x.name; - } - return o; - }; - }, - 11287: (s) => { - s.exports = function getHolder(s) { - return s.placeholder; - }; - }, - 12651: (s, o, i) => { - var u = i(74218); - s.exports = function getMapData(s, o) { - var i = s.__data__; - return u(o) ? i['string' == typeof o ? 'string' : 'hash'] : i.map; - }; - }, - 10776: (s, o, i) => { - var u = i(30756), - _ = i(95950); - s.exports = function getMatchData(s) { - for (var o = _(s), i = o.length; i--; ) { - var w = o[i], - x = s[w]; - o[i] = [w, x, u(x)]; - } - return o; - }; - }, - 56110: (s, o, i) => { - var u = i(45083), - _ = i(10392); - s.exports = function getNative(s, o) { - var i = _(s, o); - return u(i) ? i : void 0; - }; - }, - 28879: (s, o, i) => { - var u = i(74335)(Object.getPrototypeOf, Object); - s.exports = u; - }, - 659: (s, o, i) => { - var u = i(51873), - _ = Object.prototype, - w = _.hasOwnProperty, - x = _.toString, - C = u ? u.toStringTag : void 0; - s.exports = function getRawTag(s) { - var o = w.call(s, C), - i = s[C]; - try { - s[C] = void 0; - var u = !0; - } catch (s) {} - var _ = x.call(s); - return u && (o ? (s[C] = i) : delete s[C]), _; - }; - }, - 4664: (s, o, i) => { - var u = i(79770), - _ = i(63345), - w = Object.prototype.propertyIsEnumerable, - x = Object.getOwnPropertySymbols, - C = x - ? function (s) { - return null == s - ? [] - : ((s = Object(s)), - u(x(s), function (o) { - return w.call(s, o); - })); - } - : _; - s.exports = C; - }, - 86375: (s, o, i) => { - var u = i(14528), - _ = i(28879), - w = i(4664), - x = i(63345), - C = Object.getOwnPropertySymbols - ? function (s) { - for (var o = []; s; ) u(o, w(s)), (s = _(s)); - return o; - } - : x; - s.exports = C; - }, - 5861: (s, o, i) => { - var u = i(55580), - _ = i(68223), - w = i(32804), - x = i(76545), - C = i(28303), - j = i(72552), - L = i(47473), - B = '[object Map]', - $ = '[object Promise]', - V = '[object Set]', - U = '[object WeakMap]', - z = '[object DataView]', - Y = L(u), - Z = L(_), - ee = L(w), - ie = L(x), - ae = L(C), - le = j; - ((u && le(new u(new ArrayBuffer(1))) != z) || - (_ && le(new _()) != B) || - (w && le(w.resolve()) != $) || - (x && le(new x()) != V) || - (C && le(new C()) != U)) && - (le = function (s) { - var o = j(s), - i = '[object Object]' == o ? s.constructor : void 0, - u = i ? L(i) : ''; - if (u) - switch (u) { - case Y: - return z; - case Z: - return B; - case ee: - return $; - case ie: - return V; - case ae: - return U; - } - return o; - }), - (s.exports = le); - }, - 10392: (s) => { - s.exports = function getValue(s, o) { - return null == s ? void 0 : s[o]; - }; - }, - 75251: (s) => { - var o = /\{\n\/\* \[wrapped with (.+)\] \*/, - i = /,? & /; - s.exports = function getWrapDetails(s) { - var u = s.match(o); - return u ? u[1].split(i) : []; - }; - }, - 49326: (s, o, i) => { - var u = i(31769), - _ = i(72428), - w = i(56449), - x = i(30361), - C = i(30294), - j = i(77797); - s.exports = function hasPath(s, o, i) { - for (var L = -1, B = (o = u(o, s)).length, $ = !1; ++L < B; ) { - var V = j(o[L]); - if (!($ = null != s && i(s, V))) break; - s = s[V]; - } - return $ || ++L != B - ? $ - : !!(B = null == s ? 0 : s.length) && C(B) && x(V, B) && (w(s) || _(s)); - }; - }, - 49698: (s) => { - var o = RegExp( - '[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]' - ); - s.exports = function hasUnicode(s) { - return o.test(s); - }; - }, - 45434: (s) => { - var o = /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; - s.exports = function hasUnicodeWord(s) { - return o.test(s); - }; - }, - 22032: (s, o, i) => { - var u = i(81042); - s.exports = function hashClear() { - (this.__data__ = u ? u(null) : {}), (this.size = 0); - }; - }, - 63862: (s) => { - s.exports = function hashDelete(s) { - var o = this.has(s) && delete this.__data__[s]; - return (this.size -= o ? 1 : 0), o; - }; - }, - 66721: (s, o, i) => { - var u = i(81042), - _ = Object.prototype.hasOwnProperty; - s.exports = function hashGet(s) { - var o = this.__data__; - if (u) { - var i = o[s]; - return '__lodash_hash_undefined__' === i ? void 0 : i; - } - return _.call(o, s) ? o[s] : void 0; - }; - }, - 12749: (s, o, i) => { - var u = i(81042), - _ = Object.prototype.hasOwnProperty; - s.exports = function hashHas(s) { - var o = this.__data__; - return u ? void 0 !== o[s] : _.call(o, s); - }; - }, - 35749: (s, o, i) => { - var u = i(81042); - s.exports = function hashSet(s, o) { - var i = this.__data__; - return ( - (this.size += this.has(s) ? 0 : 1), - (i[s] = u && void 0 === o ? '__lodash_hash_undefined__' : o), - this - ); - }; - }, - 76189: (s) => { - var o = Object.prototype.hasOwnProperty; - s.exports = function initCloneArray(s) { - var i = s.length, - u = new s.constructor(i); - return ( - i && - 'string' == typeof s[0] && - o.call(s, 'index') && - ((u.index = s.index), (u.input = s.input)), - u - ); - }; - }, - 77199: (s, o, i) => { - var u = i(49653), - _ = i(76169), - w = i(73201), - x = i(93736), - C = i(71961); - s.exports = function initCloneByTag(s, o, i) { - var j = s.constructor; - switch (o) { - case '[object ArrayBuffer]': - return u(s); - case '[object Boolean]': - case '[object Date]': - return new j(+s); - case '[object DataView]': - return _(s, i); - case '[object Float32Array]': - case '[object Float64Array]': - case '[object Int8Array]': - case '[object Int16Array]': - case '[object Int32Array]': - case '[object Uint8Array]': - case '[object Uint8ClampedArray]': - case '[object Uint16Array]': - case '[object Uint32Array]': - return C(s, i); - case '[object Map]': - case '[object Set]': - return new j(); - case '[object Number]': - case '[object String]': - return new j(s); - case '[object RegExp]': - return w(s); - case '[object Symbol]': - return x(s); - } - }; - }, - 35529: (s, o, i) => { - var u = i(39344), - _ = i(28879), - w = i(55527); - s.exports = function initCloneObject(s) { - return 'function' != typeof s.constructor || w(s) ? {} : u(_(s)); - }; - }, - 62060: (s) => { - var o = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/; - s.exports = function insertWrapDetails(s, i) { - var u = i.length; - if (!u) return s; - var _ = u - 1; - return ( - (i[_] = (u > 1 ? '& ' : '') + i[_]), - (i = i.join(u > 2 ? ', ' : ' ')), - s.replace(o, '{\n/* [wrapped with ' + i + '] */\n') - ); - }; - }, - 45891: (s, o, i) => { - var u = i(51873), - _ = i(72428), - w = i(56449), - x = u ? u.isConcatSpreadable : void 0; - s.exports = function isFlattenable(s) { - return w(s) || _(s) || !!(x && s && s[x]); - }; - }, - 30361: (s) => { - var o = /^(?:0|[1-9]\d*)$/; - s.exports = function isIndex(s, i) { - var u = typeof s; - return ( - !!(i = null == i ? 9007199254740991 : i) && - ('number' == u || ('symbol' != u && o.test(s))) && - s > -1 && - s % 1 == 0 && - s < i - ); - }; - }, - 36800: (s, o, i) => { - var u = i(75288), - _ = i(64894), - w = i(30361), - x = i(23805); - s.exports = function isIterateeCall(s, o, i) { - if (!x(i)) return !1; - var C = typeof o; - return ( - !!('number' == C ? _(i) && w(o, i.length) : 'string' == C && o in i) && u(i[o], s) - ); - }; - }, - 28586: (s, o, i) => { - var u = i(56449), - _ = i(44394), - w = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, - x = /^\w*$/; - s.exports = function isKey(s, o) { - if (u(s)) return !1; - var i = typeof s; - return ( - !('number' != i && 'symbol' != i && 'boolean' != i && null != s && !_(s)) || - x.test(s) || - !w.test(s) || - (null != o && s in Object(o)) - ); - }; - }, - 74218: (s) => { - s.exports = function isKeyable(s) { - var o = typeof s; - return 'string' == o || 'number' == o || 'symbol' == o || 'boolean' == o - ? '__proto__' !== s - : null === s; - }; - }, - 85087: (s, o, i) => { - var u = i(30980), - _ = i(37381), - w = i(62284), - x = i(53758); - s.exports = function isLaziable(s) { - var o = w(s), - i = x[o]; - if ('function' != typeof i || !(o in u.prototype)) return !1; - if (s === i) return !0; - var C = _(i); - return !!C && s === C[0]; - }; - }, - 87296: (s, o, i) => { - var u, - _ = i(55481), - w = (u = /[^.]+$/.exec((_ && _.keys && _.keys.IE_PROTO) || '')) - ? 'Symbol(src)_1.' + u - : ''; - s.exports = function isMasked(s) { - return !!w && w in s; - }; - }, - 55527: (s) => { - var o = Object.prototype; - s.exports = function isPrototype(s) { - var i = s && s.constructor; - return s === (('function' == typeof i && i.prototype) || o); - }; - }, - 30756: (s, o, i) => { - var u = i(23805); - s.exports = function isStrictComparable(s) { - return s == s && !u(s); - }; - }, - 63702: (s) => { - s.exports = function listCacheClear() { - (this.__data__ = []), (this.size = 0); - }; - }, - 70080: (s, o, i) => { - var u = i(26025), - _ = Array.prototype.splice; - s.exports = function listCacheDelete(s) { - var o = this.__data__, - i = u(o, s); - return !(i < 0) && (i == o.length - 1 ? o.pop() : _.call(o, i, 1), --this.size, !0); - }; - }, - 24739: (s, o, i) => { - var u = i(26025); - s.exports = function listCacheGet(s) { - var o = this.__data__, - i = u(o, s); - return i < 0 ? void 0 : o[i][1]; - }; - }, - 48655: (s, o, i) => { - var u = i(26025); - s.exports = function listCacheHas(s) { - return u(this.__data__, s) > -1; - }; - }, - 31175: (s, o, i) => { - var u = i(26025); - s.exports = function listCacheSet(s, o) { - var i = this.__data__, - _ = u(i, s); - return _ < 0 ? (++this.size, i.push([s, o])) : (i[_][1] = o), this; - }; - }, - 63040: (s, o, i) => { - var u = i(21549), - _ = i(80079), - w = i(68223); - s.exports = function mapCacheClear() { - (this.size = 0), - (this.__data__ = { hash: new u(), map: new (w || _)(), string: new u() }); - }; - }, - 17670: (s, o, i) => { - var u = i(12651); - s.exports = function mapCacheDelete(s) { - var o = u(this, s).delete(s); - return (this.size -= o ? 1 : 0), o; - }; - }, - 90289: (s, o, i) => { - var u = i(12651); - s.exports = function mapCacheGet(s) { - return u(this, s).get(s); - }; - }, - 4509: (s, o, i) => { - var u = i(12651); - s.exports = function mapCacheHas(s) { - return u(this, s).has(s); - }; - }, - 72949: (s, o, i) => { - var u = i(12651); - s.exports = function mapCacheSet(s, o) { - var i = u(this, s), - _ = i.size; - return i.set(s, o), (this.size += i.size == _ ? 0 : 1), this; - }; - }, - 20317: (s) => { - s.exports = function mapToArray(s) { - var o = -1, - i = Array(s.size); - return ( - s.forEach(function (s, u) { - i[++o] = [u, s]; - }), - i - ); - }; - }, - 67197: (s) => { - s.exports = function matchesStrictComparable(s, o) { - return function (i) { - return null != i && i[s] === o && (void 0 !== o || s in Object(i)); - }; - }; - }, - 62224: (s, o, i) => { - var u = i(50104); - s.exports = function memoizeCapped(s) { - var o = u(s, function (s) { - return 500 === i.size && i.clear(), s; - }), - i = o.cache; - return o; - }; - }, - 3209: (s, o, i) => { - var u = i(91596), - _ = i(53320), - w = i(36306), - x = '__lodash_placeholder__', - C = 128, - j = Math.min; - s.exports = function mergeData(s, o) { - var i = s[1], - L = o[1], - B = i | L, - $ = B < 131, - V = - (L == C && 8 == i) || - (L == C && 256 == i && s[7].length <= o[8]) || - (384 == L && o[7].length <= o[8] && 8 == i); - if (!$ && !V) return s; - 1 & L && ((s[2] = o[2]), (B |= 1 & i ? 0 : 4)); - var U = o[3]; - if (U) { - var z = s[3]; - (s[3] = z ? u(z, U, o[4]) : U), (s[4] = z ? w(s[3], x) : o[4]); - } - return ( - (U = o[5]) && - ((z = s[5]), (s[5] = z ? _(z, U, o[6]) : U), (s[6] = z ? w(s[5], x) : o[6])), - (U = o[7]) && (s[7] = U), - L & C && (s[8] = null == s[8] ? o[8] : j(s[8], o[8])), - null == s[9] && (s[9] = o[9]), - (s[0] = o[0]), - (s[1] = B), - s - ); - }; - }, - 48152: (s, o, i) => { - var u = i(28303), - _ = u && new u(); - s.exports = _; - }, - 81042: (s, o, i) => { - var u = i(56110)(Object, 'create'); - s.exports = u; - }, - 3650: (s, o, i) => { - var u = i(74335)(Object.keys, Object); - s.exports = u; - }, - 90181: (s) => { - s.exports = function nativeKeysIn(s) { - var o = []; - if (null != s) for (var i in Object(s)) o.push(i); - return o; - }; - }, - 86009: (s, o, i) => { - s = i.nmd(s); - var u = i(34840), - _ = o && !o.nodeType && o, - w = _ && s && !s.nodeType && s, - x = w && w.exports === _ && u.process, - C = (function () { - try { - var s = w && w.require && w.require('util').types; - return s || (x && x.binding && x.binding('util')); - } catch (s) {} - })(); - s.exports = C; - }, - 59350: (s) => { - var o = Object.prototype.toString; - s.exports = function objectToString(s) { - return o.call(s); - }; - }, - 74335: (s) => { - s.exports = function overArg(s, o) { - return function (i) { - return s(o(i)); - }; - }; - }, - 56757: (s, o, i) => { - var u = i(91033), - _ = Math.max; - s.exports = function overRest(s, o, i) { - return ( - (o = _(void 0 === o ? s.length - 1 : o, 0)), - function () { - for (var w = arguments, x = -1, C = _(w.length - o, 0), j = Array(C); ++x < C; ) - j[x] = w[o + x]; - x = -1; - for (var L = Array(o + 1); ++x < o; ) L[x] = w[x]; - return (L[o] = i(j)), u(s, this, L); - } - ); - }; - }, - 68969: (s, o, i) => { - var u = i(47422), - _ = i(25160); - s.exports = function parent(s, o) { - return o.length < 2 ? s : u(s, _(o, 0, -1)); - }; - }, - 84629: (s) => { - s.exports = {}; - }, - 68294: (s, o, i) => { - var u = i(23007), - _ = i(30361), - w = Math.min; - s.exports = function reorder(s, o) { - for (var i = s.length, x = w(o.length, i), C = u(s); x--; ) { - var j = o[x]; - s[x] = _(j, i) ? C[j] : void 0; - } - return s; - }; - }, - 36306: (s) => { - var o = '__lodash_placeholder__'; - s.exports = function replaceHolders(s, i) { - for (var u = -1, _ = s.length, w = 0, x = []; ++u < _; ) { - var C = s[u]; - (C !== i && C !== o) || ((s[u] = o), (x[w++] = u)); - } - return x; - }; - }, - 9325: (s, o, i) => { - var u = i(34840), - _ = 'object' == typeof self && self && self.Object === Object && self, - w = u || _ || Function('return this')(); - s.exports = w; - }, - 14974: (s) => { - s.exports = function safeGet(s, o) { - if (('constructor' !== o || 'function' != typeof s[o]) && '__proto__' != o) return s[o]; - }; - }, - 31380: (s) => { - s.exports = function setCacheAdd(s) { - return this.__data__.set(s, '__lodash_hash_undefined__'), this; - }; - }, - 51459: (s) => { - s.exports = function setCacheHas(s) { - return this.__data__.has(s); - }; - }, - 54641: (s, o, i) => { - var u = i(68882), - _ = i(51811)(u); - s.exports = _; - }, - 84247: (s) => { - s.exports = function setToArray(s) { - var o = -1, - i = Array(s.size); - return ( - s.forEach(function (s) { - i[++o] = s; - }), - i - ); - }; - }, - 32865: (s, o, i) => { - var u = i(19570), - _ = i(51811)(u); - s.exports = _; - }, - 70981: (s, o, i) => { - var u = i(75251), - _ = i(62060), - w = i(32865), - x = i(75948); - s.exports = function setWrapToString(s, o, i) { - var C = o + ''; - return w(s, _(C, x(u(C), i))); - }; - }, - 51811: (s) => { - var o = Date.now; - s.exports = function shortOut(s) { - var i = 0, - u = 0; - return function () { - var _ = o(), - w = 16 - (_ - u); - if (((u = _), w > 0)) { - if (++i >= 800) return arguments[0]; - } else i = 0; - return s.apply(void 0, arguments); - }; - }; - }, - 51420: (s, o, i) => { - var u = i(80079); - s.exports = function stackClear() { - (this.__data__ = new u()), (this.size = 0); - }; - }, - 90938: (s) => { - s.exports = function stackDelete(s) { - var o = this.__data__, - i = o.delete(s); - return (this.size = o.size), i; - }; - }, - 63605: (s) => { - s.exports = function stackGet(s) { - return this.__data__.get(s); - }; - }, - 29817: (s) => { - s.exports = function stackHas(s) { - return this.__data__.has(s); - }; - }, - 80945: (s, o, i) => { - var u = i(80079), - _ = i(68223), - w = i(53661); - s.exports = function stackSet(s, o) { - var i = this.__data__; - if (i instanceof u) { - var x = i.__data__; - if (!_ || x.length < 199) return x.push([s, o]), (this.size = ++i.size), this; - i = this.__data__ = new w(x); - } - return i.set(s, o), (this.size = i.size), this; - }; - }, - 76959: (s) => { - s.exports = function strictIndexOf(s, o, i) { - for (var u = i - 1, _ = s.length; ++u < _; ) if (s[u] === o) return u; - return -1; - }; - }, - 63912: (s, o, i) => { - var u = i(61074), - _ = i(49698), - w = i(42054); - s.exports = function stringToArray(s) { - return _(s) ? w(s) : u(s); - }; - }, - 61802: (s, o, i) => { - var u = i(62224), - _ = - /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g, - w = /\\(\\)?/g, - x = u(function (s) { - var o = []; - return ( - 46 === s.charCodeAt(0) && o.push(''), - s.replace(_, function (s, i, u, _) { - o.push(u ? _.replace(w, '$1') : i || s); - }), - o - ); - }); - s.exports = x; - }, - 77797: (s, o, i) => { - var u = i(44394); - s.exports = function toKey(s) { - if ('string' == typeof s || u(s)) return s; - var o = s + ''; - return '0' == o && 1 / s == -1 / 0 ? '-0' : o; - }; - }, - 47473: (s) => { - var o = Function.prototype.toString; - s.exports = function toSource(s) { - if (null != s) { - try { - return o.call(s); - } catch (s) {} - try { - return s + ''; - } catch (s) {} - } - return ''; - }; - }, - 31800: (s) => { - var o = /\s/; - s.exports = function trimmedEndIndex(s) { - for (var i = s.length; i-- && o.test(s.charAt(i)); ); - return i; - }; - }, - 42054: (s) => { - var o = '\\ud800-\\udfff', - i = '[' + o + ']', - u = '[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]', - _ = '\\ud83c[\\udffb-\\udfff]', - w = '[^' + o + ']', - x = '(?:\\ud83c[\\udde6-\\uddff]){2}', - C = '[\\ud800-\\udbff][\\udc00-\\udfff]', - j = '(?:' + u + '|' + _ + ')' + '?', - L = '[\\ufe0e\\ufe0f]?', - B = L + j + ('(?:\\u200d(?:' + [w, x, C].join('|') + ')' + L + j + ')*'), - $ = '(?:' + [w + u + '?', u, x, C, i].join('|') + ')', - V = RegExp(_ + '(?=' + _ + ')|' + $ + B, 'g'); - s.exports = function unicodeToArray(s) { - return s.match(V) || []; - }; - }, - 22225: (s) => { - var o = '\\ud800-\\udfff', - i = '\\u2700-\\u27bf', - u = 'a-z\\xdf-\\xf6\\xf8-\\xff', - _ = 'A-Z\\xc0-\\xd6\\xd8-\\xde', - w = - '\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', - x = '[' + w + ']', - C = '\\d+', - j = '[' + i + ']', - L = '[' + u + ']', - B = '[^' + o + w + C + i + u + _ + ']', - $ = '(?:\\ud83c[\\udde6-\\uddff]){2}', - V = '[\\ud800-\\udbff][\\udc00-\\udfff]', - U = '[' + _ + ']', - z = '(?:' + L + '|' + B + ')', - Y = '(?:' + U + '|' + B + ')', - Z = "(?:['’](?:d|ll|m|re|s|t|ve))?", - ee = "(?:['’](?:D|LL|M|RE|S|T|VE))?", - ie = '(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?', - ae = '[\\ufe0e\\ufe0f]?', - le = - ae + ie + ('(?:\\u200d(?:' + ['[^' + o + ']', $, V].join('|') + ')' + ae + ie + ')*'), - ce = '(?:' + [j, $, V].join('|') + ')' + le, - pe = RegExp( - [ - U + '?' + L + '+' + Z + '(?=' + [x, U, '$'].join('|') + ')', - Y + '+' + ee + '(?=' + [x, U + z, '$'].join('|') + ')', - U + '?' + z + '+' + Z, - U + '+' + ee, - '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])', - '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])', - C, - ce - ].join('|'), - 'g' - ); - s.exports = function unicodeWords(s) { - return s.match(pe) || []; - }; - }, - 75948: (s, o, i) => { - var u = i(83729), - _ = i(15325), - w = [ - ['ary', 128], - ['bind', 1], - ['bindKey', 2], - ['curry', 8], - ['curryRight', 16], - ['flip', 512], - ['partial', 32], - ['partialRight', 64], - ['rearg', 256] - ]; - s.exports = function updateWrapDetails(s, o) { - return ( - u(w, function (i) { - var u = '_.' + i[0]; - o & i[1] && !_(s, u) && s.push(u); - }), - s.sort() - ); - }; - }, - 80257: (s, o, i) => { - var u = i(30980), - _ = i(56017), - w = i(23007); - s.exports = function wrapperClone(s) { - if (s instanceof u) return s.clone(); - var o = new _(s.__wrapped__, s.__chain__); - return ( - (o.__actions__ = w(s.__actions__)), - (o.__index__ = s.__index__), - (o.__values__ = s.__values__), - o - ); - }; - }, - 64626: (s, o, i) => { - var u = i(66977); - s.exports = function ary(s, o, i) { - return ( - (o = i ? void 0 : o), - (o = s && null == o ? s.length : o), - u(s, 128, void 0, void 0, void 0, void 0, o) - ); - }; - }, - 84058: (s, o, i) => { - var u = i(14792), - _ = i(45539)(function (s, o, i) { - return (o = o.toLowerCase()), s + (i ? u(o) : o); - }); - s.exports = _; - }, - 14792: (s, o, i) => { - var u = i(13222), - _ = i(55808); - s.exports = function capitalize(s) { - return _(u(s).toLowerCase()); - }; - }, - 32629: (s, o, i) => { - var u = i(9999); - s.exports = function clone(s) { - return u(s, 4); - }; - }, - 37334: (s) => { - s.exports = function constant(s) { - return function () { - return s; - }; - }; - }, - 49747: (s, o, i) => { - var u = i(66977); - function curry(s, o, i) { - var _ = u(s, 8, void 0, void 0, void 0, void 0, void 0, (o = i ? void 0 : o)); - return (_.placeholder = curry.placeholder), _; - } - (curry.placeholder = {}), (s.exports = curry); - }, - 38221: (s, o, i) => { - var u = i(23805), - _ = i(10124), - w = i(99374), - x = Math.max, - C = Math.min; - s.exports = function debounce(s, o, i) { - var j, - L, - B, - $, - V, - U, - z = 0, - Y = !1, - Z = !1, - ee = !0; - if ('function' != typeof s) throw new TypeError('Expected a function'); - function invokeFunc(o) { - var i = j, - u = L; - return (j = L = void 0), (z = o), ($ = s.apply(u, i)); - } - function shouldInvoke(s) { - var i = s - U; - return void 0 === U || i >= o || i < 0 || (Z && s - z >= B); - } - function timerExpired() { - var s = _(); - if (shouldInvoke(s)) return trailingEdge(s); - V = setTimeout( - timerExpired, - (function remainingWait(s) { - var i = o - (s - U); - return Z ? C(i, B - (s - z)) : i; - })(s) - ); - } - function trailingEdge(s) { - return (V = void 0), ee && j ? invokeFunc(s) : ((j = L = void 0), $); - } - function debounced() { - var s = _(), - i = shouldInvoke(s); - if (((j = arguments), (L = this), (U = s), i)) { - if (void 0 === V) - return (function leadingEdge(s) { - return (z = s), (V = setTimeout(timerExpired, o)), Y ? invokeFunc(s) : $; - })(U); - if (Z) return clearTimeout(V), (V = setTimeout(timerExpired, o)), invokeFunc(U); - } - return void 0 === V && (V = setTimeout(timerExpired, o)), $; - } - return ( - (o = w(o) || 0), - u(i) && - ((Y = !!i.leading), - (B = (Z = 'maxWait' in i) ? x(w(i.maxWait) || 0, o) : B), - (ee = 'trailing' in i ? !!i.trailing : ee)), - (debounced.cancel = function cancel() { - void 0 !== V && clearTimeout(V), (z = 0), (j = U = L = V = void 0); - }), - (debounced.flush = function flush() { - return void 0 === V ? $ : trailingEdge(_()); - }), - debounced - ); - }; - }, - 50828: (s, o, i) => { - var u = i(24647), - _ = i(13222), - w = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g, - x = RegExp('[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]', 'g'); - s.exports = function deburr(s) { - return (s = _(s)) && s.replace(w, u).replace(x, ''); - }; - }, - 75288: (s) => { - s.exports = function eq(s, o) { - return s === o || (s != s && o != o); - }; - }, - 60680: (s, o, i) => { - var u = i(13222), - _ = /[\\^$.*+?()[\]{}|]/g, - w = RegExp(_.source); - s.exports = function escapeRegExp(s) { - return (s = u(s)) && w.test(s) ? s.replace(_, '\\$&') : s; - }; - }, - 7309: (s, o, i) => { - var u = i(62006)(i(24713)); - s.exports = u; - }, - 24713: (s, o, i) => { - var u = i(2523), - _ = i(15389), - w = i(61489), - x = Math.max; - s.exports = function findIndex(s, o, i) { - var C = null == s ? 0 : s.length; - if (!C) return -1; - var j = null == i ? 0 : w(i); - return j < 0 && (j = x(C + j, 0)), u(s, _(o, 3), j); - }; - }, - 35970: (s, o, i) => { - var u = i(83120); - s.exports = function flatten(s) { - return (null == s ? 0 : s.length) ? u(s, 1) : []; - }; - }, - 73424: (s, o, i) => { - var u = i(16962), - _ = i(2874), - w = Array.prototype.push; - function baseAry(s, o) { - return 2 == o - ? function (o, i) { - return s(o, i); - } - : function (o) { - return s(o); - }; - } - function cloneArray(s) { - for (var o = s ? s.length : 0, i = Array(o); o--; ) i[o] = s[o]; - return i; - } - function wrapImmutable(s, o) { - return function () { - var i = arguments.length; - if (i) { - for (var u = Array(i); i--; ) u[i] = arguments[i]; - var _ = (u[0] = o.apply(void 0, u)); - return s.apply(void 0, u), _; - } - }; - } - s.exports = function baseConvert(s, o, i, x) { - var C = 'function' == typeof o, - j = o === Object(o); - if ((j && ((x = i), (i = o), (o = void 0)), null == i)) throw new TypeError(); - x || (x = {}); - var L = !('cap' in x) || x.cap, - B = !('curry' in x) || x.curry, - $ = !('fixed' in x) || x.fixed, - V = !('immutable' in x) || x.immutable, - U = !('rearg' in x) || x.rearg, - z = C ? i : _, - Y = 'curry' in x && x.curry, - Z = 'fixed' in x && x.fixed, - ee = 'rearg' in x && x.rearg, - ie = C ? i.runInContext() : void 0, - ae = C - ? i - : { - ary: s.ary, - assign: s.assign, - clone: s.clone, - curry: s.curry, - forEach: s.forEach, - isArray: s.isArray, - isError: s.isError, - isFunction: s.isFunction, - isWeakMap: s.isWeakMap, - iteratee: s.iteratee, - keys: s.keys, - rearg: s.rearg, - toInteger: s.toInteger, - toPath: s.toPath - }, - le = ae.ary, - ce = ae.assign, - pe = ae.clone, - de = ae.curry, - fe = ae.forEach, - ye = ae.isArray, - be = ae.isError, - _e = ae.isFunction, - we = ae.isWeakMap, - Se = ae.keys, - xe = ae.rearg, - Pe = ae.toInteger, - Te = ae.toPath, - Re = Se(u.aryMethod), - qe = { - castArray: function (s) { - return function () { - var o = arguments[0]; - return ye(o) ? s(cloneArray(o)) : s.apply(void 0, arguments); - }; - }, - iteratee: function (s) { - return function () { - var o = arguments[1], - i = s(arguments[0], o), - u = i.length; - return L && 'number' == typeof o - ? ((o = o > 2 ? o - 2 : 1), u && u <= o ? i : baseAry(i, o)) - : i; - }; - }, - mixin: function (s) { - return function (o) { - var i = this; - if (!_e(i)) return s(i, Object(o)); - var u = []; - return ( - fe(Se(o), function (s) { - _e(o[s]) && u.push([s, i.prototype[s]]); - }), - s(i, Object(o)), - fe(u, function (s) { - var o = s[1]; - _e(o) ? (i.prototype[s[0]] = o) : delete i.prototype[s[0]]; - }), - i - ); - }; - }, - nthArg: function (s) { - return function (o) { - var i = o < 0 ? 1 : Pe(o) + 1; - return de(s(o), i); - }; - }, - rearg: function (s) { - return function (o, i) { - var u = i ? i.length : 0; - return de(s(o, i), u); - }; - }, - runInContext: function (o) { - return function (i) { - return baseConvert(s, o(i), x); - }; - } - }; - function castCap(s, o) { - if (L) { - var i = u.iterateeRearg[s]; - if (i) - return (function iterateeRearg(s, o) { - return overArg(s, function (s) { - var i = o.length; - return (function baseArity(s, o) { - return 2 == o - ? function (o, i) { - return s.apply(void 0, arguments); - } - : function (o) { - return s.apply(void 0, arguments); - }; - })(xe(baseAry(s, i), o), i); - }); - })(o, i); - var _ = !C && u.iterateeAry[s]; - if (_) - return (function iterateeAry(s, o) { - return overArg(s, function (s) { - return 'function' == typeof s ? baseAry(s, o) : s; - }); - })(o, _); - } - return o; - } - function castFixed(s, o, i) { - if ($ && (Z || !u.skipFixed[s])) { - var _ = u.methodSpread[s], - x = _ && _.start; - return void 0 === x - ? le(o, i) - : (function flatSpread(s, o) { - return function () { - for (var i = arguments.length, u = i - 1, _ = Array(i); i--; ) - _[i] = arguments[i]; - var x = _[o], - C = _.slice(0, o); - return ( - x && w.apply(C, x), o != u && w.apply(C, _.slice(o + 1)), s.apply(this, C) - ); - }; - })(o, x); - } - return o; - } - function castRearg(s, o, i) { - return U && i > 1 && (ee || !u.skipRearg[s]) - ? xe(o, u.methodRearg[s] || u.aryRearg[i]) - : o; - } - function cloneByPath(s, o) { - for ( - var i = -1, u = (o = Te(o)).length, _ = u - 1, w = pe(Object(s)), x = w; - null != x && ++i < u; - - ) { - var C = o[i], - j = x[C]; - null == j || _e(j) || be(j) || we(j) || (x[C] = pe(i == _ ? j : Object(j))), - (x = x[C]); - } - return w; - } - function createConverter(s, o) { - var i = u.aliasToReal[s] || s, - _ = u.remap[i] || i, - w = x; - return function (s) { - var u = C ? ie : ae, - x = C ? ie[_] : o, - j = ce(ce({}, w), s); - return baseConvert(u, i, x, j); - }; - } - function overArg(s, o) { - return function () { - var i = arguments.length; - if (!i) return s(); - for (var u = Array(i); i--; ) u[i] = arguments[i]; - var _ = U ? 0 : i - 1; - return (u[_] = o(u[_])), s.apply(void 0, u); - }; - } - function wrap(s, o, i) { - var _, - w = u.aliasToReal[s] || s, - x = o, - C = qe[w]; - return ( - C - ? (x = C(o)) - : V && - (u.mutate.array[w] - ? (x = wrapImmutable(o, cloneArray)) - : u.mutate.object[w] - ? (x = wrapImmutable( - o, - (function createCloner(s) { - return function (o) { - return s({}, o); - }; - })(o) - )) - : u.mutate.set[w] && (x = wrapImmutable(o, cloneByPath))), - fe(Re, function (s) { - return ( - fe(u.aryMethod[s], function (o) { - if (w == o) { - var i = u.methodSpread[w], - C = i && i.afterRearg; - return ( - (_ = C - ? castFixed(w, castRearg(w, x, s), s) - : castRearg(w, castFixed(w, x, s), s)), - (_ = (function castCurry(s, o, i) { - return Y || (B && i > 1) ? de(o, i) : o; - })(0, (_ = castCap(w, _)), s)), - !1 - ); - } - }), - !_ - ); - }), - _ || (_ = x), - _ == o && - (_ = Y - ? de(_, 1) - : function () { - return o.apply(this, arguments); - }), - (_.convert = createConverter(w, o)), - (_.placeholder = o.placeholder = i), - _ - ); - } - if (!j) return wrap(o, i, z); - var $e = i, - ze = []; - return ( - fe(Re, function (s) { - fe(u.aryMethod[s], function (s) { - var o = $e[u.remap[s] || s]; - o && ze.push([s, wrap(s, o, $e)]); - }); - }), - fe(Se($e), function (s) { - var o = $e[s]; - if ('function' == typeof o) { - for (var i = ze.length; i--; ) if (ze[i][0] == s) return; - (o.convert = createConverter(s, o)), ze.push([s, o]); - } - }), - fe(ze, function (s) { - $e[s[0]] = s[1]; - }), - ($e.convert = function convertLib(s) { - return $e.runInContext.convert(s)(void 0); - }), - ($e.placeholder = $e), - fe(Se($e), function (s) { - fe(u.realToAlias[s] || [], function (o) { - $e[o] = $e[s]; - }); - }), - $e - ); - }; - }, - 16962: (s, o) => { - (o.aliasToReal = { - each: 'forEach', - eachRight: 'forEachRight', - entries: 'toPairs', - entriesIn: 'toPairsIn', - extend: 'assignIn', - extendAll: 'assignInAll', - extendAllWith: 'assignInAllWith', - extendWith: 'assignInWith', - first: 'head', - conforms: 'conformsTo', - matches: 'isMatch', - property: 'get', - __: 'placeholder', - F: 'stubFalse', - T: 'stubTrue', - all: 'every', - allPass: 'overEvery', - always: 'constant', - any: 'some', - anyPass: 'overSome', - apply: 'spread', - assoc: 'set', - assocPath: 'set', - complement: 'negate', - compose: 'flowRight', - contains: 'includes', - dissoc: 'unset', - dissocPath: 'unset', - dropLast: 'dropRight', - dropLastWhile: 'dropRightWhile', - equals: 'isEqual', - identical: 'eq', - indexBy: 'keyBy', - init: 'initial', - invertObj: 'invert', - juxt: 'over', - omitAll: 'omit', - nAry: 'ary', - path: 'get', - pathEq: 'matchesProperty', - pathOr: 'getOr', - paths: 'at', - pickAll: 'pick', - pipe: 'flow', - pluck: 'map', - prop: 'get', - propEq: 'matchesProperty', - propOr: 'getOr', - props: 'at', - symmetricDifference: 'xor', - symmetricDifferenceBy: 'xorBy', - symmetricDifferenceWith: 'xorWith', - takeLast: 'takeRight', - takeLastWhile: 'takeRightWhile', - unapply: 'rest', - unnest: 'flatten', - useWith: 'overArgs', - where: 'conformsTo', - whereEq: 'isMatch', - zipObj: 'zipObject' - }), - (o.aryMethod = { - 1: [ - 'assignAll', - 'assignInAll', - 'attempt', - 'castArray', - 'ceil', - 'create', - 'curry', - 'curryRight', - 'defaultsAll', - 'defaultsDeepAll', - 'floor', - 'flow', - 'flowRight', - 'fromPairs', - 'invert', - 'iteratee', - 'memoize', - 'method', - 'mergeAll', - 'methodOf', - 'mixin', - 'nthArg', - 'over', - 'overEvery', - 'overSome', - 'rest', - 'reverse', - 'round', - 'runInContext', - 'spread', - 'template', - 'trim', - 'trimEnd', - 'trimStart', - 'uniqueId', - 'words', - 'zipAll' - ], - 2: [ - 'add', - 'after', - 'ary', - 'assign', - 'assignAllWith', - 'assignIn', - 'assignInAllWith', - 'at', - 'before', - 'bind', - 'bindAll', - 'bindKey', - 'chunk', - 'cloneDeepWith', - 'cloneWith', - 'concat', - 'conformsTo', - 'countBy', - 'curryN', - 'curryRightN', - 'debounce', - 'defaults', - 'defaultsDeep', - 'defaultTo', - 'delay', - 'difference', - 'divide', - 'drop', - 'dropRight', - 'dropRightWhile', - 'dropWhile', - 'endsWith', - 'eq', - 'every', - 'filter', - 'find', - 'findIndex', - 'findKey', - 'findLast', - 'findLastIndex', - 'findLastKey', - 'flatMap', - 'flatMapDeep', - 'flattenDepth', - 'forEach', - 'forEachRight', - 'forIn', - 'forInRight', - 'forOwn', - 'forOwnRight', - 'get', - 'groupBy', - 'gt', - 'gte', - 'has', - 'hasIn', - 'includes', - 'indexOf', - 'intersection', - 'invertBy', - 'invoke', - 'invokeMap', - 'isEqual', - 'isMatch', - 'join', - 'keyBy', - 'lastIndexOf', - 'lt', - 'lte', - 'map', - 'mapKeys', - 'mapValues', - 'matchesProperty', - 'maxBy', - 'meanBy', - 'merge', - 'mergeAllWith', - 'minBy', - 'multiply', - 'nth', - 'omit', - 'omitBy', - 'overArgs', - 'pad', - 'padEnd', - 'padStart', - 'parseInt', - 'partial', - 'partialRight', - 'partition', - 'pick', - 'pickBy', - 'propertyOf', - 'pull', - 'pullAll', - 'pullAt', - 'random', - 'range', - 'rangeRight', - 'rearg', - 'reject', - 'remove', - 'repeat', - 'restFrom', - 'result', - 'sampleSize', - 'some', - 'sortBy', - 'sortedIndex', - 'sortedIndexOf', - 'sortedLastIndex', - 'sortedLastIndexOf', - 'sortedUniqBy', - 'split', - 'spreadFrom', - 'startsWith', - 'subtract', - 'sumBy', - 'take', - 'takeRight', - 'takeRightWhile', - 'takeWhile', - 'tap', - 'throttle', - 'thru', - 'times', - 'trimChars', - 'trimCharsEnd', - 'trimCharsStart', - 'truncate', - 'union', - 'uniqBy', - 'uniqWith', - 'unset', - 'unzipWith', - 'without', - 'wrap', - 'xor', - 'zip', - 'zipObject', - 'zipObjectDeep' - ], - 3: [ - 'assignInWith', - 'assignWith', - 'clamp', - 'differenceBy', - 'differenceWith', - 'findFrom', - 'findIndexFrom', - 'findLastFrom', - 'findLastIndexFrom', - 'getOr', - 'includesFrom', - 'indexOfFrom', - 'inRange', - 'intersectionBy', - 'intersectionWith', - 'invokeArgs', - 'invokeArgsMap', - 'isEqualWith', - 'isMatchWith', - 'flatMapDepth', - 'lastIndexOfFrom', - 'mergeWith', - 'orderBy', - 'padChars', - 'padCharsEnd', - 'padCharsStart', - 'pullAllBy', - 'pullAllWith', - 'rangeStep', - 'rangeStepRight', - 'reduce', - 'reduceRight', - 'replace', - 'set', - 'slice', - 'sortedIndexBy', - 'sortedLastIndexBy', - 'transform', - 'unionBy', - 'unionWith', - 'update', - 'xorBy', - 'xorWith', - 'zipWith' - ], - 4: ['fill', 'setWith', 'updateWith'] - }), - (o.aryRearg = { 2: [1, 0], 3: [2, 0, 1], 4: [3, 2, 0, 1] }), - (o.iterateeAry = { - dropRightWhile: 1, - dropWhile: 1, - every: 1, - filter: 1, - find: 1, - findFrom: 1, - findIndex: 1, - findIndexFrom: 1, - findKey: 1, - findLast: 1, - findLastFrom: 1, - findLastIndex: 1, - findLastIndexFrom: 1, - findLastKey: 1, - flatMap: 1, - flatMapDeep: 1, - flatMapDepth: 1, - forEach: 1, - forEachRight: 1, - forIn: 1, - forInRight: 1, - forOwn: 1, - forOwnRight: 1, - map: 1, - mapKeys: 1, - mapValues: 1, - partition: 1, - reduce: 2, - reduceRight: 2, - reject: 1, - remove: 1, - some: 1, - takeRightWhile: 1, - takeWhile: 1, - times: 1, - transform: 2 - }), - (o.iterateeRearg = { mapKeys: [1], reduceRight: [1, 0] }), - (o.methodRearg = { - assignInAllWith: [1, 0], - assignInWith: [1, 2, 0], - assignAllWith: [1, 0], - assignWith: [1, 2, 0], - differenceBy: [1, 2, 0], - differenceWith: [1, 2, 0], - getOr: [2, 1, 0], - intersectionBy: [1, 2, 0], - intersectionWith: [1, 2, 0], - isEqualWith: [1, 2, 0], - isMatchWith: [2, 1, 0], - mergeAllWith: [1, 0], - mergeWith: [1, 2, 0], - padChars: [2, 1, 0], - padCharsEnd: [2, 1, 0], - padCharsStart: [2, 1, 0], - pullAllBy: [2, 1, 0], - pullAllWith: [2, 1, 0], - rangeStep: [1, 2, 0], - rangeStepRight: [1, 2, 0], - setWith: [3, 1, 2, 0], - sortedIndexBy: [2, 1, 0], - sortedLastIndexBy: [2, 1, 0], - unionBy: [1, 2, 0], - unionWith: [1, 2, 0], - updateWith: [3, 1, 2, 0], - xorBy: [1, 2, 0], - xorWith: [1, 2, 0], - zipWith: [1, 2, 0] - }), - (o.methodSpread = { - assignAll: { start: 0 }, - assignAllWith: { start: 0 }, - assignInAll: { start: 0 }, - assignInAllWith: { start: 0 }, - defaultsAll: { start: 0 }, - defaultsDeepAll: { start: 0 }, - invokeArgs: { start: 2 }, - invokeArgsMap: { start: 2 }, - mergeAll: { start: 0 }, - mergeAllWith: { start: 0 }, - partial: { start: 1 }, - partialRight: { start: 1 }, - without: { start: 1 }, - zipAll: { start: 0 } - }), - (o.mutate = { - array: { - fill: !0, - pull: !0, - pullAll: !0, - pullAllBy: !0, - pullAllWith: !0, - pullAt: !0, - remove: !0, - reverse: !0 - }, - object: { - assign: !0, - assignAll: !0, - assignAllWith: !0, - assignIn: !0, - assignInAll: !0, - assignInAllWith: !0, - assignInWith: !0, - assignWith: !0, - defaults: !0, - defaultsAll: !0, - defaultsDeep: !0, - defaultsDeepAll: !0, - merge: !0, - mergeAll: !0, - mergeAllWith: !0, - mergeWith: !0 - }, - set: { set: !0, setWith: !0, unset: !0, update: !0, updateWith: !0 } - }), - (o.realToAlias = (function () { - var s = Object.prototype.hasOwnProperty, - i = o.aliasToReal, - u = {}; - for (var _ in i) { - var w = i[_]; - s.call(u, w) ? u[w].push(_) : (u[w] = [_]); - } - return u; - })()), - (o.remap = { - assignAll: 'assign', - assignAllWith: 'assignWith', - assignInAll: 'assignIn', - assignInAllWith: 'assignInWith', - curryN: 'curry', - curryRightN: 'curryRight', - defaultsAll: 'defaults', - defaultsDeepAll: 'defaultsDeep', - findFrom: 'find', - findIndexFrom: 'findIndex', - findLastFrom: 'findLast', - findLastIndexFrom: 'findLastIndex', - getOr: 'get', - includesFrom: 'includes', - indexOfFrom: 'indexOf', - invokeArgs: 'invoke', - invokeArgsMap: 'invokeMap', - lastIndexOfFrom: 'lastIndexOf', - mergeAll: 'merge', - mergeAllWith: 'mergeWith', - padChars: 'pad', - padCharsEnd: 'padEnd', - padCharsStart: 'padStart', - propertyOf: 'get', - rangeStep: 'range', - rangeStepRight: 'rangeRight', - restFrom: 'rest', - spreadFrom: 'spread', - trimChars: 'trim', - trimCharsEnd: 'trimEnd', - trimCharsStart: 'trimStart', - zipAll: 'zip' - }), - (o.skipFixed = { - castArray: !0, - flow: !0, - flowRight: !0, - iteratee: !0, - mixin: !0, - rearg: !0, - runInContext: !0 - }), - (o.skipRearg = { - add: !0, - assign: !0, - assignIn: !0, - bind: !0, - bindKey: !0, - concat: !0, - difference: !0, - divide: !0, - eq: !0, - gt: !0, - gte: !0, - isEqual: !0, - lt: !0, - lte: !0, - matchesProperty: !0, - merge: !0, - multiply: !0, - overArgs: !0, - partial: !0, - partialRight: !0, - propertyOf: !0, - random: !0, - range: !0, - rangeRight: !0, - subtract: !0, - zip: !0, - zipObject: !0, - zipObjectDeep: !0 - }); - }, - 47934: (s, o, i) => { - s.exports = { - ary: i(64626), - assign: i(74733), - clone: i(32629), - curry: i(49747), - forEach: i(83729), - isArray: i(56449), - isError: i(23546), - isFunction: i(1882), - isWeakMap: i(47886), - iteratee: i(33855), - keys: i(88984), - rearg: i(84195), - toInteger: i(61489), - toPath: i(42072) - }; - }, - 56367: (s, o, i) => { - s.exports = i(77731); - }, - 79920: (s, o, i) => { - var u = i(73424), - _ = i(47934); - s.exports = function convert(s, o, i) { - return u(_, s, o, i); - }; - }, - 2874: (s) => { - s.exports = {}; - }, - 77731: (s, o, i) => { - var u = i(79920)('set', i(63560)); - (u.placeholder = i(2874)), (s.exports = u); - }, - 58156: (s, o, i) => { - var u = i(47422); - s.exports = function get(s, o, i) { - var _ = null == s ? void 0 : u(s, o); - return void 0 === _ ? i : _; - }; - }, - 61448: (s, o, i) => { - var u = i(20426), - _ = i(49326); - s.exports = function has(s, o) { - return null != s && _(s, o, u); - }; - }, - 80631: (s, o, i) => { - var u = i(28077), - _ = i(49326); - s.exports = function hasIn(s, o) { - return null != s && _(s, o, u); - }; - }, - 83488: (s) => { - s.exports = function identity(s) { - return s; - }; - }, - 72428: (s, o, i) => { - var u = i(27534), - _ = i(40346), - w = Object.prototype, - x = w.hasOwnProperty, - C = w.propertyIsEnumerable, - j = u( - (function () { - return arguments; - })() - ) - ? u - : function (s) { - return _(s) && x.call(s, 'callee') && !C.call(s, 'callee'); - }; - s.exports = j; - }, - 56449: (s) => { - var o = Array.isArray; - s.exports = o; - }, - 64894: (s, o, i) => { - var u = i(1882), - _ = i(30294); - s.exports = function isArrayLike(s) { - return null != s && _(s.length) && !u(s); - }; - }, - 83693: (s, o, i) => { - var u = i(64894), - _ = i(40346); - s.exports = function isArrayLikeObject(s) { - return _(s) && u(s); - }; - }, - 53812: (s, o, i) => { - var u = i(72552), - _ = i(40346); - s.exports = function isBoolean(s) { - return !0 === s || !1 === s || (_(s) && '[object Boolean]' == u(s)); - }; - }, - 3656: (s, o, i) => { - s = i.nmd(s); - var u = i(9325), - _ = i(89935), - w = o && !o.nodeType && o, - x = w && s && !s.nodeType && s, - C = x && x.exports === w ? u.Buffer : void 0, - j = (C ? C.isBuffer : void 0) || _; - s.exports = j; - }, - 62193: (s, o, i) => { - var u = i(88984), - _ = i(5861), - w = i(72428), - x = i(56449), - C = i(64894), - j = i(3656), - L = i(55527), - B = i(37167), - $ = Object.prototype.hasOwnProperty; - s.exports = function isEmpty(s) { - if (null == s) return !0; - if ( - C(s) && - (x(s) || - 'string' == typeof s || - 'function' == typeof s.splice || - j(s) || - B(s) || - w(s)) - ) - return !s.length; - var o = _(s); - if ('[object Map]' == o || '[object Set]' == o) return !s.size; - if (L(s)) return !u(s).length; - for (var i in s) if ($.call(s, i)) return !1; - return !0; - }; - }, - 2404: (s, o, i) => { - var u = i(60270); - s.exports = function isEqual(s, o) { - return u(s, o); - }; - }, - 23546: (s, o, i) => { - var u = i(72552), - _ = i(40346), - w = i(11331); - s.exports = function isError(s) { - if (!_(s)) return !1; - var o = u(s); - return ( - '[object Error]' == o || - '[object DOMException]' == o || - ('string' == typeof s.message && 'string' == typeof s.name && !w(s)) - ); - }; - }, - 1882: (s, o, i) => { - var u = i(72552), - _ = i(23805); - s.exports = function isFunction(s) { - if (!_(s)) return !1; - var o = u(s); - return ( - '[object Function]' == o || - '[object GeneratorFunction]' == o || - '[object AsyncFunction]' == o || - '[object Proxy]' == o - ); - }; - }, - 30294: (s) => { - s.exports = function isLength(s) { - return 'number' == typeof s && s > -1 && s % 1 == 0 && s <= 9007199254740991; - }; - }, - 87730: (s, o, i) => { - var u = i(29172), - _ = i(27301), - w = i(86009), - x = w && w.isMap, - C = x ? _(x) : u; - s.exports = C; - }, - 5187: (s) => { - s.exports = function isNull(s) { - return null === s; - }; - }, - 98023: (s, o, i) => { - var u = i(72552), - _ = i(40346); - s.exports = function isNumber(s) { - return 'number' == typeof s || (_(s) && '[object Number]' == u(s)); - }; - }, - 23805: (s) => { - s.exports = function isObject(s) { - var o = typeof s; - return null != s && ('object' == o || 'function' == o); - }; - }, - 40346: (s) => { - s.exports = function isObjectLike(s) { - return null != s && 'object' == typeof s; - }; - }, - 11331: (s, o, i) => { - var u = i(72552), - _ = i(28879), - w = i(40346), - x = Function.prototype, - C = Object.prototype, - j = x.toString, - L = C.hasOwnProperty, - B = j.call(Object); - s.exports = function isPlainObject(s) { - if (!w(s) || '[object Object]' != u(s)) return !1; - var o = _(s); - if (null === o) return !0; - var i = L.call(o, 'constructor') && o.constructor; - return 'function' == typeof i && i instanceof i && j.call(i) == B; - }; - }, - 38440: (s, o, i) => { - var u = i(16038), - _ = i(27301), - w = i(86009), - x = w && w.isSet, - C = x ? _(x) : u; - s.exports = C; - }, - 85015: (s, o, i) => { - var u = i(72552), - _ = i(56449), - w = i(40346); - s.exports = function isString(s) { - return 'string' == typeof s || (!_(s) && w(s) && '[object String]' == u(s)); - }; - }, - 44394: (s, o, i) => { - var u = i(72552), - _ = i(40346); - s.exports = function isSymbol(s) { - return 'symbol' == typeof s || (_(s) && '[object Symbol]' == u(s)); - }; - }, - 37167: (s, o, i) => { - var u = i(4901), - _ = i(27301), - w = i(86009), - x = w && w.isTypedArray, - C = x ? _(x) : u; - s.exports = C; - }, - 47886: (s, o, i) => { - var u = i(5861), - _ = i(40346); - s.exports = function isWeakMap(s) { - return _(s) && '[object WeakMap]' == u(s); - }; - }, - 33855: (s, o, i) => { - var u = i(9999), - _ = i(15389); - s.exports = function iteratee(s) { - return _('function' == typeof s ? s : u(s, 1)); - }; - }, - 95950: (s, o, i) => { - var u = i(70695), - _ = i(88984), - w = i(64894); - s.exports = function keys(s) { - return w(s) ? u(s) : _(s); - }; - }, - 37241: (s, o, i) => { - var u = i(70695), - _ = i(72903), - w = i(64894); - s.exports = function keysIn(s) { - return w(s) ? u(s, !0) : _(s); - }; - }, - 68090: (s) => { - s.exports = function last(s) { - var o = null == s ? 0 : s.length; - return o ? s[o - 1] : void 0; - }; - }, - 50104: (s, o, i) => { - var u = i(53661); - function memoize(s, o) { - if ('function' != typeof s || (null != o && 'function' != typeof o)) - throw new TypeError('Expected a function'); - var memoized = function () { - var i = arguments, - u = o ? o.apply(this, i) : i[0], - _ = memoized.cache; - if (_.has(u)) return _.get(u); - var w = s.apply(this, i); - return (memoized.cache = _.set(u, w) || _), w; - }; - return (memoized.cache = new (memoize.Cache || u)()), memoized; - } - (memoize.Cache = u), (s.exports = memoize); - }, - 55364: (s, o, i) => { - var u = i(85250), - _ = i(20999)(function (s, o, i) { - u(s, o, i); - }); - s.exports = _; - }, - 6048: (s) => { - s.exports = function negate(s) { - if ('function' != typeof s) throw new TypeError('Expected a function'); - return function () { - var o = arguments; - switch (o.length) { - case 0: - return !s.call(this); - case 1: - return !s.call(this, o[0]); - case 2: - return !s.call(this, o[0], o[1]); - case 3: - return !s.call(this, o[0], o[1], o[2]); - } - return !s.apply(this, o); - }; - }; - }, - 63950: (s) => { - s.exports = function noop() {}; - }, - 10124: (s, o, i) => { - var u = i(9325); - s.exports = function () { - return u.Date.now(); - }; - }, - 90179: (s, o, i) => { - var u = i(34932), - _ = i(9999), - w = i(19931), - x = i(31769), - C = i(21791), - j = i(53138), - L = i(38816), - B = i(83349), - $ = L(function (s, o) { - var i = {}; - if (null == s) return i; - var L = !1; - (o = u(o, function (o) { - return (o = x(o, s)), L || (L = o.length > 1), o; - })), - C(s, B(s), i), - L && (i = _(i, 7, j)); - for (var $ = o.length; $--; ) w(i, o[$]); - return i; - }); - s.exports = $; - }, - 50583: (s, o, i) => { - var u = i(47237), - _ = i(17255), - w = i(28586), - x = i(77797); - s.exports = function property(s) { - return w(s) ? u(x(s)) : _(s); - }; - }, - 84195: (s, o, i) => { - var u = i(66977), - _ = i(38816), - w = _(function (s, o) { - return u(s, 256, void 0, void 0, void 0, o); - }); - s.exports = w; - }, - 40860: (s, o, i) => { - var u = i(40882), - _ = i(80909), - w = i(15389), - x = i(85558), - C = i(56449); - s.exports = function reduce(s, o, i) { - var j = C(s) ? u : x, - L = arguments.length < 3; - return j(s, w(o, 4), i, L, _); - }; - }, - 63560: (s, o, i) => { - var u = i(73170); - s.exports = function set(s, o, i) { - return null == s ? s : u(s, o, i); - }; - }, - 42426: (s, o, i) => { - var u = i(14248), - _ = i(15389), - w = i(90916), - x = i(56449), - C = i(36800); - s.exports = function some(s, o, i) { - var j = x(s) ? u : w; - return i && C(s, o, i) && (o = void 0), j(s, _(o, 3)); - }; - }, - 63345: (s) => { - s.exports = function stubArray() { - return []; - }; - }, - 89935: (s) => { - s.exports = function stubFalse() { - return !1; - }; - }, - 17400: (s, o, i) => { - var u = i(99374), - _ = 1 / 0; - s.exports = function toFinite(s) { - return s - ? (s = u(s)) === _ || s === -1 / 0 - ? 17976931348623157e292 * (s < 0 ? -1 : 1) - : s == s - ? s - : 0 - : 0 === s - ? s - : 0; - }; - }, - 61489: (s, o, i) => { - var u = i(17400); - s.exports = function toInteger(s) { - var o = u(s), - i = o % 1; - return o == o ? (i ? o - i : o) : 0; - }; - }, - 80218: (s, o, i) => { - var u = i(13222); - s.exports = function toLower(s) { - return u(s).toLowerCase(); - }; - }, - 99374: (s, o, i) => { - var u = i(54128), - _ = i(23805), - w = i(44394), - x = /^[-+]0x[0-9a-f]+$/i, - C = /^0b[01]+$/i, - j = /^0o[0-7]+$/i, - L = parseInt; - s.exports = function toNumber(s) { - if ('number' == typeof s) return s; - if (w(s)) return NaN; - if (_(s)) { - var o = 'function' == typeof s.valueOf ? s.valueOf() : s; - s = _(o) ? o + '' : o; - } - if ('string' != typeof s) return 0 === s ? s : +s; - s = u(s); - var i = C.test(s); - return i || j.test(s) ? L(s.slice(2), i ? 2 : 8) : x.test(s) ? NaN : +s; - }; - }, - 42072: (s, o, i) => { - var u = i(34932), - _ = i(23007), - w = i(56449), - x = i(44394), - C = i(61802), - j = i(77797), - L = i(13222); - s.exports = function toPath(s) { - return w(s) ? u(s, j) : x(s) ? [s] : _(C(L(s))); - }; - }, - 69884: (s, o, i) => { - var u = i(21791), - _ = i(37241); - s.exports = function toPlainObject(s) { - return u(s, _(s)); - }; - }, - 13222: (s, o, i) => { - var u = i(77556); - s.exports = function toString(s) { - return null == s ? '' : u(s); - }; - }, - 55808: (s, o, i) => { - var u = i(12507)('toUpperCase'); - s.exports = u; - }, - 66645: (s, o, i) => { - var u = i(1733), - _ = i(45434), - w = i(13222), - x = i(22225); - s.exports = function words(s, o, i) { - return ( - (s = w(s)), void 0 === (o = i ? void 0 : o) ? (_(s) ? x(s) : u(s)) : s.match(o) || [] - ); - }; - }, - 53758: (s, o, i) => { - var u = i(30980), - _ = i(56017), - w = i(94033), - x = i(56449), - C = i(40346), - j = i(80257), - L = Object.prototype.hasOwnProperty; - function lodash(s) { - if (C(s) && !x(s) && !(s instanceof u)) { - if (s instanceof _) return s; - if (L.call(s, '__wrapped__')) return j(s); - } - return new _(s); - } - (lodash.prototype = w.prototype), - (lodash.prototype.constructor = lodash), - (s.exports = lodash); - }, - 47248: (s, o, i) => { - var u = i(16547), - _ = i(51234); - s.exports = function zipObject(s, o) { - return _(s || [], o || [], u); - }; - }, - 43768: (s, o, i) => { - 'use strict'; - var u = i(45981), - _ = i(85587); - (o.highlight = highlight), - (o.highlightAuto = function highlightAuto(s, o) { - var i, - x, - C, - j, - L = o || {}, - B = L.subset || u.listLanguages(), - $ = L.prefix, - V = B.length, - U = -1; - null == $ && ($ = w); - if ('string' != typeof s) throw _('Expected `string` for value, got `%s`', s); - (x = { relevance: 0, language: null, value: [] }), - (i = { relevance: 0, language: null, value: [] }); - for (; ++U < V; ) - (j = B[U]), - u.getLanguage(j) && - (((C = highlight(j, s, o)).language = j), - C.relevance > x.relevance && (x = C), - C.relevance > i.relevance && ((x = i), (i = C))); - x.language && (i.secondBest = x); - return i; - }), - (o.registerLanguage = function registerLanguage(s, o) { - u.registerLanguage(s, o); - }), - (o.listLanguages = function listLanguages() { - return u.listLanguages(); - }), - (o.registerAlias = function registerAlias(s, o) { - var i, - _ = s; - o && ((_ = {})[s] = o); - for (i in _) u.registerAliases(_[i], { languageName: i }); - }), - (Emitter.prototype.addText = function text(s) { - var o, - i, - u = this.stack; - if ('' === s) return; - (o = u[u.length - 1]), - (i = o.children[o.children.length - 1]) && 'text' === i.type - ? (i.value += s) - : o.children.push({ type: 'text', value: s }); - }), - (Emitter.prototype.addKeyword = function addKeyword(s, o) { - this.openNode(o), this.addText(s), this.closeNode(); - }), - (Emitter.prototype.addSublanguage = function addSublanguage(s, o) { - var i = this.stack, - u = i[i.length - 1], - _ = s.rootNode.children, - w = o - ? { - type: 'element', - tagName: 'span', - properties: { className: [o] }, - children: _ - } - : _; - u.children = u.children.concat(w); - }), - (Emitter.prototype.openNode = function open(s) { - var o = this.stack, - i = this.options.classPrefix + s, - u = o[o.length - 1], - _ = { - type: 'element', - tagName: 'span', - properties: { className: [i] }, - children: [] - }; - u.children.push(_), o.push(_); - }), - (Emitter.prototype.closeNode = function close() { - this.stack.pop(); - }), - (Emitter.prototype.closeAllNodes = noop), - (Emitter.prototype.finalize = noop), - (Emitter.prototype.toHTML = function toHtmlNoop() { - return ''; - }); - var w = 'hljs-'; - function highlight(s, o, i) { - var x, - C = u.configure({}), - j = (i || {}).prefix; - if ('string' != typeof s) throw _('Expected `string` for name, got `%s`', s); - if (!u.getLanguage(s)) throw _('Unknown language: `%s` is not registered', s); - if ('string' != typeof o) throw _('Expected `string` for value, got `%s`', o); - if ( - (null == j && (j = w), - u.configure({ __emitter: Emitter, classPrefix: j }), - (x = u.highlight(o, { language: s, ignoreIllegals: !0 })), - u.configure(C || {}), - x.errorRaised) - ) - throw x.errorRaised; - return { - relevance: x.relevance, - language: x.language, - value: x.emitter.rootNode.children - }; - } - function Emitter(s) { - (this.options = s), (this.rootNode = { children: [] }), (this.stack = [this.rootNode]); - } - function noop() {} - }, - 92340: (s, o, i) => { - const u = i(6048); - function coerceElementMatchingCallback(s) { - return 'string' == typeof s - ? (o) => o.element === s - : s.constructor && s.extend - ? (o) => o instanceof s - : s; - } - class ArraySlice { - constructor(s) { - this.elements = s || []; - } - toValue() { - return this.elements.map((s) => s.toValue()); - } - map(s, o) { - return this.elements.map(s, o); - } - flatMap(s, o) { - return this.map(s, o).reduce((s, o) => s.concat(o), []); - } - compactMap(s, o) { - const i = []; - return ( - this.forEach((u) => { - const _ = s.bind(o)(u); - _ && i.push(_); - }), - i - ); - } - filter(s, o) { - return ( - (s = coerceElementMatchingCallback(s)), new ArraySlice(this.elements.filter(s, o)) - ); - } - reject(s, o) { - return ( - (s = coerceElementMatchingCallback(s)), - new ArraySlice(this.elements.filter(u(s), o)) - ); - } - find(s, o) { - return (s = coerceElementMatchingCallback(s)), this.elements.find(s, o); - } - forEach(s, o) { - this.elements.forEach(s, o); - } - reduce(s, o) { - return this.elements.reduce(s, o); - } - includes(s) { - return this.elements.some((o) => o.equals(s)); - } - shift() { - return this.elements.shift(); - } - unshift(s) { - this.elements.unshift(this.refract(s)); - } - push(s) { - return this.elements.push(this.refract(s)), this; - } - add(s) { - this.push(s); - } - get(s) { - return this.elements[s]; - } - getValue(s) { - const o = this.elements[s]; - if (o) return o.toValue(); - } - get length() { - return this.elements.length; - } - get isEmpty() { - return 0 === this.elements.length; - } - get first() { - return this.elements[0]; - } - } - 'undefined' != typeof Symbol && - (ArraySlice.prototype[Symbol.iterator] = function symbol() { - return this.elements[Symbol.iterator](); - }), - (s.exports = ArraySlice); - }, - 55973: (s) => { - class KeyValuePair { - constructor(s, o) { - (this.key = s), (this.value = o); - } - clone() { - const s = new KeyValuePair(); - return ( - this.key && (s.key = this.key.clone()), - this.value && (s.value = this.value.clone()), - s - ); - } - } - s.exports = KeyValuePair; - }, - 3110: (s, o, i) => { - const u = i(5187), - _ = i(85015), - w = i(98023), - x = i(53812), - C = i(23805), - j = i(85105), - L = i(86804); - class Namespace { - constructor(s) { - (this.elementMap = {}), - (this.elementDetection = []), - (this.Element = L.Element), - (this.KeyValuePair = L.KeyValuePair), - (s && s.noDefault) || this.useDefault(), - (this._attributeElementKeys = []), - (this._attributeElementArrayKeys = []); - } - use(s) { - return ( - s.namespace && s.namespace({ base: this }), s.load && s.load({ base: this }), this - ); - } - useDefault() { - return ( - this.register('null', L.NullElement) - .register('string', L.StringElement) - .register('number', L.NumberElement) - .register('boolean', L.BooleanElement) - .register('array', L.ArrayElement) - .register('object', L.ObjectElement) - .register('member', L.MemberElement) - .register('ref', L.RefElement) - .register('link', L.LinkElement), - this.detect(u, L.NullElement, !1) - .detect(_, L.StringElement, !1) - .detect(w, L.NumberElement, !1) - .detect(x, L.BooleanElement, !1) - .detect(Array.isArray, L.ArrayElement, !1) - .detect(C, L.ObjectElement, !1), - this - ); - } - register(s, o) { - return (this._elements = void 0), (this.elementMap[s] = o), this; - } - unregister(s) { - return (this._elements = void 0), delete this.elementMap[s], this; - } - detect(s, o, i) { - return ( - void 0 === i || i - ? this.elementDetection.unshift([s, o]) - : this.elementDetection.push([s, o]), - this - ); - } - toElement(s) { - if (s instanceof this.Element) return s; - let o; - for (let i = 0; i < this.elementDetection.length; i += 1) { - const u = this.elementDetection[i][0], - _ = this.elementDetection[i][1]; - if (u(s)) { - o = new _(s); - break; - } - } - return o; - } - getElementClass(s) { - const o = this.elementMap[s]; - return void 0 === o ? this.Element : o; - } - fromRefract(s) { - return this.serialiser.deserialise(s); - } - toRefract(s) { - return this.serialiser.serialise(s); - } - get elements() { - return ( - void 0 === this._elements && - ((this._elements = { Element: this.Element }), - Object.keys(this.elementMap).forEach((s) => { - const o = s[0].toUpperCase() + s.substr(1); - this._elements[o] = this.elementMap[s]; - })), - this._elements - ); - } - get serialiser() { - return new j(this); - } - } - (j.prototype.Namespace = Namespace), (s.exports = Namespace); - }, - 10866: (s, o, i) => { - const u = i(6048), - _ = i(92340); - class ObjectSlice extends _ { - map(s, o) { - return this.elements.map((i) => s.bind(o)(i.value, i.key, i)); - } - filter(s, o) { - return new ObjectSlice(this.elements.filter((i) => s.bind(o)(i.value, i.key, i))); - } - reject(s, o) { - return this.filter(u(s.bind(o))); - } - forEach(s, o) { - return this.elements.forEach((i, u) => { - s.bind(o)(i.value, i.key, i, u); - }); - } - keys() { - return this.map((s, o) => o.toValue()); - } - values() { - return this.map((s) => s.toValue()); - } - } - s.exports = ObjectSlice; - }, - 86804: (s, o, i) => { - const u = i(10316), - _ = i(41067), - w = i(71167), - x = i(40239), - C = i(12242), - j = i(6233), - L = i(87726), - B = i(61045), - $ = i(86303), - V = i(14540), - U = i(92340), - z = i(10866), - Y = i(55973); - function refract(s) { - if (s instanceof u) return s; - if ('string' == typeof s) return new w(s); - if ('number' == typeof s) return new x(s); - if ('boolean' == typeof s) return new C(s); - if (null === s) return new _(); - if (Array.isArray(s)) return new j(s.map(refract)); - if ('object' == typeof s) { - return new B(s); - } - return s; - } - (u.prototype.ObjectElement = B), - (u.prototype.RefElement = V), - (u.prototype.MemberElement = L), - (u.prototype.refract = refract), - (U.prototype.refract = refract), - (s.exports = { - Element: u, - NullElement: _, - StringElement: w, - NumberElement: x, - BooleanElement: C, - ArrayElement: j, - MemberElement: L, - ObjectElement: B, - LinkElement: $, - RefElement: V, - refract, - ArraySlice: U, - ObjectSlice: z, - KeyValuePair: Y - }); - }, - 86303: (s, o, i) => { - const u = i(10316); - s.exports = class LinkElement extends u { - constructor(s, o, i) { - super(s || [], o, i), (this.element = 'link'); - } - get relation() { - return this.attributes.get('relation'); - } - set relation(s) { - this.attributes.set('relation', s); - } - get href() { - return this.attributes.get('href'); - } - set href(s) { - this.attributes.set('href', s); - } - }; - }, - 14540: (s, o, i) => { - const u = i(10316); - s.exports = class RefElement extends u { - constructor(s, o, i) { - super(s || [], o, i), (this.element = 'ref'), this.path || (this.path = 'element'); - } - get path() { - return this.attributes.get('path'); - } - set path(s) { - this.attributes.set('path', s); - } - }; - }, - 34035: (s, o, i) => { - const u = i(3110), - _ = i(86804); - (o.g$ = u), - (o.KeyValuePair = i(55973)), - (o.G6 = _.ArraySlice), - (o.ot = _.ObjectSlice), - (o.Hg = _.Element), - (o.Om = _.StringElement), - (o.kT = _.NumberElement), - (o.bd = _.BooleanElement), - (o.Os = _.NullElement), - (o.wE = _.ArrayElement), - (o.Sh = _.ObjectElement), - (o.Pr = _.MemberElement), - (o.sI = _.RefElement), - (o.Ft = _.LinkElement), - (o.e = _.refract), - i(85105), - i(75147); - }, - 6233: (s, o, i) => { - const u = i(6048), - _ = i(10316), - w = i(92340); - class ArrayElement extends _ { - constructor(s, o, i) { - super(s || [], o, i), (this.element = 'array'); - } - primitive() { - return 'array'; - } - get(s) { - return this.content[s]; - } - getValue(s) { - const o = this.get(s); - if (o) return o.toValue(); - } - getIndex(s) { - return this.content[s]; - } - set(s, o) { - return (this.content[s] = this.refract(o)), this; - } - remove(s) { - const o = this.content.splice(s, 1); - return o.length ? o[0] : null; - } - map(s, o) { - return this.content.map(s, o); - } - flatMap(s, o) { - return this.map(s, o).reduce((s, o) => s.concat(o), []); - } - compactMap(s, o) { - const i = []; - return ( - this.forEach((u) => { - const _ = s.bind(o)(u); - _ && i.push(_); - }), - i - ); - } - filter(s, o) { - return new w(this.content.filter(s, o)); - } - reject(s, o) { - return this.filter(u(s), o); - } - reduce(s, o) { - let i, u; - void 0 !== o - ? ((i = 0), (u = this.refract(o))) - : ((i = 1), (u = 'object' === this.primitive() ? this.first.value : this.first)); - for (let o = i; o < this.length; o += 1) { - const i = this.content[o]; - u = - 'object' === this.primitive() - ? this.refract(s(u, i.value, i.key, i, this)) - : this.refract(s(u, i, o, this)); - } - return u; - } - forEach(s, o) { - this.content.forEach((i, u) => { - s.bind(o)(i, this.refract(u)); - }); - } - shift() { - return this.content.shift(); - } - unshift(s) { - this.content.unshift(this.refract(s)); - } - push(s) { - return this.content.push(this.refract(s)), this; - } - add(s) { - this.push(s); - } - findElements(s, o) { - const i = o || {}, - u = !!i.recursive, - _ = void 0 === i.results ? [] : i.results; - return ( - this.forEach((o, i, w) => { - u && void 0 !== o.findElements && o.findElements(s, { results: _, recursive: u }), - s(o, i, w) && _.push(o); - }), - _ - ); - } - find(s) { - return new w(this.findElements(s, { recursive: !0 })); - } - findByElement(s) { - return this.find((o) => o.element === s); - } - findByClass(s) { - return this.find((o) => o.classes.includes(s)); - } - getById(s) { - return this.find((o) => o.id.toValue() === s).first; - } - includes(s) { - return this.content.some((o) => o.equals(s)); - } - contains(s) { - return this.includes(s); - } - empty() { - return new this.constructor([]); - } - 'fantasy-land/empty'() { - return this.empty(); - } - concat(s) { - return new this.constructor(this.content.concat(s.content)); - } - 'fantasy-land/concat'(s) { - return this.concat(s); - } - 'fantasy-land/map'(s) { - return new this.constructor(this.map(s)); - } - 'fantasy-land/chain'(s) { - return this.map((o) => s(o), this).reduce((s, o) => s.concat(o), this.empty()); - } - 'fantasy-land/filter'(s) { - return new this.constructor(this.content.filter(s)); - } - 'fantasy-land/reduce'(s, o) { - return this.content.reduce(s, o); - } - get length() { - return this.content.length; - } - get isEmpty() { - return 0 === this.content.length; - } - get first() { - return this.getIndex(0); - } - get second() { - return this.getIndex(1); - } - get last() { - return this.getIndex(this.length - 1); - } - } - (ArrayElement.empty = function empty() { - return new this(); - }), - (ArrayElement['fantasy-land/empty'] = ArrayElement.empty), - 'undefined' != typeof Symbol && - (ArrayElement.prototype[Symbol.iterator] = function symbol() { - return this.content[Symbol.iterator](); - }), - (s.exports = ArrayElement); - }, - 12242: (s, o, i) => { - const u = i(10316); - s.exports = class BooleanElement extends u { - constructor(s, o, i) { - super(s, o, i), (this.element = 'boolean'); - } - primitive() { - return 'boolean'; - } - }; - }, - 10316: (s, o, i) => { - const u = i(2404), - _ = i(55973), - w = i(92340); - class Element { - constructor(s, o, i) { - o && (this.meta = o), i && (this.attributes = i), (this.content = s); - } - freeze() { - Object.isFrozen(this) || - (this._meta && ((this.meta.parent = this), this.meta.freeze()), - this._attributes && ((this.attributes.parent = this), this.attributes.freeze()), - this.children.forEach((s) => { - (s.parent = this), s.freeze(); - }, this), - this.content && Array.isArray(this.content) && Object.freeze(this.content), - Object.freeze(this)); - } - primitive() {} - clone() { - const s = new this.constructor(); - return ( - (s.element = this.element), - this.meta.length && (s._meta = this.meta.clone()), - this.attributes.length && (s._attributes = this.attributes.clone()), - this.content - ? this.content.clone - ? (s.content = this.content.clone()) - : Array.isArray(this.content) - ? (s.content = this.content.map((s) => s.clone())) - : (s.content = this.content) - : (s.content = this.content), - s - ); - } - toValue() { - return this.content instanceof Element - ? this.content.toValue() - : this.content instanceof _ - ? { - key: this.content.key.toValue(), - value: this.content.value ? this.content.value.toValue() : void 0 - } - : this.content && this.content.map - ? this.content.map((s) => s.toValue(), this) - : this.content; - } - toRef(s) { - if ('' === this.id.toValue()) - throw Error('Cannot create reference to an element that does not contain an ID'); - const o = new this.RefElement(this.id.toValue()); - return s && (o.path = s), o; - } - findRecursive(...s) { - if (arguments.length > 1 && !this.isFrozen) - throw new Error( - 'Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`' - ); - const o = s.pop(); - let i = new w(); - const append = (s, o) => (s.push(o), s), - checkElement = (s, i) => { - i.element === o && s.push(i); - const u = i.findRecursive(o); - return ( - u && u.reduce(append, s), - i.content instanceof _ && - (i.content.key && checkElement(s, i.content.key), - i.content.value && checkElement(s, i.content.value)), - s - ); - }; - return ( - this.content && - (this.content.element && checkElement(i, this.content), - Array.isArray(this.content) && this.content.reduce(checkElement, i)), - s.isEmpty || - (i = i.filter((o) => { - let i = o.parents.map((s) => s.element); - for (const o in s) { - const u = s[o], - _ = i.indexOf(u); - if (-1 === _) return !1; - i = i.splice(0, _); - } - return !0; - })), - i - ); - } - set(s) { - return (this.content = s), this; - } - equals(s) { - return u(this.toValue(), s); - } - getMetaProperty(s, o) { - if (!this.meta.hasKey(s)) { - if (this.isFrozen) { - const s = this.refract(o); - return s.freeze(), s; - } - this.meta.set(s, o); - } - return this.meta.get(s); - } - setMetaProperty(s, o) { - this.meta.set(s, o); - } - get element() { - return this._storedElement || 'element'; - } - set element(s) { - this._storedElement = s; - } - get content() { - return this._content; - } - set content(s) { - if (s instanceof Element) this._content = s; - else if (s instanceof w) this.content = s.elements; - else if ( - 'string' == typeof s || - 'number' == typeof s || - 'boolean' == typeof s || - 'null' === s || - null == s - ) - this._content = s; - else if (s instanceof _) this._content = s; - else if (Array.isArray(s)) this._content = s.map(this.refract); - else { - if ('object' != typeof s) throw new Error('Cannot set content to given value'); - this._content = Object.keys(s).map((o) => new this.MemberElement(o, s[o])); - } - } - get meta() { - if (!this._meta) { - if (this.isFrozen) { - const s = new this.ObjectElement(); - return s.freeze(), s; - } - this._meta = new this.ObjectElement(); - } - return this._meta; - } - set meta(s) { - s instanceof this.ObjectElement ? (this._meta = s) : this.meta.set(s || {}); - } - get attributes() { - if (!this._attributes) { - if (this.isFrozen) { - const s = new this.ObjectElement(); - return s.freeze(), s; - } - this._attributes = new this.ObjectElement(); - } - return this._attributes; - } - set attributes(s) { - s instanceof this.ObjectElement - ? (this._attributes = s) - : this.attributes.set(s || {}); - } - get id() { - return this.getMetaProperty('id', ''); - } - set id(s) { - this.setMetaProperty('id', s); - } - get classes() { - return this.getMetaProperty('classes', []); - } - set classes(s) { - this.setMetaProperty('classes', s); - } - get title() { - return this.getMetaProperty('title', ''); - } - set title(s) { - this.setMetaProperty('title', s); - } - get description() { - return this.getMetaProperty('description', ''); - } - set description(s) { - this.setMetaProperty('description', s); - } - get links() { - return this.getMetaProperty('links', []); - } - set links(s) { - this.setMetaProperty('links', s); - } - get isFrozen() { - return Object.isFrozen(this); - } - get parents() { - let { parent: s } = this; - const o = new w(); - for (; s; ) o.push(s), (s = s.parent); - return o; - } - get children() { - if (Array.isArray(this.content)) return new w(this.content); - if (this.content instanceof _) { - const s = new w([this.content.key]); - return this.content.value && s.push(this.content.value), s; - } - return this.content instanceof Element ? new w([this.content]) : new w(); - } - get recursiveChildren() { - const s = new w(); - return ( - this.children.forEach((o) => { - s.push(o), - o.recursiveChildren.forEach((o) => { - s.push(o); - }); - }), - s - ); - } - } - s.exports = Element; - }, - 87726: (s, o, i) => { - const u = i(55973), - _ = i(10316); - s.exports = class MemberElement extends _ { - constructor(s, o, i, _) { - super(new u(), i, _), (this.element = 'member'), (this.key = s), (this.value = o); - } - get key() { - return this.content.key; - } - set key(s) { - this.content.key = this.refract(s); - } - get value() { - return this.content.value; - } - set value(s) { - this.content.value = this.refract(s); - } - }; - }, - 41067: (s, o, i) => { - const u = i(10316); - s.exports = class NullElement extends u { - constructor(s, o, i) { - super(s || null, o, i), (this.element = 'null'); - } - primitive() { - return 'null'; - } - set() { - return new Error('Cannot set the value of null'); - } - }; - }, - 40239: (s, o, i) => { - const u = i(10316); - s.exports = class NumberElement extends u { - constructor(s, o, i) { - super(s, o, i), (this.element = 'number'); - } - primitive() { - return 'number'; - } - }; - }, - 61045: (s, o, i) => { - const u = i(6048), - _ = i(23805), - w = i(6233), - x = i(87726), - C = i(10866); - s.exports = class ObjectElement extends w { - constructor(s, o, i) { - super(s || [], o, i), (this.element = 'object'); - } - primitive() { - return 'object'; - } - toValue() { - return this.content.reduce( - (s, o) => ((s[o.key.toValue()] = o.value ? o.value.toValue() : void 0), s), - {} - ); - } - get(s) { - const o = this.getMember(s); - if (o) return o.value; - } - getMember(s) { - if (void 0 !== s) return this.content.find((o) => o.key.toValue() === s); - } - remove(s) { - let o = null; - return ( - (this.content = this.content.filter((i) => i.key.toValue() !== s || ((o = i), !1))), - o - ); - } - getKey(s) { - const o = this.getMember(s); - if (o) return o.key; - } - set(s, o) { - if (_(s)) - return ( - Object.keys(s).forEach((o) => { - this.set(o, s[o]); - }), - this - ); - const i = s, - u = this.getMember(i); - return u ? (u.value = o) : this.content.push(new x(i, o)), this; - } - keys() { - return this.content.map((s) => s.key.toValue()); - } - values() { - return this.content.map((s) => s.value.toValue()); - } - hasKey(s) { - return this.content.some((o) => o.key.equals(s)); - } - items() { - return this.content.map((s) => [s.key.toValue(), s.value.toValue()]); - } - map(s, o) { - return this.content.map((i) => s.bind(o)(i.value, i.key, i)); - } - compactMap(s, o) { - const i = []; - return ( - this.forEach((u, _, w) => { - const x = s.bind(o)(u, _, w); - x && i.push(x); - }), - i - ); - } - filter(s, o) { - return new C(this.content).filter(s, o); - } - reject(s, o) { - return this.filter(u(s), o); - } - forEach(s, o) { - return this.content.forEach((i) => s.bind(o)(i.value, i.key, i)); - } - }; - }, - 71167: (s, o, i) => { - const u = i(10316); - s.exports = class StringElement extends u { - constructor(s, o, i) { - super(s, o, i), (this.element = 'string'); - } - primitive() { - return 'string'; - } - get length() { - return this.content.length; - } - }; - }, - 75147: (s, o, i) => { - const u = i(85105); - s.exports = class JSON06Serialiser extends u { - serialise(s) { - if (!(s instanceof this.namespace.elements.Element)) - throw new TypeError(`Given element \`${s}\` is not an Element instance`); - let o; - s._attributes && s.attributes.get('variable') && (o = s.attributes.get('variable')); - const i = { element: s.element }; - s._meta && s._meta.length > 0 && (i.meta = this.serialiseObject(s.meta)); - const u = 'enum' === s.element || -1 !== s.attributes.keys().indexOf('enumerations'); - if (u) { - const o = this.enumSerialiseAttributes(s); - o && (i.attributes = o); - } else if (s._attributes && s._attributes.length > 0) { - let { attributes: u } = s; - u.get('metadata') && - ((u = u.clone()), u.set('meta', u.get('metadata')), u.remove('metadata')), - 'member' === s.element && o && ((u = u.clone()), u.remove('variable')), - u.length > 0 && (i.attributes = this.serialiseObject(u)); - } - if (u) i.content = this.enumSerialiseContent(s, i); - else if (this[`${s.element}SerialiseContent`]) - i.content = this[`${s.element}SerialiseContent`](s, i); - else if (void 0 !== s.content) { - let u; - o && s.content.key - ? ((u = s.content.clone()), - u.key.attributes.set('variable', o), - (u = this.serialiseContent(u))) - : (u = this.serialiseContent(s.content)), - this.shouldSerialiseContent(s, u) && (i.content = u); - } else - this.shouldSerialiseContent(s, s.content) && - s instanceof this.namespace.elements.Array && - (i.content = []); - return i; - } - shouldSerialiseContent(s, o) { - return ( - 'parseResult' === s.element || - 'httpRequest' === s.element || - 'httpResponse' === s.element || - 'category' === s.element || - 'link' === s.element || - (void 0 !== o && (!Array.isArray(o) || 0 !== o.length)) - ); - } - refSerialiseContent(s, o) { - return delete o.attributes, { href: s.toValue(), path: s.path.toValue() }; - } - sourceMapSerialiseContent(s) { - return s.toValue(); - } - dataStructureSerialiseContent(s) { - return [this.serialiseContent(s.content)]; - } - enumSerialiseAttributes(s) { - const o = s.attributes.clone(), - i = o.remove('enumerations') || new this.namespace.elements.Array([]), - u = o.get('default'); - let _ = o.get('samples') || new this.namespace.elements.Array([]); - if ( - (u && - u.content && - (u.content.attributes && u.content.attributes.remove('typeAttributes'), - o.set('default', new this.namespace.elements.Array([u.content]))), - _.forEach((s) => { - s.content && s.content.element && s.content.attributes.remove('typeAttributes'); - }), - s.content && 0 !== i.length && _.unshift(s.content), - (_ = _.map((s) => - s instanceof this.namespace.elements.Array - ? [s] - : new this.namespace.elements.Array([s.content]) - )), - _.length && o.set('samples', _), - o.length > 0) - ) - return this.serialiseObject(o); - } - enumSerialiseContent(s) { - if (s._attributes) { - const o = s.attributes.get('enumerations'); - if (o && o.length > 0) - return o.content.map((s) => { - const o = s.clone(); - return o.attributes.remove('typeAttributes'), this.serialise(o); - }); - } - if (s.content) { - const o = s.content.clone(); - return o.attributes.remove('typeAttributes'), [this.serialise(o)]; - } - return []; - } - deserialise(s) { - if ('string' == typeof s) return new this.namespace.elements.String(s); - if ('number' == typeof s) return new this.namespace.elements.Number(s); - if ('boolean' == typeof s) return new this.namespace.elements.Boolean(s); - if (null === s) return new this.namespace.elements.Null(); - if (Array.isArray(s)) - return new this.namespace.elements.Array(s.map(this.deserialise, this)); - const o = this.namespace.getElementClass(s.element), - i = new o(); - i.element !== s.element && (i.element = s.element), - s.meta && this.deserialiseObject(s.meta, i.meta), - s.attributes && this.deserialiseObject(s.attributes, i.attributes); - const u = this.deserialiseContent(s.content); - if (((void 0 === u && null !== i.content) || (i.content = u), 'enum' === i.element)) { - i.content && i.attributes.set('enumerations', i.content); - let s = i.attributes.get('samples'); - if ((i.attributes.remove('samples'), s)) { - const u = s; - (s = new this.namespace.elements.Array()), - u.forEach((u) => { - u.forEach((u) => { - const _ = new o(u); - (_.element = i.element), s.push(_); - }); - }); - const _ = s.shift(); - (i.content = _ ? _.content : void 0), i.attributes.set('samples', s); - } else i.content = void 0; - let u = i.attributes.get('default'); - if (u && u.length > 0) { - u = u.get(0); - const s = new o(u); - (s.element = i.element), i.attributes.set('default', s); - } - } else if ('dataStructure' === i.element && Array.isArray(i.content)) - [i.content] = i.content; - else if ('category' === i.element) { - const s = i.attributes.get('meta'); - s && (i.attributes.set('metadata', s), i.attributes.remove('meta')); - } else - 'member' === i.element && - i.key && - i.key._attributes && - i.key._attributes.getValue('variable') && - (i.attributes.set('variable', i.key.attributes.get('variable')), - i.key.attributes.remove('variable')); - return i; - } - serialiseContent(s) { - if (s instanceof this.namespace.elements.Element) return this.serialise(s); - if (s instanceof this.namespace.KeyValuePair) { - const o = { key: this.serialise(s.key) }; - return s.value && (o.value = this.serialise(s.value)), o; - } - return s && s.map ? s.map(this.serialise, this) : s; - } - deserialiseContent(s) { - if (s) { - if (s.element) return this.deserialise(s); - if (s.key) { - const o = new this.namespace.KeyValuePair(this.deserialise(s.key)); - return s.value && (o.value = this.deserialise(s.value)), o; - } - if (s.map) return s.map(this.deserialise, this); - } - return s; - } - shouldRefract(s) { - return ( - !!( - (s._attributes && s.attributes.keys().length) || - (s._meta && s.meta.keys().length) - ) || - ('enum' !== s.element && (s.element !== s.primitive() || 'member' === s.element)) - ); - } - convertKeyToRefract(s, o) { - return this.shouldRefract(o) - ? this.serialise(o) - : 'enum' === o.element - ? this.serialiseEnum(o) - : 'array' === o.element - ? o.map((o) => - this.shouldRefract(o) || 'default' === s - ? this.serialise(o) - : 'array' === o.element || 'object' === o.element || 'enum' === o.element - ? o.children.map((s) => this.serialise(s)) - : o.toValue() - ) - : 'object' === o.element - ? (o.content || []).map(this.serialise, this) - : o.toValue(); - } - serialiseEnum(s) { - return s.children.map((s) => this.serialise(s)); - } - serialiseObject(s) { - const o = {}; - return ( - s.forEach((s, i) => { - if (s) { - const u = i.toValue(); - o[u] = this.convertKeyToRefract(u, s); - } - }), - o - ); - } - deserialiseObject(s, o) { - Object.keys(s).forEach((i) => { - o.set(i, this.deserialise(s[i])); - }); - } - }; - }, - 85105: (s) => { - s.exports = class JSONSerialiser { - constructor(s) { - this.namespace = s || new this.Namespace(); - } - serialise(s) { - if (!(s instanceof this.namespace.elements.Element)) - throw new TypeError(`Given element \`${s}\` is not an Element instance`); - const o = { element: s.element }; - s._meta && s._meta.length > 0 && (o.meta = this.serialiseObject(s.meta)), - s._attributes && - s._attributes.length > 0 && - (o.attributes = this.serialiseObject(s.attributes)); - const i = this.serialiseContent(s.content); - return void 0 !== i && (o.content = i), o; - } - deserialise(s) { - if (!s.element) - throw new Error('Given value is not an object containing an element name'); - const o = new (this.namespace.getElementClass(s.element))(); - o.element !== s.element && (o.element = s.element), - s.meta && this.deserialiseObject(s.meta, o.meta), - s.attributes && this.deserialiseObject(s.attributes, o.attributes); - const i = this.deserialiseContent(s.content); - return (void 0 === i && null !== o.content) || (o.content = i), o; - } - serialiseContent(s) { - if (s instanceof this.namespace.elements.Element) return this.serialise(s); - if (s instanceof this.namespace.KeyValuePair) { - const o = { key: this.serialise(s.key) }; - return s.value && (o.value = this.serialise(s.value)), o; - } - if (s && s.map) { - if (0 === s.length) return; - return s.map(this.serialise, this); - } - return s; - } - deserialiseContent(s) { - if (s) { - if (s.element) return this.deserialise(s); - if (s.key) { - const o = new this.namespace.KeyValuePair(this.deserialise(s.key)); - return s.value && (o.value = this.deserialise(s.value)), o; - } - if (s.map) return s.map(this.deserialise, this); - } - return s; - } - serialiseObject(s) { - const o = {}; - if ( - (s.forEach((s, i) => { - s && (o[i.toValue()] = this.serialise(s)); - }), - 0 !== Object.keys(o).length) - ) - return o; - } - deserialiseObject(s, o) { - Object.keys(s).forEach((i) => { - o.set(i, this.deserialise(s[i])); - }); - } - }; - }, - 65606: (s) => { - var o, - i, - u = (s.exports = {}); - function defaultSetTimout() { - throw new Error('setTimeout has not been defined'); - } - function defaultClearTimeout() { - throw new Error('clearTimeout has not been defined'); - } - function runTimeout(s) { - if (o === setTimeout) return setTimeout(s, 0); - if ((o === defaultSetTimout || !o) && setTimeout) - return (o = setTimeout), setTimeout(s, 0); - try { - return o(s, 0); - } catch (i) { - try { - return o.call(null, s, 0); - } catch (i) { - return o.call(this, s, 0); - } - } - } - !(function () { - try { - o = 'function' == typeof setTimeout ? setTimeout : defaultSetTimout; - } catch (s) { - o = defaultSetTimout; - } - try { - i = 'function' == typeof clearTimeout ? clearTimeout : defaultClearTimeout; - } catch (s) { - i = defaultClearTimeout; - } - })(); - var _, - w = [], - x = !1, - C = -1; - function cleanUpNextTick() { - x && _ && ((x = !1), _.length ? (w = _.concat(w)) : (C = -1), w.length && drainQueue()); - } - function drainQueue() { - if (!x) { - var s = runTimeout(cleanUpNextTick); - x = !0; - for (var o = w.length; o; ) { - for (_ = w, w = []; ++C < o; ) _ && _[C].run(); - (C = -1), (o = w.length); - } - (_ = null), - (x = !1), - (function runClearTimeout(s) { - if (i === clearTimeout) return clearTimeout(s); - if ((i === defaultClearTimeout || !i) && clearTimeout) - return (i = clearTimeout), clearTimeout(s); - try { - return i(s); - } catch (o) { - try { - return i.call(null, s); - } catch (o) { - return i.call(this, s); - } - } - })(s); - } - } - function Item(s, o) { - (this.fun = s), (this.array = o); - } - function noop() {} - (u.nextTick = function (s) { - var o = new Array(arguments.length - 1); - if (arguments.length > 1) - for (var i = 1; i < arguments.length; i++) o[i - 1] = arguments[i]; - w.push(new Item(s, o)), 1 !== w.length || x || runTimeout(drainQueue); - }), - (Item.prototype.run = function () { - this.fun.apply(null, this.array); - }), - (u.title = 'browser'), - (u.browser = !0), - (u.env = {}), - (u.argv = []), - (u.version = ''), - (u.versions = {}), - (u.on = noop), - (u.addListener = noop), - (u.once = noop), - (u.off = noop), - (u.removeListener = noop), - (u.removeAllListeners = noop), - (u.emit = noop), - (u.prependListener = noop), - (u.prependOnceListener = noop), - (u.listeners = function (s) { - return []; - }), - (u.binding = function (s) { - throw new Error('process.binding is not supported'); - }), - (u.cwd = function () { - return '/'; - }), - (u.chdir = function (s) { - throw new Error('process.chdir is not supported'); - }), - (u.umask = function () { - return 0; - }); - }, - 2694: (s, o, i) => { - 'use strict'; - var u = i(6925); - function emptyFunction() {} - function emptyFunctionWithReset() {} - (emptyFunctionWithReset.resetWarningCache = emptyFunction), - (s.exports = function () { - function shim(s, o, i, _, w, x) { - if (x !== u) { - var C = new Error( - 'Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types' - ); - throw ((C.name = 'Invariant Violation'), C); - } - } - function getShim() { - return shim; - } - shim.isRequired = shim; - var s = { - array: shim, - bigint: shim, - bool: shim, - func: shim, - number: shim, - object: shim, - string: shim, - symbol: shim, - any: shim, - arrayOf: getShim, - element: shim, - elementType: shim, - instanceOf: getShim, - node: shim, - objectOf: getShim, - oneOf: getShim, - oneOfType: getShim, - shape: getShim, - exact: getShim, - checkPropTypes: emptyFunctionWithReset, - resetWarningCache: emptyFunction - }; - return (s.PropTypes = s), s; - }); - }, - 5556: (s, o, i) => { - s.exports = i(2694)(); - }, - 6925: (s) => { - 'use strict'; - s.exports = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; - }, - 73992: (s, o) => { - 'use strict'; - var i = Object.prototype.hasOwnProperty; - function decode(s) { - try { - return decodeURIComponent(s.replace(/\+/g, ' ')); - } catch (s) { - return null; - } - } - function encode(s) { - try { - return encodeURIComponent(s); - } catch (s) { - return null; - } - } - (o.stringify = function querystringify(s, o) { - o = o || ''; - var u, - _, - w = []; - for (_ in ('string' != typeof o && (o = '?'), s)) - if (i.call(s, _)) { - if ( - ((u = s[_]) || (null != u && !isNaN(u)) || (u = ''), - (_ = encode(_)), - (u = encode(u)), - null === _ || null === u) - ) - continue; - w.push(_ + '=' + u); - } - return w.length ? o + w.join('&') : ''; - }), - (o.parse = function querystring(s) { - for (var o, i = /([^=?#&]+)=?([^&]*)/g, u = {}; (o = i.exec(s)); ) { - var _ = decode(o[1]), - w = decode(o[2]); - null === _ || null === w || _ in u || (u[_] = w); - } - return u; - }); - }, - 41859: (s, o, i) => { - const u = i(27096), - _ = i(78004), - w = u.types; - s.exports = class RandExp { - constructor(s, o) { - if ((this._setDefaults(s), s instanceof RegExp)) - (this.ignoreCase = s.ignoreCase), (this.multiline = s.multiline), (s = s.source); - else { - if ('string' != typeof s) throw new Error('Expected a regexp or string'); - (this.ignoreCase = o && -1 !== o.indexOf('i')), - (this.multiline = o && -1 !== o.indexOf('m')); - } - this.tokens = u(s); - } - _setDefaults(s) { - (this.max = - null != s.max - ? s.max - : null != RandExp.prototype.max - ? RandExp.prototype.max - : 100), - (this.defaultRange = s.defaultRange ? s.defaultRange : this.defaultRange.clone()), - s.randInt && (this.randInt = s.randInt); - } - gen() { - return this._gen(this.tokens, []); - } - _gen(s, o) { - var i, u, _, x, C; - switch (s.type) { - case w.ROOT: - case w.GROUP: - if (s.followedBy || s.notFollowedBy) return ''; - for ( - s.remember && void 0 === s.groupNumber && (s.groupNumber = o.push(null) - 1), - u = '', - x = 0, - C = (i = s.options ? this._randSelect(s.options) : s.stack).length; - x < C; - x++ - ) - u += this._gen(i[x], o); - return s.remember && (o[s.groupNumber] = u), u; - case w.POSITION: - return ''; - case w.SET: - var j = this._expand(s); - return j.length ? String.fromCharCode(this._randSelect(j)) : ''; - case w.REPETITION: - for ( - _ = this.randInt(s.min, s.max === 1 / 0 ? s.min + this.max : s.max), - u = '', - x = 0; - x < _; - x++ - ) - u += this._gen(s.value, o); - return u; - case w.REFERENCE: - return o[s.value - 1] || ''; - case w.CHAR: - var L = - this.ignoreCase && this._randBool() ? this._toOtherCase(s.value) : s.value; - return String.fromCharCode(L); - } - } - _toOtherCase(s) { - return s + (97 <= s && s <= 122 ? -32 : 65 <= s && s <= 90 ? 32 : 0); - } - _randBool() { - return !this.randInt(0, 1); - } - _randSelect(s) { - return s instanceof _ - ? s.index(this.randInt(0, s.length - 1)) - : s[this.randInt(0, s.length - 1)]; - } - _expand(s) { - if (s.type === u.types.CHAR) return new _(s.value); - if (s.type === u.types.RANGE) return new _(s.from, s.to); - { - let o = new _(); - for (let i = 0; i < s.set.length; i++) { - let u = this._expand(s.set[i]); - if ((o.add(u), this.ignoreCase)) - for (let s = 0; s < u.length; s++) { - let i = u.index(s), - _ = this._toOtherCase(i); - i !== _ && o.add(_); - } - } - return s.not - ? this.defaultRange.clone().subtract(o) - : this.defaultRange.clone().intersect(o); - } - } - randInt(s, o) { - return s + Math.floor(Math.random() * (1 + o - s)); - } - get defaultRange() { - return (this._range = this._range || new _(32, 126)); - } - set defaultRange(s) { - this._range = s; - } - static randexp(s, o) { - var i; - return ( - 'string' == typeof s && (s = new RegExp(s, o)), - void 0 === s._randexp - ? ((i = new RandExp(s, o)), (s._randexp = i)) - : (i = s._randexp)._setDefaults(s), - i.gen() - ); - } - static sugar() { - RegExp.prototype.gen = function () { - return RandExp.randexp(this); - }; - } - }; - }, - 53209: (s, o, i) => { - 'use strict'; - var u = i(65606), - _ = 65536, - w = 4294967295; - var x = i(92861).Buffer, - C = i.g.crypto || i.g.msCrypto; - C && C.getRandomValues - ? (s.exports = function randomBytes(s, o) { - if (s > w) throw new RangeError('requested too many random bytes'); - var i = x.allocUnsafe(s); - if (s > 0) - if (s > _) for (var j = 0; j < s; j += _) C.getRandomValues(i.slice(j, j + _)); - else C.getRandomValues(i); - if ('function' == typeof o) - return u.nextTick(function () { - o(null, i); - }); - return i; - }) - : (s.exports = function oldBrowser() { - throw new Error( - 'Secure random number generation is not supported by this browser.\nUse Chrome, Firefox or Internet Explorer 11' - ); - }); - }, - 25264: (s, o, i) => { - 'use strict'; - function _typeof(s) { - return ( - (_typeof = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (s) { - return typeof s; - } - : function (s) { - return s && - 'function' == typeof Symbol && - s.constructor === Symbol && - s !== Symbol.prototype - ? 'symbol' - : typeof s; - }), - _typeof(s) - ); - } - Object.defineProperty(o, '__esModule', { value: !0 }), (o.CopyToClipboard = void 0); - var u = _interopRequireDefault(i(96540)), - _ = _interopRequireDefault(i(17965)), - w = ['text', 'onCopy', 'options', 'children']; - function _interopRequireDefault(s) { - return s && s.__esModule ? s : { default: s }; - } - function ownKeys(s, o) { - var i = Object.keys(s); - if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); - o && - (u = u.filter(function (o) { - return Object.getOwnPropertyDescriptor(s, o).enumerable; - })), - i.push.apply(i, u); - } - return i; - } - function _objectSpread(s) { - for (var o = 1; o < arguments.length; o++) { - var i = null != arguments[o] ? arguments[o] : {}; - o % 2 - ? ownKeys(Object(i), !0).forEach(function (o) { - _defineProperty(s, o, i[o]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) - : ownKeys(Object(i)).forEach(function (o) { - Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); - }); - } - return s; - } - function _objectWithoutProperties(s, o) { - if (null == s) return {}; - var i, - u, - _ = (function _objectWithoutPropertiesLoose(s, o) { - if (null == s) return {}; - var i, - u, - _ = {}, - w = Object.keys(s); - for (u = 0; u < w.length; u++) (i = w[u]), o.indexOf(i) >= 0 || (_[i] = s[i]); - return _; - })(s, o); - if (Object.getOwnPropertySymbols) { - var w = Object.getOwnPropertySymbols(s); - for (u = 0; u < w.length; u++) - (i = w[u]), - o.indexOf(i) >= 0 || - (Object.prototype.propertyIsEnumerable.call(s, i) && (_[i] = s[i])); - } - return _; - } - function _defineProperties(s, o) { - for (var i = 0; i < o.length; i++) { - var u = o[i]; - (u.enumerable = u.enumerable || !1), - (u.configurable = !0), - 'value' in u && (u.writable = !0), - Object.defineProperty(s, u.key, u); - } - } - function _setPrototypeOf(s, o) { - return ( - (_setPrototypeOf = - Object.setPrototypeOf || - function _setPrototypeOf(s, o) { - return (s.__proto__ = o), s; - }), - _setPrototypeOf(s, o) - ); - } - function _createSuper(s) { - var o = (function _isNativeReflectConstruct() { - if ('undefined' == typeof Reflect || !Reflect.construct) return !1; - if (Reflect.construct.sham) return !1; - if ('function' == typeof Proxy) return !0; - try { - return ( - Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})), !0 - ); - } catch (s) { - return !1; - } - })(); - return function _createSuperInternal() { - var i, - u = _getPrototypeOf(s); - if (o) { - var _ = _getPrototypeOf(this).constructor; - i = Reflect.construct(u, arguments, _); - } else i = u.apply(this, arguments); - return (function _possibleConstructorReturn(s, o) { - if (o && ('object' === _typeof(o) || 'function' == typeof o)) return o; - if (void 0 !== o) - throw new TypeError('Derived constructors may only return object or undefined'); - return _assertThisInitialized(s); - })(this, i); - }; - } - function _assertThisInitialized(s) { - if (void 0 === s) - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - return s; - } - function _getPrototypeOf(s) { - return ( - (_getPrototypeOf = Object.setPrototypeOf - ? Object.getPrototypeOf - : function _getPrototypeOf(s) { - return s.__proto__ || Object.getPrototypeOf(s); - }), - _getPrototypeOf(s) - ); - } - function _defineProperty(s, o, i) { - return ( - o in s - ? Object.defineProperty(s, o, { - value: i, - enumerable: !0, - configurable: !0, - writable: !0 - }) - : (s[o] = i), - s - ); - } - var x = (function (s) { - !(function _inherits(s, o) { - if ('function' != typeof o && null !== o) - throw new TypeError('Super expression must either be null or a function'); - (s.prototype = Object.create(o && o.prototype, { - constructor: { value: s, writable: !0, configurable: !0 } - })), - Object.defineProperty(s, 'prototype', { writable: !1 }), - o && _setPrototypeOf(s, o); - })(CopyToClipboard, s); - var o = _createSuper(CopyToClipboard); - function CopyToClipboard() { - var s; - !(function _classCallCheck(s, o) { - if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); - })(this, CopyToClipboard); - for (var i = arguments.length, w = new Array(i), x = 0; x < i; x++) - w[x] = arguments[x]; - return ( - _defineProperty( - _assertThisInitialized((s = o.call.apply(o, [this].concat(w)))), - 'onClick', - function (o) { - var i = s.props, - w = i.text, - x = i.onCopy, - C = i.children, - j = i.options, - L = u.default.Children.only(C), - B = (0, _.default)(w, j); - x && x(w, B), - L && L.props && 'function' == typeof L.props.onClick && L.props.onClick(o); - } - ), - s - ); - } - return ( - (function _createClass(s, o, i) { - return ( - o && _defineProperties(s.prototype, o), - i && _defineProperties(s, i), - Object.defineProperty(s, 'prototype', { writable: !1 }), - s - ); - })(CopyToClipboard, [ - { - key: 'render', - value: function render() { - var s = this.props, - o = (s.text, s.onCopy, s.options, s.children), - i = _objectWithoutProperties(s, w), - _ = u.default.Children.only(o); - return u.default.cloneElement( - _, - _objectSpread(_objectSpread({}, i), {}, { onClick: this.onClick }) - ); - } - } - ]), - CopyToClipboard - ); - })(u.default.PureComponent); - (o.CopyToClipboard = x), - _defineProperty(x, 'defaultProps', { onCopy: void 0, options: void 0 }); - }, - 59399: (s, o, i) => { - 'use strict'; - var u = i(25264).CopyToClipboard; - (u.CopyToClipboard = u), (s.exports = u); - }, - 81214: (s, o, i) => { - 'use strict'; - function _typeof(s) { - return ( - (_typeof = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (s) { - return typeof s; - } - : function (s) { - return s && - 'function' == typeof Symbol && - s.constructor === Symbol && - s !== Symbol.prototype - ? 'symbol' - : typeof s; - }), - _typeof(s) - ); - } - Object.defineProperty(o, '__esModule', { value: !0 }), (o.DebounceInput = void 0); - var u = _interopRequireDefault(i(96540)), - _ = _interopRequireDefault(i(20181)), - w = [ - 'element', - 'onChange', - 'value', - 'minLength', - 'debounceTimeout', - 'forceNotifyByEnter', - 'forceNotifyOnBlur', - 'onKeyDown', - 'onBlur', - 'inputRef' - ]; - function _interopRequireDefault(s) { - return s && s.__esModule ? s : { default: s }; - } - function _objectWithoutProperties(s, o) { - if (null == s) return {}; - var i, - u, - _ = (function _objectWithoutPropertiesLoose(s, o) { - if (null == s) return {}; - var i, - u, - _ = {}, - w = Object.keys(s); - for (u = 0; u < w.length; u++) (i = w[u]), o.indexOf(i) >= 0 || (_[i] = s[i]); - return _; - })(s, o); - if (Object.getOwnPropertySymbols) { - var w = Object.getOwnPropertySymbols(s); - for (u = 0; u < w.length; u++) - (i = w[u]), - o.indexOf(i) >= 0 || - (Object.prototype.propertyIsEnumerable.call(s, i) && (_[i] = s[i])); - } - return _; - } - function ownKeys(s, o) { - var i = Object.keys(s); - if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); - o && - (u = u.filter(function (o) { - return Object.getOwnPropertyDescriptor(s, o).enumerable; - })), - i.push.apply(i, u); - } - return i; - } - function _objectSpread(s) { - for (var o = 1; o < arguments.length; o++) { - var i = null != arguments[o] ? arguments[o] : {}; - o % 2 - ? ownKeys(Object(i), !0).forEach(function (o) { - _defineProperty(s, o, i[o]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) - : ownKeys(Object(i)).forEach(function (o) { - Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); - }); - } - return s; - } - function _defineProperties(s, o) { - for (var i = 0; i < o.length; i++) { - var u = o[i]; - (u.enumerable = u.enumerable || !1), - (u.configurable = !0), - 'value' in u && (u.writable = !0), - Object.defineProperty(s, u.key, u); - } - } - function _setPrototypeOf(s, o) { - return ( - (_setPrototypeOf = - Object.setPrototypeOf || - function _setPrototypeOf(s, o) { - return (s.__proto__ = o), s; - }), - _setPrototypeOf(s, o) - ); - } - function _createSuper(s) { - var o = (function _isNativeReflectConstruct() { - if ('undefined' == typeof Reflect || !Reflect.construct) return !1; - if (Reflect.construct.sham) return !1; - if ('function' == typeof Proxy) return !0; - try { - return ( - Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})), !0 - ); - } catch (s) { - return !1; - } - })(); - return function _createSuperInternal() { - var i, - u = _getPrototypeOf(s); - if (o) { - var _ = _getPrototypeOf(this).constructor; - i = Reflect.construct(u, arguments, _); - } else i = u.apply(this, arguments); - return (function _possibleConstructorReturn(s, o) { - if (o && ('object' === _typeof(o) || 'function' == typeof o)) return o; - if (void 0 !== o) - throw new TypeError('Derived constructors may only return object or undefined'); - return _assertThisInitialized(s); - })(this, i); - }; - } - function _assertThisInitialized(s) { - if (void 0 === s) - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - return s; - } - function _getPrototypeOf(s) { - return ( - (_getPrototypeOf = Object.setPrototypeOf - ? Object.getPrototypeOf - : function _getPrototypeOf(s) { - return s.__proto__ || Object.getPrototypeOf(s); - }), - _getPrototypeOf(s) - ); - } - function _defineProperty(s, o, i) { - return ( - o in s - ? Object.defineProperty(s, o, { - value: i, - enumerable: !0, - configurable: !0, - writable: !0 - }) - : (s[o] = i), - s - ); - } - var x = (function (s) { - !(function _inherits(s, o) { - if ('function' != typeof o && null !== o) - throw new TypeError('Super expression must either be null or a function'); - (s.prototype = Object.create(o && o.prototype, { - constructor: { value: s, writable: !0, configurable: !0 } - })), - Object.defineProperty(s, 'prototype', { writable: !1 }), - o && _setPrototypeOf(s, o); - })(DebounceInput, s); - var o = _createSuper(DebounceInput); - function DebounceInput(s) { - var i; - !(function _classCallCheck(s, o) { - if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); - })(this, DebounceInput), - _defineProperty( - _assertThisInitialized((i = o.call(this, s))), - 'onChange', - function (s) { - s.persist(); - var o = i.state.value, - u = i.props.minLength; - i.setState({ value: s.target.value }, function () { - var _ = i.state.value; - _.length >= u - ? i.notify(s) - : o.length > _.length && - i.notify( - _objectSpread( - _objectSpread({}, s), - {}, - { - target: _objectSpread( - _objectSpread({}, s.target), - {}, - { value: '' } - ) - } - ) - ); - }); - } - ), - _defineProperty(_assertThisInitialized(i), 'onKeyDown', function (s) { - 'Enter' === s.key && i.forceNotify(s); - var o = i.props.onKeyDown; - o && (s.persist(), o(s)); - }), - _defineProperty(_assertThisInitialized(i), 'onBlur', function (s) { - i.forceNotify(s); - var o = i.props.onBlur; - o && (s.persist(), o(s)); - }), - _defineProperty(_assertThisInitialized(i), 'createNotifier', function (s) { - if (s < 0) - i.notify = function () { - return null; - }; - else if (0 === s) i.notify = i.doNotify; - else { - var o = (0, _.default)(function (s) { - (i.isDebouncing = !1), i.doNotify(s); - }, s); - (i.notify = function (s) { - (i.isDebouncing = !0), o(s); - }), - (i.flush = function () { - return o.flush(); - }), - (i.cancel = function () { - (i.isDebouncing = !1), o.cancel(); - }); - } - }), - _defineProperty(_assertThisInitialized(i), 'doNotify', function () { - i.props.onChange.apply(void 0, arguments); - }), - _defineProperty(_assertThisInitialized(i), 'forceNotify', function (s) { - var o = i.props.debounceTimeout; - if (i.isDebouncing || !(o > 0)) { - i.cancel && i.cancel(); - var u = i.state.value, - _ = i.props.minLength; - u.length >= _ - ? i.doNotify(s) - : i.doNotify( - _objectSpread( - _objectSpread({}, s), - {}, - { target: _objectSpread(_objectSpread({}, s.target), {}, { value: u }) } - ) - ); - } - }), - (i.isDebouncing = !1), - (i.state = { value: void 0 === s.value || null === s.value ? '' : s.value }); - var u = i.props.debounceTimeout; - return i.createNotifier(u), i; - } - return ( - (function _createClass(s, o, i) { - return ( - o && _defineProperties(s.prototype, o), - i && _defineProperties(s, i), - Object.defineProperty(s, 'prototype', { writable: !1 }), - s - ); - })(DebounceInput, [ - { - key: 'componentDidUpdate', - value: function componentDidUpdate(s) { - if (!this.isDebouncing) { - var o = this.props, - i = o.value, - u = o.debounceTimeout, - _ = s.debounceTimeout, - w = s.value, - x = this.state.value; - void 0 !== i && w !== i && x !== i && this.setState({ value: i }), - u !== _ && this.createNotifier(u); - } - } - }, - { - key: 'componentWillUnmount', - value: function componentWillUnmount() { - this.flush && this.flush(); - } - }, - { - key: 'render', - value: function render() { - var s, - o, - i = this.props, - _ = i.element, - x = - (i.onChange, i.value, i.minLength, i.debounceTimeout, i.forceNotifyByEnter), - C = i.forceNotifyOnBlur, - j = i.onKeyDown, - L = i.onBlur, - B = i.inputRef, - $ = _objectWithoutProperties(i, w), - V = this.state.value; - (s = x ? { onKeyDown: this.onKeyDown } : j ? { onKeyDown: j } : {}), - (o = C ? { onBlur: this.onBlur } : L ? { onBlur: L } : {}); - var U = B ? { ref: B } : {}; - return u.default.createElement( - _, - _objectSpread( - _objectSpread( - _objectSpread( - _objectSpread({}, $), - {}, - { onChange: this.onChange, value: V }, - s - ), - o - ), - U - ) - ); - } - } - ]), - DebounceInput - ); - })(u.default.PureComponent); - (o.DebounceInput = x), - _defineProperty(x, 'defaultProps', { - element: 'input', - type: 'text', - onKeyDown: void 0, - onBlur: void 0, - value: void 0, - minLength: 0, - debounceTimeout: 100, - forceNotifyByEnter: !0, - forceNotifyOnBlur: !0, - inputRef: void 0 - }); - }, - 24677: (s, o, i) => { - 'use strict'; - var u = i(81214).DebounceInput; - (u.DebounceInput = u), (s.exports = u); - }, - 22551: (s, o, i) => { - 'use strict'; - var u = i(96540), - _ = i(69982); - function p(s) { - for ( - var o = 'https://reactjs.org/docs/error-decoder.html?invariant=' + s, i = 1; - i < arguments.length; - i++ - ) - o += '&args[]=' + encodeURIComponent(arguments[i]); - return ( - 'Minified React error #' + - s + - '; visit ' + - o + - ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' - ); - } - var w = new Set(), - x = {}; - function fa(s, o) { - ha(s, o), ha(s + 'Capture', o); - } - function ha(s, o) { - for (x[s] = o, s = 0; s < o.length; s++) w.add(o[s]); - } - var C = !( - 'undefined' == typeof window || - void 0 === window.document || - void 0 === window.document.createElement - ), - j = Object.prototype.hasOwnProperty, - L = - /^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/, - B = {}, - $ = {}; - function v(s, o, i, u, _, w, x) { - (this.acceptsBooleans = 2 === o || 3 === o || 4 === o), - (this.attributeName = u), - (this.attributeNamespace = _), - (this.mustUseProperty = i), - (this.propertyName = s), - (this.type = o), - (this.sanitizeURL = w), - (this.removeEmptyString = x); - } - var V = {}; - 'children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style' - .split(' ') - .forEach(function (s) { - V[s] = new v(s, 0, !1, s, null, !1, !1); - }), - [ - ['acceptCharset', 'accept-charset'], - ['className', 'class'], - ['htmlFor', 'for'], - ['httpEquiv', 'http-equiv'] - ].forEach(function (s) { - var o = s[0]; - V[o] = new v(o, 1, !1, s[1], null, !1, !1); - }), - ['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(function (s) { - V[s] = new v(s, 2, !1, s.toLowerCase(), null, !1, !1); - }), - ['autoReverse', 'externalResourcesRequired', 'focusable', 'preserveAlpha'].forEach( - function (s) { - V[s] = new v(s, 2, !1, s, null, !1, !1); - } - ), - 'allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope' - .split(' ') - .forEach(function (s) { - V[s] = new v(s, 3, !1, s.toLowerCase(), null, !1, !1); - }), - ['checked', 'multiple', 'muted', 'selected'].forEach(function (s) { - V[s] = new v(s, 3, !0, s, null, !1, !1); - }), - ['capture', 'download'].forEach(function (s) { - V[s] = new v(s, 4, !1, s, null, !1, !1); - }), - ['cols', 'rows', 'size', 'span'].forEach(function (s) { - V[s] = new v(s, 6, !1, s, null, !1, !1); - }), - ['rowSpan', 'start'].forEach(function (s) { - V[s] = new v(s, 5, !1, s.toLowerCase(), null, !1, !1); - }); - var U = /[\-:]([a-z])/g; - function sa(s) { - return s[1].toUpperCase(); - } - function ta(s, o, i, u) { - var _ = V.hasOwnProperty(o) ? V[o] : null; - (null !== _ - ? 0 !== _.type - : u || - !(2 < o.length) || - ('o' !== o[0] && 'O' !== o[0]) || - ('n' !== o[1] && 'N' !== o[1])) && - ((function qa(s, o, i, u) { - if ( - null == o || - (function pa(s, o, i, u) { - if (null !== i && 0 === i.type) return !1; - switch (typeof o) { - case 'function': - case 'symbol': - return !0; - case 'boolean': - return ( - !u && - (null !== i - ? !i.acceptsBooleans - : 'data-' !== (s = s.toLowerCase().slice(0, 5)) && 'aria-' !== s) - ); - default: - return !1; - } - })(s, o, i, u) - ) - return !0; - if (u) return !1; - if (null !== i) - switch (i.type) { - case 3: - return !o; - case 4: - return !1 === o; - case 5: - return isNaN(o); - case 6: - return isNaN(o) || 1 > o; - } - return !1; - })(o, i, _, u) && (i = null), - u || null === _ - ? (function oa(s) { - return ( - !!j.call($, s) || - (!j.call(B, s) && (L.test(s) ? ($[s] = !0) : ((B[s] = !0), !1))) - ); - })(o) && (null === i ? s.removeAttribute(o) : s.setAttribute(o, '' + i)) - : _.mustUseProperty - ? (s[_.propertyName] = null === i ? 3 !== _.type && '' : i) - : ((o = _.attributeName), - (u = _.attributeNamespace), - null === i - ? s.removeAttribute(o) - : ((i = 3 === (_ = _.type) || (4 === _ && !0 === i) ? '' : '' + i), - u ? s.setAttributeNS(u, o, i) : s.setAttribute(o, i)))); - } - 'accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height' - .split(' ') - .forEach(function (s) { - var o = s.replace(U, sa); - V[o] = new v(o, 1, !1, s, null, !1, !1); - }), - 'xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type' - .split(' ') - .forEach(function (s) { - var o = s.replace(U, sa); - V[o] = new v(o, 1, !1, s, 'http://www.w3.org/1999/xlink', !1, !1); - }), - ['xml:base', 'xml:lang', 'xml:space'].forEach(function (s) { - var o = s.replace(U, sa); - V[o] = new v(o, 1, !1, s, 'http://www.w3.org/XML/1998/namespace', !1, !1); - }), - ['tabIndex', 'crossOrigin'].forEach(function (s) { - V[s] = new v(s, 1, !1, s.toLowerCase(), null, !1, !1); - }), - (V.xlinkHref = new v( - 'xlinkHref', - 1, - !1, - 'xlink:href', - 'http://www.w3.org/1999/xlink', - !0, - !1 - )), - ['src', 'href', 'action', 'formAction'].forEach(function (s) { - V[s] = new v(s, 1, !1, s.toLowerCase(), null, !0, !0); - }); - var z = u.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, - Y = Symbol.for('react.element'), - Z = Symbol.for('react.portal'), - ee = Symbol.for('react.fragment'), - ie = Symbol.for('react.strict_mode'), - ae = Symbol.for('react.profiler'), - le = Symbol.for('react.provider'), - ce = Symbol.for('react.context'), - pe = Symbol.for('react.forward_ref'), - de = Symbol.for('react.suspense'), - fe = Symbol.for('react.suspense_list'), - ye = Symbol.for('react.memo'), - be = Symbol.for('react.lazy'); - Symbol.for('react.scope'), Symbol.for('react.debug_trace_mode'); - var _e = Symbol.for('react.offscreen'); - Symbol.for('react.legacy_hidden'), - Symbol.for('react.cache'), - Symbol.for('react.tracing_marker'); - var we = Symbol.iterator; - function Ka(s) { - return null === s || 'object' != typeof s - ? null - : 'function' == typeof (s = (we && s[we]) || s['@@iterator']) - ? s - : null; - } - var Se, - xe = Object.assign; - function Ma(s) { - if (void 0 === Se) - try { - throw Error(); - } catch (s) { - var o = s.stack.trim().match(/\n( *(at )?)/); - Se = (o && o[1]) || ''; - } - return '\n' + Se + s; - } - var Pe = !1; - function Oa(s, o) { - if (!s || Pe) return ''; - Pe = !0; - var i = Error.prepareStackTrace; - Error.prepareStackTrace = void 0; - try { - if (o) - if ( - ((o = function () { - throw Error(); - }), - Object.defineProperty(o.prototype, 'props', { - set: function () { - throw Error(); - } - }), - 'object' == typeof Reflect && Reflect.construct) - ) { - try { - Reflect.construct(o, []); - } catch (s) { - var u = s; - } - Reflect.construct(s, [], o); - } else { - try { - o.call(); - } catch (s) { - u = s; - } - s.call(o.prototype); - } - else { - try { - throw Error(); - } catch (s) { - u = s; - } - s(); - } - } catch (o) { - if (o && u && 'string' == typeof o.stack) { - for ( - var _ = o.stack.split('\n'), - w = u.stack.split('\n'), - x = _.length - 1, - C = w.length - 1; - 1 <= x && 0 <= C && _[x] !== w[C]; - - ) - C--; - for (; 1 <= x && 0 <= C; x--, C--) - if (_[x] !== w[C]) { - if (1 !== x || 1 !== C) - do { - if ((x--, 0 > --C || _[x] !== w[C])) { - var j = '\n' + _[x].replace(' at new ', ' at '); - return ( - s.displayName && - j.includes('') && - (j = j.replace('', s.displayName)), - j - ); - } - } while (1 <= x && 0 <= C); - break; - } - } - } finally { - (Pe = !1), (Error.prepareStackTrace = i); - } - return (s = s ? s.displayName || s.name : '') ? Ma(s) : ''; - } - function Pa(s) { - switch (s.tag) { - case 5: - return Ma(s.type); - case 16: - return Ma('Lazy'); - case 13: - return Ma('Suspense'); - case 19: - return Ma('SuspenseList'); - case 0: - case 2: - case 15: - return (s = Oa(s.type, !1)); - case 11: - return (s = Oa(s.type.render, !1)); - case 1: - return (s = Oa(s.type, !0)); - default: - return ''; - } - } - function Qa(s) { - if (null == s) return null; - if ('function' == typeof s) return s.displayName || s.name || null; - if ('string' == typeof s) return s; - switch (s) { - case ee: - return 'Fragment'; - case Z: - return 'Portal'; - case ae: - return 'Profiler'; - case ie: - return 'StrictMode'; - case de: - return 'Suspense'; - case fe: - return 'SuspenseList'; - } - if ('object' == typeof s) - switch (s.$$typeof) { - case ce: - return (s.displayName || 'Context') + '.Consumer'; - case le: - return (s._context.displayName || 'Context') + '.Provider'; - case pe: - var o = s.render; - return ( - (s = s.displayName) || - (s = - '' !== (s = o.displayName || o.name || '') - ? 'ForwardRef(' + s + ')' - : 'ForwardRef'), - s - ); - case ye: - return null !== (o = s.displayName || null) ? o : Qa(s.type) || 'Memo'; - case be: - (o = s._payload), (s = s._init); - try { - return Qa(s(o)); - } catch (s) {} - } - return null; - } - function Ra(s) { - var o = s.type; - switch (s.tag) { - case 24: - return 'Cache'; - case 9: - return (o.displayName || 'Context') + '.Consumer'; - case 10: - return (o._context.displayName || 'Context') + '.Provider'; - case 18: - return 'DehydratedFragment'; - case 11: - return ( - (s = (s = o.render).displayName || s.name || ''), - o.displayName || ('' !== s ? 'ForwardRef(' + s + ')' : 'ForwardRef') - ); - case 7: - return 'Fragment'; - case 5: - return o; - case 4: - return 'Portal'; - case 3: - return 'Root'; - case 6: - return 'Text'; - case 16: - return Qa(o); - case 8: - return o === ie ? 'StrictMode' : 'Mode'; - case 22: - return 'Offscreen'; - case 12: - return 'Profiler'; - case 21: - return 'Scope'; - case 13: - return 'Suspense'; - case 19: - return 'SuspenseList'; - case 25: - return 'TracingMarker'; - case 1: - case 0: - case 17: - case 2: - case 14: - case 15: - if ('function' == typeof o) return o.displayName || o.name || null; - if ('string' == typeof o) return o; - } - return null; - } - function Sa(s) { - switch (typeof s) { - case 'boolean': - case 'number': - case 'string': - case 'undefined': - case 'object': - return s; - default: - return ''; - } - } - function Ta(s) { - var o = s.type; - return ( - (s = s.nodeName) && 'input' === s.toLowerCase() && ('checkbox' === o || 'radio' === o) - ); - } - function Va(s) { - s._valueTracker || - (s._valueTracker = (function Ua(s) { - var o = Ta(s) ? 'checked' : 'value', - i = Object.getOwnPropertyDescriptor(s.constructor.prototype, o), - u = '' + s[o]; - if ( - !s.hasOwnProperty(o) && - void 0 !== i && - 'function' == typeof i.get && - 'function' == typeof i.set - ) { - var _ = i.get, - w = i.set; - return ( - Object.defineProperty(s, o, { - configurable: !0, - get: function () { - return _.call(this); - }, - set: function (s) { - (u = '' + s), w.call(this, s); - } - }), - Object.defineProperty(s, o, { enumerable: i.enumerable }), - { - getValue: function () { - return u; - }, - setValue: function (s) { - u = '' + s; - }, - stopTracking: function () { - (s._valueTracker = null), delete s[o]; - } - } - ); - } - })(s)); - } - function Wa(s) { - if (!s) return !1; - var o = s._valueTracker; - if (!o) return !0; - var i = o.getValue(), - u = ''; - return ( - s && (u = Ta(s) ? (s.checked ? 'true' : 'false') : s.value), - (s = u) !== i && (o.setValue(s), !0) - ); - } - function Xa(s) { - if (void 0 === (s = s || ('undefined' != typeof document ? document : void 0))) - return null; - try { - return s.activeElement || s.body; - } catch (o) { - return s.body; - } - } - function Ya(s, o) { - var i = o.checked; - return xe({}, o, { - defaultChecked: void 0, - defaultValue: void 0, - value: void 0, - checked: null != i ? i : s._wrapperState.initialChecked - }); - } - function Za(s, o) { - var i = null == o.defaultValue ? '' : o.defaultValue, - u = null != o.checked ? o.checked : o.defaultChecked; - (i = Sa(null != o.value ? o.value : i)), - (s._wrapperState = { - initialChecked: u, - initialValue: i, - controlled: - 'checkbox' === o.type || 'radio' === o.type ? null != o.checked : null != o.value - }); - } - function ab(s, o) { - null != (o = o.checked) && ta(s, 'checked', o, !1); - } - function bb(s, o) { - ab(s, o); - var i = Sa(o.value), - u = o.type; - if (null != i) - 'number' === u - ? ((0 === i && '' === s.value) || s.value != i) && (s.value = '' + i) - : s.value !== '' + i && (s.value = '' + i); - else if ('submit' === u || 'reset' === u) return void s.removeAttribute('value'); - o.hasOwnProperty('value') - ? cb(s, o.type, i) - : o.hasOwnProperty('defaultValue') && cb(s, o.type, Sa(o.defaultValue)), - null == o.checked && - null != o.defaultChecked && - (s.defaultChecked = !!o.defaultChecked); - } - function db(s, o, i) { - if (o.hasOwnProperty('value') || o.hasOwnProperty('defaultValue')) { - var u = o.type; - if (!(('submit' !== u && 'reset' !== u) || (void 0 !== o.value && null !== o.value))) - return; - (o = '' + s._wrapperState.initialValue), - i || o === s.value || (s.value = o), - (s.defaultValue = o); - } - '' !== (i = s.name) && (s.name = ''), - (s.defaultChecked = !!s._wrapperState.initialChecked), - '' !== i && (s.name = i); - } - function cb(s, o, i) { - ('number' === o && Xa(s.ownerDocument) === s) || - (null == i - ? (s.defaultValue = '' + s._wrapperState.initialValue) - : s.defaultValue !== '' + i && (s.defaultValue = '' + i)); - } - var Te = Array.isArray; - function fb(s, o, i, u) { - if (((s = s.options), o)) { - o = {}; - for (var _ = 0; _ < i.length; _++) o['$' + i[_]] = !0; - for (i = 0; i < s.length; i++) - (_ = o.hasOwnProperty('$' + s[i].value)), - s[i].selected !== _ && (s[i].selected = _), - _ && u && (s[i].defaultSelected = !0); - } else { - for (i = '' + Sa(i), o = null, _ = 0; _ < s.length; _++) { - if (s[_].value === i) - return (s[_].selected = !0), void (u && (s[_].defaultSelected = !0)); - null !== o || s[_].disabled || (o = s[_]); - } - null !== o && (o.selected = !0); - } - } - function gb(s, o) { - if (null != o.dangerouslySetInnerHTML) throw Error(p(91)); - return xe({}, o, { - value: void 0, - defaultValue: void 0, - children: '' + s._wrapperState.initialValue - }); - } - function hb(s, o) { - var i = o.value; - if (null == i) { - if (((i = o.children), (o = o.defaultValue), null != i)) { - if (null != o) throw Error(p(92)); - if (Te(i)) { - if (1 < i.length) throw Error(p(93)); - i = i[0]; - } - o = i; - } - null == o && (o = ''), (i = o); - } - s._wrapperState = { initialValue: Sa(i) }; - } - function ib(s, o) { - var i = Sa(o.value), - u = Sa(o.defaultValue); - null != i && - ((i = '' + i) !== s.value && (s.value = i), - null == o.defaultValue && s.defaultValue !== i && (s.defaultValue = i)), - null != u && (s.defaultValue = '' + u); - } - function jb(s) { - var o = s.textContent; - o === s._wrapperState.initialValue && '' !== o && null !== o && (s.value = o); - } - function kb(s) { - switch (s) { - case 'svg': - return 'http://www.w3.org/2000/svg'; - case 'math': - return 'http://www.w3.org/1998/Math/MathML'; - default: - return 'http://www.w3.org/1999/xhtml'; - } - } - function lb(s, o) { - return null == s || 'http://www.w3.org/1999/xhtml' === s - ? kb(o) - : 'http://www.w3.org/2000/svg' === s && 'foreignObject' === o - ? 'http://www.w3.org/1999/xhtml' - : s; - } - var Re, - qe, - $e = - ((qe = function (s, o) { - if ('http://www.w3.org/2000/svg' !== s.namespaceURI || 'innerHTML' in s) - s.innerHTML = o; - else { - for ( - (Re = Re || document.createElement('div')).innerHTML = - '' + o.valueOf().toString() + '', - o = Re.firstChild; - s.firstChild; - - ) - s.removeChild(s.firstChild); - for (; o.firstChild; ) s.appendChild(o.firstChild); - } - }), - 'undefined' != typeof MSApp && MSApp.execUnsafeLocalFunction - ? function (s, o, i, u) { - MSApp.execUnsafeLocalFunction(function () { - return qe(s, o); - }); - } - : qe); - function ob(s, o) { - if (o) { - var i = s.firstChild; - if (i && i === s.lastChild && 3 === i.nodeType) return void (i.nodeValue = o); - } - s.textContent = o; - } - var ze = { - animationIterationCount: !0, - aspectRatio: !0, - borderImageOutset: !0, - borderImageSlice: !0, - borderImageWidth: !0, - boxFlex: !0, - boxFlexGroup: !0, - boxOrdinalGroup: !0, - columnCount: !0, - columns: !0, - flex: !0, - flexGrow: !0, - flexPositive: !0, - flexShrink: !0, - flexNegative: !0, - flexOrder: !0, - gridArea: !0, - gridRow: !0, - gridRowEnd: !0, - gridRowSpan: !0, - gridRowStart: !0, - gridColumn: !0, - gridColumnEnd: !0, - gridColumnSpan: !0, - gridColumnStart: !0, - fontWeight: !0, - lineClamp: !0, - lineHeight: !0, - opacity: !0, - order: !0, - orphans: !0, - tabSize: !0, - widows: !0, - zIndex: !0, - zoom: !0, - fillOpacity: !0, - floodOpacity: !0, - stopOpacity: !0, - strokeDasharray: !0, - strokeDashoffset: !0, - strokeMiterlimit: !0, - strokeOpacity: !0, - strokeWidth: !0 - }, - We = ['Webkit', 'ms', 'Moz', 'O']; - function rb(s, o, i) { - return null == o || 'boolean' == typeof o || '' === o - ? '' - : i || 'number' != typeof o || 0 === o || (ze.hasOwnProperty(s) && ze[s]) - ? ('' + o).trim() - : o + 'px'; - } - function sb(s, o) { - for (var i in ((s = s.style), o)) - if (o.hasOwnProperty(i)) { - var u = 0 === i.indexOf('--'), - _ = rb(i, o[i], u); - 'float' === i && (i = 'cssFloat'), u ? s.setProperty(i, _) : (s[i] = _); - } - } - Object.keys(ze).forEach(function (s) { - We.forEach(function (o) { - (o = o + s.charAt(0).toUpperCase() + s.substring(1)), (ze[o] = ze[s]); - }); - }); - var He = xe( - { menuitem: !0 }, - { - area: !0, - base: !0, - br: !0, - col: !0, - embed: !0, - hr: !0, - img: !0, - input: !0, - keygen: !0, - link: !0, - meta: !0, - param: !0, - source: !0, - track: !0, - wbr: !0 - } - ); - function ub(s, o) { - if (o) { - if (He[s] && (null != o.children || null != o.dangerouslySetInnerHTML)) - throw Error(p(137, s)); - if (null != o.dangerouslySetInnerHTML) { - if (null != o.children) throw Error(p(60)); - if ( - 'object' != typeof o.dangerouslySetInnerHTML || - !('__html' in o.dangerouslySetInnerHTML) - ) - throw Error(p(61)); - } - if (null != o.style && 'object' != typeof o.style) throw Error(p(62)); - } - } - function vb(s, o) { - if (-1 === s.indexOf('-')) return 'string' == typeof o.is; - switch (s) { - case 'annotation-xml': - case 'color-profile': - case 'font-face': - case 'font-face-src': - case 'font-face-uri': - case 'font-face-format': - case 'font-face-name': - case 'missing-glyph': - return !1; - default: - return !0; - } - } - var Ye = null; - function xb(s) { - return ( - (s = s.target || s.srcElement || window).correspondingUseElement && - (s = s.correspondingUseElement), - 3 === s.nodeType ? s.parentNode : s - ); - } - var Xe = null, - Qe = null, - et = null; - function Bb(s) { - if ((s = Cb(s))) { - if ('function' != typeof Xe) throw Error(p(280)); - var o = s.stateNode; - o && ((o = Db(o)), Xe(s.stateNode, s.type, o)); - } - } - function Eb(s) { - Qe ? (et ? et.push(s) : (et = [s])) : (Qe = s); - } - function Fb() { - if (Qe) { - var s = Qe, - o = et; - if (((et = Qe = null), Bb(s), o)) for (s = 0; s < o.length; s++) Bb(o[s]); - } - } - function Gb(s, o) { - return s(o); - } - function Hb() {} - var tt = !1; - function Jb(s, o, i) { - if (tt) return s(o, i); - tt = !0; - try { - return Gb(s, o, i); - } finally { - (tt = !1), (null !== Qe || null !== et) && (Hb(), Fb()); - } - } - function Kb(s, o) { - var i = s.stateNode; - if (null === i) return null; - var u = Db(i); - if (null === u) return null; - i = u[o]; - e: switch (o) { - case 'onClick': - case 'onClickCapture': - case 'onDoubleClick': - case 'onDoubleClickCapture': - case 'onMouseDown': - case 'onMouseDownCapture': - case 'onMouseMove': - case 'onMouseMoveCapture': - case 'onMouseUp': - case 'onMouseUpCapture': - case 'onMouseEnter': - (u = !u.disabled) || - (u = !( - 'button' === (s = s.type) || - 'input' === s || - 'select' === s || - 'textarea' === s - )), - (s = !u); - break e; - default: - s = !1; - } - if (s) return null; - if (i && 'function' != typeof i) throw Error(p(231, o, typeof i)); - return i; - } - var rt = !1; - if (C) - try { - var nt = {}; - Object.defineProperty(nt, 'passive', { - get: function () { - rt = !0; - } - }), - window.addEventListener('test', nt, nt), - window.removeEventListener('test', nt, nt); - } catch (qe) { - rt = !1; - } - function Nb(s, o, i, u, _, w, x, C, j) { - var L = Array.prototype.slice.call(arguments, 3); - try { - o.apply(i, L); - } catch (s) { - this.onError(s); - } - } - var st = !1, - ot = null, - it = !1, - at = null, - lt = { - onError: function (s) { - (st = !0), (ot = s); - } - }; - function Tb(s, o, i, u, _, w, x, C, j) { - (st = !1), (ot = null), Nb.apply(lt, arguments); - } - function Vb(s) { - var o = s, - i = s; - if (s.alternate) for (; o.return; ) o = o.return; - else { - s = o; - do { - !!(4098 & (o = s).flags) && (i = o.return), (s = o.return); - } while (s); - } - return 3 === o.tag ? i : null; - } - function Wb(s) { - if (13 === s.tag) { - var o = s.memoizedState; - if ((null === o && null !== (s = s.alternate) && (o = s.memoizedState), null !== o)) - return o.dehydrated; - } - return null; - } - function Xb(s) { - if (Vb(s) !== s) throw Error(p(188)); - } - function Zb(s) { - return null !== - (s = (function Yb(s) { - var o = s.alternate; - if (!o) { - if (null === (o = Vb(s))) throw Error(p(188)); - return o !== s ? null : s; - } - for (var i = s, u = o; ; ) { - var _ = i.return; - if (null === _) break; - var w = _.alternate; - if (null === w) { - if (null !== (u = _.return)) { - i = u; - continue; - } - break; - } - if (_.child === w.child) { - for (w = _.child; w; ) { - if (w === i) return Xb(_), s; - if (w === u) return Xb(_), o; - w = w.sibling; - } - throw Error(p(188)); - } - if (i.return !== u.return) (i = _), (u = w); - else { - for (var x = !1, C = _.child; C; ) { - if (C === i) { - (x = !0), (i = _), (u = w); - break; - } - if (C === u) { - (x = !0), (u = _), (i = w); - break; - } - C = C.sibling; - } - if (!x) { - for (C = w.child; C; ) { - if (C === i) { - (x = !0), (i = w), (u = _); - break; - } - if (C === u) { - (x = !0), (u = w), (i = _); - break; - } - C = C.sibling; - } - if (!x) throw Error(p(189)); - } - } - if (i.alternate !== u) throw Error(p(190)); - } - if (3 !== i.tag) throw Error(p(188)); - return i.stateNode.current === i ? s : o; - })(s)) - ? $b(s) - : null; - } - function $b(s) { - if (5 === s.tag || 6 === s.tag) return s; - for (s = s.child; null !== s; ) { - var o = $b(s); - if (null !== o) return o; - s = s.sibling; - } - return null; - } - var ct = _.unstable_scheduleCallback, - ut = _.unstable_cancelCallback, - pt = _.unstable_shouldYield, - ht = _.unstable_requestPaint, - dt = _.unstable_now, - mt = _.unstable_getCurrentPriorityLevel, - gt = _.unstable_ImmediatePriority, - yt = _.unstable_UserBlockingPriority, - vt = _.unstable_NormalPriority, - bt = _.unstable_LowPriority, - _t = _.unstable_IdlePriority, - Et = null, - wt = null; - var St = Math.clz32 - ? Math.clz32 - : function nc(s) { - return (s >>>= 0), 0 === s ? 32 : (31 - ((xt(s) / kt) | 0)) | 0; - }, - xt = Math.log, - kt = Math.LN2; - var Ct = 64, - Ot = 4194304; - function tc(s) { - switch (s & -s) { - case 1: - return 1; - case 2: - return 2; - case 4: - return 4; - case 8: - return 8; - case 16: - return 16; - case 32: - return 32; - case 64: - case 128: - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - case 16384: - case 32768: - case 65536: - case 131072: - case 262144: - case 524288: - case 1048576: - case 2097152: - return 4194240 & s; - case 4194304: - case 8388608: - case 16777216: - case 33554432: - case 67108864: - return 130023424 & s; - case 134217728: - return 134217728; - case 268435456: - return 268435456; - case 536870912: - return 536870912; - case 1073741824: - return 1073741824; - default: - return s; - } - } - function uc(s, o) { - var i = s.pendingLanes; - if (0 === i) return 0; - var u = 0, - _ = s.suspendedLanes, - w = s.pingedLanes, - x = 268435455 & i; - if (0 !== x) { - var C = x & ~_; - 0 !== C ? (u = tc(C)) : 0 !== (w &= x) && (u = tc(w)); - } else 0 !== (x = i & ~_) ? (u = tc(x)) : 0 !== w && (u = tc(w)); - if (0 === u) return 0; - if ( - 0 !== o && - o !== u && - !(o & _) && - ((_ = u & -u) >= (w = o & -o) || (16 === _ && 4194240 & w)) - ) - return o; - if ((4 & u && (u |= 16 & i), 0 !== (o = s.entangledLanes))) - for (s = s.entanglements, o &= u; 0 < o; ) - (_ = 1 << (i = 31 - St(o))), (u |= s[i]), (o &= ~_); - return u; - } - function vc(s, o) { - switch (s) { - case 1: - case 2: - case 4: - return o + 250; - case 8: - case 16: - case 32: - case 64: - case 128: - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - case 16384: - case 32768: - case 65536: - case 131072: - case 262144: - case 524288: - case 1048576: - case 2097152: - return o + 5e3; - default: - return -1; - } - } - function xc(s) { - return 0 !== (s = -1073741825 & s.pendingLanes) ? s : 1073741824 & s ? 1073741824 : 0; - } - function yc() { - var s = Ct; - return !(4194240 & (Ct <<= 1)) && (Ct = 64), s; - } - function zc(s) { - for (var o = [], i = 0; 31 > i; i++) o.push(s); - return o; - } - function Ac(s, o, i) { - (s.pendingLanes |= o), - 536870912 !== o && ((s.suspendedLanes = 0), (s.pingedLanes = 0)), - ((s = s.eventTimes)[(o = 31 - St(o))] = i); - } - function Cc(s, o) { - var i = (s.entangledLanes |= o); - for (s = s.entanglements; i; ) { - var u = 31 - St(i), - _ = 1 << u; - (_ & o) | (s[u] & o) && (s[u] |= o), (i &= ~_); - } - } - var At = 0; - function Dc(s) { - return 1 < (s &= -s) ? (4 < s ? (268435455 & s ? 16 : 536870912) : 4) : 1; - } - var jt, - It, - Pt, - Mt, - Tt, - Nt = !1, - Rt = [], - Dt = null, - Lt = null, - Bt = null, - Ft = new Map(), - qt = new Map(), - $t = [], - Vt = - 'mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit'.split( - ' ' - ); - function Sc(s, o) { - switch (s) { - case 'focusin': - case 'focusout': - Dt = null; - break; - case 'dragenter': - case 'dragleave': - Lt = null; - break; - case 'mouseover': - case 'mouseout': - Bt = null; - break; - case 'pointerover': - case 'pointerout': - Ft.delete(o.pointerId); - break; - case 'gotpointercapture': - case 'lostpointercapture': - qt.delete(o.pointerId); - } - } - function Tc(s, o, i, u, _, w) { - return null === s || s.nativeEvent !== w - ? ((s = { - blockedOn: o, - domEventName: i, - eventSystemFlags: u, - nativeEvent: w, - targetContainers: [_] - }), - null !== o && null !== (o = Cb(o)) && It(o), - s) - : ((s.eventSystemFlags |= u), - (o = s.targetContainers), - null !== _ && -1 === o.indexOf(_) && o.push(_), - s); - } - function Vc(s) { - var o = Wc(s.target); - if (null !== o) { - var i = Vb(o); - if (null !== i) - if (13 === (o = i.tag)) { - if (null !== (o = Wb(i))) - return ( - (s.blockedOn = o), - void Tt(s.priority, function () { - Pt(i); - }) - ); - } else if (3 === o && i.stateNode.current.memoizedState.isDehydrated) - return void (s.blockedOn = 3 === i.tag ? i.stateNode.containerInfo : null); - } - s.blockedOn = null; - } - function Xc(s) { - if (null !== s.blockedOn) return !1; - for (var o = s.targetContainers; 0 < o.length; ) { - var i = Yc(s.domEventName, s.eventSystemFlags, o[0], s.nativeEvent); - if (null !== i) return null !== (o = Cb(i)) && It(o), (s.blockedOn = i), !1; - var u = new (i = s.nativeEvent).constructor(i.type, i); - (Ye = u), i.target.dispatchEvent(u), (Ye = null), o.shift(); - } - return !0; - } - function Zc(s, o, i) { - Xc(s) && i.delete(o); - } - function $c() { - (Nt = !1), - null !== Dt && Xc(Dt) && (Dt = null), - null !== Lt && Xc(Lt) && (Lt = null), - null !== Bt && Xc(Bt) && (Bt = null), - Ft.forEach(Zc), - qt.forEach(Zc); - } - function ad(s, o) { - s.blockedOn === o && - ((s.blockedOn = null), - Nt || ((Nt = !0), _.unstable_scheduleCallback(_.unstable_NormalPriority, $c))); - } - function bd(s) { - function b(o) { - return ad(o, s); - } - if (0 < Rt.length) { - ad(Rt[0], s); - for (var o = 1; o < Rt.length; o++) { - var i = Rt[o]; - i.blockedOn === s && (i.blockedOn = null); - } - } - for ( - null !== Dt && ad(Dt, s), - null !== Lt && ad(Lt, s), - null !== Bt && ad(Bt, s), - Ft.forEach(b), - qt.forEach(b), - o = 0; - o < $t.length; - o++ - ) - (i = $t[o]).blockedOn === s && (i.blockedOn = null); - for (; 0 < $t.length && null === (o = $t[0]).blockedOn; ) - Vc(o), null === o.blockedOn && $t.shift(); - } - var Ut = z.ReactCurrentBatchConfig, - zt = !0; - function ed(s, o, i, u) { - var _ = At, - w = Ut.transition; - Ut.transition = null; - try { - (At = 1), fd(s, o, i, u); - } finally { - (At = _), (Ut.transition = w); - } - } - function gd(s, o, i, u) { - var _ = At, - w = Ut.transition; - Ut.transition = null; - try { - (At = 4), fd(s, o, i, u); - } finally { - (At = _), (Ut.transition = w); - } - } - function fd(s, o, i, u) { - if (zt) { - var _ = Yc(s, o, i, u); - if (null === _) hd(s, o, u, Wt, i), Sc(s, u); - else if ( - (function Uc(s, o, i, u, _) { - switch (o) { - case 'focusin': - return (Dt = Tc(Dt, s, o, i, u, _)), !0; - case 'dragenter': - return (Lt = Tc(Lt, s, o, i, u, _)), !0; - case 'mouseover': - return (Bt = Tc(Bt, s, o, i, u, _)), !0; - case 'pointerover': - var w = _.pointerId; - return Ft.set(w, Tc(Ft.get(w) || null, s, o, i, u, _)), !0; - case 'gotpointercapture': - return (w = _.pointerId), qt.set(w, Tc(qt.get(w) || null, s, o, i, u, _)), !0; - } - return !1; - })(_, s, o, i, u) - ) - u.stopPropagation(); - else if ((Sc(s, u), 4 & o && -1 < Vt.indexOf(s))) { - for (; null !== _; ) { - var w = Cb(_); - if ( - (null !== w && jt(w), - null === (w = Yc(s, o, i, u)) && hd(s, o, u, Wt, i), - w === _) - ) - break; - _ = w; - } - null !== _ && u.stopPropagation(); - } else hd(s, o, u, null, i); - } - } - var Wt = null; - function Yc(s, o, i, u) { - if (((Wt = null), null !== (s = Wc((s = xb(u)))))) - if (null === (o = Vb(s))) s = null; - else if (13 === (i = o.tag)) { - if (null !== (s = Wb(o))) return s; - s = null; - } else if (3 === i) { - if (o.stateNode.current.memoizedState.isDehydrated) - return 3 === o.tag ? o.stateNode.containerInfo : null; - s = null; - } else o !== s && (s = null); - return (Wt = s), null; - } - function jd(s) { - switch (s) { - case 'cancel': - case 'click': - case 'close': - case 'contextmenu': - case 'copy': - case 'cut': - case 'auxclick': - case 'dblclick': - case 'dragend': - case 'dragstart': - case 'drop': - case 'focusin': - case 'focusout': - case 'input': - case 'invalid': - case 'keydown': - case 'keypress': - case 'keyup': - case 'mousedown': - case 'mouseup': - case 'paste': - case 'pause': - case 'play': - case 'pointercancel': - case 'pointerdown': - case 'pointerup': - case 'ratechange': - case 'reset': - case 'resize': - case 'seeked': - case 'submit': - case 'touchcancel': - case 'touchend': - case 'touchstart': - case 'volumechange': - case 'change': - case 'selectionchange': - case 'textInput': - case 'compositionstart': - case 'compositionend': - case 'compositionupdate': - case 'beforeblur': - case 'afterblur': - case 'beforeinput': - case 'blur': - case 'fullscreenchange': - case 'focus': - case 'hashchange': - case 'popstate': - case 'select': - case 'selectstart': - return 1; - case 'drag': - case 'dragenter': - case 'dragexit': - case 'dragleave': - case 'dragover': - case 'mousemove': - case 'mouseout': - case 'mouseover': - case 'pointermove': - case 'pointerout': - case 'pointerover': - case 'scroll': - case 'toggle': - case 'touchmove': - case 'wheel': - case 'mouseenter': - case 'mouseleave': - case 'pointerenter': - case 'pointerleave': - return 4; - case 'message': - switch (mt()) { - case gt: - return 1; - case yt: - return 4; - case vt: - case bt: - return 16; - case _t: - return 536870912; - default: - return 16; - } - default: - return 16; - } - } - var Kt = null, - Ht = null, - Jt = null; - function nd() { - if (Jt) return Jt; - var s, - o, - i = Ht, - u = i.length, - _ = 'value' in Kt ? Kt.value : Kt.textContent, - w = _.length; - for (s = 0; s < u && i[s] === _[s]; s++); - var x = u - s; - for (o = 1; o <= x && i[u - o] === _[w - o]; o++); - return (Jt = _.slice(s, 1 < o ? 1 - o : void 0)); - } - function od(s) { - var o = s.keyCode; - return ( - 'charCode' in s ? 0 === (s = s.charCode) && 13 === o && (s = 13) : (s = o), - 10 === s && (s = 13), - 32 <= s || 13 === s ? s : 0 - ); - } - function pd() { - return !0; - } - function qd() { - return !1; - } - function rd(s) { - function b(o, i, u, _, w) { - for (var x in ((this._reactName = o), - (this._targetInst = u), - (this.type = i), - (this.nativeEvent = _), - (this.target = w), - (this.currentTarget = null), - s)) - s.hasOwnProperty(x) && ((o = s[x]), (this[x] = o ? o(_) : _[x])); - return ( - (this.isDefaultPrevented = ( - null != _.defaultPrevented ? _.defaultPrevented : !1 === _.returnValue - ) - ? pd - : qd), - (this.isPropagationStopped = qd), - this - ); - } - return ( - xe(b.prototype, { - preventDefault: function () { - this.defaultPrevented = !0; - var s = this.nativeEvent; - s && - (s.preventDefault - ? s.preventDefault() - : 'unknown' != typeof s.returnValue && (s.returnValue = !1), - (this.isDefaultPrevented = pd)); - }, - stopPropagation: function () { - var s = this.nativeEvent; - s && - (s.stopPropagation - ? s.stopPropagation() - : 'unknown' != typeof s.cancelBubble && (s.cancelBubble = !0), - (this.isPropagationStopped = pd)); - }, - persist: function () {}, - isPersistent: pd - }), - b - ); - } - var Gt, - Yt, - Xt, - Zt = { - eventPhase: 0, - bubbles: 0, - cancelable: 0, - timeStamp: function (s) { - return s.timeStamp || Date.now(); - }, - defaultPrevented: 0, - isTrusted: 0 - }, - Qt = rd(Zt), - er = xe({}, Zt, { view: 0, detail: 0 }), - tr = rd(er), - rr = xe({}, er, { - screenX: 0, - screenY: 0, - clientX: 0, - clientY: 0, - pageX: 0, - pageY: 0, - ctrlKey: 0, - shiftKey: 0, - altKey: 0, - metaKey: 0, - getModifierState: zd, - button: 0, - buttons: 0, - relatedTarget: function (s) { - return void 0 === s.relatedTarget - ? s.fromElement === s.srcElement - ? s.toElement - : s.fromElement - : s.relatedTarget; - }, - movementX: function (s) { - return 'movementX' in s - ? s.movementX - : (s !== Xt && - (Xt && 'mousemove' === s.type - ? ((Gt = s.screenX - Xt.screenX), (Yt = s.screenY - Xt.screenY)) - : (Yt = Gt = 0), - (Xt = s)), - Gt); - }, - movementY: function (s) { - return 'movementY' in s ? s.movementY : Yt; - } - }), - nr = rd(rr), - sr = rd(xe({}, rr, { dataTransfer: 0 })), - ir = rd(xe({}, er, { relatedTarget: 0 })), - ar = rd(xe({}, Zt, { animationName: 0, elapsedTime: 0, pseudoElement: 0 })), - lr = xe({}, Zt, { - clipboardData: function (s) { - return 'clipboardData' in s ? s.clipboardData : window.clipboardData; - } - }), - cr = rd(lr), - ur = rd(xe({}, Zt, { data: 0 })), - pr = { - Esc: 'Escape', - Spacebar: ' ', - Left: 'ArrowLeft', - Up: 'ArrowUp', - Right: 'ArrowRight', - Down: 'ArrowDown', - Del: 'Delete', - Win: 'OS', - Menu: 'ContextMenu', - Apps: 'ContextMenu', - Scroll: 'ScrollLock', - MozPrintableKey: 'Unidentified' - }, - dr = { - 8: 'Backspace', - 9: 'Tab', - 12: 'Clear', - 13: 'Enter', - 16: 'Shift', - 17: 'Control', - 18: 'Alt', - 19: 'Pause', - 20: 'CapsLock', - 27: 'Escape', - 32: ' ', - 33: 'PageUp', - 34: 'PageDown', - 35: 'End', - 36: 'Home', - 37: 'ArrowLeft', - 38: 'ArrowUp', - 39: 'ArrowRight', - 40: 'ArrowDown', - 45: 'Insert', - 46: 'Delete', - 112: 'F1', - 113: 'F2', - 114: 'F3', - 115: 'F4', - 116: 'F5', - 117: 'F6', - 118: 'F7', - 119: 'F8', - 120: 'F9', - 121: 'F10', - 122: 'F11', - 123: 'F12', - 144: 'NumLock', - 145: 'ScrollLock', - 224: 'Meta' - }, - fr = { Alt: 'altKey', Control: 'ctrlKey', Meta: 'metaKey', Shift: 'shiftKey' }; - function Pd(s) { - var o = this.nativeEvent; - return o.getModifierState ? o.getModifierState(s) : !!(s = fr[s]) && !!o[s]; - } - function zd() { - return Pd; - } - var mr = xe({}, er, { - key: function (s) { - if (s.key) { - var o = pr[s.key] || s.key; - if ('Unidentified' !== o) return o; - } - return 'keypress' === s.type - ? 13 === (s = od(s)) - ? 'Enter' - : String.fromCharCode(s) - : 'keydown' === s.type || 'keyup' === s.type - ? dr[s.keyCode] || 'Unidentified' - : ''; - }, - code: 0, - location: 0, - ctrlKey: 0, - shiftKey: 0, - altKey: 0, - metaKey: 0, - repeat: 0, - locale: 0, - getModifierState: zd, - charCode: function (s) { - return 'keypress' === s.type ? od(s) : 0; - }, - keyCode: function (s) { - return 'keydown' === s.type || 'keyup' === s.type ? s.keyCode : 0; - }, - which: function (s) { - return 'keypress' === s.type - ? od(s) - : 'keydown' === s.type || 'keyup' === s.type - ? s.keyCode - : 0; - } - }), - gr = rd(mr), - yr = rd( - xe({}, rr, { - pointerId: 0, - width: 0, - height: 0, - pressure: 0, - tangentialPressure: 0, - tiltX: 0, - tiltY: 0, - twist: 0, - pointerType: 0, - isPrimary: 0 - }) - ), - vr = rd( - xe({}, er, { - touches: 0, - targetTouches: 0, - changedTouches: 0, - altKey: 0, - metaKey: 0, - ctrlKey: 0, - shiftKey: 0, - getModifierState: zd - }) - ), - br = rd(xe({}, Zt, { propertyName: 0, elapsedTime: 0, pseudoElement: 0 })), - _r = xe({}, rr, { - deltaX: function (s) { - return 'deltaX' in s ? s.deltaX : 'wheelDeltaX' in s ? -s.wheelDeltaX : 0; - }, - deltaY: function (s) { - return 'deltaY' in s - ? s.deltaY - : 'wheelDeltaY' in s - ? -s.wheelDeltaY - : 'wheelDelta' in s - ? -s.wheelDelta - : 0; - }, - deltaZ: 0, - deltaMode: 0 - }), - Er = rd(_r), - wr = [9, 13, 27, 32], - Sr = C && 'CompositionEvent' in window, - xr = null; - C && 'documentMode' in document && (xr = document.documentMode); - var kr = C && 'TextEvent' in window && !xr, - Cr = C && (!Sr || (xr && 8 < xr && 11 >= xr)), - Or = String.fromCharCode(32), - Ar = !1; - function ge(s, o) { - switch (s) { - case 'keyup': - return -1 !== wr.indexOf(o.keyCode); - case 'keydown': - return 229 !== o.keyCode; - case 'keypress': - case 'mousedown': - case 'focusout': - return !0; - default: - return !1; - } - } - function he(s) { - return 'object' == typeof (s = s.detail) && 'data' in s ? s.data : null; - } - var jr = !1; - var Ir = { - color: !0, - date: !0, - datetime: !0, - 'datetime-local': !0, - email: !0, - month: !0, - number: !0, - password: !0, - range: !0, - search: !0, - tel: !0, - text: !0, - time: !0, - url: !0, - week: !0 - }; - function me(s) { - var o = s && s.nodeName && s.nodeName.toLowerCase(); - return 'input' === o ? !!Ir[s.type] : 'textarea' === o; - } - function ne(s, o, i, u) { - Eb(u), - 0 < (o = oe(o, 'onChange')).length && - ((i = new Qt('onChange', 'change', null, i, u)), - s.push({ event: i, listeners: o })); - } - var Pr = null, - Mr = null; - function re(s) { - se(s, 0); - } - function te(s) { - if (Wa(ue(s))) return s; - } - function ve(s, o) { - if ('change' === s) return o; - } - var Tr = !1; - if (C) { - var Nr; - if (C) { - var Rr = 'oninput' in document; - if (!Rr) { - var Dr = document.createElement('div'); - Dr.setAttribute('oninput', 'return;'), (Rr = 'function' == typeof Dr.oninput); - } - Nr = Rr; - } else Nr = !1; - Tr = Nr && (!document.documentMode || 9 < document.documentMode); - } - function Ae() { - Pr && (Pr.detachEvent('onpropertychange', Be), (Mr = Pr = null)); - } - function Be(s) { - if ('value' === s.propertyName && te(Mr)) { - var o = []; - ne(o, Mr, s, xb(s)), Jb(re, o); - } - } - function Ce(s, o, i) { - 'focusin' === s - ? (Ae(), (Mr = i), (Pr = o).attachEvent('onpropertychange', Be)) - : 'focusout' === s && Ae(); - } - function De(s) { - if ('selectionchange' === s || 'keyup' === s || 'keydown' === s) return te(Mr); - } - function Ee(s, o) { - if ('click' === s) return te(o); - } - function Fe(s, o) { - if ('input' === s || 'change' === s) return te(o); - } - var Lr = - 'function' == typeof Object.is - ? Object.is - : function Ge(s, o) { - return (s === o && (0 !== s || 1 / s == 1 / o)) || (s != s && o != o); - }; - function Ie(s, o) { - if (Lr(s, o)) return !0; - if ('object' != typeof s || null === s || 'object' != typeof o || null === o) return !1; - var i = Object.keys(s), - u = Object.keys(o); - if (i.length !== u.length) return !1; - for (u = 0; u < i.length; u++) { - var _ = i[u]; - if (!j.call(o, _) || !Lr(s[_], o[_])) return !1; - } - return !0; - } - function Je(s) { - for (; s && s.firstChild; ) s = s.firstChild; - return s; - } - function Ke(s, o) { - var i, - u = Je(s); - for (s = 0; u; ) { - if (3 === u.nodeType) { - if (((i = s + u.textContent.length), s <= o && i >= o)) - return { node: u, offset: o - s }; - s = i; - } - e: { - for (; u; ) { - if (u.nextSibling) { - u = u.nextSibling; - break e; - } - u = u.parentNode; - } - u = void 0; - } - u = Je(u); - } - } - function Le(s, o) { - return ( - !(!s || !o) && - (s === o || - ((!s || 3 !== s.nodeType) && - (o && 3 === o.nodeType - ? Le(s, o.parentNode) - : 'contains' in s - ? s.contains(o) - : !!s.compareDocumentPosition && !!(16 & s.compareDocumentPosition(o))))) - ); - } - function Me() { - for (var s = window, o = Xa(); o instanceof s.HTMLIFrameElement; ) { - try { - var i = 'string' == typeof o.contentWindow.location.href; - } catch (s) { - i = !1; - } - if (!i) break; - o = Xa((s = o.contentWindow).document); - } - return o; - } - function Ne(s) { - var o = s && s.nodeName && s.nodeName.toLowerCase(); - return ( - o && - (('input' === o && - ('text' === s.type || - 'search' === s.type || - 'tel' === s.type || - 'url' === s.type || - 'password' === s.type)) || - 'textarea' === o || - 'true' === s.contentEditable) - ); - } - function Oe(s) { - var o = Me(), - i = s.focusedElem, - u = s.selectionRange; - if (o !== i && i && i.ownerDocument && Le(i.ownerDocument.documentElement, i)) { - if (null !== u && Ne(i)) - if (((o = u.start), void 0 === (s = u.end) && (s = o), 'selectionStart' in i)) - (i.selectionStart = o), (i.selectionEnd = Math.min(s, i.value.length)); - else if ( - (s = ((o = i.ownerDocument || document) && o.defaultView) || window).getSelection - ) { - s = s.getSelection(); - var _ = i.textContent.length, - w = Math.min(u.start, _); - (u = void 0 === u.end ? w : Math.min(u.end, _)), - !s.extend && w > u && ((_ = u), (u = w), (w = _)), - (_ = Ke(i, w)); - var x = Ke(i, u); - _ && - x && - (1 !== s.rangeCount || - s.anchorNode !== _.node || - s.anchorOffset !== _.offset || - s.focusNode !== x.node || - s.focusOffset !== x.offset) && - ((o = o.createRange()).setStart(_.node, _.offset), - s.removeAllRanges(), - w > u - ? (s.addRange(o), s.extend(x.node, x.offset)) - : (o.setEnd(x.node, x.offset), s.addRange(o))); - } - for (o = [], s = i; (s = s.parentNode); ) - 1 === s.nodeType && o.push({ element: s, left: s.scrollLeft, top: s.scrollTop }); - for ('function' == typeof i.focus && i.focus(), i = 0; i < o.length; i++) - ((s = o[i]).element.scrollLeft = s.left), (s.element.scrollTop = s.top); - } - } - var Br = C && 'documentMode' in document && 11 >= document.documentMode, - Fr = null, - qr = null, - $r = null, - Vr = !1; - function Ue(s, o, i) { - var u = i.window === i ? i.document : 9 === i.nodeType ? i : i.ownerDocument; - Vr || - null == Fr || - Fr !== Xa(u) || - ('selectionStart' in (u = Fr) && Ne(u) - ? (u = { start: u.selectionStart, end: u.selectionEnd }) - : (u = { - anchorNode: (u = ( - (u.ownerDocument && u.ownerDocument.defaultView) || - window - ).getSelection()).anchorNode, - anchorOffset: u.anchorOffset, - focusNode: u.focusNode, - focusOffset: u.focusOffset - }), - ($r && Ie($r, u)) || - (($r = u), - 0 < (u = oe(qr, 'onSelect')).length && - ((o = new Qt('onSelect', 'select', null, o, i)), - s.push({ event: o, listeners: u }), - (o.target = Fr)))); - } - function Ve(s, o) { - var i = {}; - return ( - (i[s.toLowerCase()] = o.toLowerCase()), - (i['Webkit' + s] = 'webkit' + o), - (i['Moz' + s] = 'moz' + o), - i - ); - } - var Ur = { - animationend: Ve('Animation', 'AnimationEnd'), - animationiteration: Ve('Animation', 'AnimationIteration'), - animationstart: Ve('Animation', 'AnimationStart'), - transitionend: Ve('Transition', 'TransitionEnd') - }, - zr = {}, - Wr = {}; - function Ze(s) { - if (zr[s]) return zr[s]; - if (!Ur[s]) return s; - var o, - i = Ur[s]; - for (o in i) if (i.hasOwnProperty(o) && o in Wr) return (zr[s] = i[o]); - return s; - } - C && - ((Wr = document.createElement('div').style), - 'AnimationEvent' in window || - (delete Ur.animationend.animation, - delete Ur.animationiteration.animation, - delete Ur.animationstart.animation), - 'TransitionEvent' in window || delete Ur.transitionend.transition); - var Kr = Ze('animationend'), - Hr = Ze('animationiteration'), - Jr = Ze('animationstart'), - Gr = Ze('transitionend'), - Yr = new Map(), - Xr = - 'abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel'.split( - ' ' - ); - function ff(s, o) { - Yr.set(s, o), fa(o, [s]); - } - for (var Zr = 0; Zr < Xr.length; Zr++) { - var Qr = Xr[Zr]; - ff(Qr.toLowerCase(), 'on' + (Qr[0].toUpperCase() + Qr.slice(1))); - } - ff(Kr, 'onAnimationEnd'), - ff(Hr, 'onAnimationIteration'), - ff(Jr, 'onAnimationStart'), - ff('dblclick', 'onDoubleClick'), - ff('focusin', 'onFocus'), - ff('focusout', 'onBlur'), - ff(Gr, 'onTransitionEnd'), - ha('onMouseEnter', ['mouseout', 'mouseover']), - ha('onMouseLeave', ['mouseout', 'mouseover']), - ha('onPointerEnter', ['pointerout', 'pointerover']), - ha('onPointerLeave', ['pointerout', 'pointerover']), - fa( - 'onChange', - 'change click focusin focusout input keydown keyup selectionchange'.split(' ') - ), - fa( - 'onSelect', - 'focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange'.split( - ' ' - ) - ), - fa('onBeforeInput', ['compositionend', 'keypress', 'textInput', 'paste']), - fa( - 'onCompositionEnd', - 'compositionend focusout keydown keypress keyup mousedown'.split(' ') - ), - fa( - 'onCompositionStart', - 'compositionstart focusout keydown keypress keyup mousedown'.split(' ') - ), - fa( - 'onCompositionUpdate', - 'compositionupdate focusout keydown keypress keyup mousedown'.split(' ') - ); - var en = - 'abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting'.split( - ' ' - ), - tn = new Set('cancel close invalid load scroll toggle'.split(' ').concat(en)); - function nf(s, o, i) { - var u = s.type || 'unknown-event'; - (s.currentTarget = i), - (function Ub(s, o, i, u, _, w, x, C, j) { - if ((Tb.apply(this, arguments), st)) { - if (!st) throw Error(p(198)); - var L = ot; - (st = !1), (ot = null), it || ((it = !0), (at = L)); - } - })(u, o, void 0, s), - (s.currentTarget = null); - } - function se(s, o) { - o = !!(4 & o); - for (var i = 0; i < s.length; i++) { - var u = s[i], - _ = u.event; - u = u.listeners; - e: { - var w = void 0; - if (o) - for (var x = u.length - 1; 0 <= x; x--) { - var C = u[x], - j = C.instance, - L = C.currentTarget; - if (((C = C.listener), j !== w && _.isPropagationStopped())) break e; - nf(_, C, L), (w = j); - } - else - for (x = 0; x < u.length; x++) { - if ( - ((j = (C = u[x]).instance), - (L = C.currentTarget), - (C = C.listener), - j !== w && _.isPropagationStopped()) - ) - break e; - nf(_, C, L), (w = j); - } - } - } - if (it) throw ((s = at), (it = !1), (at = null), s); - } - function D(s, o) { - var i = o[gn]; - void 0 === i && (i = o[gn] = new Set()); - var u = s + '__bubble'; - i.has(u) || (pf(o, s, 2, !1), i.add(u)); - } - function qf(s, o, i) { - var u = 0; - o && (u |= 4), pf(i, s, u, o); - } - var rn = '_reactListening' + Math.random().toString(36).slice(2); - function sf(s) { - if (!s[rn]) { - (s[rn] = !0), - w.forEach(function (o) { - 'selectionchange' !== o && (tn.has(o) || qf(o, !1, s), qf(o, !0, s)); - }); - var o = 9 === s.nodeType ? s : s.ownerDocument; - null === o || o[rn] || ((o[rn] = !0), qf('selectionchange', !1, o)); - } - } - function pf(s, o, i, u) { - switch (jd(o)) { - case 1: - var _ = ed; - break; - case 4: - _ = gd; - break; - default: - _ = fd; - } - (i = _.bind(null, o, i, s)), - (_ = void 0), - !rt || ('touchstart' !== o && 'touchmove' !== o && 'wheel' !== o) || (_ = !0), - u - ? void 0 !== _ - ? s.addEventListener(o, i, { capture: !0, passive: _ }) - : s.addEventListener(o, i, !0) - : void 0 !== _ - ? s.addEventListener(o, i, { passive: _ }) - : s.addEventListener(o, i, !1); - } - function hd(s, o, i, u, _) { - var w = u; - if (!(1 & o || 2 & o || null === u)) - e: for (;;) { - if (null === u) return; - var x = u.tag; - if (3 === x || 4 === x) { - var C = u.stateNode.containerInfo; - if (C === _ || (8 === C.nodeType && C.parentNode === _)) break; - if (4 === x) - for (x = u.return; null !== x; ) { - var j = x.tag; - if ( - (3 === j || 4 === j) && - ((j = x.stateNode.containerInfo) === _ || - (8 === j.nodeType && j.parentNode === _)) - ) - return; - x = x.return; - } - for (; null !== C; ) { - if (null === (x = Wc(C))) return; - if (5 === (j = x.tag) || 6 === j) { - u = w = x; - continue e; - } - C = C.parentNode; - } - } - u = u.return; - } - Jb(function () { - var u = w, - _ = xb(i), - x = []; - e: { - var C = Yr.get(s); - if (void 0 !== C) { - var j = Qt, - L = s; - switch (s) { - case 'keypress': - if (0 === od(i)) break e; - case 'keydown': - case 'keyup': - j = gr; - break; - case 'focusin': - (L = 'focus'), (j = ir); - break; - case 'focusout': - (L = 'blur'), (j = ir); - break; - case 'beforeblur': - case 'afterblur': - j = ir; - break; - case 'click': - if (2 === i.button) break e; - case 'auxclick': - case 'dblclick': - case 'mousedown': - case 'mousemove': - case 'mouseup': - case 'mouseout': - case 'mouseover': - case 'contextmenu': - j = nr; - break; - case 'drag': - case 'dragend': - case 'dragenter': - case 'dragexit': - case 'dragleave': - case 'dragover': - case 'dragstart': - case 'drop': - j = sr; - break; - case 'touchcancel': - case 'touchend': - case 'touchmove': - case 'touchstart': - j = vr; - break; - case Kr: - case Hr: - case Jr: - j = ar; - break; - case Gr: - j = br; - break; - case 'scroll': - j = tr; - break; - case 'wheel': - j = Er; - break; - case 'copy': - case 'cut': - case 'paste': - j = cr; - break; - case 'gotpointercapture': - case 'lostpointercapture': - case 'pointercancel': - case 'pointerdown': - case 'pointermove': - case 'pointerout': - case 'pointerover': - case 'pointerup': - j = yr; - } - var B = !!(4 & o), - $ = !B && 'scroll' === s, - V = B ? (null !== C ? C + 'Capture' : null) : C; - B = []; - for (var U, z = u; null !== z; ) { - var Y = (U = z).stateNode; - if ( - (5 === U.tag && - null !== Y && - ((U = Y), null !== V && null != (Y = Kb(z, V)) && B.push(tf(z, Y, U))), - $) - ) - break; - z = z.return; - } - 0 < B.length && - ((C = new j(C, L, null, i, _)), x.push({ event: C, listeners: B })); - } - } - if (!(7 & o)) { - if ( - ((j = 'mouseout' === s || 'pointerout' === s), - (!(C = 'mouseover' === s || 'pointerover' === s) || - i === Ye || - !(L = i.relatedTarget || i.fromElement) || - (!Wc(L) && !L[mn])) && - (j || C) && - ((C = - _.window === _ - ? _ - : (C = _.ownerDocument) - ? C.defaultView || C.parentWindow - : window), - j - ? ((j = u), - null !== (L = (L = i.relatedTarget || i.toElement) ? Wc(L) : null) && - (L !== ($ = Vb(L)) || (5 !== L.tag && 6 !== L.tag)) && - (L = null)) - : ((j = null), (L = u)), - j !== L)) - ) { - if ( - ((B = nr), - (Y = 'onMouseLeave'), - (V = 'onMouseEnter'), - (z = 'mouse'), - ('pointerout' !== s && 'pointerover' !== s) || - ((B = yr), (Y = 'onPointerLeave'), (V = 'onPointerEnter'), (z = 'pointer')), - ($ = null == j ? C : ue(j)), - (U = null == L ? C : ue(L)), - ((C = new B(Y, z + 'leave', j, i, _)).target = $), - (C.relatedTarget = U), - (Y = null), - Wc(_) === u && - (((B = new B(V, z + 'enter', L, i, _)).target = U), - (B.relatedTarget = $), - (Y = B)), - ($ = Y), - j && L) - ) - e: { - for (V = L, z = 0, U = B = j; U; U = vf(U)) z++; - for (U = 0, Y = V; Y; Y = vf(Y)) U++; - for (; 0 < z - U; ) (B = vf(B)), z--; - for (; 0 < U - z; ) (V = vf(V)), U--; - for (; z--; ) { - if (B === V || (null !== V && B === V.alternate)) break e; - (B = vf(B)), (V = vf(V)); - } - B = null; - } - else B = null; - null !== j && wf(x, C, j, B, !1), null !== L && null !== $ && wf(x, $, L, B, !0); - } - if ( - 'select' === - (j = (C = u ? ue(u) : window).nodeName && C.nodeName.toLowerCase()) || - ('input' === j && 'file' === C.type) - ) - var Z = ve; - else if (me(C)) - if (Tr) Z = Fe; - else { - Z = De; - var ee = Ce; - } - else - (j = C.nodeName) && - 'input' === j.toLowerCase() && - ('checkbox' === C.type || 'radio' === C.type) && - (Z = Ee); - switch ( - (Z && (Z = Z(s, u)) - ? ne(x, Z, i, _) - : (ee && ee(s, C, u), - 'focusout' === s && - (ee = C._wrapperState) && - ee.controlled && - 'number' === C.type && - cb(C, 'number', C.value)), - (ee = u ? ue(u) : window), - s) - ) { - case 'focusin': - (me(ee) || 'true' === ee.contentEditable) && ((Fr = ee), (qr = u), ($r = null)); - break; - case 'focusout': - $r = qr = Fr = null; - break; - case 'mousedown': - Vr = !0; - break; - case 'contextmenu': - case 'mouseup': - case 'dragend': - (Vr = !1), Ue(x, i, _); - break; - case 'selectionchange': - if (Br) break; - case 'keydown': - case 'keyup': - Ue(x, i, _); - } - var ie; - if (Sr) - e: { - switch (s) { - case 'compositionstart': - var ae = 'onCompositionStart'; - break e; - case 'compositionend': - ae = 'onCompositionEnd'; - break e; - case 'compositionupdate': - ae = 'onCompositionUpdate'; - break e; - } - ae = void 0; - } - else - jr - ? ge(s, i) && (ae = 'onCompositionEnd') - : 'keydown' === s && 229 === i.keyCode && (ae = 'onCompositionStart'); - ae && - (Cr && - 'ko' !== i.locale && - (jr || 'onCompositionStart' !== ae - ? 'onCompositionEnd' === ae && jr && (ie = nd()) - : ((Ht = 'value' in (Kt = _) ? Kt.value : Kt.textContent), (jr = !0))), - 0 < (ee = oe(u, ae)).length && - ((ae = new ur(ae, s, null, i, _)), - x.push({ event: ae, listeners: ee }), - ie ? (ae.data = ie) : null !== (ie = he(i)) && (ae.data = ie))), - (ie = kr - ? (function je(s, o) { - switch (s) { - case 'compositionend': - return he(o); - case 'keypress': - return 32 !== o.which ? null : ((Ar = !0), Or); - case 'textInput': - return (s = o.data) === Or && Ar ? null : s; - default: - return null; - } - })(s, i) - : (function ke(s, o) { - if (jr) - return 'compositionend' === s || (!Sr && ge(s, o)) - ? ((s = nd()), (Jt = Ht = Kt = null), (jr = !1), s) - : null; - switch (s) { - case 'paste': - default: - return null; - case 'keypress': - if (!(o.ctrlKey || o.altKey || o.metaKey) || (o.ctrlKey && o.altKey)) { - if (o.char && 1 < o.char.length) return o.char; - if (o.which) return String.fromCharCode(o.which); - } - return null; - case 'compositionend': - return Cr && 'ko' !== o.locale ? null : o.data; - } - })(s, i)) && - 0 < (u = oe(u, 'onBeforeInput')).length && - ((_ = new ur('onBeforeInput', 'beforeinput', null, i, _)), - x.push({ event: _, listeners: u }), - (_.data = ie)); - } - se(x, o); - }); - } - function tf(s, o, i) { - return { instance: s, listener: o, currentTarget: i }; - } - function oe(s, o) { - for (var i = o + 'Capture', u = []; null !== s; ) { - var _ = s, - w = _.stateNode; - 5 === _.tag && - null !== w && - ((_ = w), - null != (w = Kb(s, i)) && u.unshift(tf(s, w, _)), - null != (w = Kb(s, o)) && u.push(tf(s, w, _))), - (s = s.return); - } - return u; - } - function vf(s) { - if (null === s) return null; - do { - s = s.return; - } while (s && 5 !== s.tag); - return s || null; - } - function wf(s, o, i, u, _) { - for (var w = o._reactName, x = []; null !== i && i !== u; ) { - var C = i, - j = C.alternate, - L = C.stateNode; - if (null !== j && j === u) break; - 5 === C.tag && - null !== L && - ((C = L), - _ - ? null != (j = Kb(i, w)) && x.unshift(tf(i, j, C)) - : _ || (null != (j = Kb(i, w)) && x.push(tf(i, j, C)))), - (i = i.return); - } - 0 !== x.length && s.push({ event: o, listeners: x }); - } - var nn = /\r\n?/g, - sn = /\u0000|\uFFFD/g; - function zf(s) { - return ('string' == typeof s ? s : '' + s).replace(nn, '\n').replace(sn, ''); - } - function Af(s, o, i) { - if (((o = zf(o)), zf(s) !== o && i)) throw Error(p(425)); - } - function Bf() {} - var on = null, - an = null; - function Ef(s, o) { - return ( - 'textarea' === s || - 'noscript' === s || - 'string' == typeof o.children || - 'number' == typeof o.children || - ('object' == typeof o.dangerouslySetInnerHTML && - null !== o.dangerouslySetInnerHTML && - null != o.dangerouslySetInnerHTML.__html) - ); - } - var ln = 'function' == typeof setTimeout ? setTimeout : void 0, - cn = 'function' == typeof clearTimeout ? clearTimeout : void 0, - un = 'function' == typeof Promise ? Promise : void 0, - pn = - 'function' == typeof queueMicrotask - ? queueMicrotask - : void 0 !== un - ? function (s) { - return un.resolve(null).then(s).catch(If); - } - : ln; - function If(s) { - setTimeout(function () { - throw s; - }); - } - function Kf(s, o) { - var i = o, - u = 0; - do { - var _ = i.nextSibling; - if ((s.removeChild(i), _ && 8 === _.nodeType)) - if ('/$' === (i = _.data)) { - if (0 === u) return s.removeChild(_), void bd(o); - u--; - } else ('$' !== i && '$?' !== i && '$!' !== i) || u++; - i = _; - } while (i); - bd(o); - } - function Lf(s) { - for (; null != s; s = s.nextSibling) { - var o = s.nodeType; - if (1 === o || 3 === o) break; - if (8 === o) { - if ('$' === (o = s.data) || '$!' === o || '$?' === o) break; - if ('/$' === o) return null; - } - } - return s; - } - function Mf(s) { - s = s.previousSibling; - for (var o = 0; s; ) { - if (8 === s.nodeType) { - var i = s.data; - if ('$' === i || '$!' === i || '$?' === i) { - if (0 === o) return s; - o--; - } else '/$' === i && o++; - } - s = s.previousSibling; - } - return null; - } - var hn = Math.random().toString(36).slice(2), - dn = '__reactFiber$' + hn, - fn = '__reactProps$' + hn, - mn = '__reactContainer$' + hn, - gn = '__reactEvents$' + hn, - yn = '__reactListeners$' + hn, - vn = '__reactHandles$' + hn; - function Wc(s) { - var o = s[dn]; - if (o) return o; - for (var i = s.parentNode; i; ) { - if ((o = i[mn] || i[dn])) { - if (((i = o.alternate), null !== o.child || (null !== i && null !== i.child))) - for (s = Mf(s); null !== s; ) { - if ((i = s[dn])) return i; - s = Mf(s); - } - return o; - } - i = (s = i).parentNode; - } - return null; - } - function Cb(s) { - return !(s = s[dn] || s[mn]) || - (5 !== s.tag && 6 !== s.tag && 13 !== s.tag && 3 !== s.tag) - ? null - : s; - } - function ue(s) { - if (5 === s.tag || 6 === s.tag) return s.stateNode; - throw Error(p(33)); - } - function Db(s) { - return s[fn] || null; - } - var bn = [], - _n = -1; - function Uf(s) { - return { current: s }; - } - function E(s) { - 0 > _n || ((s.current = bn[_n]), (bn[_n] = null), _n--); - } - function G(s, o) { - _n++, (bn[_n] = s.current), (s.current = o); - } - var En = {}, - wn = Uf(En), - Sn = Uf(!1), - xn = En; - function Yf(s, o) { - var i = s.type.contextTypes; - if (!i) return En; - var u = s.stateNode; - if (u && u.__reactInternalMemoizedUnmaskedChildContext === o) - return u.__reactInternalMemoizedMaskedChildContext; - var _, - w = {}; - for (_ in i) w[_] = o[_]; - return ( - u && - (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = o), - (s.__reactInternalMemoizedMaskedChildContext = w)), - w - ); - } - function Zf(s) { - return null != (s = s.childContextTypes); - } - function $f() { - E(Sn), E(wn); - } - function ag(s, o, i) { - if (wn.current !== En) throw Error(p(168)); - G(wn, o), G(Sn, i); - } - function bg(s, o, i) { - var u = s.stateNode; - if (((o = o.childContextTypes), 'function' != typeof u.getChildContext)) return i; - for (var _ in (u = u.getChildContext())) - if (!(_ in o)) throw Error(p(108, Ra(s) || 'Unknown', _)); - return xe({}, i, u); - } - function cg(s) { - return ( - (s = ((s = s.stateNode) && s.__reactInternalMemoizedMergedChildContext) || En), - (xn = wn.current), - G(wn, s), - G(Sn, Sn.current), - !0 - ); - } - function dg(s, o, i) { - var u = s.stateNode; - if (!u) throw Error(p(169)); - i - ? ((s = bg(s, o, xn)), - (u.__reactInternalMemoizedMergedChildContext = s), - E(Sn), - E(wn), - G(wn, s)) - : E(Sn), - G(Sn, i); - } - var kn = null, - Cn = !1, - On = !1; - function hg(s) { - null === kn ? (kn = [s]) : kn.push(s); - } - function jg() { - if (!On && null !== kn) { - On = !0; - var s = 0, - o = At; - try { - var i = kn; - for (At = 1; s < i.length; s++) { - var u = i[s]; - do { - u = u(!0); - } while (null !== u); - } - (kn = null), (Cn = !1); - } catch (o) { - throw (null !== kn && (kn = kn.slice(s + 1)), ct(gt, jg), o); - } finally { - (At = o), (On = !1); - } - } - return null; - } - var An = [], - jn = 0, - In = null, - Pn = 0, - Mn = [], - Tn = 0, - Nn = null, - Rn = 1, - Dn = ''; - function tg(s, o) { - (An[jn++] = Pn), (An[jn++] = In), (In = s), (Pn = o); - } - function ug(s, o, i) { - (Mn[Tn++] = Rn), (Mn[Tn++] = Dn), (Mn[Tn++] = Nn), (Nn = s); - var u = Rn; - s = Dn; - var _ = 32 - St(u) - 1; - (u &= ~(1 << _)), (i += 1); - var w = 32 - St(o) + _; - if (30 < w) { - var x = _ - (_ % 5); - (w = (u & ((1 << x) - 1)).toString(32)), - (u >>= x), - (_ -= x), - (Rn = (1 << (32 - St(o) + _)) | (i << _) | u), - (Dn = w + s); - } else (Rn = (1 << w) | (i << _) | u), (Dn = s); - } - function vg(s) { - null !== s.return && (tg(s, 1), ug(s, 1, 0)); - } - function wg(s) { - for (; s === In; ) (In = An[--jn]), (An[jn] = null), (Pn = An[--jn]), (An[jn] = null); - for (; s === Nn; ) - (Nn = Mn[--Tn]), - (Mn[Tn] = null), - (Dn = Mn[--Tn]), - (Mn[Tn] = null), - (Rn = Mn[--Tn]), - (Mn[Tn] = null); - } - var Ln = null, - Bn = null, - Fn = !1, - qn = null; - function Ag(s, o) { - var i = Bg(5, null, null, 0); - (i.elementType = 'DELETED'), - (i.stateNode = o), - (i.return = s), - null === (o = s.deletions) ? ((s.deletions = [i]), (s.flags |= 16)) : o.push(i); - } - function Cg(s, o) { - switch (s.tag) { - case 5: - var i = s.type; - return ( - null !== - (o = - 1 !== o.nodeType || i.toLowerCase() !== o.nodeName.toLowerCase() - ? null - : o) && ((s.stateNode = o), (Ln = s), (Bn = Lf(o.firstChild)), !0) - ); - case 6: - return ( - null !== (o = '' === s.pendingProps || 3 !== o.nodeType ? null : o) && - ((s.stateNode = o), (Ln = s), (Bn = null), !0) - ); - case 13: - return ( - null !== (o = 8 !== o.nodeType ? null : o) && - ((i = null !== Nn ? { id: Rn, overflow: Dn } : null), - (s.memoizedState = { dehydrated: o, treeContext: i, retryLane: 1073741824 }), - ((i = Bg(18, null, null, 0)).stateNode = o), - (i.return = s), - (s.child = i), - (Ln = s), - (Bn = null), - !0) - ); - default: - return !1; - } - } - function Dg(s) { - return !(!(1 & s.mode) || 128 & s.flags); - } - function Eg(s) { - if (Fn) { - var o = Bn; - if (o) { - var i = o; - if (!Cg(s, o)) { - if (Dg(s)) throw Error(p(418)); - o = Lf(i.nextSibling); - var u = Ln; - o && Cg(s, o) - ? Ag(u, i) - : ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Ln = s)); - } - } else { - if (Dg(s)) throw Error(p(418)); - (s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Ln = s); - } - } - } - function Fg(s) { - for (s = s.return; null !== s && 5 !== s.tag && 3 !== s.tag && 13 !== s.tag; ) - s = s.return; - Ln = s; - } - function Gg(s) { - if (s !== Ln) return !1; - if (!Fn) return Fg(s), (Fn = !0), !1; - var o; - if ( - ((o = 3 !== s.tag) && - !(o = 5 !== s.tag) && - (o = 'head' !== (o = s.type) && 'body' !== o && !Ef(s.type, s.memoizedProps)), - o && (o = Bn)) - ) { - if (Dg(s)) throw (Hg(), Error(p(418))); - for (; o; ) Ag(s, o), (o = Lf(o.nextSibling)); - } - if ((Fg(s), 13 === s.tag)) { - if (!(s = null !== (s = s.memoizedState) ? s.dehydrated : null)) throw Error(p(317)); - e: { - for (s = s.nextSibling, o = 0; s; ) { - if (8 === s.nodeType) { - var i = s.data; - if ('/$' === i) { - if (0 === o) { - Bn = Lf(s.nextSibling); - break e; - } - o--; - } else ('$' !== i && '$!' !== i && '$?' !== i) || o++; - } - s = s.nextSibling; - } - Bn = null; - } - } else Bn = Ln ? Lf(s.stateNode.nextSibling) : null; - return !0; - } - function Hg() { - for (var s = Bn; s; ) s = Lf(s.nextSibling); - } - function Ig() { - (Bn = Ln = null), (Fn = !1); - } - function Jg(s) { - null === qn ? (qn = [s]) : qn.push(s); - } - var $n = z.ReactCurrentBatchConfig; - function Lg(s, o, i) { - if (null !== (s = i.ref) && 'function' != typeof s && 'object' != typeof s) { - if (i._owner) { - if ((i = i._owner)) { - if (1 !== i.tag) throw Error(p(309)); - var u = i.stateNode; - } - if (!u) throw Error(p(147, s)); - var _ = u, - w = '' + s; - return null !== o && - null !== o.ref && - 'function' == typeof o.ref && - o.ref._stringRef === w - ? o.ref - : ((o = function (s) { - var o = _.refs; - null === s ? delete o[w] : (o[w] = s); - }), - (o._stringRef = w), - o); - } - if ('string' != typeof s) throw Error(p(284)); - if (!i._owner) throw Error(p(290, s)); - } - return s; - } - function Mg(s, o) { - throw ( - ((s = Object.prototype.toString.call(o)), - Error( - p( - 31, - '[object Object]' === s - ? 'object with keys {' + Object.keys(o).join(', ') + '}' - : s - ) - )) - ); - } - function Ng(s) { - return (0, s._init)(s._payload); - } - function Og(s) { - function b(o, i) { - if (s) { - var u = o.deletions; - null === u ? ((o.deletions = [i]), (o.flags |= 16)) : u.push(i); - } - } - function c(o, i) { - if (!s) return null; - for (; null !== i; ) b(o, i), (i = i.sibling); - return null; - } - function d(s, o) { - for (s = new Map(); null !== o; ) - null !== o.key ? s.set(o.key, o) : s.set(o.index, o), (o = o.sibling); - return s; - } - function e(s, o) { - return ((s = Pg(s, o)).index = 0), (s.sibling = null), s; - } - function f(o, i, u) { - return ( - (o.index = u), - s - ? null !== (u = o.alternate) - ? (u = u.index) < i - ? ((o.flags |= 2), i) - : u - : ((o.flags |= 2), i) - : ((o.flags |= 1048576), i) - ); - } - function g(o) { - return s && null === o.alternate && (o.flags |= 2), o; - } - function h(s, o, i, u) { - return null === o || 6 !== o.tag - ? (((o = Qg(i, s.mode, u)).return = s), o) - : (((o = e(o, i)).return = s), o); - } - function k(s, o, i, u) { - var _ = i.type; - return _ === ee - ? m(s, o, i.props.children, u, i.key) - : null !== o && - (o.elementType === _ || - ('object' == typeof _ && null !== _ && _.$$typeof === be && Ng(_) === o.type)) - ? (((u = e(o, i.props)).ref = Lg(s, o, i)), (u.return = s), u) - : (((u = Rg(i.type, i.key, i.props, null, s.mode, u)).ref = Lg(s, o, i)), - (u.return = s), - u); - } - function l(s, o, i, u) { - return null === o || - 4 !== o.tag || - o.stateNode.containerInfo !== i.containerInfo || - o.stateNode.implementation !== i.implementation - ? (((o = Sg(i, s.mode, u)).return = s), o) - : (((o = e(o, i.children || [])).return = s), o); - } - function m(s, o, i, u, _) { - return null === o || 7 !== o.tag - ? (((o = Tg(i, s.mode, u, _)).return = s), o) - : (((o = e(o, i)).return = s), o); - } - function q(s, o, i) { - if (('string' == typeof o && '' !== o) || 'number' == typeof o) - return ((o = Qg('' + o, s.mode, i)).return = s), o; - if ('object' == typeof o && null !== o) { - switch (o.$$typeof) { - case Y: - return ( - ((i = Rg(o.type, o.key, o.props, null, s.mode, i)).ref = Lg(s, null, o)), - (i.return = s), - i - ); - case Z: - return ((o = Sg(o, s.mode, i)).return = s), o; - case be: - return q(s, (0, o._init)(o._payload), i); - } - if (Te(o) || Ka(o)) return ((o = Tg(o, s.mode, i, null)).return = s), o; - Mg(s, o); - } - return null; - } - function r(s, o, i, u) { - var _ = null !== o ? o.key : null; - if (('string' == typeof i && '' !== i) || 'number' == typeof i) - return null !== _ ? null : h(s, o, '' + i, u); - if ('object' == typeof i && null !== i) { - switch (i.$$typeof) { - case Y: - return i.key === _ ? k(s, o, i, u) : null; - case Z: - return i.key === _ ? l(s, o, i, u) : null; - case be: - return r(s, o, (_ = i._init)(i._payload), u); - } - if (Te(i) || Ka(i)) return null !== _ ? null : m(s, o, i, u, null); - Mg(s, i); - } - return null; - } - function y(s, o, i, u, _) { - if (('string' == typeof u && '' !== u) || 'number' == typeof u) - return h(o, (s = s.get(i) || null), '' + u, _); - if ('object' == typeof u && null !== u) { - switch (u.$$typeof) { - case Y: - return k(o, (s = s.get(null === u.key ? i : u.key) || null), u, _); - case Z: - return l(o, (s = s.get(null === u.key ? i : u.key) || null), u, _); - case be: - return y(s, o, i, (0, u._init)(u._payload), _); - } - if (Te(u) || Ka(u)) return m(o, (s = s.get(i) || null), u, _, null); - Mg(o, u); - } - return null; - } - function n(o, i, u, _) { - for ( - var w = null, x = null, C = i, j = (i = 0), L = null; - null !== C && j < u.length; - j++ - ) { - C.index > j ? ((L = C), (C = null)) : (L = C.sibling); - var B = r(o, C, u[j], _); - if (null === B) { - null === C && (C = L); - break; - } - s && C && null === B.alternate && b(o, C), - (i = f(B, i, j)), - null === x ? (w = B) : (x.sibling = B), - (x = B), - (C = L); - } - if (j === u.length) return c(o, C), Fn && tg(o, j), w; - if (null === C) { - for (; j < u.length; j++) - null !== (C = q(o, u[j], _)) && - ((i = f(C, i, j)), null === x ? (w = C) : (x.sibling = C), (x = C)); - return Fn && tg(o, j), w; - } - for (C = d(o, C); j < u.length; j++) - null !== (L = y(C, o, j, u[j], _)) && - (s && null !== L.alternate && C.delete(null === L.key ? j : L.key), - (i = f(L, i, j)), - null === x ? (w = L) : (x.sibling = L), - (x = L)); - return ( - s && - C.forEach(function (s) { - return b(o, s); - }), - Fn && tg(o, j), - w - ); - } - function t(o, i, u, _) { - var w = Ka(u); - if ('function' != typeof w) throw Error(p(150)); - if (null == (u = w.call(u))) throw Error(p(151)); - for ( - var x = (w = null), C = i, j = (i = 0), L = null, B = u.next(); - null !== C && !B.done; - j++, B = u.next() - ) { - C.index > j ? ((L = C), (C = null)) : (L = C.sibling); - var $ = r(o, C, B.value, _); - if (null === $) { - null === C && (C = L); - break; - } - s && C && null === $.alternate && b(o, C), - (i = f($, i, j)), - null === x ? (w = $) : (x.sibling = $), - (x = $), - (C = L); - } - if (B.done) return c(o, C), Fn && tg(o, j), w; - if (null === C) { - for (; !B.done; j++, B = u.next()) - null !== (B = q(o, B.value, _)) && - ((i = f(B, i, j)), null === x ? (w = B) : (x.sibling = B), (x = B)); - return Fn && tg(o, j), w; - } - for (C = d(o, C); !B.done; j++, B = u.next()) - null !== (B = y(C, o, j, B.value, _)) && - (s && null !== B.alternate && C.delete(null === B.key ? j : B.key), - (i = f(B, i, j)), - null === x ? (w = B) : (x.sibling = B), - (x = B)); - return ( - s && - C.forEach(function (s) { - return b(o, s); - }), - Fn && tg(o, j), - w - ); - } - return function J(s, o, i, u) { - if ( - ('object' == typeof i && - null !== i && - i.type === ee && - null === i.key && - (i = i.props.children), - 'object' == typeof i && null !== i) - ) { - switch (i.$$typeof) { - case Y: - e: { - for (var _ = i.key, w = o; null !== w; ) { - if (w.key === _) { - if ((_ = i.type) === ee) { - if (7 === w.tag) { - c(s, w.sibling), ((o = e(w, i.props.children)).return = s), (s = o); - break e; - } - } else if ( - w.elementType === _ || - ('object' == typeof _ && - null !== _ && - _.$$typeof === be && - Ng(_) === w.type) - ) { - c(s, w.sibling), - ((o = e(w, i.props)).ref = Lg(s, w, i)), - (o.return = s), - (s = o); - break e; - } - c(s, w); - break; - } - b(s, w), (w = w.sibling); - } - i.type === ee - ? (((o = Tg(i.props.children, s.mode, u, i.key)).return = s), (s = o)) - : (((u = Rg(i.type, i.key, i.props, null, s.mode, u)).ref = Lg(s, o, i)), - (u.return = s), - (s = u)); - } - return g(s); - case Z: - e: { - for (w = i.key; null !== o; ) { - if (o.key === w) { - if ( - 4 === o.tag && - o.stateNode.containerInfo === i.containerInfo && - o.stateNode.implementation === i.implementation - ) { - c(s, o.sibling), ((o = e(o, i.children || [])).return = s), (s = o); - break e; - } - c(s, o); - break; - } - b(s, o), (o = o.sibling); - } - ((o = Sg(i, s.mode, u)).return = s), (s = o); - } - return g(s); - case be: - return J(s, o, (w = i._init)(i._payload), u); - } - if (Te(i)) return n(s, o, i, u); - if (Ka(i)) return t(s, o, i, u); - Mg(s, i); - } - return ('string' == typeof i && '' !== i) || 'number' == typeof i - ? ((i = '' + i), - null !== o && 6 === o.tag - ? (c(s, o.sibling), ((o = e(o, i)).return = s), (s = o)) - : (c(s, o), ((o = Qg(i, s.mode, u)).return = s), (s = o)), - g(s)) - : c(s, o); - }; - } - var Vn = Og(!0), - Un = Og(!1), - zn = Uf(null), - Wn = null, - Kn = null, - Hn = null; - function $g() { - Hn = Kn = Wn = null; - } - function ah(s) { - var o = zn.current; - E(zn), (s._currentValue = o); - } - function bh(s, o, i) { - for (; null !== s; ) { - var u = s.alternate; - if ( - ((s.childLanes & o) !== o - ? ((s.childLanes |= o), null !== u && (u.childLanes |= o)) - : null !== u && (u.childLanes & o) !== o && (u.childLanes |= o), - s === i) - ) - break; - s = s.return; - } - } - function ch(s, o) { - (Wn = s), - (Hn = Kn = null), - null !== (s = s.dependencies) && - null !== s.firstContext && - (!!(s.lanes & o) && (_s = !0), (s.firstContext = null)); - } - function eh(s) { - var o = s._currentValue; - if (Hn !== s) - if (((s = { context: s, memoizedValue: o, next: null }), null === Kn)) { - if (null === Wn) throw Error(p(308)); - (Kn = s), (Wn.dependencies = { lanes: 0, firstContext: s }); - } else Kn = Kn.next = s; - return o; - } - var Jn = null; - function gh(s) { - null === Jn ? (Jn = [s]) : Jn.push(s); - } - function hh(s, o, i, u) { - var _ = o.interleaved; - return ( - null === _ ? ((i.next = i), gh(o)) : ((i.next = _.next), (_.next = i)), - (o.interleaved = i), - ih(s, u) - ); - } - function ih(s, o) { - s.lanes |= o; - var i = s.alternate; - for (null !== i && (i.lanes |= o), i = s, s = s.return; null !== s; ) - (s.childLanes |= o), - null !== (i = s.alternate) && (i.childLanes |= o), - (i = s), - (s = s.return); - return 3 === i.tag ? i.stateNode : null; - } - var Gn = !1; - function kh(s) { - s.updateQueue = { - baseState: s.memoizedState, - firstBaseUpdate: null, - lastBaseUpdate: null, - shared: { pending: null, interleaved: null, lanes: 0 }, - effects: null - }; - } - function lh(s, o) { - (s = s.updateQueue), - o.updateQueue === s && - (o.updateQueue = { - baseState: s.baseState, - firstBaseUpdate: s.firstBaseUpdate, - lastBaseUpdate: s.lastBaseUpdate, - shared: s.shared, - effects: s.effects - }); - } - function mh(s, o) { - return { eventTime: s, lane: o, tag: 0, payload: null, callback: null, next: null }; - } - function nh(s, o, i) { - var u = s.updateQueue; - if (null === u) return null; - if (((u = u.shared), 2 & Bs)) { - var _ = u.pending; - return ( - null === _ ? (o.next = o) : ((o.next = _.next), (_.next = o)), - (u.pending = o), - ih(s, i) - ); - } - return ( - null === (_ = u.interleaved) - ? ((o.next = o), gh(u)) - : ((o.next = _.next), (_.next = o)), - (u.interleaved = o), - ih(s, i) - ); - } - function oh(s, o, i) { - if (null !== (o = o.updateQueue) && ((o = o.shared), 4194240 & i)) { - var u = o.lanes; - (i |= u &= s.pendingLanes), (o.lanes = i), Cc(s, i); - } - } - function ph(s, o) { - var i = s.updateQueue, - u = s.alternate; - if (null !== u && i === (u = u.updateQueue)) { - var _ = null, - w = null; - if (null !== (i = i.firstBaseUpdate)) { - do { - var x = { - eventTime: i.eventTime, - lane: i.lane, - tag: i.tag, - payload: i.payload, - callback: i.callback, - next: null - }; - null === w ? (_ = w = x) : (w = w.next = x), (i = i.next); - } while (null !== i); - null === w ? (_ = w = o) : (w = w.next = o); - } else _ = w = o; - return ( - (i = { - baseState: u.baseState, - firstBaseUpdate: _, - lastBaseUpdate: w, - shared: u.shared, - effects: u.effects - }), - void (s.updateQueue = i) - ); - } - null === (s = i.lastBaseUpdate) ? (i.firstBaseUpdate = o) : (s.next = o), - (i.lastBaseUpdate = o); - } - function qh(s, o, i, u) { - var _ = s.updateQueue; - Gn = !1; - var w = _.firstBaseUpdate, - x = _.lastBaseUpdate, - C = _.shared.pending; - if (null !== C) { - _.shared.pending = null; - var j = C, - L = j.next; - (j.next = null), null === x ? (w = L) : (x.next = L), (x = j); - var B = s.alternate; - null !== B && - (C = (B = B.updateQueue).lastBaseUpdate) !== x && - (null === C ? (B.firstBaseUpdate = L) : (C.next = L), (B.lastBaseUpdate = j)); - } - if (null !== w) { - var $ = _.baseState; - for (x = 0, B = L = j = null, C = w; ; ) { - var V = C.lane, - U = C.eventTime; - if ((u & V) === V) { - null !== B && - (B = B.next = - { - eventTime: U, - lane: 0, - tag: C.tag, - payload: C.payload, - callback: C.callback, - next: null - }); - e: { - var z = s, - Y = C; - switch (((V = o), (U = i), Y.tag)) { - case 1: - if ('function' == typeof (z = Y.payload)) { - $ = z.call(U, $, V); - break e; - } - $ = z; - break e; - case 3: - z.flags = (-65537 & z.flags) | 128; - case 0: - if ( - null == (V = 'function' == typeof (z = Y.payload) ? z.call(U, $, V) : z) - ) - break e; - $ = xe({}, $, V); - break e; - case 2: - Gn = !0; - } - } - null !== C.callback && - 0 !== C.lane && - ((s.flags |= 64), null === (V = _.effects) ? (_.effects = [C]) : V.push(C)); - } else - (U = { - eventTime: U, - lane: V, - tag: C.tag, - payload: C.payload, - callback: C.callback, - next: null - }), - null === B ? ((L = B = U), (j = $)) : (B = B.next = U), - (x |= V); - if (null === (C = C.next)) { - if (null === (C = _.shared.pending)) break; - (C = (V = C).next), - (V.next = null), - (_.lastBaseUpdate = V), - (_.shared.pending = null); - } - } - if ( - (null === B && (j = $), - (_.baseState = j), - (_.firstBaseUpdate = L), - (_.lastBaseUpdate = B), - null !== (o = _.shared.interleaved)) - ) { - _ = o; - do { - (x |= _.lane), (_ = _.next); - } while (_ !== o); - } else null === w && (_.shared.lanes = 0); - (Ks |= x), (s.lanes = x), (s.memoizedState = $); - } - } - function sh(s, o, i) { - if (((s = o.effects), (o.effects = null), null !== s)) - for (o = 0; o < s.length; o++) { - var u = s[o], - _ = u.callback; - if (null !== _) { - if (((u.callback = null), (u = i), 'function' != typeof _)) - throw Error(p(191, _)); - _.call(u); - } - } - } - var Yn = {}, - Xn = Uf(Yn), - Zn = Uf(Yn), - Qn = Uf(Yn); - function xh(s) { - if (s === Yn) throw Error(p(174)); - return s; - } - function yh(s, o) { - switch ((G(Qn, o), G(Zn, s), G(Xn, Yn), (s = o.nodeType))) { - case 9: - case 11: - o = (o = o.documentElement) ? o.namespaceURI : lb(null, ''); - break; - default: - o = lb( - (o = (s = 8 === s ? o.parentNode : o).namespaceURI || null), - (s = s.tagName) - ); - } - E(Xn), G(Xn, o); - } - function zh() { - E(Xn), E(Zn), E(Qn); - } - function Ah(s) { - xh(Qn.current); - var o = xh(Xn.current), - i = lb(o, s.type); - o !== i && (G(Zn, s), G(Xn, i)); - } - function Bh(s) { - Zn.current === s && (E(Xn), E(Zn)); - } - var es = Uf(0); - function Ch(s) { - for (var o = s; null !== o; ) { - if (13 === o.tag) { - var i = o.memoizedState; - if ( - null !== i && - (null === (i = i.dehydrated) || '$?' === i.data || '$!' === i.data) - ) - return o; - } else if (19 === o.tag && void 0 !== o.memoizedProps.revealOrder) { - if (128 & o.flags) return o; - } else if (null !== o.child) { - (o.child.return = o), (o = o.child); - continue; - } - if (o === s) break; - for (; null === o.sibling; ) { - if (null === o.return || o.return === s) return null; - o = o.return; - } - (o.sibling.return = o.return), (o = o.sibling); - } - return null; - } - var ts = []; - function Eh() { - for (var s = 0; s < ts.length; s++) ts[s]._workInProgressVersionPrimary = null; - ts.length = 0; - } - var rs = z.ReactCurrentDispatcher, - ns = z.ReactCurrentBatchConfig, - ss = 0, - os = null, - as = null, - ls = null, - cs = !1, - us = !1, - ps = 0, - hs = 0; - function P() { - throw Error(p(321)); - } - function Mh(s, o) { - if (null === o) return !1; - for (var i = 0; i < o.length && i < s.length; i++) if (!Lr(s[i], o[i])) return !1; - return !0; - } - function Nh(s, o, i, u, _, w) { - if ( - ((ss = w), - (os = o), - (o.memoizedState = null), - (o.updateQueue = null), - (o.lanes = 0), - (rs.current = null === s || null === s.memoizedState ? fs : ms), - (s = i(u, _)), - us) - ) { - w = 0; - do { - if (((us = !1), (ps = 0), 25 <= w)) throw Error(p(301)); - (w += 1), - (ls = as = null), - (o.updateQueue = null), - (rs.current = gs), - (s = i(u, _)); - } while (us); - } - if ( - ((rs.current = ds), - (o = null !== as && null !== as.next), - (ss = 0), - (ls = as = os = null), - (cs = !1), - o) - ) - throw Error(p(300)); - return s; - } - function Sh() { - var s = 0 !== ps; - return (ps = 0), s; - } - function Th() { - var s = { - memoizedState: null, - baseState: null, - baseQueue: null, - queue: null, - next: null - }; - return null === ls ? (os.memoizedState = ls = s) : (ls = ls.next = s), ls; - } - function Uh() { - if (null === as) { - var s = os.alternate; - s = null !== s ? s.memoizedState : null; - } else s = as.next; - var o = null === ls ? os.memoizedState : ls.next; - if (null !== o) (ls = o), (as = s); - else { - if (null === s) throw Error(p(310)); - (s = { - memoizedState: (as = s).memoizedState, - baseState: as.baseState, - baseQueue: as.baseQueue, - queue: as.queue, - next: null - }), - null === ls ? (os.memoizedState = ls = s) : (ls = ls.next = s); - } - return ls; - } - function Vh(s, o) { - return 'function' == typeof o ? o(s) : o; - } - function Wh(s) { - var o = Uh(), - i = o.queue; - if (null === i) throw Error(p(311)); - i.lastRenderedReducer = s; - var u = as, - _ = u.baseQueue, - w = i.pending; - if (null !== w) { - if (null !== _) { - var x = _.next; - (_.next = w.next), (w.next = x); - } - (u.baseQueue = _ = w), (i.pending = null); - } - if (null !== _) { - (w = _.next), (u = u.baseState); - var C = (x = null), - j = null, - L = w; - do { - var B = L.lane; - if ((ss & B) === B) - null !== j && - (j = j.next = - { - lane: 0, - action: L.action, - hasEagerState: L.hasEagerState, - eagerState: L.eagerState, - next: null - }), - (u = L.hasEagerState ? L.eagerState : s(u, L.action)); - else { - var $ = { - lane: B, - action: L.action, - hasEagerState: L.hasEagerState, - eagerState: L.eagerState, - next: null - }; - null === j ? ((C = j = $), (x = u)) : (j = j.next = $), - (os.lanes |= B), - (Ks |= B); - } - L = L.next; - } while (null !== L && L !== w); - null === j ? (x = u) : (j.next = C), - Lr(u, o.memoizedState) || (_s = !0), - (o.memoizedState = u), - (o.baseState = x), - (o.baseQueue = j), - (i.lastRenderedState = u); - } - if (null !== (s = i.interleaved)) { - _ = s; - do { - (w = _.lane), (os.lanes |= w), (Ks |= w), (_ = _.next); - } while (_ !== s); - } else null === _ && (i.lanes = 0); - return [o.memoizedState, i.dispatch]; - } - function Xh(s) { - var o = Uh(), - i = o.queue; - if (null === i) throw Error(p(311)); - i.lastRenderedReducer = s; - var u = i.dispatch, - _ = i.pending, - w = o.memoizedState; - if (null !== _) { - i.pending = null; - var x = (_ = _.next); - do { - (w = s(w, x.action)), (x = x.next); - } while (x !== _); - Lr(w, o.memoizedState) || (_s = !0), - (o.memoizedState = w), - null === o.baseQueue && (o.baseState = w), - (i.lastRenderedState = w); - } - return [w, u]; - } - function Yh() {} - function Zh(s, o) { - var i = os, - u = Uh(), - _ = o(), - w = !Lr(u.memoizedState, _); - if ( - (w && ((u.memoizedState = _), (_s = !0)), - (u = u.queue), - $h(ai.bind(null, i, u, s), [s]), - u.getSnapshot !== o || w || (null !== ls && 1 & ls.memoizedState.tag)) - ) { - if (((i.flags |= 2048), bi(9, ci.bind(null, i, u, _, o), void 0, null), null === Fs)) - throw Error(p(349)); - 30 & ss || di(i, o, _); - } - return _; - } - function di(s, o, i) { - (s.flags |= 16384), - (s = { getSnapshot: o, value: i }), - null === (o = os.updateQueue) - ? ((o = { lastEffect: null, stores: null }), (os.updateQueue = o), (o.stores = [s])) - : null === (i = o.stores) - ? (o.stores = [s]) - : i.push(s); - } - function ci(s, o, i, u) { - (o.value = i), (o.getSnapshot = u), ei(o) && fi(s); - } - function ai(s, o, i) { - return i(function () { - ei(o) && fi(s); - }); - } - function ei(s) { - var o = s.getSnapshot; - s = s.value; - try { - var i = o(); - return !Lr(s, i); - } catch (s) { - return !0; - } - } - function fi(s) { - var o = ih(s, 1); - null !== o && gi(o, s, 1, -1); - } - function hi(s) { - var o = Th(); - return ( - 'function' == typeof s && (s = s()), - (o.memoizedState = o.baseState = s), - (s = { - pending: null, - interleaved: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: Vh, - lastRenderedState: s - }), - (o.queue = s), - (s = s.dispatch = ii.bind(null, os, s)), - [o.memoizedState, s] - ); - } - function bi(s, o, i, u) { - return ( - (s = { tag: s, create: o, destroy: i, deps: u, next: null }), - null === (o = os.updateQueue) - ? ((o = { lastEffect: null, stores: null }), - (os.updateQueue = o), - (o.lastEffect = s.next = s)) - : null === (i = o.lastEffect) - ? (o.lastEffect = s.next = s) - : ((u = i.next), (i.next = s), (s.next = u), (o.lastEffect = s)), - s - ); - } - function ji() { - return Uh().memoizedState; - } - function ki(s, o, i, u) { - var _ = Th(); - (os.flags |= s), (_.memoizedState = bi(1 | o, i, void 0, void 0 === u ? null : u)); - } - function li(s, o, i, u) { - var _ = Uh(); - u = void 0 === u ? null : u; - var w = void 0; - if (null !== as) { - var x = as.memoizedState; - if (((w = x.destroy), null !== u && Mh(u, x.deps))) - return void (_.memoizedState = bi(o, i, w, u)); - } - (os.flags |= s), (_.memoizedState = bi(1 | o, i, w, u)); - } - function mi(s, o) { - return ki(8390656, 8, s, o); - } - function $h(s, o) { - return li(2048, 8, s, o); - } - function ni(s, o) { - return li(4, 2, s, o); - } - function oi(s, o) { - return li(4, 4, s, o); - } - function pi(s, o) { - return 'function' == typeof o - ? ((s = s()), - o(s), - function () { - o(null); - }) - : null != o - ? ((s = s()), - (o.current = s), - function () { - o.current = null; - }) - : void 0; - } - function qi(s, o, i) { - return (i = null != i ? i.concat([s]) : null), li(4, 4, pi.bind(null, o, s), i); - } - function ri() {} - function si(s, o) { - var i = Uh(); - o = void 0 === o ? null : o; - var u = i.memoizedState; - return null !== u && null !== o && Mh(o, u[1]) ? u[0] : ((i.memoizedState = [s, o]), s); - } - function ti(s, o) { - var i = Uh(); - o = void 0 === o ? null : o; - var u = i.memoizedState; - return null !== u && null !== o && Mh(o, u[1]) - ? u[0] - : ((s = s()), (i.memoizedState = [s, o]), s); - } - function ui(s, o, i) { - return 21 & ss - ? (Lr(i, o) || ((i = yc()), (os.lanes |= i), (Ks |= i), (s.baseState = !0)), o) - : (s.baseState && ((s.baseState = !1), (_s = !0)), (s.memoizedState = i)); - } - function vi(s, o) { - var i = At; - (At = 0 !== i && 4 > i ? i : 4), s(!0); - var u = ns.transition; - ns.transition = {}; - try { - s(!1), o(); - } finally { - (At = i), (ns.transition = u); - } - } - function wi() { - return Uh().memoizedState; - } - function xi(s, o, i) { - var u = yi(s); - if ( - ((i = { lane: u, action: i, hasEagerState: !1, eagerState: null, next: null }), zi(s)) - ) - Ai(o, i); - else if (null !== (i = hh(s, o, i, u))) { - gi(i, s, u, R()), Bi(i, o, u); - } - } - function ii(s, o, i) { - var u = yi(s), - _ = { lane: u, action: i, hasEagerState: !1, eagerState: null, next: null }; - if (zi(s)) Ai(o, _); - else { - var w = s.alternate; - if ( - 0 === s.lanes && - (null === w || 0 === w.lanes) && - null !== (w = o.lastRenderedReducer) - ) - try { - var x = o.lastRenderedState, - C = w(x, i); - if (((_.hasEagerState = !0), (_.eagerState = C), Lr(C, x))) { - var j = o.interleaved; - return ( - null === j ? ((_.next = _), gh(o)) : ((_.next = j.next), (j.next = _)), - void (o.interleaved = _) - ); - } - } catch (s) {} - null !== (i = hh(s, o, _, u)) && (gi(i, s, u, (_ = R())), Bi(i, o, u)); - } - } - function zi(s) { - var o = s.alternate; - return s === os || (null !== o && o === os); - } - function Ai(s, o) { - us = cs = !0; - var i = s.pending; - null === i ? (o.next = o) : ((o.next = i.next), (i.next = o)), (s.pending = o); - } - function Bi(s, o, i) { - if (4194240 & i) { - var u = o.lanes; - (i |= u &= s.pendingLanes), (o.lanes = i), Cc(s, i); - } - } - var ds = { - readContext: eh, - useCallback: P, - useContext: P, - useEffect: P, - useImperativeHandle: P, - useInsertionEffect: P, - useLayoutEffect: P, - useMemo: P, - useReducer: P, - useRef: P, - useState: P, - useDebugValue: P, - useDeferredValue: P, - useTransition: P, - useMutableSource: P, - useSyncExternalStore: P, - useId: P, - unstable_isNewReconciler: !1 - }, - fs = { - readContext: eh, - useCallback: function (s, o) { - return (Th().memoizedState = [s, void 0 === o ? null : o]), s; - }, - useContext: eh, - useEffect: mi, - useImperativeHandle: function (s, o, i) { - return ( - (i = null != i ? i.concat([s]) : null), ki(4194308, 4, pi.bind(null, o, s), i) - ); - }, - useLayoutEffect: function (s, o) { - return ki(4194308, 4, s, o); - }, - useInsertionEffect: function (s, o) { - return ki(4, 2, s, o); - }, - useMemo: function (s, o) { - var i = Th(); - return (o = void 0 === o ? null : o), (s = s()), (i.memoizedState = [s, o]), s; - }, - useReducer: function (s, o, i) { - var u = Th(); - return ( - (o = void 0 !== i ? i(o) : o), - (u.memoizedState = u.baseState = o), - (s = { - pending: null, - interleaved: null, - lanes: 0, - dispatch: null, - lastRenderedReducer: s, - lastRenderedState: o - }), - (u.queue = s), - (s = s.dispatch = xi.bind(null, os, s)), - [u.memoizedState, s] - ); - }, - useRef: function (s) { - return (s = { current: s }), (Th().memoizedState = s); - }, - useState: hi, - useDebugValue: ri, - useDeferredValue: function (s) { - return (Th().memoizedState = s); - }, - useTransition: function () { - var s = hi(!1), - o = s[0]; - return (s = vi.bind(null, s[1])), (Th().memoizedState = s), [o, s]; - }, - useMutableSource: function () {}, - useSyncExternalStore: function (s, o, i) { - var u = os, - _ = Th(); - if (Fn) { - if (void 0 === i) throw Error(p(407)); - i = i(); - } else { - if (((i = o()), null === Fs)) throw Error(p(349)); - 30 & ss || di(u, o, i); - } - _.memoizedState = i; - var w = { value: i, getSnapshot: o }; - return ( - (_.queue = w), - mi(ai.bind(null, u, w, s), [s]), - (u.flags |= 2048), - bi(9, ci.bind(null, u, w, i, o), void 0, null), - i - ); - }, - useId: function () { - var s = Th(), - o = Fs.identifierPrefix; - if (Fn) { - var i = Dn; - (o = ':' + o + 'R' + (i = (Rn & ~(1 << (32 - St(Rn) - 1))).toString(32) + i)), - 0 < (i = ps++) && (o += 'H' + i.toString(32)), - (o += ':'); - } else o = ':' + o + 'r' + (i = hs++).toString(32) + ':'; - return (s.memoizedState = o); - }, - unstable_isNewReconciler: !1 - }, - ms = { - readContext: eh, - useCallback: si, - useContext: eh, - useEffect: $h, - useImperativeHandle: qi, - useInsertionEffect: ni, - useLayoutEffect: oi, - useMemo: ti, - useReducer: Wh, - useRef: ji, - useState: function () { - return Wh(Vh); - }, - useDebugValue: ri, - useDeferredValue: function (s) { - return ui(Uh(), as.memoizedState, s); - }, - useTransition: function () { - return [Wh(Vh)[0], Uh().memoizedState]; - }, - useMutableSource: Yh, - useSyncExternalStore: Zh, - useId: wi, - unstable_isNewReconciler: !1 - }, - gs = { - readContext: eh, - useCallback: si, - useContext: eh, - useEffect: $h, - useImperativeHandle: qi, - useInsertionEffect: ni, - useLayoutEffect: oi, - useMemo: ti, - useReducer: Xh, - useRef: ji, - useState: function () { - return Xh(Vh); - }, - useDebugValue: ri, - useDeferredValue: function (s) { - var o = Uh(); - return null === as ? (o.memoizedState = s) : ui(o, as.memoizedState, s); - }, - useTransition: function () { - return [Xh(Vh)[0], Uh().memoizedState]; - }, - useMutableSource: Yh, - useSyncExternalStore: Zh, - useId: wi, - unstable_isNewReconciler: !1 - }; - function Ci(s, o) { - if (s && s.defaultProps) { - for (var i in ((o = xe({}, o)), (s = s.defaultProps))) - void 0 === o[i] && (o[i] = s[i]); - return o; - } - return o; - } - function Di(s, o, i, u) { - (i = null == (i = i(u, (o = s.memoizedState))) ? o : xe({}, o, i)), - (s.memoizedState = i), - 0 === s.lanes && (s.updateQueue.baseState = i); - } - var ys = { - isMounted: function (s) { - return !!(s = s._reactInternals) && Vb(s) === s; - }, - enqueueSetState: function (s, o, i) { - s = s._reactInternals; - var u = R(), - _ = yi(s), - w = mh(u, _); - (w.payload = o), - null != i && (w.callback = i), - null !== (o = nh(s, w, _)) && (gi(o, s, _, u), oh(o, s, _)); - }, - enqueueReplaceState: function (s, o, i) { - s = s._reactInternals; - var u = R(), - _ = yi(s), - w = mh(u, _); - (w.tag = 1), - (w.payload = o), - null != i && (w.callback = i), - null !== (o = nh(s, w, _)) && (gi(o, s, _, u), oh(o, s, _)); - }, - enqueueForceUpdate: function (s, o) { - s = s._reactInternals; - var i = R(), - u = yi(s), - _ = mh(i, u); - (_.tag = 2), - null != o && (_.callback = o), - null !== (o = nh(s, _, u)) && (gi(o, s, u, i), oh(o, s, u)); - } - }; - function Fi(s, o, i, u, _, w, x) { - return 'function' == typeof (s = s.stateNode).shouldComponentUpdate - ? s.shouldComponentUpdate(u, w, x) - : !o.prototype || !o.prototype.isPureReactComponent || !Ie(i, u) || !Ie(_, w); - } - function Gi(s, o, i) { - var u = !1, - _ = En, - w = o.contextType; - return ( - 'object' == typeof w && null !== w - ? (w = eh(w)) - : ((_ = Zf(o) ? xn : wn.current), - (w = (u = null != (u = o.contextTypes)) ? Yf(s, _) : En)), - (o = new o(i, w)), - (s.memoizedState = null !== o.state && void 0 !== o.state ? o.state : null), - (o.updater = ys), - (s.stateNode = o), - (o._reactInternals = s), - u && - (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = _), - (s.__reactInternalMemoizedMaskedChildContext = w)), - o - ); - } - function Hi(s, o, i, u) { - (s = o.state), - 'function' == typeof o.componentWillReceiveProps && o.componentWillReceiveProps(i, u), - 'function' == typeof o.UNSAFE_componentWillReceiveProps && - o.UNSAFE_componentWillReceiveProps(i, u), - o.state !== s && ys.enqueueReplaceState(o, o.state, null); - } - function Ii(s, o, i, u) { - var _ = s.stateNode; - (_.props = i), (_.state = s.memoizedState), (_.refs = {}), kh(s); - var w = o.contextType; - 'object' == typeof w && null !== w - ? (_.context = eh(w)) - : ((w = Zf(o) ? xn : wn.current), (_.context = Yf(s, w))), - (_.state = s.memoizedState), - 'function' == typeof (w = o.getDerivedStateFromProps) && - (Di(s, o, w, i), (_.state = s.memoizedState)), - 'function' == typeof o.getDerivedStateFromProps || - 'function' == typeof _.getSnapshotBeforeUpdate || - ('function' != typeof _.UNSAFE_componentWillMount && - 'function' != typeof _.componentWillMount) || - ((o = _.state), - 'function' == typeof _.componentWillMount && _.componentWillMount(), - 'function' == typeof _.UNSAFE_componentWillMount && _.UNSAFE_componentWillMount(), - o !== _.state && ys.enqueueReplaceState(_, _.state, null), - qh(s, i, _, u), - (_.state = s.memoizedState)), - 'function' == typeof _.componentDidMount && (s.flags |= 4194308); - } - function Ji(s, o) { - try { - var i = '', - u = o; - do { - (i += Pa(u)), (u = u.return); - } while (u); - var _ = i; - } catch (s) { - _ = '\nError generating stack: ' + s.message + '\n' + s.stack; - } - return { value: s, source: o, stack: _, digest: null }; - } - function Ki(s, o, i) { - return { - value: s, - source: null, - stack: null != i ? i : null, - digest: null != o ? o : null - }; - } - function Li(s, o) { - try { - console.error(o.value); - } catch (s) { - setTimeout(function () { - throw s; - }); - } - } - var vs = 'function' == typeof WeakMap ? WeakMap : Map; - function Ni(s, o, i) { - ((i = mh(-1, i)).tag = 3), (i.payload = { element: null }); - var u = o.value; - return ( - (i.callback = function () { - eo || ((eo = !0), (to = u)), Li(0, o); - }), - i - ); - } - function Qi(s, o, i) { - (i = mh(-1, i)).tag = 3; - var u = s.type.getDerivedStateFromError; - if ('function' == typeof u) { - var _ = o.value; - (i.payload = function () { - return u(_); - }), - (i.callback = function () { - Li(0, o); - }); - } - var w = s.stateNode; - return ( - null !== w && - 'function' == typeof w.componentDidCatch && - (i.callback = function () { - Li(0, o), - 'function' != typeof u && (null === ro ? (ro = new Set([this])) : ro.add(this)); - var s = o.stack; - this.componentDidCatch(o.value, { componentStack: null !== s ? s : '' }); - }), - i - ); - } - function Si(s, o, i) { - var u = s.pingCache; - if (null === u) { - u = s.pingCache = new vs(); - var _ = new Set(); - u.set(o, _); - } else void 0 === (_ = u.get(o)) && ((_ = new Set()), u.set(o, _)); - _.has(i) || (_.add(i), (s = Ti.bind(null, s, o, i)), o.then(s, s)); - } - function Ui(s) { - do { - var o; - if ( - ((o = 13 === s.tag) && - (o = null === (o = s.memoizedState) || null !== o.dehydrated), - o) - ) - return s; - s = s.return; - } while (null !== s); - return null; - } - function Vi(s, o, i, u, _) { - return 1 & s.mode - ? ((s.flags |= 65536), (s.lanes = _), s) - : (s === o - ? (s.flags |= 65536) - : ((s.flags |= 128), - (i.flags |= 131072), - (i.flags &= -52805), - 1 === i.tag && - (null === i.alternate - ? (i.tag = 17) - : (((o = mh(-1, 1)).tag = 2), nh(i, o, 1))), - (i.lanes |= 1)), - s); - } - var bs = z.ReactCurrentOwner, - _s = !1; - function Xi(s, o, i, u) { - o.child = null === s ? Un(o, null, i, u) : Vn(o, s.child, i, u); - } - function Yi(s, o, i, u, _) { - i = i.render; - var w = o.ref; - return ( - ch(o, _), - (u = Nh(s, o, i, u, w, _)), - (i = Sh()), - null === s || _s - ? (Fn && i && vg(o), (o.flags |= 1), Xi(s, o, u, _), o.child) - : ((o.updateQueue = s.updateQueue), - (o.flags &= -2053), - (s.lanes &= ~_), - Zi(s, o, _)) - ); - } - function $i(s, o, i, u, _) { - if (null === s) { - var w = i.type; - return 'function' != typeof w || - aj(w) || - void 0 !== w.defaultProps || - null !== i.compare || - void 0 !== i.defaultProps - ? (((s = Rg(i.type, null, u, o, o.mode, _)).ref = o.ref), - (s.return = o), - (o.child = s)) - : ((o.tag = 15), (o.type = w), bj(s, o, w, u, _)); - } - if (((w = s.child), !(s.lanes & _))) { - var x = w.memoizedProps; - if ((i = null !== (i = i.compare) ? i : Ie)(x, u) && s.ref === o.ref) - return Zi(s, o, _); - } - return (o.flags |= 1), ((s = Pg(w, u)).ref = o.ref), (s.return = o), (o.child = s); - } - function bj(s, o, i, u, _) { - if (null !== s) { - var w = s.memoizedProps; - if (Ie(w, u) && s.ref === o.ref) { - if (((_s = !1), (o.pendingProps = u = w), !(s.lanes & _))) - return (o.lanes = s.lanes), Zi(s, o, _); - 131072 & s.flags && (_s = !0); - } - } - return cj(s, o, i, u, _); - } - function dj(s, o, i) { - var u = o.pendingProps, - _ = u.children, - w = null !== s ? s.memoizedState : null; - if ('hidden' === u.mode) - if (1 & o.mode) { - if (!(1073741824 & i)) - return ( - (s = null !== w ? w.baseLanes | i : i), - (o.lanes = o.childLanes = 1073741824), - (o.memoizedState = { baseLanes: s, cachePool: null, transitions: null }), - (o.updateQueue = null), - G(Us, Vs), - (Vs |= s), - null - ); - (o.memoizedState = { baseLanes: 0, cachePool: null, transitions: null }), - (u = null !== w ? w.baseLanes : i), - G(Us, Vs), - (Vs |= u); - } else - (o.memoizedState = { baseLanes: 0, cachePool: null, transitions: null }), - G(Us, Vs), - (Vs |= i); - else - null !== w ? ((u = w.baseLanes | i), (o.memoizedState = null)) : (u = i), - G(Us, Vs), - (Vs |= u); - return Xi(s, o, _, i), o.child; - } - function gj(s, o) { - var i = o.ref; - ((null === s && null !== i) || (null !== s && s.ref !== i)) && - ((o.flags |= 512), (o.flags |= 2097152)); - } - function cj(s, o, i, u, _) { - var w = Zf(i) ? xn : wn.current; - return ( - (w = Yf(o, w)), - ch(o, _), - (i = Nh(s, o, i, u, w, _)), - (u = Sh()), - null === s || _s - ? (Fn && u && vg(o), (o.flags |= 1), Xi(s, o, i, _), o.child) - : ((o.updateQueue = s.updateQueue), - (o.flags &= -2053), - (s.lanes &= ~_), - Zi(s, o, _)) - ); - } - function hj(s, o, i, u, _) { - if (Zf(i)) { - var w = !0; - cg(o); - } else w = !1; - if ((ch(o, _), null === o.stateNode)) ij(s, o), Gi(o, i, u), Ii(o, i, u, _), (u = !0); - else if (null === s) { - var x = o.stateNode, - C = o.memoizedProps; - x.props = C; - var j = x.context, - L = i.contextType; - 'object' == typeof L && null !== L - ? (L = eh(L)) - : (L = Yf(o, (L = Zf(i) ? xn : wn.current))); - var B = i.getDerivedStateFromProps, - $ = 'function' == typeof B || 'function' == typeof x.getSnapshotBeforeUpdate; - $ || - ('function' != typeof x.UNSAFE_componentWillReceiveProps && - 'function' != typeof x.componentWillReceiveProps) || - ((C !== u || j !== L) && Hi(o, x, u, L)), - (Gn = !1); - var V = o.memoizedState; - (x.state = V), - qh(o, u, x, _), - (j = o.memoizedState), - C !== u || V !== j || Sn.current || Gn - ? ('function' == typeof B && (Di(o, i, B, u), (j = o.memoizedState)), - (C = Gn || Fi(o, i, C, u, V, j, L)) - ? ($ || - ('function' != typeof x.UNSAFE_componentWillMount && - 'function' != typeof x.componentWillMount) || - ('function' == typeof x.componentWillMount && x.componentWillMount(), - 'function' == typeof x.UNSAFE_componentWillMount && - x.UNSAFE_componentWillMount()), - 'function' == typeof x.componentDidMount && (o.flags |= 4194308)) - : ('function' == typeof x.componentDidMount && (o.flags |= 4194308), - (o.memoizedProps = u), - (o.memoizedState = j)), - (x.props = u), - (x.state = j), - (x.context = L), - (u = C)) - : ('function' == typeof x.componentDidMount && (o.flags |= 4194308), (u = !1)); - } else { - (x = o.stateNode), - lh(s, o), - (C = o.memoizedProps), - (L = o.type === o.elementType ? C : Ci(o.type, C)), - (x.props = L), - ($ = o.pendingProps), - (V = x.context), - 'object' == typeof (j = i.contextType) && null !== j - ? (j = eh(j)) - : (j = Yf(o, (j = Zf(i) ? xn : wn.current))); - var U = i.getDerivedStateFromProps; - (B = 'function' == typeof U || 'function' == typeof x.getSnapshotBeforeUpdate) || - ('function' != typeof x.UNSAFE_componentWillReceiveProps && - 'function' != typeof x.componentWillReceiveProps) || - ((C !== $ || V !== j) && Hi(o, x, u, j)), - (Gn = !1), - (V = o.memoizedState), - (x.state = V), - qh(o, u, x, _); - var z = o.memoizedState; - C !== $ || V !== z || Sn.current || Gn - ? ('function' == typeof U && (Di(o, i, U, u), (z = o.memoizedState)), - (L = Gn || Fi(o, i, L, u, V, z, j) || !1) - ? (B || - ('function' != typeof x.UNSAFE_componentWillUpdate && - 'function' != typeof x.componentWillUpdate) || - ('function' == typeof x.componentWillUpdate && - x.componentWillUpdate(u, z, j), - 'function' == typeof x.UNSAFE_componentWillUpdate && - x.UNSAFE_componentWillUpdate(u, z, j)), - 'function' == typeof x.componentDidUpdate && (o.flags |= 4), - 'function' == typeof x.getSnapshotBeforeUpdate && (o.flags |= 1024)) - : ('function' != typeof x.componentDidUpdate || - (C === s.memoizedProps && V === s.memoizedState) || - (o.flags |= 4), - 'function' != typeof x.getSnapshotBeforeUpdate || - (C === s.memoizedProps && V === s.memoizedState) || - (o.flags |= 1024), - (o.memoizedProps = u), - (o.memoizedState = z)), - (x.props = u), - (x.state = z), - (x.context = j), - (u = L)) - : ('function' != typeof x.componentDidUpdate || - (C === s.memoizedProps && V === s.memoizedState) || - (o.flags |= 4), - 'function' != typeof x.getSnapshotBeforeUpdate || - (C === s.memoizedProps && V === s.memoizedState) || - (o.flags |= 1024), - (u = !1)); - } - return jj(s, o, i, u, w, _); - } - function jj(s, o, i, u, _, w) { - gj(s, o); - var x = !!(128 & o.flags); - if (!u && !x) return _ && dg(o, i, !1), Zi(s, o, w); - (u = o.stateNode), (bs.current = o); - var C = x && 'function' != typeof i.getDerivedStateFromError ? null : u.render(); - return ( - (o.flags |= 1), - null !== s && x - ? ((o.child = Vn(o, s.child, null, w)), (o.child = Vn(o, null, C, w))) - : Xi(s, o, C, w), - (o.memoizedState = u.state), - _ && dg(o, i, !0), - o.child - ); - } - function kj(s) { - var o = s.stateNode; - o.pendingContext - ? ag(0, o.pendingContext, o.pendingContext !== o.context) - : o.context && ag(0, o.context, !1), - yh(s, o.containerInfo); - } - function lj(s, o, i, u, _) { - return Ig(), Jg(_), (o.flags |= 256), Xi(s, o, i, u), o.child; - } - var Es, - ws, - Ss, - xs, - ks = { dehydrated: null, treeContext: null, retryLane: 0 }; - function nj(s) { - return { baseLanes: s, cachePool: null, transitions: null }; - } - function oj(s, o, i) { - var u, - _ = o.pendingProps, - w = es.current, - x = !1, - C = !!(128 & o.flags); - if ( - ((u = C) || (u = (null === s || null !== s.memoizedState) && !!(2 & w)), - u - ? ((x = !0), (o.flags &= -129)) - : (null !== s && null === s.memoizedState) || (w |= 1), - G(es, 1 & w), - null === s) - ) - return ( - Eg(o), - null !== (s = o.memoizedState) && null !== (s = s.dehydrated) - ? (1 & o.mode - ? '$!' === s.data - ? (o.lanes = 8) - : (o.lanes = 1073741824) - : (o.lanes = 1), - null) - : ((C = _.children), - (s = _.fallback), - x - ? ((_ = o.mode), - (x = o.child), - (C = { mode: 'hidden', children: C }), - 1 & _ || null === x - ? (x = pj(C, _, 0, null)) - : ((x.childLanes = 0), (x.pendingProps = C)), - (s = Tg(s, _, i, null)), - (x.return = o), - (s.return = o), - (x.sibling = s), - (o.child = x), - (o.child.memoizedState = nj(i)), - (o.memoizedState = ks), - s) - : qj(o, C)) - ); - if (null !== (w = s.memoizedState) && null !== (u = w.dehydrated)) - return (function rj(s, o, i, u, _, w, x) { - if (i) - return 256 & o.flags - ? ((o.flags &= -257), sj(s, o, x, (u = Ki(Error(p(422)))))) - : null !== o.memoizedState - ? ((o.child = s.child), (o.flags |= 128), null) - : ((w = u.fallback), - (_ = o.mode), - (u = pj({ mode: 'visible', children: u.children }, _, 0, null)), - ((w = Tg(w, _, x, null)).flags |= 2), - (u.return = o), - (w.return = o), - (u.sibling = w), - (o.child = u), - 1 & o.mode && Vn(o, s.child, null, x), - (o.child.memoizedState = nj(x)), - (o.memoizedState = ks), - w); - if (!(1 & o.mode)) return sj(s, o, x, null); - if ('$!' === _.data) { - if ((u = _.nextSibling && _.nextSibling.dataset)) var C = u.dgst; - return (u = C), sj(s, o, x, (u = Ki((w = Error(p(419))), u, void 0))); - } - if (((C = !!(x & s.childLanes)), _s || C)) { - if (null !== (u = Fs)) { - switch (x & -x) { - case 4: - _ = 2; - break; - case 16: - _ = 8; - break; - case 64: - case 128: - case 256: - case 512: - case 1024: - case 2048: - case 4096: - case 8192: - case 16384: - case 32768: - case 65536: - case 131072: - case 262144: - case 524288: - case 1048576: - case 2097152: - case 4194304: - case 8388608: - case 16777216: - case 33554432: - case 67108864: - _ = 32; - break; - case 536870912: - _ = 268435456; - break; - default: - _ = 0; - } - 0 !== (_ = _ & (u.suspendedLanes | x) ? 0 : _) && - _ !== w.retryLane && - ((w.retryLane = _), ih(s, _), gi(u, s, _, -1)); - } - return tj(), sj(s, o, x, (u = Ki(Error(p(421))))); - } - return '$?' === _.data - ? ((o.flags |= 128), - (o.child = s.child), - (o = uj.bind(null, s)), - (_._reactRetry = o), - null) - : ((s = w.treeContext), - (Bn = Lf(_.nextSibling)), - (Ln = o), - (Fn = !0), - (qn = null), - null !== s && - ((Mn[Tn++] = Rn), - (Mn[Tn++] = Dn), - (Mn[Tn++] = Nn), - (Rn = s.id), - (Dn = s.overflow), - (Nn = o)), - (o = qj(o, u.children)), - (o.flags |= 4096), - o); - })(s, o, C, _, u, w, i); - if (x) { - (x = _.fallback), (C = o.mode), (u = (w = s.child).sibling); - var j = { mode: 'hidden', children: _.children }; - return ( - 1 & C || o.child === w - ? ((_ = Pg(w, j)).subtreeFlags = 14680064 & w.subtreeFlags) - : (((_ = o.child).childLanes = 0), (_.pendingProps = j), (o.deletions = null)), - null !== u ? (x = Pg(u, x)) : ((x = Tg(x, C, i, null)).flags |= 2), - (x.return = o), - (_.return = o), - (_.sibling = x), - (o.child = _), - (_ = x), - (x = o.child), - (C = - null === (C = s.child.memoizedState) - ? nj(i) - : { baseLanes: C.baseLanes | i, cachePool: null, transitions: C.transitions }), - (x.memoizedState = C), - (x.childLanes = s.childLanes & ~i), - (o.memoizedState = ks), - _ - ); - } - return ( - (s = (x = s.child).sibling), - (_ = Pg(x, { mode: 'visible', children: _.children })), - !(1 & o.mode) && (_.lanes = i), - (_.return = o), - (_.sibling = null), - null !== s && - (null === (i = o.deletions) ? ((o.deletions = [s]), (o.flags |= 16)) : i.push(s)), - (o.child = _), - (o.memoizedState = null), - _ - ); - } - function qj(s, o) { - return ( - ((o = pj({ mode: 'visible', children: o }, s.mode, 0, null)).return = s), - (s.child = o) - ); - } - function sj(s, o, i, u) { - return ( - null !== u && Jg(u), - Vn(o, s.child, null, i), - ((s = qj(o, o.pendingProps.children)).flags |= 2), - (o.memoizedState = null), - s - ); - } - function vj(s, o, i) { - s.lanes |= o; - var u = s.alternate; - null !== u && (u.lanes |= o), bh(s.return, o, i); - } - function wj(s, o, i, u, _) { - var w = s.memoizedState; - null === w - ? (s.memoizedState = { - isBackwards: o, - rendering: null, - renderingStartTime: 0, - last: u, - tail: i, - tailMode: _ - }) - : ((w.isBackwards = o), - (w.rendering = null), - (w.renderingStartTime = 0), - (w.last = u), - (w.tail = i), - (w.tailMode = _)); - } - function xj(s, o, i) { - var u = o.pendingProps, - _ = u.revealOrder, - w = u.tail; - if ((Xi(s, o, u.children, i), 2 & (u = es.current))) - (u = (1 & u) | 2), (o.flags |= 128); - else { - if (null !== s && 128 & s.flags) - e: for (s = o.child; null !== s; ) { - if (13 === s.tag) null !== s.memoizedState && vj(s, i, o); - else if (19 === s.tag) vj(s, i, o); - else if (null !== s.child) { - (s.child.return = s), (s = s.child); - continue; - } - if (s === o) break e; - for (; null === s.sibling; ) { - if (null === s.return || s.return === o) break e; - s = s.return; - } - (s.sibling.return = s.return), (s = s.sibling); - } - u &= 1; - } - if ((G(es, u), 1 & o.mode)) - switch (_) { - case 'forwards': - for (i = o.child, _ = null; null !== i; ) - null !== (s = i.alternate) && null === Ch(s) && (_ = i), (i = i.sibling); - null === (i = _) - ? ((_ = o.child), (o.child = null)) - : ((_ = i.sibling), (i.sibling = null)), - wj(o, !1, _, i, w); - break; - case 'backwards': - for (i = null, _ = o.child, o.child = null; null !== _; ) { - if (null !== (s = _.alternate) && null === Ch(s)) { - o.child = _; - break; - } - (s = _.sibling), (_.sibling = i), (i = _), (_ = s); - } - wj(o, !0, i, null, w); - break; - case 'together': - wj(o, !1, null, null, void 0); - break; - default: - o.memoizedState = null; - } - else o.memoizedState = null; - return o.child; - } - function ij(s, o) { - !(1 & o.mode) && - null !== s && - ((s.alternate = null), (o.alternate = null), (o.flags |= 2)); - } - function Zi(s, o, i) { - if ( - (null !== s && (o.dependencies = s.dependencies), - (Ks |= o.lanes), - !(i & o.childLanes)) - ) - return null; - if (null !== s && o.child !== s.child) throw Error(p(153)); - if (null !== o.child) { - for ( - i = Pg((s = o.child), s.pendingProps), o.child = i, i.return = o; - null !== s.sibling; - - ) - (s = s.sibling), ((i = i.sibling = Pg(s, s.pendingProps)).return = o); - i.sibling = null; - } - return o.child; - } - function Dj(s, o) { - if (!Fn) - switch (s.tailMode) { - case 'hidden': - o = s.tail; - for (var i = null; null !== o; ) null !== o.alternate && (i = o), (o = o.sibling); - null === i ? (s.tail = null) : (i.sibling = null); - break; - case 'collapsed': - i = s.tail; - for (var u = null; null !== i; ) null !== i.alternate && (u = i), (i = i.sibling); - null === u - ? o || null === s.tail - ? (s.tail = null) - : (s.tail.sibling = null) - : (u.sibling = null); - } - } - function S(s) { - var o = null !== s.alternate && s.alternate.child === s.child, - i = 0, - u = 0; - if (o) - for (var _ = s.child; null !== _; ) - (i |= _.lanes | _.childLanes), - (u |= 14680064 & _.subtreeFlags), - (u |= 14680064 & _.flags), - (_.return = s), - (_ = _.sibling); - else - for (_ = s.child; null !== _; ) - (i |= _.lanes | _.childLanes), - (u |= _.subtreeFlags), - (u |= _.flags), - (_.return = s), - (_ = _.sibling); - return (s.subtreeFlags |= u), (s.childLanes = i), o; - } - function Ej(s, o, i) { - var u = o.pendingProps; - switch ((wg(o), o.tag)) { - case 2: - case 16: - case 15: - case 0: - case 11: - case 7: - case 8: - case 12: - case 9: - case 14: - return S(o), null; - case 1: - case 17: - return Zf(o.type) && $f(), S(o), null; - case 3: - return ( - (u = o.stateNode), - zh(), - E(Sn), - E(wn), - Eh(), - u.pendingContext && ((u.context = u.pendingContext), (u.pendingContext = null)), - (null !== s && null !== s.child) || - (Gg(o) - ? (o.flags |= 4) - : null === s || - (s.memoizedState.isDehydrated && !(256 & o.flags)) || - ((o.flags |= 1024), null !== qn && (Fj(qn), (qn = null)))), - ws(s, o), - S(o), - null - ); - case 5: - Bh(o); - var _ = xh(Qn.current); - if (((i = o.type), null !== s && null != o.stateNode)) - Ss(s, o, i, u, _), s.ref !== o.ref && ((o.flags |= 512), (o.flags |= 2097152)); - else { - if (!u) { - if (null === o.stateNode) throw Error(p(166)); - return S(o), null; - } - if (((s = xh(Xn.current)), Gg(o))) { - (u = o.stateNode), (i = o.type); - var w = o.memoizedProps; - switch (((u[dn] = o), (u[fn] = w), (s = !!(1 & o.mode)), i)) { - case 'dialog': - D('cancel', u), D('close', u); - break; - case 'iframe': - case 'object': - case 'embed': - D('load', u); - break; - case 'video': - case 'audio': - for (_ = 0; _ < en.length; _++) D(en[_], u); - break; - case 'source': - D('error', u); - break; - case 'img': - case 'image': - case 'link': - D('error', u), D('load', u); - break; - case 'details': - D('toggle', u); - break; - case 'input': - Za(u, w), D('invalid', u); - break; - case 'select': - (u._wrapperState = { wasMultiple: !!w.multiple }), D('invalid', u); - break; - case 'textarea': - hb(u, w), D('invalid', u); - } - for (var C in (ub(i, w), (_ = null), w)) - if (w.hasOwnProperty(C)) { - var j = w[C]; - 'children' === C - ? 'string' == typeof j - ? u.textContent !== j && - (!0 !== w.suppressHydrationWarning && Af(u.textContent, j, s), - (_ = ['children', j])) - : 'number' == typeof j && - u.textContent !== '' + j && - (!0 !== w.suppressHydrationWarning && Af(u.textContent, j, s), - (_ = ['children', '' + j])) - : x.hasOwnProperty(C) && null != j && 'onScroll' === C && D('scroll', u); - } - switch (i) { - case 'input': - Va(u), db(u, w, !0); - break; - case 'textarea': - Va(u), jb(u); - break; - case 'select': - case 'option': - break; - default: - 'function' == typeof w.onClick && (u.onclick = Bf); - } - (u = _), (o.updateQueue = u), null !== u && (o.flags |= 4); - } else { - (C = 9 === _.nodeType ? _ : _.ownerDocument), - 'http://www.w3.org/1999/xhtml' === s && (s = kb(i)), - 'http://www.w3.org/1999/xhtml' === s - ? 'script' === i - ? (((s = C.createElement('div')).innerHTML = ''), - (s = s.removeChild(s.firstChild))) - : 'string' == typeof u.is - ? (s = C.createElement(i, { is: u.is })) - : ((s = C.createElement(i)), - 'select' === i && - ((C = s), - u.multiple ? (C.multiple = !0) : u.size && (C.size = u.size))) - : (s = C.createElementNS(s, i)), - (s[dn] = o), - (s[fn] = u), - Es(s, o, !1, !1), - (o.stateNode = s); - e: { - switch (((C = vb(i, u)), i)) { - case 'dialog': - D('cancel', s), D('close', s), (_ = u); - break; - case 'iframe': - case 'object': - case 'embed': - D('load', s), (_ = u); - break; - case 'video': - case 'audio': - for (_ = 0; _ < en.length; _++) D(en[_], s); - _ = u; - break; - case 'source': - D('error', s), (_ = u); - break; - case 'img': - case 'image': - case 'link': - D('error', s), D('load', s), (_ = u); - break; - case 'details': - D('toggle', s), (_ = u); - break; - case 'input': - Za(s, u), (_ = Ya(s, u)), D('invalid', s); - break; - case 'option': - default: - _ = u; - break; - case 'select': - (s._wrapperState = { wasMultiple: !!u.multiple }), - (_ = xe({}, u, { value: void 0 })), - D('invalid', s); - break; - case 'textarea': - hb(s, u), (_ = gb(s, u)), D('invalid', s); - } - for (w in (ub(i, _), (j = _))) - if (j.hasOwnProperty(w)) { - var L = j[w]; - 'style' === w - ? sb(s, L) - : 'dangerouslySetInnerHTML' === w - ? null != (L = L ? L.__html : void 0) && $e(s, L) - : 'children' === w - ? 'string' == typeof L - ? ('textarea' !== i || '' !== L) && ob(s, L) - : 'number' == typeof L && ob(s, '' + L) - : 'suppressContentEditableWarning' !== w && - 'suppressHydrationWarning' !== w && - 'autoFocus' !== w && - (x.hasOwnProperty(w) - ? null != L && 'onScroll' === w && D('scroll', s) - : null != L && ta(s, w, L, C)); - } - switch (i) { - case 'input': - Va(s), db(s, u, !1); - break; - case 'textarea': - Va(s), jb(s); - break; - case 'option': - null != u.value && s.setAttribute('value', '' + Sa(u.value)); - break; - case 'select': - (s.multiple = !!u.multiple), - null != (w = u.value) - ? fb(s, !!u.multiple, w, !1) - : null != u.defaultValue && fb(s, !!u.multiple, u.defaultValue, !0); - break; - default: - 'function' == typeof _.onClick && (s.onclick = Bf); - } - switch (i) { - case 'button': - case 'input': - case 'select': - case 'textarea': - u = !!u.autoFocus; - break e; - case 'img': - u = !0; - break e; - default: - u = !1; - } - } - u && (o.flags |= 4); - } - null !== o.ref && ((o.flags |= 512), (o.flags |= 2097152)); - } - return S(o), null; - case 6: - if (s && null != o.stateNode) xs(s, o, s.memoizedProps, u); - else { - if ('string' != typeof u && null === o.stateNode) throw Error(p(166)); - if (((i = xh(Qn.current)), xh(Xn.current), Gg(o))) { - if ( - ((u = o.stateNode), - (i = o.memoizedProps), - (u[dn] = o), - (w = u.nodeValue !== i) && null !== (s = Ln)) - ) - switch (s.tag) { - case 3: - Af(u.nodeValue, i, !!(1 & s.mode)); - break; - case 5: - !0 !== s.memoizedProps.suppressHydrationWarning && - Af(u.nodeValue, i, !!(1 & s.mode)); - } - w && (o.flags |= 4); - } else - ((u = (9 === i.nodeType ? i : i.ownerDocument).createTextNode(u))[dn] = o), - (o.stateNode = u); - } - return S(o), null; - case 13: - if ( - (E(es), - (u = o.memoizedState), - null === s || (null !== s.memoizedState && null !== s.memoizedState.dehydrated)) - ) { - if (Fn && null !== Bn && 1 & o.mode && !(128 & o.flags)) - Hg(), Ig(), (o.flags |= 98560), (w = !1); - else if (((w = Gg(o)), null !== u && null !== u.dehydrated)) { - if (null === s) { - if (!w) throw Error(p(318)); - if (!(w = null !== (w = o.memoizedState) ? w.dehydrated : null)) - throw Error(p(317)); - w[dn] = o; - } else Ig(), !(128 & o.flags) && (o.memoizedState = null), (o.flags |= 4); - S(o), (w = !1); - } else null !== qn && (Fj(qn), (qn = null)), (w = !0); - if (!w) return 65536 & o.flags ? o : null; - } - return 128 & o.flags - ? ((o.lanes = i), o) - : ((u = null !== u) !== (null !== s && null !== s.memoizedState) && - u && - ((o.child.flags |= 8192), - 1 & o.mode && (null === s || 1 & es.current ? 0 === zs && (zs = 3) : tj())), - null !== o.updateQueue && (o.flags |= 4), - S(o), - null); - case 4: - return zh(), ws(s, o), null === s && sf(o.stateNode.containerInfo), S(o), null; - case 10: - return ah(o.type._context), S(o), null; - case 19: - if ((E(es), null === (w = o.memoizedState))) return S(o), null; - if (((u = !!(128 & o.flags)), null === (C = w.rendering))) - if (u) Dj(w, !1); - else { - if (0 !== zs || (null !== s && 128 & s.flags)) - for (s = o.child; null !== s; ) { - if (null !== (C = Ch(s))) { - for ( - o.flags |= 128, - Dj(w, !1), - null !== (u = C.updateQueue) && ((o.updateQueue = u), (o.flags |= 4)), - o.subtreeFlags = 0, - u = i, - i = o.child; - null !== i; - - ) - (s = u), - ((w = i).flags &= 14680066), - null === (C = w.alternate) - ? ((w.childLanes = 0), - (w.lanes = s), - (w.child = null), - (w.subtreeFlags = 0), - (w.memoizedProps = null), - (w.memoizedState = null), - (w.updateQueue = null), - (w.dependencies = null), - (w.stateNode = null)) - : ((w.childLanes = C.childLanes), - (w.lanes = C.lanes), - (w.child = C.child), - (w.subtreeFlags = 0), - (w.deletions = null), - (w.memoizedProps = C.memoizedProps), - (w.memoizedState = C.memoizedState), - (w.updateQueue = C.updateQueue), - (w.type = C.type), - (s = C.dependencies), - (w.dependencies = - null === s - ? null - : { lanes: s.lanes, firstContext: s.firstContext })), - (i = i.sibling); - return G(es, (1 & es.current) | 2), o.child; - } - s = s.sibling; - } - null !== w.tail && - dt() > Zs && - ((o.flags |= 128), (u = !0), Dj(w, !1), (o.lanes = 4194304)); - } - else { - if (!u) - if (null !== (s = Ch(C))) { - if ( - ((o.flags |= 128), - (u = !0), - null !== (i = s.updateQueue) && ((o.updateQueue = i), (o.flags |= 4)), - Dj(w, !0), - null === w.tail && 'hidden' === w.tailMode && !C.alternate && !Fn) - ) - return S(o), null; - } else - 2 * dt() - w.renderingStartTime > Zs && - 1073741824 !== i && - ((o.flags |= 128), (u = !0), Dj(w, !1), (o.lanes = 4194304)); - w.isBackwards - ? ((C.sibling = o.child), (o.child = C)) - : (null !== (i = w.last) ? (i.sibling = C) : (o.child = C), (w.last = C)); - } - return null !== w.tail - ? ((o = w.tail), - (w.rendering = o), - (w.tail = o.sibling), - (w.renderingStartTime = dt()), - (o.sibling = null), - (i = es.current), - G(es, u ? (1 & i) | 2 : 1 & i), - o) - : (S(o), null); - case 22: - case 23: - return ( - Hj(), - (u = null !== o.memoizedState), - null !== s && (null !== s.memoizedState) !== u && (o.flags |= 8192), - u && 1 & o.mode - ? !!(1073741824 & Vs) && (S(o), 6 & o.subtreeFlags && (o.flags |= 8192)) - : S(o), - null - ); - case 24: - case 25: - return null; - } - throw Error(p(156, o.tag)); - } - function Ij(s, o) { - switch ((wg(o), o.tag)) { - case 1: - return ( - Zf(o.type) && $f(), - 65536 & (s = o.flags) ? ((o.flags = (-65537 & s) | 128), o) : null - ); - case 3: - return ( - zh(), - E(Sn), - E(wn), - Eh(), - 65536 & (s = o.flags) && !(128 & s) ? ((o.flags = (-65537 & s) | 128), o) : null - ); - case 5: - return Bh(o), null; - case 13: - if ((E(es), null !== (s = o.memoizedState) && null !== s.dehydrated)) { - if (null === o.alternate) throw Error(p(340)); - Ig(); - } - return 65536 & (s = o.flags) ? ((o.flags = (-65537 & s) | 128), o) : null; - case 19: - return E(es), null; - case 4: - return zh(), null; - case 10: - return ah(o.type._context), null; - case 22: - case 23: - return Hj(), null; - default: - return null; - } - } - (Es = function (s, o) { - for (var i = o.child; null !== i; ) { - if (5 === i.tag || 6 === i.tag) s.appendChild(i.stateNode); - else if (4 !== i.tag && null !== i.child) { - (i.child.return = i), (i = i.child); - continue; - } - if (i === o) break; - for (; null === i.sibling; ) { - if (null === i.return || i.return === o) return; - i = i.return; - } - (i.sibling.return = i.return), (i = i.sibling); - } - }), - (ws = function () {}), - (Ss = function (s, o, i, u) { - var _ = s.memoizedProps; - if (_ !== u) { - (s = o.stateNode), xh(Xn.current); - var w, - C = null; - switch (i) { - case 'input': - (_ = Ya(s, _)), (u = Ya(s, u)), (C = []); - break; - case 'select': - (_ = xe({}, _, { value: void 0 })), - (u = xe({}, u, { value: void 0 })), - (C = []); - break; - case 'textarea': - (_ = gb(s, _)), (u = gb(s, u)), (C = []); - break; - default: - 'function' != typeof _.onClick && - 'function' == typeof u.onClick && - (s.onclick = Bf); - } - for (B in (ub(i, u), (i = null), _)) - if (!u.hasOwnProperty(B) && _.hasOwnProperty(B) && null != _[B]) - if ('style' === B) { - var j = _[B]; - for (w in j) j.hasOwnProperty(w) && (i || (i = {}), (i[w] = '')); - } else - 'dangerouslySetInnerHTML' !== B && - 'children' !== B && - 'suppressContentEditableWarning' !== B && - 'suppressHydrationWarning' !== B && - 'autoFocus' !== B && - (x.hasOwnProperty(B) ? C || (C = []) : (C = C || []).push(B, null)); - for (B in u) { - var L = u[B]; - if ( - ((j = null != _ ? _[B] : void 0), - u.hasOwnProperty(B) && L !== j && (null != L || null != j)) - ) - if ('style' === B) - if (j) { - for (w in j) - !j.hasOwnProperty(w) || - (L && L.hasOwnProperty(w)) || - (i || (i = {}), (i[w] = '')); - for (w in L) - L.hasOwnProperty(w) && j[w] !== L[w] && (i || (i = {}), (i[w] = L[w])); - } else i || (C || (C = []), C.push(B, i)), (i = L); - else - 'dangerouslySetInnerHTML' === B - ? ((L = L ? L.__html : void 0), - (j = j ? j.__html : void 0), - null != L && j !== L && (C = C || []).push(B, L)) - : 'children' === B - ? ('string' != typeof L && 'number' != typeof L) || - (C = C || []).push(B, '' + L) - : 'suppressContentEditableWarning' !== B && - 'suppressHydrationWarning' !== B && - (x.hasOwnProperty(B) - ? (null != L && 'onScroll' === B && D('scroll', s), - C || j === L || (C = [])) - : (C = C || []).push(B, L)); - } - i && (C = C || []).push('style', i); - var B = C; - (o.updateQueue = B) && (o.flags |= 4); - } - }), - (xs = function (s, o, i, u) { - i !== u && (o.flags |= 4); - }); - var Cs = !1, - Os = !1, - As = 'function' == typeof WeakSet ? WeakSet : Set, - js = null; - function Lj(s, o) { - var i = s.ref; - if (null !== i) - if ('function' == typeof i) - try { - i(null); - } catch (i) { - W(s, o, i); - } - else i.current = null; - } - function Mj(s, o, i) { - try { - i(); - } catch (i) { - W(s, o, i); - } - } - var Is = !1; - function Pj(s, o, i) { - var u = o.updateQueue; - if (null !== (u = null !== u ? u.lastEffect : null)) { - var _ = (u = u.next); - do { - if ((_.tag & s) === s) { - var w = _.destroy; - (_.destroy = void 0), void 0 !== w && Mj(o, i, w); - } - _ = _.next; - } while (_ !== u); - } - } - function Qj(s, o) { - if (null !== (o = null !== (o = o.updateQueue) ? o.lastEffect : null)) { - var i = (o = o.next); - do { - if ((i.tag & s) === s) { - var u = i.create; - i.destroy = u(); - } - i = i.next; - } while (i !== o); - } - } - function Rj(s) { - var o = s.ref; - if (null !== o) { - var i = s.stateNode; - s.tag, (s = i), 'function' == typeof o ? o(s) : (o.current = s); - } - } - function Sj(s) { - var o = s.alternate; - null !== o && ((s.alternate = null), Sj(o)), - (s.child = null), - (s.deletions = null), - (s.sibling = null), - 5 === s.tag && - null !== (o = s.stateNode) && - (delete o[dn], delete o[fn], delete o[gn], delete o[yn], delete o[vn]), - (s.stateNode = null), - (s.return = null), - (s.dependencies = null), - (s.memoizedProps = null), - (s.memoizedState = null), - (s.pendingProps = null), - (s.stateNode = null), - (s.updateQueue = null); - } - function Tj(s) { - return 5 === s.tag || 3 === s.tag || 4 === s.tag; - } - function Uj(s) { - e: for (;;) { - for (; null === s.sibling; ) { - if (null === s.return || Tj(s.return)) return null; - s = s.return; - } - for ( - s.sibling.return = s.return, s = s.sibling; - 5 !== s.tag && 6 !== s.tag && 18 !== s.tag; - - ) { - if (2 & s.flags) continue e; - if (null === s.child || 4 === s.tag) continue e; - (s.child.return = s), (s = s.child); - } - if (!(2 & s.flags)) return s.stateNode; - } - } - function Vj(s, o, i) { - var u = s.tag; - if (5 === u || 6 === u) - (s = s.stateNode), - o - ? 8 === i.nodeType - ? i.parentNode.insertBefore(s, o) - : i.insertBefore(s, o) - : (8 === i.nodeType - ? (o = i.parentNode).insertBefore(s, i) - : (o = i).appendChild(s), - null != (i = i._reactRootContainer) || null !== o.onclick || (o.onclick = Bf)); - else if (4 !== u && null !== (s = s.child)) - for (Vj(s, o, i), s = s.sibling; null !== s; ) Vj(s, o, i), (s = s.sibling); - } - function Wj(s, o, i) { - var u = s.tag; - if (5 === u || 6 === u) (s = s.stateNode), o ? i.insertBefore(s, o) : i.appendChild(s); - else if (4 !== u && null !== (s = s.child)) - for (Wj(s, o, i), s = s.sibling; null !== s; ) Wj(s, o, i), (s = s.sibling); - } - var Ps = null, - Ms = !1; - function Yj(s, o, i) { - for (i = i.child; null !== i; ) Zj(s, o, i), (i = i.sibling); - } - function Zj(s, o, i) { - if (wt && 'function' == typeof wt.onCommitFiberUnmount) - try { - wt.onCommitFiberUnmount(Et, i); - } catch (s) {} - switch (i.tag) { - case 5: - Os || Lj(i, o); - case 6: - var u = Ps, - _ = Ms; - (Ps = null), - Yj(s, o, i), - (Ms = _), - null !== (Ps = u) && - (Ms - ? ((s = Ps), - (i = i.stateNode), - 8 === s.nodeType ? s.parentNode.removeChild(i) : s.removeChild(i)) - : Ps.removeChild(i.stateNode)); - break; - case 18: - null !== Ps && - (Ms - ? ((s = Ps), - (i = i.stateNode), - 8 === s.nodeType ? Kf(s.parentNode, i) : 1 === s.nodeType && Kf(s, i), - bd(s)) - : Kf(Ps, i.stateNode)); - break; - case 4: - (u = Ps), - (_ = Ms), - (Ps = i.stateNode.containerInfo), - (Ms = !0), - Yj(s, o, i), - (Ps = u), - (Ms = _); - break; - case 0: - case 11: - case 14: - case 15: - if (!Os && null !== (u = i.updateQueue) && null !== (u = u.lastEffect)) { - _ = u = u.next; - do { - var w = _, - x = w.destroy; - (w = w.tag), void 0 !== x && (2 & w || 4 & w) && Mj(i, o, x), (_ = _.next); - } while (_ !== u); - } - Yj(s, o, i); - break; - case 1: - if (!Os && (Lj(i, o), 'function' == typeof (u = i.stateNode).componentWillUnmount)) - try { - (u.props = i.memoizedProps), - (u.state = i.memoizedState), - u.componentWillUnmount(); - } catch (s) { - W(i, o, s); - } - Yj(s, o, i); - break; - case 21: - Yj(s, o, i); - break; - case 22: - 1 & i.mode - ? ((Os = (u = Os) || null !== i.memoizedState), Yj(s, o, i), (Os = u)) - : Yj(s, o, i); - break; - default: - Yj(s, o, i); - } - } - function ak(s) { - var o = s.updateQueue; - if (null !== o) { - s.updateQueue = null; - var i = s.stateNode; - null === i && (i = s.stateNode = new As()), - o.forEach(function (o) { - var u = bk.bind(null, s, o); - i.has(o) || (i.add(o), o.then(u, u)); - }); - } - } - function ck(s, o) { - var i = o.deletions; - if (null !== i) - for (var u = 0; u < i.length; u++) { - var _ = i[u]; - try { - var w = s, - x = o, - C = x; - e: for (; null !== C; ) { - switch (C.tag) { - case 5: - (Ps = C.stateNode), (Ms = !1); - break e; - case 3: - case 4: - (Ps = C.stateNode.containerInfo), (Ms = !0); - break e; - } - C = C.return; - } - if (null === Ps) throw Error(p(160)); - Zj(w, x, _), (Ps = null), (Ms = !1); - var j = _.alternate; - null !== j && (j.return = null), (_.return = null); - } catch (s) { - W(_, o, s); - } - } - if (12854 & o.subtreeFlags) for (o = o.child; null !== o; ) dk(o, s), (o = o.sibling); - } - function dk(s, o) { - var i = s.alternate, - u = s.flags; - switch (s.tag) { - case 0: - case 11: - case 14: - case 15: - if ((ck(o, s), ek(s), 4 & u)) { - try { - Pj(3, s, s.return), Qj(3, s); - } catch (o) { - W(s, s.return, o); - } - try { - Pj(5, s, s.return); - } catch (o) { - W(s, s.return, o); - } - } - break; - case 1: - ck(o, s), ek(s), 512 & u && null !== i && Lj(i, i.return); - break; - case 5: - if ((ck(o, s), ek(s), 512 & u && null !== i && Lj(i, i.return), 32 & s.flags)) { - var _ = s.stateNode; - try { - ob(_, ''); - } catch (o) { - W(s, s.return, o); - } - } - if (4 & u && null != (_ = s.stateNode)) { - var w = s.memoizedProps, - x = null !== i ? i.memoizedProps : w, - C = s.type, - j = s.updateQueue; - if (((s.updateQueue = null), null !== j)) - try { - 'input' === C && 'radio' === w.type && null != w.name && ab(_, w), vb(C, x); - var L = vb(C, w); - for (x = 0; x < j.length; x += 2) { - var B = j[x], - $ = j[x + 1]; - 'style' === B - ? sb(_, $) - : 'dangerouslySetInnerHTML' === B - ? $e(_, $) - : 'children' === B - ? ob(_, $) - : ta(_, B, $, L); - } - switch (C) { - case 'input': - bb(_, w); - break; - case 'textarea': - ib(_, w); - break; - case 'select': - var V = _._wrapperState.wasMultiple; - _._wrapperState.wasMultiple = !!w.multiple; - var U = w.value; - null != U - ? fb(_, !!w.multiple, U, !1) - : V !== !!w.multiple && - (null != w.defaultValue - ? fb(_, !!w.multiple, w.defaultValue, !0) - : fb(_, !!w.multiple, w.multiple ? [] : '', !1)); - } - _[fn] = w; - } catch (o) { - W(s, s.return, o); - } - } - break; - case 6: - if ((ck(o, s), ek(s), 4 & u)) { - if (null === s.stateNode) throw Error(p(162)); - (_ = s.stateNode), (w = s.memoizedProps); - try { - _.nodeValue = w; - } catch (o) { - W(s, s.return, o); - } - } - break; - case 3: - if ((ck(o, s), ek(s), 4 & u && null !== i && i.memoizedState.isDehydrated)) - try { - bd(o.containerInfo); - } catch (o) { - W(s, s.return, o); - } - break; - case 4: - default: - ck(o, s), ek(s); - break; - case 13: - ck(o, s), - ek(s), - 8192 & (_ = s.child).flags && - ((w = null !== _.memoizedState), - (_.stateNode.isHidden = w), - !w || - (null !== _.alternate && null !== _.alternate.memoizedState) || - (Xs = dt())), - 4 & u && ak(s); - break; - case 22: - if ( - ((B = null !== i && null !== i.memoizedState), - 1 & s.mode ? ((Os = (L = Os) || B), ck(o, s), (Os = L)) : ck(o, s), - ek(s), - 8192 & u) - ) { - if ( - ((L = null !== s.memoizedState), (s.stateNode.isHidden = L) && !B && 1 & s.mode) - ) - for (js = s, B = s.child; null !== B; ) { - for ($ = js = B; null !== js; ) { - switch (((U = (V = js).child), V.tag)) { - case 0: - case 11: - case 14: - case 15: - Pj(4, V, V.return); - break; - case 1: - Lj(V, V.return); - var z = V.stateNode; - if ('function' == typeof z.componentWillUnmount) { - (u = V), (i = V.return); - try { - (o = u), - (z.props = o.memoizedProps), - (z.state = o.memoizedState), - z.componentWillUnmount(); - } catch (s) { - W(u, i, s); - } - } - break; - case 5: - Lj(V, V.return); - break; - case 22: - if (null !== V.memoizedState) { - gk($); - continue; - } - } - null !== U ? ((U.return = V), (js = U)) : gk($); - } - B = B.sibling; - } - e: for (B = null, $ = s; ; ) { - if (5 === $.tag) { - if (null === B) { - B = $; - try { - (_ = $.stateNode), - L - ? 'function' == typeof (w = _.style).setProperty - ? w.setProperty('display', 'none', 'important') - : (w.display = 'none') - : ((C = $.stateNode), - (x = - null != (j = $.memoizedProps.style) && j.hasOwnProperty('display') - ? j.display - : null), - (C.style.display = rb('display', x))); - } catch (o) { - W(s, s.return, o); - } - } - } else if (6 === $.tag) { - if (null === B) - try { - $.stateNode.nodeValue = L ? '' : $.memoizedProps; - } catch (o) { - W(s, s.return, o); - } - } else if ( - ((22 !== $.tag && 23 !== $.tag) || null === $.memoizedState || $ === s) && - null !== $.child - ) { - ($.child.return = $), ($ = $.child); - continue; - } - if ($ === s) break e; - for (; null === $.sibling; ) { - if (null === $.return || $.return === s) break e; - B === $ && (B = null), ($ = $.return); - } - B === $ && (B = null), ($.sibling.return = $.return), ($ = $.sibling); - } - } - break; - case 19: - ck(o, s), ek(s), 4 & u && ak(s); - case 21: - } - } - function ek(s) { - var o = s.flags; - if (2 & o) { - try { - e: { - for (var i = s.return; null !== i; ) { - if (Tj(i)) { - var u = i; - break e; - } - i = i.return; - } - throw Error(p(160)); - } - switch (u.tag) { - case 5: - var _ = u.stateNode; - 32 & u.flags && (ob(_, ''), (u.flags &= -33)), Wj(s, Uj(s), _); - break; - case 3: - case 4: - var w = u.stateNode.containerInfo; - Vj(s, Uj(s), w); - break; - default: - throw Error(p(161)); - } - } catch (o) { - W(s, s.return, o); - } - s.flags &= -3; - } - 4096 & o && (s.flags &= -4097); - } - function hk(s, o, i) { - (js = s), ik(s, o, i); - } - function ik(s, o, i) { - for (var u = !!(1 & s.mode); null !== js; ) { - var _ = js, - w = _.child; - if (22 === _.tag && u) { - var x = null !== _.memoizedState || Cs; - if (!x) { - var C = _.alternate, - j = (null !== C && null !== C.memoizedState) || Os; - C = Cs; - var L = Os; - if (((Cs = x), (Os = j) && !L)) - for (js = _; null !== js; ) - (j = (x = js).child), - 22 === x.tag && null !== x.memoizedState - ? jk(_) - : null !== j - ? ((j.return = x), (js = j)) - : jk(_); - for (; null !== w; ) (js = w), ik(w, o, i), (w = w.sibling); - (js = _), (Cs = C), (Os = L); - } - kk(s); - } else 8772 & _.subtreeFlags && null !== w ? ((w.return = _), (js = w)) : kk(s); - } - } - function kk(s) { - for (; null !== js; ) { - var o = js; - if (8772 & o.flags) { - var i = o.alternate; - try { - if (8772 & o.flags) - switch (o.tag) { - case 0: - case 11: - case 15: - Os || Qj(5, o); - break; - case 1: - var u = o.stateNode; - if (4 & o.flags && !Os) - if (null === i) u.componentDidMount(); - else { - var _ = - o.elementType === o.type - ? i.memoizedProps - : Ci(o.type, i.memoizedProps); - u.componentDidUpdate( - _, - i.memoizedState, - u.__reactInternalSnapshotBeforeUpdate - ); - } - var w = o.updateQueue; - null !== w && sh(o, w, u); - break; - case 3: - var x = o.updateQueue; - if (null !== x) { - if (((i = null), null !== o.child)) - switch (o.child.tag) { - case 5: - case 1: - i = o.child.stateNode; - } - sh(o, x, i); - } - break; - case 5: - var C = o.stateNode; - if (null === i && 4 & o.flags) { - i = C; - var j = o.memoizedProps; - switch (o.type) { - case 'button': - case 'input': - case 'select': - case 'textarea': - j.autoFocus && i.focus(); - break; - case 'img': - j.src && (i.src = j.src); - } - } - break; - case 6: - case 4: - case 12: - case 19: - case 17: - case 21: - case 22: - case 23: - case 25: - break; - case 13: - if (null === o.memoizedState) { - var L = o.alternate; - if (null !== L) { - var B = L.memoizedState; - if (null !== B) { - var $ = B.dehydrated; - null !== $ && bd($); - } - } - } - break; - default: - throw Error(p(163)); - } - Os || (512 & o.flags && Rj(o)); - } catch (s) { - W(o, o.return, s); - } - } - if (o === s) { - js = null; - break; - } - if (null !== (i = o.sibling)) { - (i.return = o.return), (js = i); - break; - } - js = o.return; - } - } - function gk(s) { - for (; null !== js; ) { - var o = js; - if (o === s) { - js = null; - break; - } - var i = o.sibling; - if (null !== i) { - (i.return = o.return), (js = i); - break; - } - js = o.return; - } - } - function jk(s) { - for (; null !== js; ) { - var o = js; - try { - switch (o.tag) { - case 0: - case 11: - case 15: - var i = o.return; - try { - Qj(4, o); - } catch (s) { - W(o, i, s); - } - break; - case 1: - var u = o.stateNode; - if ('function' == typeof u.componentDidMount) { - var _ = o.return; - try { - u.componentDidMount(); - } catch (s) { - W(o, _, s); - } - } - var w = o.return; - try { - Rj(o); - } catch (s) { - W(o, w, s); - } - break; - case 5: - var x = o.return; - try { - Rj(o); - } catch (s) { - W(o, x, s); - } - } - } catch (s) { - W(o, o.return, s); - } - if (o === s) { - js = null; - break; - } - var C = o.sibling; - if (null !== C) { - (C.return = o.return), (js = C); - break; - } - js = o.return; - } - } - var Ts, - Ns = Math.ceil, - Rs = z.ReactCurrentDispatcher, - Ds = z.ReactCurrentOwner, - Ls = z.ReactCurrentBatchConfig, - Bs = 0, - Fs = null, - qs = null, - $s = 0, - Vs = 0, - Us = Uf(0), - zs = 0, - Ws = null, - Ks = 0, - Hs = 0, - Js = 0, - Gs = null, - Ys = null, - Xs = 0, - Zs = 1 / 0, - Qs = null, - eo = !1, - to = null, - ro = null, - no = !1, - so = null, - oo = 0, - io = 0, - ao = null, - lo = -1, - co = 0; - function R() { - return 6 & Bs ? dt() : -1 !== lo ? lo : (lo = dt()); - } - function yi(s) { - return 1 & s.mode - ? 2 & Bs && 0 !== $s - ? $s & -$s - : null !== $n.transition - ? (0 === co && (co = yc()), co) - : 0 !== (s = At) - ? s - : (s = void 0 === (s = window.event) ? 16 : jd(s.type)) - : 1; - } - function gi(s, o, i, u) { - if (50 < io) throw ((io = 0), (ao = null), Error(p(185))); - Ac(s, i, u), - (2 & Bs && s === Fs) || - (s === Fs && (!(2 & Bs) && (Hs |= i), 4 === zs && Ck(s, $s)), - Dk(s, u), - 1 === i && 0 === Bs && !(1 & o.mode) && ((Zs = dt() + 500), Cn && jg())); - } - function Dk(s, o) { - var i = s.callbackNode; - !(function wc(s, o) { - for ( - var i = s.suspendedLanes, - u = s.pingedLanes, - _ = s.expirationTimes, - w = s.pendingLanes; - 0 < w; - - ) { - var x = 31 - St(w), - C = 1 << x, - j = _[x]; - -1 === j - ? (C & i && !(C & u)) || (_[x] = vc(C, o)) - : j <= o && (s.expiredLanes |= C), - (w &= ~C); - } - })(s, o); - var u = uc(s, s === Fs ? $s : 0); - if (0 === u) null !== i && ut(i), (s.callbackNode = null), (s.callbackPriority = 0); - else if (((o = u & -u), s.callbackPriority !== o)) { - if ((null != i && ut(i), 1 === o)) - 0 === s.tag - ? (function ig(s) { - (Cn = !0), hg(s); - })(Ek.bind(null, s)) - : hg(Ek.bind(null, s)), - pn(function () { - !(6 & Bs) && jg(); - }), - (i = null); - else { - switch (Dc(u)) { - case 1: - i = gt; - break; - case 4: - i = yt; - break; - case 16: - default: - i = vt; - break; - case 536870912: - i = _t; - } - i = Fk(i, Gk.bind(null, s)); - } - (s.callbackPriority = o), (s.callbackNode = i); - } - } - function Gk(s, o) { - if (((lo = -1), (co = 0), 6 & Bs)) throw Error(p(327)); - var i = s.callbackNode; - if (Hk() && s.callbackNode !== i) return null; - var u = uc(s, s === Fs ? $s : 0); - if (0 === u) return null; - if (30 & u || u & s.expiredLanes || o) o = Ik(s, u); - else { - o = u; - var _ = Bs; - Bs |= 2; - var w = Jk(); - for ((Fs === s && $s === o) || ((Qs = null), (Zs = dt() + 500), Kk(s, o)); ; ) - try { - Lk(); - break; - } catch (o) { - Mk(s, o); - } - $g(), - (Rs.current = w), - (Bs = _), - null !== qs ? (o = 0) : ((Fs = null), ($s = 0), (o = zs)); - } - if (0 !== o) { - if ((2 === o && 0 !== (_ = xc(s)) && ((u = _), (o = Nk(s, _))), 1 === o)) - throw ((i = Ws), Kk(s, 0), Ck(s, u), Dk(s, dt()), i); - if (6 === o) Ck(s, u); - else { - if ( - ((_ = s.current.alternate), - !( - 30 & u || - (function Ok(s) { - for (var o = s; ; ) { - if (16384 & o.flags) { - var i = o.updateQueue; - if (null !== i && null !== (i = i.stores)) - for (var u = 0; u < i.length; u++) { - var _ = i[u], - w = _.getSnapshot; - _ = _.value; - try { - if (!Lr(w(), _)) return !1; - } catch (s) { - return !1; - } - } - } - if (((i = o.child), 16384 & o.subtreeFlags && null !== i)) - (i.return = o), (o = i); - else { - if (o === s) break; - for (; null === o.sibling; ) { - if (null === o.return || o.return === s) return !0; - o = o.return; - } - (o.sibling.return = o.return), (o = o.sibling); - } - } - return !0; - })(_) || - ((o = Ik(s, u)), - 2 === o && ((w = xc(s)), 0 !== w && ((u = w), (o = Nk(s, w)))), - 1 !== o) - )) - ) - throw ((i = Ws), Kk(s, 0), Ck(s, u), Dk(s, dt()), i); - switch (((s.finishedWork = _), (s.finishedLanes = u), o)) { - case 0: - case 1: - throw Error(p(345)); - case 2: - case 5: - Pk(s, Ys, Qs); - break; - case 3: - if ((Ck(s, u), (130023424 & u) === u && 10 < (o = Xs + 500 - dt()))) { - if (0 !== uc(s, 0)) break; - if (((_ = s.suspendedLanes) & u) !== u) { - R(), (s.pingedLanes |= s.suspendedLanes & _); - break; - } - s.timeoutHandle = ln(Pk.bind(null, s, Ys, Qs), o); - break; - } - Pk(s, Ys, Qs); - break; - case 4: - if ((Ck(s, u), (4194240 & u) === u)) break; - for (o = s.eventTimes, _ = -1; 0 < u; ) { - var x = 31 - St(u); - (w = 1 << x), (x = o[x]) > _ && (_ = x), (u &= ~w); - } - if ( - ((u = _), - 10 < - (u = - (120 > (u = dt() - u) - ? 120 - : 480 > u - ? 480 - : 1080 > u - ? 1080 - : 1920 > u - ? 1920 - : 3e3 > u - ? 3e3 - : 4320 > u - ? 4320 - : 1960 * Ns(u / 1960)) - u)) - ) { - s.timeoutHandle = ln(Pk.bind(null, s, Ys, Qs), u); - break; - } - Pk(s, Ys, Qs); - break; - default: - throw Error(p(329)); - } - } - } - return Dk(s, dt()), s.callbackNode === i ? Gk.bind(null, s) : null; - } - function Nk(s, o) { - var i = Gs; - return ( - s.current.memoizedState.isDehydrated && (Kk(s, o).flags |= 256), - 2 !== (s = Ik(s, o)) && ((o = Ys), (Ys = i), null !== o && Fj(o)), - s - ); - } - function Fj(s) { - null === Ys ? (Ys = s) : Ys.push.apply(Ys, s); - } - function Ck(s, o) { - for ( - o &= ~Js, o &= ~Hs, s.suspendedLanes |= o, s.pingedLanes &= ~o, s = s.expirationTimes; - 0 < o; - - ) { - var i = 31 - St(o), - u = 1 << i; - (s[i] = -1), (o &= ~u); - } - } - function Ek(s) { - if (6 & Bs) throw Error(p(327)); - Hk(); - var o = uc(s, 0); - if (!(1 & o)) return Dk(s, dt()), null; - var i = Ik(s, o); - if (0 !== s.tag && 2 === i) { - var u = xc(s); - 0 !== u && ((o = u), (i = Nk(s, u))); - } - if (1 === i) throw ((i = Ws), Kk(s, 0), Ck(s, o), Dk(s, dt()), i); - if (6 === i) throw Error(p(345)); - return ( - (s.finishedWork = s.current.alternate), - (s.finishedLanes = o), - Pk(s, Ys, Qs), - Dk(s, dt()), - null - ); - } - function Qk(s, o) { - var i = Bs; - Bs |= 1; - try { - return s(o); - } finally { - 0 === (Bs = i) && ((Zs = dt() + 500), Cn && jg()); - } - } - function Rk(s) { - null !== so && 0 === so.tag && !(6 & Bs) && Hk(); - var o = Bs; - Bs |= 1; - var i = Ls.transition, - u = At; - try { - if (((Ls.transition = null), (At = 1), s)) return s(); - } finally { - (At = u), (Ls.transition = i), !(6 & (Bs = o)) && jg(); - } - } - function Hj() { - (Vs = Us.current), E(Us); - } - function Kk(s, o) { - (s.finishedWork = null), (s.finishedLanes = 0); - var i = s.timeoutHandle; - if ((-1 !== i && ((s.timeoutHandle = -1), cn(i)), null !== qs)) - for (i = qs.return; null !== i; ) { - var u = i; - switch ((wg(u), u.tag)) { - case 1: - null != (u = u.type.childContextTypes) && $f(); - break; - case 3: - zh(), E(Sn), E(wn), Eh(); - break; - case 5: - Bh(u); - break; - case 4: - zh(); - break; - case 13: - case 19: - E(es); - break; - case 10: - ah(u.type._context); - break; - case 22: - case 23: - Hj(); - } - i = i.return; - } - if ( - ((Fs = s), - (qs = s = Pg(s.current, null)), - ($s = Vs = o), - (zs = 0), - (Ws = null), - (Js = Hs = Ks = 0), - (Ys = Gs = null), - null !== Jn) - ) { - for (o = 0; o < Jn.length; o++) - if (null !== (u = (i = Jn[o]).interleaved)) { - i.interleaved = null; - var _ = u.next, - w = i.pending; - if (null !== w) { - var x = w.next; - (w.next = _), (u.next = x); - } - i.pending = u; - } - Jn = null; - } - return s; - } - function Mk(s, o) { - for (;;) { - var i = qs; - try { - if (($g(), (rs.current = ds), cs)) { - for (var u = os.memoizedState; null !== u; ) { - var _ = u.queue; - null !== _ && (_.pending = null), (u = u.next); - } - cs = !1; - } - if ( - ((ss = 0), - (ls = as = os = null), - (us = !1), - (ps = 0), - (Ds.current = null), - null === i || null === i.return) - ) { - (zs = 1), (Ws = o), (qs = null); - break; - } - e: { - var w = s, - x = i.return, - C = i, - j = o; - if ( - ((o = $s), - (C.flags |= 32768), - null !== j && 'object' == typeof j && 'function' == typeof j.then) - ) { - var L = j, - B = C, - $ = B.tag; - if (!(1 & B.mode || (0 !== $ && 11 !== $ && 15 !== $))) { - var V = B.alternate; - V - ? ((B.updateQueue = V.updateQueue), - (B.memoizedState = V.memoizedState), - (B.lanes = V.lanes)) - : ((B.updateQueue = null), (B.memoizedState = null)); - } - var U = Ui(x); - if (null !== U) { - (U.flags &= -257), Vi(U, x, C, 0, o), 1 & U.mode && Si(w, L, o), (j = L); - var z = (o = U).updateQueue; - if (null === z) { - var Y = new Set(); - Y.add(j), (o.updateQueue = Y); - } else z.add(j); - break e; - } - if (!(1 & o)) { - Si(w, L, o), tj(); - break e; - } - j = Error(p(426)); - } else if (Fn && 1 & C.mode) { - var Z = Ui(x); - if (null !== Z) { - !(65536 & Z.flags) && (Z.flags |= 256), Vi(Z, x, C, 0, o), Jg(Ji(j, C)); - break e; - } - } - (w = j = Ji(j, C)), - 4 !== zs && (zs = 2), - null === Gs ? (Gs = [w]) : Gs.push(w), - (w = x); - do { - switch (w.tag) { - case 3: - (w.flags |= 65536), (o &= -o), (w.lanes |= o), ph(w, Ni(0, j, o)); - break e; - case 1: - C = j; - var ee = w.type, - ie = w.stateNode; - if ( - !( - 128 & w.flags || - ('function' != typeof ee.getDerivedStateFromError && - (null === ie || - 'function' != typeof ie.componentDidCatch || - (null !== ro && ro.has(ie)))) - ) - ) { - (w.flags |= 65536), (o &= -o), (w.lanes |= o), ph(w, Qi(w, C, o)); - break e; - } - } - w = w.return; - } while (null !== w); - } - Sk(i); - } catch (s) { - (o = s), qs === i && null !== i && (qs = i = i.return); - continue; - } - break; - } - } - function Jk() { - var s = Rs.current; - return (Rs.current = ds), null === s ? ds : s; - } - function tj() { - (0 !== zs && 3 !== zs && 2 !== zs) || (zs = 4), - null === Fs || (!(268435455 & Ks) && !(268435455 & Hs)) || Ck(Fs, $s); - } - function Ik(s, o) { - var i = Bs; - Bs |= 2; - var u = Jk(); - for ((Fs === s && $s === o) || ((Qs = null), Kk(s, o)); ; ) - try { - Tk(); - break; - } catch (o) { - Mk(s, o); - } - if (($g(), (Bs = i), (Rs.current = u), null !== qs)) throw Error(p(261)); - return (Fs = null), ($s = 0), zs; - } - function Tk() { - for (; null !== qs; ) Uk(qs); - } - function Lk() { - for (; null !== qs && !pt(); ) Uk(qs); - } - function Uk(s) { - var o = Ts(s.alternate, s, Vs); - (s.memoizedProps = s.pendingProps), null === o ? Sk(s) : (qs = o), (Ds.current = null); - } - function Sk(s) { - var o = s; - do { - var i = o.alternate; - if (((s = o.return), 32768 & o.flags)) { - if (null !== (i = Ij(i, o))) return (i.flags &= 32767), void (qs = i); - if (null === s) return (zs = 6), void (qs = null); - (s.flags |= 32768), (s.subtreeFlags = 0), (s.deletions = null); - } else if (null !== (i = Ej(i, o, Vs))) return void (qs = i); - if (null !== (o = o.sibling)) return void (qs = o); - qs = o = s; - } while (null !== o); - 0 === zs && (zs = 5); - } - function Pk(s, o, i) { - var u = At, - _ = Ls.transition; - try { - (Ls.transition = null), - (At = 1), - (function Wk(s, o, i, u) { - do { - Hk(); - } while (null !== so); - if (6 & Bs) throw Error(p(327)); - i = s.finishedWork; - var _ = s.finishedLanes; - if (null === i) return null; - if (((s.finishedWork = null), (s.finishedLanes = 0), i === s.current)) - throw Error(p(177)); - (s.callbackNode = null), (s.callbackPriority = 0); - var w = i.lanes | i.childLanes; - if ( - ((function Bc(s, o) { - var i = s.pendingLanes & ~o; - (s.pendingLanes = o), - (s.suspendedLanes = 0), - (s.pingedLanes = 0), - (s.expiredLanes &= o), - (s.mutableReadLanes &= o), - (s.entangledLanes &= o), - (o = s.entanglements); - var u = s.eventTimes; - for (s = s.expirationTimes; 0 < i; ) { - var _ = 31 - St(i), - w = 1 << _; - (o[_] = 0), (u[_] = -1), (s[_] = -1), (i &= ~w); - } - })(s, w), - s === Fs && ((qs = Fs = null), ($s = 0)), - (!(2064 & i.subtreeFlags) && !(2064 & i.flags)) || - no || - ((no = !0), - Fk(vt, function () { - return Hk(), null; - })), - (w = !!(15990 & i.flags)), - !!(15990 & i.subtreeFlags) || w) - ) { - (w = Ls.transition), (Ls.transition = null); - var x = At; - At = 1; - var C = Bs; - (Bs |= 4), - (Ds.current = null), - (function Oj(s, o) { - if (((on = zt), Ne((s = Me())))) { - if ('selectionStart' in s) - var i = { start: s.selectionStart, end: s.selectionEnd }; - else - e: { - var u = - (i = ((i = s.ownerDocument) && i.defaultView) || window) - .getSelection && i.getSelection(); - if (u && 0 !== u.rangeCount) { - i = u.anchorNode; - var _ = u.anchorOffset, - w = u.focusNode; - u = u.focusOffset; - try { - i.nodeType, w.nodeType; - } catch (s) { - i = null; - break e; - } - var x = 0, - C = -1, - j = -1, - L = 0, - B = 0, - $ = s, - V = null; - t: for (;;) { - for ( - var U; - $ !== i || (0 !== _ && 3 !== $.nodeType) || (C = x + _), - $ !== w || (0 !== u && 3 !== $.nodeType) || (j = x + u), - 3 === $.nodeType && (x += $.nodeValue.length), - null !== (U = $.firstChild); - - ) - (V = $), ($ = U); - for (;;) { - if ($ === s) break t; - if ( - (V === i && ++L === _ && (C = x), - V === w && ++B === u && (j = x), - null !== (U = $.nextSibling)) - ) - break; - V = ($ = V).parentNode; - } - $ = U; - } - i = -1 === C || -1 === j ? null : { start: C, end: j }; - } else i = null; - } - i = i || { start: 0, end: 0 }; - } else i = null; - for ( - an = { focusedElem: s, selectionRange: i }, zt = !1, js = o; - null !== js; - - ) - if (((s = (o = js).child), 1028 & o.subtreeFlags && null !== s)) - (s.return = o), (js = s); - else - for (; null !== js; ) { - o = js; - try { - var z = o.alternate; - if (1024 & o.flags) - switch (o.tag) { - case 0: - case 11: - case 15: - case 5: - case 6: - case 4: - case 17: - break; - case 1: - if (null !== z) { - var Y = z.memoizedProps, - Z = z.memoizedState, - ee = o.stateNode, - ie = ee.getSnapshotBeforeUpdate( - o.elementType === o.type ? Y : Ci(o.type, Y), - Z - ); - ee.__reactInternalSnapshotBeforeUpdate = ie; - } - break; - case 3: - var ae = o.stateNode.containerInfo; - 1 === ae.nodeType - ? (ae.textContent = '') - : 9 === ae.nodeType && - ae.documentElement && - ae.removeChild(ae.documentElement); - break; - default: - throw Error(p(163)); - } - } catch (s) { - W(o, o.return, s); - } - if (null !== (s = o.sibling)) { - (s.return = o.return), (js = s); - break; - } - js = o.return; - } - return (z = Is), (Is = !1), z; - })(s, i), - dk(i, s), - Oe(an), - (zt = !!on), - (an = on = null), - (s.current = i), - hk(i, s, _), - ht(), - (Bs = C), - (At = x), - (Ls.transition = w); - } else s.current = i; - if ( - (no && ((no = !1), (so = s), (oo = _)), - (w = s.pendingLanes), - 0 === w && (ro = null), - (function mc(s) { - if (wt && 'function' == typeof wt.onCommitFiberRoot) - try { - wt.onCommitFiberRoot(Et, s, void 0, !(128 & ~s.current.flags)); - } catch (s) {} - })(i.stateNode), - Dk(s, dt()), - null !== o) - ) - for (u = s.onRecoverableError, i = 0; i < o.length; i++) - (_ = o[i]), u(_.value, { componentStack: _.stack, digest: _.digest }); - if (eo) throw ((eo = !1), (s = to), (to = null), s); - return ( - !!(1 & oo) && 0 !== s.tag && Hk(), - (w = s.pendingLanes), - 1 & w ? (s === ao ? io++ : ((io = 0), (ao = s))) : (io = 0), - jg(), - null - ); - })(s, o, i, u); - } finally { - (Ls.transition = _), (At = u); - } - return null; - } - function Hk() { - if (null !== so) { - var s = Dc(oo), - o = Ls.transition, - i = At; - try { - if (((Ls.transition = null), (At = 16 > s ? 16 : s), null === so)) var u = !1; - else { - if (((s = so), (so = null), (oo = 0), 6 & Bs)) throw Error(p(331)); - var _ = Bs; - for (Bs |= 4, js = s.current; null !== js; ) { - var w = js, - x = w.child; - if (16 & js.flags) { - var C = w.deletions; - if (null !== C) { - for (var j = 0; j < C.length; j++) { - var L = C[j]; - for (js = L; null !== js; ) { - var B = js; - switch (B.tag) { - case 0: - case 11: - case 15: - Pj(8, B, w); - } - var $ = B.child; - if (null !== $) ($.return = B), (js = $); - else - for (; null !== js; ) { - var V = (B = js).sibling, - U = B.return; - if ((Sj(B), B === L)) { - js = null; - break; - } - if (null !== V) { - (V.return = U), (js = V); - break; - } - js = U; - } - } - } - var z = w.alternate; - if (null !== z) { - var Y = z.child; - if (null !== Y) { - z.child = null; - do { - var Z = Y.sibling; - (Y.sibling = null), (Y = Z); - } while (null !== Y); - } - } - js = w; - } - } - if (2064 & w.subtreeFlags && null !== x) (x.return = w), (js = x); - else - e: for (; null !== js; ) { - if (2048 & (w = js).flags) - switch (w.tag) { - case 0: - case 11: - case 15: - Pj(9, w, w.return); - } - var ee = w.sibling; - if (null !== ee) { - (ee.return = w.return), (js = ee); - break e; - } - js = w.return; - } - } - var ie = s.current; - for (js = ie; null !== js; ) { - var ae = (x = js).child; - if (2064 & x.subtreeFlags && null !== ae) (ae.return = x), (js = ae); - else - e: for (x = ie; null !== js; ) { - if (2048 & (C = js).flags) - try { - switch (C.tag) { - case 0: - case 11: - case 15: - Qj(9, C); - } - } catch (s) { - W(C, C.return, s); - } - if (C === x) { - js = null; - break e; - } - var le = C.sibling; - if (null !== le) { - (le.return = C.return), (js = le); - break e; - } - js = C.return; - } - } - if (((Bs = _), jg(), wt && 'function' == typeof wt.onPostCommitFiberRoot)) - try { - wt.onPostCommitFiberRoot(Et, s); - } catch (s) {} - u = !0; - } - return u; - } finally { - (At = i), (Ls.transition = o); - } - } - return !1; - } - function Xk(s, o, i) { - (s = nh(s, (o = Ni(0, (o = Ji(i, o)), 1)), 1)), - (o = R()), - null !== s && (Ac(s, 1, o), Dk(s, o)); - } - function W(s, o, i) { - if (3 === s.tag) Xk(s, s, i); - else - for (; null !== o; ) { - if (3 === o.tag) { - Xk(o, s, i); - break; - } - if (1 === o.tag) { - var u = o.stateNode; - if ( - 'function' == typeof o.type.getDerivedStateFromError || - ('function' == typeof u.componentDidCatch && (null === ro || !ro.has(u))) - ) { - (o = nh(o, (s = Qi(o, (s = Ji(i, s)), 1)), 1)), - (s = R()), - null !== o && (Ac(o, 1, s), Dk(o, s)); - break; - } - } - o = o.return; - } - } - function Ti(s, o, i) { - var u = s.pingCache; - null !== u && u.delete(o), - (o = R()), - (s.pingedLanes |= s.suspendedLanes & i), - Fs === s && - ($s & i) === i && - (4 === zs || (3 === zs && (130023424 & $s) === $s && 500 > dt() - Xs) - ? Kk(s, 0) - : (Js |= i)), - Dk(s, o); - } - function Yk(s, o) { - 0 === o && - (1 & s.mode ? ((o = Ot), !(130023424 & (Ot <<= 1)) && (Ot = 4194304)) : (o = 1)); - var i = R(); - null !== (s = ih(s, o)) && (Ac(s, o, i), Dk(s, i)); - } - function uj(s) { - var o = s.memoizedState, - i = 0; - null !== o && (i = o.retryLane), Yk(s, i); - } - function bk(s, o) { - var i = 0; - switch (s.tag) { - case 13: - var u = s.stateNode, - _ = s.memoizedState; - null !== _ && (i = _.retryLane); - break; - case 19: - u = s.stateNode; - break; - default: - throw Error(p(314)); - } - null !== u && u.delete(o), Yk(s, i); - } - function Fk(s, o) { - return ct(s, o); - } - function $k(s, o, i, u) { - (this.tag = s), - (this.key = i), - (this.sibling = - this.child = - this.return = - this.stateNode = - this.type = - this.elementType = - null), - (this.index = 0), - (this.ref = null), - (this.pendingProps = o), - (this.dependencies = - this.memoizedState = - this.updateQueue = - this.memoizedProps = - null), - (this.mode = u), - (this.subtreeFlags = this.flags = 0), - (this.deletions = null), - (this.childLanes = this.lanes = 0), - (this.alternate = null); - } - function Bg(s, o, i, u) { - return new $k(s, o, i, u); - } - function aj(s) { - return !(!(s = s.prototype) || !s.isReactComponent); - } - function Pg(s, o) { - var i = s.alternate; - return ( - null === i - ? (((i = Bg(s.tag, o, s.key, s.mode)).elementType = s.elementType), - (i.type = s.type), - (i.stateNode = s.stateNode), - (i.alternate = s), - (s.alternate = i)) - : ((i.pendingProps = o), - (i.type = s.type), - (i.flags = 0), - (i.subtreeFlags = 0), - (i.deletions = null)), - (i.flags = 14680064 & s.flags), - (i.childLanes = s.childLanes), - (i.lanes = s.lanes), - (i.child = s.child), - (i.memoizedProps = s.memoizedProps), - (i.memoizedState = s.memoizedState), - (i.updateQueue = s.updateQueue), - (o = s.dependencies), - (i.dependencies = - null === o ? null : { lanes: o.lanes, firstContext: o.firstContext }), - (i.sibling = s.sibling), - (i.index = s.index), - (i.ref = s.ref), - i - ); - } - function Rg(s, o, i, u, _, w) { - var x = 2; - if (((u = s), 'function' == typeof s)) aj(s) && (x = 1); - else if ('string' == typeof s) x = 5; - else - e: switch (s) { - case ee: - return Tg(i.children, _, w, o); - case ie: - (x = 8), (_ |= 8); - break; - case ae: - return ((s = Bg(12, i, o, 2 | _)).elementType = ae), (s.lanes = w), s; - case de: - return ((s = Bg(13, i, o, _)).elementType = de), (s.lanes = w), s; - case fe: - return ((s = Bg(19, i, o, _)).elementType = fe), (s.lanes = w), s; - case _e: - return pj(i, _, w, o); - default: - if ('object' == typeof s && null !== s) - switch (s.$$typeof) { - case le: - x = 10; - break e; - case ce: - x = 9; - break e; - case pe: - x = 11; - break e; - case ye: - x = 14; - break e; - case be: - (x = 16), (u = null); - break e; - } - throw Error(p(130, null == s ? s : typeof s, '')); - } - return ((o = Bg(x, i, o, _)).elementType = s), (o.type = u), (o.lanes = w), o; - } - function Tg(s, o, i, u) { - return ((s = Bg(7, s, u, o)).lanes = i), s; - } - function pj(s, o, i, u) { - return ( - ((s = Bg(22, s, u, o)).elementType = _e), - (s.lanes = i), - (s.stateNode = { isHidden: !1 }), - s - ); - } - function Qg(s, o, i) { - return ((s = Bg(6, s, null, o)).lanes = i), s; - } - function Sg(s, o, i) { - return ( - ((o = Bg(4, null !== s.children ? s.children : [], s.key, o)).lanes = i), - (o.stateNode = { - containerInfo: s.containerInfo, - pendingChildren: null, - implementation: s.implementation - }), - o - ); - } - function al(s, o, i, u, _) { - (this.tag = o), - (this.containerInfo = s), - (this.finishedWork = this.pingCache = this.current = this.pendingChildren = null), - (this.timeoutHandle = -1), - (this.callbackNode = this.pendingContext = this.context = null), - (this.callbackPriority = 0), - (this.eventTimes = zc(0)), - (this.expirationTimes = zc(-1)), - (this.entangledLanes = - this.finishedLanes = - this.mutableReadLanes = - this.expiredLanes = - this.pingedLanes = - this.suspendedLanes = - this.pendingLanes = - 0), - (this.entanglements = zc(0)), - (this.identifierPrefix = u), - (this.onRecoverableError = _), - (this.mutableSourceEagerHydrationData = null); - } - function bl(s, o, i, u, _, w, x, C, j) { - return ( - (s = new al(s, o, i, C, j)), - 1 === o ? ((o = 1), !0 === w && (o |= 8)) : (o = 0), - (w = Bg(3, null, null, o)), - (s.current = w), - (w.stateNode = s), - (w.memoizedState = { - element: u, - isDehydrated: i, - cache: null, - transitions: null, - pendingSuspenseBoundaries: null - }), - kh(w), - s - ); - } - function dl(s) { - if (!s) return En; - e: { - if (Vb((s = s._reactInternals)) !== s || 1 !== s.tag) throw Error(p(170)); - var o = s; - do { - switch (o.tag) { - case 3: - o = o.stateNode.context; - break e; - case 1: - if (Zf(o.type)) { - o = o.stateNode.__reactInternalMemoizedMergedChildContext; - break e; - } - } - o = o.return; - } while (null !== o); - throw Error(p(171)); - } - if (1 === s.tag) { - var i = s.type; - if (Zf(i)) return bg(s, i, o); - } - return o; - } - function el(s, o, i, u, _, w, x, C, j) { - return ( - ((s = bl(i, u, !0, s, 0, w, 0, C, j)).context = dl(null)), - (i = s.current), - ((w = mh((u = R()), (_ = yi(i)))).callback = null != o ? o : null), - nh(i, w, _), - (s.current.lanes = _), - Ac(s, _, u), - Dk(s, u), - s - ); - } - function fl(s, o, i, u) { - var _ = o.current, - w = R(), - x = yi(_); - return ( - (i = dl(i)), - null === o.context ? (o.context = i) : (o.pendingContext = i), - ((o = mh(w, x)).payload = { element: s }), - null !== (u = void 0 === u ? null : u) && (o.callback = u), - null !== (s = nh(_, o, x)) && (gi(s, _, x, w), oh(s, _, x)), - x - ); - } - function gl(s) { - return (s = s.current).child ? (s.child.tag, s.child.stateNode) : null; - } - function hl(s, o) { - if (null !== (s = s.memoizedState) && null !== s.dehydrated) { - var i = s.retryLane; - s.retryLane = 0 !== i && i < o ? i : o; - } - } - function il(s, o) { - hl(s, o), (s = s.alternate) && hl(s, o); - } - Ts = function (s, o, i) { - if (null !== s) - if (s.memoizedProps !== o.pendingProps || Sn.current) _s = !0; - else { - if (!(s.lanes & i || 128 & o.flags)) - return ( - (_s = !1), - (function yj(s, o, i) { - switch (o.tag) { - case 3: - kj(o), Ig(); - break; - case 5: - Ah(o); - break; - case 1: - Zf(o.type) && cg(o); - break; - case 4: - yh(o, o.stateNode.containerInfo); - break; - case 10: - var u = o.type._context, - _ = o.memoizedProps.value; - G(zn, u._currentValue), (u._currentValue = _); - break; - case 13: - if (null !== (u = o.memoizedState)) - return null !== u.dehydrated - ? (G(es, 1 & es.current), (o.flags |= 128), null) - : i & o.child.childLanes - ? oj(s, o, i) - : (G(es, 1 & es.current), - null !== (s = Zi(s, o, i)) ? s.sibling : null); - G(es, 1 & es.current); - break; - case 19: - if (((u = !!(i & o.childLanes)), 128 & s.flags)) { - if (u) return xj(s, o, i); - o.flags |= 128; - } - if ( - (null !== (_ = o.memoizedState) && - ((_.rendering = null), (_.tail = null), (_.lastEffect = null)), - G(es, es.current), - u) - ) - break; - return null; - case 22: - case 23: - return (o.lanes = 0), dj(s, o, i); - } - return Zi(s, o, i); - })(s, o, i) - ); - _s = !!(131072 & s.flags); - } - else (_s = !1), Fn && 1048576 & o.flags && ug(o, Pn, o.index); - switch (((o.lanes = 0), o.tag)) { - case 2: - var u = o.type; - ij(s, o), (s = o.pendingProps); - var _ = Yf(o, wn.current); - ch(o, i), (_ = Nh(null, o, u, s, _, i)); - var w = Sh(); - return ( - (o.flags |= 1), - 'object' == typeof _ && - null !== _ && - 'function' == typeof _.render && - void 0 === _.$$typeof - ? ((o.tag = 1), - (o.memoizedState = null), - (o.updateQueue = null), - Zf(u) ? ((w = !0), cg(o)) : (w = !1), - (o.memoizedState = null !== _.state && void 0 !== _.state ? _.state : null), - kh(o), - (_.updater = ys), - (o.stateNode = _), - (_._reactInternals = o), - Ii(o, u, s, i), - (o = jj(null, o, u, !0, w, i))) - : ((o.tag = 0), Fn && w && vg(o), Xi(null, o, _, i), (o = o.child)), - o - ); - case 16: - u = o.elementType; - e: { - switch ( - (ij(s, o), - (s = o.pendingProps), - (u = (_ = u._init)(u._payload)), - (o.type = u), - (_ = o.tag = - (function Zk(s) { - if ('function' == typeof s) return aj(s) ? 1 : 0; - if (null != s) { - if ((s = s.$$typeof) === pe) return 11; - if (s === ye) return 14; - } - return 2; - })(u)), - (s = Ci(u, s)), - _) - ) { - case 0: - o = cj(null, o, u, s, i); - break e; - case 1: - o = hj(null, o, u, s, i); - break e; - case 11: - o = Yi(null, o, u, s, i); - break e; - case 14: - o = $i(null, o, u, Ci(u.type, s), i); - break e; - } - throw Error(p(306, u, '')); - } - return o; - case 0: - return ( - (u = o.type), - (_ = o.pendingProps), - cj(s, o, u, (_ = o.elementType === u ? _ : Ci(u, _)), i) - ); - case 1: - return ( - (u = o.type), - (_ = o.pendingProps), - hj(s, o, u, (_ = o.elementType === u ? _ : Ci(u, _)), i) - ); - case 3: - e: { - if ((kj(o), null === s)) throw Error(p(387)); - (u = o.pendingProps), - (_ = (w = o.memoizedState).element), - lh(s, o), - qh(o, u, null, i); - var x = o.memoizedState; - if (((u = x.element), w.isDehydrated)) { - if ( - ((w = { - element: u, - isDehydrated: !1, - cache: x.cache, - pendingSuspenseBoundaries: x.pendingSuspenseBoundaries, - transitions: x.transitions - }), - (o.updateQueue.baseState = w), - (o.memoizedState = w), - 256 & o.flags) - ) { - o = lj(s, o, u, i, (_ = Ji(Error(p(423)), o))); - break e; - } - if (u !== _) { - o = lj(s, o, u, i, (_ = Ji(Error(p(424)), o))); - break e; - } - for ( - Bn = Lf(o.stateNode.containerInfo.firstChild), - Ln = o, - Fn = !0, - qn = null, - i = Un(o, null, u, i), - o.child = i; - i; - - ) - (i.flags = (-3 & i.flags) | 4096), (i = i.sibling); - } else { - if ((Ig(), u === _)) { - o = Zi(s, o, i); - break e; - } - Xi(s, o, u, i); - } - o = o.child; - } - return o; - case 5: - return ( - Ah(o), - null === s && Eg(o), - (u = o.type), - (_ = o.pendingProps), - (w = null !== s ? s.memoizedProps : null), - (x = _.children), - Ef(u, _) ? (x = null) : null !== w && Ef(u, w) && (o.flags |= 32), - gj(s, o), - Xi(s, o, x, i), - o.child - ); - case 6: - return null === s && Eg(o), null; - case 13: - return oj(s, o, i); - case 4: - return ( - yh(o, o.stateNode.containerInfo), - (u = o.pendingProps), - null === s ? (o.child = Vn(o, null, u, i)) : Xi(s, o, u, i), - o.child - ); - case 11: - return ( - (u = o.type), - (_ = o.pendingProps), - Yi(s, o, u, (_ = o.elementType === u ? _ : Ci(u, _)), i) - ); - case 7: - return Xi(s, o, o.pendingProps, i), o.child; - case 8: - case 12: - return Xi(s, o, o.pendingProps.children, i), o.child; - case 10: - e: { - if ( - ((u = o.type._context), - (_ = o.pendingProps), - (w = o.memoizedProps), - (x = _.value), - G(zn, u._currentValue), - (u._currentValue = x), - null !== w) - ) - if (Lr(w.value, x)) { - if (w.children === _.children && !Sn.current) { - o = Zi(s, o, i); - break e; - } - } else - for (null !== (w = o.child) && (w.return = o); null !== w; ) { - var C = w.dependencies; - if (null !== C) { - x = w.child; - for (var j = C.firstContext; null !== j; ) { - if (j.context === u) { - if (1 === w.tag) { - (j = mh(-1, i & -i)).tag = 2; - var L = w.updateQueue; - if (null !== L) { - var B = (L = L.shared).pending; - null === B ? (j.next = j) : ((j.next = B.next), (B.next = j)), - (L.pending = j); - } - } - (w.lanes |= i), - null !== (j = w.alternate) && (j.lanes |= i), - bh(w.return, i, o), - (C.lanes |= i); - break; - } - j = j.next; - } - } else if (10 === w.tag) x = w.type === o.type ? null : w.child; - else if (18 === w.tag) { - if (null === (x = w.return)) throw Error(p(341)); - (x.lanes |= i), - null !== (C = x.alternate) && (C.lanes |= i), - bh(x, i, o), - (x = w.sibling); - } else x = w.child; - if (null !== x) x.return = w; - else - for (x = w; null !== x; ) { - if (x === o) { - x = null; - break; - } - if (null !== (w = x.sibling)) { - (w.return = x.return), (x = w); - break; - } - x = x.return; - } - w = x; - } - Xi(s, o, _.children, i), (o = o.child); - } - return o; - case 9: - return ( - (_ = o.type), - (u = o.pendingProps.children), - ch(o, i), - (u = u((_ = eh(_)))), - (o.flags |= 1), - Xi(s, o, u, i), - o.child - ); - case 14: - return (_ = Ci((u = o.type), o.pendingProps)), $i(s, o, u, (_ = Ci(u.type, _)), i); - case 15: - return bj(s, o, o.type, o.pendingProps, i); - case 17: - return ( - (u = o.type), - (_ = o.pendingProps), - (_ = o.elementType === u ? _ : Ci(u, _)), - ij(s, o), - (o.tag = 1), - Zf(u) ? ((s = !0), cg(o)) : (s = !1), - ch(o, i), - Gi(o, u, _), - Ii(o, u, _, i), - jj(null, o, u, !0, s, i) - ); - case 19: - return xj(s, o, i); - case 22: - return dj(s, o, i); - } - throw Error(p(156, o.tag)); - }; - var uo = - 'function' == typeof reportError - ? reportError - : function (s) { - console.error(s); - }; - function ll(s) { - this._internalRoot = s; - } - function ml(s) { - this._internalRoot = s; - } - function nl(s) { - return !(!s || (1 !== s.nodeType && 9 !== s.nodeType && 11 !== s.nodeType)); - } - function ol(s) { - return !( - !s || - (1 !== s.nodeType && - 9 !== s.nodeType && - 11 !== s.nodeType && - (8 !== s.nodeType || ' react-mount-point-unstable ' !== s.nodeValue)) - ); - } - function pl() {} - function rl(s, o, i, u, _) { - var w = i._reactRootContainer; - if (w) { - var x = w; - if ('function' == typeof _) { - var C = _; - _ = function () { - var s = gl(x); - C.call(s); - }; - } - fl(o, x, s, _); - } else - x = (function ql(s, o, i, u, _) { - if (_) { - if ('function' == typeof u) { - var w = u; - u = function () { - var s = gl(x); - w.call(s); - }; - } - var x = el(o, u, s, 0, null, !1, 0, '', pl); - return ( - (s._reactRootContainer = x), - (s[mn] = x.current), - sf(8 === s.nodeType ? s.parentNode : s), - Rk(), - x - ); - } - for (; (_ = s.lastChild); ) s.removeChild(_); - if ('function' == typeof u) { - var C = u; - u = function () { - var s = gl(j); - C.call(s); - }; - } - var j = bl(s, 0, !1, null, 0, !1, 0, '', pl); - return ( - (s._reactRootContainer = j), - (s[mn] = j.current), - sf(8 === s.nodeType ? s.parentNode : s), - Rk(function () { - fl(o, j, i, u); - }), - j - ); - })(i, o, s, _, u); - return gl(x); - } - (ml.prototype.render = ll.prototype.render = - function (s) { - var o = this._internalRoot; - if (null === o) throw Error(p(409)); - fl(s, o, null, null); - }), - (ml.prototype.unmount = ll.prototype.unmount = - function () { - var s = this._internalRoot; - if (null !== s) { - this._internalRoot = null; - var o = s.containerInfo; - Rk(function () { - fl(null, s, null, null); - }), - (o[mn] = null); - } - }), - (ml.prototype.unstable_scheduleHydration = function (s) { - if (s) { - var o = Mt(); - s = { blockedOn: null, target: s, priority: o }; - for (var i = 0; i < $t.length && 0 !== o && o < $t[i].priority; i++); - $t.splice(i, 0, s), 0 === i && Vc(s); - } - }), - (jt = function (s) { - switch (s.tag) { - case 3: - var o = s.stateNode; - if (o.current.memoizedState.isDehydrated) { - var i = tc(o.pendingLanes); - 0 !== i && (Cc(o, 1 | i), Dk(o, dt()), !(6 & Bs) && ((Zs = dt() + 500), jg())); - } - break; - case 13: - Rk(function () { - var o = ih(s, 1); - if (null !== o) { - var i = R(); - gi(o, s, 1, i); - } - }), - il(s, 1); - } - }), - (It = function (s) { - if (13 === s.tag) { - var o = ih(s, 134217728); - if (null !== o) gi(o, s, 134217728, R()); - il(s, 134217728); - } - }), - (Pt = function (s) { - if (13 === s.tag) { - var o = yi(s), - i = ih(s, o); - if (null !== i) gi(i, s, o, R()); - il(s, o); - } - }), - (Mt = function () { - return At; - }), - (Tt = function (s, o) { - var i = At; - try { - return (At = s), o(); - } finally { - At = i; - } - }), - (Xe = function (s, o, i) { - switch (o) { - case 'input': - if ((bb(s, i), (o = i.name), 'radio' === i.type && null != o)) { - for (i = s; i.parentNode; ) i = i.parentNode; - for ( - i = i.querySelectorAll( - 'input[name=' + JSON.stringify('' + o) + '][type="radio"]' - ), - o = 0; - o < i.length; - o++ - ) { - var u = i[o]; - if (u !== s && u.form === s.form) { - var _ = Db(u); - if (!_) throw Error(p(90)); - Wa(u), bb(u, _); - } - } - } - break; - case 'textarea': - ib(s, i); - break; - case 'select': - null != (o = i.value) && fb(s, !!i.multiple, o, !1); - } - }), - (Gb = Qk), - (Hb = Rk); - var po = { usingClientEntryPoint: !1, Events: [Cb, ue, Db, Eb, Fb, Qk] }, - ho = { - findFiberByHostInstance: Wc, - bundleType: 0, - version: '18.3.1', - rendererPackageName: 'react-dom' - }, - fo = { - bundleType: ho.bundleType, - version: ho.version, - rendererPackageName: ho.rendererPackageName, - rendererConfig: ho.rendererConfig, - overrideHookState: null, - overrideHookStateDeletePath: null, - overrideHookStateRenamePath: null, - overrideProps: null, - overridePropsDeletePath: null, - overridePropsRenamePath: null, - setErrorHandler: null, - setSuspenseHandler: null, - scheduleUpdate: null, - currentDispatcherRef: z.ReactCurrentDispatcher, - findHostInstanceByFiber: function (s) { - return null === (s = Zb(s)) ? null : s.stateNode; - }, - findFiberByHostInstance: - ho.findFiberByHostInstance || - function jl() { - return null; - }, - findHostInstancesForRefresh: null, - scheduleRefresh: null, - scheduleRoot: null, - setRefreshHandler: null, - getCurrentFiber: null, - reconcilerVersion: '18.3.1-next-f1338f8080-20240426' - }; - if ('undefined' != typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { - var mo = __REACT_DEVTOOLS_GLOBAL_HOOK__; - if (!mo.isDisabled && mo.supportsFiber) - try { - (Et = mo.inject(fo)), (wt = mo); - } catch (qe) {} - } - (o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = po), - (o.createPortal = function (s, o) { - var i = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null; - if (!nl(o)) throw Error(p(200)); - return (function cl(s, o, i) { - var u = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null; - return { - $$typeof: Z, - key: null == u ? null : '' + u, - children: s, - containerInfo: o, - implementation: i - }; - })(s, o, null, i); - }), - (o.createRoot = function (s, o) { - if (!nl(s)) throw Error(p(299)); - var i = !1, - u = '', - _ = uo; - return ( - null != o && - (!0 === o.unstable_strictMode && (i = !0), - void 0 !== o.identifierPrefix && (u = o.identifierPrefix), - void 0 !== o.onRecoverableError && (_ = o.onRecoverableError)), - (o = bl(s, 1, !1, null, 0, i, 0, u, _)), - (s[mn] = o.current), - sf(8 === s.nodeType ? s.parentNode : s), - new ll(o) - ); - }), - (o.findDOMNode = function (s) { - if (null == s) return null; - if (1 === s.nodeType) return s; - var o = s._reactInternals; - if (void 0 === o) { - if ('function' == typeof s.render) throw Error(p(188)); - throw ((s = Object.keys(s).join(',')), Error(p(268, s))); - } - return (s = null === (s = Zb(o)) ? null : s.stateNode); - }), - (o.flushSync = function (s) { - return Rk(s); - }), - (o.hydrate = function (s, o, i) { - if (!ol(o)) throw Error(p(200)); - return rl(null, s, o, !0, i); - }), - (o.hydrateRoot = function (s, o, i) { - if (!nl(s)) throw Error(p(405)); - var u = (null != i && i.hydratedSources) || null, - _ = !1, - w = '', - x = uo; - if ( - (null != i && - (!0 === i.unstable_strictMode && (_ = !0), - void 0 !== i.identifierPrefix && (w = i.identifierPrefix), - void 0 !== i.onRecoverableError && (x = i.onRecoverableError)), - (o = el(o, null, s, 1, null != i ? i : null, _, 0, w, x)), - (s[mn] = o.current), - sf(s), - u) - ) - for (s = 0; s < u.length; s++) - (_ = (_ = (i = u[s])._getVersion)(i._source)), - null == o.mutableSourceEagerHydrationData - ? (o.mutableSourceEagerHydrationData = [i, _]) - : o.mutableSourceEagerHydrationData.push(i, _); - return new ml(o); - }), - (o.render = function (s, o, i) { - if (!ol(o)) throw Error(p(200)); - return rl(null, s, o, !1, i); - }), - (o.unmountComponentAtNode = function (s) { - if (!ol(s)) throw Error(p(40)); - return ( - !!s._reactRootContainer && - (Rk(function () { - rl(null, null, s, !1, function () { - (s._reactRootContainer = null), (s[mn] = null); - }); - }), - !0) - ); - }), - (o.unstable_batchedUpdates = Qk), - (o.unstable_renderSubtreeIntoContainer = function (s, o, i, u) { - if (!ol(i)) throw Error(p(200)); - if (null == s || void 0 === s._reactInternals) throw Error(p(38)); - return rl(s, o, i, !1, u); - }), - (o.version = '18.3.1-next-f1338f8080-20240426'); - }, - 40961: (s, o, i) => { - 'use strict'; - !(function checkDCE() { - if ( - 'undefined' != typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && - 'function' == typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE - ) - try { - __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(checkDCE); - } catch (s) { - console.error(s); - } - })(), - (s.exports = i(22551)); - }, - 2209: (s, o, i) => { - 'use strict'; - var u, - _ = i(9404), - w = function productionTypeChecker() { - invariant(!1, 'ImmutablePropTypes type checking code is stripped in production.'); - }; - w.isRequired = w; - var x = function getProductionTypeChecker() { - return w; - }; - function getPropType(s) { - var o = typeof s; - return Array.isArray(s) - ? 'array' - : s instanceof RegExp - ? 'object' - : s instanceof _.Iterable - ? 'Immutable.' + s.toSource().split(' ')[0] - : o; - } - function createChainableTypeChecker(s) { - function checkType(o, i, u, _, w, x) { - for (var C = arguments.length, j = Array(C > 6 ? C - 6 : 0), L = 6; L < C; L++) - j[L - 6] = arguments[L]; - return ( - (x = x || u), - (_ = _ || '<>'), - null != i[u] - ? s.apply(void 0, [i, u, _, w, x].concat(j)) - : o - ? new Error('Required ' + w + ' `' + x + '` was not specified in `' + _ + '`.') - : void 0 - ); - } - var o = checkType.bind(null, !1); - return (o.isRequired = checkType.bind(null, !0)), o; - } - function createIterableSubclassTypeChecker(s, o) { - return (function createImmutableTypeChecker(s, o) { - return createChainableTypeChecker(function validate(i, u, _, w, x) { - var C = i[u]; - if (!o(C)) { - var j = getPropType(C); - return new Error( - 'Invalid ' + - w + - ' `' + - x + - '` of type `' + - j + - '` supplied to `' + - _ + - '`, expected `' + - s + - '`.' - ); - } - return null; - }); - })('Iterable.' + s, function (s) { - return _.Iterable.isIterable(s) && o(s); - }); - } - ((u = { - listOf: x, - mapOf: x, - orderedMapOf: x, - setOf: x, - orderedSetOf: x, - stackOf: x, - iterableOf: x, - recordOf: x, - shape: x, - contains: x, - mapContains: x, - orderedMapContains: x, - list: w, - map: w, - orderedMap: w, - set: w, - orderedSet: w, - stack: w, - seq: w, - record: w, - iterable: w - }).iterable.indexed = createIterableSubclassTypeChecker('Indexed', _.Iterable.isIndexed)), - (u.iterable.keyed = createIterableSubclassTypeChecker('Keyed', _.Iterable.isKeyed)), - (s.exports = u); - }, - 15287: (s, o) => { - 'use strict'; - var i = Symbol.for('react.element'), - u = Symbol.for('react.portal'), - _ = Symbol.for('react.fragment'), - w = Symbol.for('react.strict_mode'), - x = Symbol.for('react.profiler'), - C = Symbol.for('react.provider'), - j = Symbol.for('react.context'), - L = Symbol.for('react.forward_ref'), - B = Symbol.for('react.suspense'), - $ = Symbol.for('react.memo'), - V = Symbol.for('react.lazy'), - U = Symbol.iterator; - var z = { - isMounted: function () { - return !1; - }, - enqueueForceUpdate: function () {}, - enqueueReplaceState: function () {}, - enqueueSetState: function () {} - }, - Y = Object.assign, - Z = {}; - function E(s, o, i) { - (this.props = s), (this.context = o), (this.refs = Z), (this.updater = i || z); - } - function F() {} - function G(s, o, i) { - (this.props = s), (this.context = o), (this.refs = Z), (this.updater = i || z); - } - (E.prototype.isReactComponent = {}), - (E.prototype.setState = function (s, o) { - if ('object' != typeof s && 'function' != typeof s && null != s) - throw Error( - 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.' - ); - this.updater.enqueueSetState(this, s, o, 'setState'); - }), - (E.prototype.forceUpdate = function (s) { - this.updater.enqueueForceUpdate(this, s, 'forceUpdate'); - }), - (F.prototype = E.prototype); - var ee = (G.prototype = new F()); - (ee.constructor = G), Y(ee, E.prototype), (ee.isPureReactComponent = !0); - var ie = Array.isArray, - ae = Object.prototype.hasOwnProperty, - le = { current: null }, - ce = { key: !0, ref: !0, __self: !0, __source: !0 }; - function M(s, o, u) { - var _, - w = {}, - x = null, - C = null; - if (null != o) - for (_ in (void 0 !== o.ref && (C = o.ref), void 0 !== o.key && (x = '' + o.key), o)) - ae.call(o, _) && !ce.hasOwnProperty(_) && (w[_] = o[_]); - var j = arguments.length - 2; - if (1 === j) w.children = u; - else if (1 < j) { - for (var L = Array(j), B = 0; B < j; B++) L[B] = arguments[B + 2]; - w.children = L; - } - if (s && s.defaultProps) - for (_ in (j = s.defaultProps)) void 0 === w[_] && (w[_] = j[_]); - return { $$typeof: i, type: s, key: x, ref: C, props: w, _owner: le.current }; - } - function O(s) { - return 'object' == typeof s && null !== s && s.$$typeof === i; - } - var pe = /\/+/g; - function Q(s, o) { - return 'object' == typeof s && null !== s && null != s.key - ? (function escape(s) { - var o = { '=': '=0', ':': '=2' }; - return ( - '$' + - s.replace(/[=:]/g, function (s) { - return o[s]; - }) - ); - })('' + s.key) - : o.toString(36); - } - function R(s, o, _, w, x) { - var C = typeof s; - ('undefined' !== C && 'boolean' !== C) || (s = null); - var j = !1; - if (null === s) j = !0; - else - switch (C) { - case 'string': - case 'number': - j = !0; - break; - case 'object': - switch (s.$$typeof) { - case i: - case u: - j = !0; - } - } - if (j) - return ( - (x = x((j = s))), - (s = '' === w ? '.' + Q(j, 0) : w), - ie(x) - ? ((_ = ''), - null != s && (_ = s.replace(pe, '$&/') + '/'), - R(x, o, _, '', function (s) { - return s; - })) - : null != x && - (O(x) && - (x = (function N(s, o) { - return { - $$typeof: i, - type: s.type, - key: o, - ref: s.ref, - props: s.props, - _owner: s._owner - }; - })( - x, - _ + - (!x.key || (j && j.key === x.key) - ? '' - : ('' + x.key).replace(pe, '$&/') + '/') + - s - )), - o.push(x)), - 1 - ); - if (((j = 0), (w = '' === w ? '.' : w + ':'), ie(s))) - for (var L = 0; L < s.length; L++) { - var B = w + Q((C = s[L]), L); - j += R(C, o, _, B, x); - } - else if ( - ((B = (function A(s) { - return null === s || 'object' != typeof s - ? null - : 'function' == typeof (s = (U && s[U]) || s['@@iterator']) - ? s - : null; - })(s)), - 'function' == typeof B) - ) - for (s = B.call(s), L = 0; !(C = s.next()).done; ) - j += R((C = C.value), o, _, (B = w + Q(C, L++)), x); - else if ('object' === C) - throw ( - ((o = String(s)), - Error( - 'Objects are not valid as a React child (found: ' + - ('[object Object]' === o - ? 'object with keys {' + Object.keys(s).join(', ') + '}' - : o) + - '). If you meant to render a collection of children, use an array instead.' - )) - ); - return j; - } - function S(s, o, i) { - if (null == s) return s; - var u = [], - _ = 0; - return ( - R(s, u, '', '', function (s) { - return o.call(i, s, _++); - }), - u - ); - } - function T(s) { - if (-1 === s._status) { - var o = s._result; - (o = o()).then( - function (o) { - (0 !== s._status && -1 !== s._status) || ((s._status = 1), (s._result = o)); - }, - function (o) { - (0 !== s._status && -1 !== s._status) || ((s._status = 2), (s._result = o)); - } - ), - -1 === s._status && ((s._status = 0), (s._result = o)); - } - if (1 === s._status) return s._result.default; - throw s._result; - } - var de = { current: null }, - fe = { transition: null }, - ye = { ReactCurrentDispatcher: de, ReactCurrentBatchConfig: fe, ReactCurrentOwner: le }; - function X() { - throw Error('act(...) is not supported in production builds of React.'); - } - (o.Children = { - map: S, - forEach: function (s, o, i) { - S( - s, - function () { - o.apply(this, arguments); - }, - i - ); - }, - count: function (s) { - var o = 0; - return ( - S(s, function () { - o++; - }), - o - ); - }, - toArray: function (s) { - return ( - S(s, function (s) { - return s; - }) || [] - ); - }, - only: function (s) { - if (!O(s)) - throw Error( - 'React.Children.only expected to receive a single React element child.' - ); - return s; - } - }), - (o.Component = E), - (o.Fragment = _), - (o.Profiler = x), - (o.PureComponent = G), - (o.StrictMode = w), - (o.Suspense = B), - (o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ye), - (o.act = X), - (o.cloneElement = function (s, o, u) { - if (null == s) - throw Error( - 'React.cloneElement(...): The argument must be a React element, but you passed ' + - s + - '.' - ); - var _ = Y({}, s.props), - w = s.key, - x = s.ref, - C = s._owner; - if (null != o) { - if ( - (void 0 !== o.ref && ((x = o.ref), (C = le.current)), - void 0 !== o.key && (w = '' + o.key), - s.type && s.type.defaultProps) - ) - var j = s.type.defaultProps; - for (L in o) - ae.call(o, L) && - !ce.hasOwnProperty(L) && - (_[L] = void 0 === o[L] && void 0 !== j ? j[L] : o[L]); - } - var L = arguments.length - 2; - if (1 === L) _.children = u; - else if (1 < L) { - j = Array(L); - for (var B = 0; B < L; B++) j[B] = arguments[B + 2]; - _.children = j; - } - return { $$typeof: i, type: s.type, key: w, ref: x, props: _, _owner: C }; - }), - (o.createContext = function (s) { - return ( - ((s = { - $$typeof: j, - _currentValue: s, - _currentValue2: s, - _threadCount: 0, - Provider: null, - Consumer: null, - _defaultValue: null, - _globalName: null - }).Provider = { $$typeof: C, _context: s }), - (s.Consumer = s) - ); - }), - (o.createElement = M), - (o.createFactory = function (s) { - var o = M.bind(null, s); - return (o.type = s), o; - }), - (o.createRef = function () { - return { current: null }; - }), - (o.forwardRef = function (s) { - return { $$typeof: L, render: s }; - }), - (o.isValidElement = O), - (o.lazy = function (s) { - return { $$typeof: V, _payload: { _status: -1, _result: s }, _init: T }; - }), - (o.memo = function (s, o) { - return { $$typeof: $, type: s, compare: void 0 === o ? null : o }; - }), - (o.startTransition = function (s) { - var o = fe.transition; - fe.transition = {}; - try { - s(); - } finally { - fe.transition = o; - } - }), - (o.unstable_act = X), - (o.useCallback = function (s, o) { - return de.current.useCallback(s, o); - }), - (o.useContext = function (s) { - return de.current.useContext(s); - }), - (o.useDebugValue = function () {}), - (o.useDeferredValue = function (s) { - return de.current.useDeferredValue(s); - }), - (o.useEffect = function (s, o) { - return de.current.useEffect(s, o); - }), - (o.useId = function () { - return de.current.useId(); - }), - (o.useImperativeHandle = function (s, o, i) { - return de.current.useImperativeHandle(s, o, i); - }), - (o.useInsertionEffect = function (s, o) { - return de.current.useInsertionEffect(s, o); - }), - (o.useLayoutEffect = function (s, o) { - return de.current.useLayoutEffect(s, o); - }), - (o.useMemo = function (s, o) { - return de.current.useMemo(s, o); - }), - (o.useReducer = function (s, o, i) { - return de.current.useReducer(s, o, i); - }), - (o.useRef = function (s) { - return de.current.useRef(s); - }), - (o.useState = function (s) { - return de.current.useState(s); - }), - (o.useSyncExternalStore = function (s, o, i) { - return de.current.useSyncExternalStore(s, o, i); - }), - (o.useTransition = function () { - return de.current.useTransition(); - }), - (o.version = '18.3.1'); - }, - 96540: (s, o, i) => { - 'use strict'; - s.exports = i(15287); - }, - 86048: (s) => { - 'use strict'; - var o = {}; - function createErrorType(s, i, u) { - u || (u = Error); - var _ = (function (s) { - function NodeError(o, u, _) { - return ( - s.call( - this, - (function getMessage(s, o, u) { - return 'string' == typeof i ? i : i(s, o, u); - })(o, u, _) - ) || this - ); - } - return ( - (function _inheritsLoose(s, o) { - (s.prototype = Object.create(o.prototype)), - (s.prototype.constructor = s), - (s.__proto__ = o); - })(NodeError, s), - NodeError - ); - })(u); - (_.prototype.name = u.name), (_.prototype.code = s), (o[s] = _); - } - function oneOf(s, o) { - if (Array.isArray(s)) { - var i = s.length; - return ( - (s = s.map(function (s) { - return String(s); - })), - i > 2 - ? 'one of '.concat(o, ' ').concat(s.slice(0, i - 1).join(', '), ', or ') + - s[i - 1] - : 2 === i - ? 'one of '.concat(o, ' ').concat(s[0], ' or ').concat(s[1]) - : 'of '.concat(o, ' ').concat(s[0]) - ); - } - return 'of '.concat(o, ' ').concat(String(s)); - } - createErrorType( - 'ERR_INVALID_OPT_VALUE', - function (s, o) { - return 'The value "' + o + '" is invalid for option "' + s + '"'; - }, - TypeError - ), - createErrorType( - 'ERR_INVALID_ARG_TYPE', - function (s, o, i) { - var u, _; - if ( - ('string' == typeof o && - (function startsWith(s, o, i) { - return s.substr(!i || i < 0 ? 0 : +i, o.length) === o; - })(o, 'not ') - ? ((u = 'must not be'), (o = o.replace(/^not /, ''))) - : (u = 'must be'), - (function endsWith(s, o, i) { - return ( - (void 0 === i || i > s.length) && (i = s.length), - s.substring(i - o.length, i) === o - ); - })(s, ' argument')) - ) - _ = 'The '.concat(s, ' ').concat(u, ' ').concat(oneOf(o, 'type')); - else { - var w = (function includes(s, o, i) { - return ( - 'number' != typeof i && (i = 0), - !(i + o.length > s.length) && -1 !== s.indexOf(o, i) - ); - })(s, '.') - ? 'property' - : 'argument'; - _ = 'The "' - .concat(s, '" ') - .concat(w, ' ') - .concat(u, ' ') - .concat(oneOf(o, 'type')); - } - return (_ += '. Received type '.concat(typeof i)); - }, - TypeError - ), - createErrorType('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF'), - createErrorType('ERR_METHOD_NOT_IMPLEMENTED', function (s) { - return 'The ' + s + ' method is not implemented'; - }), - createErrorType('ERR_STREAM_PREMATURE_CLOSE', 'Premature close'), - createErrorType('ERR_STREAM_DESTROYED', function (s) { - return 'Cannot call ' + s + ' after a stream was destroyed'; - }), - createErrorType('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times'), - createErrorType('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable'), - createErrorType('ERR_STREAM_WRITE_AFTER_END', 'write after end'), - createErrorType( - 'ERR_STREAM_NULL_VALUES', - 'May not write null values to stream', - TypeError - ), - createErrorType( - 'ERR_UNKNOWN_ENCODING', - function (s) { - return 'Unknown encoding: ' + s; - }, - TypeError - ), - createErrorType( - 'ERR_STREAM_UNSHIFT_AFTER_END_EVENT', - 'stream.unshift() after end event' - ), - (s.exports.F = o); - }, - 25382: (s, o, i) => { - 'use strict'; - var u = i(65606), - _ = - Object.keys || - function (s) { - var o = []; - for (var i in s) o.push(i); - return o; - }; - s.exports = Duplex; - var w = i(45412), - x = i(16708); - i(56698)(Duplex, w); - for (var C = _(x.prototype), j = 0; j < C.length; j++) { - var L = C[j]; - Duplex.prototype[L] || (Duplex.prototype[L] = x.prototype[L]); - } - function Duplex(s) { - if (!(this instanceof Duplex)) return new Duplex(s); - w.call(this, s), - x.call(this, s), - (this.allowHalfOpen = !0), - s && - (!1 === s.readable && (this.readable = !1), - !1 === s.writable && (this.writable = !1), - !1 === s.allowHalfOpen && ((this.allowHalfOpen = !1), this.once('end', onend))); - } - function onend() { - this._writableState.ended || u.nextTick(onEndNT, this); - } - function onEndNT(s) { - s.end(); - } - Object.defineProperty(Duplex.prototype, 'writableHighWaterMark', { - enumerable: !1, - get: function get() { - return this._writableState.highWaterMark; - } - }), - Object.defineProperty(Duplex.prototype, 'writableBuffer', { - enumerable: !1, - get: function get() { - return this._writableState && this._writableState.getBuffer(); - } - }), - Object.defineProperty(Duplex.prototype, 'writableLength', { - enumerable: !1, - get: function get() { - return this._writableState.length; - } - }), - Object.defineProperty(Duplex.prototype, 'destroyed', { - enumerable: !1, - get: function get() { - return ( - void 0 !== this._readableState && - void 0 !== this._writableState && - this._readableState.destroyed && - this._writableState.destroyed - ); - }, - set: function set(s) { - void 0 !== this._readableState && - void 0 !== this._writableState && - ((this._readableState.destroyed = s), (this._writableState.destroyed = s)); - } - }); - }, - 63600: (s, o, i) => { - 'use strict'; - s.exports = PassThrough; - var u = i(74610); - function PassThrough(s) { - if (!(this instanceof PassThrough)) return new PassThrough(s); - u.call(this, s); - } - i(56698)(PassThrough, u), - (PassThrough.prototype._transform = function (s, o, i) { - i(null, s); - }); - }, - 45412: (s, o, i) => { - 'use strict'; - var u, - _ = i(65606); - (s.exports = Readable), (Readable.ReadableState = ReadableState); - i(37007).EventEmitter; - var w = function EElistenerCount(s, o) { - return s.listeners(o).length; - }, - x = i(40345), - C = i(48287).Buffer, - j = - (void 0 !== i.g - ? i.g - : 'undefined' != typeof window - ? window - : 'undefined' != typeof self - ? self - : {} - ).Uint8Array || function () {}; - var L, - B = i(79838); - L = B && B.debuglog ? B.debuglog('stream') : function debug() {}; - var $, - V, - U, - z = i(80345), - Y = i(75896), - Z = i(65291).getHighWaterMark, - ee = i(86048).F, - ie = ee.ERR_INVALID_ARG_TYPE, - ae = ee.ERR_STREAM_PUSH_AFTER_EOF, - le = ee.ERR_METHOD_NOT_IMPLEMENTED, - ce = ee.ERR_STREAM_UNSHIFT_AFTER_END_EVENT; - i(56698)(Readable, x); - var pe = Y.errorOrDestroy, - de = ['error', 'close', 'destroy', 'pause', 'resume']; - function ReadableState(s, o, _) { - (u = u || i(25382)), - (s = s || {}), - 'boolean' != typeof _ && (_ = o instanceof u), - (this.objectMode = !!s.objectMode), - _ && (this.objectMode = this.objectMode || !!s.readableObjectMode), - (this.highWaterMark = Z(this, s, 'readableHighWaterMark', _)), - (this.buffer = new z()), - (this.length = 0), - (this.pipes = null), - (this.pipesCount = 0), - (this.flowing = null), - (this.ended = !1), - (this.endEmitted = !1), - (this.reading = !1), - (this.sync = !0), - (this.needReadable = !1), - (this.emittedReadable = !1), - (this.readableListening = !1), - (this.resumeScheduled = !1), - (this.paused = !0), - (this.emitClose = !1 !== s.emitClose), - (this.autoDestroy = !!s.autoDestroy), - (this.destroyed = !1), - (this.defaultEncoding = s.defaultEncoding || 'utf8'), - (this.awaitDrain = 0), - (this.readingMore = !1), - (this.decoder = null), - (this.encoding = null), - s.encoding && - ($ || ($ = i(83141).I), - (this.decoder = new $(s.encoding)), - (this.encoding = s.encoding)); - } - function Readable(s) { - if (((u = u || i(25382)), !(this instanceof Readable))) return new Readable(s); - var o = this instanceof u; - (this._readableState = new ReadableState(s, this, o)), - (this.readable = !0), - s && - ('function' == typeof s.read && (this._read = s.read), - 'function' == typeof s.destroy && (this._destroy = s.destroy)), - x.call(this); - } - function readableAddChunk(s, o, i, u, _) { - L('readableAddChunk', o); - var w, - x = s._readableState; - if (null === o) - (x.reading = !1), - (function onEofChunk(s, o) { - if ((L('onEofChunk'), o.ended)) return; - if (o.decoder) { - var i = o.decoder.end(); - i && i.length && (o.buffer.push(i), (o.length += o.objectMode ? 1 : i.length)); - } - (o.ended = !0), - o.sync - ? emitReadable(s) - : ((o.needReadable = !1), - o.emittedReadable || ((o.emittedReadable = !0), emitReadable_(s))); - })(s, x); - else if ( - (_ || - (w = (function chunkInvalid(s, o) { - var i; - (function _isUint8Array(s) { - return C.isBuffer(s) || s instanceof j; - })(o) || - 'string' == typeof o || - void 0 === o || - s.objectMode || - (i = new ie('chunk', ['string', 'Buffer', 'Uint8Array'], o)); - return i; - })(x, o)), - w) - ) - pe(s, w); - else if (x.objectMode || (o && o.length > 0)) - if ( - ('string' == typeof o || - x.objectMode || - Object.getPrototypeOf(o) === C.prototype || - (o = (function _uint8ArrayToBuffer(s) { - return C.from(s); - })(o)), - u) - ) - x.endEmitted ? pe(s, new ce()) : addChunk(s, x, o, !0); - else if (x.ended) pe(s, new ae()); - else { - if (x.destroyed) return !1; - (x.reading = !1), - x.decoder && !i - ? ((o = x.decoder.write(o)), - x.objectMode || 0 !== o.length ? addChunk(s, x, o, !1) : maybeReadMore(s, x)) - : addChunk(s, x, o, !1); - } - else u || ((x.reading = !1), maybeReadMore(s, x)); - return !x.ended && (x.length < x.highWaterMark || 0 === x.length); - } - function addChunk(s, o, i, u) { - o.flowing && 0 === o.length && !o.sync - ? ((o.awaitDrain = 0), s.emit('data', i)) - : ((o.length += o.objectMode ? 1 : i.length), - u ? o.buffer.unshift(i) : o.buffer.push(i), - o.needReadable && emitReadable(s)), - maybeReadMore(s, o); - } - Object.defineProperty(Readable.prototype, 'destroyed', { - enumerable: !1, - get: function get() { - return void 0 !== this._readableState && this._readableState.destroyed; - }, - set: function set(s) { - this._readableState && (this._readableState.destroyed = s); - } - }), - (Readable.prototype.destroy = Y.destroy), - (Readable.prototype._undestroy = Y.undestroy), - (Readable.prototype._destroy = function (s, o) { - o(s); - }), - (Readable.prototype.push = function (s, o) { - var i, - u = this._readableState; - return ( - u.objectMode - ? (i = !0) - : 'string' == typeof s && - ((o = o || u.defaultEncoding) !== u.encoding && ((s = C.from(s, o)), (o = '')), - (i = !0)), - readableAddChunk(this, s, o, !1, i) - ); - }), - (Readable.prototype.unshift = function (s) { - return readableAddChunk(this, s, null, !0, !1); - }), - (Readable.prototype.isPaused = function () { - return !1 === this._readableState.flowing; - }), - (Readable.prototype.setEncoding = function (s) { - $ || ($ = i(83141).I); - var o = new $(s); - (this._readableState.decoder = o), - (this._readableState.encoding = this._readableState.decoder.encoding); - for (var u = this._readableState.buffer.head, _ = ''; null !== u; ) - (_ += o.write(u.data)), (u = u.next); - return ( - this._readableState.buffer.clear(), - '' !== _ && this._readableState.buffer.push(_), - (this._readableState.length = _.length), - this - ); - }); - var fe = 1073741824; - function howMuchToRead(s, o) { - return s <= 0 || (0 === o.length && o.ended) - ? 0 - : o.objectMode - ? 1 - : s != s - ? o.flowing && o.length - ? o.buffer.head.data.length - : o.length - : (s > o.highWaterMark && - (o.highWaterMark = (function computeNewHighWaterMark(s) { - return ( - s >= fe - ? (s = fe) - : (s--, - (s |= s >>> 1), - (s |= s >>> 2), - (s |= s >>> 4), - (s |= s >>> 8), - (s |= s >>> 16), - s++), - s - ); - })(s)), - s <= o.length ? s : o.ended ? o.length : ((o.needReadable = !0), 0)); - } - function emitReadable(s) { - var o = s._readableState; - L('emitReadable', o.needReadable, o.emittedReadable), - (o.needReadable = !1), - o.emittedReadable || - (L('emitReadable', o.flowing), - (o.emittedReadable = !0), - _.nextTick(emitReadable_, s)); - } - function emitReadable_(s) { - var o = s._readableState; - L('emitReadable_', o.destroyed, o.length, o.ended), - o.destroyed || - (!o.length && !o.ended) || - (s.emit('readable'), (o.emittedReadable = !1)), - (o.needReadable = !o.flowing && !o.ended && o.length <= o.highWaterMark), - flow(s); - } - function maybeReadMore(s, o) { - o.readingMore || ((o.readingMore = !0), _.nextTick(maybeReadMore_, s, o)); - } - function maybeReadMore_(s, o) { - for ( - ; - !o.reading && - !o.ended && - (o.length < o.highWaterMark || (o.flowing && 0 === o.length)); - - ) { - var i = o.length; - if ((L('maybeReadMore read 0'), s.read(0), i === o.length)) break; - } - o.readingMore = !1; - } - function updateReadableListening(s) { - var o = s._readableState; - (o.readableListening = s.listenerCount('readable') > 0), - o.resumeScheduled && !o.paused - ? (o.flowing = !0) - : s.listenerCount('data') > 0 && s.resume(); - } - function nReadingNextTick(s) { - L('readable nexttick read 0'), s.read(0); - } - function resume_(s, o) { - L('resume', o.reading), - o.reading || s.read(0), - (o.resumeScheduled = !1), - s.emit('resume'), - flow(s), - o.flowing && !o.reading && s.read(0); - } - function flow(s) { - var o = s._readableState; - for (L('flow', o.flowing); o.flowing && null !== s.read(); ); - } - function fromList(s, o) { - return 0 === o.length - ? null - : (o.objectMode - ? (i = o.buffer.shift()) - : !s || s >= o.length - ? ((i = o.decoder - ? o.buffer.join('') - : 1 === o.buffer.length - ? o.buffer.first() - : o.buffer.concat(o.length)), - o.buffer.clear()) - : (i = o.buffer.consume(s, o.decoder)), - i); - var i; - } - function endReadable(s) { - var o = s._readableState; - L('endReadable', o.endEmitted), - o.endEmitted || ((o.ended = !0), _.nextTick(endReadableNT, o, s)); - } - function endReadableNT(s, o) { - if ( - (L('endReadableNT', s.endEmitted, s.length), - !s.endEmitted && - 0 === s.length && - ((s.endEmitted = !0), (o.readable = !1), o.emit('end'), s.autoDestroy)) - ) { - var i = o._writableState; - (!i || (i.autoDestroy && i.finished)) && o.destroy(); - } - } - function indexOf(s, o) { - for (var i = 0, u = s.length; i < u; i++) if (s[i] === o) return i; - return -1; - } - (Readable.prototype.read = function (s) { - L('read', s), (s = parseInt(s, 10)); - var o = this._readableState, - i = s; - if ( - (0 !== s && (o.emittedReadable = !1), - 0 === s && - o.needReadable && - ((0 !== o.highWaterMark ? o.length >= o.highWaterMark : o.length > 0) || o.ended)) - ) - return ( - L('read: emitReadable', o.length, o.ended), - 0 === o.length && o.ended ? endReadable(this) : emitReadable(this), - null - ); - if (0 === (s = howMuchToRead(s, o)) && o.ended) - return 0 === o.length && endReadable(this), null; - var u, - _ = o.needReadable; - return ( - L('need readable', _), - (0 === o.length || o.length - s < o.highWaterMark) && - L('length less than watermark', (_ = !0)), - o.ended || o.reading - ? L('reading or ended', (_ = !1)) - : _ && - (L('do read'), - (o.reading = !0), - (o.sync = !0), - 0 === o.length && (o.needReadable = !0), - this._read(o.highWaterMark), - (o.sync = !1), - o.reading || (s = howMuchToRead(i, o))), - null === (u = s > 0 ? fromList(s, o) : null) - ? ((o.needReadable = o.length <= o.highWaterMark), (s = 0)) - : ((o.length -= s), (o.awaitDrain = 0)), - 0 === o.length && - (o.ended || (o.needReadable = !0), i !== s && o.ended && endReadable(this)), - null !== u && this.emit('data', u), - u - ); - }), - (Readable.prototype._read = function (s) { - pe(this, new le('_read()')); - }), - (Readable.prototype.pipe = function (s, o) { - var i = this, - u = this._readableState; - switch (u.pipesCount) { - case 0: - u.pipes = s; - break; - case 1: - u.pipes = [u.pipes, s]; - break; - default: - u.pipes.push(s); - } - (u.pipesCount += 1), L('pipe count=%d opts=%j', u.pipesCount, o); - var x = (!o || !1 !== o.end) && s !== _.stdout && s !== _.stderr ? onend : unpipe; - function onunpipe(o, _) { - L('onunpipe'), - o === i && - _ && - !1 === _.hasUnpiped && - ((_.hasUnpiped = !0), - (function cleanup() { - L('cleanup'), - s.removeListener('close', onclose), - s.removeListener('finish', onfinish), - s.removeListener('drain', C), - s.removeListener('error', onerror), - s.removeListener('unpipe', onunpipe), - i.removeListener('end', onend), - i.removeListener('end', unpipe), - i.removeListener('data', ondata), - (j = !0), - !u.awaitDrain || (s._writableState && !s._writableState.needDrain) || C(); - })()); - } - function onend() { - L('onend'), s.end(); - } - u.endEmitted ? _.nextTick(x) : i.once('end', x), s.on('unpipe', onunpipe); - var C = (function pipeOnDrain(s) { - return function pipeOnDrainFunctionResult() { - var o = s._readableState; - L('pipeOnDrain', o.awaitDrain), - o.awaitDrain && o.awaitDrain--, - 0 === o.awaitDrain && w(s, 'data') && ((o.flowing = !0), flow(s)); - }; - })(i); - s.on('drain', C); - var j = !1; - function ondata(o) { - L('ondata'); - var _ = s.write(o); - L('dest.write', _), - !1 === _ && - (((1 === u.pipesCount && u.pipes === s) || - (u.pipesCount > 1 && -1 !== indexOf(u.pipes, s))) && - !j && - (L('false write response, pause', u.awaitDrain), u.awaitDrain++), - i.pause()); - } - function onerror(o) { - L('onerror', o), - unpipe(), - s.removeListener('error', onerror), - 0 === w(s, 'error') && pe(s, o); - } - function onclose() { - s.removeListener('finish', onfinish), unpipe(); - } - function onfinish() { - L('onfinish'), s.removeListener('close', onclose), unpipe(); - } - function unpipe() { - L('unpipe'), i.unpipe(s); - } - return ( - i.on('data', ondata), - (function prependListener(s, o, i) { - if ('function' == typeof s.prependListener) return s.prependListener(o, i); - s._events && s._events[o] - ? Array.isArray(s._events[o]) - ? s._events[o].unshift(i) - : (s._events[o] = [i, s._events[o]]) - : s.on(o, i); - })(s, 'error', onerror), - s.once('close', onclose), - s.once('finish', onfinish), - s.emit('pipe', i), - u.flowing || (L('pipe resume'), i.resume()), - s - ); - }), - (Readable.prototype.unpipe = function (s) { - var o = this._readableState, - i = { hasUnpiped: !1 }; - if (0 === o.pipesCount) return this; - if (1 === o.pipesCount) - return ( - (s && s !== o.pipes) || - (s || (s = o.pipes), - (o.pipes = null), - (o.pipesCount = 0), - (o.flowing = !1), - s && s.emit('unpipe', this, i)), - this - ); - if (!s) { - var u = o.pipes, - _ = o.pipesCount; - (o.pipes = null), (o.pipesCount = 0), (o.flowing = !1); - for (var w = 0; w < _; w++) u[w].emit('unpipe', this, { hasUnpiped: !1 }); - return this; - } - var x = indexOf(o.pipes, s); - return ( - -1 === x || - (o.pipes.splice(x, 1), - (o.pipesCount -= 1), - 1 === o.pipesCount && (o.pipes = o.pipes[0]), - s.emit('unpipe', this, i)), - this - ); - }), - (Readable.prototype.on = function (s, o) { - var i = x.prototype.on.call(this, s, o), - u = this._readableState; - return ( - 'data' === s - ? ((u.readableListening = this.listenerCount('readable') > 0), - !1 !== u.flowing && this.resume()) - : 'readable' === s && - (u.endEmitted || - u.readableListening || - ((u.readableListening = u.needReadable = !0), - (u.flowing = !1), - (u.emittedReadable = !1), - L('on readable', u.length, u.reading), - u.length - ? emitReadable(this) - : u.reading || _.nextTick(nReadingNextTick, this))), - i - ); - }), - (Readable.prototype.addListener = Readable.prototype.on), - (Readable.prototype.removeListener = function (s, o) { - var i = x.prototype.removeListener.call(this, s, o); - return 'readable' === s && _.nextTick(updateReadableListening, this), i; - }), - (Readable.prototype.removeAllListeners = function (s) { - var o = x.prototype.removeAllListeners.apply(this, arguments); - return ( - ('readable' !== s && void 0 !== s) || _.nextTick(updateReadableListening, this), o - ); - }), - (Readable.prototype.resume = function () { - var s = this._readableState; - return ( - s.flowing || - (L('resume'), - (s.flowing = !s.readableListening), - (function resume(s, o) { - o.resumeScheduled || ((o.resumeScheduled = !0), _.nextTick(resume_, s, o)); - })(this, s)), - (s.paused = !1), - this - ); - }), - (Readable.prototype.pause = function () { - return ( - L('call pause flowing=%j', this._readableState.flowing), - !1 !== this._readableState.flowing && - (L('pause'), (this._readableState.flowing = !1), this.emit('pause')), - (this._readableState.paused = !0), - this - ); - }), - (Readable.prototype.wrap = function (s) { - var o = this, - i = this._readableState, - u = !1; - for (var _ in (s.on('end', function () { - if ((L('wrapped end'), i.decoder && !i.ended)) { - var s = i.decoder.end(); - s && s.length && o.push(s); - } - o.push(null); - }), - s.on('data', function (_) { - (L('wrapped data'), - i.decoder && (_ = i.decoder.write(_)), - i.objectMode && null == _) || - ((i.objectMode || (_ && _.length)) && (o.push(_) || ((u = !0), s.pause()))); - }), - s)) - void 0 === this[_] && - 'function' == typeof s[_] && - (this[_] = (function methodWrap(o) { - return function methodWrapReturnFunction() { - return s[o].apply(s, arguments); - }; - })(_)); - for (var w = 0; w < de.length; w++) s.on(de[w], this.emit.bind(this, de[w])); - return ( - (this._read = function (o) { - L('wrapped _read', o), u && ((u = !1), s.resume()); - }), - this - ); - }), - 'function' == typeof Symbol && - (Readable.prototype[Symbol.asyncIterator] = function () { - return void 0 === V && (V = i(2955)), V(this); - }), - Object.defineProperty(Readable.prototype, 'readableHighWaterMark', { - enumerable: !1, - get: function get() { - return this._readableState.highWaterMark; - } - }), - Object.defineProperty(Readable.prototype, 'readableBuffer', { - enumerable: !1, - get: function get() { - return this._readableState && this._readableState.buffer; - } - }), - Object.defineProperty(Readable.prototype, 'readableFlowing', { - enumerable: !1, - get: function get() { - return this._readableState.flowing; - }, - set: function set(s) { - this._readableState && (this._readableState.flowing = s); - } - }), - (Readable._fromList = fromList), - Object.defineProperty(Readable.prototype, 'readableLength', { - enumerable: !1, - get: function get() { - return this._readableState.length; - } - }), - 'function' == typeof Symbol && - (Readable.from = function (s, o) { - return void 0 === U && (U = i(55157)), U(Readable, s, o); - }); - }, - 74610: (s, o, i) => { - 'use strict'; - s.exports = Transform; - var u = i(86048).F, - _ = u.ERR_METHOD_NOT_IMPLEMENTED, - w = u.ERR_MULTIPLE_CALLBACK, - x = u.ERR_TRANSFORM_ALREADY_TRANSFORMING, - C = u.ERR_TRANSFORM_WITH_LENGTH_0, - j = i(25382); - function afterTransform(s, o) { - var i = this._transformState; - i.transforming = !1; - var u = i.writecb; - if (null === u) return this.emit('error', new w()); - (i.writechunk = null), (i.writecb = null), null != o && this.push(o), u(s); - var _ = this._readableState; - (_.reading = !1), - (_.needReadable || _.length < _.highWaterMark) && this._read(_.highWaterMark); - } - function Transform(s) { - if (!(this instanceof Transform)) return new Transform(s); - j.call(this, s), - (this._transformState = { - afterTransform: afterTransform.bind(this), - needTransform: !1, - transforming: !1, - writecb: null, - writechunk: null, - writeencoding: null - }), - (this._readableState.needReadable = !0), - (this._readableState.sync = !1), - s && - ('function' == typeof s.transform && (this._transform = s.transform), - 'function' == typeof s.flush && (this._flush = s.flush)), - this.on('prefinish', prefinish); - } - function prefinish() { - var s = this; - 'function' != typeof this._flush || this._readableState.destroyed - ? done(this, null, null) - : this._flush(function (o, i) { - done(s, o, i); - }); - } - function done(s, o, i) { - if (o) return s.emit('error', o); - if ((null != i && s.push(i), s._writableState.length)) throw new C(); - if (s._transformState.transforming) throw new x(); - return s.push(null); - } - i(56698)(Transform, j), - (Transform.prototype.push = function (s, o) { - return (this._transformState.needTransform = !1), j.prototype.push.call(this, s, o); - }), - (Transform.prototype._transform = function (s, o, i) { - i(new _('_transform()')); - }), - (Transform.prototype._write = function (s, o, i) { - var u = this._transformState; - if (((u.writecb = i), (u.writechunk = s), (u.writeencoding = o), !u.transforming)) { - var _ = this._readableState; - (u.needTransform || _.needReadable || _.length < _.highWaterMark) && - this._read(_.highWaterMark); - } - }), - (Transform.prototype._read = function (s) { - var o = this._transformState; - null === o.writechunk || o.transforming - ? (o.needTransform = !0) - : ((o.transforming = !0), - this._transform(o.writechunk, o.writeencoding, o.afterTransform)); - }), - (Transform.prototype._destroy = function (s, o) { - j.prototype._destroy.call(this, s, function (s) { - o(s); - }); - }); - }, - 16708: (s, o, i) => { - 'use strict'; - var u, - _ = i(65606); - function CorkedRequest(s) { - var o = this; - (this.next = null), - (this.entry = null), - (this.finish = function () { - !(function onCorkedFinish(s, o, i) { - var u = s.entry; - s.entry = null; - for (; u; ) { - var _ = u.callback; - o.pendingcb--, _(i), (u = u.next); - } - o.corkedRequestsFree.next = s; - })(o, s); - }); - } - (s.exports = Writable), (Writable.WritableState = WritableState); - var w = { deprecate: i(94643) }, - x = i(40345), - C = i(48287).Buffer, - j = - (void 0 !== i.g - ? i.g - : 'undefined' != typeof window - ? window - : 'undefined' != typeof self - ? self - : {} - ).Uint8Array || function () {}; - var L, - B = i(75896), - $ = i(65291).getHighWaterMark, - V = i(86048).F, - U = V.ERR_INVALID_ARG_TYPE, - z = V.ERR_METHOD_NOT_IMPLEMENTED, - Y = V.ERR_MULTIPLE_CALLBACK, - Z = V.ERR_STREAM_CANNOT_PIPE, - ee = V.ERR_STREAM_DESTROYED, - ie = V.ERR_STREAM_NULL_VALUES, - ae = V.ERR_STREAM_WRITE_AFTER_END, - le = V.ERR_UNKNOWN_ENCODING, - ce = B.errorOrDestroy; - function nop() {} - function WritableState(s, o, w) { - (u = u || i(25382)), - (s = s || {}), - 'boolean' != typeof w && (w = o instanceof u), - (this.objectMode = !!s.objectMode), - w && (this.objectMode = this.objectMode || !!s.writableObjectMode), - (this.highWaterMark = $(this, s, 'writableHighWaterMark', w)), - (this.finalCalled = !1), - (this.needDrain = !1), - (this.ending = !1), - (this.ended = !1), - (this.finished = !1), - (this.destroyed = !1); - var x = !1 === s.decodeStrings; - (this.decodeStrings = !x), - (this.defaultEncoding = s.defaultEncoding || 'utf8'), - (this.length = 0), - (this.writing = !1), - (this.corked = 0), - (this.sync = !0), - (this.bufferProcessing = !1), - (this.onwrite = function (s) { - !(function onwrite(s, o) { - var i = s._writableState, - u = i.sync, - w = i.writecb; - if ('function' != typeof w) throw new Y(); - if ( - ((function onwriteStateUpdate(s) { - (s.writing = !1), - (s.writecb = null), - (s.length -= s.writelen), - (s.writelen = 0); - })(i), - o) - ) - !(function onwriteError(s, o, i, u, w) { - --o.pendingcb, - i - ? (_.nextTick(w, u), - _.nextTick(finishMaybe, s, o), - (s._writableState.errorEmitted = !0), - ce(s, u)) - : (w(u), - (s._writableState.errorEmitted = !0), - ce(s, u), - finishMaybe(s, o)); - })(s, i, u, o, w); - else { - var x = needFinish(i) || s.destroyed; - x || i.corked || i.bufferProcessing || !i.bufferedRequest || clearBuffer(s, i), - u ? _.nextTick(afterWrite, s, i, x, w) : afterWrite(s, i, x, w); - } - })(o, s); - }), - (this.writecb = null), - (this.writelen = 0), - (this.bufferedRequest = null), - (this.lastBufferedRequest = null), - (this.pendingcb = 0), - (this.prefinished = !1), - (this.errorEmitted = !1), - (this.emitClose = !1 !== s.emitClose), - (this.autoDestroy = !!s.autoDestroy), - (this.bufferedRequestCount = 0), - (this.corkedRequestsFree = new CorkedRequest(this)); - } - function Writable(s) { - var o = this instanceof (u = u || i(25382)); - if (!o && !L.call(Writable, this)) return new Writable(s); - (this._writableState = new WritableState(s, this, o)), - (this.writable = !0), - s && - ('function' == typeof s.write && (this._write = s.write), - 'function' == typeof s.writev && (this._writev = s.writev), - 'function' == typeof s.destroy && (this._destroy = s.destroy), - 'function' == typeof s.final && (this._final = s.final)), - x.call(this); - } - function doWrite(s, o, i, u, _, w, x) { - (o.writelen = u), - (o.writecb = x), - (o.writing = !0), - (o.sync = !0), - o.destroyed - ? o.onwrite(new ee('write')) - : i - ? s._writev(_, o.onwrite) - : s._write(_, w, o.onwrite), - (o.sync = !1); - } - function afterWrite(s, o, i, u) { - i || - (function onwriteDrain(s, o) { - 0 === o.length && o.needDrain && ((o.needDrain = !1), s.emit('drain')); - })(s, o), - o.pendingcb--, - u(), - finishMaybe(s, o); - } - function clearBuffer(s, o) { - o.bufferProcessing = !0; - var i = o.bufferedRequest; - if (s._writev && i && i.next) { - var u = o.bufferedRequestCount, - _ = new Array(u), - w = o.corkedRequestsFree; - w.entry = i; - for (var x = 0, C = !0; i; ) (_[x] = i), i.isBuf || (C = !1), (i = i.next), (x += 1); - (_.allBuffers = C), - doWrite(s, o, !0, o.length, _, '', w.finish), - o.pendingcb++, - (o.lastBufferedRequest = null), - w.next - ? ((o.corkedRequestsFree = w.next), (w.next = null)) - : (o.corkedRequestsFree = new CorkedRequest(o)), - (o.bufferedRequestCount = 0); - } else { - for (; i; ) { - var j = i.chunk, - L = i.encoding, - B = i.callback; - if ( - (doWrite(s, o, !1, o.objectMode ? 1 : j.length, j, L, B), - (i = i.next), - o.bufferedRequestCount--, - o.writing) - ) - break; - } - null === i && (o.lastBufferedRequest = null); - } - (o.bufferedRequest = i), (o.bufferProcessing = !1); - } - function needFinish(s) { - return ( - s.ending && 0 === s.length && null === s.bufferedRequest && !s.finished && !s.writing - ); - } - function callFinal(s, o) { - s._final(function (i) { - o.pendingcb--, - i && ce(s, i), - (o.prefinished = !0), - s.emit('prefinish'), - finishMaybe(s, o); - }); - } - function finishMaybe(s, o) { - var i = needFinish(o); - if ( - i && - ((function prefinish(s, o) { - o.prefinished || - o.finalCalled || - ('function' != typeof s._final || o.destroyed - ? ((o.prefinished = !0), s.emit('prefinish')) - : (o.pendingcb++, (o.finalCalled = !0), _.nextTick(callFinal, s, o))); - })(s, o), - 0 === o.pendingcb && ((o.finished = !0), s.emit('finish'), o.autoDestroy)) - ) { - var u = s._readableState; - (!u || (u.autoDestroy && u.endEmitted)) && s.destroy(); - } - return i; - } - i(56698)(Writable, x), - (WritableState.prototype.getBuffer = function getBuffer() { - for (var s = this.bufferedRequest, o = []; s; ) o.push(s), (s = s.next); - return o; - }), - (function () { - try { - Object.defineProperty(WritableState.prototype, 'buffer', { - get: w.deprecate( - function writableStateBufferGetter() { - return this.getBuffer(); - }, - '_writableState.buffer is deprecated. Use _writableState.getBuffer instead.', - 'DEP0003' - ) - }); - } catch (s) {} - })(), - 'function' == typeof Symbol && - Symbol.hasInstance && - 'function' == typeof Function.prototype[Symbol.hasInstance] - ? ((L = Function.prototype[Symbol.hasInstance]), - Object.defineProperty(Writable, Symbol.hasInstance, { - value: function value(s) { - return ( - !!L.call(this, s) || - (this === Writable && s && s._writableState instanceof WritableState) - ); - } - })) - : (L = function realHasInstance(s) { - return s instanceof this; - }), - (Writable.prototype.pipe = function () { - ce(this, new Z()); - }), - (Writable.prototype.write = function (s, o, i) { - var u = this._writableState, - w = !1, - x = - !u.objectMode && - (function _isUint8Array(s) { - return C.isBuffer(s) || s instanceof j; - })(s); - return ( - x && - !C.isBuffer(s) && - (s = (function _uint8ArrayToBuffer(s) { - return C.from(s); - })(s)), - 'function' == typeof o && ((i = o), (o = null)), - x ? (o = 'buffer') : o || (o = u.defaultEncoding), - 'function' != typeof i && (i = nop), - u.ending - ? (function writeAfterEnd(s, o) { - var i = new ae(); - ce(s, i), _.nextTick(o, i); - })(this, i) - : (x || - (function validChunk(s, o, i, u) { - var w; - return ( - null === i - ? (w = new ie()) - : 'string' == typeof i || - o.objectMode || - (w = new U('chunk', ['string', 'Buffer'], i)), - !w || (ce(s, w), _.nextTick(u, w), !1) - ); - })(this, u, s, i)) && - (u.pendingcb++, - (w = (function writeOrBuffer(s, o, i, u, _, w) { - if (!i) { - var x = (function decodeChunk(s, o, i) { - s.objectMode || - !1 === s.decodeStrings || - 'string' != typeof o || - (o = C.from(o, i)); - return o; - })(o, u, _); - u !== x && ((i = !0), (_ = 'buffer'), (u = x)); - } - var j = o.objectMode ? 1 : u.length; - o.length += j; - var L = o.length < o.highWaterMark; - L || (o.needDrain = !0); - if (o.writing || o.corked) { - var B = o.lastBufferedRequest; - (o.lastBufferedRequest = { - chunk: u, - encoding: _, - isBuf: i, - callback: w, - next: null - }), - B - ? (B.next = o.lastBufferedRequest) - : (o.bufferedRequest = o.lastBufferedRequest), - (o.bufferedRequestCount += 1); - } else doWrite(s, o, !1, j, u, _, w); - return L; - })(this, u, x, s, o, i))), - w - ); - }), - (Writable.prototype.cork = function () { - this._writableState.corked++; - }), - (Writable.prototype.uncork = function () { - var s = this._writableState; - s.corked && - (s.corked--, - s.writing || - s.corked || - s.bufferProcessing || - !s.bufferedRequest || - clearBuffer(this, s)); - }), - (Writable.prototype.setDefaultEncoding = function setDefaultEncoding(s) { - if ( - ('string' == typeof s && (s = s.toLowerCase()), - !( - [ - 'hex', - 'utf8', - 'utf-8', - 'ascii', - 'binary', - 'base64', - 'ucs2', - 'ucs-2', - 'utf16le', - 'utf-16le', - 'raw' - ].indexOf((s + '').toLowerCase()) > -1 - )) - ) - throw new le(s); - return (this._writableState.defaultEncoding = s), this; - }), - Object.defineProperty(Writable.prototype, 'writableBuffer', { - enumerable: !1, - get: function get() { - return this._writableState && this._writableState.getBuffer(); - } - }), - Object.defineProperty(Writable.prototype, 'writableHighWaterMark', { - enumerable: !1, - get: function get() { - return this._writableState.highWaterMark; - } - }), - (Writable.prototype._write = function (s, o, i) { - i(new z('_write()')); - }), - (Writable.prototype._writev = null), - (Writable.prototype.end = function (s, o, i) { - var u = this._writableState; - return ( - 'function' == typeof s - ? ((i = s), (s = null), (o = null)) - : 'function' == typeof o && ((i = o), (o = null)), - null != s && this.write(s, o), - u.corked && ((u.corked = 1), this.uncork()), - u.ending || - (function endWritable(s, o, i) { - (o.ending = !0), - finishMaybe(s, o), - i && (o.finished ? _.nextTick(i) : s.once('finish', i)); - (o.ended = !0), (s.writable = !1); - })(this, u, i), - this - ); - }), - Object.defineProperty(Writable.prototype, 'writableLength', { - enumerable: !1, - get: function get() { - return this._writableState.length; - } - }), - Object.defineProperty(Writable.prototype, 'destroyed', { - enumerable: !1, - get: function get() { - return void 0 !== this._writableState && this._writableState.destroyed; - }, - set: function set(s) { - this._writableState && (this._writableState.destroyed = s); - } - }), - (Writable.prototype.destroy = B.destroy), - (Writable.prototype._undestroy = B.undestroy), - (Writable.prototype._destroy = function (s, o) { - o(s); - }); - }, - 2955: (s, o, i) => { - 'use strict'; - var u, - _ = i(65606); - function _defineProperty(s, o, i) { - return ( - (o = (function _toPropertyKey(s) { - var o = (function _toPrimitive(s, o) { - if ('object' != typeof s || null === s) return s; - var i = s[Symbol.toPrimitive]; - if (void 0 !== i) { - var u = i.call(s, o || 'default'); - if ('object' != typeof u) return u; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return ('string' === o ? String : Number)(s); - })(s, 'string'); - return 'symbol' == typeof o ? o : String(o); - })(o)) in s - ? Object.defineProperty(s, o, { - value: i, - enumerable: !0, - configurable: !0, - writable: !0 - }) - : (s[o] = i), - s - ); - } - var w = i(86238), - x = Symbol('lastResolve'), - C = Symbol('lastReject'), - j = Symbol('error'), - L = Symbol('ended'), - B = Symbol('lastPromise'), - $ = Symbol('handlePromise'), - V = Symbol('stream'); - function createIterResult(s, o) { - return { value: s, done: o }; - } - function readAndResolve(s) { - var o = s[x]; - if (null !== o) { - var i = s[V].read(); - null !== i && - ((s[B] = null), (s[x] = null), (s[C] = null), o(createIterResult(i, !1))); - } - } - function onReadable(s) { - _.nextTick(readAndResolve, s); - } - var U = Object.getPrototypeOf(function () {}), - z = Object.setPrototypeOf( - (_defineProperty( - (u = { - get stream() { - return this[V]; - }, - next: function next() { - var s = this, - o = this[j]; - if (null !== o) return Promise.reject(o); - if (this[L]) return Promise.resolve(createIterResult(void 0, !0)); - if (this[V].destroyed) - return new Promise(function (o, i) { - _.nextTick(function () { - s[j] ? i(s[j]) : o(createIterResult(void 0, !0)); - }); - }); - var i, - u = this[B]; - if (u) - i = new Promise( - (function wrapForNext(s, o) { - return function (i, u) { - s.then(function () { - o[L] ? i(createIterResult(void 0, !0)) : o[$](i, u); - }, u); - }; - })(u, this) - ); - else { - var w = this[V].read(); - if (null !== w) return Promise.resolve(createIterResult(w, !1)); - i = new Promise(this[$]); - } - return (this[B] = i), i; - } - }), - Symbol.asyncIterator, - function () { - return this; - } - ), - _defineProperty(u, 'return', function _return() { - var s = this; - return new Promise(function (o, i) { - s[V].destroy(null, function (s) { - s ? i(s) : o(createIterResult(void 0, !0)); - }); - }); - }), - u), - U - ); - s.exports = function createReadableStreamAsyncIterator(s) { - var o, - i = Object.create( - z, - (_defineProperty((o = {}), V, { value: s, writable: !0 }), - _defineProperty(o, x, { value: null, writable: !0 }), - _defineProperty(o, C, { value: null, writable: !0 }), - _defineProperty(o, j, { value: null, writable: !0 }), - _defineProperty(o, L, { value: s._readableState.endEmitted, writable: !0 }), - _defineProperty(o, $, { - value: function value(s, o) { - var u = i[V].read(); - u - ? ((i[B] = null), (i[x] = null), (i[C] = null), s(createIterResult(u, !1))) - : ((i[x] = s), (i[C] = o)); - }, - writable: !0 - }), - o) - ); - return ( - (i[B] = null), - w(s, function (s) { - if (s && 'ERR_STREAM_PREMATURE_CLOSE' !== s.code) { - var o = i[C]; - return ( - null !== o && ((i[B] = null), (i[x] = null), (i[C] = null), o(s)), - void (i[j] = s) - ); - } - var u = i[x]; - null !== u && - ((i[B] = null), (i[x] = null), (i[C] = null), u(createIterResult(void 0, !0))), - (i[L] = !0); - }), - s.on('readable', onReadable.bind(null, i)), - i - ); - }; - }, - 80345: (s, o, i) => { - 'use strict'; - function ownKeys(s, o) { - var i = Object.keys(s); - if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); - o && - (u = u.filter(function (o) { - return Object.getOwnPropertyDescriptor(s, o).enumerable; - })), - i.push.apply(i, u); - } - return i; - } - function _objectSpread(s) { - for (var o = 1; o < arguments.length; o++) { - var i = null != arguments[o] ? arguments[o] : {}; - o % 2 - ? ownKeys(Object(i), !0).forEach(function (o) { - _defineProperty(s, o, i[o]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) - : ownKeys(Object(i)).forEach(function (o) { - Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); - }); - } - return s; - } - function _defineProperty(s, o, i) { - return ( - (o = _toPropertyKey(o)) in s - ? Object.defineProperty(s, o, { - value: i, - enumerable: !0, - configurable: !0, - writable: !0 - }) - : (s[o] = i), - s - ); - } - function _defineProperties(s, o) { - for (var i = 0; i < o.length; i++) { - var u = o[i]; - (u.enumerable = u.enumerable || !1), - (u.configurable = !0), - 'value' in u && (u.writable = !0), - Object.defineProperty(s, _toPropertyKey(u.key), u); - } - } - function _toPropertyKey(s) { - var o = (function _toPrimitive(s, o) { - if ('object' != typeof s || null === s) return s; - var i = s[Symbol.toPrimitive]; - if (void 0 !== i) { - var u = i.call(s, o || 'default'); - if ('object' != typeof u) return u; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return ('string' === o ? String : Number)(s); - })(s, 'string'); - return 'symbol' == typeof o ? o : String(o); - } - var u = i(48287).Buffer, - _ = i(15340).inspect, - w = (_ && _.custom) || 'inspect'; - s.exports = (function () { - function BufferList() { - !(function _classCallCheck(s, o) { - if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); - })(this, BufferList), - (this.head = null), - (this.tail = null), - (this.length = 0); - } - return ( - (function _createClass(s, o, i) { - return ( - o && _defineProperties(s.prototype, o), - i && _defineProperties(s, i), - Object.defineProperty(s, 'prototype', { writable: !1 }), - s - ); - })(BufferList, [ - { - key: 'push', - value: function push(s) { - var o = { data: s, next: null }; - this.length > 0 ? (this.tail.next = o) : (this.head = o), - (this.tail = o), - ++this.length; - } - }, - { - key: 'unshift', - value: function unshift(s) { - var o = { data: s, next: this.head }; - 0 === this.length && (this.tail = o), (this.head = o), ++this.length; - } - }, - { - key: 'shift', - value: function shift() { - if (0 !== this.length) { - var s = this.head.data; - return ( - 1 === this.length - ? (this.head = this.tail = null) - : (this.head = this.head.next), - --this.length, - s - ); - } - } - }, - { - key: 'clear', - value: function clear() { - (this.head = this.tail = null), (this.length = 0); - } - }, - { - key: 'join', - value: function join(s) { - if (0 === this.length) return ''; - for (var o = this.head, i = '' + o.data; (o = o.next); ) i += s + o.data; - return i; - } - }, - { - key: 'concat', - value: function concat(s) { - if (0 === this.length) return u.alloc(0); - for (var o, i, _, w = u.allocUnsafe(s >>> 0), x = this.head, C = 0; x; ) - (o = x.data), - (i = w), - (_ = C), - u.prototype.copy.call(o, i, _), - (C += x.data.length), - (x = x.next); - return w; - } - }, - { - key: 'consume', - value: function consume(s, o) { - var i; - return ( - s < this.head.data.length - ? ((i = this.head.data.slice(0, s)), - (this.head.data = this.head.data.slice(s))) - : (i = - s === this.head.data.length - ? this.shift() - : o - ? this._getString(s) - : this._getBuffer(s)), - i - ); - } - }, - { - key: 'first', - value: function first() { - return this.head.data; - } - }, - { - key: '_getString', - value: function _getString(s) { - var o = this.head, - i = 1, - u = o.data; - for (s -= u.length; (o = o.next); ) { - var _ = o.data, - w = s > _.length ? _.length : s; - if ((w === _.length ? (u += _) : (u += _.slice(0, s)), 0 === (s -= w))) { - w === _.length - ? (++i, o.next ? (this.head = o.next) : (this.head = this.tail = null)) - : ((this.head = o), (o.data = _.slice(w))); - break; - } - ++i; - } - return (this.length -= i), u; - } - }, - { - key: '_getBuffer', - value: function _getBuffer(s) { - var o = u.allocUnsafe(s), - i = this.head, - _ = 1; - for (i.data.copy(o), s -= i.data.length; (i = i.next); ) { - var w = i.data, - x = s > w.length ? w.length : s; - if ((w.copy(o, o.length - s, 0, x), 0 === (s -= x))) { - x === w.length - ? (++_, i.next ? (this.head = i.next) : (this.head = this.tail = null)) - : ((this.head = i), (i.data = w.slice(x))); - break; - } - ++_; - } - return (this.length -= _), o; - } - }, - { - key: w, - value: function value(s, o) { - return _( - this, - _objectSpread(_objectSpread({}, o), {}, { depth: 0, customInspect: !1 }) - ); - } - } - ]), - BufferList - ); - })(); - }, - 75896: (s, o, i) => { - 'use strict'; - var u = i(65606); - function emitErrorAndCloseNT(s, o) { - emitErrorNT(s, o), emitCloseNT(s); - } - function emitCloseNT(s) { - (s._writableState && !s._writableState.emitClose) || - (s._readableState && !s._readableState.emitClose) || - s.emit('close'); - } - function emitErrorNT(s, o) { - s.emit('error', o); - } - s.exports = { - destroy: function destroy(s, o) { - var i = this, - _ = this._readableState && this._readableState.destroyed, - w = this._writableState && this._writableState.destroyed; - return _ || w - ? (o - ? o(s) - : s && - (this._writableState - ? this._writableState.errorEmitted || - ((this._writableState.errorEmitted = !0), - u.nextTick(emitErrorNT, this, s)) - : u.nextTick(emitErrorNT, this, s)), - this) - : (this._readableState && (this._readableState.destroyed = !0), - this._writableState && (this._writableState.destroyed = !0), - this._destroy(s || null, function (s) { - !o && s - ? i._writableState - ? i._writableState.errorEmitted - ? u.nextTick(emitCloseNT, i) - : ((i._writableState.errorEmitted = !0), - u.nextTick(emitErrorAndCloseNT, i, s)) - : u.nextTick(emitErrorAndCloseNT, i, s) - : o - ? (u.nextTick(emitCloseNT, i), o(s)) - : u.nextTick(emitCloseNT, i); - }), - this); - }, - undestroy: function undestroy() { - this._readableState && - ((this._readableState.destroyed = !1), - (this._readableState.reading = !1), - (this._readableState.ended = !1), - (this._readableState.endEmitted = !1)), - this._writableState && - ((this._writableState.destroyed = !1), - (this._writableState.ended = !1), - (this._writableState.ending = !1), - (this._writableState.finalCalled = !1), - (this._writableState.prefinished = !1), - (this._writableState.finished = !1), - (this._writableState.errorEmitted = !1)); - }, - errorOrDestroy: function errorOrDestroy(s, o) { - var i = s._readableState, - u = s._writableState; - (i && i.autoDestroy) || (u && u.autoDestroy) ? s.destroy(o) : s.emit('error', o); - } - }; - }, - 86238: (s, o, i) => { - 'use strict'; - var u = i(86048).F.ERR_STREAM_PREMATURE_CLOSE; - function noop() {} - s.exports = function eos(s, o, i) { - if ('function' == typeof o) return eos(s, null, o); - o || (o = {}), - (i = (function once(s) { - var o = !1; - return function () { - if (!o) { - o = !0; - for (var i = arguments.length, u = new Array(i), _ = 0; _ < i; _++) - u[_] = arguments[_]; - s.apply(this, u); - } - }; - })(i || noop)); - var _ = o.readable || (!1 !== o.readable && s.readable), - w = o.writable || (!1 !== o.writable && s.writable), - x = function onlegacyfinish() { - s.writable || j(); - }, - C = s._writableState && s._writableState.finished, - j = function onfinish() { - (w = !1), (C = !0), _ || i.call(s); - }, - L = s._readableState && s._readableState.endEmitted, - B = function onend() { - (_ = !1), (L = !0), w || i.call(s); - }, - $ = function onerror(o) { - i.call(s, o); - }, - V = function onclose() { - var o; - return _ && !L - ? ((s._readableState && s._readableState.ended) || (o = new u()), i.call(s, o)) - : w && !C - ? ((s._writableState && s._writableState.ended) || (o = new u()), i.call(s, o)) - : void 0; - }, - U = function onrequest() { - s.req.on('finish', j); - }; - return ( - !(function isRequest(s) { - return s.setHeader && 'function' == typeof s.abort; - })(s) - ? w && !s._writableState && (s.on('end', x), s.on('close', x)) - : (s.on('complete', j), s.on('abort', V), s.req ? U() : s.on('request', U)), - s.on('end', B), - s.on('finish', j), - !1 !== o.error && s.on('error', $), - s.on('close', V), - function () { - s.removeListener('complete', j), - s.removeListener('abort', V), - s.removeListener('request', U), - s.req && s.req.removeListener('finish', j), - s.removeListener('end', x), - s.removeListener('close', x), - s.removeListener('finish', j), - s.removeListener('end', B), - s.removeListener('error', $), - s.removeListener('close', V); - } - ); - }; - }, - 55157: (s) => { - s.exports = function () { - throw new Error('Readable.from is not available in the browser'); - }; - }, - 57758: (s, o, i) => { - 'use strict'; - var u; - var _ = i(86048).F, - w = _.ERR_MISSING_ARGS, - x = _.ERR_STREAM_DESTROYED; - function noop(s) { - if (s) throw s; - } - function call(s) { - s(); - } - function pipe(s, o) { - return s.pipe(o); - } - s.exports = function pipeline() { - for (var s = arguments.length, o = new Array(s), _ = 0; _ < s; _++) o[_] = arguments[_]; - var C, - j = (function popCallback(s) { - return s.length ? ('function' != typeof s[s.length - 1] ? noop : s.pop()) : noop; - })(o); - if ((Array.isArray(o[0]) && (o = o[0]), o.length < 2)) throw new w('streams'); - var L = o.map(function (s, _) { - var w = _ < o.length - 1; - return (function destroyer(s, o, _, w) { - w = (function once(s) { - var o = !1; - return function () { - o || ((o = !0), s.apply(void 0, arguments)); - }; - })(w); - var C = !1; - s.on('close', function () { - C = !0; - }), - void 0 === u && (u = i(86238)), - u(s, { readable: o, writable: _ }, function (s) { - if (s) return w(s); - (C = !0), w(); - }); - var j = !1; - return function (o) { - if (!C && !j) - return ( - (j = !0), - (function isRequest(s) { - return s.setHeader && 'function' == typeof s.abort; - })(s) - ? s.abort() - : 'function' == typeof s.destroy - ? s.destroy() - : void w(o || new x('pipe')) - ); - }; - })(s, w, _ > 0, function (s) { - C || (C = s), s && L.forEach(call), w || (L.forEach(call), j(C)); - }); - }); - return o.reduce(pipe); - }; - }, - 65291: (s, o, i) => { - 'use strict'; - var u = i(86048).F.ERR_INVALID_OPT_VALUE; - s.exports = { - getHighWaterMark: function getHighWaterMark(s, o, i, _) { - var w = (function highWaterMarkFrom(s, o, i) { - return null != s.highWaterMark ? s.highWaterMark : o ? s[i] : null; - })(o, _, i); - if (null != w) { - if (!isFinite(w) || Math.floor(w) !== w || w < 0) - throw new u(_ ? i : 'highWaterMark', w); - return Math.floor(w); - } - return s.objectMode ? 16 : 16384; - } - }; - }, - 40345: (s, o, i) => { - s.exports = i(37007).EventEmitter; - }, - 84977: (s, o, i) => { - 'use strict'; - Object.defineProperty(o, '__esModule', { value: !0 }); - var u = (function _interopRequireDefault(s) { - return s && s.__esModule ? s : { default: s }; - })(i(9404)), - _ = i(55674); - (o.default = function (s) { - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : u.default.Map, - i = Object.keys(s); - return function () { - var u = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : o(), - w = arguments[1]; - return u.withMutations(function (o) { - i.forEach(function (i) { - var u = (0, s[i])(o.get(i), w); - (0, _.validateNextState)(u, i, w), o.set(i, u); - }); - }); - }; - }), - (s.exports = o.default); - }, - 89593: (s, o, i) => { - 'use strict'; - o.H = void 0; - var u = (function _interopRequireDefault(s) { - return s && s.__esModule ? s : { default: s }; - })(i(84977)); - o.H = u.default; - }, - 48590: (s, o) => { - 'use strict'; - Object.defineProperty(o, '__esModule', { value: !0 }), - (o.default = function (s) { - return s && '@@redux/INIT' === s.type - ? 'initialState argument passed to createStore' - : 'previous state received by the reducer'; - }), - (s.exports = o.default); - }, - 82261: (s, o, i) => { - 'use strict'; - Object.defineProperty(o, '__esModule', { value: !0 }); - var u = _interopRequireDefault(i(9404)), - _ = _interopRequireDefault(i(48590)); - function _interopRequireDefault(s) { - return s && s.__esModule ? s : { default: s }; - } - (o.default = function (s, o, i) { - var w = Object.keys(o); - if (!w.length) - return 'Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.'; - var x = (0, _.default)(i); - if ( - u.default.isImmutable ? !u.default.isImmutable(s) : !u.default.Iterable.isIterable(s) - ) - return ( - 'The ' + - x + - ' is of unexpected type. Expected argument to be an instance of Immutable.Collection or Immutable.Record with the following properties: "' + - w.join('", "') + - '".' - ); - var C = s - .toSeq() - .keySeq() - .toArray() - .filter(function (s) { - return !o.hasOwnProperty(s); - }); - return C.length > 0 - ? 'Unexpected ' + - (1 === C.length ? 'property' : 'properties') + - ' "' + - C.join('", "') + - '" found in ' + - x + - '. Expected to find one of the known reducer property names instead: "' + - w.join('", "') + - '". Unexpected properties will be ignored.' - : null; - }), - (s.exports = o.default); - }, - 55674: (s, o, i) => { - 'use strict'; - Object.defineProperty(o, '__esModule', { value: !0 }), - (o.validateNextState = - o.getUnexpectedInvocationParameterMessage = - o.getStateName = - void 0); - var u = _interopRequireDefault(i(48590)), - _ = _interopRequireDefault(i(82261)), - w = _interopRequireDefault(i(27374)); - function _interopRequireDefault(s) { - return s && s.__esModule ? s : { default: s }; - } - (o.getStateName = u.default), - (o.getUnexpectedInvocationParameterMessage = _.default), - (o.validateNextState = w.default); - }, - 27374: (s, o) => { - 'use strict'; - Object.defineProperty(o, '__esModule', { value: !0 }), - (o.default = function (s, o, i) { - if (void 0 === s) - throw new Error( - 'Reducer "' + - o + - '" returned undefined when handling "' + - i.type + - '" action. To ignore an action, you must explicitly return the previous state.' - ); - }), - (s.exports = o.default); - }, - 75208: (s) => { - 'use strict'; - var o, - i = ''; - s.exports = function repeat(s, u) { - if ('string' != typeof s) throw new TypeError('expected a string'); - if (1 === u) return s; - if (2 === u) return s + s; - var _ = s.length * u; - if (o !== s || void 0 === o) (o = s), (i = ''); - else if (i.length >= _) return i.substr(0, _); - for (; _ > i.length && u > 1; ) 1 & u && (i += s), (u >>= 1), (s += s); - return (i = (i += s).substr(0, _)); - }; - }, - 92063: (s) => { - 'use strict'; - s.exports = function required(s, o) { - if (((o = o.split(':')[0]), !(s = +s))) return !1; - switch (o) { - case 'http': - case 'ws': - return 80 !== s; - case 'https': - case 'wss': - return 443 !== s; - case 'ftp': - return 21 !== s; - case 'gopher': - return 70 !== s; - case 'file': - return !1; - } - return 0 !== s; - }; - }, - 27096: (s, o, i) => { - const u = i(87586), - _ = i(6205), - w = i(10023), - x = i(8048); - (s.exports = (s) => { - var o, - i, - C = 0, - j = { type: _.ROOT, stack: [] }, - L = j, - B = j.stack, - $ = [], - repeatErr = (o) => { - u.error(s, 'Nothing to repeat at column ' + (o - 1)); - }, - V = u.strToChars(s); - for (o = V.length; C < o; ) - switch ((i = V[C++])) { - case '\\': - switch ((i = V[C++])) { - case 'b': - B.push(x.wordBoundary()); - break; - case 'B': - B.push(x.nonWordBoundary()); - break; - case 'w': - B.push(w.words()); - break; - case 'W': - B.push(w.notWords()); - break; - case 'd': - B.push(w.ints()); - break; - case 'D': - B.push(w.notInts()); - break; - case 's': - B.push(w.whitespace()); - break; - case 'S': - B.push(w.notWhitespace()); - break; - default: - /\d/.test(i) - ? B.push({ type: _.REFERENCE, value: parseInt(i, 10) }) - : B.push({ type: _.CHAR, value: i.charCodeAt(0) }); - } - break; - case '^': - B.push(x.begin()); - break; - case '$': - B.push(x.end()); - break; - case '[': - var U; - '^' === V[C] ? ((U = !0), C++) : (U = !1); - var z = u.tokenizeClass(V.slice(C), s); - (C += z[1]), B.push({ type: _.SET, set: z[0], not: U }); - break; - case '.': - B.push(w.anyChar()); - break; - case '(': - var Y = { type: _.GROUP, stack: [], remember: !0 }; - '?' === (i = V[C]) && - ((i = V[C + 1]), - (C += 2), - '=' === i - ? (Y.followedBy = !0) - : '!' === i - ? (Y.notFollowedBy = !0) - : ':' !== i && - u.error( - s, - `Invalid group, character '${i}' after '?' at column ` + (C - 1) - ), - (Y.remember = !1)), - B.push(Y), - $.push(L), - (L = Y), - (B = Y.stack); - break; - case ')': - 0 === $.length && u.error(s, 'Unmatched ) at column ' + (C - 1)), - (B = (L = $.pop()).options ? L.options[L.options.length - 1] : L.stack); - break; - case '|': - L.options || ((L.options = [L.stack]), delete L.stack); - var Z = []; - L.options.push(Z), (B = Z); - break; - case '{': - var ee, - ie, - ae = /^(\d+)(,(\d+)?)?\}/.exec(V.slice(C)); - null !== ae - ? (0 === B.length && repeatErr(C), - (ee = parseInt(ae[1], 10)), - (ie = ae[2] ? (ae[3] ? parseInt(ae[3], 10) : 1 / 0) : ee), - (C += ae[0].length), - B.push({ type: _.REPETITION, min: ee, max: ie, value: B.pop() })) - : B.push({ type: _.CHAR, value: 123 }); - break; - case '?': - 0 === B.length && repeatErr(C), - B.push({ type: _.REPETITION, min: 0, max: 1, value: B.pop() }); - break; - case '+': - 0 === B.length && repeatErr(C), - B.push({ type: _.REPETITION, min: 1, max: 1 / 0, value: B.pop() }); - break; - case '*': - 0 === B.length && repeatErr(C), - B.push({ type: _.REPETITION, min: 0, max: 1 / 0, value: B.pop() }); - break; - default: - B.push({ type: _.CHAR, value: i.charCodeAt(0) }); - } - return 0 !== $.length && u.error(s, 'Unterminated group'), j; - }), - (s.exports.types = _); - }, - 8048: (s, o, i) => { - const u = i(6205); - (o.wordBoundary = () => ({ type: u.POSITION, value: 'b' })), - (o.nonWordBoundary = () => ({ type: u.POSITION, value: 'B' })), - (o.begin = () => ({ type: u.POSITION, value: '^' })), - (o.end = () => ({ type: u.POSITION, value: '$' })); - }, - 10023: (s, o, i) => { - const u = i(6205), - INTS = () => [{ type: u.RANGE, from: 48, to: 57 }], - WORDS = () => - [ - { type: u.CHAR, value: 95 }, - { type: u.RANGE, from: 97, to: 122 }, - { type: u.RANGE, from: 65, to: 90 } - ].concat(INTS()), - WHITESPACE = () => [ - { type: u.CHAR, value: 9 }, - { type: u.CHAR, value: 10 }, - { type: u.CHAR, value: 11 }, - { type: u.CHAR, value: 12 }, - { type: u.CHAR, value: 13 }, - { type: u.CHAR, value: 32 }, - { type: u.CHAR, value: 160 }, - { type: u.CHAR, value: 5760 }, - { type: u.RANGE, from: 8192, to: 8202 }, - { type: u.CHAR, value: 8232 }, - { type: u.CHAR, value: 8233 }, - { type: u.CHAR, value: 8239 }, - { type: u.CHAR, value: 8287 }, - { type: u.CHAR, value: 12288 }, - { type: u.CHAR, value: 65279 } - ]; - (o.words = () => ({ type: u.SET, set: WORDS(), not: !1 })), - (o.notWords = () => ({ type: u.SET, set: WORDS(), not: !0 })), - (o.ints = () => ({ type: u.SET, set: INTS(), not: !1 })), - (o.notInts = () => ({ type: u.SET, set: INTS(), not: !0 })), - (o.whitespace = () => ({ type: u.SET, set: WHITESPACE(), not: !1 })), - (o.notWhitespace = () => ({ type: u.SET, set: WHITESPACE(), not: !0 })), - (o.anyChar = () => ({ - type: u.SET, - set: [ - { type: u.CHAR, value: 10 }, - { type: u.CHAR, value: 13 }, - { type: u.CHAR, value: 8232 }, - { type: u.CHAR, value: 8233 } - ], - not: !0 - })); - }, - 6205: (s) => { - s.exports = { - ROOT: 0, - GROUP: 1, - POSITION: 2, - SET: 3, - RANGE: 4, - REPETITION: 5, - REFERENCE: 6, - CHAR: 7 - }; - }, - 87586: (s, o, i) => { - const u = i(6205), - _ = i(10023), - w = { 0: 0, t: 9, n: 10, v: 11, f: 12, r: 13 }; - (o.strToChars = function (s) { - return (s = s.replace( - /(\[\\b\])|(\\)?\\(?:u([A-F0-9]{4})|x([A-F0-9]{2})|(0?[0-7]{2})|c([@A-Z[\\\]^?])|([0tnvfr]))/g, - function (s, o, i, u, _, x, C, j) { - if (i) return s; - var L = o - ? 8 - : u - ? parseInt(u, 16) - : _ - ? parseInt(_, 16) - : x - ? parseInt(x, 8) - : C - ? '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^ ?'.indexOf(C) - : w[j], - B = String.fromCharCode(L); - return /[[\]{}^$.|?*+()]/.test(B) && (B = '\\' + B), B; - } - )); - }), - (o.tokenizeClass = (s, i) => { - for ( - var w, - x, - C = [], - j = - /\\(?:(w)|(d)|(s)|(W)|(D)|(S))|((?:(?:\\)(.)|([^\]\\]))-(?:\\)?([^\]]))|(\])|(?:\\)?([^])/g; - null != (w = j.exec(s)); - - ) - if (w[1]) C.push(_.words()); - else if (w[2]) C.push(_.ints()); - else if (w[3]) C.push(_.whitespace()); - else if (w[4]) C.push(_.notWords()); - else if (w[5]) C.push(_.notInts()); - else if (w[6]) C.push(_.notWhitespace()); - else if (w[7]) - C.push({ - type: u.RANGE, - from: (w[8] || w[9]).charCodeAt(0), - to: w[10].charCodeAt(0) - }); - else { - if (!(x = w[12])) return [C, j.lastIndex]; - C.push({ type: u.CHAR, value: x.charCodeAt(0) }); - } - o.error(i, 'Unterminated character class'); - }), - (o.error = (s, o) => { - throw new SyntaxError('Invalid regular expression: /' + s + '/: ' + o); - }); - }, - 92861: (s, o, i) => { - var u = i(48287), - _ = u.Buffer; - function copyProps(s, o) { - for (var i in s) o[i] = s[i]; - } - function SafeBuffer(s, o, i) { - return _(s, o, i); - } - _.from && _.alloc && _.allocUnsafe && _.allocUnsafeSlow - ? (s.exports = u) - : (copyProps(u, o), (o.Buffer = SafeBuffer)), - (SafeBuffer.prototype = Object.create(_.prototype)), - copyProps(_, SafeBuffer), - (SafeBuffer.from = function (s, o, i) { - if ('number' == typeof s) throw new TypeError('Argument must not be a number'); - return _(s, o, i); - }), - (SafeBuffer.alloc = function (s, o, i) { - if ('number' != typeof s) throw new TypeError('Argument must be a number'); - var u = _(s); - return ( - void 0 !== o ? ('string' == typeof i ? u.fill(o, i) : u.fill(o)) : u.fill(0), u - ); - }), - (SafeBuffer.allocUnsafe = function (s) { - if ('number' != typeof s) throw new TypeError('Argument must be a number'); - return _(s); - }), - (SafeBuffer.allocUnsafeSlow = function (s) { - if ('number' != typeof s) throw new TypeError('Argument must be a number'); - return u.SlowBuffer(s); - }); - }, - 29844: (s, o) => { - 'use strict'; - function f(s, o) { - var i = s.length; - s.push(o); - e: for (; 0 < i; ) { - var u = (i - 1) >>> 1, - _ = s[u]; - if (!(0 < g(_, o))) break e; - (s[u] = o), (s[i] = _), (i = u); - } - } - function h(s) { - return 0 === s.length ? null : s[0]; - } - function k(s) { - if (0 === s.length) return null; - var o = s[0], - i = s.pop(); - if (i !== o) { - s[0] = i; - e: for (var u = 0, _ = s.length, w = _ >>> 1; u < w; ) { - var x = 2 * (u + 1) - 1, - C = s[x], - j = x + 1, - L = s[j]; - if (0 > g(C, i)) - j < _ && 0 > g(L, C) - ? ((s[u] = L), (s[j] = i), (u = j)) - : ((s[u] = C), (s[x] = i), (u = x)); - else { - if (!(j < _ && 0 > g(L, i))) break e; - (s[u] = L), (s[j] = i), (u = j); - } - } - } - return o; - } - function g(s, o) { - var i = s.sortIndex - o.sortIndex; - return 0 !== i ? i : s.id - o.id; - } - if ('object' == typeof performance && 'function' == typeof performance.now) { - var i = performance; - o.unstable_now = function () { - return i.now(); - }; - } else { - var u = Date, - _ = u.now(); - o.unstable_now = function () { - return u.now() - _; - }; - } - var w = [], - x = [], - C = 1, - j = null, - L = 3, - B = !1, - $ = !1, - V = !1, - U = 'function' == typeof setTimeout ? setTimeout : null, - z = 'function' == typeof clearTimeout ? clearTimeout : null, - Y = 'undefined' != typeof setImmediate ? setImmediate : null; - function G(s) { - for (var o = h(x); null !== o; ) { - if (null === o.callback) k(x); - else { - if (!(o.startTime <= s)) break; - k(x), (o.sortIndex = o.expirationTime), f(w, o); - } - o = h(x); - } - } - function H(s) { - if (((V = !1), G(s), !$)) - if (null !== h(w)) ($ = !0), I(J); - else { - var o = h(x); - null !== o && K(H, o.startTime - s); - } - } - function J(s, i) { - ($ = !1), V && ((V = !1), z(ae), (ae = -1)), (B = !0); - var u = L; - try { - for (G(i), j = h(w); null !== j && (!(j.expirationTime > i) || (s && !M())); ) { - var _ = j.callback; - if ('function' == typeof _) { - (j.callback = null), (L = j.priorityLevel); - var C = _(j.expirationTime <= i); - (i = o.unstable_now()), - 'function' == typeof C ? (j.callback = C) : j === h(w) && k(w), - G(i); - } else k(w); - j = h(w); - } - if (null !== j) var U = !0; - else { - var Y = h(x); - null !== Y && K(H, Y.startTime - i), (U = !1); - } - return U; - } finally { - (j = null), (L = u), (B = !1); - } - } - 'undefined' != typeof navigator && - void 0 !== navigator.scheduling && - void 0 !== navigator.scheduling.isInputPending && - navigator.scheduling.isInputPending.bind(navigator.scheduling); - var Z, - ee = !1, - ie = null, - ae = -1, - le = 5, - ce = -1; - function M() { - return !(o.unstable_now() - ce < le); - } - function R() { - if (null !== ie) { - var s = o.unstable_now(); - ce = s; - var i = !0; - try { - i = ie(!0, s); - } finally { - i ? Z() : ((ee = !1), (ie = null)); - } - } else ee = !1; - } - if ('function' == typeof Y) - Z = function () { - Y(R); - }; - else if ('undefined' != typeof MessageChannel) { - var pe = new MessageChannel(), - de = pe.port2; - (pe.port1.onmessage = R), - (Z = function () { - de.postMessage(null); - }); - } else - Z = function () { - U(R, 0); - }; - function I(s) { - (ie = s), ee || ((ee = !0), Z()); - } - function K(s, i) { - ae = U(function () { - s(o.unstable_now()); - }, i); - } - (o.unstable_IdlePriority = 5), - (o.unstable_ImmediatePriority = 1), - (o.unstable_LowPriority = 4), - (o.unstable_NormalPriority = 3), - (o.unstable_Profiling = null), - (o.unstable_UserBlockingPriority = 2), - (o.unstable_cancelCallback = function (s) { - s.callback = null; - }), - (o.unstable_continueExecution = function () { - $ || B || (($ = !0), I(J)); - }), - (o.unstable_forceFrameRate = function (s) { - 0 > s || 125 < s - ? console.error( - 'forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported' - ) - : (le = 0 < s ? Math.floor(1e3 / s) : 5); - }), - (o.unstable_getCurrentPriorityLevel = function () { - return L; - }), - (o.unstable_getFirstCallbackNode = function () { - return h(w); - }), - (o.unstable_next = function (s) { - switch (L) { - case 1: - case 2: - case 3: - var o = 3; - break; - default: - o = L; - } - var i = L; - L = o; - try { - return s(); - } finally { - L = i; - } - }), - (o.unstable_pauseExecution = function () {}), - (o.unstable_requestPaint = function () {}), - (o.unstable_runWithPriority = function (s, o) { - switch (s) { - case 1: - case 2: - case 3: - case 4: - case 5: - break; - default: - s = 3; - } - var i = L; - L = s; - try { - return o(); - } finally { - L = i; - } - }), - (o.unstable_scheduleCallback = function (s, i, u) { - var _ = o.unstable_now(); - switch ( - ('object' == typeof u && null !== u - ? (u = 'number' == typeof (u = u.delay) && 0 < u ? _ + u : _) - : (u = _), - s) - ) { - case 1: - var j = -1; - break; - case 2: - j = 250; - break; - case 5: - j = 1073741823; - break; - case 4: - j = 1e4; - break; - default: - j = 5e3; - } - return ( - (s = { - id: C++, - callback: i, - priorityLevel: s, - startTime: u, - expirationTime: (j = u + j), - sortIndex: -1 - }), - u > _ - ? ((s.sortIndex = u), - f(x, s), - null === h(w) && s === h(x) && (V ? (z(ae), (ae = -1)) : (V = !0), K(H, u - _))) - : ((s.sortIndex = j), f(w, s), $ || B || (($ = !0), I(J))), - s - ); - }), - (o.unstable_shouldYield = M), - (o.unstable_wrapCallback = function (s) { - var o = L; - return function () { - var i = L; - L = o; - try { - return s.apply(this, arguments); - } finally { - L = i; - } - }; - }); - }, - 69982: (s, o, i) => { - 'use strict'; - s.exports = i(29844); - }, - 20334: (s, o, i) => { - 'use strict'; - var u = i(48287).Buffer; - class NonError extends Error { - constructor(s) { - super(NonError._prepareSuperMessage(s)), - Object.defineProperty(this, 'name', { - value: 'NonError', - configurable: !0, - writable: !0 - }), - Error.captureStackTrace && Error.captureStackTrace(this, NonError); - } - static _prepareSuperMessage(s) { - try { - return JSON.stringify(s); - } catch { - return String(s); - } - } - } - const _ = [ - { property: 'name', enumerable: !1 }, - { property: 'message', enumerable: !1 }, - { property: 'stack', enumerable: !1 }, - { property: 'code', enumerable: !0 } - ], - w = Symbol('.toJSON called'), - destroyCircular = ({ - from: s, - seen: o, - to_: i, - forceEnumerable: x, - maxDepth: C, - depth: j - }) => { - const L = i || (Array.isArray(s) ? [] : {}); - if ((o.push(s), j >= C)) return L; - if ('function' == typeof s.toJSON && !0 !== s[w]) - return ((s) => { - s[w] = !0; - const o = s.toJSON(); - return delete s[w], o; - })(s); - for (const [i, _] of Object.entries(s)) - 'function' == typeof u && u.isBuffer(_) - ? (L[i] = '[object Buffer]') - : 'function' != typeof _ && - (_ && 'object' == typeof _ - ? o.includes(s[i]) - ? (L[i] = '[Circular]') - : (j++, - (L[i] = destroyCircular({ - from: s[i], - seen: o.slice(), - forceEnumerable: x, - maxDepth: C, - depth: j - }))) - : (L[i] = _)); - for (const { property: o, enumerable: i } of _) - 'string' == typeof s[o] && - Object.defineProperty(L, o, { - value: s[o], - enumerable: !!x || i, - configurable: !0, - writable: !0 - }); - return L; - }; - s.exports = { - serializeError: (s, o = {}) => { - const { maxDepth: i = Number.POSITIVE_INFINITY } = o; - return 'object' == typeof s && null !== s - ? destroyCircular({ from: s, seen: [], forceEnumerable: !0, maxDepth: i, depth: 0 }) - : 'function' == typeof s - ? `[Function: ${s.name || 'anonymous'}]` - : s; - }, - deserializeError: (s, o = {}) => { - const { maxDepth: i = Number.POSITIVE_INFINITY } = o; - if (s instanceof Error) return s; - if ('object' == typeof s && null !== s && !Array.isArray(s)) { - const o = new Error(); - return destroyCircular({ from: s, seen: [], to_: o, maxDepth: i, depth: 0 }), o; - } - return new NonError(s); - } - }; - }, - 90392: (s, o, i) => { - var u = i(92861).Buffer; - function Hash(s, o) { - (this._block = u.alloc(s)), - (this._finalSize = o), - (this._blockSize = s), - (this._len = 0); - } - (Hash.prototype.update = function (s, o) { - 'string' == typeof s && ((o = o || 'utf8'), (s = u.from(s, o))); - for ( - var i = this._block, _ = this._blockSize, w = s.length, x = this._len, C = 0; - C < w; - - ) { - for (var j = x % _, L = Math.min(w - C, _ - j), B = 0; B < L; B++) - i[j + B] = s[C + B]; - (C += L), (x += L) % _ == 0 && this._update(i); - } - return (this._len += w), this; - }), - (Hash.prototype.digest = function (s) { - var o = this._len % this._blockSize; - (this._block[o] = 128), - this._block.fill(0, o + 1), - o >= this._finalSize && (this._update(this._block), this._block.fill(0)); - var i = 8 * this._len; - if (i <= 4294967295) this._block.writeUInt32BE(i, this._blockSize - 4); - else { - var u = (4294967295 & i) >>> 0, - _ = (i - u) / 4294967296; - this._block.writeUInt32BE(_, this._blockSize - 8), - this._block.writeUInt32BE(u, this._blockSize - 4); - } - this._update(this._block); - var w = this._hash(); - return s ? w.toString(s) : w; - }), - (Hash.prototype._update = function () { - throw new Error('_update must be implemented by subclass'); - }), - (s.exports = Hash); - }, - 62802: (s, o, i) => { - var u = (s.exports = function SHA(s) { - s = s.toLowerCase(); - var o = u[s]; - if (!o) throw new Error(s + ' is not supported (we accept pull requests)'); - return new o(); - }); - (u.sha = i(27816)), - (u.sha1 = i(63737)), - (u.sha224 = i(26710)), - (u.sha256 = i(24107)), - (u.sha384 = i(32827)), - (u.sha512 = i(82890)); - }, - 27816: (s, o, i) => { - var u = i(56698), - _ = i(90392), - w = i(92861).Buffer, - x = [1518500249, 1859775393, -1894007588, -899497514], - C = new Array(80); - function Sha() { - this.init(), (this._w = C), _.call(this, 64, 56); - } - function rotl30(s) { - return (s << 30) | (s >>> 2); - } - function ft(s, o, i, u) { - return 0 === s ? (o & i) | (~o & u) : 2 === s ? (o & i) | (o & u) | (i & u) : o ^ i ^ u; - } - u(Sha, _), - (Sha.prototype.init = function () { - return ( - (this._a = 1732584193), - (this._b = 4023233417), - (this._c = 2562383102), - (this._d = 271733878), - (this._e = 3285377520), - this - ); - }), - (Sha.prototype._update = function (s) { - for ( - var o, - i = this._w, - u = 0 | this._a, - _ = 0 | this._b, - w = 0 | this._c, - C = 0 | this._d, - j = 0 | this._e, - L = 0; - L < 16; - ++L - ) - i[L] = s.readInt32BE(4 * L); - for (; L < 80; ++L) i[L] = i[L - 3] ^ i[L - 8] ^ i[L - 14] ^ i[L - 16]; - for (var B = 0; B < 80; ++B) { - var $ = ~~(B / 20), - V = 0 | ((((o = u) << 5) | (o >>> 27)) + ft($, _, w, C) + j + i[B] + x[$]); - (j = C), (C = w), (w = rotl30(_)), (_ = u), (u = V); - } - (this._a = (u + this._a) | 0), - (this._b = (_ + this._b) | 0), - (this._c = (w + this._c) | 0), - (this._d = (C + this._d) | 0), - (this._e = (j + this._e) | 0); - }), - (Sha.prototype._hash = function () { - var s = w.allocUnsafe(20); - return ( - s.writeInt32BE(0 | this._a, 0), - s.writeInt32BE(0 | this._b, 4), - s.writeInt32BE(0 | this._c, 8), - s.writeInt32BE(0 | this._d, 12), - s.writeInt32BE(0 | this._e, 16), - s - ); - }), - (s.exports = Sha); - }, - 63737: (s, o, i) => { - var u = i(56698), - _ = i(90392), - w = i(92861).Buffer, - x = [1518500249, 1859775393, -1894007588, -899497514], - C = new Array(80); - function Sha1() { - this.init(), (this._w = C), _.call(this, 64, 56); - } - function rotl5(s) { - return (s << 5) | (s >>> 27); - } - function rotl30(s) { - return (s << 30) | (s >>> 2); - } - function ft(s, o, i, u) { - return 0 === s ? (o & i) | (~o & u) : 2 === s ? (o & i) | (o & u) | (i & u) : o ^ i ^ u; - } - u(Sha1, _), - (Sha1.prototype.init = function () { - return ( - (this._a = 1732584193), - (this._b = 4023233417), - (this._c = 2562383102), - (this._d = 271733878), - (this._e = 3285377520), - this - ); - }), - (Sha1.prototype._update = function (s) { - for ( - var o, - i = this._w, - u = 0 | this._a, - _ = 0 | this._b, - w = 0 | this._c, - C = 0 | this._d, - j = 0 | this._e, - L = 0; - L < 16; - ++L - ) - i[L] = s.readInt32BE(4 * L); - for (; L < 80; ++L) - i[L] = ((o = i[L - 3] ^ i[L - 8] ^ i[L - 14] ^ i[L - 16]) << 1) | (o >>> 31); - for (var B = 0; B < 80; ++B) { - var $ = ~~(B / 20), - V = (rotl5(u) + ft($, _, w, C) + j + i[B] + x[$]) | 0; - (j = C), (C = w), (w = rotl30(_)), (_ = u), (u = V); - } - (this._a = (u + this._a) | 0), - (this._b = (_ + this._b) | 0), - (this._c = (w + this._c) | 0), - (this._d = (C + this._d) | 0), - (this._e = (j + this._e) | 0); - }), - (Sha1.prototype._hash = function () { - var s = w.allocUnsafe(20); - return ( - s.writeInt32BE(0 | this._a, 0), - s.writeInt32BE(0 | this._b, 4), - s.writeInt32BE(0 | this._c, 8), - s.writeInt32BE(0 | this._d, 12), - s.writeInt32BE(0 | this._e, 16), - s - ); - }), - (s.exports = Sha1); - }, - 26710: (s, o, i) => { - var u = i(56698), - _ = i(24107), - w = i(90392), - x = i(92861).Buffer, - C = new Array(64); - function Sha224() { - this.init(), (this._w = C), w.call(this, 64, 56); - } - u(Sha224, _), - (Sha224.prototype.init = function () { - return ( - (this._a = 3238371032), - (this._b = 914150663), - (this._c = 812702999), - (this._d = 4144912697), - (this._e = 4290775857), - (this._f = 1750603025), - (this._g = 1694076839), - (this._h = 3204075428), - this - ); - }), - (Sha224.prototype._hash = function () { - var s = x.allocUnsafe(28); - return ( - s.writeInt32BE(this._a, 0), - s.writeInt32BE(this._b, 4), - s.writeInt32BE(this._c, 8), - s.writeInt32BE(this._d, 12), - s.writeInt32BE(this._e, 16), - s.writeInt32BE(this._f, 20), - s.writeInt32BE(this._g, 24), - s - ); - }), - (s.exports = Sha224); - }, - 24107: (s, o, i) => { - var u = i(56698), - _ = i(90392), - w = i(92861).Buffer, - x = [ - 1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, - 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, - 2614888103, 3248222580, 3835390401, 4022224774, 264347078, 604807628, 770255983, - 1249150122, 1555081692, 1996064986, 2554220882, 2821834349, 2952996808, 3210313671, - 3336571891, 3584528711, 113926993, 338241895, 666307205, 773529912, 1294757372, - 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, 2730485921, 2820302411, - 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, - 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, - 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, - 3329325298 - ], - C = new Array(64); - function Sha256() { - this.init(), (this._w = C), _.call(this, 64, 56); - } - function ch(s, o, i) { - return i ^ (s & (o ^ i)); - } - function maj(s, o, i) { - return (s & o) | (i & (s | o)); - } - function sigma0(s) { - return ((s >>> 2) | (s << 30)) ^ ((s >>> 13) | (s << 19)) ^ ((s >>> 22) | (s << 10)); - } - function sigma1(s) { - return ((s >>> 6) | (s << 26)) ^ ((s >>> 11) | (s << 21)) ^ ((s >>> 25) | (s << 7)); - } - function gamma0(s) { - return ((s >>> 7) | (s << 25)) ^ ((s >>> 18) | (s << 14)) ^ (s >>> 3); - } - u(Sha256, _), - (Sha256.prototype.init = function () { - return ( - (this._a = 1779033703), - (this._b = 3144134277), - (this._c = 1013904242), - (this._d = 2773480762), - (this._e = 1359893119), - (this._f = 2600822924), - (this._g = 528734635), - (this._h = 1541459225), - this - ); - }), - (Sha256.prototype._update = function (s) { - for ( - var o, - i = this._w, - u = 0 | this._a, - _ = 0 | this._b, - w = 0 | this._c, - C = 0 | this._d, - j = 0 | this._e, - L = 0 | this._f, - B = 0 | this._g, - $ = 0 | this._h, - V = 0; - V < 16; - ++V - ) - i[V] = s.readInt32BE(4 * V); - for (; V < 64; ++V) - i[V] = - 0 | - (((((o = i[V - 2]) >>> 17) | (o << 15)) ^ ((o >>> 19) | (o << 13)) ^ (o >>> 10)) + - i[V - 7] + - gamma0(i[V - 15]) + - i[V - 16]); - for (var U = 0; U < 64; ++U) { - var z = ($ + sigma1(j) + ch(j, L, B) + x[U] + i[U]) | 0, - Y = (sigma0(u) + maj(u, _, w)) | 0; - ($ = B), - (B = L), - (L = j), - (j = (C + z) | 0), - (C = w), - (w = _), - (_ = u), - (u = (z + Y) | 0); - } - (this._a = (u + this._a) | 0), - (this._b = (_ + this._b) | 0), - (this._c = (w + this._c) | 0), - (this._d = (C + this._d) | 0), - (this._e = (j + this._e) | 0), - (this._f = (L + this._f) | 0), - (this._g = (B + this._g) | 0), - (this._h = ($ + this._h) | 0); - }), - (Sha256.prototype._hash = function () { - var s = w.allocUnsafe(32); - return ( - s.writeInt32BE(this._a, 0), - s.writeInt32BE(this._b, 4), - s.writeInt32BE(this._c, 8), - s.writeInt32BE(this._d, 12), - s.writeInt32BE(this._e, 16), - s.writeInt32BE(this._f, 20), - s.writeInt32BE(this._g, 24), - s.writeInt32BE(this._h, 28), - s - ); - }), - (s.exports = Sha256); - }, - 32827: (s, o, i) => { - var u = i(56698), - _ = i(82890), - w = i(90392), - x = i(92861).Buffer, - C = new Array(160); - function Sha384() { - this.init(), (this._w = C), w.call(this, 128, 112); - } - u(Sha384, _), - (Sha384.prototype.init = function () { - return ( - (this._ah = 3418070365), - (this._bh = 1654270250), - (this._ch = 2438529370), - (this._dh = 355462360), - (this._eh = 1731405415), - (this._fh = 2394180231), - (this._gh = 3675008525), - (this._hh = 1203062813), - (this._al = 3238371032), - (this._bl = 914150663), - (this._cl = 812702999), - (this._dl = 4144912697), - (this._el = 4290775857), - (this._fl = 1750603025), - (this._gl = 1694076839), - (this._hl = 3204075428), - this - ); - }), - (Sha384.prototype._hash = function () { - var s = x.allocUnsafe(48); - function writeInt64BE(o, i, u) { - s.writeInt32BE(o, u), s.writeInt32BE(i, u + 4); - } - return ( - writeInt64BE(this._ah, this._al, 0), - writeInt64BE(this._bh, this._bl, 8), - writeInt64BE(this._ch, this._cl, 16), - writeInt64BE(this._dh, this._dl, 24), - writeInt64BE(this._eh, this._el, 32), - writeInt64BE(this._fh, this._fl, 40), - s - ); - }), - (s.exports = Sha384); - }, - 82890: (s, o, i) => { - var u = i(56698), - _ = i(90392), - w = i(92861).Buffer, - x = [ - 1116352408, 3609767458, 1899447441, 602891725, 3049323471, 3964484399, 3921009573, - 2173295548, 961987163, 4081628472, 1508970993, 3053834265, 2453635748, 2937671579, - 2870763221, 3664609560, 3624381080, 2734883394, 310598401, 1164996542, 607225278, - 1323610764, 1426881987, 3590304994, 1925078388, 4068182383, 2162078206, 991336113, - 2614888103, 633803317, 3248222580, 3479774868, 3835390401, 2666613458, 4022224774, - 944711139, 264347078, 2341262773, 604807628, 2007800933, 770255983, 1495990901, - 1249150122, 1856431235, 1555081692, 3175218132, 1996064986, 2198950837, 2554220882, - 3999719339, 2821834349, 766784016, 2952996808, 2566594879, 3210313671, 3203337956, - 3336571891, 1034457026, 3584528711, 2466948901, 113926993, 3758326383, 338241895, - 168717936, 666307205, 1188179964, 773529912, 1546045734, 1294757372, 1522805485, - 1396182291, 2643833823, 1695183700, 2343527390, 1986661051, 1014477480, 2177026350, - 1206759142, 2456956037, 344077627, 2730485921, 1290863460, 2820302411, 3158454273, - 3259730800, 3505952657, 3345764771, 106217008, 3516065817, 3606008344, 3600352804, - 1432725776, 4094571909, 1467031594, 275423344, 851169720, 430227734, 3100823752, - 506948616, 1363258195, 659060556, 3750685593, 883997877, 3785050280, 958139571, - 3318307427, 1322822218, 3812723403, 1537002063, 2003034995, 1747873779, 3602036899, - 1955562222, 1575990012, 2024104815, 1125592928, 2227730452, 2716904306, 2361852424, - 442776044, 2428436474, 593698344, 2756734187, 3733110249, 3204031479, 2999351573, - 3329325298, 3815920427, 3391569614, 3928383900, 3515267271, 566280711, 3940187606, - 3454069534, 4118630271, 4000239992, 116418474, 1914138554, 174292421, 2731055270, - 289380356, 3203993006, 460393269, 320620315, 685471733, 587496836, 852142971, - 1086792851, 1017036298, 365543100, 1126000580, 2618297676, 1288033470, 3409855158, - 1501505948, 4234509866, 1607167915, 987167468, 1816402316, 1246189591 - ], - C = new Array(160); - function Sha512() { - this.init(), (this._w = C), _.call(this, 128, 112); - } - function Ch(s, o, i) { - return i ^ (s & (o ^ i)); - } - function maj(s, o, i) { - return (s & o) | (i & (s | o)); - } - function sigma0(s, o) { - return ((s >>> 28) | (o << 4)) ^ ((o >>> 2) | (s << 30)) ^ ((o >>> 7) | (s << 25)); - } - function sigma1(s, o) { - return ((s >>> 14) | (o << 18)) ^ ((s >>> 18) | (o << 14)) ^ ((o >>> 9) | (s << 23)); - } - function Gamma0(s, o) { - return ((s >>> 1) | (o << 31)) ^ ((s >>> 8) | (o << 24)) ^ (s >>> 7); - } - function Gamma0l(s, o) { - return ((s >>> 1) | (o << 31)) ^ ((s >>> 8) | (o << 24)) ^ ((s >>> 7) | (o << 25)); - } - function Gamma1(s, o) { - return ((s >>> 19) | (o << 13)) ^ ((o >>> 29) | (s << 3)) ^ (s >>> 6); - } - function Gamma1l(s, o) { - return ((s >>> 19) | (o << 13)) ^ ((o >>> 29) | (s << 3)) ^ ((s >>> 6) | (o << 26)); - } - function getCarry(s, o) { - return s >>> 0 < o >>> 0 ? 1 : 0; - } - u(Sha512, _), - (Sha512.prototype.init = function () { - return ( - (this._ah = 1779033703), - (this._bh = 3144134277), - (this._ch = 1013904242), - (this._dh = 2773480762), - (this._eh = 1359893119), - (this._fh = 2600822924), - (this._gh = 528734635), - (this._hh = 1541459225), - (this._al = 4089235720), - (this._bl = 2227873595), - (this._cl = 4271175723), - (this._dl = 1595750129), - (this._el = 2917565137), - (this._fl = 725511199), - (this._gl = 4215389547), - (this._hl = 327033209), - this - ); - }), - (Sha512.prototype._update = function (s) { - for ( - var o = this._w, - i = 0 | this._ah, - u = 0 | this._bh, - _ = 0 | this._ch, - w = 0 | this._dh, - C = 0 | this._eh, - j = 0 | this._fh, - L = 0 | this._gh, - B = 0 | this._hh, - $ = 0 | this._al, - V = 0 | this._bl, - U = 0 | this._cl, - z = 0 | this._dl, - Y = 0 | this._el, - Z = 0 | this._fl, - ee = 0 | this._gl, - ie = 0 | this._hl, - ae = 0; - ae < 32; - ae += 2 - ) - (o[ae] = s.readInt32BE(4 * ae)), (o[ae + 1] = s.readInt32BE(4 * ae + 4)); - for (; ae < 160; ae += 2) { - var le = o[ae - 30], - ce = o[ae - 30 + 1], - pe = Gamma0(le, ce), - de = Gamma0l(ce, le), - fe = Gamma1((le = o[ae - 4]), (ce = o[ae - 4 + 1])), - ye = Gamma1l(ce, le), - be = o[ae - 14], - _e = o[ae - 14 + 1], - we = o[ae - 32], - Se = o[ae - 32 + 1], - xe = (de + _e) | 0, - Pe = (pe + be + getCarry(xe, de)) | 0; - (Pe = - ((Pe = (Pe + fe + getCarry((xe = (xe + ye) | 0), ye)) | 0) + - we + - getCarry((xe = (xe + Se) | 0), Se)) | - 0), - (o[ae] = Pe), - (o[ae + 1] = xe); - } - for (var Te = 0; Te < 160; Te += 2) { - (Pe = o[Te]), (xe = o[Te + 1]); - var Re = maj(i, u, _), - qe = maj($, V, U), - $e = sigma0(i, $), - ze = sigma0($, i), - We = sigma1(C, Y), - He = sigma1(Y, C), - Ye = x[Te], - Xe = x[Te + 1], - Qe = Ch(C, j, L), - et = Ch(Y, Z, ee), - tt = (ie + He) | 0, - rt = (B + We + getCarry(tt, ie)) | 0; - rt = - ((rt = - ((rt = (rt + Qe + getCarry((tt = (tt + et) | 0), et)) | 0) + - Ye + - getCarry((tt = (tt + Xe) | 0), Xe)) | - 0) + - Pe + - getCarry((tt = (tt + xe) | 0), xe)) | - 0; - var nt = (ze + qe) | 0, - st = ($e + Re + getCarry(nt, ze)) | 0; - (B = L), - (ie = ee), - (L = j), - (ee = Z), - (j = C), - (Z = Y), - (C = (w + rt + getCarry((Y = (z + tt) | 0), z)) | 0), - (w = _), - (z = U), - (_ = u), - (U = V), - (u = i), - (V = $), - (i = (rt + st + getCarry(($ = (tt + nt) | 0), tt)) | 0); - } - (this._al = (this._al + $) | 0), - (this._bl = (this._bl + V) | 0), - (this._cl = (this._cl + U) | 0), - (this._dl = (this._dl + z) | 0), - (this._el = (this._el + Y) | 0), - (this._fl = (this._fl + Z) | 0), - (this._gl = (this._gl + ee) | 0), - (this._hl = (this._hl + ie) | 0), - (this._ah = (this._ah + i + getCarry(this._al, $)) | 0), - (this._bh = (this._bh + u + getCarry(this._bl, V)) | 0), - (this._ch = (this._ch + _ + getCarry(this._cl, U)) | 0), - (this._dh = (this._dh + w + getCarry(this._dl, z)) | 0), - (this._eh = (this._eh + C + getCarry(this._el, Y)) | 0), - (this._fh = (this._fh + j + getCarry(this._fl, Z)) | 0), - (this._gh = (this._gh + L + getCarry(this._gl, ee)) | 0), - (this._hh = (this._hh + B + getCarry(this._hl, ie)) | 0); - }), - (Sha512.prototype._hash = function () { - var s = w.allocUnsafe(64); - function writeInt64BE(o, i, u) { - s.writeInt32BE(o, u), s.writeInt32BE(i, u + 4); - } - return ( - writeInt64BE(this._ah, this._al, 0), - writeInt64BE(this._bh, this._bl, 8), - writeInt64BE(this._ch, this._cl, 16), - writeInt64BE(this._dh, this._dl, 24), - writeInt64BE(this._eh, this._el, 32), - writeInt64BE(this._fh, this._fl, 40), - writeInt64BE(this._gh, this._gl, 48), - writeInt64BE(this._hh, this._hl, 56), - s - ); - }), - (s.exports = Sha512); - }, - 8068: (s) => { - 'use strict'; - var o = (() => { - var s = Object.defineProperty, - o = Object.getOwnPropertyDescriptor, - i = Object.getOwnPropertyNames, - u = Object.getOwnPropertySymbols, - _ = Object.prototype.hasOwnProperty, - w = Object.prototype.propertyIsEnumerable, - __defNormalProp = (o, i, u) => - i in o - ? s(o, i, { enumerable: !0, configurable: !0, writable: !0, value: u }) - : (o[i] = u), - __spreadValues = (s, o) => { - for (var i in o || (o = {})) _.call(o, i) && __defNormalProp(s, i, o[i]); - if (u) for (var i of u(o)) w.call(o, i) && __defNormalProp(s, i, o[i]); - return s; - }, - __publicField = (s, o, i) => ( - __defNormalProp(s, 'symbol' != typeof o ? o + '' : o, i), i - ), - x = {}; - ((o, i) => { - for (var u in i) s(o, u, { get: i[u], enumerable: !0 }); - })(x, { DEFAULT_OPTIONS: () => j, DEFAULT_UUID_LENGTH: () => C, default: () => $ }); - var C = 6, - j = { dictionary: 'alphanum', shuffle: !0, debug: !1, length: C, counter: 0 }, - L = class _ShortUniqueId { - constructor(s = {}) { - __publicField(this, 'counter'), - __publicField(this, 'debug'), - __publicField(this, 'dict'), - __publicField(this, 'version'), - __publicField(this, 'dictIndex', 0), - __publicField(this, 'dictRange', []), - __publicField(this, 'lowerBound', 0), - __publicField(this, 'upperBound', 0), - __publicField(this, 'dictLength', 0), - __publicField(this, 'uuidLength'), - __publicField(this, '_digit_first_ascii', 48), - __publicField(this, '_digit_last_ascii', 58), - __publicField(this, '_alpha_lower_first_ascii', 97), - __publicField(this, '_alpha_lower_last_ascii', 123), - __publicField(this, '_hex_last_ascii', 103), - __publicField(this, '_alpha_upper_first_ascii', 65), - __publicField(this, '_alpha_upper_last_ascii', 91), - __publicField(this, '_number_dict_ranges', { - digits: [this._digit_first_ascii, this._digit_last_ascii] - }), - __publicField(this, '_alpha_dict_ranges', { - lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii], - upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] - }), - __publicField(this, '_alpha_lower_dict_ranges', { - lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii] - }), - __publicField(this, '_alpha_upper_dict_ranges', { - upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] - }), - __publicField(this, '_alphanum_dict_ranges', { - digits: [this._digit_first_ascii, this._digit_last_ascii], - lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii], - upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] - }), - __publicField(this, '_alphanum_lower_dict_ranges', { - digits: [this._digit_first_ascii, this._digit_last_ascii], - lowerCase: [this._alpha_lower_first_ascii, this._alpha_lower_last_ascii] - }), - __publicField(this, '_alphanum_upper_dict_ranges', { - digits: [this._digit_first_ascii, this._digit_last_ascii], - upperCase: [this._alpha_upper_first_ascii, this._alpha_upper_last_ascii] - }), - __publicField(this, '_hex_dict_ranges', { - decDigits: [this._digit_first_ascii, this._digit_last_ascii], - alphaDigits: [this._alpha_lower_first_ascii, this._hex_last_ascii] - }), - __publicField(this, '_dict_ranges', { - _number_dict_ranges: this._number_dict_ranges, - _alpha_dict_ranges: this._alpha_dict_ranges, - _alpha_lower_dict_ranges: this._alpha_lower_dict_ranges, - _alpha_upper_dict_ranges: this._alpha_upper_dict_ranges, - _alphanum_dict_ranges: this._alphanum_dict_ranges, - _alphanum_lower_dict_ranges: this._alphanum_lower_dict_ranges, - _alphanum_upper_dict_ranges: this._alphanum_upper_dict_ranges, - _hex_dict_ranges: this._hex_dict_ranges - }), - __publicField(this, 'log', (...s) => { - const o = [...s]; - if ( - ((o[0] = `[short-unique-id] ${s[0]}`), - !0 === this.debug && 'undefined' != typeof console && null !== console) - ) - return console.log(...o); - }), - __publicField(this, '_normalizeDictionary', (s, o) => { - let i; - if (s && Array.isArray(s) && s.length > 1) i = s; - else { - let o; - (i = []), (this.dictIndex = o = 0); - const u = `_${s}_dict_ranges`, - _ = this._dict_ranges[u]; - Object.keys(_).forEach((s) => { - const u = s; - for ( - this.dictRange = _[u], - this.lowerBound = this.dictRange[0], - this.upperBound = this.dictRange[1], - this.dictIndex = o = this.lowerBound; - this.lowerBound <= this.upperBound - ? o < this.upperBound - : o > this.upperBound; - this.dictIndex = - this.lowerBound <= this.upperBound ? (o += 1) : (o -= 1) - ) - i.push(String.fromCharCode(this.dictIndex)); - }); - } - if (o) { - const s = 0.5; - i = i.sort(() => Math.random() - s); - } - return i; - }), - __publicField(this, 'setDictionary', (s, o) => { - (this.dict = this._normalizeDictionary(s, o)), - (this.dictLength = this.dict.length), - this.setCounter(0); - }), - __publicField(this, 'seq', () => this.sequentialUUID()), - __publicField(this, 'sequentialUUID', () => { - let s, - o, - i = ''; - s = this.counter; - do { - (o = s % this.dictLength), - (s = Math.trunc(s / this.dictLength)), - (i += this.dict[o]); - } while (0 !== s); - return (this.counter += 1), i; - }), - __publicField(this, 'rnd', (s = this.uuidLength || C) => this.randomUUID(s)), - __publicField(this, 'randomUUID', (s = this.uuidLength || C) => { - let o, i, u; - if (null == s || s < 1) throw new Error('Invalid UUID Length Provided'); - for (o = '', u = 0; u < s; u += 1) - (i = - parseInt((Math.random() * this.dictLength).toFixed(0), 10) % - this.dictLength), - (o += this.dict[i]); - return o; - }), - __publicField(this, 'fmt', (s, o) => this.formattedUUID(s, o)), - __publicField(this, 'formattedUUID', (s, o) => { - const i = { $r: this.randomUUID, $s: this.sequentialUUID, $t: this.stamp }; - return s.replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g, (s) => { - const u = s.slice(0, 2), - _ = parseInt(s.slice(2), 10); - return '$s' === u - ? i[u]().padStart(_, '0') - : '$t' === u && o - ? i[u](_, o) - : i[u](_); - }); - }), - __publicField(this, 'availableUUIDs', (s = this.uuidLength) => - parseFloat(Math.pow([...new Set(this.dict)].length, s).toFixed(0)) - ), - __publicField( - this, - 'approxMaxBeforeCollision', - (s = this.availableUUIDs(this.uuidLength)) => - parseFloat(Math.sqrt((Math.PI / 2) * s).toFixed(20)) - ), - __publicField( - this, - 'collisionProbability', - (s = this.availableUUIDs(this.uuidLength), o = this.uuidLength) => - parseFloat( - (this.approxMaxBeforeCollision(s) / this.availableUUIDs(o)).toFixed(20) - ) - ), - __publicField( - this, - 'uniqueness', - (s = this.availableUUIDs(this.uuidLength)) => { - const o = parseFloat( - (1 - this.approxMaxBeforeCollision(s) / s).toFixed(20) - ); - return o > 1 ? 1 : o < 0 ? 0 : o; - } - ), - __publicField(this, 'getVersion', () => this.version), - __publicField(this, 'stamp', (s, o) => { - const i = Math.floor(+(o || new Date()) / 1e3).toString(16); - if ('number' == typeof s && 0 === s) return i; - if ('number' != typeof s || s < 10) - throw new Error( - [ - 'Param finalLength must be a number greater than or equal to 10,', - 'or 0 if you want the raw hexadecimal timestamp' - ].join('\n') - ); - const u = s - 9, - _ = Math.round(Math.random() * (u > 15 ? 15 : u)), - w = this.randomUUID(u); - return `${w.substring(0, _)}${i}${w.substring(_)}${_.toString(16)}`; - }), - __publicField(this, 'parseStamp', (s, o) => { - if (o && !/t0|t[1-9]\d{1,}/.test(o)) - throw new Error( - 'Cannot extract date from a formated UUID with no timestamp in the format' - ); - const i = o - ? o - .replace(/\$[rs]\d{0,}|\$t0|\$t[1-9]\d{1,}/g, (s) => { - const o = { - $r: (s) => [...Array(s)].map(() => 'r').join(''), - $s: (s) => [...Array(s)].map(() => 's').join(''), - $t: (s) => [...Array(s)].map(() => 't').join('') - }, - i = s.slice(0, 2), - u = parseInt(s.slice(2), 10); - return o[i](u); - }) - .replace(/^(.*?)(t{8,})(.*)$/g, (o, i, u) => - s.substring(i.length, i.length + u.length) - ) - : s; - if (8 === i.length) return new Date(1e3 * parseInt(i, 16)); - if (i.length < 10) throw new Error('Stamp length invalid'); - const u = parseInt(i.substring(i.length - 1), 16); - return new Date(1e3 * parseInt(i.substring(u, u + 8), 16)); - }), - __publicField(this, 'setCounter', (s) => { - this.counter = s; - }), - __publicField(this, 'validate', (s, o) => { - const i = o ? this._normalizeDictionary(o) : this.dict; - return s.split('').every((s) => i.includes(s)); - }); - const o = __spreadValues(__spreadValues({}, j), s); - (this.counter = 0), (this.debug = !1), (this.dict = []), (this.version = '5.2.0'); - const { dictionary: i, shuffle: u, length: _, counter: w } = o; - return ( - (this.uuidLength = _), - this.setDictionary(i, u), - this.setCounter(w), - (this.debug = o.debug), - this.log(this.dict), - this.log( - `Generator instantiated with Dictionary Size ${this.dictLength} and counter set to ${this.counter}` - ), - (this.log = this.log.bind(this)), - (this.setDictionary = this.setDictionary.bind(this)), - (this.setCounter = this.setCounter.bind(this)), - (this.seq = this.seq.bind(this)), - (this.sequentialUUID = this.sequentialUUID.bind(this)), - (this.rnd = this.rnd.bind(this)), - (this.randomUUID = this.randomUUID.bind(this)), - (this.fmt = this.fmt.bind(this)), - (this.formattedUUID = this.formattedUUID.bind(this)), - (this.availableUUIDs = this.availableUUIDs.bind(this)), - (this.approxMaxBeforeCollision = this.approxMaxBeforeCollision.bind(this)), - (this.collisionProbability = this.collisionProbability.bind(this)), - (this.uniqueness = this.uniqueness.bind(this)), - (this.getVersion = this.getVersion.bind(this)), - (this.stamp = this.stamp.bind(this)), - (this.parseStamp = this.parseStamp.bind(this)), - this - ); - } - }; - __publicField(L, 'default', L); - var B, - $ = L; - return ( - (B = x), - ((u, w, x, C) => { - if ((w && 'object' == typeof w) || 'function' == typeof w) - for (let j of i(w)) - _.call(u, j) || - j === x || - s(u, j, { get: () => w[j], enumerable: !(C = o(w, j)) || C.enumerable }); - return u; - })(s({}, '__esModule', { value: !0 }), B) - ); - })(); - (s.exports = o.default), 'undefined' != typeof window && (o = o.default); - }, - 88310: (s, o, i) => { - s.exports = Stream; - var u = i(37007).EventEmitter; - function Stream() { - u.call(this); - } - i(56698)(Stream, u), - (Stream.Readable = i(45412)), - (Stream.Writable = i(16708)), - (Stream.Duplex = i(25382)), - (Stream.Transform = i(74610)), - (Stream.PassThrough = i(63600)), - (Stream.finished = i(86238)), - (Stream.pipeline = i(57758)), - (Stream.Stream = Stream), - (Stream.prototype.pipe = function (s, o) { - var i = this; - function ondata(o) { - s.writable && !1 === s.write(o) && i.pause && i.pause(); - } - function ondrain() { - i.readable && i.resume && i.resume(); - } - i.on('data', ondata), - s.on('drain', ondrain), - s._isStdio || (o && !1 === o.end) || (i.on('end', onend), i.on('close', onclose)); - var _ = !1; - function onend() { - _ || ((_ = !0), s.end()); - } - function onclose() { - _ || ((_ = !0), 'function' == typeof s.destroy && s.destroy()); - } - function onerror(s) { - if ((cleanup(), 0 === u.listenerCount(this, 'error'))) throw s; - } - function cleanup() { - i.removeListener('data', ondata), - s.removeListener('drain', ondrain), - i.removeListener('end', onend), - i.removeListener('close', onclose), - i.removeListener('error', onerror), - s.removeListener('error', onerror), - i.removeListener('end', cleanup), - i.removeListener('close', cleanup), - s.removeListener('close', cleanup); - } - return ( - i.on('error', onerror), - s.on('error', onerror), - i.on('end', cleanup), - i.on('close', cleanup), - s.on('close', cleanup), - s.emit('pipe', i), - s - ); - }); - }, - 83141: (s, o, i) => { - 'use strict'; - var u = i(92861).Buffer, - _ = - u.isEncoding || - function (s) { - switch ((s = '' + s) && s.toLowerCase()) { - case 'hex': - case 'utf8': - case 'utf-8': - case 'ascii': - case 'binary': - case 'base64': - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - case 'raw': - return !0; - default: - return !1; - } - }; - function StringDecoder(s) { - var o; - switch ( - ((this.encoding = (function normalizeEncoding(s) { - var o = (function _normalizeEncoding(s) { - if (!s) return 'utf8'; - for (var o; ; ) - switch (s) { - case 'utf8': - case 'utf-8': - return 'utf8'; - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return 'utf16le'; - case 'latin1': - case 'binary': - return 'latin1'; - case 'base64': - case 'ascii': - case 'hex': - return s; - default: - if (o) return; - (s = ('' + s).toLowerCase()), (o = !0); - } - })(s); - if ('string' != typeof o && (u.isEncoding === _ || !_(s))) - throw new Error('Unknown encoding: ' + s); - return o || s; - })(s)), - this.encoding) - ) { - case 'utf16le': - (this.text = utf16Text), (this.end = utf16End), (o = 4); - break; - case 'utf8': - (this.fillLast = utf8FillLast), (o = 4); - break; - case 'base64': - (this.text = base64Text), (this.end = base64End), (o = 3); - break; - default: - return (this.write = simpleWrite), void (this.end = simpleEnd); - } - (this.lastNeed = 0), (this.lastTotal = 0), (this.lastChar = u.allocUnsafe(o)); - } - function utf8CheckByte(s) { - return s <= 127 - ? 0 - : s >> 5 == 6 - ? 2 - : s >> 4 == 14 - ? 3 - : s >> 3 == 30 - ? 4 - : s >> 6 == 2 - ? -1 - : -2; - } - function utf8FillLast(s) { - var o = this.lastTotal - this.lastNeed, - i = (function utf8CheckExtraBytes(s, o, i) { - if (128 != (192 & o[0])) return (s.lastNeed = 0), '�'; - if (s.lastNeed > 1 && o.length > 1) { - if (128 != (192 & o[1])) return (s.lastNeed = 1), '�'; - if (s.lastNeed > 2 && o.length > 2 && 128 != (192 & o[2])) - return (s.lastNeed = 2), '�'; - } - })(this, s); - return void 0 !== i - ? i - : this.lastNeed <= s.length - ? (s.copy(this.lastChar, o, 0, this.lastNeed), - this.lastChar.toString(this.encoding, 0, this.lastTotal)) - : (s.copy(this.lastChar, o, 0, s.length), void (this.lastNeed -= s.length)); - } - function utf16Text(s, o) { - if ((s.length - o) % 2 == 0) { - var i = s.toString('utf16le', o); - if (i) { - var u = i.charCodeAt(i.length - 1); - if (u >= 55296 && u <= 56319) - return ( - (this.lastNeed = 2), - (this.lastTotal = 4), - (this.lastChar[0] = s[s.length - 2]), - (this.lastChar[1] = s[s.length - 1]), - i.slice(0, -1) - ); - } - return i; - } - return ( - (this.lastNeed = 1), - (this.lastTotal = 2), - (this.lastChar[0] = s[s.length - 1]), - s.toString('utf16le', o, s.length - 1) - ); - } - function utf16End(s) { - var o = s && s.length ? this.write(s) : ''; - if (this.lastNeed) { - var i = this.lastTotal - this.lastNeed; - return o + this.lastChar.toString('utf16le', 0, i); - } - return o; - } - function base64Text(s, o) { - var i = (s.length - o) % 3; - return 0 === i - ? s.toString('base64', o) - : ((this.lastNeed = 3 - i), - (this.lastTotal = 3), - 1 === i - ? (this.lastChar[0] = s[s.length - 1]) - : ((this.lastChar[0] = s[s.length - 2]), (this.lastChar[1] = s[s.length - 1])), - s.toString('base64', o, s.length - i)); - } - function base64End(s) { - var o = s && s.length ? this.write(s) : ''; - return this.lastNeed ? o + this.lastChar.toString('base64', 0, 3 - this.lastNeed) : o; - } - function simpleWrite(s) { - return s.toString(this.encoding); - } - function simpleEnd(s) { - return s && s.length ? this.write(s) : ''; - } - (o.I = StringDecoder), - (StringDecoder.prototype.write = function (s) { - if (0 === s.length) return ''; - var o, i; - if (this.lastNeed) { - if (void 0 === (o = this.fillLast(s))) return ''; - (i = this.lastNeed), (this.lastNeed = 0); - } else i = 0; - return i < s.length ? (o ? o + this.text(s, i) : this.text(s, i)) : o || ''; - }), - (StringDecoder.prototype.end = function utf8End(s) { - var o = s && s.length ? this.write(s) : ''; - return this.lastNeed ? o + '�' : o; - }), - (StringDecoder.prototype.text = function utf8Text(s, o) { - var i = (function utf8CheckIncomplete(s, o, i) { - var u = o.length - 1; - if (u < i) return 0; - var _ = utf8CheckByte(o[u]); - if (_ >= 0) return _ > 0 && (s.lastNeed = _ - 1), _; - if (--u < i || -2 === _) return 0; - if (((_ = utf8CheckByte(o[u])), _ >= 0)) return _ > 0 && (s.lastNeed = _ - 2), _; - if (--u < i || -2 === _) return 0; - if (((_ = utf8CheckByte(o[u])), _ >= 0)) - return _ > 0 && (2 === _ ? (_ = 0) : (s.lastNeed = _ - 3)), _; - return 0; - })(this, s, o); - if (!this.lastNeed) return s.toString('utf8', o); - this.lastTotal = i; - var u = s.length - (i - this.lastNeed); - return s.copy(this.lastChar, 0, u), s.toString('utf8', o, u); - }), - (StringDecoder.prototype.fillLast = function (s) { - if (this.lastNeed <= s.length) - return ( - s.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed), - this.lastChar.toString(this.encoding, 0, this.lastTotal) - ); - s.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, s.length), - (this.lastNeed -= s.length); - }); - }, - 69883: (s, o) => { - 'use strict'; - (o.parse = function parse(s, o) { - if ('string' != typeof s) throw new TypeError('argument str must be a string'); - var i = {}, - _ = s.length; - if (_ < 2) return i; - var w = (o && o.decode) || decode, - x = 0, - C = 0, - j = 0; - do { - if (-1 === (C = s.indexOf('=', x))) break; - if (-1 === (j = s.indexOf(';', x))) j = _; - else if (C > j) { - x = s.lastIndexOf(';', C - 1) + 1; - continue; - } - var L = startIndex(s, x, C), - B = endIndex(s, C, L), - $ = s.slice(L, B); - if (!u.call(i, $)) { - var V = startIndex(s, C + 1, j), - U = endIndex(s, j, V); - 34 === s.charCodeAt(V) && 34 === s.charCodeAt(U - 1) && (V++, U--); - var z = s.slice(V, U); - i[$] = tryDecode(z, w); - } - x = j + 1; - } while (x < _); - return i; - }), - (o.serialize = function serialize(s, o, u) { - var j = (u && u.encode) || encodeURIComponent; - if ('function' != typeof j) throw new TypeError('option encode is invalid'); - if (!_.test(s)) throw new TypeError('argument name is invalid'); - var L = j(o); - if (!w.test(L)) throw new TypeError('argument val is invalid'); - var B = s + '=' + L; - if (!u) return B; - if (null != u.maxAge) { - var $ = Math.floor(u.maxAge); - if (!isFinite($)) throw new TypeError('option maxAge is invalid'); - B += '; Max-Age=' + $; - } - if (u.domain) { - if (!x.test(u.domain)) throw new TypeError('option domain is invalid'); - B += '; Domain=' + u.domain; - } - if (u.path) { - if (!C.test(u.path)) throw new TypeError('option path is invalid'); - B += '; Path=' + u.path; - } - if (u.expires) { - var V = u.expires; - if ( - !(function isDate(s) { - return '[object Date]' === i.call(s); - })(V) || - isNaN(V.valueOf()) - ) - throw new TypeError('option expires is invalid'); - B += '; Expires=' + V.toUTCString(); - } - u.httpOnly && (B += '; HttpOnly'); - u.secure && (B += '; Secure'); - u.partitioned && (B += '; Partitioned'); - if (u.priority) { - switch ('string' == typeof u.priority ? u.priority.toLowerCase() : u.priority) { - case 'low': - B += '; Priority=Low'; - break; - case 'medium': - B += '; Priority=Medium'; - break; - case 'high': - B += '; Priority=High'; - break; - default: - throw new TypeError('option priority is invalid'); - } - } - if (u.sameSite) { - switch ('string' == typeof u.sameSite ? u.sameSite.toLowerCase() : u.sameSite) { - case !0: - B += '; SameSite=Strict'; - break; - case 'lax': - B += '; SameSite=Lax'; - break; - case 'strict': - B += '; SameSite=Strict'; - break; - case 'none': - B += '; SameSite=None'; - break; - default: - throw new TypeError('option sameSite is invalid'); - } - } - return B; - }); - var i = Object.prototype.toString, - u = Object.prototype.hasOwnProperty, - _ = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/, - w = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/, - x = - /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i, - C = /^[\u0020-\u003A\u003D-\u007E]*$/; - function startIndex(s, o, i) { - do { - var u = s.charCodeAt(o); - if (32 !== u && 9 !== u) return o; - } while (++o < i); - return i; - } - function endIndex(s, o, i) { - for (; o > i; ) { - var u = s.charCodeAt(--o); - if (32 !== u && 9 !== u) return o + 1; - } - return i; - } - function decode(s) { - return -1 !== s.indexOf('%') ? decodeURIComponent(s) : s; - } - function tryDecode(s, o) { - try { - return o(s); - } catch (o) { - return s; - } - } - }, - 16426: (s) => { - s.exports = function () { - var s = document.getSelection(); - if (!s.rangeCount) return function () {}; - for (var o = document.activeElement, i = [], u = 0; u < s.rangeCount; u++) - i.push(s.getRangeAt(u)); - switch (o.tagName.toUpperCase()) { - case 'INPUT': - case 'TEXTAREA': - o.blur(); - break; - default: - o = null; - } - return ( - s.removeAllRanges(), - function () { - 'Caret' === s.type && s.removeAllRanges(), - s.rangeCount || - i.forEach(function (o) { - s.addRange(o); - }), - o && o.focus(); - } - ); - }; - }, - 61160: (s, o, i) => { - 'use strict'; - var u = i(92063), - _ = i(73992), - w = /^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/, - x = /[\n\r\t]/g, - C = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//, - j = /:\d+$/, - L = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i, - B = /^[a-zA-Z]:/; - function trimLeft(s) { - return (s || '').toString().replace(w, ''); - } - var $ = [ - ['#', 'hash'], - ['?', 'query'], - function sanitize(s, o) { - return isSpecial(o.protocol) ? s.replace(/\\/g, '/') : s; - }, - ['/', 'pathname'], - ['@', 'auth', 1], - [NaN, 'host', void 0, 1, 1], - [/:(\d*)$/, 'port', void 0, 1], - [NaN, 'hostname', void 0, 1, 1] - ], - V = { hash: 1, query: 1 }; - function lolcation(s) { - var o, - u = - ('undefined' != typeof window - ? window - : void 0 !== i.g - ? i.g - : 'undefined' != typeof self - ? self - : {} - ).location || {}, - _ = {}, - w = typeof (s = s || u); - if ('blob:' === s.protocol) _ = new Url(unescape(s.pathname), {}); - else if ('string' === w) for (o in ((_ = new Url(s, {})), V)) delete _[o]; - else if ('object' === w) { - for (o in s) o in V || (_[o] = s[o]); - void 0 === _.slashes && (_.slashes = C.test(s.href)); - } - return _; - } - function isSpecial(s) { - return ( - 'file:' === s || - 'ftp:' === s || - 'http:' === s || - 'https:' === s || - 'ws:' === s || - 'wss:' === s - ); - } - function extractProtocol(s, o) { - (s = (s = trimLeft(s)).replace(x, '')), (o = o || {}); - var i, - u = L.exec(s), - _ = u[1] ? u[1].toLowerCase() : '', - w = !!u[2], - C = !!u[3], - j = 0; - return ( - w - ? C - ? ((i = u[2] + u[3] + u[4]), (j = u[2].length + u[3].length)) - : ((i = u[2] + u[4]), (j = u[2].length)) - : C - ? ((i = u[3] + u[4]), (j = u[3].length)) - : (i = u[4]), - 'file:' === _ - ? j >= 2 && (i = i.slice(2)) - : isSpecial(_) - ? (i = u[4]) - : _ - ? w && (i = i.slice(2)) - : j >= 2 && isSpecial(o.protocol) && (i = u[4]), - { protocol: _, slashes: w || isSpecial(_), slashesCount: j, rest: i } - ); - } - function Url(s, o, i) { - if (((s = (s = trimLeft(s)).replace(x, '')), !(this instanceof Url))) - return new Url(s, o, i); - var w, - C, - j, - L, - V, - U, - z = $.slice(), - Y = typeof o, - Z = this, - ee = 0; - for ( - 'object' !== Y && 'string' !== Y && ((i = o), (o = null)), - i && 'function' != typeof i && (i = _.parse), - w = !(C = extractProtocol(s || '', (o = lolcation(o)))).protocol && !C.slashes, - Z.slashes = C.slashes || (w && o.slashes), - Z.protocol = C.protocol || o.protocol || '', - s = C.rest, - (('file:' === C.protocol && (2 !== C.slashesCount || B.test(s))) || - (!C.slashes && (C.protocol || C.slashesCount < 2 || !isSpecial(Z.protocol)))) && - (z[3] = [/(.*)/, 'pathname']); - ee < z.length; - ee++ - ) - 'function' != typeof (L = z[ee]) - ? ((j = L[0]), - (U = L[1]), - j != j - ? (Z[U] = s) - : 'string' == typeof j - ? ~(V = '@' === j ? s.lastIndexOf(j) : s.indexOf(j)) && - ('number' == typeof L[2] - ? ((Z[U] = s.slice(0, V)), (s = s.slice(V + L[2]))) - : ((Z[U] = s.slice(V)), (s = s.slice(0, V)))) - : (V = j.exec(s)) && ((Z[U] = V[1]), (s = s.slice(0, V.index))), - (Z[U] = Z[U] || (w && L[3] && o[U]) || ''), - L[4] && (Z[U] = Z[U].toLowerCase())) - : (s = L(s, Z)); - i && (Z.query = i(Z.query)), - w && - o.slashes && - '/' !== Z.pathname.charAt(0) && - ('' !== Z.pathname || '' !== o.pathname) && - (Z.pathname = (function resolve(s, o) { - if ('' === s) return o; - for ( - var i = (o || '/').split('/').slice(0, -1).concat(s.split('/')), - u = i.length, - _ = i[u - 1], - w = !1, - x = 0; - u--; - - ) - '.' === i[u] - ? i.splice(u, 1) - : '..' === i[u] - ? (i.splice(u, 1), x++) - : x && (0 === u && (w = !0), i.splice(u, 1), x--); - return w && i.unshift(''), ('.' !== _ && '..' !== _) || i.push(''), i.join('/'); - })(Z.pathname, o.pathname)), - '/' !== Z.pathname.charAt(0) && - isSpecial(Z.protocol) && - (Z.pathname = '/' + Z.pathname), - u(Z.port, Z.protocol) || ((Z.host = Z.hostname), (Z.port = '')), - (Z.username = Z.password = ''), - Z.auth && - (~(V = Z.auth.indexOf(':')) - ? ((Z.username = Z.auth.slice(0, V)), - (Z.username = encodeURIComponent(decodeURIComponent(Z.username))), - (Z.password = Z.auth.slice(V + 1)), - (Z.password = encodeURIComponent(decodeURIComponent(Z.password)))) - : (Z.username = encodeURIComponent(decodeURIComponent(Z.auth))), - (Z.auth = Z.password ? Z.username + ':' + Z.password : Z.username)), - (Z.origin = - 'file:' !== Z.protocol && isSpecial(Z.protocol) && Z.host - ? Z.protocol + '//' + Z.host - : 'null'), - (Z.href = Z.toString()); - } - (Url.prototype = { - set: function set(s, o, i) { - var w = this; - switch (s) { - case 'query': - 'string' == typeof o && o.length && (o = (i || _.parse)(o)), (w[s] = o); - break; - case 'port': - (w[s] = o), - u(o, w.protocol) - ? o && (w.host = w.hostname + ':' + o) - : ((w.host = w.hostname), (w[s] = '')); - break; - case 'hostname': - (w[s] = o), w.port && (o += ':' + w.port), (w.host = o); - break; - case 'host': - (w[s] = o), - j.test(o) - ? ((o = o.split(':')), (w.port = o.pop()), (w.hostname = o.join(':'))) - : ((w.hostname = o), (w.port = '')); - break; - case 'protocol': - (w.protocol = o.toLowerCase()), (w.slashes = !i); - break; - case 'pathname': - case 'hash': - if (o) { - var x = 'pathname' === s ? '/' : '#'; - w[s] = o.charAt(0) !== x ? x + o : o; - } else w[s] = o; - break; - case 'username': - case 'password': - w[s] = encodeURIComponent(o); - break; - case 'auth': - var C = o.indexOf(':'); - ~C - ? ((w.username = o.slice(0, C)), - (w.username = encodeURIComponent(decodeURIComponent(w.username))), - (w.password = o.slice(C + 1)), - (w.password = encodeURIComponent(decodeURIComponent(w.password)))) - : (w.username = encodeURIComponent(decodeURIComponent(o))); - } - for (var L = 0; L < $.length; L++) { - var B = $[L]; - B[4] && (w[B[1]] = w[B[1]].toLowerCase()); - } - return ( - (w.auth = w.password ? w.username + ':' + w.password : w.username), - (w.origin = - 'file:' !== w.protocol && isSpecial(w.protocol) && w.host - ? w.protocol + '//' + w.host - : 'null'), - (w.href = w.toString()), - w - ); - }, - toString: function toString(s) { - (s && 'function' == typeof s) || (s = _.stringify); - var o, - i = this, - u = i.host, - w = i.protocol; - w && ':' !== w.charAt(w.length - 1) && (w += ':'); - var x = w + ((i.protocol && i.slashes) || isSpecial(i.protocol) ? '//' : ''); - return ( - i.username - ? ((x += i.username), i.password && (x += ':' + i.password), (x += '@')) - : i.password - ? ((x += ':' + i.password), (x += '@')) - : 'file:' !== i.protocol && - isSpecial(i.protocol) && - !u && - '/' !== i.pathname && - (x += '@'), - (':' === u[u.length - 1] || (j.test(i.hostname) && !i.port)) && (u += ':'), - (x += u + i.pathname), - (o = 'object' == typeof i.query ? s(i.query) : i.query) && - (x += '?' !== o.charAt(0) ? '?' + o : o), - i.hash && (x += i.hash), - x - ); - } - }), - (Url.extractProtocol = extractProtocol), - (Url.location = lolcation), - (Url.trimLeft = trimLeft), - (Url.qs = _), - (s.exports = Url); - }, - 77154: (s, o, i) => { - 'use strict'; - var u = i(96540); - var _ = - 'function' == typeof Object.is - ? Object.is - : function n(s, o) { - return (s === o && (0 !== s || 1 / s == 1 / o)) || (s != s && o != o); - }, - w = u.useSyncExternalStore, - x = u.useRef, - C = u.useEffect, - j = u.useMemo, - L = u.useDebugValue; - o.useSyncExternalStoreWithSelector = function (s, o, i, u, B) { - var $ = x(null); - if (null === $.current) { - var V = { hasValue: !1, value: null }; - $.current = V; - } else V = $.current; - $ = j( - function () { - function a(o) { - if (!x) { - if (((x = !0), (s = o), (o = u(o)), void 0 !== B && V.hasValue)) { - var i = V.value; - if (B(i, o)) return (w = i); - } - return (w = o); - } - if (((i = w), _(s, o))) return i; - var C = u(o); - return void 0 !== B && B(i, C) ? i : ((s = o), (w = C)); - } - var s, - w, - x = !1, - C = void 0 === i ? null : i; - return [ - function () { - return a(o()); - }, - null === C - ? void 0 - : function () { - return a(C()); - } - ]; - }, - [o, i, u, B] - ); - var U = w(s, $[0], $[1]); - return ( - C( - function () { - (V.hasValue = !0), (V.value = U); - }, - [U] - ), - L(U), - U - ); - }; - }, - 78418: (s, o, i) => { - 'use strict'; - s.exports = i(77154); - }, - 94643: (s, o, i) => { - function config(s) { - try { - if (!i.g.localStorage) return !1; - } catch (s) { - return !1; - } - var o = i.g.localStorage[s]; - return null != o && 'true' === String(o).toLowerCase(); - } - s.exports = function deprecate(s, o) { - if (config('noDeprecation')) return s; - var i = !1; - return function deprecated() { - if (!i) { - if (config('throwDeprecation')) throw new Error(o); - config('traceDeprecation') ? console.trace(o) : console.warn(o), (i = !0); - } - return s.apply(this, arguments); - }; - }; - }, - 26657: (s, o, i) => { - 'use strict'; - var u = i(75208), - _ = function isClosingTag(s) { - return /<\/+[^>]+>/.test(s); - }, - w = function isSelfClosingTag(s) { - return /<[^>]+\/>/.test(s); - }; - function getType(s) { - return _(s) - ? 'ClosingTag' - : (function isOpeningTag(s) { - return ( - (function isTag(s) { - return /<[^>!]+>/.test(s); - })(s) && - !_(s) && - !w(s) - ); - })(s) - ? 'OpeningTag' - : w(s) - ? 'SelfClosingTag' - : 'Text'; - } - s.exports = function (s) { - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, - i = o.indentor, - _ = o.textNodesOnSameLine, - w = 0, - x = []; - i = i || ' '; - var C = (function lexer(s) { - return (function splitOnTags(s) { - return s.split(/(<\/?[^>]+>)/g).filter(function (s) { - return '' !== s.trim(); - }); - })(s).map(function (s) { - return { value: s, type: getType(s) }; - }); - })(s).map(function (s, o, C) { - var j = s.value, - L = s.type; - 'ClosingTag' === L && w--; - var B = u(i, w), - $ = B + j; - if (('OpeningTag' === L && w++, _)) { - var V = C[o - 1], - U = C[o - 2]; - 'ClosingTag' === L && - 'Text' === V.type && - 'OpeningTag' === U.type && - (($ = '' + B + U.value + V.value + j), x.push(o - 2, o - 1)); - } - return $; - }); - return ( - x.forEach(function (s) { - return (C[s] = null); - }), - C.filter(function (s) { - return !!s; - }).join('\n') - ); - }; - }, - 31499: (s) => { - var o = { '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }; - s.exports = function escapeForXML(s) { - return s && s.replace - ? s.replace(/([&"<>'])/g, function (s, i) { - return o[i]; - }) - : s; - }; - }, - 19123: (s, o, i) => { - var u = i(65606), - _ = i(31499), - w = i(88310).Stream; - function resolve(s, o, i) { - var u, - w = (function create_indent(s, o) { - return new Array(o || 0).join(s || ''); - })(o, (i = i || 0)), - x = s; - if ('object' == typeof s && (x = s[(u = Object.keys(s)[0])]) && x._elem) - return ( - (x._elem.name = u), - (x._elem.icount = i), - (x._elem.indent = o), - (x._elem.indents = w), - (x._elem.interrupt = x), - x._elem - ); - var C, - j = [], - L = []; - function get_attributes(s) { - Object.keys(s).forEach(function (o) { - j.push( - (function attribute(s, o) { - return s + '="' + _(o) + '"'; - })(o, s[o]) - ); - }); - } - switch (typeof x) { - case 'object': - if (null === x) break; - x._attr && get_attributes(x._attr), - x._cdata && - L.push(('/g, ']]]]>') + ']]>'), - x.forEach && - ((C = !1), - L.push(''), - x.forEach(function (s) { - 'object' == typeof s - ? '_attr' == Object.keys(s)[0] - ? get_attributes(s._attr) - : L.push(resolve(s, o, i + 1)) - : (L.pop(), (C = !0), L.push(_(s))); - }), - C || L.push('')); - break; - default: - L.push(_(x)); - } - return { - name: u, - interrupt: !1, - attributes: j, - content: L, - icount: i, - indents: w, - indent: o - }; - } - function format(s, o, i) { - if ('object' != typeof o) return s(!1, o); - var u = o.interrupt ? 1 : o.content.length; - function proceed() { - for (; o.content.length; ) { - var _ = o.content.shift(); - if (void 0 !== _) { - if (interrupt(_)) return; - format(s, _); - } - } - s( - !1, - (u > 1 ? o.indents : '') + - (o.name ? '' : '') + - (o.indent && !i ? '\n' : '') - ), - i && i(); - } - function interrupt(o) { - return ( - !!o.interrupt && - ((o.interrupt.append = s), - (o.interrupt.end = proceed), - (o.interrupt = !1), - s(!0), - !0) - ); - } - if ( - (s( - !1, - o.indents + - (o.name ? '<' + o.name : '') + - (o.attributes.length ? ' ' + o.attributes.join(' ') : '') + - (u ? (o.name ? '>' : '') : o.name ? '/>' : '') + - (o.indent && u > 1 ? '\n' : '') - ), - !u) - ) - return s(!1, o.indent ? '\n' : ''); - interrupt(o) || proceed(); - } - (s.exports = function xml(s, o) { - 'object' != typeof o && (o = { indent: o }); - var i = o.stream ? new w() : null, - _ = '', - x = !1, - C = o.indent ? (!0 === o.indent ? ' ' : o.indent) : '', - j = !0; - function delay(s) { - j ? u.nextTick(s) : s(); - } - function append(s, o) { - if ((void 0 !== o && (_ += o), s && !x && ((i = i || new w()), (x = !0)), s && x)) { - var u = _; - delay(function () { - i.emit('data', u); - }), - (_ = ''); - } - } - function add(s, o) { - format(append, resolve(s, C, C ? 1 : 0), o); - } - function end() { - if (i) { - var s = _; - delay(function () { - i.emit('data', s), i.emit('end'), (i.readable = !1), i.emit('close'); - }); - } - } - return ( - delay(function () { - j = !1; - }), - o.declaration && - (function addXmlDeclaration(s) { - var o = { version: '1.0', encoding: s.encoding || 'UTF-8' }; - s.standalone && (o.standalone = s.standalone), - add({ '?xml': { _attr: o } }), - (_ = _.replace('/>', '?>')); - })(o.declaration), - s && s.forEach - ? s.forEach(function (o, i) { - var u; - i + 1 === s.length && (u = end), add(o, u); - }) - : add(s, end), - i ? ((i.readable = !0), i) : _ - ); - }), - (s.exports.element = s.exports.Element = - function element() { - var s = { - _elem: resolve(Array.prototype.slice.call(arguments)), - push: function (s) { - if (!this.append) throw new Error('not assigned to a parent!'); - var o = this, - i = this._elem.indent; - format( - this.append, - resolve(s, i, this._elem.icount + (i ? 1 : 0)), - function () { - o.append(!0); - } - ); - }, - close: function (s) { - void 0 !== s && this.push(s), this.end && this.end(); - } - }; - return s; - }); - }, - 86215: function (s, o) { - var i, u, _; - (u = []), - (i = (function () { - 'use strict'; - var isNativeSmoothScrollEnabledOn = function (s) { - return ( - s && - 'getComputedStyle' in window && - 'smooth' === window.getComputedStyle(s)['scroll-behavior'] - ); - }; - if ('undefined' == typeof window || !('document' in window)) return {}; - var makeScroller = function (s, o, i) { - var u; - (o = o || 999), i || 0 === i || (i = 9); - var setScrollTimeoutId = function (s) { - u = s; - }, - stopScroll = function () { - clearTimeout(u), setScrollTimeoutId(0); - }, - getTopWithEdgeOffset = function (o) { - return Math.max(0, s.getTopOf(o) - i); - }, - scrollToY = function (i, u, _) { - if ( - (stopScroll(), - 0 === u || (u && u < 0) || isNativeSmoothScrollEnabledOn(s.body)) - ) - s.toY(i), _ && _(); - else { - var w = s.getY(), - x = Math.max(0, i) - w, - C = new Date().getTime(); - (u = u || Math.min(Math.abs(x), o)), - (function loopScroll() { - setScrollTimeoutId( - setTimeout(function () { - var o = Math.min(1, (new Date().getTime() - C) / u), - i = Math.max( - 0, - Math.floor(w + x * (o < 0.5 ? 2 * o * o : o * (4 - 2 * o) - 1)) - ); - s.toY(i), - o < 1 && s.getHeight() + i < s.body.scrollHeight - ? loopScroll() - : (setTimeout(stopScroll, 99), _ && _()); - }, 9) - ); - })(); - } - }, - scrollToElem = function (s, o, i) { - scrollToY(getTopWithEdgeOffset(s), o, i); - }, - scrollIntoView = function (o, u, _) { - var w = o.getBoundingClientRect().height, - x = s.getTopOf(o) + w, - C = s.getHeight(), - j = s.getY(), - L = j + C; - getTopWithEdgeOffset(o) < j || w + i > C - ? scrollToElem(o, u, _) - : x + i > L - ? scrollToY(x - C + i, u, _) - : _ && _(); - }, - scrollToCenterOf = function (o, i, u, _) { - scrollToY( - Math.max( - 0, - s.getTopOf(o) - - s.getHeight() / 2 + - (u || o.getBoundingClientRect().height / 2) - ), - i, - _ - ); - }; - return { - setup: function (s, u) { - return ( - (0 === s || s) && (o = s), - (0 === u || u) && (i = u), - { defaultDuration: o, edgeOffset: i } - ); - }, - to: scrollToElem, - toY: scrollToY, - intoView: scrollIntoView, - center: scrollToCenterOf, - stop: stopScroll, - moving: function () { - return !!u; - }, - getY: s.getY, - getTopOf: s.getTopOf - }; - }, - s = document.documentElement, - getDocY = function () { - return window.scrollY || s.scrollTop; - }, - o = makeScroller({ - body: document.scrollingElement || document.body, - toY: function (s) { - window.scrollTo(0, s); - }, - getY: getDocY, - getHeight: function () { - return window.innerHeight || s.clientHeight; - }, - getTopOf: function (o) { - return o.getBoundingClientRect().top + getDocY() - s.offsetTop; - } - }); - if ( - ((o.createScroller = function (o, i, u) { - return makeScroller( - { - body: o, - toY: function (s) { - o.scrollTop = s; - }, - getY: function () { - return o.scrollTop; - }, - getHeight: function () { - return Math.min(o.clientHeight, window.innerHeight || s.clientHeight); - }, - getTopOf: function (s) { - return s.offsetTop; - } - }, - i, - u - ); - }), - 'addEventListener' in window && - !window.noZensmooth && - !isNativeSmoothScrollEnabledOn(document.body)) - ) { - var i = 'history' in window && 'pushState' in history, - u = i && 'scrollRestoration' in history; - u && (history.scrollRestoration = 'auto'), - window.addEventListener( - 'load', - function () { - u && - (setTimeout(function () { - history.scrollRestoration = 'manual'; - }, 9), - window.addEventListener( - 'popstate', - function (s) { - s.state && 'zenscrollY' in s.state && o.toY(s.state.zenscrollY); - }, - !1 - )), - window.location.hash && - setTimeout(function () { - var s = o.setup().edgeOffset; - if (s) { - var i = document.getElementById(window.location.href.split('#')[1]); - if (i) { - var u = Math.max(0, o.getTopOf(i) - s), - _ = o.getY() - u; - 0 <= _ && _ < 9 && window.scrollTo(0, u); - } - } - }, 9); - }, - !1 - ); - var _ = new RegExp('(^|\\s)noZensmooth(\\s|$)'); - window.addEventListener( - 'click', - function (s) { - for (var w = s.target; w && 'A' !== w.tagName; ) w = w.parentNode; - if ( - !(!w || 1 !== s.which || s.shiftKey || s.metaKey || s.ctrlKey || s.altKey) - ) { - if (u) { - var x = - history.state && 'object' == typeof history.state ? history.state : {}; - x.zenscrollY = o.getY(); - try { - history.replaceState(x, ''); - } catch (s) {} - } - var C = w.getAttribute('href') || ''; - if (0 === C.indexOf('#') && !_.test(w.className)) { - var j = 0, - L = document.getElementById(C.substring(1)); - if ('#' !== C) { - if (!L) return; - j = o.getTopOf(L); - } - s.preventDefault(); - var onDone = function () { - window.location = C; - }, - B = o.setup().edgeOffset; - B && - ((j = Math.max(0, j - B)), - i && - (onDone = function () { - history.pushState({}, '', C); - })), - o.toY(j, null, onDone); - } - } - }, - !1 - ); - } - return o; - })()), - void 0 === (_ = 'function' == typeof i ? i.apply(o, u) : i) || (s.exports = _); - }, - 15340: () => {}, - 79838: () => {}, - 48675: (s, o, i) => { - s.exports = i(20850); - }, - 7666: (s, o, i) => { - var u = i(84851), - _ = i(953); - function _extends() { - var o; - return ( - (s.exports = _extends = - u - ? _((o = u)).call(o) - : function (s) { - for (var o = 1; o < arguments.length; o++) { - var i = arguments[o]; - for (var u in i) ({}).hasOwnProperty.call(i, u) && (s[u] = i[u]); - } - return s; - }), - (s.exports.__esModule = !0), - (s.exports.default = s.exports), - _extends.apply(null, arguments) - ); - } - (s.exports = _extends), (s.exports.__esModule = !0), (s.exports.default = s.exports); - }, - 46942: (s, o) => { - var i; - !(function () { - 'use strict'; - var u = {}.hasOwnProperty; - function classNames() { - for (var s = '', o = 0; o < arguments.length; o++) { - var i = arguments[o]; - i && (s = appendClass(s, parseValue(i))); - } - return s; - } - function parseValue(s) { - if ('string' == typeof s || 'number' == typeof s) return s; - if ('object' != typeof s) return ''; - if (Array.isArray(s)) return classNames.apply(null, s); - if ( - s.toString !== Object.prototype.toString && - !s.toString.toString().includes('[native code]') - ) - return s.toString(); - var o = ''; - for (var i in s) u.call(s, i) && s[i] && (o = appendClass(o, i)); - return o; - } - function appendClass(s, o) { - return o ? (s ? s + ' ' + o : s + o) : s; - } - s.exports - ? ((classNames.default = classNames), (s.exports = classNames)) - : void 0 === - (i = function () { - return classNames; - }.apply(o, [])) || (s.exports = i); - })(); - }, - 68623: (s, o, i) => { - 'use strict'; - var u = i(694); - s.exports = u; - }, - 93700: (s, o, i) => { - 'use strict'; - var u = i(19709); - s.exports = u; - }, - 462: (s, o, i) => { - 'use strict'; - var u = i(40975); - s.exports = u; - }, - 37257: (s, o, i) => { - 'use strict'; - i(96605), i(64502), i(36371), i(99363), i(7057); - var u = i(92046); - s.exports = u.AggregateError; - }, - 32567: (s, o, i) => { - 'use strict'; - i(79307); - var u = i(61747); - s.exports = u('Function', 'bind'); - }, - 23034: (s, o, i) => { - 'use strict'; - var u = i(88280), - _ = i(32567), - w = Function.prototype; - s.exports = function (s) { - var o = s.bind; - return s === w || (u(w, s) && o === w.bind) ? _ : o; - }; - }, - 9748: (s, o, i) => { - 'use strict'; - i(71340); - var u = i(92046); - s.exports = u.Object.assign; - }, - 20850: (s, o, i) => { - 'use strict'; - s.exports = i(46076); - }, - 953: (s, o, i) => { - 'use strict'; - s.exports = i(53375); - }, - 84851: (s, o, i) => { - 'use strict'; - s.exports = i(85401); - }, - 46076: (s, o, i) => { - 'use strict'; - i(91599); - var u = i(68623); - s.exports = u; - }, - 53375: (s, o, i) => { - 'use strict'; - var u = i(93700); - s.exports = u; - }, - 85401: (s, o, i) => { - 'use strict'; - var u = i(462); - s.exports = u; - }, - 82159: (s, o, i) => { - 'use strict'; - var u = i(62250), - _ = i(4640), - w = TypeError; - s.exports = function (s) { - if (u(s)) return s; - throw new w(_(s) + ' is not a function'); - }; - }, - 10043: (s, o, i) => { - 'use strict'; - var u = i(54018), - _ = String, - w = TypeError; - s.exports = function (s) { - if (u(s)) return s; - throw new w("Can't set " + _(s) + ' as a prototype'); - }; - }, - 42156: (s) => { - 'use strict'; - s.exports = function () {}; - }, - 36624: (s, o, i) => { - 'use strict'; - var u = i(46285), - _ = String, - w = TypeError; - s.exports = function (s) { - if (u(s)) return s; - throw new w(_(s) + ' is not an object'); - }; - }, - 74436: (s, o, i) => { - 'use strict'; - var u = i(4993), - _ = i(34849), - w = i(20575), - createMethod = function (s) { - return function (o, i, x) { - var C = u(o), - j = w(C); - if (0 === j) return !s && -1; - var L, - B = _(x, j); - if (s && i != i) { - for (; j > B; ) if ((L = C[B++]) != L) return !0; - } else for (; j > B; B++) if ((s || B in C) && C[B] === i) return s || B || 0; - return !s && -1; - }; - }; - s.exports = { includes: createMethod(!0), indexOf: createMethod(!1) }; - }, - 93427: (s, o, i) => { - 'use strict'; - var u = i(1907); - s.exports = u([].slice); - }, - 45807: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = u({}.toString), - w = u(''.slice); - s.exports = function (s) { - return w(_(s), 8, -1); - }; - }, - 73948: (s, o, i) => { - 'use strict'; - var u = i(52623), - _ = i(62250), - w = i(45807), - x = i(76264)('toStringTag'), - C = Object, - j = - 'Arguments' === - w( - (function () { - return arguments; - })() - ); - s.exports = u - ? w - : function (s) { - var o, i, u; - return void 0 === s - ? 'Undefined' - : null === s - ? 'Null' - : 'string' == - typeof (i = (function (s, o) { - try { - return s[o]; - } catch (s) {} - })((o = C(s)), x)) - ? i - : j - ? w(o) - : 'Object' === (u = w(o)) && _(o.callee) - ? 'Arguments' - : u; - }; - }, - 19595: (s, o, i) => { - 'use strict'; - var u = i(49724), - _ = i(11042), - w = i(13846), - x = i(74284); - s.exports = function (s, o, i) { - for (var C = _(o), j = x.f, L = w.f, B = 0; B < C.length; B++) { - var $ = C[B]; - u(s, $) || (i && u(i, $)) || j(s, $, L(o, $)); - } - }; - }, - 57382: (s, o, i) => { - 'use strict'; - var u = i(98828); - s.exports = !u(function () { - function F() {} - return (F.prototype.constructor = null), Object.getPrototypeOf(new F()) !== F.prototype; - }); - }, - 59550: (s) => { - 'use strict'; - s.exports = function (s, o) { - return { value: s, done: o }; - }; - }, - 61626: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(74284), - w = i(75817); - s.exports = u - ? function (s, o, i) { - return _.f(s, o, w(1, i)); - } - : function (s, o, i) { - return (s[o] = i), s; - }; - }, - 75817: (s) => { - 'use strict'; - s.exports = function (s, o) { - return { enumerable: !(1 & s), configurable: !(2 & s), writable: !(4 & s), value: o }; - }; - }, - 68055: (s, o, i) => { - 'use strict'; - var u = i(61626); - s.exports = function (s, o, i, _) { - return _ && _.enumerable ? (s[o] = i) : u(s, o, i), s; - }; - }, - 2532: (s, o, i) => { - 'use strict'; - var u = i(45951), - _ = Object.defineProperty; - s.exports = function (s, o) { - try { - _(u, s, { value: o, configurable: !0, writable: !0 }); - } catch (i) { - u[s] = o; - } - return o; - }; - }, - 39447: (s, o, i) => { - 'use strict'; - var u = i(98828); - s.exports = !u(function () { - return ( - 7 !== - Object.defineProperty({}, 1, { - get: function () { - return 7; - } - })[1] - ); - }); - }, - 49552: (s, o, i) => { - 'use strict'; - var u = i(45951), - _ = i(46285), - w = u.document, - x = _(w) && _(w.createElement); - s.exports = function (s) { - return x ? w.createElement(s) : {}; - }; - }, - 19287: (s) => { - 'use strict'; - s.exports = { - CSSRuleList: 0, - CSSStyleDeclaration: 0, - CSSValueList: 0, - ClientRectList: 0, - DOMRectList: 0, - DOMStringList: 0, - DOMTokenList: 1, - DataTransferItemList: 0, - FileList: 0, - HTMLAllCollection: 0, - HTMLCollection: 0, - HTMLFormElement: 0, - HTMLSelectElement: 0, - MediaList: 0, - MimeTypeArray: 0, - NamedNodeMap: 0, - NodeList: 1, - PaintRequestList: 0, - Plugin: 0, - PluginArray: 0, - SVGLengthList: 0, - SVGNumberList: 0, - SVGPathSegList: 0, - SVGPointList: 0, - SVGStringList: 0, - SVGTransformList: 0, - SourceBufferList: 0, - StyleSheetList: 0, - TextTrackCueList: 0, - TextTrackList: 0, - TouchList: 0 - }; - }, - 80376: (s) => { - 'use strict'; - s.exports = [ - 'constructor', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'toLocaleString', - 'toString', - 'valueOf' - ]; - }, - 96794: (s, o, i) => { - 'use strict'; - var u = i(45951).navigator, - _ = u && u.userAgent; - s.exports = _ ? String(_) : ''; - }, - 20798: (s, o, i) => { - 'use strict'; - var u, - _, - w = i(45951), - x = i(96794), - C = w.process, - j = w.Deno, - L = (C && C.versions) || (j && j.version), - B = L && L.v8; - B && (_ = (u = B.split('.'))[0] > 0 && u[0] < 4 ? 1 : +(u[0] + u[1])), - !_ && - x && - (!(u = x.match(/Edge\/(\d+)/)) || u[1] >= 74) && - (u = x.match(/Chrome\/(\d+)/)) && - (_ = +u[1]), - (s.exports = _); - }, - 85762: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = Error, - w = u(''.replace), - x = String(new _('zxcasd').stack), - C = /\n\s*at [^:]*:[^\n]*/, - j = C.test(x); - s.exports = function (s, o) { - if (j && 'string' == typeof s && !_.prepareStackTrace) for (; o--; ) s = w(s, C, ''); - return s; - }; - }, - 85884: (s, o, i) => { - 'use strict'; - var u = i(61626), - _ = i(85762), - w = i(23888), - x = Error.captureStackTrace; - s.exports = function (s, o, i, C) { - w && (x ? x(s, o) : u(s, 'stack', _(i, C))); - }; - }, - 23888: (s, o, i) => { - 'use strict'; - var u = i(98828), - _ = i(75817); - s.exports = !u(function () { - var s = new Error('a'); - return !('stack' in s) || (Object.defineProperty(s, 'stack', _(1, 7)), 7 !== s.stack); - }); - }, - 11091: (s, o, i) => { - 'use strict'; - var u = i(45951), - _ = i(76024), - w = i(92361), - x = i(62250), - C = i(13846).f, - j = i(7463), - L = i(92046), - B = i(28311), - $ = i(61626), - V = i(49724); - i(36128); - var wrapConstructor = function (s) { - var Wrapper = function (o, i, u) { - if (this instanceof Wrapper) { - switch (arguments.length) { - case 0: - return new s(); - case 1: - return new s(o); - case 2: - return new s(o, i); - } - return new s(o, i, u); - } - return _(s, this, arguments); - }; - return (Wrapper.prototype = s.prototype), Wrapper; - }; - s.exports = function (s, o) { - var i, - _, - U, - z, - Y, - Z, - ee, - ie, - ae, - le = s.target, - ce = s.global, - pe = s.stat, - de = s.proto, - fe = ce ? u : pe ? u[le] : u[le] && u[le].prototype, - ye = ce ? L : L[le] || $(L, le, {})[le], - be = ye.prototype; - for (z in o) - (_ = !(i = j(ce ? z : le + (pe ? '.' : '#') + z, s.forced)) && fe && V(fe, z)), - (Z = ye[z]), - _ && (ee = s.dontCallGetSet ? (ae = C(fe, z)) && ae.value : fe[z]), - (Y = _ && ee ? ee : o[z]), - (i || de || typeof Z != typeof Y) && - ((ie = - s.bind && _ - ? B(Y, u) - : s.wrap && _ - ? wrapConstructor(Y) - : de && x(Y) - ? w(Y) - : Y), - (s.sham || (Y && Y.sham) || (Z && Z.sham)) && $(ie, 'sham', !0), - $(ye, z, ie), - de && - (V(L, (U = le + 'Prototype')) || $(L, U, {}), - $(L[U], z, Y), - s.real && be && (i || !be[z]) && $(be, z, Y))); - }; - }, - 98828: (s) => { - 'use strict'; - s.exports = function (s) { - try { - return !!s(); - } catch (s) { - return !0; - } - }; - }, - 76024: (s, o, i) => { - 'use strict'; - var u = i(41505), - _ = Function.prototype, - w = _.apply, - x = _.call; - s.exports = - ('object' == typeof Reflect && Reflect.apply) || - (u - ? x.bind(w) - : function () { - return x.apply(w, arguments); - }); - }, - 28311: (s, o, i) => { - 'use strict'; - var u = i(92361), - _ = i(82159), - w = i(41505), - x = u(u.bind); - s.exports = function (s, o) { - return ( - _(s), - void 0 === o - ? s - : w - ? x(s, o) - : function () { - return s.apply(o, arguments); - } - ); - }; - }, - 41505: (s, o, i) => { - 'use strict'; - var u = i(98828); - s.exports = !u(function () { - var s = function () {}.bind(); - return 'function' != typeof s || s.hasOwnProperty('prototype'); - }); - }, - 44673: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = i(82159), - w = i(46285), - x = i(49724), - C = i(93427), - j = i(41505), - L = Function, - B = u([].concat), - $ = u([].join), - V = {}; - s.exports = j - ? L.bind - : function bind(s) { - var o = _(this), - i = o.prototype, - u = C(arguments, 1), - j = function bound() { - var i = B(u, C(arguments)); - return this instanceof j - ? (function (s, o, i) { - if (!x(V, o)) { - for (var u = [], _ = 0; _ < o; _++) u[_] = 'a[' + _ + ']'; - V[o] = L('C,a', 'return new C(' + $(u, ',') + ')'); - } - return V[o](s, i); - })(o, i.length, i) - : o.apply(s, i); - }; - return w(i) && (j.prototype = i), j; - }; - }, - 13930: (s, o, i) => { - 'use strict'; - var u = i(41505), - _ = Function.prototype.call; - s.exports = u - ? _.bind(_) - : function () { - return _.apply(_, arguments); - }; - }, - 36833: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(49724), - w = Function.prototype, - x = u && Object.getOwnPropertyDescriptor, - C = _(w, 'name'), - j = C && 'something' === function something() {}.name, - L = C && (!u || (u && x(w, 'name').configurable)); - s.exports = { EXISTS: C, PROPER: j, CONFIGURABLE: L }; - }, - 51871: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = i(82159); - s.exports = function (s, o, i) { - try { - return u(_(Object.getOwnPropertyDescriptor(s, o)[i])); - } catch (s) {} - }; - }, - 92361: (s, o, i) => { - 'use strict'; - var u = i(45807), - _ = i(1907); - s.exports = function (s) { - if ('Function' === u(s)) return _(s); - }; - }, - 1907: (s, o, i) => { - 'use strict'; - var u = i(41505), - _ = Function.prototype, - w = _.call, - x = u && _.bind.bind(w, w); - s.exports = u - ? x - : function (s) { - return function () { - return w.apply(s, arguments); - }; - }; - }, - 61747: (s, o, i) => { - 'use strict'; - var u = i(45951), - _ = i(92046); - s.exports = function (s, o) { - var i = _[s + 'Prototype'], - w = i && i[o]; - if (w) return w; - var x = u[s], - C = x && x.prototype; - return C && C[o]; - }; - }, - 85582: (s, o, i) => { - 'use strict'; - var u = i(92046), - _ = i(45951), - w = i(62250), - aFunction = function (s) { - return w(s) ? s : void 0; - }; - s.exports = function (s, o) { - return arguments.length < 2 - ? aFunction(u[s]) || aFunction(_[s]) - : (u[s] && u[s][o]) || (_[s] && _[s][o]); - }; - }, - 73448: (s, o, i) => { - 'use strict'; - var u = i(73948), - _ = i(29367), - w = i(87136), - x = i(93742), - C = i(76264)('iterator'); - s.exports = function (s) { - if (!w(s)) return _(s, C) || _(s, '@@iterator') || x[u(s)]; - }; - }, - 10300: (s, o, i) => { - 'use strict'; - var u = i(13930), - _ = i(82159), - w = i(36624), - x = i(4640), - C = i(73448), - j = TypeError; - s.exports = function (s, o) { - var i = arguments.length < 2 ? C(s) : o; - if (_(i)) return w(u(i, s)); - throw new j(x(s) + ' is not iterable'); - }; - }, - 29367: (s, o, i) => { - 'use strict'; - var u = i(82159), - _ = i(87136); - s.exports = function (s, o) { - var i = s[o]; - return _(i) ? void 0 : u(i); - }; - }, - 45951: function (s, o, i) { - 'use strict'; - var check = function (s) { - return s && s.Math === Math && s; - }; - s.exports = - check('object' == typeof globalThis && globalThis) || - check('object' == typeof window && window) || - check('object' == typeof self && self) || - check('object' == typeof i.g && i.g) || - check('object' == typeof this && this) || - (function () { - return this; - })() || - Function('return this')(); - }, - 49724: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = i(39298), - w = u({}.hasOwnProperty); - s.exports = - Object.hasOwn || - function hasOwn(s, o) { - return w(_(s), o); - }; - }, - 38530: (s) => { - 'use strict'; - s.exports = {}; - }, - 62416: (s, o, i) => { - 'use strict'; - var u = i(85582); - s.exports = u('document', 'documentElement'); - }, - 73648: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(98828), - w = i(49552); - s.exports = - !u && - !_(function () { - return ( - 7 !== - Object.defineProperty(w('div'), 'a', { - get: function () { - return 7; - } - }).a - ); - }); - }, - 16946: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = i(98828), - w = i(45807), - x = Object, - C = u(''.split); - s.exports = _(function () { - return !x('z').propertyIsEnumerable(0); - }) - ? function (s) { - return 'String' === w(s) ? C(s, '') : x(s); - } - : x; - }, - 34084: (s, o, i) => { - 'use strict'; - var u = i(62250), - _ = i(46285), - w = i(79192); - s.exports = function (s, o, i) { - var x, C; - return ( - w && - u((x = o.constructor)) && - x !== i && - _((C = x.prototype)) && - C !== i.prototype && - w(s, C), - s - ); - }; - }, - 39259: (s, o, i) => { - 'use strict'; - var u = i(46285), - _ = i(61626); - s.exports = function (s, o) { - u(o) && 'cause' in o && _(s, 'cause', o.cause); - }; - }, - 64932: (s, o, i) => { - 'use strict'; - var u, - _, - w, - x = i(40551), - C = i(45951), - j = i(46285), - L = i(61626), - B = i(49724), - $ = i(36128), - V = i(92522), - U = i(38530), - z = 'Object already initialized', - Y = C.TypeError, - Z = C.WeakMap; - if (x || $.state) { - var ee = $.state || ($.state = new Z()); - (ee.get = ee.get), - (ee.has = ee.has), - (ee.set = ee.set), - (u = function (s, o) { - if (ee.has(s)) throw new Y(z); - return (o.facade = s), ee.set(s, o), o; - }), - (_ = function (s) { - return ee.get(s) || {}; - }), - (w = function (s) { - return ee.has(s); - }); - } else { - var ie = V('state'); - (U[ie] = !0), - (u = function (s, o) { - if (B(s, ie)) throw new Y(z); - return (o.facade = s), L(s, ie, o), o; - }), - (_ = function (s) { - return B(s, ie) ? s[ie] : {}; - }), - (w = function (s) { - return B(s, ie); - }); - } - s.exports = { - set: u, - get: _, - has: w, - enforce: function (s) { - return w(s) ? _(s) : u(s, {}); - }, - getterFor: function (s) { - return function (o) { - var i; - if (!j(o) || (i = _(o)).type !== s) - throw new Y('Incompatible receiver, ' + s + ' required'); - return i; - }; - } - }; - }, - 37812: (s, o, i) => { - 'use strict'; - var u = i(76264), - _ = i(93742), - w = u('iterator'), - x = Array.prototype; - s.exports = function (s) { - return void 0 !== s && (_.Array === s || x[w] === s); - }; - }, - 62250: (s) => { - 'use strict'; - var o = 'object' == typeof document && document.all; - s.exports = - void 0 === o && void 0 !== o - ? function (s) { - return 'function' == typeof s || s === o; - } - : function (s) { - return 'function' == typeof s; - }; - }, - 7463: (s, o, i) => { - 'use strict'; - var u = i(98828), - _ = i(62250), - w = /#|\.prototype\./, - isForced = function (s, o) { - var i = C[x(s)]; - return i === L || (i !== j && (_(o) ? u(o) : !!o)); - }, - x = (isForced.normalize = function (s) { - return String(s).replace(w, '.').toLowerCase(); - }), - C = (isForced.data = {}), - j = (isForced.NATIVE = 'N'), - L = (isForced.POLYFILL = 'P'); - s.exports = isForced; - }, - 87136: (s) => { - 'use strict'; - s.exports = function (s) { - return null == s; - }; - }, - 46285: (s, o, i) => { - 'use strict'; - var u = i(62250); - s.exports = function (s) { - return 'object' == typeof s ? null !== s : u(s); - }; - }, - 54018: (s, o, i) => { - 'use strict'; - var u = i(46285); - s.exports = function (s) { - return u(s) || null === s; - }; - }, - 7376: (s) => { - 'use strict'; - s.exports = !0; - }, - 25594: (s, o, i) => { - 'use strict'; - var u = i(85582), - _ = i(62250), - w = i(88280), - x = i(51175), - C = Object; - s.exports = x - ? function (s) { - return 'symbol' == typeof s; - } - : function (s) { - var o = u('Symbol'); - return _(o) && w(o.prototype, C(s)); - }; - }, - 24823: (s, o, i) => { - 'use strict'; - var u = i(28311), - _ = i(13930), - w = i(36624), - x = i(4640), - C = i(37812), - j = i(20575), - L = i(88280), - B = i(10300), - $ = i(73448), - V = i(40154), - U = TypeError, - Result = function (s, o) { - (this.stopped = s), (this.result = o); - }, - z = Result.prototype; - s.exports = function (s, o, i) { - var Y, - Z, - ee, - ie, - ae, - le, - ce, - pe = i && i.that, - de = !(!i || !i.AS_ENTRIES), - fe = !(!i || !i.IS_RECORD), - ye = !(!i || !i.IS_ITERATOR), - be = !(!i || !i.INTERRUPTED), - _e = u(o, pe), - stop = function (s) { - return Y && V(Y, 'normal', s), new Result(!0, s); - }, - callFn = function (s) { - return de - ? (w(s), be ? _e(s[0], s[1], stop) : _e(s[0], s[1])) - : be - ? _e(s, stop) - : _e(s); - }; - if (fe) Y = s.iterator; - else if (ye) Y = s; - else { - if (!(Z = $(s))) throw new U(x(s) + ' is not iterable'); - if (C(Z)) { - for (ee = 0, ie = j(s); ie > ee; ee++) - if ((ae = callFn(s[ee])) && L(z, ae)) return ae; - return new Result(!1); - } - Y = B(s, Z); - } - for (le = fe ? s.next : Y.next; !(ce = _(le, Y)).done; ) { - try { - ae = callFn(ce.value); - } catch (s) { - V(Y, 'throw', s); - } - if ('object' == typeof ae && ae && L(z, ae)) return ae; - } - return new Result(!1); - }; - }, - 40154: (s, o, i) => { - 'use strict'; - var u = i(13930), - _ = i(36624), - w = i(29367); - s.exports = function (s, o, i) { - var x, C; - _(s); - try { - if (!(x = w(s, 'return'))) { - if ('throw' === o) throw i; - return i; - } - x = u(x, s); - } catch (s) { - (C = !0), (x = s); - } - if ('throw' === o) throw i; - if (C) throw x; - return _(x), i; - }; - }, - 47181: (s, o, i) => { - 'use strict'; - var u = i(95116).IteratorPrototype, - _ = i(58075), - w = i(75817), - x = i(14840), - C = i(93742), - returnThis = function () { - return this; - }; - s.exports = function (s, o, i, j) { - var L = o + ' Iterator'; - return ( - (s.prototype = _(u, { next: w(+!j, i) })), x(s, L, !1, !0), (C[L] = returnThis), s - ); - }; - }, - 60183: (s, o, i) => { - 'use strict'; - var u = i(11091), - _ = i(13930), - w = i(7376), - x = i(36833), - C = i(62250), - j = i(47181), - L = i(15972), - B = i(79192), - $ = i(14840), - V = i(61626), - U = i(68055), - z = i(76264), - Y = i(93742), - Z = i(95116), - ee = x.PROPER, - ie = x.CONFIGURABLE, - ae = Z.IteratorPrototype, - le = Z.BUGGY_SAFARI_ITERATORS, - ce = z('iterator'), - pe = 'keys', - de = 'values', - fe = 'entries', - returnThis = function () { - return this; - }; - s.exports = function (s, o, i, x, z, Z, ye) { - j(i, o, x); - var be, - _e, - we, - getIterationMethod = function (s) { - if (s === z && Re) return Re; - if (!le && s && s in Pe) return Pe[s]; - switch (s) { - case pe: - return function keys() { - return new i(this, s); - }; - case de: - return function values() { - return new i(this, s); - }; - case fe: - return function entries() { - return new i(this, s); - }; - } - return function () { - return new i(this); - }; - }, - Se = o + ' Iterator', - xe = !1, - Pe = s.prototype, - Te = Pe[ce] || Pe['@@iterator'] || (z && Pe[z]), - Re = (!le && Te) || getIterationMethod(z), - qe = ('Array' === o && Pe.entries) || Te; - if ( - (qe && - (be = L(qe.call(new s()))) !== Object.prototype && - be.next && - (w || L(be) === ae || (B ? B(be, ae) : C(be[ce]) || U(be, ce, returnThis)), - $(be, Se, !0, !0), - w && (Y[Se] = returnThis)), - ee && - z === de && - Te && - Te.name !== de && - (!w && ie - ? V(Pe, 'name', de) - : ((xe = !0), - (Re = function values() { - return _(Te, this); - }))), - z) - ) - if ( - ((_e = { - values: getIterationMethod(de), - keys: Z ? Re : getIterationMethod(pe), - entries: getIterationMethod(fe) - }), - ye) - ) - for (we in _e) (le || xe || !(we in Pe)) && U(Pe, we, _e[we]); - else u({ target: o, proto: !0, forced: le || xe }, _e); - return (w && !ye) || Pe[ce] === Re || U(Pe, ce, Re, { name: z }), (Y[o] = Re), _e; - }; - }, - 95116: (s, o, i) => { - 'use strict'; - var u, - _, - w, - x = i(98828), - C = i(62250), - j = i(46285), - L = i(58075), - B = i(15972), - $ = i(68055), - V = i(76264), - U = i(7376), - z = V('iterator'), - Y = !1; - [].keys && - ('next' in (w = [].keys()) ? (_ = B(B(w))) !== Object.prototype && (u = _) : (Y = !0)), - !j(u) || - x(function () { - var s = {}; - return u[z].call(s) !== s; - }) - ? (u = {}) - : U && (u = L(u)), - C(u[z]) || - $(u, z, function () { - return this; - }), - (s.exports = { IteratorPrototype: u, BUGGY_SAFARI_ITERATORS: Y }); - }, - 93742: (s) => { - 'use strict'; - s.exports = {}; - }, - 20575: (s, o, i) => { - 'use strict'; - var u = i(3121); - s.exports = function (s) { - return u(s.length); - }; - }, - 41176: (s) => { - 'use strict'; - var o = Math.ceil, - i = Math.floor; - s.exports = - Math.trunc || - function trunc(s) { - var u = +s; - return (u > 0 ? i : o)(u); - }; - }, - 32096: (s, o, i) => { - 'use strict'; - var u = i(90160); - s.exports = function (s, o) { - return void 0 === s ? (arguments.length < 2 ? '' : o) : u(s); - }; - }, - 29538: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(1907), - w = i(13930), - x = i(98828), - C = i(2875), - j = i(87170), - L = i(22574), - B = i(39298), - $ = i(16946), - V = Object.assign, - U = Object.defineProperty, - z = _([].concat); - s.exports = - !V || - x(function () { - if ( - u && - 1 !== - V( - { b: 1 }, - V( - U({}, 'a', { - enumerable: !0, - get: function () { - U(this, 'b', { value: 3, enumerable: !1 }); - } - }), - { b: 2 } - ) - ).b - ) - return !0; - var s = {}, - o = {}, - i = Symbol('assign detection'), - _ = 'abcdefghijklmnopqrst'; - return ( - (s[i] = 7), - _.split('').forEach(function (s) { - o[s] = s; - }), - 7 !== V({}, s)[i] || C(V({}, o)).join('') !== _ - ); - }) - ? function assign(s, o) { - for (var i = B(s), _ = arguments.length, x = 1, V = j.f, U = L.f; _ > x; ) - for ( - var Y, - Z = $(arguments[x++]), - ee = V ? z(C(Z), V(Z)) : C(Z), - ie = ee.length, - ae = 0; - ie > ae; - - ) - (Y = ee[ae++]), (u && !w(U, Z, Y)) || (i[Y] = Z[Y]); - return i; - } - : V; - }, - 58075: (s, o, i) => { - 'use strict'; - var u, - _ = i(36624), - w = i(42220), - x = i(80376), - C = i(38530), - j = i(62416), - L = i(49552), - B = i(92522), - $ = 'prototype', - V = 'script', - U = B('IE_PROTO'), - EmptyConstructor = function () {}, - scriptTag = function (s) { - return '<' + V + '>' + s + ''; - }, - NullProtoObjectViaActiveX = function (s) { - s.write(scriptTag('')), s.close(); - var o = s.parentWindow.Object; - return (s = null), o; - }, - NullProtoObject = function () { - try { - u = new ActiveXObject('htmlfile'); - } catch (s) {} - var s, o, i; - NullProtoObject = - 'undefined' != typeof document - ? document.domain && u - ? NullProtoObjectViaActiveX(u) - : ((o = L('iframe')), - (i = 'java' + V + ':'), - (o.style.display = 'none'), - j.appendChild(o), - (o.src = String(i)), - (s = o.contentWindow.document).open(), - s.write(scriptTag('document.F=Object')), - s.close(), - s.F) - : NullProtoObjectViaActiveX(u); - for (var _ = x.length; _--; ) delete NullProtoObject[$][x[_]]; - return NullProtoObject(); - }; - (C[U] = !0), - (s.exports = - Object.create || - function create(s, o) { - var i; - return ( - null !== s - ? ((EmptyConstructor[$] = _(s)), - (i = new EmptyConstructor()), - (EmptyConstructor[$] = null), - (i[U] = s)) - : (i = NullProtoObject()), - void 0 === o ? i : w.f(i, o) - ); - }); - }, - 42220: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(58661), - w = i(74284), - x = i(36624), - C = i(4993), - j = i(2875); - o.f = - u && !_ - ? Object.defineProperties - : function defineProperties(s, o) { - x(s); - for (var i, u = C(o), _ = j(o), L = _.length, B = 0; L > B; ) - w.f(s, (i = _[B++]), u[i]); - return s; - }; - }, - 74284: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(73648), - w = i(58661), - x = i(36624), - C = i(70470), - j = TypeError, - L = Object.defineProperty, - B = Object.getOwnPropertyDescriptor, - $ = 'enumerable', - V = 'configurable', - U = 'writable'; - o.f = u - ? w - ? function defineProperty(s, o, i) { - if ( - (x(s), - (o = C(o)), - x(i), - 'function' == typeof s && 'prototype' === o && 'value' in i && U in i && !i[U]) - ) { - var u = B(s, o); - u && - u[U] && - ((s[o] = i.value), - (i = { - configurable: V in i ? i[V] : u[V], - enumerable: $ in i ? i[$] : u[$], - writable: !1 - })); - } - return L(s, o, i); - } - : L - : function defineProperty(s, o, i) { - if ((x(s), (o = C(o)), x(i), _)) - try { - return L(s, o, i); - } catch (s) {} - if ('get' in i || 'set' in i) throw new j('Accessors not supported'); - return 'value' in i && (s[o] = i.value), s; - }; - }, - 13846: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(13930), - w = i(22574), - x = i(75817), - C = i(4993), - j = i(70470), - L = i(49724), - B = i(73648), - $ = Object.getOwnPropertyDescriptor; - o.f = u - ? $ - : function getOwnPropertyDescriptor(s, o) { - if (((s = C(s)), (o = j(o)), B)) - try { - return $(s, o); - } catch (s) {} - if (L(s, o)) return x(!_(w.f, s, o), s[o]); - }; - }, - 24443: (s, o, i) => { - 'use strict'; - var u = i(23045), - _ = i(80376).concat('length', 'prototype'); - o.f = - Object.getOwnPropertyNames || - function getOwnPropertyNames(s) { - return u(s, _); - }; - }, - 87170: (s, o) => { - 'use strict'; - o.f = Object.getOwnPropertySymbols; - }, - 15972: (s, o, i) => { - 'use strict'; - var u = i(49724), - _ = i(62250), - w = i(39298), - x = i(92522), - C = i(57382), - j = x('IE_PROTO'), - L = Object, - B = L.prototype; - s.exports = C - ? L.getPrototypeOf - : function (s) { - var o = w(s); - if (u(o, j)) return o[j]; - var i = o.constructor; - return _(i) && o instanceof i ? i.prototype : o instanceof L ? B : null; - }; - }, - 88280: (s, o, i) => { - 'use strict'; - var u = i(1907); - s.exports = u({}.isPrototypeOf); - }, - 23045: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = i(49724), - w = i(4993), - x = i(74436).indexOf, - C = i(38530), - j = u([].push); - s.exports = function (s, o) { - var i, - u = w(s), - L = 0, - B = []; - for (i in u) !_(C, i) && _(u, i) && j(B, i); - for (; o.length > L; ) _(u, (i = o[L++])) && (~x(B, i) || j(B, i)); - return B; - }; - }, - 2875: (s, o, i) => { - 'use strict'; - var u = i(23045), - _ = i(80376); - s.exports = - Object.keys || - function keys(s) { - return u(s, _); - }; - }, - 22574: (s, o) => { - 'use strict'; - var i = {}.propertyIsEnumerable, - u = Object.getOwnPropertyDescriptor, - _ = u && !i.call({ 1: 2 }, 1); - o.f = _ - ? function propertyIsEnumerable(s) { - var o = u(this, s); - return !!o && o.enumerable; - } - : i; - }, - 79192: (s, o, i) => { - 'use strict'; - var u = i(51871), - _ = i(46285), - w = i(74239), - x = i(10043); - s.exports = - Object.setPrototypeOf || - ('__proto__' in {} - ? (function () { - var s, - o = !1, - i = {}; - try { - (s = u(Object.prototype, '__proto__', 'set'))(i, []), (o = i instanceof Array); - } catch (s) {} - return function setPrototypeOf(i, u) { - return w(i), x(u), _(i) ? (o ? s(i, u) : (i.__proto__ = u), i) : i; - }; - })() - : void 0); - }, - 54878: (s, o, i) => { - 'use strict'; - var u = i(52623), - _ = i(73948); - s.exports = u - ? {}.toString - : function toString() { - return '[object ' + _(this) + ']'; - }; - }, - 60581: (s, o, i) => { - 'use strict'; - var u = i(13930), - _ = i(62250), - w = i(46285), - x = TypeError; - s.exports = function (s, o) { - var i, C; - if ('string' === o && _((i = s.toString)) && !w((C = u(i, s)))) return C; - if (_((i = s.valueOf)) && !w((C = u(i, s)))) return C; - if ('string' !== o && _((i = s.toString)) && !w((C = u(i, s)))) return C; - throw new x("Can't convert object to primitive value"); - }; - }, - 11042: (s, o, i) => { - 'use strict'; - var u = i(85582), - _ = i(1907), - w = i(24443), - x = i(87170), - C = i(36624), - j = _([].concat); - s.exports = - u('Reflect', 'ownKeys') || - function ownKeys(s) { - var o = w.f(C(s)), - i = x.f; - return i ? j(o, i(s)) : o; - }; - }, - 92046: (s) => { - 'use strict'; - s.exports = {}; - }, - 54829: (s, o, i) => { - 'use strict'; - var u = i(74284).f; - s.exports = function (s, o, i) { - i in s || - u(s, i, { - configurable: !0, - get: function () { - return o[i]; - }, - set: function (s) { - o[i] = s; - } - }); - }; - }, - 74239: (s, o, i) => { - 'use strict'; - var u = i(87136), - _ = TypeError; - s.exports = function (s) { - if (u(s)) throw new _("Can't call method on " + s); - return s; - }; - }, - 14840: (s, o, i) => { - 'use strict'; - var u = i(52623), - _ = i(74284).f, - w = i(61626), - x = i(49724), - C = i(54878), - j = i(76264)('toStringTag'); - s.exports = function (s, o, i, L) { - var B = i ? s : s && s.prototype; - B && - (x(B, j) || _(B, j, { configurable: !0, value: o }), L && !u && w(B, 'toString', C)); - }; - }, - 92522: (s, o, i) => { - 'use strict'; - var u = i(85816), - _ = i(6499), - w = u('keys'); - s.exports = function (s) { - return w[s] || (w[s] = _(s)); - }; - }, - 36128: (s, o, i) => { - 'use strict'; - var u = i(7376), - _ = i(45951), - w = i(2532), - x = '__core-js_shared__', - C = (s.exports = _[x] || w(x, {})); - (C.versions || (C.versions = [])).push({ - version: '3.39.0', - mode: u ? 'pure' : 'global', - copyright: '© 2014-2024 Denis Pushkarev (zloirock.ru)', - license: 'https://github.com/zloirock/core-js/blob/v3.39.0/LICENSE', - source: 'https://github.com/zloirock/core-js' - }); - }, - 85816: (s, o, i) => { - 'use strict'; - var u = i(36128); - s.exports = function (s, o) { - return u[s] || (u[s] = o || {}); - }; - }, - 11470: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = i(65482), - w = i(90160), - x = i(74239), - C = u(''.charAt), - j = u(''.charCodeAt), - L = u(''.slice), - createMethod = function (s) { - return function (o, i) { - var u, - B, - $ = w(x(o)), - V = _(i), - U = $.length; - return V < 0 || V >= U - ? s - ? '' - : void 0 - : (u = j($, V)) < 55296 || - u > 56319 || - V + 1 === U || - (B = j($, V + 1)) < 56320 || - B > 57343 - ? s - ? C($, V) - : u - : s - ? L($, V, V + 2) - : B - 56320 + ((u - 55296) << 10) + 65536; - }; - }; - s.exports = { codeAt: createMethod(!1), charAt: createMethod(!0) }; - }, - 19846: (s, o, i) => { - 'use strict'; - var u = i(20798), - _ = i(98828), - w = i(45951).String; - s.exports = - !!Object.getOwnPropertySymbols && - !_(function () { - var s = Symbol('symbol detection'); - return !w(s) || !(Object(s) instanceof Symbol) || (!Symbol.sham && u && u < 41); - }); - }, - 34849: (s, o, i) => { - 'use strict'; - var u = i(65482), - _ = Math.max, - w = Math.min; - s.exports = function (s, o) { - var i = u(s); - return i < 0 ? _(i + o, 0) : w(i, o); - }; - }, - 4993: (s, o, i) => { - 'use strict'; - var u = i(16946), - _ = i(74239); - s.exports = function (s) { - return u(_(s)); - }; - }, - 65482: (s, o, i) => { - 'use strict'; - var u = i(41176); - s.exports = function (s) { - var o = +s; - return o != o || 0 === o ? 0 : u(o); - }; - }, - 3121: (s, o, i) => { - 'use strict'; - var u = i(65482), - _ = Math.min; - s.exports = function (s) { - var o = u(s); - return o > 0 ? _(o, 9007199254740991) : 0; - }; - }, - 39298: (s, o, i) => { - 'use strict'; - var u = i(74239), - _ = Object; - s.exports = function (s) { - return _(u(s)); - }; - }, - 46028: (s, o, i) => { - 'use strict'; - var u = i(13930), - _ = i(46285), - w = i(25594), - x = i(29367), - C = i(60581), - j = i(76264), - L = TypeError, - B = j('toPrimitive'); - s.exports = function (s, o) { - if (!_(s) || w(s)) return s; - var i, - j = x(s, B); - if (j) { - if ((void 0 === o && (o = 'default'), (i = u(j, s, o)), !_(i) || w(i))) return i; - throw new L("Can't convert object to primitive value"); - } - return void 0 === o && (o = 'number'), C(s, o); - }; - }, - 70470: (s, o, i) => { - 'use strict'; - var u = i(46028), - _ = i(25594); - s.exports = function (s) { - var o = u(s, 'string'); - return _(o) ? o : o + ''; - }; - }, - 52623: (s, o, i) => { - 'use strict'; - var u = {}; - (u[i(76264)('toStringTag')] = 'z'), (s.exports = '[object z]' === String(u)); - }, - 90160: (s, o, i) => { - 'use strict'; - var u = i(73948), - _ = String; - s.exports = function (s) { - if ('Symbol' === u(s)) throw new TypeError('Cannot convert a Symbol value to a string'); - return _(s); - }; - }, - 4640: (s) => { - 'use strict'; - var o = String; - s.exports = function (s) { - try { - return o(s); - } catch (s) { - return 'Object'; - } - }; - }, - 6499: (s, o, i) => { - 'use strict'; - var u = i(1907), - _ = 0, - w = Math.random(), - x = u((1).toString); - s.exports = function (s) { - return 'Symbol(' + (void 0 === s ? '' : s) + ')_' + x(++_ + w, 36); - }; - }, - 51175: (s, o, i) => { - 'use strict'; - var u = i(19846); - s.exports = u && !Symbol.sham && 'symbol' == typeof Symbol.iterator; - }, - 58661: (s, o, i) => { - 'use strict'; - var u = i(39447), - _ = i(98828); - s.exports = - u && - _(function () { - return ( - 42 !== - Object.defineProperty(function () {}, 'prototype', { value: 42, writable: !1 }) - .prototype - ); - }); - }, - 40551: (s, o, i) => { - 'use strict'; - var u = i(45951), - _ = i(62250), - w = u.WeakMap; - s.exports = _(w) && /native code/.test(String(w)); - }, - 76264: (s, o, i) => { - 'use strict'; - var u = i(45951), - _ = i(85816), - w = i(49724), - x = i(6499), - C = i(19846), - j = i(51175), - L = u.Symbol, - B = _('wks'), - $ = j ? L.for || L : (L && L.withoutSetter) || x; - s.exports = function (s) { - return w(B, s) || (B[s] = C && w(L, s) ? L[s] : $('Symbol.' + s)), B[s]; - }; - }, - 19358: (s, o, i) => { - 'use strict'; - var u = i(85582), - _ = i(49724), - w = i(61626), - x = i(88280), - C = i(79192), - j = i(19595), - L = i(54829), - B = i(34084), - $ = i(32096), - V = i(39259), - U = i(85884), - z = i(39447), - Y = i(7376); - s.exports = function (s, o, i, Z) { - var ee = 'stackTraceLimit', - ie = Z ? 2 : 1, - ae = s.split('.'), - le = ae[ae.length - 1], - ce = u.apply(null, ae); - if (ce) { - var pe = ce.prototype; - if ((!Y && _(pe, 'cause') && delete pe.cause, !i)) return ce; - var de = u('Error'), - fe = o(function (s, o) { - var i = $(Z ? o : s, void 0), - u = Z ? new ce(s) : new ce(); - return ( - void 0 !== i && w(u, 'message', i), - U(u, fe, u.stack, 2), - this && x(pe, this) && B(u, this, fe), - arguments.length > ie && V(u, arguments[ie]), - u - ); - }); - if ( - ((fe.prototype = pe), - 'Error' !== le - ? C - ? C(fe, de) - : j(fe, de, { name: !0 }) - : z && ee in ce && (L(fe, ce, ee), L(fe, ce, 'prepareStackTrace')), - j(fe, ce), - !Y) - ) - try { - pe.name !== le && w(pe, 'name', le), (pe.constructor = fe); - } catch (s) {} - return fe; - } - }; - }, - 36371: (s, o, i) => { - 'use strict'; - var u = i(11091), - _ = i(85582), - w = i(76024), - x = i(98828), - C = i(19358), - j = 'AggregateError', - L = _(j), - B = - !x(function () { - return 1 !== L([1]).errors[0]; - }) && - x(function () { - return 7 !== L([1], j, { cause: 7 }).cause; - }); - u( - { global: !0, constructor: !0, arity: 2, forced: B }, - { - AggregateError: C( - j, - function (s) { - return function AggregateError(o, i) { - return w(s, this, arguments); - }; - }, - B, - !0 - ) - } - ); - }, - 82048: (s, o, i) => { - 'use strict'; - var u = i(11091), - _ = i(88280), - w = i(15972), - x = i(79192), - C = i(19595), - j = i(58075), - L = i(61626), - B = i(75817), - $ = i(39259), - V = i(85884), - U = i(24823), - z = i(32096), - Y = i(76264)('toStringTag'), - Z = Error, - ee = [].push, - ie = function AggregateError(s, o) { - var i, - u = _(ae, this); - x ? (i = x(new Z(), u ? w(this) : ae)) : ((i = u ? this : j(ae)), L(i, Y, 'Error')), - void 0 !== o && L(i, 'message', z(o)), - V(i, ie, i.stack, 1), - arguments.length > 2 && $(i, arguments[2]); - var C = []; - return U(s, ee, { that: C }), L(i, 'errors', C), i; - }; - x ? x(ie, Z) : C(ie, Z, { name: !0 }); - var ae = (ie.prototype = j(Z.prototype, { - constructor: B(1, ie), - message: B(1, ''), - name: B(1, 'AggregateError') - })); - u({ global: !0, constructor: !0, arity: 2 }, { AggregateError: ie }); - }, - 64502: (s, o, i) => { - 'use strict'; - i(82048); - }, - 99363: (s, o, i) => { - 'use strict'; - var u = i(4993), - _ = i(42156), - w = i(93742), - x = i(64932), - C = i(74284).f, - j = i(60183), - L = i(59550), - B = i(7376), - $ = i(39447), - V = 'Array Iterator', - U = x.set, - z = x.getterFor(V); - s.exports = j( - Array, - 'Array', - function (s, o) { - U(this, { type: V, target: u(s), index: 0, kind: o }); - }, - function () { - var s = z(this), - o = s.target, - i = s.index++; - if (!o || i >= o.length) return (s.target = null), L(void 0, !0); - switch (s.kind) { - case 'keys': - return L(i, !1); - case 'values': - return L(o[i], !1); - } - return L([i, o[i]], !1); - }, - 'values' - ); - var Y = (w.Arguments = w.Array); - if ((_('keys'), _('values'), _('entries'), !B && $ && 'values' !== Y.name)) - try { - C(Y, 'name', { value: 'values' }); - } catch (s) {} - }, - 96605: (s, o, i) => { - 'use strict'; - var u = i(11091), - _ = i(45951), - w = i(76024), - x = i(19358), - C = 'WebAssembly', - j = _[C], - L = 7 !== new Error('e', { cause: 7 }).cause, - exportGlobalErrorCauseWrapper = function (s, o) { - var i = {}; - (i[s] = x(s, o, L)), u({ global: !0, constructor: !0, arity: 1, forced: L }, i); - }, - exportWebAssemblyErrorCauseWrapper = function (s, o) { - if (j && j[s]) { - var i = {}; - (i[s] = x(C + '.' + s, o, L)), - u({ target: C, stat: !0, constructor: !0, arity: 1, forced: L }, i); - } - }; - exportGlobalErrorCauseWrapper('Error', function (s) { - return function Error(o) { - return w(s, this, arguments); - }; - }), - exportGlobalErrorCauseWrapper('EvalError', function (s) { - return function EvalError(o) { - return w(s, this, arguments); - }; - }), - exportGlobalErrorCauseWrapper('RangeError', function (s) { - return function RangeError(o) { - return w(s, this, arguments); - }; - }), - exportGlobalErrorCauseWrapper('ReferenceError', function (s) { - return function ReferenceError(o) { - return w(s, this, arguments); - }; - }), - exportGlobalErrorCauseWrapper('SyntaxError', function (s) { - return function SyntaxError(o) { - return w(s, this, arguments); - }; - }), - exportGlobalErrorCauseWrapper('TypeError', function (s) { - return function TypeError(o) { - return w(s, this, arguments); - }; - }), - exportGlobalErrorCauseWrapper('URIError', function (s) { - return function URIError(o) { - return w(s, this, arguments); - }; - }), - exportWebAssemblyErrorCauseWrapper('CompileError', function (s) { - return function CompileError(o) { - return w(s, this, arguments); - }; - }), - exportWebAssemblyErrorCauseWrapper('LinkError', function (s) { - return function LinkError(o) { - return w(s, this, arguments); - }; - }), - exportWebAssemblyErrorCauseWrapper('RuntimeError', function (s) { - return function RuntimeError(o) { - return w(s, this, arguments); - }; - }); - }, - 79307: (s, o, i) => { - 'use strict'; - var u = i(11091), - _ = i(44673); - u({ target: 'Function', proto: !0, forced: Function.bind !== _ }, { bind: _ }); - }, - 71340: (s, o, i) => { - 'use strict'; - var u = i(11091), - _ = i(29538); - u({ target: 'Object', stat: !0, arity: 2, forced: Object.assign !== _ }, { assign: _ }); - }, - 7057: (s, o, i) => { - 'use strict'; - var u = i(11470).charAt, - _ = i(90160), - w = i(64932), - x = i(60183), - C = i(59550), - j = 'String Iterator', - L = w.set, - B = w.getterFor(j); - x( - String, - 'String', - function (s) { - L(this, { type: j, string: _(s), index: 0 }); - }, - function next() { - var s, - o = B(this), - i = o.string, - _ = o.index; - return _ >= i.length - ? C(void 0, !0) - : ((s = u(i, _)), (o.index += s.length), C(s, !1)); - } - ); - }, - 91599: (s, o, i) => { - 'use strict'; - i(64502); - }, - 12560: (s, o, i) => { - 'use strict'; - i(99363); - var u = i(19287), - _ = i(45951), - w = i(14840), - x = i(93742); - for (var C in u) w(_[C], C), (x[C] = x.Array); - }, - 694: (s, o, i) => { - 'use strict'; - i(91599); - var u = i(37257); - i(12560), (s.exports = u); - }, - 19709: (s, o, i) => { - 'use strict'; - var u = i(23034); - s.exports = u; - }, - 40975: (s, o, i) => { - 'use strict'; - var u = i(9748); - s.exports = u; - } - }, - u = {}; - function __webpack_require__(s) { - var o = u[s]; - if (void 0 !== o) return o.exports; - var _ = (u[s] = { id: s, loaded: !1, exports: {} }); - return i[s].call(_.exports, _, _.exports, __webpack_require__), (_.loaded = !0), _.exports; - } - (__webpack_require__.n = (s) => { - var o = s && s.__esModule ? () => s.default : () => s; - return __webpack_require__.d(o, { a: o }), o; - }), - (o = Object.getPrototypeOf ? (s) => Object.getPrototypeOf(s) : (s) => s.__proto__), - (__webpack_require__.t = function (i, u) { - if ((1 & u && (i = this(i)), 8 & u)) return i; - if ('object' == typeof i && i) { - if (4 & u && i.__esModule) return i; - if (16 & u && 'function' == typeof i.then) return i; - } - var _ = Object.create(null); - __webpack_require__.r(_); - var w = {}; - s = s || [null, o({}), o([]), o(o)]; - for (var x = 2 & u && i; 'object' == typeof x && !~s.indexOf(x); x = o(x)) - Object.getOwnPropertyNames(x).forEach((s) => (w[s] = () => i[s])); - return (w.default = () => i), __webpack_require__.d(_, w), _; - }), - (__webpack_require__.d = (s, o) => { - for (var i in o) - __webpack_require__.o(o, i) && - !__webpack_require__.o(s, i) && - Object.defineProperty(s, i, { enumerable: !0, get: o[i] }); - }), - (__webpack_require__.g = (function () { - if ('object' == typeof globalThis) return globalThis; - try { - return this || new Function('return this')(); - } catch (s) { - if ('object' == typeof window) return window; - } - })()), - (__webpack_require__.o = (s, o) => Object.prototype.hasOwnProperty.call(s, o)), - (__webpack_require__.r = (s) => { - 'undefined' != typeof Symbol && - Symbol.toStringTag && - Object.defineProperty(s, Symbol.toStringTag, { value: 'Module' }), - Object.defineProperty(s, '__esModule', { value: !0 }); - }), - (__webpack_require__.nmd = (s) => ((s.paths = []), s.children || (s.children = []), s)); - var _ = {}; - return ( - (() => { - 'use strict'; - __webpack_require__.d(_, { default: () => WI }); - var s = {}; - __webpack_require__.r(s), - __webpack_require__.d(s, { - CLEAR: () => ot, - CLEAR_BY: () => it, - NEW_AUTH_ERR: () => st, - NEW_SPEC_ERR: () => rt, - NEW_SPEC_ERR_BATCH: () => nt, - NEW_THROWN_ERR: () => et, - NEW_THROWN_ERR_BATCH: () => tt, - clear: () => clear, - clearBy: () => clearBy, - newAuthErr: () => newAuthErr, - newSpecErr: () => newSpecErr, - newSpecErrBatch: () => newSpecErrBatch, - newThrownErr: () => newThrownErr, - newThrownErrBatch: () => newThrownErrBatch - }); - var o = {}; - __webpack_require__.r(o), - __webpack_require__.d(o, { - AUTHORIZE: () => Nt, - AUTHORIZE_OAUTH2: () => Lt, - CONFIGURE_AUTH: () => Ft, - LOGOUT: () => Rt, - PRE_AUTHORIZE_OAUTH2: () => Dt, - RESTORE_AUTHORIZATION: () => qt, - SHOW_AUTH_POPUP: () => Tt, - VALIDATE: () => Bt, - authPopup: () => authPopup, - authorize: () => authorize, - authorizeAccessCodeWithBasicAuthentication: () => - authorizeAccessCodeWithBasicAuthentication, - authorizeAccessCodeWithFormParams: () => authorizeAccessCodeWithFormParams, - authorizeApplication: () => authorizeApplication, - authorizeOauth2: () => authorizeOauth2, - authorizeOauth2WithPersistOption: () => authorizeOauth2WithPersistOption, - authorizePassword: () => authorizePassword, - authorizeRequest: () => authorizeRequest, - authorizeWithPersistOption: () => authorizeWithPersistOption, - configureAuth: () => configureAuth, - logout: () => logout, - logoutWithPersistOption: () => logoutWithPersistOption, - persistAuthorizationIfNeeded: () => persistAuthorizationIfNeeded, - preAuthorizeImplicit: () => preAuthorizeImplicit, - restoreAuthorization: () => restoreAuthorization, - showDefinitions: () => showDefinitions - }); - var i = {}; - __webpack_require__.r(i), - __webpack_require__.d(i, { - authorized: () => Ht, - definitionsForRequirements: () => definitionsForRequirements, - definitionsToAuthorize: () => Kt, - getConfigs: () => Jt, - getDefinitionsByNames: () => getDefinitionsByNames, - isAuthorized: () => isAuthorized, - shownDefinitions: () => Wt - }); - var u = {}; - __webpack_require__.r(u), - __webpack_require__.d(u, { - TOGGLE_CONFIGS: () => yn, - UPDATE_CONFIGS: () => gn, - downloadConfig: () => downloadConfig, - getConfigByUrl: () => getConfigByUrl, - loaded: () => actions_loaded, - toggle: () => toggle, - update: () => update - }); - var w = {}; - __webpack_require__.r(w), __webpack_require__.d(w, { get: () => get }); - var x = {}; - __webpack_require__.r(x), __webpack_require__.d(x, { transform: () => transform }); - var C = {}; - __webpack_require__.r(C), - __webpack_require__.d(C, { transform: () => parameter_oneof_transform }); - var j = {}; - __webpack_require__.r(j), - __webpack_require__.d(j, { allErrors: () => Mn, lastError: () => Tn }); - var L = {}; - __webpack_require__.r(L), - __webpack_require__.d(L, { - SHOW: () => Fn, - UPDATE_FILTER: () => Ln, - UPDATE_LAYOUT: () => Dn, - UPDATE_MODE: () => Bn, - changeMode: () => changeMode, - show: () => actions_show, - updateFilter: () => updateFilter, - updateLayout: () => updateLayout - }); - var B = {}; - __webpack_require__.r(B), - __webpack_require__.d(B, { - current: () => current, - currentFilter: () => currentFilter, - isShown: () => isShown, - showSummary: () => $n, - whatMode: () => whatMode - }); - var $ = {}; - __webpack_require__.r($), - __webpack_require__.d($, { taggedOperations: () => taggedOperations }); - var V = {}; - __webpack_require__.r(V), - __webpack_require__.d(V, { - requestSnippetGenerator_curl_bash: () => requestSnippetGenerator_curl_bash, - requestSnippetGenerator_curl_cmd: () => requestSnippetGenerator_curl_cmd, - requestSnippetGenerator_curl_powershell: () => requestSnippetGenerator_curl_powershell - }); - var U = {}; - __webpack_require__.r(U), - __webpack_require__.d(U, { - getActiveLanguage: () => zn, - getDefaultExpanded: () => Wn, - getGenerators: () => Un, - getSnippetGenerators: () => getSnippetGenerators - }); - var z = {}; - __webpack_require__.r(z), - __webpack_require__.d(z, { - JsonSchemaArrayItemFile: () => JsonSchemaArrayItemFile, - JsonSchemaArrayItemText: () => JsonSchemaArrayItemText, - JsonSchemaForm: () => JsonSchemaForm, - JsonSchema_array: () => JsonSchema_array, - JsonSchema_boolean: () => JsonSchema_boolean, - JsonSchema_object: () => JsonSchema_object, - JsonSchema_string: () => JsonSchema_string - }); - var Y = {}; - __webpack_require__.r(Y), - __webpack_require__.d(Y, { - allowTryItOutFor: () => allowTryItOutFor, - basePath: () => Ks, - canExecuteScheme: () => canExecuteScheme, - consumes: () => $s, - consumesOptionsFor: () => consumesOptionsFor, - contentTypeValues: () => contentTypeValues, - currentProducesFor: () => currentProducesFor, - definitions: () => Ws, - externalDocs: () => Rs, - findDefinition: () => findDefinition, - getOAS3RequiredRequestBodyContentType: () => getOAS3RequiredRequestBodyContentType, - getParameter: () => getParameter, - hasHost: () => to, - host: () => Hs, - info: () => Ns, - isMediaTypeSchemaPropertiesEqual: () => isMediaTypeSchemaPropertiesEqual, - isOAS3: () => Ts, - lastError: () => ks, - mutatedRequestFor: () => mutatedRequestFor, - mutatedRequests: () => eo, - operationScheme: () => operationScheme, - operationWithMeta: () => operationWithMeta, - operations: () => qs, - operationsWithRootInherited: () => Gs, - operationsWithTags: () => Xs, - parameterInclusionSettingFor: () => parameterInclusionSettingFor, - parameterValues: () => parameterValues, - parameterWithMeta: () => parameterWithMeta, - parameterWithMetaByIdentity: () => parameterWithMetaByIdentity, - parametersIncludeIn: () => parametersIncludeIn, - parametersIncludeType: () => parametersIncludeType, - paths: () => Bs, - produces: () => Vs, - producesOptionsFor: () => producesOptionsFor, - requestFor: () => requestFor, - requests: () => Qs, - responseFor: () => responseFor, - responses: () => Zs, - schemes: () => Js, - security: () => Us, - securityDefinitions: () => zs, - semver: () => Ls, - spec: () => spec, - specJS: () => Is, - specJson: () => js, - specJsonWithResolvedSubtrees: () => Ms, - specResolved: () => Ps, - specResolvedSubtree: () => specResolvedSubtree, - specSource: () => As, - specStr: () => Os, - tagDetails: () => tagDetails, - taggedOperations: () => selectors_taggedOperations, - tags: () => Ys, - url: () => Cs, - validOperationMethods: () => Fs, - validateBeforeExecute: () => validateBeforeExecute, - validationErrors: () => validationErrors, - version: () => Ds - }); - var Z = {}; - __webpack_require__.r(Z), - __webpack_require__.d(Z, { - CLEAR_REQUEST: () => wo, - CLEAR_RESPONSE: () => Eo, - CLEAR_VALIDATE_PARAMS: () => So, - LOG_REQUEST: () => _o, - SET_MUTATED_REQUEST: () => bo, - SET_REQUEST: () => vo, - SET_RESPONSE: () => yo, - SET_SCHEME: () => Oo, - UPDATE_EMPTY_PARAM_INCLUSION: () => mo, - UPDATE_JSON: () => ho, - UPDATE_OPERATION_META_VALUE: () => xo, - UPDATE_PARAM: () => fo, - UPDATE_RESOLVED: () => ko, - UPDATE_RESOLVED_SUBTREE: () => Co, - UPDATE_SPEC: () => uo, - UPDATE_URL: () => po, - VALIDATE_PARAMS: () => go, - changeConsumesValue: () => changeConsumesValue, - changeParam: () => changeParam, - changeParamByIdentity: () => changeParamByIdentity, - changeProducesValue: () => changeProducesValue, - clearRequest: () => clearRequest, - clearResponse: () => clearResponse, - clearValidateParams: () => clearValidateParams, - execute: () => actions_execute, - executeRequest: () => executeRequest, - invalidateResolvedSubtreeCache: () => invalidateResolvedSubtreeCache, - logRequest: () => logRequest, - parseToJson: () => parseToJson, - requestResolvedSubtree: () => requestResolvedSubtree, - resolveSpec: () => resolveSpec, - setMutatedRequest: () => setMutatedRequest, - setRequest: () => setRequest, - setResponse: () => setResponse, - setScheme: () => setScheme, - updateEmptyParamInclusion: () => updateEmptyParamInclusion, - updateJsonSpec: () => updateJsonSpec, - updateResolved: () => updateResolved, - updateResolvedSubtree: () => updateResolvedSubtree, - updateSpec: () => updateSpec, - updateUrl: () => updateUrl, - validateParams: () => validateParams - }); - var ee = {}; - __webpack_require__.r(ee), - __webpack_require__.d(ee, { - executeRequest: () => wrap_actions_executeRequest, - updateJsonSpec: () => wrap_actions_updateJsonSpec, - updateSpec: () => wrap_actions_updateSpec, - validateParams: () => wrap_actions_validateParams - }); - var ie = {}; - __webpack_require__.r(ie), - __webpack_require__.d(ie, { - JsonPatchError: () => Ro, - _areEquals: () => _areEquals, - applyOperation: () => applyOperation, - applyPatch: () => applyPatch, - applyReducer: () => applyReducer, - deepClone: () => Do, - getValueByPointer: () => getValueByPointer, - validate: () => validate, - validator: () => validator - }); - var ae = {}; - __webpack_require__.r(ae), - __webpack_require__.d(ae, { - compare: () => compare, - generate: () => generate, - observe: () => observe, - unobserve: () => unobserve - }); - var le = {}; - __webpack_require__.r(le), - __webpack_require__.d(le, { - hasElementSourceMap: () => hasElementSourceMap, - includesClasses: () => includesClasses, - includesSymbols: () => includesSymbols, - isAnnotationElement: () => zu, - isArrayElement: () => qu, - isBooleanElement: () => Bu, - isCommentElement: () => Wu, - isElement: () => Nu, - isLinkElement: () => Vu, - isMemberElement: () => $u, - isNullElement: () => Lu, - isNumberElement: () => Du, - isObjectElement: () => Fu, - isParseResultElement: () => Ku, - isPrimitiveElement: () => isPrimitiveElement, - isRefElement: () => Uu, - isSourceMapElement: () => Hu, - isStringElement: () => Ru - }); - var ce = {}; - __webpack_require__.r(ce), - __webpack_require__.d(ce, { - isJSONReferenceElement: () => Nf, - isJSONSchemaElement: () => Tf, - isLinkDescriptionElement: () => Df, - isMediaElement: () => Rf - }); - var pe = {}; - __webpack_require__.r(pe), - __webpack_require__.d(pe, { - isBooleanJsonSchemaElement: () => isBooleanJsonSchemaElement, - isCallbackElement: () => Im, - isComponentsElement: () => Pm, - isContactElement: () => Mm, - isExampleElement: () => Tm, - isExternalDocumentationElement: () => Nm, - isHeaderElement: () => Rm, - isInfoElement: () => Dm, - isLicenseElement: () => Lm, - isLinkElement: () => Bm, - isMediaTypeElement: () => eg, - isOpenApi3_0Element: () => qm, - isOpenapiElement: () => Fm, - isOperationElement: () => $m, - isParameterElement: () => Vm, - isPathItemElement: () => Um, - isPathsElement: () => zm, - isReferenceElement: () => Wm, - isRequestBodyElement: () => Km, - isResponseElement: () => Hm, - isResponsesElement: () => Jm, - isSchemaElement: () => Gm, - isSecurityRequirementElement: () => Ym, - isSecuritySchemeElement: () => Xm, - isServerElement: () => Zm, - isServerVariableElement: () => Qm, - isServersElement: () => rg - }); - var de = {}; - __webpack_require__.r(de), - __webpack_require__.d(de, { - isBooleanJsonSchemaElement: () => predicates_isBooleanJsonSchemaElement, - isCallbackElement: () => T_, - isComponentsElement: () => N_, - isContactElement: () => R_, - isExampleElement: () => D_, - isExternalDocumentationElement: () => L_, - isHeaderElement: () => B_, - isInfoElement: () => F_, - isJsonSchemaDialectElement: () => q_, - isLicenseElement: () => $_, - isLinkElement: () => V_, - isMediaTypeElement: () => sE, - isOpenApi3_1Element: () => z_, - isOpenapiElement: () => U_, - isOperationElement: () => W_, - isParameterElement: () => K_, - isPathItemElement: () => H_, - isPathItemElementExternal: () => isPathItemElementExternal, - isPathsElement: () => J_, - isReferenceElement: () => G_, - isReferenceElementExternal: () => isReferenceElementExternal, - isRequestBodyElement: () => Y_, - isResponseElement: () => X_, - isResponsesElement: () => Z_, - isSchemaElement: () => Q_, - isSecurityRequirementElement: () => eE, - isSecuritySchemeElement: () => tE, - isServerElement: () => rE, - isServerVariableElement: () => nE - }); - var fe = {}; - __webpack_require__.r(fe), - __webpack_require__.d(fe, { - cookie: () => parameter_builders_cookie, - header: () => parameter_builders_header, - path: () => parameter_builders_path, - query: () => parameter_builders_query - }); - var ye = {}; - __webpack_require__.r(ye), - __webpack_require__.d(ye, { - Button: () => Button, - Col: () => Col, - Collapse: () => Collapse, - Container: () => Container, - Input: () => Input, - Link: () => layout_utils_Link, - Row: () => Row, - Select: () => Select, - TextArea: () => TextArea - }); - var be = {}; - __webpack_require__.r(be), - __webpack_require__.d(be, { - basePath: () => KO, - consumes: () => HO, - definitions: () => VO, - findDefinition: () => $O, - hasHost: () => UO, - host: () => WO, - produces: () => JO, - schemes: () => GO, - securityDefinitions: () => zO, - validOperationMethods: () => wrap_selectors_validOperationMethods - }); - var _e = {}; - __webpack_require__.r(_e), __webpack_require__.d(_e, { definitionsToAuthorize: () => YO }); - var we = {}; - __webpack_require__.r(we), - __webpack_require__.d(we, { - callbacksOperations: () => QO, - findSchema: () => findSchema, - isOAS3: () => selectors_isOAS3, - isOAS30: () => selectors_isOAS30, - isSwagger2: () => selectors_isSwagger2, - servers: () => ZO - }); - var Se = {}; - __webpack_require__.r(Se), - __webpack_require__.d(Se, { - CLEAR_REQUEST_BODY_VALIDATE_ERROR: () => bA, - CLEAR_REQUEST_BODY_VALUE: () => _A, - SET_REQUEST_BODY_VALIDATE_ERROR: () => vA, - UPDATE_ACTIVE_EXAMPLES_MEMBER: () => fA, - UPDATE_REQUEST_BODY_INCLUSION: () => dA, - UPDATE_REQUEST_BODY_VALUE: () => pA, - UPDATE_REQUEST_BODY_VALUE_RETAIN_FLAG: () => hA, - UPDATE_REQUEST_CONTENT_TYPE: () => mA, - UPDATE_RESPONSE_CONTENT_TYPE: () => gA, - UPDATE_SELECTED_SERVER: () => uA, - UPDATE_SERVER_VARIABLE_VALUE: () => yA, - clearRequestBodyValidateError: () => clearRequestBodyValidateError, - clearRequestBodyValue: () => clearRequestBodyValue, - initRequestBodyValidateError: () => initRequestBodyValidateError, - setActiveExamplesMember: () => setActiveExamplesMember, - setRequestBodyInclusion: () => setRequestBodyInclusion, - setRequestBodyValidateError: () => setRequestBodyValidateError, - setRequestBodyValue: () => setRequestBodyValue, - setRequestContentType: () => setRequestContentType, - setResponseContentType: () => setResponseContentType, - setRetainRequestBodyValueFlag: () => setRetainRequestBodyValueFlag, - setSelectedServer: () => setSelectedServer, - setServerVariableValue: () => setServerVariableValue - }); - var xe = {}; - __webpack_require__.r(xe), - __webpack_require__.d(xe, { - activeExamplesMember: () => jA, - hasUserEditedBody: () => CA, - requestBodyErrors: () => AA, - requestBodyInclusionSetting: () => OA, - requestBodyValue: () => xA, - requestContentType: () => IA, - responseContentType: () => PA, - selectDefaultRequestBodyValue: () => selectDefaultRequestBodyValue, - selectedServer: () => SA, - serverEffectiveValue: () => NA, - serverVariableValue: () => MA, - serverVariables: () => TA, - shouldRetainRequestBodyValue: () => kA, - validOperationMethods: () => DA, - validateBeforeExecute: () => RA, - validateShallowRequired: () => validateShallowRequired - }); - var Pe = __webpack_require__(96540); - function formatProdErrorMessage(s) { - return `Minified Redux error #${s}; visit https://redux.js.org/Errors?code=${s} for the full message or use the non-minified dev environment for full errors. `; - } - var Te = (() => ('function' == typeof Symbol && Symbol.observable) || '@@observable')(), - randomString = () => Math.random().toString(36).substring(7).split('').join('.'), - Re = { - INIT: `@@redux/INIT${randomString()}`, - REPLACE: `@@redux/REPLACE${randomString()}`, - PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` - }; - function isPlainObject(s) { - if ('object' != typeof s || null === s) return !1; - let o = s; - for (; null !== Object.getPrototypeOf(o); ) o = Object.getPrototypeOf(o); - return Object.getPrototypeOf(s) === o || null === Object.getPrototypeOf(s); - } - function createStore(s, o, i) { - if ('function' != typeof s) throw new Error(formatProdErrorMessage(2)); - if ( - ('function' == typeof o && 'function' == typeof i) || - ('function' == typeof i && 'function' == typeof arguments[3]) - ) - throw new Error(formatProdErrorMessage(0)); - if (('function' == typeof o && void 0 === i && ((i = o), (o = void 0)), void 0 !== i)) { - if ('function' != typeof i) throw new Error(formatProdErrorMessage(1)); - return i(createStore)(s, o); - } - let u = s, - _ = o, - w = new Map(), - x = w, - C = 0, - j = !1; - function ensureCanMutateNextListeners() { - x === w && - ((x = new Map()), - w.forEach((s, o) => { - x.set(o, s); - })); - } - function getState() { - if (j) throw new Error(formatProdErrorMessage(3)); - return _; - } - function subscribe(s) { - if ('function' != typeof s) throw new Error(formatProdErrorMessage(4)); - if (j) throw new Error(formatProdErrorMessage(5)); - let o = !0; - ensureCanMutateNextListeners(); - const i = C++; - return ( - x.set(i, s), - function unsubscribe() { - if (o) { - if (j) throw new Error(formatProdErrorMessage(6)); - (o = !1), ensureCanMutateNextListeners(), x.delete(i), (w = null); - } - } - ); - } - function dispatch(s) { - if (!isPlainObject(s)) throw new Error(formatProdErrorMessage(7)); - if (void 0 === s.type) throw new Error(formatProdErrorMessage(8)); - if ('string' != typeof s.type) throw new Error(formatProdErrorMessage(17)); - if (j) throw new Error(formatProdErrorMessage(9)); - try { - (j = !0), (_ = u(_, s)); - } finally { - j = !1; - } - return ( - (w = x).forEach((s) => { - s(); - }), - s - ); - } - dispatch({ type: Re.INIT }); - return { - dispatch, - subscribe, - getState, - replaceReducer: function replaceReducer(s) { - if ('function' != typeof s) throw new Error(formatProdErrorMessage(10)); - (u = s), dispatch({ type: Re.REPLACE }); - }, - [Te]: function observable() { - const s = subscribe; - return { - subscribe(o) { - if ('object' != typeof o || null === o) - throw new Error(formatProdErrorMessage(11)); - function observeState() { - const s = o; - s.next && s.next(getState()); - } - observeState(); - return { unsubscribe: s(observeState) }; - }, - [Te]() { - return this; - } - }; - } - }; - } - function bindActionCreator(s, o) { - return function (...i) { - return o(s.apply(this, i)); - }; - } - function compose(...s) { - return 0 === s.length - ? (s) => s - : 1 === s.length - ? s[0] - : s.reduce( - (s, o) => - (...i) => - s(o(...i)) - ); - } - var qe = __webpack_require__(9404), - $e = __webpack_require__.n(qe), - ze = __webpack_require__(81919), - We = __webpack_require__.n(ze), - He = __webpack_require__(89593), - Ye = __webpack_require__(20334), - Xe = __webpack_require__(55364), - Qe = __webpack_require__.n(Xe); - const et = 'err_new_thrown_err', - tt = 'err_new_thrown_err_batch', - rt = 'err_new_spec_err', - nt = 'err_new_spec_err_batch', - st = 'err_new_auth_err', - ot = 'err_clear', - it = 'err_clear_by'; - function newThrownErr(s) { - return { type: et, payload: (0, Ye.serializeError)(s) }; - } - function newThrownErrBatch(s) { - return { type: tt, payload: s }; - } - function newSpecErr(s) { - return { type: rt, payload: s }; - } - function newSpecErrBatch(s) { - return { type: nt, payload: s }; - } - function newAuthErr(s) { - return { type: st, payload: s }; - } - function clear(s = {}) { - return { type: ot, payload: s }; - } - function clearBy(s = () => !0) { - return { type: it, payload: s }; - } - const at = (function makeWindow() { - var s = { - location: {}, - history: {}, - open: () => {}, - close: () => {}, - File: function () {}, - FormData: function () {} - }; - if ('undefined' == typeof window) return s; - try { - s = window; - for (var o of ['File', 'Blob', 'FormData']) o in window && (s[o] = window[o]); - } catch (s) { - console.error(s); - } - return s; - })(); - var lt = __webpack_require__(16750), - ct = (__webpack_require__(84058), __webpack_require__(55808), __webpack_require__(50104)), - ut = __webpack_require__.n(ct), - pt = __webpack_require__(7309), - ht = __webpack_require__.n(pt), - dt = __webpack_require__(42426), - mt = __webpack_require__.n(dt), - gt = __webpack_require__(75288), - yt = __webpack_require__.n(gt), - vt = __webpack_require__(1882), - bt = __webpack_require__.n(vt), - _t = __webpack_require__(2205), - Et = __webpack_require__.n(_t), - wt = __webpack_require__(53209), - St = __webpack_require__.n(wt), - xt = __webpack_require__(62802), - kt = __webpack_require__.n(xt); - const Ct = $e().Set.of( - 'type', - 'format', - 'items', - 'default', - 'maximum', - 'exclusiveMaximum', - 'minimum', - 'exclusiveMinimum', - 'maxLength', - 'minLength', - 'pattern', - 'maxItems', - 'minItems', - 'uniqueItems', - 'enum', - 'multipleOf' - ); - function getParameterSchema(s, { isOAS3: o } = {}) { - if (!$e().Map.isMap(s)) return { schema: $e().Map(), parameterContentMediaType: null }; - if (!o) - return 'body' === s.get('in') - ? { schema: s.get('schema', $e().Map()), parameterContentMediaType: null } - : { schema: s.filter((s, o) => Ct.includes(o)), parameterContentMediaType: null }; - if (s.get('content')) { - const o = s.get('content', $e().Map({})).keySeq().first(); - return { - schema: s.getIn(['content', o, 'schema'], $e().Map()), - parameterContentMediaType: o - }; - } - return { - schema: s.get('schema') ? s.get('schema', $e().Map()) : $e().Map(), - parameterContentMediaType: null - }; - } - var Ot = __webpack_require__(48287).Buffer; - const At = 'default', - isImmutable = (s) => $e().Iterable.isIterable(s); - function objectify(s) { - return isObject(s) ? (isImmutable(s) ? s.toJS() : s) : {}; - } - function fromJSOrdered(s) { - if (isImmutable(s)) return s; - if (s instanceof at.File) return s; - if (!isObject(s)) return s; - if (Array.isArray(s)) return $e().Seq(s).map(fromJSOrdered).toList(); - if (bt()(s.entries)) { - const o = (function createObjWithHashedKeys(s) { - if (!bt()(s.entries)) return s; - const o = {}, - i = '_**[]', - u = {}; - for (let _ of s.entries()) - if (o[_[0]] || (u[_[0]] && u[_[0]].containsMultiple)) { - if (!u[_[0]]) { - (u[_[0]] = { containsMultiple: !0, length: 1 }), - (o[`${_[0]}${i}${u[_[0]].length}`] = o[_[0]]), - delete o[_[0]]; - } - (u[_[0]].length += 1), (o[`${_[0]}${i}${u[_[0]].length}`] = _[1]); - } else o[_[0]] = _[1]; - return o; - })(s); - return $e().OrderedMap(o).map(fromJSOrdered); - } - return $e().OrderedMap(s).map(fromJSOrdered); - } - function normalizeArray(s) { - return Array.isArray(s) ? s : [s]; - } - function isFn(s) { - return 'function' == typeof s; - } - function isObject(s) { - return !!s && 'object' == typeof s; - } - function isFunc(s) { - return 'function' == typeof s; - } - function isArray(s) { - return Array.isArray(s); - } - const jt = ut(); - function objMap(s, o) { - return Object.keys(s).reduce((i, u) => ((i[u] = o(s[u], u)), i), {}); - } - function objReduce(s, o) { - return Object.keys(s).reduce((i, u) => { - let _ = o(s[u], u); - return _ && 'object' == typeof _ && Object.assign(i, _), i; - }, {}); - } - function systemThunkMiddleware(s) { - return ({ dispatch: o, getState: i }) => - (o) => - (i) => - 'function' == typeof i ? i(s()) : o(i); - } - function validateValueBySchema(s, o, i, u, _) { - if (!o) return []; - let w = [], - x = o.get('nullable'), - C = o.get('required'), - j = o.get('maximum'), - L = o.get('minimum'), - B = o.get('type'), - $ = o.get('format'), - V = o.get('maxLength'), - U = o.get('minLength'), - z = o.get('uniqueItems'), - Y = o.get('maxItems'), - Z = o.get('minItems'), - ee = o.get('pattern'); - const ie = i || !0 === C, - ae = null != s, - le = ie || (ae && 'array' === B) || !(!ie && !ae), - ce = x && null === s; - if (ie && !ae && !ce && !u && !B) return w.push('Required field is not provided'), w; - if (ce || !B || !le) return []; - let pe = 'string' === B && s, - de = 'array' === B && Array.isArray(s) && s.length, - fe = 'array' === B && $e().List.isList(s) && s.count(); - const ye = [ - pe, - de, - fe, - 'array' === B && 'string' == typeof s && s, - 'file' === B && s instanceof at.File, - 'boolean' === B && (s || !1 === s), - 'number' === B && (s || 0 === s), - 'integer' === B && (s || 0 === s), - 'object' === B && 'object' == typeof s && null !== s, - 'object' === B && 'string' == typeof s && s - ].some((s) => !!s); - if (ie && !ye && !u) return w.push('Required field is not provided'), w; - if ('object' === B && (null === _ || 'application/json' === _)) { - let i = s; - if ('string' == typeof s) - try { - i = JSON.parse(s); - } catch (s) { - return w.push('Parameter string value must be valid JSON'), w; - } - o && - o.has('required') && - isFunc(C.isList) && - C.isList() && - C.forEach((s) => { - void 0 === i[s] && w.push({ propKey: s, error: 'Required property not found' }); - }), - o && - o.has('properties') && - o.get('properties').forEach((s, o) => { - const x = validateValueBySchema(i[o], s, !1, u, _); - w.push(...x.map((s) => ({ propKey: o, error: s }))); - }); - } - if (ee) { - let o = ((s, o) => { - if (!new RegExp(o).test(s)) return 'Value must follow pattern ' + o; - })(s, ee); - o && w.push(o); - } - if (Z && 'array' === B) { - let o = ((s, o) => { - if ((!s && o >= 1) || (s && s.length < o)) - return `Array must contain at least ${o} item${1 === o ? '' : 's'}`; - })(s, Z); - o && w.push(o); - } - if (Y && 'array' === B) { - let o = ((s, o) => { - if (s && s.length > o) - return `Array must not contain more then ${o} item${1 === o ? '' : 's'}`; - })(s, Y); - o && w.push({ needRemove: !0, error: o }); - } - if (z && 'array' === B) { - let o = ((s, o) => { - if (s && ('true' === o || !0 === o)) { - const o = (0, qe.fromJS)(s), - i = o.toSet(); - if (s.length > i.size) { - let s = (0, qe.Set)(); - if ( - (o.forEach((i, u) => { - o.filter((s) => (isFunc(s.equals) ? s.equals(i) : s === i)).size > 1 && - (s = s.add(u)); - }), - 0 !== s.size) - ) - return s.map((s) => ({ index: s, error: 'No duplicates allowed.' })).toArray(); - } - } - })(s, z); - o && w.push(...o); - } - if (V || 0 === V) { - let o = ((s, o) => { - if (s.length > o) - return `Value must be no longer than ${o} character${1 !== o ? 's' : ''}`; - })(s, V); - o && w.push(o); - } - if (U) { - let o = ((s, o) => { - if (s.length < o) return `Value must be at least ${o} character${1 !== o ? 's' : ''}`; - })(s, U); - o && w.push(o); - } - if (j || 0 === j) { - let o = ((s, o) => { - if (s > o) return `Value must be less than ${o}`; - })(s, j); - o && w.push(o); - } - if (L || 0 === L) { - let o = ((s, o) => { - if (s < o) return `Value must be greater than ${o}`; - })(s, L); - o && w.push(o); - } - if ('string' === B) { - let o; - if ( - ((o = - 'date-time' === $ - ? ((s) => { - if (isNaN(Date.parse(s))) return 'Value must be a DateTime'; - })(s) - : 'uuid' === $ - ? ((s) => { - if ( - ((s = s.toString().toLowerCase()), - !/^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/.test( - s - )) - ) - return 'Value must be a Guid'; - })(s) - : ((s) => { - if (s && 'string' != typeof s) return 'Value must be a string'; - })(s)), - !o) - ) - return w; - w.push(o); - } else if ('boolean' === B) { - let o = ((s) => { - if ('true' !== s && 'false' !== s && !0 !== s && !1 !== s) - return 'Value must be a boolean'; - })(s); - if (!o) return w; - w.push(o); - } else if ('number' === B) { - let o = ((s) => { - if (!/^-?\d+(\.?\d+)?$/.test(s)) return 'Value must be a number'; - })(s); - if (!o) return w; - w.push(o); - } else if ('integer' === B) { - let o = ((s) => { - if (!/^-?\d+$/.test(s)) return 'Value must be an integer'; - })(s); - if (!o) return w; - w.push(o); - } else if ('array' === B) { - if (!de && !fe) return w; - s && - s.forEach((s, i) => { - const x = validateValueBySchema(s, o.get('items'), !1, u, _); - w.push(...x.map((s) => ({ index: i, error: s }))); - }); - } else if ('file' === B) { - let o = ((s) => { - if (s && !(s instanceof at.File)) return 'Value must be a file'; - })(s); - if (!o) return w; - w.push(o); - } - return w; - } - const utils_btoa = (s) => { - let o; - return (o = s instanceof Ot ? s : Ot.from(s.toString(), 'utf-8')), o.toString('base64'); - }, - It = { - operationsSorter: { - alpha: (s, o) => s.get('path').localeCompare(o.get('path')), - method: (s, o) => s.get('method').localeCompare(o.get('method')) - }, - tagsSorter: { alpha: (s, o) => s.localeCompare(o) } - }, - buildFormData = (s) => { - let o = []; - for (let i in s) { - let u = s[i]; - void 0 !== u && - '' !== u && - o.push([i, '=', encodeURIComponent(u).replace(/%20/g, '+')].join('')); - } - return o.join('&'); - }, - shallowEqualKeys = (s, o, i) => !!ht()(i, (i) => yt()(s[i], o[i])); - function sanitizeUrl(s) { - return 'string' != typeof s || '' === s ? '' : (0, lt.J)(s); - } - function requiresValidationURL(s) { - return !( - !s || - s.indexOf('localhost') >= 0 || - s.indexOf('127.0.0.1') >= 0 || - 'none' === s - ); - } - const createDeepLinkPath = (s) => - 'string' == typeof s || s instanceof String ? s.trim().replace(/\s/g, '%20') : '', - escapeDeepLinkPath = (s) => Et()(createDeepLinkPath(s).replace(/%20/g, '_')), - getExtensions = (s) => s.filter((s, o) => /^x-/.test(o)), - getCommonExtensions = (s) => - s.filter((s, o) => /^pattern|maxLength|minLength|maximum|minimum/.test(o)); - function deeplyStripKey(s, o, i = () => !0) { - if ('object' != typeof s || Array.isArray(s) || null === s || !o) return s; - const u = Object.assign({}, s); - return ( - Object.keys(u).forEach((s) => { - s === o && i(u[s], s) ? delete u[s] : (u[s] = deeplyStripKey(u[s], o, i)); - }), - u - ); - } - function stringify(s) { - if ('string' == typeof s) return s; - if ((s && s.toJS && (s = s.toJS()), 'object' == typeof s && null !== s)) - try { - return JSON.stringify(s, null, 2); - } catch (o) { - return String(s); - } - return null == s ? '' : s.toString(); - } - function paramToIdentifier(s, { returnAll: o = !1, allowHashes: i = !0 } = {}) { - if (!$e().Map.isMap(s)) - throw new Error('paramToIdentifier: received a non-Im.Map parameter as input'); - const u = s.get('name'), - _ = s.get('in'); - let w = []; - return ( - s && s.hashCode && _ && u && i && w.push(`${_}.${u}.hash-${s.hashCode()}`), - _ && u && w.push(`${_}.${u}`), - w.push(u), - o ? w : w[0] || '' - ); - } - function paramToValue(s, o) { - return paramToIdentifier(s, { returnAll: !0 }) - .map((s) => o[s]) - .filter((s) => void 0 !== s)[0]; - } - function b64toB64UrlEncoded(s) { - return s.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); - } - const isEmptyValue = (s) => !s || !(!isImmutable(s) || !s.isEmpty()), - idFn = (s) => s; - function createStoreWithMiddleware(s, o, i) { - let u = [systemThunkMiddleware(i)]; - return createStore( - s, - o, - (at.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose)( - (function applyMiddleware(...s) { - return (o) => (i, u) => { - const _ = o(i, u); - let dispatch = () => { - throw new Error(formatProdErrorMessage(15)); - }; - const w = { getState: _.getState, dispatch: (s, ...o) => dispatch(s, ...o) }, - x = s.map((s) => s(w)); - return (dispatch = compose(...x)(_.dispatch)), { ..._, dispatch }; - }; - })(...u) - ) - ); - } - class Store { - constructor(s = {}) { - We()( - this, - { - state: {}, - plugins: [], - system: { configs: {}, fn: {}, components: {}, rootInjects: {}, statePlugins: {} }, - boundSystem: {}, - toolbox: {} - }, - s - ), - (this.getSystem = this._getSystem.bind(this)), - (this.store = (function configureStore(s, o, i) { - return createStoreWithMiddleware(s, o, i); - })(idFn, (0, qe.fromJS)(this.state), this.getSystem)), - this.buildSystem(!1), - this.register(this.plugins); - } - getStore() { - return this.store; - } - register(s, o = !0) { - var i = combinePlugins(s, this.getSystem()); - systemExtend(this.system, i), o && this.buildSystem(); - callAfterLoad.call(this.system, s, this.getSystem()) && this.buildSystem(); - } - buildSystem(s = !0) { - let o = this.getStore().dispatch, - i = this.getStore().getState; - (this.boundSystem = Object.assign( - {}, - this.getRootInjects(), - this.getWrappedAndBoundActions(o), - this.getWrappedAndBoundSelectors(i, this.getSystem), - this.getStateThunks(i), - this.getFn(), - this.getConfigs() - )), - s && this.rebuildReducer(); - } - _getSystem() { - return this.boundSystem; - } - getRootInjects() { - return Object.assign( - { - getSystem: this.getSystem, - getStore: this.getStore.bind(this), - getComponents: this.getComponents.bind(this), - getState: this.getStore().getState, - getConfigs: this._getConfigs.bind(this), - Im: $e(), - React: Pe - }, - this.system.rootInjects || {} - ); - } - _getConfigs() { - return this.system.configs; - } - getConfigs() { - return { configs: this.system.configs }; - } - setConfigs(s) { - this.system.configs = s; - } - rebuildReducer() { - this.store.replaceReducer( - (function buildReducer(s) { - return (function allReducers(s) { - let o = Object.keys(s).reduce( - (o, i) => ( - (o[i] = (function makeReducer(s) { - return (o = new qe.Map(), i) => { - if (!s) return o; - let u = s[i.type]; - if (u) { - const s = wrapWithTryCatch(u)(o, i); - return null === s ? o : s; - } - return o; - }; - })(s[i])), - o - ), - {} - ); - if (!Object.keys(o).length) return idFn; - return (0, He.H)(o); - })(objMap(s, (s) => s.reducers)); - })(this.system.statePlugins) - ); - } - getType(s) { - let o = s[0].toUpperCase() + s.slice(1); - return objReduce(this.system.statePlugins, (i, u) => { - let _ = i[s]; - if (_) return { [u + o]: _ }; - }); - } - getSelectors() { - return this.getType('selectors'); - } - getActions() { - return objMap(this.getType('actions'), (s) => - objReduce(s, (s, o) => { - if (isFn(s)) return { [o]: s }; - }) - ); - } - getWrappedAndBoundActions(s) { - return objMap(this.getBoundActions(s), (s, o) => { - let i = this.system.statePlugins[o.slice(0, -7)].wrapActions; - return i - ? objMap(s, (s, o) => { - let u = i[o]; - return u - ? (Array.isArray(u) || (u = [u]), - u.reduce((s, o) => { - let newAction = (...i) => o(s, this.getSystem())(...i); - if (!isFn(newAction)) - throw new TypeError( - 'wrapActions needs to return a function that returns a new function (ie the wrapped action)' - ); - return wrapWithTryCatch(newAction); - }, s || Function.prototype)) - : s; - }) - : s; - }); - } - getWrappedAndBoundSelectors(s, o) { - return objMap(this.getBoundSelectors(s, o), (o, i) => { - let u = [i.slice(0, -9)], - _ = this.system.statePlugins[u].wrapSelectors; - return _ - ? objMap(o, (o, i) => { - let w = _[i]; - return w - ? (Array.isArray(w) || (w = [w]), - w.reduce((o, i) => { - let wrappedSelector = (..._) => - i(o, this.getSystem())(s().getIn(u), ..._); - if (!isFn(wrappedSelector)) - throw new TypeError( - 'wrapSelector needs to return a function that returns a new function (ie the wrapped action)' - ); - return wrappedSelector; - }, o || Function.prototype)) - : o; - }) - : o; - }); - } - getStates(s) { - return Object.keys(this.system.statePlugins).reduce( - (o, i) => ((o[i] = s.get(i)), o), - {} - ); - } - getStateThunks(s) { - return Object.keys(this.system.statePlugins).reduce( - (o, i) => ((o[i] = () => s().get(i)), o), - {} - ); - } - getFn() { - return { fn: this.system.fn }; - } - getComponents(s) { - const o = this.system.components[s]; - return Array.isArray(o) - ? o.reduce((s, o) => o(s, this.getSystem())) - : void 0 !== s - ? this.system.components[s] - : this.system.components; - } - getBoundSelectors(s, o) { - return objMap(this.getSelectors(), (i, u) => { - let _ = [u.slice(0, -9)]; - return objMap(i, (i) => (...u) => { - let w = wrapWithTryCatch(i).apply(null, [s().getIn(_), ...u]); - return 'function' == typeof w && (w = wrapWithTryCatch(w)(o())), w; - }); - }); - } - getBoundActions(s) { - s = s || this.getStore().dispatch; - const o = this.getActions(), - process = (s) => - 'function' != typeof s - ? objMap(s, (s) => process(s)) - : (...o) => { - var i = null; - try { - i = s(...o); - } catch (s) { - i = { type: et, error: !0, payload: (0, Ye.serializeError)(s) }; - } finally { - return i; - } - }; - return objMap(o, (o) => - (function bindActionCreators(s, o) { - if ('function' == typeof s) return bindActionCreator(s, o); - if ('object' != typeof s || null === s) throw new Error(formatProdErrorMessage(16)); - const i = {}; - for (const u in s) { - const _ = s[u]; - 'function' == typeof _ && (i[u] = bindActionCreator(_, o)); - } - return i; - })(process(o), s) - ); - } - getMapStateToProps() { - return () => Object.assign({}, this.getSystem()); - } - getMapDispatchToProps(s) { - return (o) => We()({}, this.getWrappedAndBoundActions(o), this.getFn(), s); - } - } - function combinePlugins(s, o) { - return isObject(s) && !isArray(s) - ? Qe()({}, s) - : isFunc(s) - ? combinePlugins(s(o), o) - : isArray(s) - ? s - .map((s) => combinePlugins(s, o)) - .reduce(systemExtend, { components: o.getComponents() }) - : {}; - } - function callAfterLoad(s, o, { hasLoaded: i } = {}) { - let u = i; - return ( - isObject(s) && - !isArray(s) && - 'function' == typeof s.afterLoad && - ((u = !0), wrapWithTryCatch(s.afterLoad).call(this, o)), - isFunc(s) - ? callAfterLoad.call(this, s(o), o, { hasLoaded: u }) - : isArray(s) - ? s.map((s) => callAfterLoad.call(this, s, o, { hasLoaded: u })) - : u - ); - } - function systemExtend(s = {}, o = {}) { - if (!isObject(s)) return {}; - if (!isObject(o)) return s; - o.wrapComponents && - (objMap(o.wrapComponents, (i, u) => { - const _ = s.components && s.components[u]; - _ && Array.isArray(_) - ? ((s.components[u] = _.concat([i])), delete o.wrapComponents[u]) - : _ && ((s.components[u] = [_, i]), delete o.wrapComponents[u]); - }), - Object.keys(o.wrapComponents).length || delete o.wrapComponents); - const { statePlugins: i } = s; - if (isObject(i)) - for (let s in i) { - const u = i[s]; - if (!isObject(u)) continue; - const { wrapActions: _, wrapSelectors: w } = u; - if (isObject(_)) - for (let i in _) { - let u = _[i]; - Array.isArray(u) || ((u = [u]), (_[i] = u)), - o && - o.statePlugins && - o.statePlugins[s] && - o.statePlugins[s].wrapActions && - o.statePlugins[s].wrapActions[i] && - (o.statePlugins[s].wrapActions[i] = _[i].concat( - o.statePlugins[s].wrapActions[i] - )); - } - if (isObject(w)) - for (let i in w) { - let u = w[i]; - Array.isArray(u) || ((u = [u]), (w[i] = u)), - o && - o.statePlugins && - o.statePlugins[s] && - o.statePlugins[s].wrapSelectors && - o.statePlugins[s].wrapSelectors[i] && - (o.statePlugins[s].wrapSelectors[i] = w[i].concat( - o.statePlugins[s].wrapSelectors[i] - )); - } - } - return We()(s, o); - } - function wrapWithTryCatch(s, { logErrors: o = !0 } = {}) { - return 'function' != typeof s - ? s - : function (...i) { - try { - return s.call(this, ...i); - } catch (s) { - return o && console.error(s), null; - } - }; - } - var Pt = __webpack_require__(61160), - Mt = __webpack_require__.n(Pt); - const Tt = 'show_popup', - Nt = 'authorize', - Rt = 'logout', - Dt = 'pre_authorize_oauth2', - Lt = 'authorize_oauth2', - Bt = 'validate', - Ft = 'configure_auth', - qt = 'restore_authorization'; - function showDefinitions(s) { - return { type: Tt, payload: s }; - } - function authorize(s) { - return { type: Nt, payload: s }; - } - const authorizeWithPersistOption = - (s) => - ({ authActions: o }) => { - o.authorize(s), o.persistAuthorizationIfNeeded(); - }; - function logout(s) { - return { type: Rt, payload: s }; - } - const logoutWithPersistOption = - (s) => - ({ authActions: o }) => { - o.logout(s), o.persistAuthorizationIfNeeded(); - }, - preAuthorizeImplicit = - (s) => - ({ authActions: o, errActions: i }) => { - let { auth: u, token: _, isValid: w } = s, - { schema: x, name: C } = u, - j = x.get('flow'); - delete at.swaggerUIRedirectOauth2, - 'accessCode' === j || - w || - i.newAuthErr({ - authId: C, - source: 'auth', - level: 'warning', - message: - "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server" - }), - _.error - ? i.newAuthErr({ - authId: C, - source: 'auth', - level: 'error', - message: JSON.stringify(_) - }) - : o.authorizeOauth2WithPersistOption({ auth: u, token: _ }); - }; - function authorizeOauth2(s) { - return { type: Lt, payload: s }; - } - const authorizeOauth2WithPersistOption = - (s) => - ({ authActions: o }) => { - o.authorizeOauth2(s), o.persistAuthorizationIfNeeded(); - }, - authorizePassword = - (s) => - ({ authActions: o }) => { - let { - schema: i, - name: u, - username: _, - password: w, - passwordType: x, - clientId: C, - clientSecret: j - } = s, - L = { grant_type: 'password', scope: s.scopes.join(' '), username: _, password: w }, - B = {}; - switch (x) { - case 'request-body': - !(function setClientIdAndSecret(s, o, i) { - o && Object.assign(s, { client_id: o }); - i && Object.assign(s, { client_secret: i }); - })(L, C, j); - break; - case 'basic': - B.Authorization = 'Basic ' + utils_btoa(C + ':' + j); - break; - default: - console.warn( - `Warning: invalid passwordType ${x} was passed, not including client id and secret` - ); - } - return o.authorizeRequest({ - body: buildFormData(L), - url: i.get('tokenUrl'), - name: u, - headers: B, - query: {}, - auth: s - }); - }; - const authorizeApplication = - (s) => - ({ authActions: o }) => { - let { schema: i, scopes: u, name: _, clientId: w, clientSecret: x } = s, - C = { Authorization: 'Basic ' + utils_btoa(w + ':' + x) }, - j = { grant_type: 'client_credentials', scope: u.join(' ') }; - return o.authorizeRequest({ - body: buildFormData(j), - name: _, - url: i.get('tokenUrl'), - auth: s, - headers: C - }); - }, - authorizeAccessCodeWithFormParams = - ({ auth: s, redirectUrl: o }) => - ({ authActions: i }) => { - let { schema: u, name: _, clientId: w, clientSecret: x, codeVerifier: C } = s, - j = { - grant_type: 'authorization_code', - code: s.code, - client_id: w, - client_secret: x, - redirect_uri: o, - code_verifier: C - }; - return i.authorizeRequest({ - body: buildFormData(j), - name: _, - url: u.get('tokenUrl'), - auth: s - }); - }, - authorizeAccessCodeWithBasicAuthentication = - ({ auth: s, redirectUrl: o }) => - ({ authActions: i }) => { - let { schema: u, name: _, clientId: w, clientSecret: x, codeVerifier: C } = s, - j = { Authorization: 'Basic ' + utils_btoa(w + ':' + x) }, - L = { - grant_type: 'authorization_code', - code: s.code, - client_id: w, - redirect_uri: o, - code_verifier: C - }; - return i.authorizeRequest({ - body: buildFormData(L), - name: _, - url: u.get('tokenUrl'), - auth: s, - headers: j - }); - }, - authorizeRequest = - (s) => - ({ - fn: o, - getConfigs: i, - authActions: u, - errActions: _, - oas3Selectors: w, - specSelectors: x, - authSelectors: C - }) => { - let j, - { body: L, query: B = {}, headers: $ = {}, name: V, url: U, auth: z } = s, - { additionalQueryStringParams: Y } = C.getConfigs() || {}; - if (x.isOAS3()) { - let s = w.serverEffectiveValue(w.selectedServer()); - j = Mt()(U, s, !0); - } else j = Mt()(U, x.url(), !0); - 'object' == typeof Y && (j.query = Object.assign({}, j.query, Y)); - const Z = j.toString(); - let ee = Object.assign( - { - Accept: 'application/json, text/plain, */*', - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest' - }, - $ - ); - o.fetch({ - url: Z, - method: 'post', - headers: ee, - query: B, - body: L, - requestInterceptor: i().requestInterceptor, - responseInterceptor: i().responseInterceptor - }) - .then(function (s) { - let o = JSON.parse(s.data), - i = o && (o.error || ''), - w = o && (o.parseError || ''); - s.ok - ? i || w - ? _.newAuthErr({ - authId: V, - level: 'error', - source: 'auth', - message: JSON.stringify(o) - }) - : u.authorizeOauth2WithPersistOption({ auth: z, token: o }) - : _.newAuthErr({ - authId: V, - level: 'error', - source: 'auth', - message: s.statusText - }); - }) - .catch((s) => { - let o = new Error(s).message; - if (s.response && s.response.data) { - const i = s.response.data; - try { - const s = 'string' == typeof i ? JSON.parse(i) : i; - s.error && (o += `, error: ${s.error}`), - s.error_description && (o += `, description: ${s.error_description}`); - } catch (s) {} - } - _.newAuthErr({ authId: V, level: 'error', source: 'auth', message: o }); - }); - }; - function configureAuth(s) { - return { type: Ft, payload: s }; - } - function restoreAuthorization(s) { - return { type: qt, payload: s }; - } - const persistAuthorizationIfNeeded = - () => - ({ authSelectors: s, getConfigs: o }) => { - if (!o().persistAuthorization) return; - const i = s.authorized().toJS(); - localStorage.setItem('authorized', JSON.stringify(i)); - }, - authPopup = (s, o) => () => { - (at.swaggerUIRedirectOauth2 = o), at.open(s); - }, - $t = { - [Tt]: (s, { payload: o }) => s.set('showDefinitions', o), - [Nt]: (s, { payload: o }) => { - let i = (0, qe.fromJS)(o), - u = s.get('authorized') || (0, qe.Map)(); - return ( - i.entrySeq().forEach(([o, i]) => { - if (!isFunc(i.getIn)) return s.set('authorized', u); - let _ = i.getIn(['schema', 'type']); - if ('apiKey' === _ || 'http' === _) u = u.set(o, i); - else if ('basic' === _) { - let s = i.getIn(['value', 'username']), - _ = i.getIn(['value', 'password']); - (u = u.setIn([o, 'value'], { - username: s, - header: 'Basic ' + utils_btoa(s + ':' + _) - })), - (u = u.setIn([o, 'schema'], i.get('schema'))); - } - }), - s.set('authorized', u) - ); - }, - [Lt]: (s, { payload: o }) => { - let i, - { auth: u, token: _ } = o; - (u.token = Object.assign({}, _)), (i = (0, qe.fromJS)(u)); - let w = s.get('authorized') || (0, qe.Map)(); - return (w = w.set(i.get('name'), i)), s.set('authorized', w); - }, - [Rt]: (s, { payload: o }) => { - let i = s.get('authorized').withMutations((s) => { - o.forEach((o) => { - s.delete(o); - }); - }); - return s.set('authorized', i); - }, - [Ft]: (s, { payload: o }) => s.set('configs', o), - [qt]: (s, { payload: o }) => s.set('authorized', (0, qe.fromJS)(o.authorized)) - }; - function assertIsFunction(s, o = 'expected a function, instead received ' + typeof s) { - if ('function' != typeof s) throw new TypeError(o); - } - var ensureIsArray = (s) => (Array.isArray(s) ? s : [s]); - function getDependencies(s) { - const o = Array.isArray(s[0]) ? s[0] : s; - return ( - (function assertIsArrayOfFunctions( - s, - o = 'expected all items to be functions, instead received the following types: ' - ) { - if (!s.every((s) => 'function' == typeof s)) { - const i = s - .map((s) => - 'function' == typeof s ? `function ${s.name || 'unnamed'}()` : typeof s - ) - .join(', '); - throw new TypeError(`${o}[${i}]`); - } - })( - o, - 'createSelector expects all input-selectors to be functions, but received the following types: ' - ), - o - ); - } - Symbol(), Object.getPrototypeOf({}); - var Vt = - 'undefined' != typeof WeakRef - ? WeakRef - : class { - constructor(s) { - this.value = s; - } - deref() { - return this.value; - } - }; - function weakMapMemoize(s, o = {}) { - let i = { s: 0, v: void 0, o: null, p: null }; - const { resultEqualityCheck: u } = o; - let _, - w = 0; - function memoized() { - let o = i; - const { length: x } = arguments; - for (let s = 0, i = x; s < i; s++) { - const i = arguments[s]; - if ('function' == typeof i || ('object' == typeof i && null !== i)) { - let s = o.o; - null === s && (o.o = s = new WeakMap()); - const u = s.get(i); - void 0 === u ? ((o = { s: 0, v: void 0, o: null, p: null }), s.set(i, o)) : (o = u); - } else { - let s = o.p; - null === s && (o.p = s = new Map()); - const u = s.get(i); - void 0 === u ? ((o = { s: 0, v: void 0, o: null, p: null }), s.set(i, o)) : (o = u); - } - } - const C = o; - let j; - if (1 === o.s) j = o.v; - else if (((j = s.apply(null, arguments)), w++, u)) { - const s = _?.deref?.() ?? _; - null != s && u(s, j) && ((j = s), 0 !== w && w--); - _ = ('object' == typeof j && null !== j) || 'function' == typeof j ? new Vt(j) : j; - } - return (C.s = 1), (C.v = j), j; - } - return ( - (memoized.clearCache = () => { - (i = { s: 0, v: void 0, o: null, p: null }), memoized.resetResultsCount(); - }), - (memoized.resultsCount = () => w), - (memoized.resetResultsCount = () => { - w = 0; - }), - memoized - ); - } - function createSelectorCreator(s, ...o) { - const i = 'function' == typeof s ? { memoize: s, memoizeOptions: o } : s, - createSelector2 = (...s) => { - let o, - u = 0, - _ = 0, - w = {}, - x = s.pop(); - 'object' == typeof x && ((w = x), (x = s.pop())), - assertIsFunction( - x, - `createSelector expects an output function after the inputs, but received: [${typeof x}]` - ); - const C = { ...i, ...w }, - { - memoize: j, - memoizeOptions: L = [], - argsMemoize: B = weakMapMemoize, - argsMemoizeOptions: $ = [], - devModeChecks: V = {} - } = C, - U = ensureIsArray(L), - z = ensureIsArray($), - Y = getDependencies(s), - Z = j( - function recomputationWrapper() { - return u++, x.apply(null, arguments); - }, - ...U - ); - const ee = B( - function dependenciesChecker() { - _++; - const s = (function collectInputSelectorResults(s, o) { - const i = [], - { length: u } = s; - for (let _ = 0; _ < u; _++) i.push(s[_].apply(null, o)); - return i; - })(Y, arguments); - return (o = Z.apply(null, s)), o; - }, - ...z - ); - return Object.assign(ee, { - resultFunc: x, - memoizedResultFunc: Z, - dependencies: Y, - dependencyRecomputations: () => _, - resetDependencyRecomputations: () => { - _ = 0; - }, - lastResult: () => o, - recomputations: () => u, - resetRecomputations: () => { - u = 0; - }, - memoize: j, - argsMemoize: B - }); - }; - return ( - Object.assign(createSelector2, { withTypes: () => createSelector2 }), createSelector2 - ); - } - var Ut = createSelectorCreator(weakMapMemoize), - zt = Object.assign( - (s, o = Ut) => { - !(function assertIsObject(s, o = 'expected an object, instead received ' + typeof s) { - if ('object' != typeof s) throw new TypeError(o); - })( - s, - 'createStructuredSelector expects first argument to be an object where each property is a selector, instead received a ' + - typeof s - ); - const i = Object.keys(s); - return o( - i.map((o) => s[o]), - (...s) => s.reduce((s, o, u) => ((s[i[u]] = o), s), {}) - ); - }, - { withTypes: () => zt } - ); - const state = (s) => s, - Wt = Ut(state, (s) => s.get('showDefinitions')), - Kt = Ut(state, () => ({ specSelectors: s }) => { - let o = s.securityDefinitions() || (0, qe.Map)({}), - i = (0, qe.List)(); - return ( - o.entrySeq().forEach(([s, o]) => { - let u = (0, qe.Map)(); - (u = u.set(s, o)), (i = i.push(u)); - }), - i - ); - }), - getDefinitionsByNames = - (s, o) => - ({ specSelectors: s }) => { - console.warn( - 'WARNING: getDefinitionsByNames is deprecated and will be removed in the next major version.' - ); - let i = s.securityDefinitions(), - u = (0, qe.List)(); - return ( - o.valueSeq().forEach((s) => { - let o = (0, qe.Map)(); - s.entrySeq().forEach(([s, u]) => { - let _, - w = i.get(s); - 'oauth2' === w.get('type') && - u.size && - ((_ = w.get('scopes')), - _.keySeq().forEach((s) => { - u.contains(s) || (_ = _.delete(s)); - }), - (w = w.set('allowedScopes', _))), - (o = o.set(s, w)); - }), - (u = u.push(o)); - }), - u - ); - }, - definitionsForRequirements = - (s, o = (0, qe.List)()) => - ({ authSelectors: s }) => { - const i = s.definitionsToAuthorize() || (0, qe.List)(); - let u = (0, qe.List)(); - return ( - i.forEach((s) => { - let i = o.find((o) => o.get(s.keySeq().first())); - i && - (s.forEach((o, u) => { - if ('oauth2' === o.get('type')) { - const _ = i.get(u); - let w = o.get('scopes'); - qe.List.isList(_) && - qe.Map.isMap(w) && - (w.keySeq().forEach((s) => { - _.contains(s) || (w = w.delete(s)); - }), - (s = s.set(u, o.set('scopes', w)))); - } - }), - (u = u.push(s))); - }), - u - ); - }, - Ht = Ut(state, (s) => s.get('authorized') || (0, qe.Map)()), - isAuthorized = - (s, o) => - ({ authSelectors: s }) => { - let i = s.authorized(); - return qe.List.isList(o) - ? !!o.toJS().filter( - (s) => - -1 === - Object.keys(s) - .map((s) => !!i.get(s)) - .indexOf(!1) - ).length - : null; - }, - Jt = Ut(state, (s) => s.get('configs')), - execute = - (s, { authSelectors: o, specSelectors: i }) => - ({ path: u, method: _, operation: w, extras: x }) => { - let C = { - authorized: o.authorized() && o.authorized().toJS(), - definitions: i.securityDefinitions() && i.securityDefinitions().toJS(), - specSecurity: i.security() && i.security().toJS() - }; - return s({ path: u, method: _, operation: w, securities: C, ...x }); - }, - loaded = (s, o) => (i) => { - const { getConfigs: u, authActions: _ } = o, - w = u(); - if ((s(i), w.persistAuthorization)) { - const s = localStorage.getItem('authorized'); - s && _.restoreAuthorization({ authorized: JSON.parse(s) }); - } - }, - wrap_actions_authorize = (s, o) => (i) => { - s(i); - if (o.getConfigs().persistAuthorization) - try { - const [{ schema: s, value: o }] = Object.values(i), - u = 'apiKey' === s.get('type'), - _ = 'cookie' === s.get('in'); - u && _ && (document.cookie = `${s.get('name')}=${o}; SameSite=None; Secure`); - } catch (s) { - console.error('Error persisting cookie based apiKey in document.cookie.', s); - } - }, - wrap_actions_logout = (s, o) => (i) => { - const u = o.getConfigs(), - _ = o.authSelectors.authorized(); - try { - u.persistAuthorization && - Array.isArray(i) && - i.forEach((s) => { - const o = _.get(s, {}), - i = 'apiKey' === o.getIn(['schema', 'type']), - u = 'cookie' === o.getIn(['schema', 'in']); - if (i && u) { - const s = o.getIn(['schema', 'name']); - document.cookie = `${s}=; Max-Age=-99999999`; - } - }); - } catch (s) { - console.error('Error deleting cookie based apiKey from document.cookie.', s); - } - s(i); - }; - var Gt = __webpack_require__(90179), - Yt = __webpack_require__.n(Gt); - class LockAuthIcon extends Pe.Component { - mapStateToProps(s, o) { - return { state: s, ownProps: Yt()(o, Object.keys(o.getSystem())) }; - } - render() { - const { getComponent: s, ownProps: o } = this.props, - i = s('LockIcon'); - return Pe.createElement(i, o); - } - } - const Xt = LockAuthIcon; - class UnlockAuthIcon extends Pe.Component { - mapStateToProps(s, o) { - return { state: s, ownProps: Yt()(o, Object.keys(o.getSystem())) }; - } - render() { - const { getComponent: s, ownProps: o } = this.props, - i = s('UnlockIcon'); - return Pe.createElement(i, o); - } - } - const Zt = UnlockAuthIcon; - function auth() { - return { - afterLoad(s) { - (this.rootInjects = this.rootInjects || {}), - (this.rootInjects.initOAuth = s.authActions.configureAuth), - (this.rootInjects.preauthorizeApiKey = preauthorizeApiKey.bind(null, s)), - (this.rootInjects.preauthorizeBasic = preauthorizeBasic.bind(null, s)); - }, - components: { - LockAuthIcon: Xt, - UnlockAuthIcon: Zt, - LockAuthOperationIcon: Xt, - UnlockAuthOperationIcon: Zt - }, - statePlugins: { - auth: { - reducers: $t, - actions: o, - selectors: i, - wrapActions: { authorize: wrap_actions_authorize, logout: wrap_actions_logout } - }, - configs: { wrapActions: { loaded } }, - spec: { wrapActions: { execute } } - } - }; - } - function preauthorizeBasic(s, o, i, u) { - const { - authActions: { authorize: _ }, - specSelectors: { specJson: w, isOAS3: x } - } = s, - C = x() ? ['components', 'securitySchemes'] : ['securityDefinitions'], - j = w().getIn([...C, o]); - return j ? _({ [o]: { value: { username: i, password: u }, schema: j.toJS() } }) : null; - } - function preauthorizeApiKey(s, o, i) { - const { - authActions: { authorize: u }, - specSelectors: { specJson: _, isOAS3: w } - } = s, - x = w() ? ['components', 'securitySchemes'] : ['securityDefinitions'], - C = _().getIn([...x, o]); - return C ? u({ [o]: { value: i, schema: C.toJS() } }) : null; - } - function isNothing(s) { - return null == s; - } - var Qt = function repeat(s, o) { - var i, - u = ''; - for (i = 0; i < o; i += 1) u += s; - return u; - }, - er = function isNegativeZero(s) { - return 0 === s && Number.NEGATIVE_INFINITY === 1 / s; - }, - tr = { - isNothing, - isObject: function js_yaml_isObject(s) { - return 'object' == typeof s && null !== s; - }, - toArray: function toArray(s) { - return Array.isArray(s) ? s : isNothing(s) ? [] : [s]; - }, - repeat: Qt, - isNegativeZero: er, - extend: function extend(s, o) { - var i, u, _, w; - if (o) - for (i = 0, u = (w = Object.keys(o)).length; i < u; i += 1) s[(_ = w[i])] = o[_]; - return s; - } - }; - function formatError(s, o) { - var i = '', - u = s.reason || '(unknown reason)'; - return s.mark - ? (s.mark.name && (i += 'in "' + s.mark.name + '" '), - (i += '(' + (s.mark.line + 1) + ':' + (s.mark.column + 1) + ')'), - !o && s.mark.snippet && (i += '\n\n' + s.mark.snippet), - u + ' ' + i) - : u; - } - function YAMLException$1(s, o) { - Error.call(this), - (this.name = 'YAMLException'), - (this.reason = s), - (this.mark = o), - (this.message = formatError(this, !1)), - Error.captureStackTrace - ? Error.captureStackTrace(this, this.constructor) - : (this.stack = new Error().stack || ''); - } - (YAMLException$1.prototype = Object.create(Error.prototype)), - (YAMLException$1.prototype.constructor = YAMLException$1), - (YAMLException$1.prototype.toString = function toString(s) { - return this.name + ': ' + formatError(this, s); - }); - var rr = YAMLException$1; - function getLine(s, o, i, u, _) { - var w = '', - x = '', - C = Math.floor(_ / 2) - 1; - return ( - u - o > C && (o = u - C + (w = ' ... ').length), - i - u > C && (i = u + C - (x = ' ...').length), - { str: w + s.slice(o, i).replace(/\t/g, '→') + x, pos: u - o + w.length } - ); - } - function padStart(s, o) { - return tr.repeat(' ', o - s.length) + s; - } - var nr = function makeSnippet(s, o) { - if (((o = Object.create(o || null)), !s.buffer)) return null; - o.maxLength || (o.maxLength = 79), - 'number' != typeof o.indent && (o.indent = 1), - 'number' != typeof o.linesBefore && (o.linesBefore = 3), - 'number' != typeof o.linesAfter && (o.linesAfter = 2); - for (var i, u = /\r?\n|\r|\0/g, _ = [0], w = [], x = -1; (i = u.exec(s.buffer)); ) - w.push(i.index), - _.push(i.index + i[0].length), - s.position <= i.index && x < 0 && (x = _.length - 2); - x < 0 && (x = _.length - 1); - var C, - j, - L = '', - B = Math.min(s.line + o.linesAfter, w.length).toString().length, - $ = o.maxLength - (o.indent + B + 3); - for (C = 1; C <= o.linesBefore && !(x - C < 0); C++) - (j = getLine(s.buffer, _[x - C], w[x - C], s.position - (_[x] - _[x - C]), $)), - (L = - tr.repeat(' ', o.indent) + - padStart((s.line - C + 1).toString(), B) + - ' | ' + - j.str + - '\n' + - L); - for ( - j = getLine(s.buffer, _[x], w[x], s.position, $), - L += - tr.repeat(' ', o.indent) + - padStart((s.line + 1).toString(), B) + - ' | ' + - j.str + - '\n', - L += tr.repeat('-', o.indent + B + 3 + j.pos) + '^\n', - C = 1; - C <= o.linesAfter && !(x + C >= w.length); - C++ - ) - (j = getLine(s.buffer, _[x + C], w[x + C], s.position - (_[x] - _[x + C]), $)), - (L += - tr.repeat(' ', o.indent) + - padStart((s.line + C + 1).toString(), B) + - ' | ' + - j.str + - '\n'); - return L.replace(/\n$/, ''); - }, - sr = [ - 'kind', - 'multi', - 'resolve', - 'construct', - 'instanceOf', - 'predicate', - 'represent', - 'representName', - 'defaultStyle', - 'styleAliases' - ], - ir = ['scalar', 'sequence', 'mapping']; - var ar = function Type$1(s, o) { - if ( - ((o = o || {}), - Object.keys(o).forEach(function (o) { - if (-1 === sr.indexOf(o)) - throw new rr( - 'Unknown option "' + o + '" is met in definition of "' + s + '" YAML type.' - ); - }), - (this.options = o), - (this.tag = s), - (this.kind = o.kind || null), - (this.resolve = - o.resolve || - function () { - return !0; - }), - (this.construct = - o.construct || - function (s) { - return s; - }), - (this.instanceOf = o.instanceOf || null), - (this.predicate = o.predicate || null), - (this.represent = o.represent || null), - (this.representName = o.representName || null), - (this.defaultStyle = o.defaultStyle || null), - (this.multi = o.multi || !1), - (this.styleAliases = (function compileStyleAliases(s) { - var o = {}; - return ( - null !== s && - Object.keys(s).forEach(function (i) { - s[i].forEach(function (s) { - o[String(s)] = i; - }); - }), - o - ); - })(o.styleAliases || null)), - -1 === ir.indexOf(this.kind)) - ) - throw new rr( - 'Unknown kind "' + this.kind + '" is specified for "' + s + '" YAML type.' - ); - }; - function compileList(s, o) { - var i = []; - return ( - s[o].forEach(function (s) { - var o = i.length; - i.forEach(function (i, u) { - i.tag === s.tag && i.kind === s.kind && i.multi === s.multi && (o = u); - }), - (i[o] = s); - }), - i - ); - } - function Schema$1(s) { - return this.extend(s); - } - Schema$1.prototype.extend = function extend(s) { - var o = [], - i = []; - if (s instanceof ar) i.push(s); - else if (Array.isArray(s)) i = i.concat(s); - else { - if (!s || (!Array.isArray(s.implicit) && !Array.isArray(s.explicit))) - throw new rr( - 'Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })' - ); - s.implicit && (o = o.concat(s.implicit)), s.explicit && (i = i.concat(s.explicit)); - } - o.forEach(function (s) { - if (!(s instanceof ar)) - throw new rr( - 'Specified list of YAML types (or a single Type object) contains a non-Type object.' - ); - if (s.loadKind && 'scalar' !== s.loadKind) - throw new rr( - 'There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.' - ); - if (s.multi) - throw new rr( - 'There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.' - ); - }), - i.forEach(function (s) { - if (!(s instanceof ar)) - throw new rr( - 'Specified list of YAML types (or a single Type object) contains a non-Type object.' - ); - }); - var u = Object.create(Schema$1.prototype); - return ( - (u.implicit = (this.implicit || []).concat(o)), - (u.explicit = (this.explicit || []).concat(i)), - (u.compiledImplicit = compileList(u, 'implicit')), - (u.compiledExplicit = compileList(u, 'explicit')), - (u.compiledTypeMap = (function compileMap() { - var s, - o, - i = { - scalar: {}, - sequence: {}, - mapping: {}, - fallback: {}, - multi: { scalar: [], sequence: [], mapping: [], fallback: [] } - }; - function collectType(s) { - s.multi - ? (i.multi[s.kind].push(s), i.multi.fallback.push(s)) - : (i[s.kind][s.tag] = i.fallback[s.tag] = s); - } - for (s = 0, o = arguments.length; s < o; s += 1) arguments[s].forEach(collectType); - return i; - })(u.compiledImplicit, u.compiledExplicit)), - u - ); - }; - var lr = Schema$1, - cr = new ar('tag:yaml.org,2002:str', { - kind: 'scalar', - construct: function (s) { - return null !== s ? s : ''; - } - }), - ur = new ar('tag:yaml.org,2002:seq', { - kind: 'sequence', - construct: function (s) { - return null !== s ? s : []; - } - }), - pr = new ar('tag:yaml.org,2002:map', { - kind: 'mapping', - construct: function (s) { - return null !== s ? s : {}; - } - }), - dr = new lr({ explicit: [cr, ur, pr] }); - var fr = new ar('tag:yaml.org,2002:null', { - kind: 'scalar', - resolve: function resolveYamlNull(s) { - if (null === s) return !0; - var o = s.length; - return ( - (1 === o && '~' === s) || (4 === o && ('null' === s || 'Null' === s || 'NULL' === s)) - ); - }, - construct: function constructYamlNull() { - return null; - }, - predicate: function isNull(s) { - return null === s; - }, - represent: { - canonical: function () { - return '~'; - }, - lowercase: function () { - return 'null'; - }, - uppercase: function () { - return 'NULL'; - }, - camelcase: function () { - return 'Null'; - }, - empty: function () { - return ''; - } - }, - defaultStyle: 'lowercase' - }); - var mr = new ar('tag:yaml.org,2002:bool', { - kind: 'scalar', - resolve: function resolveYamlBoolean(s) { - if (null === s) return !1; - var o = s.length; - return ( - (4 === o && ('true' === s || 'True' === s || 'TRUE' === s)) || - (5 === o && ('false' === s || 'False' === s || 'FALSE' === s)) - ); - }, - construct: function constructYamlBoolean(s) { - return 'true' === s || 'True' === s || 'TRUE' === s; - }, - predicate: function isBoolean(s) { - return '[object Boolean]' === Object.prototype.toString.call(s); - }, - represent: { - lowercase: function (s) { - return s ? 'true' : 'false'; - }, - uppercase: function (s) { - return s ? 'TRUE' : 'FALSE'; - }, - camelcase: function (s) { - return s ? 'True' : 'False'; - } - }, - defaultStyle: 'lowercase' - }); - function isOctCode(s) { - return 48 <= s && s <= 55; - } - function isDecCode(s) { - return 48 <= s && s <= 57; - } - var gr = new ar('tag:yaml.org,2002:int', { - kind: 'scalar', - resolve: function resolveYamlInteger(s) { - if (null === s) return !1; - var o, - i, - u = s.length, - _ = 0, - w = !1; - if (!u) return !1; - if ((('-' !== (o = s[_]) && '+' !== o) || (o = s[++_]), '0' === o)) { - if (_ + 1 === u) return !0; - if ('b' === (o = s[++_])) { - for (_++; _ < u; _++) - if ('_' !== (o = s[_])) { - if ('0' !== o && '1' !== o) return !1; - w = !0; - } - return w && '_' !== o; - } - if ('x' === o) { - for (_++; _ < u; _++) - if ('_' !== (o = s[_])) { - if ( - !( - (48 <= (i = s.charCodeAt(_)) && i <= 57) || - (65 <= i && i <= 70) || - (97 <= i && i <= 102) - ) - ) - return !1; - w = !0; - } - return w && '_' !== o; - } - if ('o' === o) { - for (_++; _ < u; _++) - if ('_' !== (o = s[_])) { - if (!isOctCode(s.charCodeAt(_))) return !1; - w = !0; - } - return w && '_' !== o; - } - } - if ('_' === o) return !1; - for (; _ < u; _++) - if ('_' !== (o = s[_])) { - if (!isDecCode(s.charCodeAt(_))) return !1; - w = !0; - } - return !(!w || '_' === o); - }, - construct: function constructYamlInteger(s) { - var o, - i = s, - u = 1; - if ( - (-1 !== i.indexOf('_') && (i = i.replace(/_/g, '')), - ('-' !== (o = i[0]) && '+' !== o) || - ('-' === o && (u = -1), (o = (i = i.slice(1))[0])), - '0' === i) - ) - return 0; - if ('0' === o) { - if ('b' === i[1]) return u * parseInt(i.slice(2), 2); - if ('x' === i[1]) return u * parseInt(i.slice(2), 16); - if ('o' === i[1]) return u * parseInt(i.slice(2), 8); - } - return u * parseInt(i, 10); - }, - predicate: function isInteger(s) { - return ( - '[object Number]' === Object.prototype.toString.call(s) && - s % 1 == 0 && - !tr.isNegativeZero(s) - ); - }, - represent: { - binary: function (s) { - return s >= 0 ? '0b' + s.toString(2) : '-0b' + s.toString(2).slice(1); - }, - octal: function (s) { - return s >= 0 ? '0o' + s.toString(8) : '-0o' + s.toString(8).slice(1); - }, - decimal: function (s) { - return s.toString(10); - }, - hexadecimal: function (s) { - return s >= 0 - ? '0x' + s.toString(16).toUpperCase() - : '-0x' + s.toString(16).toUpperCase().slice(1); - } - }, - defaultStyle: 'decimal', - styleAliases: { - binary: [2, 'bin'], - octal: [8, 'oct'], - decimal: [10, 'dec'], - hexadecimal: [16, 'hex'] - } - }), - yr = new RegExp( - '^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$' - ); - var vr = /^[-+]?[0-9]+e/; - var br = new ar('tag:yaml.org,2002:float', { - kind: 'scalar', - resolve: function resolveYamlFloat(s) { - return null !== s && !(!yr.test(s) || '_' === s[s.length - 1]); - }, - construct: function constructYamlFloat(s) { - var o, i; - return ( - (i = '-' === (o = s.replace(/_/g, '').toLowerCase())[0] ? -1 : 1), - '+-'.indexOf(o[0]) >= 0 && (o = o.slice(1)), - '.inf' === o - ? 1 === i - ? Number.POSITIVE_INFINITY - : Number.NEGATIVE_INFINITY - : '.nan' === o - ? NaN - : i * parseFloat(o, 10) - ); - }, - predicate: function isFloat(s) { - return ( - '[object Number]' === Object.prototype.toString.call(s) && - (s % 1 != 0 || tr.isNegativeZero(s)) - ); - }, - represent: function representYamlFloat(s, o) { - var i; - if (isNaN(s)) - switch (o) { - case 'lowercase': - return '.nan'; - case 'uppercase': - return '.NAN'; - case 'camelcase': - return '.NaN'; - } - else if (Number.POSITIVE_INFINITY === s) - switch (o) { - case 'lowercase': - return '.inf'; - case 'uppercase': - return '.INF'; - case 'camelcase': - return '.Inf'; - } - else if (Number.NEGATIVE_INFINITY === s) - switch (o) { - case 'lowercase': - return '-.inf'; - case 'uppercase': - return '-.INF'; - case 'camelcase': - return '-.Inf'; - } - else if (tr.isNegativeZero(s)) return '-0.0'; - return (i = s.toString(10)), vr.test(i) ? i.replace('e', '.e') : i; - }, - defaultStyle: 'lowercase' - }), - _r = dr.extend({ implicit: [fr, mr, gr, br] }), - Er = _r, - wr = new RegExp('^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$'), - Sr = new RegExp( - '^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$' - ); - var xr = new ar('tag:yaml.org,2002:timestamp', { - kind: 'scalar', - resolve: function resolveYamlTimestamp(s) { - return null !== s && (null !== wr.exec(s) || null !== Sr.exec(s)); - }, - construct: function constructYamlTimestamp(s) { - var o, - i, - u, - _, - w, - x, - C, - j, - L = 0, - B = null; - if ((null === (o = wr.exec(s)) && (o = Sr.exec(s)), null === o)) - throw new Error('Date resolve error'); - if (((i = +o[1]), (u = +o[2] - 1), (_ = +o[3]), !o[4])) - return new Date(Date.UTC(i, u, _)); - if (((w = +o[4]), (x = +o[5]), (C = +o[6]), o[7])) { - for (L = o[7].slice(0, 3); L.length < 3; ) L += '0'; - L = +L; - } - return ( - o[9] && ((B = 6e4 * (60 * +o[10] + +(o[11] || 0))), '-' === o[9] && (B = -B)), - (j = new Date(Date.UTC(i, u, _, w, x, C, L))), - B && j.setTime(j.getTime() - B), - j - ); - }, - instanceOf: Date, - represent: function representYamlTimestamp(s) { - return s.toISOString(); - } - }); - var kr = new ar('tag:yaml.org,2002:merge', { - kind: 'scalar', - resolve: function resolveYamlMerge(s) { - return '<<' === s || null === s; - } - }), - Cr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r'; - var Or = new ar('tag:yaml.org,2002:binary', { - kind: 'scalar', - resolve: function resolveYamlBinary(s) { - if (null === s) return !1; - var o, - i, - u = 0, - _ = s.length, - w = Cr; - for (i = 0; i < _; i++) - if (!((o = w.indexOf(s.charAt(i))) > 64)) { - if (o < 0) return !1; - u += 6; - } - return u % 8 == 0; - }, - construct: function constructYamlBinary(s) { - var o, - i, - u = s.replace(/[\r\n=]/g, ''), - _ = u.length, - w = Cr, - x = 0, - C = []; - for (o = 0; o < _; o++) - o % 4 == 0 && - o && - (C.push((x >> 16) & 255), C.push((x >> 8) & 255), C.push(255 & x)), - (x = (x << 6) | w.indexOf(u.charAt(o))); - return ( - 0 === (i = (_ % 4) * 6) - ? (C.push((x >> 16) & 255), C.push((x >> 8) & 255), C.push(255 & x)) - : 18 === i - ? (C.push((x >> 10) & 255), C.push((x >> 2) & 255)) - : 12 === i && C.push((x >> 4) & 255), - new Uint8Array(C) - ); - }, - predicate: function isBinary(s) { - return '[object Uint8Array]' === Object.prototype.toString.call(s); - }, - represent: function representYamlBinary(s) { - var o, - i, - u = '', - _ = 0, - w = s.length, - x = Cr; - for (o = 0; o < w; o++) - o % 3 == 0 && - o && - ((u += x[(_ >> 18) & 63]), - (u += x[(_ >> 12) & 63]), - (u += x[(_ >> 6) & 63]), - (u += x[63 & _])), - (_ = (_ << 8) + s[o]); - return ( - 0 === (i = w % 3) - ? ((u += x[(_ >> 18) & 63]), - (u += x[(_ >> 12) & 63]), - (u += x[(_ >> 6) & 63]), - (u += x[63 & _])) - : 2 === i - ? ((u += x[(_ >> 10) & 63]), - (u += x[(_ >> 4) & 63]), - (u += x[(_ << 2) & 63]), - (u += x[64])) - : 1 === i && - ((u += x[(_ >> 2) & 63]), - (u += x[(_ << 4) & 63]), - (u += x[64]), - (u += x[64])), - u - ); - } - }), - Ar = Object.prototype.hasOwnProperty, - jr = Object.prototype.toString; - var Ir = new ar('tag:yaml.org,2002:omap', { - kind: 'sequence', - resolve: function resolveYamlOmap(s) { - if (null === s) return !0; - var o, - i, - u, - _, - w, - x = [], - C = s; - for (o = 0, i = C.length; o < i; o += 1) { - if (((u = C[o]), (w = !1), '[object Object]' !== jr.call(u))) return !1; - for (_ in u) - if (Ar.call(u, _)) { - if (w) return !1; - w = !0; - } - if (!w) return !1; - if (-1 !== x.indexOf(_)) return !1; - x.push(_); - } - return !0; - }, - construct: function constructYamlOmap(s) { - return null !== s ? s : []; - } - }), - Pr = Object.prototype.toString; - var Mr = new ar('tag:yaml.org,2002:pairs', { - kind: 'sequence', - resolve: function resolveYamlPairs(s) { - if (null === s) return !0; - var o, - i, - u, - _, - w, - x = s; - for (w = new Array(x.length), o = 0, i = x.length; o < i; o += 1) { - if (((u = x[o]), '[object Object]' !== Pr.call(u))) return !1; - if (1 !== (_ = Object.keys(u)).length) return !1; - w[o] = [_[0], u[_[0]]]; - } - return !0; - }, - construct: function constructYamlPairs(s) { - if (null === s) return []; - var o, - i, - u, - _, - w, - x = s; - for (w = new Array(x.length), o = 0, i = x.length; o < i; o += 1) - (u = x[o]), (_ = Object.keys(u)), (w[o] = [_[0], u[_[0]]]); - return w; - } - }), - Tr = Object.prototype.hasOwnProperty; - var Nr = new ar('tag:yaml.org,2002:set', { - kind: 'mapping', - resolve: function resolveYamlSet(s) { - if (null === s) return !0; - var o, - i = s; - for (o in i) if (Tr.call(i, o) && null !== i[o]) return !1; - return !0; - }, - construct: function constructYamlSet(s) { - return null !== s ? s : {}; - } - }), - Rr = Er.extend({ implicit: [xr, kr], explicit: [Or, Ir, Mr, Nr] }), - Dr = Object.prototype.hasOwnProperty, - Lr = - /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/, - Br = /[\x85\u2028\u2029]/, - Fr = /[,\[\]\{\}]/, - qr = /^(?:!|!!|![a-z\-]+!)$/i, - $r = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; - function _class(s) { - return Object.prototype.toString.call(s); - } - function is_EOL(s) { - return 10 === s || 13 === s; - } - function is_WHITE_SPACE(s) { - return 9 === s || 32 === s; - } - function is_WS_OR_EOL(s) { - return 9 === s || 32 === s || 10 === s || 13 === s; - } - function is_FLOW_INDICATOR(s) { - return 44 === s || 91 === s || 93 === s || 123 === s || 125 === s; - } - function fromHexCode(s) { - var o; - return 48 <= s && s <= 57 ? s - 48 : 97 <= (o = 32 | s) && o <= 102 ? o - 97 + 10 : -1; - } - function simpleEscapeSequence(s) { - return 48 === s - ? '\0' - : 97 === s - ? '' - : 98 === s - ? '\b' - : 116 === s || 9 === s - ? '\t' - : 110 === s - ? '\n' - : 118 === s - ? '\v' - : 102 === s - ? '\f' - : 114 === s - ? '\r' - : 101 === s - ? '' - : 32 === s - ? ' ' - : 34 === s - ? '"' - : 47 === s - ? '/' - : 92 === s - ? '\\' - : 78 === s - ? '…' - : 95 === s - ? ' ' - : 76 === s - ? '\u2028' - : 80 === s - ? '\u2029' - : ''; - } - function charFromCodepoint(s) { - return s <= 65535 - ? String.fromCharCode(s) - : String.fromCharCode(55296 + ((s - 65536) >> 10), 56320 + ((s - 65536) & 1023)); - } - for (var Vr = new Array(256), Ur = new Array(256), zr = 0; zr < 256; zr++) - (Vr[zr] = simpleEscapeSequence(zr) ? 1 : 0), (Ur[zr] = simpleEscapeSequence(zr)); - function State$1(s, o) { - (this.input = s), - (this.filename = o.filename || null), - (this.schema = o.schema || Rr), - (this.onWarning = o.onWarning || null), - (this.legacy = o.legacy || !1), - (this.json = o.json || !1), - (this.listener = o.listener || null), - (this.implicitTypes = this.schema.compiledImplicit), - (this.typeMap = this.schema.compiledTypeMap), - (this.length = s.length), - (this.position = 0), - (this.line = 0), - (this.lineStart = 0), - (this.lineIndent = 0), - (this.firstTabInLine = -1), - (this.documents = []); - } - function generateError(s, o) { - var i = { - name: s.filename, - buffer: s.input.slice(0, -1), - position: s.position, - line: s.line, - column: s.position - s.lineStart - }; - return (i.snippet = nr(i)), new rr(o, i); - } - function throwError(s, o) { - throw generateError(s, o); - } - function throwWarning(s, o) { - s.onWarning && s.onWarning.call(null, generateError(s, o)); - } - var Wr = { - YAML: function handleYamlDirective(s, o, i) { - var u, _, w; - null !== s.version && throwError(s, 'duplication of %YAML directive'), - 1 !== i.length && throwError(s, 'YAML directive accepts exactly one argument'), - null === (u = /^([0-9]+)\.([0-9]+)$/.exec(i[0])) && - throwError(s, 'ill-formed argument of the YAML directive'), - (_ = parseInt(u[1], 10)), - (w = parseInt(u[2], 10)), - 1 !== _ && throwError(s, 'unacceptable YAML version of the document'), - (s.version = i[0]), - (s.checkLineBreaks = w < 2), - 1 !== w && 2 !== w && throwWarning(s, 'unsupported YAML version of the document'); - }, - TAG: function handleTagDirective(s, o, i) { - var u, _; - 2 !== i.length && throwError(s, 'TAG directive accepts exactly two arguments'), - (u = i[0]), - (_ = i[1]), - qr.test(u) || - throwError(s, 'ill-formed tag handle (first argument) of the TAG directive'), - Dr.call(s.tagMap, u) && - throwError(s, 'there is a previously declared suffix for "' + u + '" tag handle'), - $r.test(_) || - throwError(s, 'ill-formed tag prefix (second argument) of the TAG directive'); - try { - _ = decodeURIComponent(_); - } catch (o) { - throwError(s, 'tag prefix is malformed: ' + _); - } - s.tagMap[u] = _; - } - }; - function captureSegment(s, o, i, u) { - var _, w, x, C; - if (o < i) { - if (((C = s.input.slice(o, i)), u)) - for (_ = 0, w = C.length; _ < w; _ += 1) - 9 === (x = C.charCodeAt(_)) || - (32 <= x && x <= 1114111) || - throwError(s, 'expected valid JSON character'); - else Lr.test(C) && throwError(s, 'the stream contains non-printable characters'); - s.result += C; - } - } - function mergeMappings(s, o, i, u) { - var _, w, x, C; - for ( - tr.isObject(i) || - throwError(s, 'cannot merge mappings; the provided source object is unacceptable'), - x = 0, - C = (_ = Object.keys(i)).length; - x < C; - x += 1 - ) - (w = _[x]), Dr.call(o, w) || ((o[w] = i[w]), (u[w] = !0)); - } - function storeMappingPair(s, o, i, u, _, w, x, C, j) { - var L, B; - if (Array.isArray(_)) - for (L = 0, B = (_ = Array.prototype.slice.call(_)).length; L < B; L += 1) - Array.isArray(_[L]) && throwError(s, 'nested arrays are not supported inside keys'), - 'object' == typeof _ && - '[object Object]' === _class(_[L]) && - (_[L] = '[object Object]'); - if ( - ('object' == typeof _ && '[object Object]' === _class(_) && (_ = '[object Object]'), - (_ = String(_)), - null === o && (o = {}), - 'tag:yaml.org,2002:merge' === u) - ) - if (Array.isArray(w)) - for (L = 0, B = w.length; L < B; L += 1) mergeMappings(s, o, w[L], i); - else mergeMappings(s, o, w, i); - else - s.json || - Dr.call(i, _) || - !Dr.call(o, _) || - ((s.line = x || s.line), - (s.lineStart = C || s.lineStart), - (s.position = j || s.position), - throwError(s, 'duplicated mapping key')), - '__proto__' === _ - ? Object.defineProperty(o, _, { - configurable: !0, - enumerable: !0, - writable: !0, - value: w - }) - : (o[_] = w), - delete i[_]; - return o; - } - function readLineBreak(s) { - var o; - 10 === (o = s.input.charCodeAt(s.position)) - ? s.position++ - : 13 === o - ? (s.position++, 10 === s.input.charCodeAt(s.position) && s.position++) - : throwError(s, 'a line break is expected'), - (s.line += 1), - (s.lineStart = s.position), - (s.firstTabInLine = -1); - } - function skipSeparationSpace(s, o, i) { - for (var u = 0, _ = s.input.charCodeAt(s.position); 0 !== _; ) { - for (; is_WHITE_SPACE(_); ) - 9 === _ && -1 === s.firstTabInLine && (s.firstTabInLine = s.position), - (_ = s.input.charCodeAt(++s.position)); - if (o && 35 === _) - do { - _ = s.input.charCodeAt(++s.position); - } while (10 !== _ && 13 !== _ && 0 !== _); - if (!is_EOL(_)) break; - for ( - readLineBreak(s), _ = s.input.charCodeAt(s.position), u++, s.lineIndent = 0; - 32 === _; - - ) - s.lineIndent++, (_ = s.input.charCodeAt(++s.position)); - } - return ( - -1 !== i && 0 !== u && s.lineIndent < i && throwWarning(s, 'deficient indentation'), u - ); - } - function testDocumentSeparator(s) { - var o, - i = s.position; - return !( - (45 !== (o = s.input.charCodeAt(i)) && 46 !== o) || - o !== s.input.charCodeAt(i + 1) || - o !== s.input.charCodeAt(i + 2) || - ((i += 3), 0 !== (o = s.input.charCodeAt(i)) && !is_WS_OR_EOL(o)) - ); - } - function writeFoldedLines(s, o) { - 1 === o ? (s.result += ' ') : o > 1 && (s.result += tr.repeat('\n', o - 1)); - } - function readBlockSequence(s, o) { - var i, - u, - _ = s.tag, - w = s.anchor, - x = [], - C = !1; - if (-1 !== s.firstTabInLine) return !1; - for ( - null !== s.anchor && (s.anchorMap[s.anchor] = x), u = s.input.charCodeAt(s.position); - 0 !== u && - (-1 !== s.firstTabInLine && - ((s.position = s.firstTabInLine), - throwError(s, 'tab characters must not be used in indentation')), - 45 === u) && - is_WS_OR_EOL(s.input.charCodeAt(s.position + 1)); - - ) - if (((C = !0), s.position++, skipSeparationSpace(s, !0, -1) && s.lineIndent <= o)) - x.push(null), (u = s.input.charCodeAt(s.position)); - else if ( - ((i = s.line), - composeNode(s, o, 3, !1, !0), - x.push(s.result), - skipSeparationSpace(s, !0, -1), - (u = s.input.charCodeAt(s.position)), - (s.line === i || s.lineIndent > o) && 0 !== u) - ) - throwError(s, 'bad indentation of a sequence entry'); - else if (s.lineIndent < o) break; - return !!C && ((s.tag = _), (s.anchor = w), (s.kind = 'sequence'), (s.result = x), !0); - } - function readTagProperty(s) { - var o, - i, - u, - _, - w = !1, - x = !1; - if (33 !== (_ = s.input.charCodeAt(s.position))) return !1; - if ( - (null !== s.tag && throwError(s, 'duplication of a tag property'), - 60 === (_ = s.input.charCodeAt(++s.position)) - ? ((w = !0), (_ = s.input.charCodeAt(++s.position))) - : 33 === _ - ? ((x = !0), (i = '!!'), (_ = s.input.charCodeAt(++s.position))) - : (i = '!'), - (o = s.position), - w) - ) { - do { - _ = s.input.charCodeAt(++s.position); - } while (0 !== _ && 62 !== _); - s.position < s.length - ? ((u = s.input.slice(o, s.position)), (_ = s.input.charCodeAt(++s.position))) - : throwError(s, 'unexpected end of the stream within a verbatim tag'); - } else { - for (; 0 !== _ && !is_WS_OR_EOL(_); ) - 33 === _ && - (x - ? throwError(s, 'tag suffix cannot contain exclamation marks') - : ((i = s.input.slice(o - 1, s.position + 1)), - qr.test(i) || throwError(s, 'named tag handle cannot contain such characters'), - (x = !0), - (o = s.position + 1))), - (_ = s.input.charCodeAt(++s.position)); - (u = s.input.slice(o, s.position)), - Fr.test(u) && throwError(s, 'tag suffix cannot contain flow indicator characters'); - } - u && !$r.test(u) && throwError(s, 'tag name cannot contain such characters: ' + u); - try { - u = decodeURIComponent(u); - } catch (o) { - throwError(s, 'tag name is malformed: ' + u); - } - return ( - w - ? (s.tag = u) - : Dr.call(s.tagMap, i) - ? (s.tag = s.tagMap[i] + u) - : '!' === i - ? (s.tag = '!' + u) - : '!!' === i - ? (s.tag = 'tag:yaml.org,2002:' + u) - : throwError(s, 'undeclared tag handle "' + i + '"'), - !0 - ); - } - function readAnchorProperty(s) { - var o, i; - if (38 !== (i = s.input.charCodeAt(s.position))) return !1; - for ( - null !== s.anchor && throwError(s, 'duplication of an anchor property'), - i = s.input.charCodeAt(++s.position), - o = s.position; - 0 !== i && !is_WS_OR_EOL(i) && !is_FLOW_INDICATOR(i); - - ) - i = s.input.charCodeAt(++s.position); - return ( - s.position === o && - throwError(s, 'name of an anchor node must contain at least one character'), - (s.anchor = s.input.slice(o, s.position)), - !0 - ); - } - function composeNode(s, o, i, u, _) { - var w, - x, - C, - j, - L, - B, - $, - V, - U, - z = 1, - Y = !1, - Z = !1; - if ( - (null !== s.listener && s.listener('open', s), - (s.tag = null), - (s.anchor = null), - (s.kind = null), - (s.result = null), - (w = x = C = 4 === i || 3 === i), - u && - skipSeparationSpace(s, !0, -1) && - ((Y = !0), - s.lineIndent > o - ? (z = 1) - : s.lineIndent === o - ? (z = 0) - : s.lineIndent < o && (z = -1)), - 1 === z) - ) - for (; readTagProperty(s) || readAnchorProperty(s); ) - skipSeparationSpace(s, !0, -1) - ? ((Y = !0), - (C = w), - s.lineIndent > o - ? (z = 1) - : s.lineIndent === o - ? (z = 0) - : s.lineIndent < o && (z = -1)) - : (C = !1); - if ( - (C && (C = Y || _), - (1 !== z && 4 !== i) || - ((V = 1 === i || 2 === i ? o : o + 1), - (U = s.position - s.lineStart), - 1 === z - ? (C && - (readBlockSequence(s, U) || - (function readBlockMapping(s, o, i) { - var u, - _, - w, - x, - C, - j, - L, - B = s.tag, - $ = s.anchor, - V = {}, - U = Object.create(null), - z = null, - Y = null, - Z = null, - ee = !1, - ie = !1; - if (-1 !== s.firstTabInLine) return !1; - for ( - null !== s.anchor && (s.anchorMap[s.anchor] = V), - L = s.input.charCodeAt(s.position); - 0 !== L; - - ) { - if ( - (ee || - -1 === s.firstTabInLine || - ((s.position = s.firstTabInLine), - throwError(s, 'tab characters must not be used in indentation')), - (u = s.input.charCodeAt(s.position + 1)), - (w = s.line), - (63 !== L && 58 !== L) || !is_WS_OR_EOL(u)) - ) { - if ( - ((x = s.line), - (C = s.lineStart), - (j = s.position), - !composeNode(s, i, 2, !1, !0)) - ) - break; - if (s.line === w) { - for (L = s.input.charCodeAt(s.position); is_WHITE_SPACE(L); ) - L = s.input.charCodeAt(++s.position); - if (58 === L) - is_WS_OR_EOL((L = s.input.charCodeAt(++s.position))) || - throwError( - s, - 'a whitespace character is expected after the key-value separator within a block mapping' - ), - ee && - (storeMappingPair(s, V, U, z, Y, null, x, C, j), - (z = Y = Z = null)), - (ie = !0), - (ee = !1), - (_ = !1), - (z = s.tag), - (Y = s.result); - else { - if (!ie) return (s.tag = B), (s.anchor = $), !0; - throwError( - s, - 'can not read an implicit mapping pair; a colon is missed' - ); - } - } else { - if (!ie) return (s.tag = B), (s.anchor = $), !0; - throwError( - s, - 'can not read a block mapping entry; a multiline key may not be an implicit key' - ); - } - } else - 63 === L - ? (ee && - (storeMappingPair(s, V, U, z, Y, null, x, C, j), - (z = Y = Z = null)), - (ie = !0), - (ee = !0), - (_ = !0)) - : ee - ? ((ee = !1), (_ = !0)) - : throwError( - s, - 'incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line' - ), - (s.position += 1), - (L = u); - if ( - ((s.line === w || s.lineIndent > o) && - (ee && ((x = s.line), (C = s.lineStart), (j = s.position)), - composeNode(s, o, 4, !0, _) && (ee ? (Y = s.result) : (Z = s.result)), - ee || - (storeMappingPair(s, V, U, z, Y, Z, x, C, j), (z = Y = Z = null)), - skipSeparationSpace(s, !0, -1), - (L = s.input.charCodeAt(s.position))), - (s.line === w || s.lineIndent > o) && 0 !== L) - ) - throwError(s, 'bad indentation of a mapping entry'); - else if (s.lineIndent < o) break; - } - return ( - ee && storeMappingPair(s, V, U, z, Y, null, x, C, j), - ie && ((s.tag = B), (s.anchor = $), (s.kind = 'mapping'), (s.result = V)), - ie - ); - })(s, U, V))) || - (function readFlowCollection(s, o) { - var i, - u, - _, - w, - x, - C, - j, - L, - B, - $, - V, - U, - z = !0, - Y = s.tag, - Z = s.anchor, - ee = Object.create(null); - if (91 === (U = s.input.charCodeAt(s.position))) (x = 93), (L = !1), (w = []); - else { - if (123 !== U) return !1; - (x = 125), (L = !0), (w = {}); - } - for ( - null !== s.anchor && (s.anchorMap[s.anchor] = w), - U = s.input.charCodeAt(++s.position); - 0 !== U; - - ) { - if ( - (skipSeparationSpace(s, !0, o), (U = s.input.charCodeAt(s.position)) === x) - ) - return ( - s.position++, - (s.tag = Y), - (s.anchor = Z), - (s.kind = L ? 'mapping' : 'sequence'), - (s.result = w), - !0 - ); - z - ? 44 === U && throwError(s, "expected the node content, but found ','") - : throwError(s, 'missed comma between flow collection entries'), - (V = null), - (C = j = !1), - 63 === U && - is_WS_OR_EOL(s.input.charCodeAt(s.position + 1)) && - ((C = j = !0), s.position++, skipSeparationSpace(s, !0, o)), - (i = s.line), - (u = s.lineStart), - (_ = s.position), - composeNode(s, o, 1, !1, !0), - ($ = s.tag), - (B = s.result), - skipSeparationSpace(s, !0, o), - (U = s.input.charCodeAt(s.position)), - (!j && s.line !== i) || - 58 !== U || - ((C = !0), - (U = s.input.charCodeAt(++s.position)), - skipSeparationSpace(s, !0, o), - composeNode(s, o, 1, !1, !0), - (V = s.result)), - L - ? storeMappingPair(s, w, ee, $, B, V, i, u, _) - : C - ? w.push(storeMappingPair(s, null, ee, $, B, V, i, u, _)) - : w.push(B), - skipSeparationSpace(s, !0, o), - 44 === (U = s.input.charCodeAt(s.position)) - ? ((z = !0), (U = s.input.charCodeAt(++s.position))) - : (z = !1); - } - throwError(s, 'unexpected end of the stream within a flow collection'); - })(s, V) - ? (Z = !0) - : ((x && - (function readBlockScalar(s, o) { - var i, - u, - _, - w, - x, - C = 1, - j = !1, - L = !1, - B = o, - $ = 0, - V = !1; - if (124 === (w = s.input.charCodeAt(s.position))) u = !1; - else { - if (62 !== w) return !1; - u = !0; - } - for (s.kind = 'scalar', s.result = ''; 0 !== w; ) - if (43 === (w = s.input.charCodeAt(++s.position)) || 45 === w) - 1 === C - ? (C = 43 === w ? 3 : 2) - : throwError(s, 'repeat of a chomping mode identifier'); - else { - if (!((_ = 48 <= (x = w) && x <= 57 ? x - 48 : -1) >= 0)) break; - 0 === _ - ? throwError( - s, - 'bad explicit indentation width of a block scalar; it cannot be less than one' - ) - : L - ? throwError(s, 'repeat of an indentation width identifier') - : ((B = o + _ - 1), (L = !0)); - } - if (is_WHITE_SPACE(w)) { - do { - w = s.input.charCodeAt(++s.position); - } while (is_WHITE_SPACE(w)); - if (35 === w) - do { - w = s.input.charCodeAt(++s.position); - } while (!is_EOL(w) && 0 !== w); - } - for (; 0 !== w; ) { - for ( - readLineBreak(s), s.lineIndent = 0, w = s.input.charCodeAt(s.position); - (!L || s.lineIndent < B) && 32 === w; - - ) - s.lineIndent++, (w = s.input.charCodeAt(++s.position)); - if ((!L && s.lineIndent > B && (B = s.lineIndent), is_EOL(w))) $++; - else { - if (s.lineIndent < B) { - 3 === C - ? (s.result += tr.repeat('\n', j ? 1 + $ : $)) - : 1 === C && j && (s.result += '\n'); - break; - } - for ( - u - ? is_WHITE_SPACE(w) - ? ((V = !0), (s.result += tr.repeat('\n', j ? 1 + $ : $))) - : V - ? ((V = !1), (s.result += tr.repeat('\n', $ + 1))) - : 0 === $ - ? j && (s.result += ' ') - : (s.result += tr.repeat('\n', $)) - : (s.result += tr.repeat('\n', j ? 1 + $ : $)), - j = !0, - L = !0, - $ = 0, - i = s.position; - !is_EOL(w) && 0 !== w; - - ) - w = s.input.charCodeAt(++s.position); - captureSegment(s, i, s.position, !1); - } - } - return !0; - })(s, V)) || - (function readSingleQuotedScalar(s, o) { - var i, u, _; - if (39 !== (i = s.input.charCodeAt(s.position))) return !1; - for ( - s.kind = 'scalar', s.result = '', s.position++, u = _ = s.position; - 0 !== (i = s.input.charCodeAt(s.position)); - - ) - if (39 === i) { - if ( - (captureSegment(s, u, s.position, !0), - 39 !== (i = s.input.charCodeAt(++s.position))) - ) - return !0; - (u = s.position), s.position++, (_ = s.position); - } else - is_EOL(i) - ? (captureSegment(s, u, _, !0), - writeFoldedLines(s, skipSeparationSpace(s, !1, o)), - (u = _ = s.position)) - : s.position === s.lineStart && testDocumentSeparator(s) - ? throwError( - s, - 'unexpected end of the document within a single quoted scalar' - ) - : (s.position++, (_ = s.position)); - throwError(s, 'unexpected end of the stream within a single quoted scalar'); - })(s, V) || - (function readDoubleQuotedScalar(s, o) { - var i, u, _, w, x, C, j; - if (34 !== (C = s.input.charCodeAt(s.position))) return !1; - for ( - s.kind = 'scalar', s.result = '', s.position++, i = u = s.position; - 0 !== (C = s.input.charCodeAt(s.position)); - - ) { - if (34 === C) return captureSegment(s, i, s.position, !0), s.position++, !0; - if (92 === C) { - if ( - (captureSegment(s, i, s.position, !0), - is_EOL((C = s.input.charCodeAt(++s.position)))) - ) - skipSeparationSpace(s, !1, o); - else if (C < 256 && Vr[C]) (s.result += Ur[C]), s.position++; - else if ( - (x = 120 === (j = C) ? 2 : 117 === j ? 4 : 85 === j ? 8 : 0) > 0 - ) { - for (_ = x, w = 0; _ > 0; _--) - (x = fromHexCode((C = s.input.charCodeAt(++s.position)))) >= 0 - ? (w = (w << 4) + x) - : throwError(s, 'expected hexadecimal character'); - (s.result += charFromCodepoint(w)), s.position++; - } else throwError(s, 'unknown escape sequence'); - i = u = s.position; - } else - is_EOL(C) - ? (captureSegment(s, i, u, !0), - writeFoldedLines(s, skipSeparationSpace(s, !1, o)), - (i = u = s.position)) - : s.position === s.lineStart && testDocumentSeparator(s) - ? throwError( - s, - 'unexpected end of the document within a double quoted scalar' - ) - : (s.position++, (u = s.position)); - } - throwError(s, 'unexpected end of the stream within a double quoted scalar'); - })(s, V) - ? (Z = !0) - : !(function readAlias(s) { - var o, i, u; - if (42 !== (u = s.input.charCodeAt(s.position))) return !1; - for ( - u = s.input.charCodeAt(++s.position), o = s.position; - 0 !== u && !is_WS_OR_EOL(u) && !is_FLOW_INDICATOR(u); - - ) - u = s.input.charCodeAt(++s.position); - return ( - s.position === o && - throwError( - s, - 'name of an alias node must contain at least one character' - ), - (i = s.input.slice(o, s.position)), - Dr.call(s.anchorMap, i) || - throwError(s, 'unidentified alias "' + i + '"'), - (s.result = s.anchorMap[i]), - skipSeparationSpace(s, !0, -1), - !0 - ); - })(s) - ? (function readPlainScalar(s, o, i) { - var u, - _, - w, - x, - C, - j, - L, - B, - $ = s.kind, - V = s.result; - if ( - is_WS_OR_EOL((B = s.input.charCodeAt(s.position))) || - is_FLOW_INDICATOR(B) || - 35 === B || - 38 === B || - 42 === B || - 33 === B || - 124 === B || - 62 === B || - 39 === B || - 34 === B || - 37 === B || - 64 === B || - 96 === B - ) - return !1; - if ( - (63 === B || 45 === B) && - (is_WS_OR_EOL((u = s.input.charCodeAt(s.position + 1))) || - (i && is_FLOW_INDICATOR(u))) - ) - return !1; - for ( - s.kind = 'scalar', s.result = '', _ = w = s.position, x = !1; - 0 !== B; - - ) { - if (58 === B) { - if ( - is_WS_OR_EOL((u = s.input.charCodeAt(s.position + 1))) || - (i && is_FLOW_INDICATOR(u)) - ) - break; - } else if (35 === B) { - if (is_WS_OR_EOL(s.input.charCodeAt(s.position - 1))) break; - } else { - if ( - (s.position === s.lineStart && testDocumentSeparator(s)) || - (i && is_FLOW_INDICATOR(B)) - ) - break; - if (is_EOL(B)) { - if ( - ((C = s.line), - (j = s.lineStart), - (L = s.lineIndent), - skipSeparationSpace(s, !1, -1), - s.lineIndent >= o) - ) { - (x = !0), (B = s.input.charCodeAt(s.position)); - continue; - } - (s.position = w), - (s.line = C), - (s.lineStart = j), - (s.lineIndent = L); - break; - } - } - x && - (captureSegment(s, _, w, !1), - writeFoldedLines(s, s.line - C), - (_ = w = s.position), - (x = !1)), - is_WHITE_SPACE(B) || (w = s.position + 1), - (B = s.input.charCodeAt(++s.position)); - } - return ( - captureSegment(s, _, w, !1), - !!s.result || ((s.kind = $), (s.result = V), !1) - ); - })(s, V, 1 === i) && ((Z = !0), null === s.tag && (s.tag = '?')) - : ((Z = !0), - (null === s.tag && null === s.anchor) || - throwError(s, 'alias node should not have any properties')), - null !== s.anchor && (s.anchorMap[s.anchor] = s.result)) - : 0 === z && (Z = C && readBlockSequence(s, U))), - null === s.tag) - ) - null !== s.anchor && (s.anchorMap[s.anchor] = s.result); - else if ('?' === s.tag) { - for ( - null !== s.result && - 'scalar' !== s.kind && - throwError( - s, - 'unacceptable node kind for ! tag; it should be "scalar", not "' + s.kind + '"' - ), - j = 0, - L = s.implicitTypes.length; - j < L; - j += 1 - ) - if (($ = s.implicitTypes[j]).resolve(s.result)) { - (s.result = $.construct(s.result)), - (s.tag = $.tag), - null !== s.anchor && (s.anchorMap[s.anchor] = s.result); - break; - } - } else if ('!' !== s.tag) { - if (Dr.call(s.typeMap[s.kind || 'fallback'], s.tag)) - $ = s.typeMap[s.kind || 'fallback'][s.tag]; - else - for ( - $ = null, j = 0, L = (B = s.typeMap.multi[s.kind || 'fallback']).length; - j < L; - j += 1 - ) - if (s.tag.slice(0, B[j].tag.length) === B[j].tag) { - $ = B[j]; - break; - } - $ || throwError(s, 'unknown tag !<' + s.tag + '>'), - null !== s.result && - $.kind !== s.kind && - throwError( - s, - 'unacceptable node kind for !<' + - s.tag + - '> tag; it should be "' + - $.kind + - '", not "' + - s.kind + - '"' - ), - $.resolve(s.result, s.tag) - ? ((s.result = $.construct(s.result, s.tag)), - null !== s.anchor && (s.anchorMap[s.anchor] = s.result)) - : throwError(s, 'cannot resolve a node with !<' + s.tag + '> explicit tag'); - } - return ( - null !== s.listener && s.listener('close', s), null !== s.tag || null !== s.anchor || Z - ); - } - function readDocument(s) { - var o, - i, - u, - _, - w = s.position, - x = !1; - for ( - s.version = null, - s.checkLineBreaks = s.legacy, - s.tagMap = Object.create(null), - s.anchorMap = Object.create(null); - 0 !== (_ = s.input.charCodeAt(s.position)) && - (skipSeparationSpace(s, !0, -1), - (_ = s.input.charCodeAt(s.position)), - !(s.lineIndent > 0 || 37 !== _)); - - ) { - for ( - x = !0, _ = s.input.charCodeAt(++s.position), o = s.position; - 0 !== _ && !is_WS_OR_EOL(_); - - ) - _ = s.input.charCodeAt(++s.position); - for ( - u = [], - (i = s.input.slice(o, s.position)).length < 1 && - throwError(s, 'directive name must not be less than one character in length'); - 0 !== _; - - ) { - for (; is_WHITE_SPACE(_); ) _ = s.input.charCodeAt(++s.position); - if (35 === _) { - do { - _ = s.input.charCodeAt(++s.position); - } while (0 !== _ && !is_EOL(_)); - break; - } - if (is_EOL(_)) break; - for (o = s.position; 0 !== _ && !is_WS_OR_EOL(_); ) - _ = s.input.charCodeAt(++s.position); - u.push(s.input.slice(o, s.position)); - } - 0 !== _ && readLineBreak(s), - Dr.call(Wr, i) - ? Wr[i](s, i, u) - : throwWarning(s, 'unknown document directive "' + i + '"'); - } - skipSeparationSpace(s, !0, -1), - 0 === s.lineIndent && - 45 === s.input.charCodeAt(s.position) && - 45 === s.input.charCodeAt(s.position + 1) && - 45 === s.input.charCodeAt(s.position + 2) - ? ((s.position += 3), skipSeparationSpace(s, !0, -1)) - : x && throwError(s, 'directives end mark is expected'), - composeNode(s, s.lineIndent - 1, 4, !1, !0), - skipSeparationSpace(s, !0, -1), - s.checkLineBreaks && - Br.test(s.input.slice(w, s.position)) && - throwWarning(s, 'non-ASCII line breaks are interpreted as content'), - s.documents.push(s.result), - s.position === s.lineStart && testDocumentSeparator(s) - ? 46 === s.input.charCodeAt(s.position) && - ((s.position += 3), skipSeparationSpace(s, !0, -1)) - : s.position < s.length - 1 && - throwError(s, 'end of the stream or a document separator is expected'); - } - function loadDocuments(s, o) { - (o = o || {}), - 0 !== (s = String(s)).length && - (10 !== s.charCodeAt(s.length - 1) && - 13 !== s.charCodeAt(s.length - 1) && - (s += '\n'), - 65279 === s.charCodeAt(0) && (s = s.slice(1))); - var i = new State$1(s, o), - u = s.indexOf('\0'); - for ( - -1 !== u && ((i.position = u), throwError(i, 'null byte is not allowed in input')), - i.input += '\0'; - 32 === i.input.charCodeAt(i.position); - - ) - (i.lineIndent += 1), (i.position += 1); - for (; i.position < i.length - 1; ) readDocument(i); - return i.documents; - } - var Kr = { - loadAll: function loadAll$1(s, o, i) { - null !== o && 'object' == typeof o && void 0 === i && ((i = o), (o = null)); - var u = loadDocuments(s, i); - if ('function' != typeof o) return u; - for (var _ = 0, w = u.length; _ < w; _ += 1) o(u[_]); - }, - load: function load$1(s, o) { - var i = loadDocuments(s, o); - if (0 !== i.length) { - if (1 === i.length) return i[0]; - throw new rr('expected a single document in the stream, but found more'); - } - } - }, - Hr = Object.prototype.toString, - Jr = Object.prototype.hasOwnProperty, - Gr = 65279, - Yr = { - 0: '\\0', - 7: '\\a', - 8: '\\b', - 9: '\\t', - 10: '\\n', - 11: '\\v', - 12: '\\f', - 13: '\\r', - 27: '\\e', - 34: '\\"', - 92: '\\\\', - 133: '\\N', - 160: '\\_', - 8232: '\\L', - 8233: '\\P' - }, - Xr = [ - 'y', - 'Y', - 'yes', - 'Yes', - 'YES', - 'on', - 'On', - 'ON', - 'n', - 'N', - 'no', - 'No', - 'NO', - 'off', - 'Off', - 'OFF' - ], - Zr = /^[-+]?[0-9_]+(?::[0-9_]+)+(?:\.[0-9_]*)?$/; - function encodeHex(s) { - var o, i, u; - if (((o = s.toString(16).toUpperCase()), s <= 255)) (i = 'x'), (u = 2); - else if (s <= 65535) (i = 'u'), (u = 4); - else { - if (!(s <= 4294967295)) - throw new rr('code point within a string may not be greater than 0xFFFFFFFF'); - (i = 'U'), (u = 8); - } - return '\\' + i + tr.repeat('0', u - o.length) + o; - } - function State(s) { - (this.schema = s.schema || Rr), - (this.indent = Math.max(1, s.indent || 2)), - (this.noArrayIndent = s.noArrayIndent || !1), - (this.skipInvalid = s.skipInvalid || !1), - (this.flowLevel = tr.isNothing(s.flowLevel) ? -1 : s.flowLevel), - (this.styleMap = (function compileStyleMap(s, o) { - var i, u, _, w, x, C, j; - if (null === o) return {}; - for (i = {}, _ = 0, w = (u = Object.keys(o)).length; _ < w; _ += 1) - (x = u[_]), - (C = String(o[x])), - '!!' === x.slice(0, 2) && (x = 'tag:yaml.org,2002:' + x.slice(2)), - (j = s.compiledTypeMap.fallback[x]) && - Jr.call(j.styleAliases, C) && - (C = j.styleAliases[C]), - (i[x] = C); - return i; - })(this.schema, s.styles || null)), - (this.sortKeys = s.sortKeys || !1), - (this.lineWidth = s.lineWidth || 80), - (this.noRefs = s.noRefs || !1), - (this.noCompatMode = s.noCompatMode || !1), - (this.condenseFlow = s.condenseFlow || !1), - (this.quotingType = '"' === s.quotingType ? 2 : 1), - (this.forceQuotes = s.forceQuotes || !1), - (this.replacer = 'function' == typeof s.replacer ? s.replacer : null), - (this.implicitTypes = this.schema.compiledImplicit), - (this.explicitTypes = this.schema.compiledExplicit), - (this.tag = null), - (this.result = ''), - (this.duplicates = []), - (this.usedDuplicates = null); - } - function indentString(s, o) { - for (var i, u = tr.repeat(' ', o), _ = 0, w = -1, x = '', C = s.length; _ < C; ) - -1 === (w = s.indexOf('\n', _)) - ? ((i = s.slice(_)), (_ = C)) - : ((i = s.slice(_, w + 1)), (_ = w + 1)), - i.length && '\n' !== i && (x += u), - (x += i); - return x; - } - function generateNextLine(s, o) { - return '\n' + tr.repeat(' ', s.indent * o); - } - function isWhitespace(s) { - return 32 === s || 9 === s; - } - function isPrintable(s) { - return ( - (32 <= s && s <= 126) || - (161 <= s && s <= 55295 && 8232 !== s && 8233 !== s) || - (57344 <= s && s <= 65533 && s !== Gr) || - (65536 <= s && s <= 1114111) - ); - } - function isNsCharOrWhitespace(s) { - return isPrintable(s) && s !== Gr && 13 !== s && 10 !== s; - } - function isPlainSafe(s, o, i) { - var u = isNsCharOrWhitespace(s), - _ = u && !isWhitespace(s); - return ( - ((i ? u : u && 44 !== s && 91 !== s && 93 !== s && 123 !== s && 125 !== s) && - 35 !== s && - !(58 === o && !_)) || - (isNsCharOrWhitespace(o) && !isWhitespace(o) && 35 === s) || - (58 === o && _) - ); - } - function codePointAt(s, o) { - var i, - u = s.charCodeAt(o); - return u >= 55296 && - u <= 56319 && - o + 1 < s.length && - (i = s.charCodeAt(o + 1)) >= 56320 && - i <= 57343 - ? 1024 * (u - 55296) + i - 56320 + 65536 - : u; - } - function needIndentIndicator(s) { - return /^\n* /.test(s); - } - function chooseScalarStyle(s, o, i, u, _, w, x, C) { - var j, - L = 0, - B = null, - $ = !1, - V = !1, - U = -1 !== u, - z = -1, - Y = - (function isPlainSafeFirst(s) { - return ( - isPrintable(s) && - s !== Gr && - !isWhitespace(s) && - 45 !== s && - 63 !== s && - 58 !== s && - 44 !== s && - 91 !== s && - 93 !== s && - 123 !== s && - 125 !== s && - 35 !== s && - 38 !== s && - 42 !== s && - 33 !== s && - 124 !== s && - 61 !== s && - 62 !== s && - 39 !== s && - 34 !== s && - 37 !== s && - 64 !== s && - 96 !== s - ); - })(codePointAt(s, 0)) && - (function isPlainSafeLast(s) { - return !isWhitespace(s) && 58 !== s; - })(codePointAt(s, s.length - 1)); - if (o || x) - for (j = 0; j < s.length; L >= 65536 ? (j += 2) : j++) { - if (!isPrintable((L = codePointAt(s, j)))) return 5; - (Y = Y && isPlainSafe(L, B, C)), (B = L); - } - else { - for (j = 0; j < s.length; L >= 65536 ? (j += 2) : j++) { - if (10 === (L = codePointAt(s, j))) - ($ = !0), U && ((V = V || (j - z - 1 > u && ' ' !== s[z + 1])), (z = j)); - else if (!isPrintable(L)) return 5; - (Y = Y && isPlainSafe(L, B, C)), (B = L); - } - V = V || (U && j - z - 1 > u && ' ' !== s[z + 1]); - } - return $ || V - ? i > 9 && needIndentIndicator(s) - ? 5 - : x - ? 2 === w - ? 5 - : 2 - : V - ? 4 - : 3 - : !Y || x || _(s) - ? 2 === w - ? 5 - : 2 - : 1; - } - function writeScalar(s, o, i, u, _) { - s.dump = (function () { - if (0 === o.length) return 2 === s.quotingType ? '""' : "''"; - if (!s.noCompatMode && (-1 !== Xr.indexOf(o) || Zr.test(o))) - return 2 === s.quotingType ? '"' + o + '"' : "'" + o + "'"; - var w = s.indent * Math.max(1, i), - x = -1 === s.lineWidth ? -1 : Math.max(Math.min(s.lineWidth, 40), s.lineWidth - w), - C = u || (s.flowLevel > -1 && i >= s.flowLevel); - switch ( - chooseScalarStyle( - o, - C, - s.indent, - x, - function testAmbiguity(o) { - return (function testImplicitResolving(s, o) { - var i, u; - for (i = 0, u = s.implicitTypes.length; i < u; i += 1) - if (s.implicitTypes[i].resolve(o)) return !0; - return !1; - })(s, o); - }, - s.quotingType, - s.forceQuotes && !u, - _ - ) - ) { - case 1: - return o; - case 2: - return "'" + o.replace(/'/g, "''") + "'"; - case 3: - return '|' + blockHeader(o, s.indent) + dropEndingNewline(indentString(o, w)); - case 4: - return ( - '>' + - blockHeader(o, s.indent) + - dropEndingNewline( - indentString( - (function foldString(s, o) { - var i, - u, - _ = /(\n+)([^\n]*)/g, - w = - ((C = s.indexOf('\n')), - (C = -1 !== C ? C : s.length), - (_.lastIndex = C), - foldLine(s.slice(0, C), o)), - x = '\n' === s[0] || ' ' === s[0]; - var C; - for (; (u = _.exec(s)); ) { - var j = u[1], - L = u[2]; - (i = ' ' === L[0]), - (w += j + (x || i || '' === L ? '' : '\n') + foldLine(L, o)), - (x = i); - } - return w; - })(o, x), - w - ) - ) - ); - case 5: - return ( - '"' + - (function escapeString(s) { - for (var o, i = '', u = 0, _ = 0; _ < s.length; u >= 65536 ? (_ += 2) : _++) - (u = codePointAt(s, _)), - !(o = Yr[u]) && isPrintable(u) - ? ((i += s[_]), u >= 65536 && (i += s[_ + 1])) - : (i += o || encodeHex(u)); - return i; - })(o) + - '"' - ); - default: - throw new rr('impossible error: invalid scalar style'); - } - })(); - } - function blockHeader(s, o) { - var i = needIndentIndicator(s) ? String(o) : '', - u = '\n' === s[s.length - 1]; - return i + (u && ('\n' === s[s.length - 2] || '\n' === s) ? '+' : u ? '' : '-') + '\n'; - } - function dropEndingNewline(s) { - return '\n' === s[s.length - 1] ? s.slice(0, -1) : s; - } - function foldLine(s, o) { - if ('' === s || ' ' === s[0]) return s; - for (var i, u, _ = / [^ ]/g, w = 0, x = 0, C = 0, j = ''; (i = _.exec(s)); ) - (C = i.index) - w > o && - ((u = x > w ? x : C), (j += '\n' + s.slice(w, u)), (w = u + 1)), - (x = C); - return ( - (j += '\n'), - s.length - w > o && x > w - ? (j += s.slice(w, x) + '\n' + s.slice(x + 1)) - : (j += s.slice(w)), - j.slice(1) - ); - } - function writeBlockSequence(s, o, i, u) { - var _, - w, - x, - C = '', - j = s.tag; - for (_ = 0, w = i.length; _ < w; _ += 1) - (x = i[_]), - s.replacer && (x = s.replacer.call(i, String(_), x)), - (writeNode(s, o + 1, x, !0, !0, !1, !0) || - (void 0 === x && writeNode(s, o + 1, null, !0, !0, !1, !0))) && - ((u && '' === C) || (C += generateNextLine(s, o)), - s.dump && 10 === s.dump.charCodeAt(0) ? (C += '-') : (C += '- '), - (C += s.dump)); - (s.tag = j), (s.dump = C || '[]'); - } - function detectType(s, o, i) { - var u, _, w, x, C, j; - for (w = 0, x = (_ = i ? s.explicitTypes : s.implicitTypes).length; w < x; w += 1) - if ( - ((C = _[w]).instanceOf || C.predicate) && - (!C.instanceOf || ('object' == typeof o && o instanceof C.instanceOf)) && - (!C.predicate || C.predicate(o)) - ) { - if ( - (i - ? C.multi && C.representName - ? (s.tag = C.representName(o)) - : (s.tag = C.tag) - : (s.tag = '?'), - C.represent) - ) { - if ( - ((j = s.styleMap[C.tag] || C.defaultStyle), - '[object Function]' === Hr.call(C.represent)) - ) - u = C.represent(o, j); - else { - if (!Jr.call(C.represent, j)) - throw new rr('!<' + C.tag + '> tag resolver accepts not "' + j + '" style'); - u = C.represent[j](o, j); - } - s.dump = u; - } - return !0; - } - return !1; - } - function writeNode(s, o, i, u, _, w, x) { - (s.tag = null), (s.dump = i), detectType(s, i, !1) || detectType(s, i, !0); - var C, - j = Hr.call(s.dump), - L = u; - u && (u = s.flowLevel < 0 || s.flowLevel > o); - var B, - $, - V = '[object Object]' === j || '[object Array]' === j; - if ( - (V && ($ = -1 !== (B = s.duplicates.indexOf(i))), - ((null !== s.tag && '?' !== s.tag) || $ || (2 !== s.indent && o > 0)) && (_ = !1), - $ && s.usedDuplicates[B]) - ) - s.dump = '*ref_' + B; - else { - if ( - (V && $ && !s.usedDuplicates[B] && (s.usedDuplicates[B] = !0), - '[object Object]' === j) - ) - u && 0 !== Object.keys(s.dump).length - ? (!(function writeBlockMapping(s, o, i, u) { - var _, - w, - x, - C, - j, - L, - B = '', - $ = s.tag, - V = Object.keys(i); - if (!0 === s.sortKeys) V.sort(); - else if ('function' == typeof s.sortKeys) V.sort(s.sortKeys); - else if (s.sortKeys) throw new rr('sortKeys must be a boolean or a function'); - for (_ = 0, w = V.length; _ < w; _ += 1) - (L = ''), - (u && '' === B) || (L += generateNextLine(s, o)), - (C = i[(x = V[_])]), - s.replacer && (C = s.replacer.call(i, x, C)), - writeNode(s, o + 1, x, !0, !0, !0) && - ((j = - (null !== s.tag && '?' !== s.tag) || - (s.dump && s.dump.length > 1024)) && - (s.dump && 10 === s.dump.charCodeAt(0) ? (L += '?') : (L += '? ')), - (L += s.dump), - j && (L += generateNextLine(s, o)), - writeNode(s, o + 1, C, !0, j) && - (s.dump && 10 === s.dump.charCodeAt(0) ? (L += ':') : (L += ': '), - (B += L += s.dump))); - (s.tag = $), (s.dump = B || '{}'); - })(s, o, s.dump, _), - $ && (s.dump = '&ref_' + B + s.dump)) - : (!(function writeFlowMapping(s, o, i) { - var u, - _, - w, - x, - C, - j = '', - L = s.tag, - B = Object.keys(i); - for (u = 0, _ = B.length; u < _; u += 1) - (C = ''), - '' !== j && (C += ', '), - s.condenseFlow && (C += '"'), - (x = i[(w = B[u])]), - s.replacer && (x = s.replacer.call(i, w, x)), - writeNode(s, o, w, !1, !1) && - (s.dump.length > 1024 && (C += '? '), - (C += - s.dump + - (s.condenseFlow ? '"' : '') + - ':' + - (s.condenseFlow ? '' : ' ')), - writeNode(s, o, x, !1, !1) && (j += C += s.dump)); - (s.tag = L), (s.dump = '{' + j + '}'); - })(s, o, s.dump), - $ && (s.dump = '&ref_' + B + ' ' + s.dump)); - else if ('[object Array]' === j) - u && 0 !== s.dump.length - ? (s.noArrayIndent && !x && o > 0 - ? writeBlockSequence(s, o - 1, s.dump, _) - : writeBlockSequence(s, o, s.dump, _), - $ && (s.dump = '&ref_' + B + s.dump)) - : (!(function writeFlowSequence(s, o, i) { - var u, - _, - w, - x = '', - C = s.tag; - for (u = 0, _ = i.length; u < _; u += 1) - (w = i[u]), - s.replacer && (w = s.replacer.call(i, String(u), w)), - (writeNode(s, o, w, !1, !1) || - (void 0 === w && writeNode(s, o, null, !1, !1))) && - ('' !== x && (x += ',' + (s.condenseFlow ? '' : ' ')), (x += s.dump)); - (s.tag = C), (s.dump = '[' + x + ']'); - })(s, o, s.dump), - $ && (s.dump = '&ref_' + B + ' ' + s.dump)); - else { - if ('[object String]' !== j) { - if ('[object Undefined]' === j) return !1; - if (s.skipInvalid) return !1; - throw new rr('unacceptable kind of an object to dump ' + j); - } - '?' !== s.tag && writeScalar(s, s.dump, o, w, L); - } - null !== s.tag && - '?' !== s.tag && - ((C = encodeURI('!' === s.tag[0] ? s.tag.slice(1) : s.tag).replace(/!/g, '%21')), - (C = - '!' === s.tag[0] - ? '!' + C - : 'tag:yaml.org,2002:' === C.slice(0, 18) - ? '!!' + C.slice(18) - : '!<' + C + '>'), - (s.dump = C + ' ' + s.dump)); - } - return !0; - } - function getDuplicateReferences(s, o) { - var i, - u, - _ = [], - w = []; - for (inspectNode(s, _, w), i = 0, u = w.length; i < u; i += 1) o.duplicates.push(_[w[i]]); - o.usedDuplicates = new Array(u); - } - function inspectNode(s, o, i) { - var u, _, w; - if (null !== s && 'object' == typeof s) - if (-1 !== (_ = o.indexOf(s))) -1 === i.indexOf(_) && i.push(_); - else if ((o.push(s), Array.isArray(s))) - for (_ = 0, w = s.length; _ < w; _ += 1) inspectNode(s[_], o, i); - else - for (_ = 0, w = (u = Object.keys(s)).length; _ < w; _ += 1) - inspectNode(s[u[_]], o, i); - } - var Qr = function dump$1(s, o) { - var i = new State((o = o || {})); - i.noRefs || getDuplicateReferences(s, i); - var u = s; - return ( - i.replacer && (u = i.replacer.call({ '': u }, '', u)), - writeNode(i, 0, u, !0, !0) ? i.dump + '\n' : '' - ); - }; - function renamed(s, o) { - return function () { - throw new Error( - 'Function yaml.' + - s + - ' is removed in js-yaml 4. Use yaml.' + - o + - ' instead, which is now safe by default.' - ); - }; - } - var en = ar, - tn = lr, - rn = dr, - nn = _r, - sn = Er, - on = Rr, - an = Kr.load, - ln = Kr.loadAll, - cn = { dump: Qr }.dump, - un = rr, - pn = { - binary: Or, - float: br, - map: pr, - null: fr, - pairs: Mr, - set: Nr, - timestamp: xr, - bool: mr, - int: gr, - merge: kr, - omap: Ir, - seq: ur, - str: cr - }, - hn = renamed('safeLoad', 'load'), - dn = renamed('safeLoadAll', 'loadAll'), - fn = renamed('safeDump', 'dump'); - const mn = { - Type: en, - Schema: tn, - FAILSAFE_SCHEMA: rn, - JSON_SCHEMA: nn, - CORE_SCHEMA: sn, - DEFAULT_SCHEMA: on, - load: an, - loadAll: ln, - dump: cn, - YAMLException: un, - types: pn, - safeLoad: hn, - safeLoadAll: dn, - safeDump: fn - }, - gn = 'configs_update', - yn = 'configs_toggle'; - function update(s, o) { - return { type: gn, payload: { [s]: o } }; - } - function toggle(s) { - return { type: yn, payload: s }; - } - const actions_loaded = () => () => {}, - downloadConfig = (s) => (o) => { - const { - fn: { fetch: i } - } = o; - return i(s); - }, - getConfigByUrl = (s, o) => (i) => { - const { specActions: u, configsActions: _ } = i; - if (s) return _.downloadConfig(s).then(next, next); - function next(_) { - _ instanceof Error || _.status >= 400 - ? (u.updateLoadingStatus('failedConfig'), - u.updateLoadingStatus('failedConfig'), - u.updateUrl(''), - console.error(_.statusText + ' ' + s.url), - o(null)) - : o( - ((s, o) => { - try { - return mn.load(s); - } catch (s) { - return o && o.errActions.newThrownErr(new Error(s)), {}; - } - })(_.text, i) - ); - } - }, - get = (s, o) => s.getIn(Array.isArray(o) ? o : [o]), - vn = { - [gn]: (s, o) => s.merge((0, qe.fromJS)(o.payload)), - [yn]: (s, o) => { - const i = o.payload, - u = s.get(i); - return s.set(i, !u); - } - }; - function configsPlugin() { - return { statePlugins: { configs: { reducers: vn, actions: u, selectors: w } } }; - } - const setHash = (s) => - s ? history.pushState(null, null, `#${s}`) : (window.location.hash = ''); - var bn = __webpack_require__(86215), - _n = __webpack_require__.n(bn); - const En = 'layout_scroll_to', - wn = 'layout_clear_scroll'; - const Sn = { - fn: { - getScrollParent: function getScrollParent(s, o) { - const i = document.documentElement; - let u = getComputedStyle(s); - const _ = 'absolute' === u.position, - w = o ? /(auto|scroll|hidden)/ : /(auto|scroll)/; - if ('fixed' === u.position) return i; - for (let o = s; (o = o.parentElement); ) - if ( - ((u = getComputedStyle(o)), - (!_ || 'static' !== u.position) && w.test(u.overflow + u.overflowY + u.overflowX)) - ) - return o; - return i; - } - }, - statePlugins: { - layout: { - actions: { - scrollToElement: (s, o) => (i) => { - try { - (o = o || i.fn.getScrollParent(s)), _n().createScroller(o).to(s); - } catch (s) { - console.error(s); - } - }, - scrollTo: (s) => ({ type: En, payload: Array.isArray(s) ? s : [s] }), - clearScrollTo: () => ({ type: wn }), - readyToScroll: (s, o) => (i) => { - const u = i.layoutSelectors.getScrollToKey(); - $e().is(u, (0, qe.fromJS)(s)) && - (i.layoutActions.scrollToElement(o), i.layoutActions.clearScrollTo()); - }, - parseDeepLinkHash: - (s) => - ({ layoutActions: o, layoutSelectors: i, getConfigs: u }) => { - if (u().deepLinking && s) { - let u = s.slice(1); - '!' === u[0] && (u = u.slice(1)), '/' === u[0] && (u = u.slice(1)); - const _ = u.split('/').map((s) => s || ''), - w = i.isShownKeyFromUrlHashArray(_), - [x, C = '', j = ''] = w; - if ('operations' === x) { - const s = i.isShownKeyFromUrlHashArray([C]); - C.indexOf('_') > -1 && - (console.warn( - 'Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.' - ), - o.show( - s.map((s) => s.replace(/_/g, ' ')), - !0 - )), - o.show(s, !0); - } - (C.indexOf('_') > -1 || j.indexOf('_') > -1) && - (console.warn( - 'Warning: escaping deep link whitespace with `_` will be unsupported in v4.0, use `%20` instead.' - ), - o.show( - w.map((s) => s.replace(/_/g, ' ')), - !0 - )), - o.show(w, !0), - o.scrollTo(w); - } - } - }, - selectors: { - getScrollToKey: (s) => s.get('scrollToKey'), - isShownKeyFromUrlHashArray(s, o) { - const [i, u] = o; - return u ? ['operations', i, u] : i ? ['operations-tag', i] : []; - }, - urlHashArrayFromIsShownKey(s, o) { - let [i, u, _] = o; - return 'operations' == i ? [u, _] : 'operations-tag' == i ? [u] : []; - } - }, - reducers: { - [En]: (s, o) => s.set('scrollToKey', $e().fromJS(o.payload)), - [wn]: (s) => s.delete('scrollToKey') - }, - wrapActions: { - show: - (s, { getConfigs: o, layoutSelectors: i }) => - (...u) => { - if ((s(...u), o().deepLinking)) - try { - let [s, o] = u; - s = Array.isArray(s) ? s : [s]; - const _ = i.urlHashArrayFromIsShownKey(s); - if (!_.length) return; - const [w, x] = _; - if (!o) return setHash('/'); - 2 === _.length - ? setHash( - createDeepLinkPath( - `/${encodeURIComponent(w)}/${encodeURIComponent(x)}` - ) - ) - : 1 === _.length && - setHash(createDeepLinkPath(`/${encodeURIComponent(w)}`)); - } catch (s) { - console.error(s); - } - } - } - } - } - }; - var xn = __webpack_require__(2209), - kn = __webpack_require__.n(xn); - const operation_wrapper = (s, o) => - class OperationWrapper extends Pe.Component { - onLoad = (s) => { - const { operation: i } = this.props, - { tag: u, operationId: _ } = i.toObject(); - let { isShownKey: w } = i.toObject(); - (w = w || ['operations', u, _]), o.layoutActions.readyToScroll(w, s); - }; - render() { - return Pe.createElement( - 'span', - { ref: this.onLoad }, - Pe.createElement(s, this.props) - ); - } - }, - operation_tag_wrapper = (s, o) => - class OperationTagWrapper extends Pe.Component { - onLoad = (s) => { - const { tag: i } = this.props, - u = ['operations-tag', i]; - o.layoutActions.readyToScroll(u, s); - }; - render() { - return Pe.createElement( - 'span', - { ref: this.onLoad }, - Pe.createElement(s, this.props) - ); - } - }; - function deep_linking() { - return [ - Sn, - { - statePlugins: { - configs: { - wrapActions: { - loaded: - (s, o) => - (...i) => { - s(...i); - const u = decodeURIComponent(window.location.hash); - o.layoutActions.parseDeepLinkHash(u); - } - } - } - }, - wrapComponents: { operation: operation_wrapper, OperationTag: operation_tag_wrapper } - } - ]; - } - var Cn = __webpack_require__(40860), - On = __webpack_require__.n(Cn); - function transform(s) { - return s.map((s) => { - let o = 'is not of a type(s)', - i = s.get('message').indexOf(o); - if (i > -1) { - let o = s - .get('message') - .slice(i + 19) - .split(','); - return s.set( - 'message', - s.get('message').slice(0, i) + - (function makeNewMessage(s) { - return s.reduce( - (s, o, i, u) => - i === u.length - 1 && u.length > 1 - ? s + 'or ' + o - : u[i + 1] && u.length > 2 - ? s + o + ', ' - : u[i + 1] - ? s + o + ' ' - : s + o, - 'should be a' - ); - })(o) - ); - } - return s; - }); - } - var An = __webpack_require__(58156), - jn = __webpack_require__.n(An); - function parameter_oneof_transform(s, { jsSpec: o }) { - return s; - } - const In = [x, C]; - function transformErrors(s) { - let o = { jsSpec: {} }, - i = On()( - In, - (s, i) => { - try { - return i.transform(s, o).filter((s) => !!s); - } catch (o) { - return console.error('Transformer error:', o), s; - } - }, - s - ); - return i.filter((s) => !!s).map((s) => (!s.get('line') && s.get('path'), s)); - } - let Pn = { line: 0, level: 'error', message: 'Unknown error' }; - const Mn = Ut( - (s) => s, - (s) => s.get('errors', (0, qe.List)()) - ), - Tn = Ut(Mn, (s) => s.last()); - function err(o) { - return { - statePlugins: { - err: { - reducers: { - [et]: (s, { payload: o }) => { - let i = Object.assign(Pn, o, { type: 'thrown' }); - return s - .update('errors', (s) => (s || (0, qe.List)()).push((0, qe.fromJS)(i))) - .update('errors', (s) => transformErrors(s)); - }, - [tt]: (s, { payload: o }) => ( - (o = o.map((s) => (0, qe.fromJS)(Object.assign(Pn, s, { type: 'thrown' })))), - s - .update('errors', (s) => (s || (0, qe.List)()).concat((0, qe.fromJS)(o))) - .update('errors', (s) => transformErrors(s)) - ), - [rt]: (s, { payload: o }) => { - let i = (0, qe.fromJS)(o); - return ( - (i = i.set('type', 'spec')), - s - .update('errors', (s) => - (s || (0, qe.List)()).push((0, qe.fromJS)(i)).sortBy((s) => s.get('line')) - ) - .update('errors', (s) => transformErrors(s)) - ); - }, - [nt]: (s, { payload: o }) => ( - (o = o.map((s) => (0, qe.fromJS)(Object.assign(Pn, s, { type: 'spec' })))), - s - .update('errors', (s) => (s || (0, qe.List)()).concat((0, qe.fromJS)(o))) - .update('errors', (s) => transformErrors(s)) - ), - [st]: (s, { payload: o }) => { - let i = (0, qe.fromJS)(Object.assign({}, o)); - return ( - (i = i.set('type', 'auth')), - s - .update('errors', (s) => (s || (0, qe.List)()).push((0, qe.fromJS)(i))) - .update('errors', (s) => transformErrors(s)) - ); - }, - [ot]: (s, { payload: o }) => { - if (!o || !s.get('errors')) return s; - let i = s.get('errors').filter((s) => - s.keySeq().every((i) => { - const u = s.get(i), - _ = o[i]; - return !_ || u !== _; - }) - ); - return s.merge({ errors: i }); - }, - [it]: (s, { payload: o }) => { - if (!o || 'function' != typeof o) return s; - let i = s.get('errors').filter((s) => o(s)); - return s.merge({ errors: i }); - } - }, - actions: s, - selectors: j - } - } - }; - } - function opsFilter(s, o) { - return s.filter((s, i) => -1 !== i.indexOf(o)); - } - function filter() { - return { fn: { opsFilter } }; - } - var Nn = __webpack_require__(7666), - Rn = __webpack_require__.n(Nn); - const arrow_up = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 20 20', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement('path', { - d: 'M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z' - }) - ), - arrow_down = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 20 20', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement('path', { - d: 'M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z' - }) - ), - arrow = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 20 20', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement('path', { - d: 'M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z' - }) - ), - components_close = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 20 20', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement('path', { - d: 'M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z' - }) - ), - copy = ({ className: s = null, width: o = 15, height: i = 16, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 15 16', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement( - 'g', - { transform: 'translate(2, -1)' }, - Pe.createElement('path', { - fill: '#ffffff', - fillRule: 'evenodd', - d: 'M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z' - }) - ) - ), - lock = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 20 20', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement('path', { - d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z' - }) - ), - unlock = ({ className: s = null, width: o = 20, height: i = 20, ...u }) => - Pe.createElement( - 'svg', - Rn()( - { - xmlns: 'http://www.w3.org/2000/svg', - viewBox: '0 0 20 20', - className: s, - width: o, - height: i, - 'aria-hidden': 'true', - focusable: 'false' - }, - u - ), - Pe.createElement('path', { - d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z' - }) - ), - icons = () => ({ - components: { - ArrowUpIcon: arrow_up, - ArrowDownIcon: arrow_down, - ArrowIcon: arrow, - CloseIcon: components_close, - CopyIcon: copy, - LockIcon: lock, - UnlockIcon: unlock - } - }), - Dn = 'layout_update_layout', - Ln = 'layout_update_filter', - Bn = 'layout_update_mode', - Fn = 'layout_show'; - function updateLayout(s) { - return { type: Dn, payload: s }; - } - function updateFilter(s) { - return { type: Ln, payload: s }; - } - function actions_show(s, o = !0) { - return (s = normalizeArray(s)), { type: Fn, payload: { thing: s, shown: o } }; - } - function changeMode(s, o = '') { - return (s = normalizeArray(s)), { type: Bn, payload: { thing: s, mode: o } }; - } - const qn = { - [Dn]: (s, o) => s.set('layout', o.payload), - [Ln]: (s, o) => s.set('filter', o.payload), - [Fn]: (s, o) => { - const i = o.payload.shown, - u = (0, qe.fromJS)(o.payload.thing); - return s.update('shown', (0, qe.fromJS)({}), (s) => s.set(u, i)); - }, - [Bn]: (s, o) => { - let i = o.payload.thing, - u = o.payload.mode; - return s.setIn(['modes'].concat(i), (u || '') + ''); - } - }, - current = (s) => s.get('layout'), - currentFilter = (s) => s.get('filter'), - isShown = (s, o, i) => ( - (o = normalizeArray(o)), s.get('shown', (0, qe.fromJS)({})).get((0, qe.fromJS)(o), i) - ), - whatMode = (s, o, i = '') => ((o = normalizeArray(o)), s.getIn(['modes', ...o], i)), - $n = Ut( - (s) => s, - (s) => !isShown(s, 'editor') - ), - taggedOperations = - (s, o) => - (i, ...u) => { - let _ = s(i, ...u); - const { fn: w, layoutSelectors: x, getConfigs: C } = o.getSystem(), - j = C(), - { maxDisplayedTags: L } = j; - let B = x.currentFilter(); - return B && !0 !== B && (_ = w.opsFilter(_, B)), L >= 0 && (_ = _.slice(0, L)), _; - }; - function plugins_layout() { - return { - statePlugins: { - layout: { reducers: qn, actions: L, selectors: B }, - spec: { wrapSelectors: $ } - } - }; - } - function logs({ configs: s }) { - const o = { debug: 0, info: 1, log: 2, warn: 3, error: 4 }, - getLevel = (s) => o[s] || -1; - let { logLevel: i } = s, - u = getLevel(i); - function log(s, ...o) { - getLevel(s) >= u && console[s](...o); - } - return ( - (log.warn = log.bind(null, 'warn')), - (log.error = log.bind(null, 'error')), - (log.info = log.bind(null, 'info')), - (log.debug = log.bind(null, 'debug')), - { rootInjects: { log } } - ); - } - let Vn = !1; - function on_complete() { - return { - statePlugins: { - spec: { - wrapActions: { - updateSpec: - (s) => - (...o) => ((Vn = !0), s(...o)), - updateJsonSpec: - (s, o) => - (...i) => { - const u = o.getConfigs().onComplete; - return Vn && 'function' == typeof u && (setTimeout(u, 0), (Vn = !1)), s(...i); - } - } - } - } - }; - } - const extractKey = (s) => { - const o = '_**[]'; - return s.indexOf(o) < 0 ? s : s.split(o)[0].trim(); - }, - escapeShell = (s) => - '-d ' === s || /^[_\/-]/g.test(s) ? s : "'" + s.replace(/'/g, "'\\''") + "'", - escapeCMD = (s) => - '-d ' === - (s = s - .replace(/\^/g, '^^') - .replace(/\\"/g, '\\\\"') - .replace(/"/g, '""') - .replace(/\n/g, '^\n')) - ? s.replace(/-d /g, '-d ^\n') - : /^[_\/-]/g.test(s) - ? s - : '"' + s + '"', - escapePowershell = (s) => { - if ('-d ' === s) return s; - if (/\n/.test(s)) { - return `@"\n${s.replace(/`/g, '``').replace(/\$/g, '`$')}\n"@`; - } - if (!/^[_\/-]/.test(s)) { - return `'${s.replace(/'/g, "''")}'`; - } - return s; - }; - const curlify = (s, o, i, u = '') => { - let _ = !1, - w = ''; - const addWords = (...s) => (w += ' ' + s.map(o).join(' ')), - addWordsWithoutLeadingSpace = (...s) => (w += s.map(o).join(' ')), - addNewLine = () => (w += ` ${i}`), - addIndent = (s = 1) => (w += ' '.repeat(s)); - let x = s.get('headers'); - w += 'curl' + u; - const C = s.get('curlOptions'); - if ( - (qe.List.isList(C) && !C.isEmpty() && addWords(...s.get('curlOptions')), - addWords('-X', s.get('method')), - addNewLine(), - addIndent(), - addWordsWithoutLeadingSpace(`${s.get('url')}`), - x && x.size) - ) - for (let o of s.get('headers').entries()) { - addNewLine(), addIndent(); - let [s, i] = o; - addWordsWithoutLeadingSpace('-H', `${s}: ${i}`), - (_ = _ || (/^content-type$/i.test(s) && /^multipart\/form-data$/i.test(i))); - } - const j = s.get('body'); - if (j) - if (_ && ['POST', 'PUT', 'PATCH'].includes(s.get('method'))) - for (let [s, o] of j.entrySeq()) { - let i = extractKey(s); - addNewLine(), - addIndent(), - addWordsWithoutLeadingSpace('-F'), - o instanceof at.File && 'string' == typeof o.valueOf() - ? addWords(`${i}=${o.data}${o.type ? `;type=${o.type}` : ''}`) - : o instanceof at.File - ? addWords(`${i}=@${o.name}${o.type ? `;type=${o.type}` : ''}`) - : addWords(`${i}=${o}`); - } - else if (j instanceof at.File) - addNewLine(), - addIndent(), - addWordsWithoutLeadingSpace(`--data-binary '@${j.name}'`); - else { - addNewLine(), addIndent(), addWordsWithoutLeadingSpace('-d '); - let o = j; - qe.Map.isMap(o) - ? addWordsWithoutLeadingSpace( - (function getStringBodyOfMap(s) { - let o = []; - for (let [i, u] of s.get('body').entrySeq()) { - let s = extractKey(i); - u instanceof at.File - ? o.push( - ` "${s}": {\n "name": "${u.name}"${u.type ? `,\n "type": "${u.type}"` : ''}\n }` - ) - : o.push( - ` "${s}": ${JSON.stringify(u, null, 2).replace(/(\r\n|\r|\n)/g, '\n ')}` - ); - } - return `{\n${o.join(',\n')}\n}`; - })(s) - ) - : ('string' != typeof o && (o = JSON.stringify(o)), - addWordsWithoutLeadingSpace(o)); - } - else - j || - 'POST' !== s.get('method') || - (addNewLine(), addIndent(), addWordsWithoutLeadingSpace("-d ''")); - return w; - }, - requestSnippetGenerator_curl_powershell = (s) => - curlify(s, escapePowershell, '`\n', '.exe'), - requestSnippetGenerator_curl_bash = (s) => curlify(s, escapeShell, '\\\n'), - requestSnippetGenerator_curl_cmd = (s) => curlify(s, escapeCMD, '^\n'), - request_snippets_selectors_state = (s) => s || (0, qe.Map)(), - Un = Ut(request_snippets_selectors_state, (s) => { - const o = s.get('languages'), - i = s.get('generators', (0, qe.Map)()); - return !o || o.isEmpty() ? i : i.filter((s, i) => o.includes(i)); - }), - getSnippetGenerators = - (s) => - ({ fn: o }) => - Un(s) - .map((s, i) => { - const u = ((s) => o[`requestSnippetGenerator_${s}`])(i); - return 'function' != typeof u ? null : s.set('fn', u); - }) - .filter((s) => s), - zn = Ut(request_snippets_selectors_state, (s) => s.get('activeLanguage')), - Wn = Ut(request_snippets_selectors_state, (s) => s.get('defaultExpanded')); - var Kn = __webpack_require__(46942), - Hn = __webpack_require__.n(Kn), - Jn = __webpack_require__(59399); - const Gn = { - cursor: 'pointer', - lineHeight: 1, - display: 'inline-flex', - backgroundColor: 'rgb(250, 250, 250)', - paddingBottom: '0', - paddingTop: '0', - border: '1px solid rgb(51, 51, 51)', - borderRadius: '4px 4px 0 0', - boxShadow: 'none', - borderBottom: 'none' - }, - Yn = { - cursor: 'pointer', - lineHeight: 1, - display: 'inline-flex', - backgroundColor: 'rgb(51, 51, 51)', - boxShadow: 'none', - border: '1px solid rgb(51, 51, 51)', - paddingBottom: '0', - paddingTop: '0', - borderRadius: '4px 4px 0 0', - marginTop: '-5px', - marginRight: '-5px', - marginLeft: '-5px', - zIndex: '9999', - borderBottom: 'none' - }, - request_snippets = ({ request: s, requestSnippetsSelectors: o, getComponent: i }) => { - const u = (0, Pe.useRef)(null), - _ = i('ArrowUpIcon'), - w = i('ArrowDownIcon'), - x = i('SyntaxHighlighter', !0), - [C, j] = (0, Pe.useState)(o.getSnippetGenerators()?.keySeq().first()), - [L, B] = (0, Pe.useState)(o?.getDefaultExpanded()), - $ = o.getSnippetGenerators(), - V = $.get(C), - U = V.get('fn')(s), - handleSetIsExpanded = () => { - B(!L); - }, - handleGetBtnStyle = (s) => (s === C ? Yn : Gn), - handlePreventYScrollingBeyondElement = (s) => { - const { target: o, deltaY: i } = s, - { scrollHeight: u, offsetHeight: _, scrollTop: w } = o; - u > _ && ((0 === w && i < 0) || (_ + w >= u && i > 0)) && s.preventDefault(); - }; - return ( - (0, Pe.useEffect)(() => {}, []), - (0, Pe.useEffect)(() => { - const s = Array.from(u.current.childNodes).filter( - (s) => !!s.nodeType && s.classList?.contains('curl-command') - ); - return ( - s.forEach((s) => - s.addEventListener('mousewheel', handlePreventYScrollingBeyondElement, { - passive: !1 - }) - ), - () => { - s.forEach((s) => - s.removeEventListener('mousewheel', handlePreventYScrollingBeyondElement) - ); - } - ); - }, [s]), - Pe.createElement( - 'div', - { className: 'request-snippets', ref: u }, - Pe.createElement( - 'div', - { - style: { - width: '100%', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - marginBottom: '15px' - } - }, - Pe.createElement( - 'h4', - { onClick: () => handleSetIsExpanded(), style: { cursor: 'pointer' } }, - 'Snippets' - ), - Pe.createElement( - 'button', - { - onClick: () => handleSetIsExpanded(), - style: { border: 'none', background: 'none' }, - title: L ? 'Collapse operation' : 'Expand operation' - }, - L - ? Pe.createElement(w, { className: 'arrow', width: '10', height: '10' }) - : Pe.createElement(_, { className: 'arrow', width: '10', height: '10' }) - ) - ), - L && - Pe.createElement( - 'div', - { className: 'curl-command' }, - Pe.createElement( - 'div', - { - style: { - paddingLeft: '15px', - paddingRight: '10px', - width: '100%', - display: 'flex' - } - }, - $.entrySeq().map(([s, o]) => - Pe.createElement( - 'div', - { - className: Hn()('btn', { active: s === C }), - style: handleGetBtnStyle(s), - key: s, - onClick: () => - ((s) => { - C !== s && j(s); - })(s) - }, - Pe.createElement( - 'h4', - { style: s === C ? { color: 'white' } : {} }, - o.get('title') - ) - ) - ) - ), - Pe.createElement( - 'div', - { className: 'copy-to-clipboard' }, - Pe.createElement( - Jn.CopyToClipboard, - { text: U }, - Pe.createElement('button', null) - ) - ), - Pe.createElement( - 'div', - null, - Pe.createElement( - x, - { - language: V.get('syntax'), - className: 'curl microlight', - renderPlainText: ({ children: s, PlainTextViewer: o }) => - Pe.createElement(o, { className: 'curl' }, s) - }, - U - ) - ) - ) - ) - ); - }, - plugins_request_snippets = () => ({ - components: { RequestSnippets: request_snippets }, - fn: V, - statePlugins: { requestSnippets: { selectors: U } } - }); - class ModelCollapse extends Pe.Component { - static defaultProps = { - collapsedContent: '{...}', - expanded: !1, - title: null, - onToggle: () => {}, - hideSelfOnExpand: !1, - specPath: $e().List([]) - }; - constructor(s, o) { - super(s, o); - let { expanded: i, collapsedContent: u } = this.props; - this.state = { - expanded: i, - collapsedContent: u || ModelCollapse.defaultProps.collapsedContent - }; - } - componentDidMount() { - const { hideSelfOnExpand: s, expanded: o, modelName: i } = this.props; - s && o && this.props.onToggle(i, o); - } - UNSAFE_componentWillReceiveProps(s) { - this.props.expanded !== s.expanded && this.setState({ expanded: s.expanded }); - } - toggleCollapsed = () => { - this.props.onToggle && this.props.onToggle(this.props.modelName, !this.state.expanded), - this.setState({ expanded: !this.state.expanded }); - }; - onLoad = (s) => { - if (s && this.props.layoutSelectors) { - const o = this.props.layoutSelectors.getScrollToKey(); - $e().is(o, this.props.specPath) && this.toggleCollapsed(), - this.props.layoutActions.readyToScroll(this.props.specPath, s.parentElement); - } - }; - render() { - const { title: s, classes: o } = this.props; - return this.state.expanded && this.props.hideSelfOnExpand - ? Pe.createElement('span', { className: o || '' }, this.props.children) - : Pe.createElement( - 'span', - { className: o || '', ref: this.onLoad }, - Pe.createElement( - 'button', - { - 'aria-expanded': this.state.expanded, - className: 'model-box-control', - onClick: this.toggleCollapsed - }, - s && Pe.createElement('span', { className: 'pointer' }, s), - Pe.createElement('span', { - className: 'model-toggle' + (this.state.expanded ? '' : ' collapsed') - }), - !this.state.expanded && - Pe.createElement('span', null, this.state.collapsedContent) - ), - this.state.expanded && this.props.children - ); - } - } - const useTabs = ({ initialTab: s, isExecute: o, schema: i, example: u }) => { - const _ = (0, Pe.useMemo)(() => ({ example: 'example', model: 'model' }), []), - w = (0, Pe.useMemo)(() => Object.keys(_), [_]).includes(s) && i && !o ? s : _.example, - x = ((s) => { - const o = (0, Pe.useRef)(); - return ( - (0, Pe.useEffect)(() => { - o.current = s; - }), - o.current - ); - })(o), - [C, j] = (0, Pe.useState)(w), - L = (0, Pe.useCallback)((s) => { - j(s.target.dataset.name); - }, []); - return ( - (0, Pe.useEffect)(() => { - x && !o && u && j(_.example); - }, [x, o, u]), - { activeTab: C, onTabChange: L, tabs: _ } - ); - }, - model_example = ({ - schema: s, - example: o, - isExecute: i = !1, - specPath: u, - includeWriteOnly: _ = !1, - includeReadOnly: w = !1, - getComponent: x, - getConfigs: C, - specSelectors: j - }) => { - const { defaultModelRendering: L, defaultModelExpandDepth: B } = C(), - $ = x('ModelWrapper'), - V = x('HighlightCode', !0), - U = St()(5).toString('base64'), - z = St()(5).toString('base64'), - Y = St()(5).toString('base64'), - Z = St()(5).toString('base64'), - ee = j.isOAS3(), - { - activeTab: ie, - tabs: ae, - onTabChange: le - } = useTabs({ initialTab: L, isExecute: i, schema: s, example: o }); - return Pe.createElement( - 'div', - { className: 'model-example' }, - Pe.createElement( - 'ul', - { className: 'tab', role: 'tablist' }, - Pe.createElement( - 'li', - { - className: Hn()('tabitem', { active: ie === ae.example }), - role: 'presentation' - }, - Pe.createElement( - 'button', - { - 'aria-controls': z, - 'aria-selected': ie === ae.example, - className: 'tablinks', - 'data-name': 'example', - id: U, - onClick: le, - role: 'tab' - }, - i ? 'Edit Value' : 'Example Value' - ) - ), - s && - Pe.createElement( - 'li', - { - className: Hn()('tabitem', { active: ie === ae.model }), - role: 'presentation' - }, - Pe.createElement( - 'button', - { - 'aria-controls': Z, - 'aria-selected': ie === ae.model, - className: Hn()('tablinks', { inactive: i }), - 'data-name': 'model', - id: Y, - onClick: le, - role: 'tab' - }, - ee ? 'Schema' : 'Model' - ) - ) - ), - ie === ae.example && - Pe.createElement( - 'div', - { - 'aria-hidden': ie !== ae.example, - 'aria-labelledby': U, - 'data-name': 'examplePanel', - id: z, - role: 'tabpanel', - tabIndex: '0' - }, - o || Pe.createElement(V, null, '(no example available') - ), - ie === ae.model && - Pe.createElement( - 'div', - { - 'aria-hidden': ie === ae.example, - 'aria-labelledby': Y, - 'data-name': 'modelPanel', - id: Z, - role: 'tabpanel', - tabIndex: '0' - }, - Pe.createElement($, { - schema: s, - getComponent: x, - getConfigs: C, - specSelectors: j, - expandDepth: B, - specPath: u, - includeReadOnly: w, - includeWriteOnly: _ - }) - ) - ); - }; - class ModelWrapper extends Pe.Component { - onToggle = (s, o) => { - this.props.layoutActions && this.props.layoutActions.show(this.props.fullPath, o); - }; - render() { - let { getComponent: s, getConfigs: o } = this.props; - const i = s('Model'); - let u; - return ( - this.props.layoutSelectors && - (u = this.props.layoutSelectors.isShown(this.props.fullPath)), - Pe.createElement( - 'div', - { className: 'model-box' }, - Pe.createElement( - i, - Rn()({}, this.props, { - getConfigs: o, - expanded: u, - depth: 1, - onToggle: this.onToggle, - expandDepth: this.props.expandDepth || 0 - }) - ) - ) - ); - } - } - function _typeof(s) { - return ( - (_typeof = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (s) { - return typeof s; - } - : function (s) { - return s && - 'function' == typeof Symbol && - s.constructor === Symbol && - s !== Symbol.prototype - ? 'symbol' - : typeof s; - }), - _typeof(s) - ); - } - function _defineProperties(s, o) { - for (var i = 0; i < o.length; i++) { - var u = o[i]; - (u.enumerable = u.enumerable || !1), - (u.configurable = !0), - 'value' in u && (u.writable = !0), - Object.defineProperty(s, u.key, u); - } - } - function _defineProperty(s, o, i) { - return ( - o in s - ? Object.defineProperty(s, o, { - value: i, - enumerable: !0, - configurable: !0, - writable: !0 - }) - : (s[o] = i), - s - ); - } - function ownKeys(s, o) { - var i = Object.keys(s); - if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); - o && - (u = u.filter(function (o) { - return Object.getOwnPropertyDescriptor(s, o).enumerable; - })), - i.push.apply(i, u); - } - return i; - } - function _getPrototypeOf(s) { - return ( - (_getPrototypeOf = Object.setPrototypeOf - ? Object.getPrototypeOf - : function _getPrototypeOf(s) { - return s.__proto__ || Object.getPrototypeOf(s); - }), - _getPrototypeOf(s) - ); - } - function _setPrototypeOf(s, o) { - return ( - (_setPrototypeOf = - Object.setPrototypeOf || - function _setPrototypeOf(s, o) { - return (s.__proto__ = o), s; - }), - _setPrototypeOf(s, o) - ); - } - function _possibleConstructorReturn(s, o) { - return !o || ('object' != typeof o && 'function' != typeof o) - ? (function _assertThisInitialized(s) { - if (void 0 === s) - throw new ReferenceError( - "this hasn't been initialised - super() hasn't been called" - ); - return s; - })(s) - : o; - } - var Xn = {}; - function react_immutable_pure_component_es_get(s, o, i) { - return (function isInvalid(s) { - return null == s; - })(s) - ? i - : (function isMapLike(s) { - return ( - null !== s && - 'object' === _typeof(s) && - 'function' == typeof s.get && - 'function' == typeof s.has - ); - })(s) - ? s.has(o) - ? s.get(o) - : i - : hasOwnProperty.call(s, o) - ? s[o] - : i; - } - function getIn(s, o, i) { - for (var u = 0; u !== o.length; ) - if ((s = react_immutable_pure_component_es_get(s, o[u++], Xn)) === Xn) return i; - return s; - } - function check(s) { - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, - i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}, - u = (function createChecker(s, o) { - return function (i) { - if ('string' == typeof i) return (0, qe.is)(o[i], s[i]); - if (Array.isArray(i)) return (0, qe.is)(getIn(o, i), getIn(s, i)); - throw new TypeError('Invalid key: expected Array or string: ' + i); - }; - })(o, i), - _ = - s || - Object.keys( - (function _objectSpread2(s) { - for (var o = 1; o < arguments.length; o++) { - var i = null != arguments[o] ? arguments[o] : {}; - o % 2 - ? ownKeys(i, !0).forEach(function (o) { - _defineProperty(s, o, i[o]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) - : ownKeys(i).forEach(function (o) { - Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); - }); - } - return s; - })({}, i, {}, o) - ); - return _.every(u); - } - const Zn = (function (s) { - function ImmutablePureComponent() { - return ( - (function _classCallCheck(s, o) { - if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); - })(this, ImmutablePureComponent), - _possibleConstructorReturn( - this, - _getPrototypeOf(ImmutablePureComponent).apply(this, arguments) - ) - ); - } - return ( - (function _inherits(s, o) { - if ('function' != typeof o && null !== o) - throw new TypeError('Super expression must either be null or a function'); - (s.prototype = Object.create(o && o.prototype, { - constructor: { value: s, writable: !0, configurable: !0 } - })), - o && _setPrototypeOf(s, o); - })(ImmutablePureComponent, s), - (function _createClass(s, o, i) { - return o && _defineProperties(s.prototype, o), i && _defineProperties(s, i), s; - })(ImmutablePureComponent, [ - { - key: 'shouldComponentUpdate', - value: function shouldComponentUpdate(s) { - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; - return ( - !check(this.updateOnProps, this.props, s, 'updateOnProps') || - !check(this.updateOnStates, this.state, o, 'updateOnStates') - ); - } - } - ]), - ImmutablePureComponent - ); - })(Pe.Component); - var Qn, - es = __webpack_require__(5556), - ts = __webpack_require__.n(es); - function _extends() { - return ( - (_extends = Object.assign - ? Object.assign.bind() - : function (s) { - for (var o = 1; o < arguments.length; o++) { - var i = arguments[o]; - for (var u in i) ({}).hasOwnProperty.call(i, u) && (s[u] = i[u]); - } - return s; - }), - _extends.apply(null, arguments) - ); - } - const rolling_load = (s) => - Pe.createElement( - 'svg', - _extends( - { - xmlns: 'http://www.w3.org/2000/svg', - width: 200, - height: 200, - className: 'rolling-load_svg__lds-rolling', - preserveAspectRatio: 'xMidYMid', - style: { - backgroundImage: 'none', - backgroundPosition: 'initial initial', - backgroundRepeat: 'initial initial' - }, - viewBox: '0 0 100 100' - }, - s - ), - Qn || - (Qn = Pe.createElement( - 'circle', - { - cx: 50, - cy: 50, - r: 35, - fill: 'none', - stroke: '#555', - strokeDasharray: '164.93361431346415 56.97787143782138', - strokeWidth: 10 - }, - Pe.createElement('animateTransform', { - attributeName: 'transform', - begin: '0s', - calcMode: 'linear', - dur: '1s', - keyTimes: '0;1', - repeatCount: 'indefinite', - type: 'rotate', - values: '0 50 50;360 50 50' - }) - )) - ), - decodeRefName = (s) => { - const o = s.replace(/~1/g, '/').replace(/~0/g, '~'); - try { - return decodeURIComponent(o); - } catch { - return o; - } - }; - class Model extends Zn { - static propTypes = { - schema: kn().map.isRequired, - getComponent: ts().func.isRequired, - getConfigs: ts().func.isRequired, - specSelectors: ts().object.isRequired, - name: ts().string, - displayName: ts().string, - isRef: ts().bool, - required: ts().bool, - expandDepth: ts().number, - depth: ts().number, - specPath: kn().list.isRequired, - includeReadOnly: ts().bool, - includeWriteOnly: ts().bool - }; - getModelName = (s) => - -1 !== s.indexOf('#/definitions/') - ? decodeRefName(s.replace(/^.*#\/definitions\//, '')) - : -1 !== s.indexOf('#/components/schemas/') - ? decodeRefName(s.replace(/^.*#\/components\/schemas\//, '')) - : void 0; - getRefSchema = (s) => { - let { specSelectors: o } = this.props; - return o.findDefinition(s); - }; - render() { - let { - getComponent: s, - getConfigs: o, - specSelectors: i, - schema: u, - required: _, - name: w, - isRef: x, - specPath: C, - displayName: j, - includeReadOnly: L, - includeWriteOnly: B - } = this.props; - const $ = s('ObjectModel'), - V = s('ArrayModel'), - U = s('PrimitiveModel'); - let z = 'object', - Y = u && u.get('$$ref'), - Z = u && u.get('$ref'); - if ((!w && Y && (w = this.getModelName(Y)), Z)) { - const s = this.getModelName(Z), - o = this.getRefSchema(s); - qe.Map.isMap(o) - ? ((u = o.mergeDeep(u)), Y || ((u = u.set('$$ref', Z)), (Y = Z))) - : qe.Map.isMap(u) && 1 === u.size && ((u = null), (w = Z)); - } - if (!u) - return Pe.createElement( - 'span', - { className: 'model model-title' }, - Pe.createElement('span', { className: 'model-title__text' }, j || w), - !Z && Pe.createElement(rolling_load, { height: '20px', width: '20px' }) - ); - const ee = i.isOAS3() && u.get('deprecated'); - switch (((x = void 0 !== x ? x : !!Y), (z = (u && u.get('type')) || z), z)) { - case 'object': - return Pe.createElement( - $, - Rn()({ className: 'object' }, this.props, { - specPath: C, - getConfigs: o, - schema: u, - name: w, - deprecated: ee, - isRef: x, - includeReadOnly: L, - includeWriteOnly: B - }) - ); - case 'array': - return Pe.createElement( - V, - Rn()({ className: 'array' }, this.props, { - getConfigs: o, - schema: u, - name: w, - deprecated: ee, - required: _, - includeReadOnly: L, - includeWriteOnly: B - }) - ); - default: - return Pe.createElement( - U, - Rn()({}, this.props, { - getComponent: s, - getConfigs: o, - schema: u, - name: w, - deprecated: ee, - required: _ - }) - ); - } - } - } - class Models extends Pe.Component { - getSchemaBasePath = () => - this.props.specSelectors.isOAS3() ? ['components', 'schemas'] : ['definitions']; - getCollapsedContent = () => ' '; - handleToggle = (s, o) => { - const { layoutActions: i } = this.props; - i.show([...this.getSchemaBasePath(), s], o), - o && this.props.specActions.requestResolvedSubtree([...this.getSchemaBasePath(), s]); - }; - onLoadModels = (s) => { - s && this.props.layoutActions.readyToScroll(this.getSchemaBasePath(), s); - }; - onLoadModel = (s) => { - if (s) { - const o = s.getAttribute('data-name'); - this.props.layoutActions.readyToScroll([...this.getSchemaBasePath(), o], s); - } - }; - render() { - let { - specSelectors: s, - getComponent: o, - layoutSelectors: i, - layoutActions: u, - getConfigs: _ - } = this.props, - w = s.definitions(), - { docExpansion: x, defaultModelsExpandDepth: C } = _(); - if (!w.size || C < 0) return null; - const j = this.getSchemaBasePath(); - let L = i.isShown(j, C > 0 && 'none' !== x); - const B = s.isOAS3(), - $ = o('ModelWrapper'), - V = o('Collapse'), - U = o('ModelCollapse'), - z = o('JumpToPath', !0), - Y = o('ArrowUpIcon'), - Z = o('ArrowDownIcon'); - return Pe.createElement( - 'section', - { className: L ? 'models is-open' : 'models', ref: this.onLoadModels }, - Pe.createElement( - 'h4', - null, - Pe.createElement( - 'button', - { 'aria-expanded': L, className: 'models-control', onClick: () => u.show(j, !L) }, - Pe.createElement('span', null, B ? 'Schemas' : 'Models'), - L ? Pe.createElement(Y, null) : Pe.createElement(Z, null) - ) - ), - Pe.createElement( - V, - { isOpened: L }, - w - .entrySeq() - .map(([w]) => { - const x = [...j, w], - L = $e().List(x), - B = s.specResolvedSubtree(x), - V = s.specJson().getIn(x), - Y = qe.Map.isMap(B) ? B : $e().Map(), - Z = qe.Map.isMap(V) ? V : $e().Map(), - ee = Y.get('title') || Z.get('title') || w, - ie = i.isShown(x, !1); - ie && - 0 === Y.size && - Z.size > 0 && - this.props.specActions.requestResolvedSubtree(x); - const ae = Pe.createElement($, { - name: w, - expandDepth: C, - schema: Y || $e().Map(), - displayName: ee, - fullPath: x, - specPath: L, - getComponent: o, - specSelectors: s, - getConfigs: _, - layoutSelectors: i, - layoutActions: u, - includeReadOnly: !0, - includeWriteOnly: !0 - }), - le = Pe.createElement( - 'span', - { className: 'model-box' }, - Pe.createElement('span', { className: 'model model-title' }, ee) - ); - return Pe.createElement( - 'div', - { - id: `model-${w}`, - className: 'model-container', - key: `models-section-${w}`, - 'data-name': w, - ref: this.onLoadModel - }, - Pe.createElement( - 'span', - { className: 'models-jump-to-path' }, - Pe.createElement(z, { specPath: L }) - ), - Pe.createElement( - U, - { - classes: 'model-box', - collapsedContent: this.getCollapsedContent(w), - onToggle: this.handleToggle, - title: le, - displayName: ee, - modelName: w, - specPath: L, - layoutSelectors: i, - layoutActions: u, - hideSelfOnExpand: !0, - expanded: C > 0 && ie - }, - ae - ) - ); - }) - .toArray() - ) - ); - } - } - const enum_model = ({ value: s, getComponent: o }) => { - let i = o('ModelCollapse'), - u = Pe.createElement('span', null, 'Array [ ', s.count(), ' ]'); - return Pe.createElement( - 'span', - { className: 'prop-enum' }, - 'Enum:', - Pe.createElement('br', null), - Pe.createElement(i, { collapsedContent: u }, '[ ', s.map(String).join(', '), ' ]') - ); - }; - class ObjectModel extends Pe.Component { - render() { - let { - schema: s, - name: o, - displayName: i, - isRef: u, - getComponent: _, - getConfigs: w, - depth: x, - onToggle: C, - expanded: j, - specPath: L, - ...B - } = this.props, - { specSelectors: $, expandDepth: V, includeReadOnly: U, includeWriteOnly: z } = B; - const { isOAS3: Y } = $; - if (!s) return null; - const { showExtensions: Z } = w(); - let ee = s.get('description'), - ie = s.get('properties'), - ae = s.get('additionalProperties'), - le = s.get('title') || i || o, - ce = s.get('required'), - pe = s.filter( - (s, o) => - -1 !== ['maxProperties', 'minProperties', 'nullable', 'example'].indexOf(o) - ), - de = s.get('deprecated'), - fe = s.getIn(['externalDocs', 'url']), - ye = s.getIn(['externalDocs', 'description']); - const be = _('JumpToPath', !0), - _e = _('Markdown', !0), - we = _('Model'), - Se = _('ModelCollapse'), - xe = _('Property'), - Te = _('Link'), - JumpToPathSection = () => - Pe.createElement( - 'span', - { className: 'model-jump-to-path' }, - Pe.createElement(be, { specPath: L }) - ), - Re = Pe.createElement( - 'span', - null, - Pe.createElement('span', null, '{'), - '...', - Pe.createElement('span', null, '}'), - u ? Pe.createElement(JumpToPathSection, null) : '' - ), - $e = $.isOAS3() ? s.get('allOf') : null, - ze = $.isOAS3() ? s.get('anyOf') : null, - We = $.isOAS3() ? s.get('oneOf') : null, - He = $.isOAS3() ? s.get('not') : null, - Ye = - le && - Pe.createElement( - 'span', - { className: 'model-title' }, - u && - s.get('$$ref') && - Pe.createElement('span', { className: 'model-hint' }, s.get('$$ref')), - Pe.createElement('span', { className: 'model-title__text' }, le) - ); - return Pe.createElement( - 'span', - { className: 'model' }, - Pe.createElement( - Se, - { - modelName: o, - title: Ye, - onToggle: C, - expanded: !!j || x <= V, - collapsedContent: Re - }, - Pe.createElement('span', { className: 'brace-open object' }, '{'), - u ? Pe.createElement(JumpToPathSection, null) : null, - Pe.createElement( - 'span', - { className: 'inner-object' }, - Pe.createElement( - 'table', - { className: 'model' }, - Pe.createElement( - 'tbody', - null, - ee - ? Pe.createElement( - 'tr', - { className: 'description' }, - Pe.createElement('td', null, 'description:'), - Pe.createElement('td', null, Pe.createElement(_e, { source: ee })) - ) - : null, - fe && - Pe.createElement( - 'tr', - { className: 'external-docs' }, - Pe.createElement('td', null, 'externalDocs:'), - Pe.createElement( - 'td', - null, - Pe.createElement( - Te, - { target: '_blank', href: sanitizeUrl(fe) }, - ye || fe - ) - ) - ), - de - ? Pe.createElement( - 'tr', - { className: 'property' }, - Pe.createElement('td', null, 'deprecated:'), - Pe.createElement('td', null, 'true') - ) - : null, - ie && ie.size - ? ie - .entrySeq() - .filter( - ([, s]) => (!s.get('readOnly') || U) && (!s.get('writeOnly') || z) - ) - .map(([s, i]) => { - let u = Y() && i.get('deprecated'), - C = qe.List.isList(ce) && ce.contains(s), - j = ['property-row']; - return ( - u && j.push('deprecated'), - C && j.push('required'), - Pe.createElement( - 'tr', - { key: s, className: j.join(' ') }, - Pe.createElement( - 'td', - null, - s, - C && Pe.createElement('span', { className: 'star' }, '*') - ), - Pe.createElement( - 'td', - null, - Pe.createElement( - we, - Rn()({ key: `object-${o}-${s}_${i}` }, B, { - required: C, - getComponent: _, - specPath: L.push('properties', s), - getConfigs: w, - schema: i, - depth: x + 1 - }) - ) - ) - ) - ); - }) - .toArray() - : null, - Z ? Pe.createElement('tr', null, Pe.createElement('td', null, ' ')) : null, - Z - ? s - .entrySeq() - .map(([s, o]) => { - if ('x-' !== s.slice(0, 2)) return; - const i = o ? (o.toJS ? o.toJS() : o) : null; - return Pe.createElement( - 'tr', - { key: s, className: 'extension' }, - Pe.createElement('td', null, s), - Pe.createElement('td', null, JSON.stringify(i)) - ); - }) - .toArray() - : null, - ae && ae.size - ? Pe.createElement( - 'tr', - null, - Pe.createElement('td', null, '< * >:'), - Pe.createElement( - 'td', - null, - Pe.createElement( - we, - Rn()({}, B, { - required: !1, - getComponent: _, - specPath: L.push('additionalProperties'), - getConfigs: w, - schema: ae, - depth: x + 1 - }) - ) - ) - ) - : null, - $e - ? Pe.createElement( - 'tr', - null, - Pe.createElement('td', null, 'allOf ->'), - Pe.createElement( - 'td', - null, - $e.map((s, o) => - Pe.createElement( - 'div', - { key: o }, - Pe.createElement( - we, - Rn()({}, B, { - required: !1, - getComponent: _, - specPath: L.push('allOf', o), - getConfigs: w, - schema: s, - depth: x + 1 - }) - ) - ) - ) - ) - ) - : null, - ze - ? Pe.createElement( - 'tr', - null, - Pe.createElement('td', null, 'anyOf ->'), - Pe.createElement( - 'td', - null, - ze.map((s, o) => - Pe.createElement( - 'div', - { key: o }, - Pe.createElement( - we, - Rn()({}, B, { - required: !1, - getComponent: _, - specPath: L.push('anyOf', o), - getConfigs: w, - schema: s, - depth: x + 1 - }) - ) - ) - ) - ) - ) - : null, - We - ? Pe.createElement( - 'tr', - null, - Pe.createElement('td', null, 'oneOf ->'), - Pe.createElement( - 'td', - null, - We.map((s, o) => - Pe.createElement( - 'div', - { key: o }, - Pe.createElement( - we, - Rn()({}, B, { - required: !1, - getComponent: _, - specPath: L.push('oneOf', o), - getConfigs: w, - schema: s, - depth: x + 1 - }) - ) - ) - ) - ) - ) - : null, - He - ? Pe.createElement( - 'tr', - null, - Pe.createElement('td', null, 'not ->'), - Pe.createElement( - 'td', - null, - Pe.createElement( - 'div', - null, - Pe.createElement( - we, - Rn()({}, B, { - required: !1, - getComponent: _, - specPath: L.push('not'), - getConfigs: w, - schema: He, - depth: x + 1 - }) - ) - ) - ) - ) - : null - ) - ) - ), - Pe.createElement('span', { className: 'brace-close' }, '}') - ), - pe.size - ? pe.entrySeq().map(([s, o]) => - Pe.createElement(xe, { - key: `${s}-${o}`, - propKey: s, - propVal: o, - propClass: 'property' - }) - ) - : null - ); - } - } - class ArrayModel extends Pe.Component { - render() { - let { - getComponent: s, - getConfigs: o, - schema: i, - depth: u, - expandDepth: _, - name: w, - displayName: x, - specPath: C - } = this.props, - j = i.get('description'), - L = i.get('items'), - B = i.get('title') || x || w, - $ = i.filter( - (s, o) => - -1 === ['type', 'items', 'description', '$$ref', 'externalDocs'].indexOf(o) - ), - V = i.getIn(['externalDocs', 'url']), - U = i.getIn(['externalDocs', 'description']); - const z = s('Markdown', !0), - Y = s('ModelCollapse'), - Z = s('Model'), - ee = s('Property'), - ie = s('Link'), - ae = - B && - Pe.createElement( - 'span', - { className: 'model-title' }, - Pe.createElement('span', { className: 'model-title__text' }, B) - ); - return Pe.createElement( - 'span', - { className: 'model' }, - Pe.createElement( - Y, - { title: ae, expanded: u <= _, collapsedContent: '[...]' }, - '[', - $.size - ? $.entrySeq().map(([s, o]) => - Pe.createElement(ee, { - key: `${s}-${o}`, - propKey: s, - propVal: o, - propClass: 'property' - }) - ) - : null, - j - ? Pe.createElement(z, { source: j }) - : $.size - ? Pe.createElement('div', { className: 'markdown' }) - : null, - V && - Pe.createElement( - 'div', - { className: 'external-docs' }, - Pe.createElement(ie, { target: '_blank', href: sanitizeUrl(V) }, U || V) - ), - Pe.createElement( - 'span', - null, - Pe.createElement( - Z, - Rn()({}, this.props, { - getConfigs: o, - specPath: C.push('items'), - name: null, - schema: L, - required: !1, - depth: u + 1 - }) - ) - ), - ']' - ) - ); - } - } - const rs = 'property primitive'; - class Primitive extends Pe.Component { - render() { - let { - schema: s, - getComponent: o, - getConfigs: i, - name: u, - displayName: _, - depth: w, - expandDepth: x - } = this.props; - const { showExtensions: C } = i(); - if (!s || !s.get) return Pe.createElement('div', null); - let j = s.get('type'), - L = s.get('format'), - B = s.get('xml'), - $ = s.get('enum'), - V = s.get('title') || _ || u, - U = s.get('description'), - z = getExtensions(s), - Y = s - .filter( - (s, o) => - -1 === - ['enum', 'type', 'format', 'description', '$$ref', 'externalDocs'].indexOf(o) - ) - .filterNot((s, o) => z.has(o)), - Z = s.getIn(['externalDocs', 'url']), - ee = s.getIn(['externalDocs', 'description']); - const ie = o('Markdown', !0), - ae = o('EnumModel'), - le = o('Property'), - ce = o('ModelCollapse'), - pe = o('Link'), - de = - V && - Pe.createElement( - 'span', - { className: 'model-title' }, - Pe.createElement('span', { className: 'model-title__text' }, V) - ); - return Pe.createElement( - 'span', - { className: 'model' }, - Pe.createElement( - ce, - { title: de, expanded: w <= x, collapsedContent: '[...]' }, - Pe.createElement( - 'span', - { className: 'prop' }, - u && w > 1 && Pe.createElement('span', { className: 'prop-name' }, V), - Pe.createElement('span', { className: 'prop-type' }, j), - L && Pe.createElement('span', { className: 'prop-format' }, '($', L, ')'), - Y.size - ? Y.entrySeq().map(([s, o]) => - Pe.createElement(le, { - key: `${s}-${o}`, - propKey: s, - propVal: o, - propClass: rs - }) - ) - : null, - C && z.size - ? z.entrySeq().map(([s, o]) => - Pe.createElement(le, { - key: `${s}-${o}`, - propKey: s, - propVal: o, - propClass: rs - }) - ) - : null, - U ? Pe.createElement(ie, { source: U }) : null, - Z && - Pe.createElement( - 'div', - { className: 'external-docs' }, - Pe.createElement(pe, { target: '_blank', href: sanitizeUrl(Z) }, ee || Z) - ), - B && B.size - ? Pe.createElement( - 'span', - null, - Pe.createElement('br', null), - Pe.createElement('span', { className: rs }, 'xml:'), - B.entrySeq() - .map(([s, o]) => - Pe.createElement( - 'span', - { key: `${s}-${o}`, className: rs }, - Pe.createElement('br', null), - '   ', - s, - ': ', - String(o) - ) - ) - .toArray() - ) - : null, - $ && Pe.createElement(ae, { value: $, getComponent: o }) - ) - ) - ); - } - } - class Schemes extends Pe.Component { - UNSAFE_componentWillMount() { - let { schemes: s } = this.props; - this.setScheme(s.first()); - } - UNSAFE_componentWillReceiveProps(s) { - (this.props.currentScheme && s.schemes.includes(this.props.currentScheme)) || - this.setScheme(s.schemes.first()); - } - onChange = (s) => { - this.setScheme(s.target.value); - }; - setScheme = (s) => { - let { path: o, method: i, specActions: u } = this.props; - u.setScheme(s, o, i); - }; - render() { - let { schemes: s, currentScheme: o } = this.props; - return Pe.createElement( - 'label', - { htmlFor: 'schemes' }, - Pe.createElement('span', { className: 'schemes-title' }, 'Schemes'), - Pe.createElement( - 'select', - { onChange: this.onChange, value: o, id: 'schemes' }, - s - .valueSeq() - .map((s) => Pe.createElement('option', { value: s, key: s }, s)) - .toArray() - ) - ); - } - } - class SchemesContainer extends Pe.Component { - render() { - const { specActions: s, specSelectors: o, getComponent: i } = this.props, - u = o.operationScheme(), - _ = o.schemes(), - w = i('schemes'); - return _ && _.size - ? Pe.createElement(w, { currentScheme: u, schemes: _, specActions: s }) - : null; - } - } - var ns = __webpack_require__(24677), - ss = __webpack_require__.n(ns); - const os = { - value: '', - onChange: () => {}, - schema: {}, - keyName: '', - required: !1, - errors: (0, qe.List)() - }; - class JsonSchemaForm extends Pe.Component { - static defaultProps = os; - componentDidMount() { - const { dispatchInitialValue: s, value: o, onChange: i } = this.props; - s ? i(o) : !1 === s && i(''); - } - render() { - let { - schema: s, - errors: o, - value: i, - onChange: u, - getComponent: _, - fn: w, - disabled: x - } = this.props; - const C = s && s.get ? s.get('format') : null, - j = s && s.get ? s.get('type') : null; - let getComponentSilently = (s) => _(s, !1, { failSilently: !0 }), - L = j - ? getComponentSilently(C ? `JsonSchema_${j}_${C}` : `JsonSchema_${j}`) - : _('JsonSchema_string'); - return ( - L || (L = _('JsonSchema_string')), - Pe.createElement( - L, - Rn()({}, this.props, { - errors: o, - fn: w, - getComponent: _, - value: i, - onChange: u, - schema: s, - disabled: x - }) - ) - ); - } - } - class JsonSchema_string extends Pe.Component { - static defaultProps = os; - onChange = (s) => { - const o = - this.props.schema && 'file' === this.props.schema.get('type') - ? s.target.files[0] - : s.target.value; - this.props.onChange(o, this.props.keyName); - }; - onEnumChange = (s) => this.props.onChange(s); - render() { - let { - getComponent: s, - value: o, - schema: i, - errors: u, - required: _, - description: w, - disabled: x - } = this.props; - const C = i && i.get ? i.get('enum') : null, - j = i && i.get ? i.get('format') : null, - L = i && i.get ? i.get('type') : null, - B = i && i.get ? i.get('in') : null; - if ((o || (o = ''), (u = u.toJS ? u.toJS() : []), C)) { - const i = s('Select'); - return Pe.createElement(i, { - className: u.length ? 'invalid' : '', - title: u.length ? u : '', - allowedValues: [...C], - value: o, - allowEmptyValue: !_, - disabled: x, - onChange: this.onEnumChange - }); - } - const $ = x || (B && 'formData' === B && !('FormData' in window)), - V = s('Input'); - return L && 'file' === L - ? Pe.createElement(V, { - type: 'file', - className: u.length ? 'invalid' : '', - title: u.length ? u : '', - onChange: this.onChange, - disabled: $ - }) - : Pe.createElement(ss(), { - type: j && 'password' === j ? 'password' : 'text', - className: u.length ? 'invalid' : '', - title: u.length ? u : '', - value: o, - minLength: 0, - debounceTimeout: 350, - placeholder: w, - onChange: this.onChange, - disabled: $ - }); - } - } - class JsonSchema_array extends Pe.PureComponent { - static defaultProps = os; - constructor(s, o) { - super(s, o), (this.state = { value: valueOrEmptyList(s.value), schema: s.schema }); - } - UNSAFE_componentWillReceiveProps(s) { - const o = valueOrEmptyList(s.value); - o !== this.state.value && this.setState({ value: o }), - s.schema !== this.state.schema && this.setState({ schema: s.schema }); - } - onChange = () => { - this.props.onChange(this.state.value); - }; - onItemChange = (s, o) => { - this.setState(({ value: i }) => ({ value: i.set(o, s) }), this.onChange); - }; - removeItem = (s) => { - this.setState(({ value: o }) => ({ value: o.delete(s) }), this.onChange); - }; - addItem = () => { - const { fn: s } = this.props; - let o = valueOrEmptyList(this.state.value); - this.setState( - () => ({ - value: o.push( - s.getSampleSchema(this.state.schema.get('items'), !1, { includeWriteOnly: !0 }) - ) - }), - this.onChange - ); - }; - onEnumChange = (s) => { - this.setState(() => ({ value: s }), this.onChange); - }; - render() { - let { - getComponent: s, - required: o, - schema: i, - errors: u, - fn: _, - disabled: w - } = this.props; - u = u.toJS ? u.toJS() : Array.isArray(u) ? u : []; - const x = u.filter((s) => 'string' == typeof s), - C = u.filter((s) => void 0 !== s.needRemove).map((s) => s.error), - j = this.state.value, - L = !!(j && j.count && j.count() > 0), - B = i.getIn(['items', 'enum']), - $ = i.getIn(['items', 'type']), - V = i.getIn(['items', 'format']), - U = i.get('items'); - let z, - Y = !1, - Z = 'file' === $ || ('string' === $ && 'binary' === V); - if ( - ($ && V - ? (z = s(`JsonSchema_${$}_${V}`)) - : ('boolean' !== $ && 'array' !== $ && 'object' !== $) || - (z = s(`JsonSchema_${$}`)), - z || Z || (Y = !0), - B) - ) { - const i = s('Select'); - return Pe.createElement(i, { - className: u.length ? 'invalid' : '', - title: u.length ? u : '', - multiple: !0, - value: j, - disabled: w, - allowedValues: B, - allowEmptyValue: !o, - onChange: this.onEnumChange - }); - } - const ee = s('Button'); - return Pe.createElement( - 'div', - { className: 'json-schema-array' }, - L - ? j.map((o, i) => { - const x = (0, qe.fromJS)([ - ...u.filter((s) => s.index === i).map((s) => s.error) - ]); - return Pe.createElement( - 'div', - { key: i, className: 'json-schema-form-item' }, - Z - ? Pe.createElement(JsonSchemaArrayItemFile, { - value: o, - onChange: (s) => this.onItemChange(s, i), - disabled: w, - errors: x, - getComponent: s - }) - : Y - ? Pe.createElement(JsonSchemaArrayItemText, { - value: o, - onChange: (s) => this.onItemChange(s, i), - disabled: w, - errors: x - }) - : Pe.createElement( - z, - Rn()({}, this.props, { - value: o, - onChange: (s) => this.onItemChange(s, i), - disabled: w, - errors: x, - schema: U, - getComponent: s, - fn: _ - }) - ), - w - ? null - : Pe.createElement( - ee, - { - className: `btn btn-sm json-schema-form-item-remove ${C.length ? 'invalid' : null}`, - title: C.length ? C : '', - onClick: () => this.removeItem(i) - }, - ' - ' - ) - ); - }) - : null, - w - ? null - : Pe.createElement( - ee, - { - className: `btn btn-sm json-schema-form-item-add ${x.length ? 'invalid' : null}`, - title: x.length ? x : '', - onClick: this.addItem - }, - 'Add ', - $ ? `${$} ` : '', - 'item' - ) - ); - } - } - class JsonSchemaArrayItemText extends Pe.Component { - static defaultProps = os; - onChange = (s) => { - const o = s.target.value; - this.props.onChange(o, this.props.keyName); - }; - render() { - let { value: s, errors: o, description: i, disabled: u } = this.props; - return ( - s || (s = ''), - (o = o.toJS ? o.toJS() : []), - Pe.createElement(ss(), { - type: 'text', - className: o.length ? 'invalid' : '', - title: o.length ? o : '', - value: s, - minLength: 0, - debounceTimeout: 350, - placeholder: i, - onChange: this.onChange, - disabled: u - }) - ); - } - } - class JsonSchemaArrayItemFile extends Pe.Component { - static defaultProps = os; - onFileChange = (s) => { - const o = s.target.files[0]; - this.props.onChange(o, this.props.keyName); - }; - render() { - let { getComponent: s, errors: o, disabled: i } = this.props; - const u = s('Input'), - _ = i || !('FormData' in window); - return Pe.createElement(u, { - type: 'file', - className: o.length ? 'invalid' : '', - title: o.length ? o : '', - onChange: this.onFileChange, - disabled: _ - }); - } - } - class JsonSchema_boolean extends Pe.Component { - static defaultProps = os; - onEnumChange = (s) => this.props.onChange(s); - render() { - let { - getComponent: s, - value: o, - errors: i, - schema: u, - required: _, - disabled: w - } = this.props; - i = i.toJS ? i.toJS() : []; - let x = u && u.get ? u.get('enum') : null, - C = !x || !_, - j = !x && ['true', 'false']; - const L = s('Select'); - return Pe.createElement(L, { - className: i.length ? 'invalid' : '', - title: i.length ? i : '', - value: String(o), - disabled: w, - allowedValues: x ? [...x] : j, - allowEmptyValue: C, - onChange: this.onEnumChange - }); - } - } - const stringifyObjectErrors = (s) => - s.map((s) => { - const o = void 0 !== s.propKey ? s.propKey : s.index; - let i = 'string' == typeof s ? s : 'string' == typeof s.error ? s.error : null; - if (!o && i) return i; - let u = s.error, - _ = `/${s.propKey}`; - for (; 'object' == typeof u; ) { - const s = void 0 !== u.propKey ? u.propKey : u.index; - if (void 0 === s) break; - if (((_ += `/${s}`), !u.error)) break; - u = u.error; - } - return `${_}: ${u}`; - }); - class JsonSchema_object extends Pe.PureComponent { - constructor() { - super(); - } - static defaultProps = os; - onChange = (s) => { - this.props.onChange(s); - }; - handleOnChange = (s) => { - const o = s.target.value; - this.onChange(o); - }; - render() { - let { getComponent: s, value: o, errors: i, disabled: u } = this.props; - const _ = s('TextArea'); - return ( - (i = i.toJS ? i.toJS() : Array.isArray(i) ? i : []), - Pe.createElement( - 'div', - null, - Pe.createElement(_, { - className: Hn()({ invalid: i.length }), - title: i.length ? stringifyObjectErrors(i).join(', ') : '', - value: stringify(o), - disabled: u, - onChange: this.handleOnChange - }) - ) - ); - } - } - function valueOrEmptyList(s) { - return qe.List.isList(s) ? s : Array.isArray(s) ? (0, qe.fromJS)(s) : (0, qe.List)(); - } - const json_schema_5 = () => ({ - components: { - modelExample: model_example, - ModelWrapper, - ModelCollapse, - Model, - Models, - EnumModel: enum_model, - ObjectModel, - ArrayModel, - PrimitiveModel: Primitive, - schemes: Schemes, - SchemesContainer, - ...z - } - }); - var as = __webpack_require__(19123), - ls = __webpack_require__.n(as), - cs = __webpack_require__(41859), - us = __webpack_require__.n(cs), - ps = __webpack_require__(62193), - hs = __webpack_require__.n(ps); - const shallowArrayEquals = (s) => (o) => - Array.isArray(s) && - Array.isArray(o) && - s.length === o.length && - s.every((s, i) => s === o[i]), - list = (...s) => s; - class Cache extends Map { - delete(s) { - const o = Array.from(this.keys()).find(shallowArrayEquals(s)); - return super.delete(o); - } - get(s) { - const o = Array.from(this.keys()).find(shallowArrayEquals(s)); - return super.get(o); - } - has(s) { - return -1 !== Array.from(this.keys()).findIndex(shallowArrayEquals(s)); - } - } - const utils_memoizeN = (s, o = list) => { - const { Cache: i } = ut(); - ut().Cache = Cache; - const u = ut()(s, o); - return (ut().Cache = i), u; - }, - ds = { - string: (s) => - s.pattern - ? ((s) => { - try { - return new (us())(s).gen(); - } catch (s) { - return 'string'; - } - })(s.pattern) - : 'string', - string_email: () => 'user@example.com', - 'string_date-time': () => new Date().toISOString(), - string_date: () => new Date().toISOString().substring(0, 10), - string_uuid: () => '3fa85f64-5717-4562-b3fc-2c963f66afa6', - string_hostname: () => 'example.com', - string_ipv4: () => '198.51.100.42', - string_ipv6: () => '2001:0db8:5b96:0000:0000:426f:8e17:642a', - number: () => 0, - number_float: () => 0, - integer: () => 0, - boolean: (s) => 'boolean' != typeof s.default || s.default - }, - primitive = (s) => { - s = objectify(s); - let { type: o, format: i } = s, - u = ds[`${o}_${i}`] || ds[o]; - return isFunc(u) ? u(s) : 'Unknown Type: ' + s.type; - }, - sanitizeRef = (s) => - deeplyStripKey(s, '$$ref', (s) => 'string' == typeof s && s.indexOf('#') > -1), - fs = ['maxProperties', 'minProperties'], - ms = ['minItems', 'maxItems'], - gs = ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum'], - ys = ['minLength', 'maxLength'], - mergeJsonSchema = (s, o, i = {}) => { - const u = { ...s }; - if ( - (['example', 'default', 'enum', 'xml', 'type', ...fs, ...ms, ...gs, ...ys].forEach( - (s) => - ((s) => { - void 0 === u[s] && void 0 !== o[s] && (u[s] = o[s]); - })(s) - ), - void 0 !== o.required && - Array.isArray(o.required) && - ((void 0 !== u.required && u.required.length) || (u.required = []), - o.required.forEach((s) => { - u.required.includes(s) || u.required.push(s); - })), - o.properties) - ) { - u.properties || (u.properties = {}); - let s = objectify(o.properties); - for (let _ in s) - Object.prototype.hasOwnProperty.call(s, _) && - ((s[_] && s[_].deprecated) || - (s[_] && s[_].readOnly && !i.includeReadOnly) || - (s[_] && s[_].writeOnly && !i.includeWriteOnly) || - u.properties[_] || - ((u.properties[_] = s[_]), - !o.required && - Array.isArray(o.required) && - -1 !== o.required.indexOf(_) && - (u.required ? u.required.push(_) : (u.required = [_])))); - } - return ( - o.items && - (u.items || (u.items = {}), (u.items = mergeJsonSchema(u.items, o.items, i))), - u - ); - }, - sampleFromSchemaGeneric = (s, o = {}, i = void 0, u = !1) => { - s && isFunc(s.toJS) && (s = s.toJS()); - let _ = void 0 !== i || (s && void 0 !== s.example) || (s && void 0 !== s.default); - const w = !_ && s && s.oneOf && s.oneOf.length > 0, - x = !_ && s && s.anyOf && s.anyOf.length > 0; - if (!_ && (w || x)) { - const i = objectify(w ? s.oneOf[0] : s.anyOf[0]); - if ( - (!(s = mergeJsonSchema(s, i, o)).xml && i.xml && (s.xml = i.xml), - void 0 !== s.example && void 0 !== i.example) - ) - _ = !0; - else if (i.properties) { - s.properties || (s.properties = {}); - let u = objectify(i.properties); - for (let _ in u) - Object.prototype.hasOwnProperty.call(u, _) && - ((u[_] && u[_].deprecated) || - (u[_] && u[_].readOnly && !o.includeReadOnly) || - (u[_] && u[_].writeOnly && !o.includeWriteOnly) || - s.properties[_] || - ((s.properties[_] = u[_]), - !i.required && - Array.isArray(i.required) && - -1 !== i.required.indexOf(_) && - (s.required ? s.required.push(_) : (s.required = [_])))); - } - } - const C = {}; - let { - xml: j, - type: L, - example: B, - properties: $, - additionalProperties: V, - items: U - } = s || {}, - { includeReadOnly: z, includeWriteOnly: Y } = o; - j = j || {}; - let Z, - { name: ee, prefix: ie, namespace: ae } = j, - le = {}; - if (u && ((ee = ee || 'notagname'), (Z = (ie ? ie + ':' : '') + ee), ae)) { - C[ie ? 'xmlns:' + ie : 'xmlns'] = ae; - } - u && (le[Z] = []); - const schemaHasAny = (o) => o.some((o) => Object.prototype.hasOwnProperty.call(s, o)); - s && - !L && - ($ || V || schemaHasAny(fs) - ? (L = 'object') - : U || schemaHasAny(ms) - ? (L = 'array') - : schemaHasAny(gs) - ? ((L = 'number'), (s.type = 'number')) - : _ || s.enum || ((L = 'string'), (s.type = 'string'))); - const handleMinMaxItems = (o) => { - if ((null != s?.maxItems && (o = o.slice(0, s?.maxItems)), null != s?.minItems)) { - let i = 0; - for (; o.length < s?.minItems; ) o.push(o[i++ % o.length]); - } - return o; - }, - ce = objectify($); - let pe, - de = 0; - const hasExceededMaxProperties = () => - s && - null !== s.maxProperties && - void 0 !== s.maxProperties && - de >= s.maxProperties, - canAddProperty = (o) => - !s || - null === s.maxProperties || - void 0 === s.maxProperties || - (!hasExceededMaxProperties() && - (!((o) => !(s && s.required && s.required.length && s.required.includes(o)))(o) || - s.maxProperties - - de - - (() => { - if (!s || !s.required) return 0; - let o = 0; - return ( - u - ? s.required.forEach((s) => (o += void 0 === le[s] ? 0 : 1)) - : s.required.forEach( - (s) => (o += void 0 === le[Z]?.find((o) => void 0 !== o[s]) ? 0 : 1) - ), - s.required.length - o - ); - })() > - 0)); - if ( - ((pe = u - ? (i, _ = void 0) => { - if (s && ce[i]) { - if (((ce[i].xml = ce[i].xml || {}), ce[i].xml.attribute)) { - const s = Array.isArray(ce[i].enum) ? ce[i].enum[0] : void 0, - o = ce[i].example, - u = ce[i].default; - return void (C[ce[i].xml.name || i] = - void 0 !== o - ? o - : void 0 !== u - ? u - : void 0 !== s - ? s - : primitive(ce[i])); - } - ce[i].xml.name = ce[i].xml.name || i; - } else ce[i] || !1 === V || (ce[i] = { xml: { name: i } }); - let w = sampleFromSchemaGeneric((s && ce[i]) || void 0, o, _, u); - canAddProperty(i) && - (de++, Array.isArray(w) ? (le[Z] = le[Z].concat(w)) : le[Z].push(w)); - } - : (i, _) => { - if (canAddProperty(i)) { - if ( - Object.prototype.hasOwnProperty.call(s, 'discriminator') && - s.discriminator && - Object.prototype.hasOwnProperty.call(s.discriminator, 'mapping') && - s.discriminator.mapping && - Object.prototype.hasOwnProperty.call(s, '$$ref') && - s.$$ref && - s.discriminator.propertyName === i - ) { - for (let o in s.discriminator.mapping) - if (-1 !== s.$$ref.search(s.discriminator.mapping[o])) { - le[i] = o; - break; - } - } else le[i] = sampleFromSchemaGeneric(ce[i], o, _, u); - de++; - } - }), - _) - ) { - let _; - if (((_ = sanitizeRef(void 0 !== i ? i : void 0 !== B ? B : s.default)), !u)) { - if ('number' == typeof _ && 'string' === L) return `${_}`; - if ('string' != typeof _ || 'string' === L) return _; - try { - return JSON.parse(_); - } catch (s) { - return _; - } - } - if ((s || (L = Array.isArray(_) ? 'array' : typeof _), 'array' === L)) { - if (!Array.isArray(_)) { - if ('string' == typeof _) return _; - _ = [_]; - } - const i = s ? s.items : void 0; - i && ((i.xml = i.xml || j || {}), (i.xml.name = i.xml.name || j.name)); - let w = _.map((s) => sampleFromSchemaGeneric(i, o, s, u)); - return ( - (w = handleMinMaxItems(w)), - j.wrapped ? ((le[Z] = w), hs()(C) || le[Z].push({ _attr: C })) : (le = w), - le - ); - } - if ('object' === L) { - if ('string' == typeof _) return _; - for (let o in _) - Object.prototype.hasOwnProperty.call(_, o) && - ((s && ce[o] && ce[o].readOnly && !z) || - (s && ce[o] && ce[o].writeOnly && !Y) || - (s && ce[o] && ce[o].xml && ce[o].xml.attribute - ? (C[ce[o].xml.name || o] = _[o]) - : pe(o, _[o]))); - return hs()(C) || le[Z].push({ _attr: C }), le; - } - return (le[Z] = hs()(C) ? _ : [{ _attr: C }, _]), le; - } - if ('object' === L) { - for (let s in ce) - Object.prototype.hasOwnProperty.call(ce, s) && - ((ce[s] && ce[s].deprecated) || - (ce[s] && ce[s].readOnly && !z) || - (ce[s] && ce[s].writeOnly && !Y) || - pe(s)); - if ((u && C && le[Z].push({ _attr: C }), hasExceededMaxProperties())) return le; - if (!0 === V) - u - ? le[Z].push({ additionalProp: 'Anything can be here' }) - : (le.additionalProp1 = {}), - de++; - else if (V) { - const i = objectify(V), - _ = sampleFromSchemaGeneric(i, o, void 0, u); - if (u && i.xml && i.xml.name && 'notagname' !== i.xml.name) le[Z].push(_); - else { - const o = - null !== s.minProperties && void 0 !== s.minProperties && de < s.minProperties - ? s.minProperties - de - : 3; - for (let s = 1; s <= o; s++) { - if (hasExceededMaxProperties()) return le; - if (u) { - const o = {}; - (o['additionalProp' + s] = _.notagname), le[Z].push(o); - } else le['additionalProp' + s] = _; - de++; - } - } - } - return le; - } - if ('array' === L) { - if (!U) return; - let i; - if ( - (u && ((U.xml = U.xml || s?.xml || {}), (U.xml.name = U.xml.name || j.name)), - Array.isArray(U.anyOf)) - ) - i = U.anyOf.map((s) => - sampleFromSchemaGeneric(mergeJsonSchema(s, U, o), o, void 0, u) - ); - else if (Array.isArray(U.oneOf)) - i = U.oneOf.map((s) => - sampleFromSchemaGeneric(mergeJsonSchema(s, U, o), o, void 0, u) - ); - else { - if (!(!u || (u && j.wrapped))) return sampleFromSchemaGeneric(U, o, void 0, u); - i = [sampleFromSchemaGeneric(U, o, void 0, u)]; - } - return ( - (i = handleMinMaxItems(i)), - u && j.wrapped ? ((le[Z] = i), hs()(C) || le[Z].push({ _attr: C }), le) : i - ); - } - let fe; - if (s && Array.isArray(s.enum)) fe = normalizeArray(s.enum)[0]; - else { - if (!s) return; - if (((fe = primitive(s)), 'number' == typeof fe)) { - let o = s.minimum; - null != o && (s.exclusiveMinimum && o++, (fe = o)); - let i = s.maximum; - null != i && (s.exclusiveMaximum && i--, (fe = i)); - } - if ( - 'string' == typeof fe && - (null !== s.maxLength && void 0 !== s.maxLength && (fe = fe.slice(0, s.maxLength)), - null !== s.minLength && void 0 !== s.minLength) - ) { - let o = 0; - for (; fe.length < s.minLength; ) fe += fe[o++ % fe.length]; - } - } - if ('file' !== L) return u ? ((le[Z] = hs()(C) ? fe : [{ _attr: C }, fe]), le) : fe; - }, - inferSchema = (s) => (s.schema && (s = s.schema), s.properties && (s.type = 'object'), s), - createXMLExample = (s, o, i) => { - const u = sampleFromSchemaGeneric(s, o, i, !0); - if (u) return 'string' == typeof u ? u : ls()(u, { declaration: !0, indent: '\t' }); - }, - sampleFromSchema = (s, o, i) => sampleFromSchemaGeneric(s, o, i, !1), - resolver = (s, o, i) => [s, JSON.stringify(o), JSON.stringify(i)], - vs = utils_memoizeN(createXMLExample, resolver), - bs = utils_memoizeN(sampleFromSchema, resolver), - _s = [{ when: /json/, shouldStringifyTypes: ['string'] }], - Es = ['object'], - get_json_sample_schema = (s) => (o, i, u, _) => { - const { fn: w } = s(), - x = w.memoizedSampleFromSchema(o, i, _), - C = typeof x, - j = _s.reduce((s, o) => (o.when.test(u) ? [...s, ...o.shouldStringifyTypes] : s), Es); - return mt()(j, (s) => s === C) ? JSON.stringify(x, null, 2) : x; - }, - get_yaml_sample_schema = (s) => (o, i, u, _) => { - const { fn: w } = s(), - x = w.getJsonSampleSchema(o, i, u, _); - let C; - try { - (C = mn.dump(mn.load(x), { lineWidth: -1 }, { schema: nn })), - '\n' === C[C.length - 1] && (C = C.slice(0, C.length - 1)); - } catch (s) { - return console.error(s), 'error: could not generate yaml example'; - } - return C.replace(/\t/g, ' '); - }, - get_xml_sample_schema = (s) => (o, i, u) => { - const { fn: _ } = s(); - if ((o && !o.xml && (o.xml = {}), o && !o.xml.name)) { - if (!o.$$ref && (o.type || o.items || o.properties || o.additionalProperties)) - return '\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e'; - if (o.$$ref) { - let s = o.$$ref.match(/\S*\/(\S+)$/); - o.xml.name = s[1]; - } - } - return _.memoizedCreateXMLExample(o, i, u); - }, - get_sample_schema = - (s) => - (o, i = '', u = {}, _ = void 0) => { - const { fn: w } = s(); - return ( - 'function' == typeof o?.toJS && (o = o.toJS()), - 'function' == typeof _?.toJS && (_ = _.toJS()), - /xml/.test(i) - ? w.getXmlSampleSchema(o, u, _) - : /(yaml|yml)/.test(i) - ? w.getYamlSampleSchema(o, u, i, _) - : w.getJsonSampleSchema(o, u, i, _) - ); - }, - json_schema_5_samples = ({ getSystem: s }) => { - const o = get_json_sample_schema(s), - i = get_yaml_sample_schema(s), - u = get_xml_sample_schema(s), - _ = get_sample_schema(s); - return { - fn: { - jsonSchema5: { - inferSchema, - sampleFromSchema, - sampleFromSchemaGeneric, - createXMLExample, - memoizedSampleFromSchema: bs, - memoizedCreateXMLExample: vs, - getJsonSampleSchema: o, - getYamlSampleSchema: i, - getXmlSampleSchema: u, - getSampleSchema: _, - mergeJsonSchema - }, - inferSchema, - sampleFromSchema, - sampleFromSchemaGeneric, - createXMLExample, - memoizedSampleFromSchema: bs, - memoizedCreateXMLExample: vs, - getJsonSampleSchema: o, - getYamlSampleSchema: i, - getXmlSampleSchema: u, - getSampleSchema: _, - mergeJsonSchema - } - }; - }; - var ws = __webpack_require__(37334), - Ss = __webpack_require__.n(ws); - const xs = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'], - spec_selectors_state = (s) => s || (0, qe.Map)(), - ks = Ut(spec_selectors_state, (s) => s.get('lastError')), - Cs = Ut(spec_selectors_state, (s) => s.get('url')), - Os = Ut(spec_selectors_state, (s) => s.get('spec') || ''), - As = Ut(spec_selectors_state, (s) => s.get('specSource') || 'not-editor'), - js = Ut(spec_selectors_state, (s) => s.get('json', (0, qe.Map)())), - Is = Ut(js, (s) => s.toJS()), - Ps = Ut(spec_selectors_state, (s) => s.get('resolved', (0, qe.Map)())), - specResolvedSubtree = (s, o) => s.getIn(['resolvedSubtrees', ...o], void 0), - mergerFn = (s, o) => - qe.Map.isMap(s) && qe.Map.isMap(o) - ? o.get('$$ref') - ? o - : (0, qe.OrderedMap)().mergeWith(mergerFn, s, o) - : o, - Ms = Ut(spec_selectors_state, (s) => - (0, qe.OrderedMap)().mergeWith(mergerFn, s.get('json'), s.get('resolvedSubtrees')) - ), - spec = (s) => js(s), - Ts = Ut(spec, () => !1), - Ns = Ut(spec, (s) => returnSelfOrNewMap(s && s.get('info'))), - Rs = Ut(spec, (s) => returnSelfOrNewMap(s && s.get('externalDocs'))), - Ds = Ut(Ns, (s) => s && s.get('version')), - Ls = Ut(Ds, (s) => /v?([0-9]*)\.([0-9]*)\.([0-9]*)/i.exec(s).slice(1)), - Bs = Ut(Ms, (s) => s.get('paths')), - Fs = Ss()(['get', 'put', 'post', 'delete', 'options', 'head', 'patch']), - qs = Ut(Bs, (s) => { - if (!s || s.size < 1) return (0, qe.List)(); - let o = (0, qe.List)(); - return s && s.forEach - ? (s.forEach((s, i) => { - if (!s || !s.forEach) return {}; - s.forEach((s, u) => { - xs.indexOf(u) < 0 || - (o = o.push( - (0, qe.fromJS)({ path: i, method: u, operation: s, id: `${u}-${i}` }) - )); - }); - }), - o) - : (0, qe.List)(); - }), - $s = Ut(spec, (s) => (0, qe.Set)(s.get('consumes'))), - Vs = Ut(spec, (s) => (0, qe.Set)(s.get('produces'))), - Us = Ut(spec, (s) => s.get('security', (0, qe.List)())), - zs = Ut(spec, (s) => s.get('securityDefinitions')), - findDefinition = (s, o) => { - const i = s.getIn(['resolvedSubtrees', 'definitions', o], null), - u = s.getIn(['json', 'definitions', o], null); - return i || u || null; - }, - Ws = Ut(spec, (s) => { - const o = s.get('definitions'); - return qe.Map.isMap(o) ? o : (0, qe.Map)(); - }), - Ks = Ut(spec, (s) => s.get('basePath')), - Hs = Ut(spec, (s) => s.get('host')), - Js = Ut(spec, (s) => s.get('schemes', (0, qe.Map)())), - Gs = Ut([qs, $s, Vs], (s, o, i) => - s.map((s) => - s.update('operation', (s) => { - if (s) { - if (!qe.Map.isMap(s)) return; - return s.withMutations( - (s) => ( - s.get('consumes') || s.update('consumes', (s) => (0, qe.Set)(s).merge(o)), - s.get('produces') || s.update('produces', (s) => (0, qe.Set)(s).merge(i)), - s - ) - ); - } - return (0, qe.Map)(); - }) - ) - ), - Ys = Ut(spec, (s) => { - const o = s.get('tags', (0, qe.List)()); - return qe.List.isList(o) ? o.filter((s) => qe.Map.isMap(s)) : (0, qe.List)(); - }), - tagDetails = (s, o) => - (Ys(s) || (0, qe.List)()) - .filter(qe.Map.isMap) - .find((s) => s.get('name') === o, (0, qe.Map)()), - Xs = Ut(Gs, Ys, (s, o) => - s.reduce( - (s, o) => { - let i = (0, qe.Set)(o.getIn(['operation', 'tags'])); - return i.count() < 1 - ? s.update('default', (0, qe.List)(), (s) => s.push(o)) - : i.reduce((s, i) => s.update(i, (0, qe.List)(), (s) => s.push(o)), s); - }, - o.reduce((s, o) => s.set(o.get('name'), (0, qe.List)()), (0, qe.OrderedMap)()) - ) - ), - selectors_taggedOperations = - (s) => - ({ getConfigs: o }) => { - let { tagsSorter: i, operationsSorter: u } = o(); - return Xs(s) - .sortBy( - (s, o) => o, - (s, o) => { - let u = 'function' == typeof i ? i : It.tagsSorter[i]; - return u ? u(s, o) : null; - } - ) - .map((o, i) => { - let _ = 'function' == typeof u ? u : It.operationsSorter[u], - w = _ ? o.sort(_) : o; - return (0, qe.Map)({ tagDetails: tagDetails(s, i), operations: w }); - }); - }, - Zs = Ut(spec_selectors_state, (s) => s.get('responses', (0, qe.Map)())), - Qs = Ut(spec_selectors_state, (s) => s.get('requests', (0, qe.Map)())), - eo = Ut(spec_selectors_state, (s) => s.get('mutatedRequests', (0, qe.Map)())), - responseFor = (s, o, i) => Zs(s).getIn([o, i], null), - requestFor = (s, o, i) => Qs(s).getIn([o, i], null), - mutatedRequestFor = (s, o, i) => eo(s).getIn([o, i], null), - allowTryItOutFor = () => !0, - parameterWithMetaByIdentity = (s, o, i) => { - const u = Ms(s).getIn(['paths', ...o, 'parameters'], (0, qe.OrderedMap)()), - _ = s.getIn(['meta', 'paths', ...o, 'parameters'], (0, qe.OrderedMap)()); - return u - .map((s) => { - const o = _.get(`${i.get('in')}.${i.get('name')}`), - u = _.get(`${i.get('in')}.${i.get('name')}.hash-${i.hashCode()}`); - return (0, qe.OrderedMap)().merge(s, o, u); - }) - .find( - (s) => s.get('in') === i.get('in') && s.get('name') === i.get('name'), - (0, qe.OrderedMap)() - ); - }, - parameterInclusionSettingFor = (s, o, i, u) => { - const _ = `${u}.${i}`; - return s.getIn(['meta', 'paths', ...o, 'parameter_inclusions', _], !1); - }, - parameterWithMeta = (s, o, i, u) => { - const _ = Ms(s) - .getIn(['paths', ...o, 'parameters'], (0, qe.OrderedMap)()) - .find((s) => s.get('in') === u && s.get('name') === i, (0, qe.OrderedMap)()); - return parameterWithMetaByIdentity(s, o, _); - }, - operationWithMeta = (s, o, i) => { - const u = Ms(s).getIn(['paths', o, i], (0, qe.OrderedMap)()), - _ = s.getIn(['meta', 'paths', o, i], (0, qe.OrderedMap)()), - w = u - .get('parameters', (0, qe.List)()) - .map((u) => parameterWithMetaByIdentity(s, [o, i], u)); - return (0, qe.OrderedMap)().merge(u, _).set('parameters', w); - }; - function getParameter(s, o, i, u) { - return ( - (o = o || []), - s - .getIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)([])) - .find((s) => qe.Map.isMap(s) && s.get('name') === i && s.get('in') === u) || - (0, qe.Map)() - ); - } - const to = Ut(spec, (s) => { - const o = s.get('host'); - return 'string' == typeof o && o.length > 0 && '/' !== o[0]; - }); - function parameterValues(s, o, i) { - return ( - (o = o || []), - operationWithMeta(s, ...o) - .get('parameters', (0, qe.List)()) - .reduce( - (s, o) => { - let u = i && 'body' === o.get('in') ? o.get('value_xml') : o.get('value'); - return ( - qe.List.isList(u) && (u = u.filter((s) => '' !== s)), - s.set(paramToIdentifier(o, { allowHashes: !1 }), u) - ); - }, - (0, qe.fromJS)({}) - ) - ); - } - function parametersIncludeIn(s, o = '') { - if (qe.List.isList(s)) return s.some((s) => qe.Map.isMap(s) && s.get('in') === o); - } - function parametersIncludeType(s, o = '') { - if (qe.List.isList(s)) return s.some((s) => qe.Map.isMap(s) && s.get('type') === o); - } - function contentTypeValues(s, o) { - o = o || []; - let i = Ms(s).getIn(['paths', ...o], (0, qe.fromJS)({})), - u = s.getIn(['meta', 'paths', ...o], (0, qe.fromJS)({})), - _ = currentProducesFor(s, o); - const w = i.get('parameters') || new qe.List(), - x = u.get('consumes_value') - ? u.get('consumes_value') - : parametersIncludeType(w, 'file') - ? 'multipart/form-data' - : parametersIncludeType(w, 'formData') - ? 'application/x-www-form-urlencoded' - : void 0; - return (0, qe.fromJS)({ requestContentType: x, responseContentType: _ }); - } - function currentProducesFor(s, o) { - o = o || []; - const i = Ms(s).getIn(['paths', ...o], null); - if (null === i) return; - const u = s.getIn(['meta', 'paths', ...o, 'produces_value'], null), - _ = i.getIn(['produces', 0], null); - return u || _ || 'application/json'; - } - function producesOptionsFor(s, o) { - o = o || []; - const i = Ms(s), - u = i.getIn(['paths', ...o], null); - if (null === u) return; - const [_] = o, - w = u.get('produces', null), - x = i.getIn(['paths', _, 'produces'], null), - C = i.getIn(['produces'], null); - return w || x || C; - } - function consumesOptionsFor(s, o) { - o = o || []; - const i = Ms(s), - u = i.getIn(['paths', ...o], null); - if (null === u) return; - const [_] = o, - w = u.get('consumes', null), - x = i.getIn(['paths', _, 'consumes'], null), - C = i.getIn(['consumes'], null); - return w || x || C; - } - const operationScheme = (s, o, i) => { - let u = s.get('url').match(/^([a-z][a-z0-9+\-.]*):/), - _ = Array.isArray(u) ? u[1] : null; - return s.getIn(['scheme', o, i]) || s.getIn(['scheme', '_defaultScheme']) || _ || ''; - }, - canExecuteScheme = (s, o, i) => ['http', 'https'].indexOf(operationScheme(s, o, i)) > -1, - validationErrors = (s, o) => { - o = o || []; - const i = s.getIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)([])), - u = []; - if (0 === i.length) return u; - const getErrorsWithPaths = (s, o = []) => { - const getNestedErrorsWithPaths = (s, o) => { - const i = [...o, s.get('propKey') || s.get('index')]; - return qe.Map.isMap(s.get('error')) - ? getErrorsWithPaths(s.get('error'), i) - : { error: s.get('error'), path: i }; - }; - return qe.List.isList(s) - ? s.map((s) => - qe.Map.isMap(s) ? getNestedErrorsWithPaths(s, o) : { error: s, path: o } - ) - : getNestedErrorsWithPaths(s, o); - }; - return ( - i.forEach((s, o) => { - const i = o.split('.').slice(1, -1).join('.'), - _ = s.get('errors'); - if (_ && _.count()) { - getErrorsWithPaths(_).forEach(({ error: s, path: o }) => { - u.push( - ((s, o, i) => - `For '${i}'${(o = o.reduce((s, o) => ('number' == typeof o ? `${s}[${o}]` : s ? `${s}.${o}` : o), '')) ? ` at path '${o}'` : ''}: ${s}.`)( - s, - o, - i - ) - ); - }); - } - }), - u - ); - }, - validateBeforeExecute = (s, o) => 0 === validationErrors(s, o).length, - getOAS3RequiredRequestBodyContentType = (s, o) => { - let i = { requestBody: !1, requestContentType: {} }, - u = s.getIn(['resolvedSubtrees', 'paths', ...o, 'requestBody'], (0, qe.fromJS)([])); - return ( - u.size < 1 || - (u.getIn(['required']) && (i.requestBody = u.getIn(['required'])), - u - .getIn(['content']) - .entrySeq() - .forEach((s) => { - const o = s[0]; - if (s[1].getIn(['schema', 'required'])) { - const u = s[1].getIn(['schema', 'required']).toJS(); - i.requestContentType[o] = u; - } - })), - i - ); - }, - isMediaTypeSchemaPropertiesEqual = (s, o, i, u) => { - if ((i || u) && i === u) return !0; - let _ = s.getIn( - ['resolvedSubtrees', 'paths', ...o, 'requestBody', 'content'], - (0, qe.fromJS)([]) - ); - if (_.size < 2 || !i || !u) return !1; - let w = _.getIn([i, 'schema', 'properties'], (0, qe.fromJS)([])), - x = _.getIn([u, 'schema', 'properties'], (0, qe.fromJS)([])); - return !!w.equals(x); - }; - function returnSelfOrNewMap(s) { - return qe.Map.isMap(s) ? s : new qe.Map(); - } - var ro = __webpack_require__(85015), - no = __webpack_require__.n(ro), - so = __webpack_require__(38221), - oo = __webpack_require__.n(so), - io = __webpack_require__(63560), - ao = __webpack_require__.n(io), - lo = __webpack_require__(56367), - co = __webpack_require__.n(lo); - const uo = 'spec_update_spec', - po = 'spec_update_url', - ho = 'spec_update_json', - fo = 'spec_update_param', - mo = 'spec_update_empty_param_inclusion', - go = 'spec_validate_param', - yo = 'spec_set_response', - vo = 'spec_set_request', - bo = 'spec_set_mutated_request', - _o = 'spec_log_request', - Eo = 'spec_clear_response', - wo = 'spec_clear_request', - So = 'spec_clear_validate_param', - xo = 'spec_update_operation_meta_value', - ko = 'spec_update_resolved', - Co = 'spec_update_resolved_subtree', - Oo = 'set_scheme', - toStr = (s) => (no()(s) ? s : ''); - function updateSpec(s) { - const o = toStr(s).replace(/\t/g, ' '); - if ('string' == typeof s) return { type: uo, payload: o }; - } - function updateResolved(s) { - return { type: ko, payload: s }; - } - function updateUrl(s) { - return { type: po, payload: s }; - } - function updateJsonSpec(s) { - return { type: ho, payload: s }; - } - const parseToJson = - (s) => - ({ specActions: o, specSelectors: i, errActions: u }) => { - let { specStr: _ } = i, - w = null; - try { - (s = s || _()), u.clear({ source: 'parser' }), (w = mn.load(s, { schema: nn })); - } catch (s) { - return ( - console.error(s), - u.newSpecErr({ - source: 'parser', - level: 'error', - message: s.reason, - line: s.mark && s.mark.line ? s.mark.line + 1 : void 0 - }) - ); - } - return w && 'object' == typeof w ? o.updateJsonSpec(w) : {}; - }; - let Ao = !1; - const resolveSpec = - (s, o) => - ({ - specActions: i, - specSelectors: u, - errActions: _, - fn: { fetch: w, resolve: x, AST: C = {} }, - getConfigs: j - }) => { - Ao || - (console.warn( - 'specActions.resolveSpec is deprecated since v3.10.0 and will be removed in v4.0.0; use requestResolvedSubtree instead!' - ), - (Ao = !0)); - const { - modelPropertyMacro: L, - parameterMacro: B, - requestInterceptor: $, - responseInterceptor: V - } = j(); - void 0 === s && (s = u.specJson()), void 0 === o && (o = u.url()); - let U = C.getLineNumberForPath ? C.getLineNumberForPath : () => {}, - z = u.specStr(); - return x({ - fetch: w, - spec: s, - baseDoc: String(new URL(o, document.baseURI)), - modelPropertyMacro: L, - parameterMacro: B, - requestInterceptor: $, - responseInterceptor: V - }).then(({ spec: s, errors: o }) => { - if ((_.clear({ type: 'thrown' }), Array.isArray(o) && o.length > 0)) { - let s = o.map( - (s) => ( - console.error(s), - (s.line = s.fullPath ? U(z, s.fullPath) : null), - (s.path = s.fullPath ? s.fullPath.join('.') : null), - (s.level = 'error'), - (s.type = 'thrown'), - (s.source = 'resolver'), - Object.defineProperty(s, 'message', { enumerable: !0, value: s.message }), - s - ) - ); - _.newThrownErrBatch(s); - } - return i.updateResolved(s); - }); - }; - let jo = []; - const Io = oo()(() => { - const s = jo.reduce( - (s, { path: o, system: i }) => (s.has(i) || s.set(i, []), s.get(i).push(o), s), - new Map() - ); - (jo = []), - s.forEach(async (s, o) => { - if (!o) - return void console.error( - "debResolveSubtrees: don't have a system to operate on, aborting." - ); - if (!o.fn.resolveSubtree) - return void console.error( - 'Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing.' - ); - const { - errActions: i, - errSelectors: u, - fn: { resolveSubtree: _, fetch: w, AST: x = {} }, - specSelectors: C, - specActions: j - } = o, - L = x.getLineNumberForPath ?? Ss()(void 0), - B = C.specStr(), - { - modelPropertyMacro: $, - parameterMacro: V, - requestInterceptor: U, - responseInterceptor: z - } = o.getConfigs(); - try { - const o = await s.reduce( - async (s, o) => { - let { resultMap: x, specWithCurrentSubtrees: j } = await s; - const { errors: Y, spec: Z } = await _(j, o, { - baseDoc: String(new URL(C.url(), document.baseURI)), - modelPropertyMacro: $, - parameterMacro: V, - requestInterceptor: U, - responseInterceptor: z - }); - if ( - (u.allErrors().size && - i.clearBy( - (s) => - 'thrown' !== s.get('type') || - 'resolver' !== s.get('source') || - !s.get('fullPath').every((s, i) => s === o[i] || void 0 === o[i]) - ), - Array.isArray(Y) && Y.length > 0) - ) { - let s = Y.map( - (s) => ( - (s.line = s.fullPath ? L(B, s.fullPath) : null), - (s.path = s.fullPath ? s.fullPath.join('.') : null), - (s.level = 'error'), - (s.type = 'thrown'), - (s.source = 'resolver'), - Object.defineProperty(s, 'message', { - enumerable: !0, - value: s.message - }), - s - ) - ); - i.newThrownErrBatch(s); - } - return ( - Z && - C.isOAS3() && - 'components' === o[0] && - 'securitySchemes' === o[1] && - (await Promise.all( - Object.values(Z) - .filter((s) => 'openIdConnect' === s.type) - .map(async (s) => { - const o = { - url: s.openIdConnectUrl, - requestInterceptor: U, - responseInterceptor: z - }; - try { - const i = await w(o); - i instanceof Error || i.status >= 400 - ? console.error(i.statusText + ' ' + o.url) - : (s.openIdConnectData = JSON.parse(i.text)); - } catch (s) { - console.error(s); - } - }) - )), - ao()(x, o, Z), - (j = co()(o, Z, j)), - { resultMap: x, specWithCurrentSubtrees: j } - ); - }, - Promise.resolve({ - resultMap: (C.specResolvedSubtree([]) || (0, qe.Map)()).toJS(), - specWithCurrentSubtrees: C.specJS() - }) - ); - j.updateResolvedSubtree([], o.resultMap); - } catch (s) { - console.error(s); - } - }); - }, 35), - requestResolvedSubtree = (s) => (o) => { - jo.find(({ path: i, system: u }) => u === o && i.toString() === s.toString()) || - (jo.push({ path: s, system: o }), Io()); - }; - function changeParam(s, o, i, u, _) { - return { type: fo, payload: { path: s, value: u, paramName: o, paramIn: i, isXml: _ } }; - } - function changeParamByIdentity(s, o, i, u) { - return { type: fo, payload: { path: s, param: o, value: i, isXml: u } }; - } - const updateResolvedSubtree = (s, o) => ({ type: Co, payload: { path: s, value: o } }), - invalidateResolvedSubtreeCache = () => ({ - type: Co, - payload: { path: [], value: (0, qe.Map)() } - }), - validateParams = (s, o) => ({ type: go, payload: { pathMethod: s, isOAS3: o } }), - updateEmptyParamInclusion = (s, o, i, u) => ({ - type: mo, - payload: { pathMethod: s, paramName: o, paramIn: i, includeEmptyValue: u } - }); - function clearValidateParams(s) { - return { type: So, payload: { pathMethod: s } }; - } - function changeConsumesValue(s, o) { - return { type: xo, payload: { path: s, value: o, key: 'consumes_value' } }; - } - function changeProducesValue(s, o) { - return { type: xo, payload: { path: s, value: o, key: 'produces_value' } }; - } - const setResponse = (s, o, i) => ({ payload: { path: s, method: o, res: i }, type: yo }), - setRequest = (s, o, i) => ({ payload: { path: s, method: o, req: i }, type: vo }), - setMutatedRequest = (s, o, i) => ({ payload: { path: s, method: o, req: i }, type: bo }), - logRequest = (s) => ({ payload: s, type: _o }), - executeRequest = - (s) => - ({ fn: o, specActions: i, specSelectors: u, getConfigs: _, oas3Selectors: w }) => { - let { pathName: x, method: C, operation: j } = s, - { requestInterceptor: L, responseInterceptor: B } = _(), - $ = j.toJS(); - if ( - (j && - j.get('parameters') && - j - .get('parameters') - .filter((s) => s && !0 === s.get('allowEmptyValue')) - .forEach((o) => { - if (u.parameterInclusionSettingFor([x, C], o.get('name'), o.get('in'))) { - s.parameters = s.parameters || {}; - const i = paramToValue(o, s.parameters); - (!i || (i && 0 === i.size)) && (s.parameters[o.get('name')] = ''); - } - }), - (s.contextUrl = Mt()(u.url()).toString()), - $ && $.operationId - ? (s.operationId = $.operationId) - : $ && x && C && (s.operationId = o.opId($, x, C)), - u.isOAS3()) - ) { - const o = `${x}:${C}`; - s.server = w.selectedServer(o) || w.selectedServer(); - const i = w.serverVariables({ server: s.server, namespace: o }).toJS(), - u = w.serverVariables({ server: s.server }).toJS(); - (s.serverVariables = Object.keys(i).length ? i : u), - (s.requestContentType = w.requestContentType(x, C)), - (s.responseContentType = w.responseContentType(x, C) || '*/*'); - const _ = w.requestBodyValue(x, C), - j = w.requestBodyInclusionSetting(x, C); - _ && _.toJS - ? (s.requestBody = _.map((s) => (qe.Map.isMap(s) ? s.get('value') : s)) - .filter( - (s, o) => (Array.isArray(s) ? 0 !== s.length : !isEmptyValue(s)) || j.get(o) - ) - .toJS()) - : (s.requestBody = _); - } - let V = Object.assign({}, s); - (V = o.buildRequest(V)), i.setRequest(s.pathName, s.method, V); - (s.requestInterceptor = async (o) => { - let u = await L.apply(void 0, [o]), - _ = Object.assign({}, u); - return i.setMutatedRequest(s.pathName, s.method, _), u; - }), - (s.responseInterceptor = B); - const U = Date.now(); - return o - .execute(s) - .then((o) => { - (o.duration = Date.now() - U), i.setResponse(s.pathName, s.method, o); - }) - .catch((o) => { - 'Failed to fetch' === o.message && - ((o.name = ''), - (o.message = - '**Failed to fetch.** \n**Possible Reasons:** \n - CORS \n - Network Failure \n - URL scheme must be "http" or "https" for CORS request.')), - i.setResponse(s.pathName, s.method, { error: !0, err: o }); - }); - }, - actions_execute = - ({ path: s, method: o, ...i } = {}) => - (u) => { - let { - fn: { fetch: _ }, - specSelectors: w, - specActions: x - } = u, - C = w.specJsonWithResolvedSubtrees().toJS(), - j = w.operationScheme(s, o), - { requestContentType: L, responseContentType: B } = w - .contentTypeValues([s, o]) - .toJS(), - $ = /xml/i.test(L), - V = w.parameterValues([s, o], $).toJS(); - return x.executeRequest({ - ...i, - fetch: _, - spec: C, - pathName: s, - method: o, - parameters: V, - requestContentType: L, - scheme: j, - responseContentType: B - }); - }; - function clearResponse(s, o) { - return { type: Eo, payload: { path: s, method: o } }; - } - function clearRequest(s, o) { - return { type: wo, payload: { path: s, method: o } }; - } - function setScheme(s, o, i) { - return { type: Oo, payload: { scheme: s, path: o, method: i } }; - } - const Po = { - [uo]: (s, o) => ('string' == typeof o.payload ? s.set('spec', o.payload) : s), - [po]: (s, o) => s.set('url', o.payload + ''), - [ho]: (s, o) => s.set('json', fromJSOrdered(o.payload)), - [ko]: (s, o) => s.setIn(['resolved'], fromJSOrdered(o.payload)), - [Co]: (s, o) => { - const { value: i, path: u } = o.payload; - return s.setIn(['resolvedSubtrees', ...u], fromJSOrdered(i)); - }, - [fo]: (s, { payload: o }) => { - let { path: i, paramName: u, paramIn: _, param: w, value: x, isXml: C } = o, - j = w ? paramToIdentifier(w) : `${_}.${u}`; - const L = C ? 'value_xml' : 'value'; - return s.setIn(['meta', 'paths', ...i, 'parameters', j, L], (0, qe.fromJS)(x)); - }, - [mo]: (s, { payload: o }) => { - let { pathMethod: i, paramName: u, paramIn: _, includeEmptyValue: w } = o; - if (!u || !_) - return ( - console.warn( - 'Warning: UPDATE_EMPTY_PARAM_INCLUSION could not generate a paramKey.' - ), - s - ); - const x = `${_}.${u}`; - return s.setIn(['meta', 'paths', ...i, 'parameter_inclusions', x], w); - }, - [go]: (s, { payload: { pathMethod: o, isOAS3: i } }) => { - const u = Ms(s).getIn(['paths', ...o]), - _ = parameterValues(s, o).toJS(); - return s.updateIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)({}), (w) => - u.get('parameters', (0, qe.List)()).reduce((u, w) => { - const x = paramToValue(w, _), - C = parameterInclusionSettingFor(s, o, w.get('name'), w.get('in')), - j = ((s, o, { isOAS3: i = !1, bypassRequiredCheck: u = !1 } = {}) => { - let _ = s.get('required'), - { schema: w, parameterContentMediaType: x } = getParameterSchema(s, { - isOAS3: i - }); - return validateValueBySchema(o, w, _, u, x); - })(w, x, { bypassRequiredCheck: C, isOAS3: i }); - return u.setIn([paramToIdentifier(w), 'errors'], (0, qe.fromJS)(j)); - }, w) - ); - }, - [So]: (s, { payload: { pathMethod: o } }) => - s.updateIn(['meta', 'paths', ...o, 'parameters'], (0, qe.fromJS)([]), (s) => - s.map((s) => s.set('errors', (0, qe.fromJS)([]))) - ), - [yo]: (s, { payload: { res: o, path: i, method: u } }) => { - let _; - (_ = o.error - ? Object.assign( - { - error: !0, - name: o.err.name, - message: o.err.message, - statusCode: o.err.statusCode - }, - o.err.response - ) - : o), - (_.headers = _.headers || {}); - let w = s.setIn(['responses', i, u], fromJSOrdered(_)); - return ( - at.Blob && - _.data instanceof at.Blob && - (w = w.setIn(['responses', i, u, 'text'], _.data)), - w - ); - }, - [vo]: (s, { payload: { req: o, path: i, method: u } }) => - s.setIn(['requests', i, u], fromJSOrdered(o)), - [bo]: (s, { payload: { req: o, path: i, method: u } }) => - s.setIn(['mutatedRequests', i, u], fromJSOrdered(o)), - [xo]: (s, { payload: { path: o, value: i, key: u } }) => { - let _ = ['paths', ...o], - w = ['meta', 'paths', ...o]; - return s.getIn(['json', ..._]) || - s.getIn(['resolved', ..._]) || - s.getIn(['resolvedSubtrees', ..._]) - ? s.setIn([...w, u], (0, qe.fromJS)(i)) - : s; - }, - [Eo]: (s, { payload: { path: o, method: i } }) => s.deleteIn(['responses', o, i]), - [wo]: (s, { payload: { path: o, method: i } }) => s.deleteIn(['requests', o, i]), - [Oo]: (s, { payload: { scheme: o, path: i, method: u } }) => - i && u - ? s.setIn(['scheme', i, u], o) - : i || u - ? void 0 - : s.setIn(['scheme', '_defaultScheme'], o) - }, - wrap_actions_updateSpec = - (s, { specActions: o }) => - (...i) => { - s(...i), o.parseToJson(...i); - }, - wrap_actions_updateJsonSpec = - (s, { specActions: o }) => - (...i) => { - s(...i), o.invalidateResolvedSubtreeCache(); - const [u] = i, - _ = jn()(u, ['paths']) || {}; - Object.keys(_).forEach((s) => { - jn()(_, [s]).$ref && o.requestResolvedSubtree(['paths', s]); - }), - o.requestResolvedSubtree(['components', 'securitySchemes']); - }, - wrap_actions_executeRequest = - (s, { specActions: o }) => - (i) => (o.logRequest(i), s(i)), - wrap_actions_validateParams = - (s, { specSelectors: o }) => - (i) => - s(i, o.isOAS3()), - plugins_spec = () => ({ - statePlugins: { - spec: { - wrapActions: { ...ee }, - reducers: { ...Po }, - actions: { ...Z }, - selectors: { ...Y } - } - } - }); - var Mo = (function () { - var extendStatics = function (s, o) { - return ( - (extendStatics = - Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && - function (s, o) { - s.__proto__ = o; - }) || - function (s, o) { - for (var i in o) o.hasOwnProperty(i) && (s[i] = o[i]); - }), - extendStatics(s, o) - ); - }; - return function (s, o) { - function __() { - this.constructor = s; - } - extendStatics(s, o), - (s.prototype = - null === o ? Object.create(o) : ((__.prototype = o.prototype), new __())); - }; - })(), - To = Object.prototype.hasOwnProperty; - function module_helpers_hasOwnProperty(s, o) { - return To.call(s, o); - } - function _objectKeys(s) { - if (Array.isArray(s)) { - for (var o = new Array(s.length), i = 0; i < o.length; i++) o[i] = '' + i; - return o; - } - if (Object.keys) return Object.keys(s); - var u = []; - for (var _ in s) module_helpers_hasOwnProperty(s, _) && u.push(_); - return u; - } - function _deepClone(s) { - switch (typeof s) { - case 'object': - return JSON.parse(JSON.stringify(s)); - case 'undefined': - return null; - default: - return s; - } - } - function helpers_isInteger(s) { - for (var o, i = 0, u = s.length; i < u; ) { - if (!((o = s.charCodeAt(i)) >= 48 && o <= 57)) return !1; - i++; - } - return !0; - } - function escapePathComponent(s) { - return -1 === s.indexOf('/') && -1 === s.indexOf('~') - ? s - : s.replace(/~/g, '~0').replace(/\//g, '~1'); - } - function unescapePathComponent(s) { - return s.replace(/~1/g, '/').replace(/~0/g, '~'); - } - function hasUndefined(s) { - if (void 0 === s) return !0; - if (s) - if (Array.isArray(s)) { - for (var o = 0, i = s.length; o < i; o++) if (hasUndefined(s[o])) return !0; - } else if ('object' == typeof s) - for (var u = _objectKeys(s), _ = u.length, w = 0; w < _; w++) - if (hasUndefined(s[u[w]])) return !0; - return !1; - } - function patchErrorMessageFormatter(s, o) { - var i = [s]; - for (var u in o) { - var _ = 'object' == typeof o[u] ? JSON.stringify(o[u], null, 2) : o[u]; - void 0 !== _ && i.push(u + ': ' + _); - } - return i.join('\n'); - } - var No = (function (s) { - function PatchError(o, i, u, _, w) { - var x = this.constructor, - C = - s.call( - this, - patchErrorMessageFormatter(o, { name: i, index: u, operation: _, tree: w }) - ) || this; - return ( - (C.name = i), - (C.index = u), - (C.operation = _), - (C.tree = w), - Object.setPrototypeOf(C, x.prototype), - (C.message = patchErrorMessageFormatter(o, { - name: i, - index: u, - operation: _, - tree: w - })), - C - ); - } - return Mo(PatchError, s), PatchError; - })(Error), - Ro = No, - Do = _deepClone, - Lo = { - add: function (s, o, i) { - return (s[o] = this.value), { newDocument: i }; - }, - remove: function (s, o, i) { - var u = s[o]; - return delete s[o], { newDocument: i, removed: u }; - }, - replace: function (s, o, i) { - var u = s[o]; - return (s[o] = this.value), { newDocument: i, removed: u }; - }, - move: function (s, o, i) { - var u = getValueByPointer(i, this.path); - u && (u = _deepClone(u)); - var _ = applyOperation(i, { op: 'remove', path: this.from }).removed; - return ( - applyOperation(i, { op: 'add', path: this.path, value: _ }), - { newDocument: i, removed: u } - ); - }, - copy: function (s, o, i) { - var u = getValueByPointer(i, this.from); - return ( - applyOperation(i, { op: 'add', path: this.path, value: _deepClone(u) }), - { newDocument: i } - ); - }, - test: function (s, o, i) { - return { newDocument: i, test: _areEquals(s[o], this.value) }; - }, - _get: function (s, o, i) { - return (this.value = s[o]), { newDocument: i }; - } - }, - Bo = { - add: function (s, o, i) { - return ( - helpers_isInteger(o) ? s.splice(o, 0, this.value) : (s[o] = this.value), - { newDocument: i, index: o } - ); - }, - remove: function (s, o, i) { - return { newDocument: i, removed: s.splice(o, 1)[0] }; - }, - replace: function (s, o, i) { - var u = s[o]; - return (s[o] = this.value), { newDocument: i, removed: u }; - }, - move: Lo.move, - copy: Lo.copy, - test: Lo.test, - _get: Lo._get - }; - function getValueByPointer(s, o) { - if ('' == o) return s; - var i = { op: '_get', path: o }; - return applyOperation(s, i), i.value; - } - function applyOperation(s, o, i, u, _, w) { - if ( - (void 0 === i && (i = !1), - void 0 === u && (u = !0), - void 0 === _ && (_ = !0), - void 0 === w && (w = 0), - i && ('function' == typeof i ? i(o, 0, s, o.path) : validator(o, 0)), - '' === o.path) - ) { - var x = { newDocument: s }; - if ('add' === o.op) return (x.newDocument = o.value), x; - if ('replace' === o.op) return (x.newDocument = o.value), (x.removed = s), x; - if ('move' === o.op || 'copy' === o.op) - return ( - (x.newDocument = getValueByPointer(s, o.from)), - 'move' === o.op && (x.removed = s), - x - ); - if ('test' === o.op) { - if (((x.test = _areEquals(s, o.value)), !1 === x.test)) - throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', w, o, s); - return (x.newDocument = s), x; - } - if ('remove' === o.op) return (x.removed = s), (x.newDocument = null), x; - if ('_get' === o.op) return (o.value = s), x; - if (i) - throw new Ro( - 'Operation `op` property is not one of operations defined in RFC-6902', - 'OPERATION_OP_INVALID', - w, - o, - s - ); - return x; - } - u || (s = _deepClone(s)); - var C = (o.path || '').split('/'), - j = s, - L = 1, - B = C.length, - $ = void 0, - V = void 0, - U = void 0; - for (U = 'function' == typeof i ? i : validator; ; ) { - if ( - ((V = C[L]) && -1 != V.indexOf('~') && (V = unescapePathComponent(V)), - _ && ('__proto__' == V || ('prototype' == V && L > 0 && 'constructor' == C[L - 1]))) - ) - throw new TypeError( - 'JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README' - ); - if ( - (i && - void 0 === $ && - (void 0 === j[V] ? ($ = C.slice(0, L).join('/')) : L == B - 1 && ($ = o.path), - void 0 !== $ && U(o, 0, s, $)), - L++, - Array.isArray(j)) - ) { - if ('-' === V) V = j.length; - else { - if (i && !helpers_isInteger(V)) - throw new Ro( - 'Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index', - 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX', - w, - o, - s - ); - helpers_isInteger(V) && (V = ~~V); - } - if (L >= B) { - if (i && 'add' === o.op && V > j.length) - throw new Ro( - 'The specified index MUST NOT be greater than the number of elements in the array', - 'OPERATION_VALUE_OUT_OF_BOUNDS', - w, - o, - s - ); - if (!1 === (x = Bo[o.op].call(o, j, V, s)).test) - throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', w, o, s); - return x; - } - } else if (L >= B) { - if (!1 === (x = Lo[o.op].call(o, j, V, s)).test) - throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', w, o, s); - return x; - } - if (((j = j[V]), i && L < B && (!j || 'object' != typeof j))) - throw new Ro( - 'Cannot perform operation at the desired path', - 'OPERATION_PATH_UNRESOLVABLE', - w, - o, - s - ); - } - } - function applyPatch(s, o, i, u, _) { - if ((void 0 === u && (u = !0), void 0 === _ && (_ = !0), i && !Array.isArray(o))) - throw new Ro('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); - u || (s = _deepClone(s)); - for (var w = new Array(o.length), x = 0, C = o.length; x < C; x++) - (w[x] = applyOperation(s, o[x], i, !0, _, x)), (s = w[x].newDocument); - return (w.newDocument = s), w; - } - function applyReducer(s, o, i) { - var u = applyOperation(s, o); - if (!1 === u.test) - throw new Ro('Test operation failed', 'TEST_OPERATION_FAILED', i, o, s); - return u.newDocument; - } - function validator(s, o, i, u) { - if ('object' != typeof s || null === s || Array.isArray(s)) - throw new Ro('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', o, s, i); - if (!Lo[s.op]) - throw new Ro( - 'Operation `op` property is not one of operations defined in RFC-6902', - 'OPERATION_OP_INVALID', - o, - s, - i - ); - if ('string' != typeof s.path) - throw new Ro( - 'Operation `path` property is not a string', - 'OPERATION_PATH_INVALID', - o, - s, - i - ); - if (0 !== s.path.indexOf('/') && s.path.length > 0) - throw new Ro( - 'Operation `path` property must start with "/"', - 'OPERATION_PATH_INVALID', - o, - s, - i - ); - if (('move' === s.op || 'copy' === s.op) && 'string' != typeof s.from) - throw new Ro( - 'Operation `from` property is not present (applicable in `move` and `copy` operations)', - 'OPERATION_FROM_REQUIRED', - o, - s, - i - ); - if (('add' === s.op || 'replace' === s.op || 'test' === s.op) && void 0 === s.value) - throw new Ro( - 'Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', - 'OPERATION_VALUE_REQUIRED', - o, - s, - i - ); - if (('add' === s.op || 'replace' === s.op || 'test' === s.op) && hasUndefined(s.value)) - throw new Ro( - 'Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', - 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', - o, - s, - i - ); - if (i) - if ('add' == s.op) { - var _ = s.path.split('/').length, - w = u.split('/').length; - if (_ !== w + 1 && _ !== w) - throw new Ro( - 'Cannot perform an `add` operation at the desired path', - 'OPERATION_PATH_CANNOT_ADD', - o, - s, - i - ); - } else if ('replace' === s.op || 'remove' === s.op || '_get' === s.op) { - if (s.path !== u) - throw new Ro( - 'Cannot perform the operation at a path that does not exist', - 'OPERATION_PATH_UNRESOLVABLE', - o, - s, - i - ); - } else if ('move' === s.op || 'copy' === s.op) { - var x = validate([{ op: '_get', path: s.from, value: void 0 }], i); - if (x && 'OPERATION_PATH_UNRESOLVABLE' === x.name) - throw new Ro( - 'Cannot perform the operation from a path that does not exist', - 'OPERATION_FROM_UNRESOLVABLE', - o, - s, - i - ); - } - } - function validate(s, o, i) { - try { - if (!Array.isArray(s)) - throw new Ro('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); - if (o) applyPatch(_deepClone(o), _deepClone(s), i || !0); - else { - i = i || validator; - for (var u = 0; u < s.length; u++) i(s[u], u, o, void 0); - } - } catch (s) { - if (s instanceof Ro) return s; - throw s; - } - } - function _areEquals(s, o) { - if (s === o) return !0; - if (s && o && 'object' == typeof s && 'object' == typeof o) { - var i, - u, - _, - w = Array.isArray(s), - x = Array.isArray(o); - if (w && x) { - if ((u = s.length) != o.length) return !1; - for (i = u; 0 != i--; ) if (!_areEquals(s[i], o[i])) return !1; - return !0; - } - if (w != x) return !1; - var C = Object.keys(s); - if ((u = C.length) !== Object.keys(o).length) return !1; - for (i = u; 0 != i--; ) if (!o.hasOwnProperty(C[i])) return !1; - for (i = u; 0 != i--; ) if (!_areEquals(s[(_ = C[i])], o[_])) return !1; - return !0; - } - return s != s && o != o; - } - var Fo = new WeakMap(), - qo = function qo(s) { - (this.observers = new Map()), (this.obj = s); - }, - $o = function $o(s, o) { - (this.callback = s), (this.observer = o); - }; - function unobserve(s, o) { - o.unobserve(); - } - function observe(s, o) { - var i, - u = (function getMirror(s) { - return Fo.get(s); - })(s); - if (u) { - var _ = (function getObserverFromMirror(s, o) { - return s.observers.get(o); - })(u, o); - i = _ && _.observer; - } else (u = new qo(s)), Fo.set(s, u); - if (i) return i; - if (((i = {}), (u.value = _deepClone(s)), o)) { - (i.callback = o), (i.next = null); - var dirtyCheck = function () { - generate(i); - }, - fastCheck = function () { - clearTimeout(i.next), (i.next = setTimeout(dirtyCheck)); - }; - 'undefined' != typeof window && - (window.addEventListener('mouseup', fastCheck), - window.addEventListener('keyup', fastCheck), - window.addEventListener('mousedown', fastCheck), - window.addEventListener('keydown', fastCheck), - window.addEventListener('change', fastCheck)); - } - return ( - (i.patches = []), - (i.object = s), - (i.unobserve = function () { - generate(i), - clearTimeout(i.next), - (function removeObserverFromMirror(s, o) { - s.observers.delete(o.callback); - })(u, i), - 'undefined' != typeof window && - (window.removeEventListener('mouseup', fastCheck), - window.removeEventListener('keyup', fastCheck), - window.removeEventListener('mousedown', fastCheck), - window.removeEventListener('keydown', fastCheck), - window.removeEventListener('change', fastCheck)); - }), - u.observers.set(o, new $o(o, i)), - i - ); - } - function generate(s, o) { - void 0 === o && (o = !1); - var i = Fo.get(s.object); - _generate(i.value, s.object, s.patches, '', o), - s.patches.length && applyPatch(i.value, s.patches); - var u = s.patches; - return u.length > 0 && ((s.patches = []), s.callback && s.callback(u)), u; - } - function _generate(s, o, i, u, _) { - if (o !== s) { - 'function' == typeof o.toJSON && (o = o.toJSON()); - for ( - var w = _objectKeys(o), x = _objectKeys(s), C = !1, j = x.length - 1; - j >= 0; - j-- - ) { - var L = s[($ = x[j])]; - if ( - !module_helpers_hasOwnProperty(o, $) || - (void 0 === o[$] && void 0 !== L && !1 === Array.isArray(o)) - ) - Array.isArray(s) === Array.isArray(o) - ? (_ && - i.push({ - op: 'test', - path: u + '/' + escapePathComponent($), - value: _deepClone(L) - }), - i.push({ op: 'remove', path: u + '/' + escapePathComponent($) }), - (C = !0)) - : (_ && i.push({ op: 'test', path: u, value: s }), - i.push({ op: 'replace', path: u, value: o }), - !0); - else { - var B = o[$]; - 'object' == typeof L && - null != L && - 'object' == typeof B && - null != B && - Array.isArray(L) === Array.isArray(B) - ? _generate(L, B, i, u + '/' + escapePathComponent($), _) - : L !== B && - (_ && - i.push({ - op: 'test', - path: u + '/' + escapePathComponent($), - value: _deepClone(L) - }), - i.push({ - op: 'replace', - path: u + '/' + escapePathComponent($), - value: _deepClone(B) - })); - } - } - if (C || w.length != x.length) - for (j = 0; j < w.length; j++) { - var $; - module_helpers_hasOwnProperty(s, ($ = w[j])) || - void 0 === o[$] || - i.push({ - op: 'add', - path: u + '/' + escapePathComponent($), - value: _deepClone(o[$]) - }); - } - } - } - function compare(s, o, i) { - void 0 === i && (i = !1); - var u = []; - return _generate(s, o, u, '', i), u; - } - Object.assign({}, ie, ae, { - JsonPatchError: No, - deepClone: _deepClone, - escapePathComponent, - unescapePathComponent - }); - var Vo = __webpack_require__(14744), - Uo = __webpack_require__.n(Vo); - const zo = { - add: function add(s, o) { - return { op: 'add', path: s, value: o }; - }, - replace, - remove: function remove(s) { - return { op: 'remove', path: s }; - }, - merge: function lib_merge(s, o) { - return { type: 'mutation', op: 'merge', path: s, value: o }; - }, - mergeDeep: function mergeDeep(s, o) { - return { type: 'mutation', op: 'mergeDeep', path: s, value: o }; - }, - context: function context(s, o) { - return { type: 'context', path: s, value: o }; - }, - getIn: function lib_getIn(s, o) { - return o.reduce((s, o) => (void 0 !== o && s ? s[o] : s), s); - }, - applyPatch: function lib_applyPatch(s, o, i) { - if ( - ((i = i || {}), - 'merge' === (o = { ...o, path: o.path && normalizeJSONPath(o.path) }).op) - ) { - const i = getInByJsonPath(s, o.path); - Object.assign(i, o.value), applyPatch(s, [replace(o.path, i)]); - } else if ('mergeDeep' === o.op) { - const i = getInByJsonPath(s, o.path), - u = Uo()(i, o.value); - s = applyPatch(s, [replace(o.path, u)]).newDocument; - } else if ('add' === o.op && '' === o.path && lib_isObject(o.value)) { - applyPatch( - s, - Object.keys(o.value).reduce( - (s, i) => ( - s.push({ op: 'add', path: `/${normalizeJSONPath(i)}`, value: o.value[i] }), s - ), - [] - ) - ); - } else if ('replace' === o.op && '' === o.path) { - let { value: u } = o; - i.allowMetaPatches && - o.meta && - isAdditiveMutation(o) && - (Array.isArray(o.value) || lib_isObject(o.value)) && - (u = { ...u, ...o.meta }), - (s = u); - } else if ( - (applyPatch(s, [o]), - i.allowMetaPatches && - o.meta && - isAdditiveMutation(o) && - (Array.isArray(o.value) || lib_isObject(o.value))) - ) { - const i = { ...getInByJsonPath(s, o.path), ...o.meta }; - applyPatch(s, [replace(o.path, i)]); - } - return s; - }, - parentPathMatch: function parentPathMatch(s, o) { - if (!Array.isArray(o)) return !1; - for (let i = 0, u = o.length; i < u; i += 1) if (o[i] !== s[i]) return !1; - return !0; - }, - flatten, - fullyNormalizeArray: function fullyNormalizeArray(s) { - return cleanArray(flatten(lib_normalizeArray(s))); - }, - normalizeArray: lib_normalizeArray, - isPromise: function isPromise(s) { - return lib_isObject(s) && lib_isFunction(s.then); - }, - forEachNew: function forEachNew(s, o) { - try { - return forEachNewPatch(s, forEach, o); - } catch (s) { - return s; - } - }, - forEachNewPrimitive: function forEachNewPrimitive(s, o) { - try { - return forEachNewPatch(s, forEachPrimitive, o); - } catch (s) { - return s; - } - }, - isJsonPatch, - isContextPatch: function isContextPatch(s) { - return isPatch(s) && 'context' === s.type; - }, - isPatch, - isMutation, - isAdditiveMutation, - isGenerator: function isGenerator(s) { - return '[object GeneratorFunction]' === Object.prototype.toString.call(s); - }, - isFunction: lib_isFunction, - isObject: lib_isObject, - isError: function lib_isError(s) { - return s instanceof Error; - } - }; - function normalizeJSONPath(s) { - return Array.isArray(s) - ? s.length < 1 - ? '' - : `/${s.map((s) => (s + '').replace(/~/g, '~0').replace(/\//g, '~1')).join('/')}` - : s; - } - function replace(s, o, i) { - return { op: 'replace', path: s, value: o, meta: i }; - } - function forEachNewPatch(s, o, i) { - return cleanArray( - flatten(s.filter(isAdditiveMutation).map((s) => o(s.value, i, s.path)) || []) - ); - } - function forEachPrimitive(s, o, i) { - return ( - (i = i || []), - Array.isArray(s) - ? s.map((s, u) => forEachPrimitive(s, o, i.concat(u))) - : lib_isObject(s) - ? Object.keys(s).map((u) => forEachPrimitive(s[u], o, i.concat(u))) - : o(s, i[i.length - 1], i) - ); - } - function forEach(s, o, i) { - let u = []; - if ((i = i || []).length > 0) { - const _ = o(s, i[i.length - 1], i); - _ && (u = u.concat(_)); - } - if (Array.isArray(s)) { - const _ = s.map((s, u) => forEach(s, o, i.concat(u))); - _ && (u = u.concat(_)); - } else if (lib_isObject(s)) { - const _ = Object.keys(s).map((u) => forEach(s[u], o, i.concat(u))); - _ && (u = u.concat(_)); - } - return (u = flatten(u)), u; - } - function lib_normalizeArray(s) { - return Array.isArray(s) ? s : [s]; - } - function flatten(s) { - return [].concat(...s.map((s) => (Array.isArray(s) ? flatten(s) : s))); - } - function cleanArray(s) { - return s.filter((s) => void 0 !== s); - } - function lib_isObject(s) { - return s && 'object' == typeof s; - } - function lib_isFunction(s) { - return s && 'function' == typeof s; - } - function isJsonPatch(s) { - if (isPatch(s)) { - const { op: o } = s; - return 'add' === o || 'remove' === o || 'replace' === o; - } - return !1; - } - function isMutation(s) { - return isJsonPatch(s) || (isPatch(s) && 'mutation' === s.type); - } - function isAdditiveMutation(s) { - return ( - isMutation(s) && - ('add' === s.op || 'replace' === s.op || 'merge' === s.op || 'mergeDeep' === s.op) - ); - } - function isPatch(s) { - return s && 'object' == typeof s; - } - function getInByJsonPath(s, o) { - try { - return getValueByPointer(s, o); - } catch (s) { - return console.error(s), {}; - } - } - var Wo = __webpack_require__(48675); - const Ko = class ApiDOMAggregateError extends Wo { - constructor(s, o, i) { - if ( - (super(s, o, i), - (this.name = this.constructor.name), - 'string' == typeof o && (this.message = o), - 'function' == typeof Error.captureStackTrace - ? Error.captureStackTrace(this, this.constructor) - : (this.stack = new Error(o).stack), - null != i && 'object' == typeof i && Object.hasOwn(i, 'cause') && !('cause' in this)) - ) { - const { cause: s } = i; - (this.cause = s), - s instanceof Error && - 'stack' in s && - (this.stack = `${this.stack}\nCAUSE: ${s.stack}`); - } - } - }; - class ApiDOMError extends Error { - static [Symbol.hasInstance](s) { - return ( - super[Symbol.hasInstance](s) || Function.prototype[Symbol.hasInstance].call(Ko, s) - ); - } - constructor(s, o) { - if ( - (super(s, o), - (this.name = this.constructor.name), - 'string' == typeof s && (this.message = s), - 'function' == typeof Error.captureStackTrace - ? Error.captureStackTrace(this, this.constructor) - : (this.stack = new Error(s).stack), - null != o && 'object' == typeof o && Object.hasOwn(o, 'cause') && !('cause' in this)) - ) { - const { cause: s } = o; - (this.cause = s), - s instanceof Error && - 'stack' in s && - (this.stack = `${this.stack}\nCAUSE: ${s.stack}`); - } - } - } - const Ho = ApiDOMError; - const Jo = class ApiDOMStructuredError extends Ho { - constructor(s, o) { - if ((super(s, o), null != o && 'object' == typeof o)) { - const { cause: s, ...i } = o; - Object.assign(this, i); - } - } - }; - var Go = __webpack_require__(65606); - function _isPlaceholder(s) { - return null != s && 'object' == typeof s && !0 === s['@@functional/placeholder']; - } - function _curry1(s) { - return function f1(o) { - return 0 === arguments.length || _isPlaceholder(o) ? f1 : s.apply(this, arguments); - }; - } - function _curry2(s) { - return function f2(o, i) { - switch (arguments.length) { - case 0: - return f2; - case 1: - return _isPlaceholder(o) - ? f2 - : _curry1(function (i) { - return s(o, i); - }); - default: - return _isPlaceholder(o) && _isPlaceholder(i) - ? f2 - : _isPlaceholder(o) - ? _curry1(function (o) { - return s(o, i); - }) - : _isPlaceholder(i) - ? _curry1(function (i) { - return s(o, i); - }) - : s(o, i); - } - }; - } - function _curry3(s) { - return function f3(o, i, u) { - switch (arguments.length) { - case 0: - return f3; - case 1: - return _isPlaceholder(o) - ? f3 - : _curry2(function (i, u) { - return s(o, i, u); - }); - case 2: - return _isPlaceholder(o) && _isPlaceholder(i) - ? f3 - : _isPlaceholder(o) - ? _curry2(function (o, u) { - return s(o, i, u); - }) - : _isPlaceholder(i) - ? _curry2(function (i, u) { - return s(o, i, u); - }) - : _curry1(function (u) { - return s(o, i, u); - }); - default: - return _isPlaceholder(o) && _isPlaceholder(i) && _isPlaceholder(u) - ? f3 - : _isPlaceholder(o) && _isPlaceholder(i) - ? _curry2(function (o, i) { - return s(o, i, u); - }) - : _isPlaceholder(o) && _isPlaceholder(u) - ? _curry2(function (o, u) { - return s(o, i, u); - }) - : _isPlaceholder(i) && _isPlaceholder(u) - ? _curry2(function (i, u) { - return s(o, i, u); - }) - : _isPlaceholder(o) - ? _curry1(function (o) { - return s(o, i, u); - }) - : _isPlaceholder(i) - ? _curry1(function (i) { - return s(o, i, u); - }) - : _isPlaceholder(u) - ? _curry1(function (u) { - return s(o, i, u); - }) - : s(o, i, u); - } - }; - } - const Yo = - Number.isInteger || - function _isInteger(s) { - return (s | 0) === s; - }; - function _isString(s) { - return '[object String]' === Object.prototype.toString.call(s); - } - function _nth(s, o) { - var i = s < 0 ? o.length + s : s; - return _isString(o) ? o.charAt(i) : o[i]; - } - function _path(s, o) { - for (var i = o, u = 0; u < s.length; u += 1) { - if (null == i) return; - var _ = s[u]; - i = Yo(_) ? _nth(_, i) : i[_]; - } - return i; - } - const Xo = _curry3(function pathSatisfies(s, o, i) { - return s(_path(o, i)); - }); - function _cloneRegExp(s) { - return new RegExp( - s.source, - s.flags - ? s.flags - : (s.global ? 'g' : '') + - (s.ignoreCase ? 'i' : '') + - (s.multiline ? 'm' : '') + - (s.sticky ? 'y' : '') + - (s.unicode ? 'u' : '') + - (s.dotAll ? 's' : '') - ); - } - function _arrayFromIterator(s) { - for (var o, i = []; !(o = s.next()).done; ) i.push(o.value); - return i; - } - function _includesWith(s, o, i) { - for (var u = 0, _ = i.length; u < _; ) { - if (s(o, i[u])) return !0; - u += 1; - } - return !1; - } - function _has(s, o) { - return Object.prototype.hasOwnProperty.call(o, s); - } - const Zo = - 'function' == typeof Object.is - ? Object.is - : function _objectIs(s, o) { - return s === o ? 0 !== s || 1 / s == 1 / o : s != s && o != o; - }; - var Qo = Object.prototype.toString; - const _i = (function () { - return '[object Arguments]' === Qo.call(arguments) - ? function _isArguments(s) { - return '[object Arguments]' === Qo.call(s); - } - : function _isArguments(s) { - return _has('callee', s); - }; - })(); - var Ei = !{ toString: null }.propertyIsEnumerable('toString'), - Oi = [ - 'constructor', - 'valueOf', - 'isPrototypeOf', - 'toString', - 'propertyIsEnumerable', - 'hasOwnProperty', - 'toLocaleString' - ], - Pi = (function () { - return arguments.propertyIsEnumerable('length'); - })(), - Mi = function contains(s, o) { - for (var i = 0; i < s.length; ) { - if (s[i] === o) return !0; - i += 1; - } - return !1; - }, - Ri = - 'function' != typeof Object.keys || Pi - ? _curry1(function keys(s) { - if (Object(s) !== s) return []; - var o, - i, - u = [], - _ = Pi && _i(s); - for (o in s) !_has(o, s) || (_ && 'length' === o) || (u[u.length] = o); - if (Ei) - for (i = Oi.length - 1; i >= 0; ) - _has((o = Oi[i]), s) && !Mi(u, o) && (u[u.length] = o), (i -= 1); - return u; - }) - : _curry1(function keys(s) { - return Object(s) !== s ? [] : Object.keys(s); - }); - const Wi = Ri; - const ea = _curry1(function type(s) { - return null === s - ? 'Null' - : void 0 === s - ? 'Undefined' - : Object.prototype.toString.call(s).slice(8, -1); - }); - function _uniqContentEquals(s, o, i, u) { - var _ = _arrayFromIterator(s); - function eq(s, o) { - return _equals(s, o, i.slice(), u.slice()); - } - return !_includesWith( - function (s, o) { - return !_includesWith(eq, o, s); - }, - _arrayFromIterator(o), - _ - ); - } - function _equals(s, o, i, u) { - if (Zo(s, o)) return !0; - var _ = ea(s); - if (_ !== ea(o)) return !1; - if ( - 'function' == typeof s['fantasy-land/equals'] || - 'function' == typeof o['fantasy-land/equals'] - ) - return ( - 'function' == typeof s['fantasy-land/equals'] && - s['fantasy-land/equals'](o) && - 'function' == typeof o['fantasy-land/equals'] && - o['fantasy-land/equals'](s) - ); - if ('function' == typeof s.equals || 'function' == typeof o.equals) - return ( - 'function' == typeof s.equals && - s.equals(o) && - 'function' == typeof o.equals && - o.equals(s) - ); - switch (_) { - case 'Arguments': - case 'Array': - case 'Object': - if ( - 'function' == typeof s.constructor && - 'Promise' === - (function _functionName(s) { - var o = String(s).match(/^function (\w*)/); - return null == o ? '' : o[1]; - })(s.constructor) - ) - return s === o; - break; - case 'Boolean': - case 'Number': - case 'String': - if (typeof s != typeof o || !Zo(s.valueOf(), o.valueOf())) return !1; - break; - case 'Date': - if (!Zo(s.valueOf(), o.valueOf())) return !1; - break; - case 'Error': - return s.name === o.name && s.message === o.message; - case 'RegExp': - if ( - s.source !== o.source || - s.global !== o.global || - s.ignoreCase !== o.ignoreCase || - s.multiline !== o.multiline || - s.sticky !== o.sticky || - s.unicode !== o.unicode - ) - return !1; - } - for (var w = i.length - 1; w >= 0; ) { - if (i[w] === s) return u[w] === o; - w -= 1; - } - switch (_) { - case 'Map': - return ( - s.size === o.size && - _uniqContentEquals(s.entries(), o.entries(), i.concat([s]), u.concat([o])) - ); - case 'Set': - return ( - s.size === o.size && - _uniqContentEquals(s.values(), o.values(), i.concat([s]), u.concat([o])) - ); - case 'Arguments': - case 'Array': - case 'Object': - case 'Boolean': - case 'Number': - case 'String': - case 'Date': - case 'Error': - case 'RegExp': - case 'Int8Array': - case 'Uint8Array': - case 'Uint8ClampedArray': - case 'Int16Array': - case 'Uint16Array': - case 'Int32Array': - case 'Uint32Array': - case 'Float32Array': - case 'Float64Array': - case 'ArrayBuffer': - break; - default: - return !1; - } - var x = Wi(s); - if (x.length !== Wi(o).length) return !1; - var C = i.concat([s]), - j = u.concat([o]); - for (w = x.length - 1; w >= 0; ) { - var L = x[w]; - if (!_has(L, o) || !_equals(o[L], s[L], C, j)) return !1; - w -= 1; - } - return !0; - } - const ra = _curry2(function equals(s, o) { - return _equals(s, o, [], []); - }); - function _includes(s, o) { - return ( - (function _indexOf(s, o, i) { - var u, _; - if ('function' == typeof s.indexOf) - switch (typeof o) { - case 'number': - if (0 === o) { - for (u = 1 / o; i < s.length; ) { - if (0 === (_ = s[i]) && 1 / _ === u) return i; - i += 1; - } - return -1; - } - if (o != o) { - for (; i < s.length; ) { - if ('number' == typeof (_ = s[i]) && _ != _) return i; - i += 1; - } - return -1; - } - return s.indexOf(o, i); - case 'string': - case 'boolean': - case 'function': - case 'undefined': - return s.indexOf(o, i); - case 'object': - if (null === o) return s.indexOf(o, i); - } - for (; i < s.length; ) { - if (ra(s[i], o)) return i; - i += 1; - } - return -1; - })(o, s, 0) >= 0 - ); - } - function _map(s, o) { - for (var i = 0, u = o.length, _ = Array(u); i < u; ) (_[i] = s(o[i])), (i += 1); - return _; - } - function _quote(s) { - return ( - '"' + - s - .replace(/\\/g, '\\\\') - .replace(/[\b]/g, '\\b') - .replace(/\f/g, '\\f') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\t/g, '\\t') - .replace(/\v/g, '\\v') - .replace(/\0/g, '\\0') - .replace(/"/g, '\\"') + - '"' - ); - } - var na = function pad(s) { - return (s < 10 ? '0' : '') + s; - }; - const ia = - 'function' == typeof Date.prototype.toISOString - ? function _toISOString(s) { - return s.toISOString(); - } - : function _toISOString(s) { - return ( - s.getUTCFullYear() + - '-' + - na(s.getUTCMonth() + 1) + - '-' + - na(s.getUTCDate()) + - 'T' + - na(s.getUTCHours()) + - ':' + - na(s.getUTCMinutes()) + - ':' + - na(s.getUTCSeconds()) + - '.' + - (s.getUTCMilliseconds() / 1e3).toFixed(3).slice(2, 5) + - 'Z' - ); - }; - function _complement(s) { - return function () { - return !s.apply(this, arguments); - }; - } - function _arrayReduce(s, o, i) { - for (var u = 0, _ = i.length; u < _; ) (o = s(o, i[u])), (u += 1); - return o; - } - const aa = - Array.isArray || - function _isArray(s) { - return ( - null != s && s.length >= 0 && '[object Array]' === Object.prototype.toString.call(s) - ); - }; - function _dispatchable(s, o, i) { - return function () { - if (0 === arguments.length) return i(); - var u = arguments[arguments.length - 1]; - if (!aa(u)) { - for (var _ = 0; _ < s.length; ) { - if ('function' == typeof u[s[_]]) - return u[s[_]].apply(u, Array.prototype.slice.call(arguments, 0, -1)); - _ += 1; - } - if ( - (function _isTransformer(s) { - return null != s && 'function' == typeof s['@@transducer/step']; - })(u) - ) - return o.apply(null, Array.prototype.slice.call(arguments, 0, -1))(u); - } - return i.apply(this, arguments); - }; - } - function _isObject(s) { - return '[object Object]' === Object.prototype.toString.call(s); - } - const _xfBase_init = function () { - return this.xf['@@transducer/init'](); - }, - _xfBase_result = function (s) { - return this.xf['@@transducer/result'](s); - }; - var la = (function () { - function XFilter(s, o) { - (this.xf = o), (this.f = s); - } - return ( - (XFilter.prototype['@@transducer/init'] = _xfBase_init), - (XFilter.prototype['@@transducer/result'] = _xfBase_result), - (XFilter.prototype['@@transducer/step'] = function (s, o) { - return this.f(o) ? this.xf['@@transducer/step'](s, o) : s; - }), - XFilter - ); - })(); - function _xfilter(s) { - return function (o) { - return new la(s, o); - }; - } - var ca = _curry2( - _dispatchable(['fantasy-land/filter', 'filter'], _xfilter, function (s, o) { - return _isObject(o) - ? _arrayReduce( - function (i, u) { - return s(o[u]) && (i[u] = o[u]), i; - }, - {}, - Wi(o) - ) - : (function _filter(s, o) { - for (var i = 0, u = o.length, _ = []; i < u; ) - s(o[i]) && (_[_.length] = o[i]), (i += 1); - return _; - })(s, o); - }) - ); - const ua = ca; - const da = _curry2(function reject(s, o) { - return ua(_complement(s), o); - }); - function _toString_toString(s, o) { - var i = function recur(i) { - var u = o.concat([s]); - return _includes(i, u) ? '' : _toString_toString(i, u); - }, - mapPairs = function (s, o) { - return _map(function (o) { - return _quote(o) + ': ' + i(s[o]); - }, o.slice().sort()); - }; - switch (Object.prototype.toString.call(s)) { - case '[object Arguments]': - return '(function() { return arguments; }(' + _map(i, s).join(', ') + '))'; - case '[object Array]': - return ( - '[' + - _map(i, s) - .concat( - mapPairs( - s, - da(function (s) { - return /^\d+$/.test(s); - }, Wi(s)) - ) - ) - .join(', ') + - ']' - ); - case '[object Boolean]': - return 'object' == typeof s ? 'new Boolean(' + i(s.valueOf()) + ')' : s.toString(); - case '[object Date]': - return 'new Date(' + (isNaN(s.valueOf()) ? i(NaN) : _quote(ia(s))) + ')'; - case '[object Map]': - return 'new Map(' + i(Array.from(s)) + ')'; - case '[object Null]': - return 'null'; - case '[object Number]': - return 'object' == typeof s - ? 'new Number(' + i(s.valueOf()) + ')' - : 1 / s == -1 / 0 - ? '-0' - : s.toString(10); - case '[object Set]': - return 'new Set(' + i(Array.from(s).sort()) + ')'; - case '[object String]': - return 'object' == typeof s ? 'new String(' + i(s.valueOf()) + ')' : _quote(s); - case '[object Undefined]': - return 'undefined'; - default: - if ('function' == typeof s.toString) { - var u = s.toString(); - if ('[object Object]' !== u) return u; - } - return '{' + mapPairs(s, Wi(s)).join(', ') + '}'; - } - } - const ma = _curry1(function toString(s) { - return _toString_toString(s, []); - }); - var ga = _curry2(function test(s, o) { - if ( - !(function _isRegExp(s) { - return '[object RegExp]' === Object.prototype.toString.call(s); - })(s) - ) - throw new TypeError( - '‘test’ requires a value of type RegExp as its first argument; received ' + ma(s) - ); - return _cloneRegExp(s).test(o); - }); - const ya = ga; - function _arity(s, o) { - switch (s) { - case 0: - return function () { - return o.apply(this, arguments); - }; - case 1: - return function (s) { - return o.apply(this, arguments); - }; - case 2: - return function (s, i) { - return o.apply(this, arguments); - }; - case 3: - return function (s, i, u) { - return o.apply(this, arguments); - }; - case 4: - return function (s, i, u, _) { - return o.apply(this, arguments); - }; - case 5: - return function (s, i, u, _, w) { - return o.apply(this, arguments); - }; - case 6: - return function (s, i, u, _, w, x) { - return o.apply(this, arguments); - }; - case 7: - return function (s, i, u, _, w, x, C) { - return o.apply(this, arguments); - }; - case 8: - return function (s, i, u, _, w, x, C, j) { - return o.apply(this, arguments); - }; - case 9: - return function (s, i, u, _, w, x, C, j, L) { - return o.apply(this, arguments); - }; - case 10: - return function (s, i, u, _, w, x, C, j, L, B) { - return o.apply(this, arguments); - }; - default: - throw new Error( - 'First argument to _arity must be a non-negative integer no greater than ten' - ); - } - } - function _pipe(s, o) { - return function () { - return o.call(this, s.apply(this, arguments)); - }; - } - const va = _curry1(function isArrayLike(s) { - return ( - !!aa(s) || - (!!s && - 'object' == typeof s && - !_isString(s) && - (0 === s.length || - (s.length > 0 && s.hasOwnProperty(0) && s.hasOwnProperty(s.length - 1)))) - ); - }); - var ba = 'undefined' != typeof Symbol ? Symbol.iterator : '@@iterator'; - function _createReduce(s, o, i) { - return function _reduce(u, _, w) { - if (va(w)) return s(u, _, w); - if (null == w) return _; - if ('function' == typeof w['fantasy-land/reduce']) - return o(u, _, w, 'fantasy-land/reduce'); - if (null != w[ba]) return i(u, _, w[ba]()); - if ('function' == typeof w.next) return i(u, _, w); - if ('function' == typeof w.reduce) return o(u, _, w, 'reduce'); - throw new TypeError('reduce: list must be array or iterable'); - }; - } - function _xArrayReduce(s, o, i) { - for (var u = 0, _ = i.length; u < _; ) { - if ((o = s['@@transducer/step'](o, i[u])) && o['@@transducer/reduced']) { - o = o['@@transducer/value']; - break; - } - u += 1; - } - return s['@@transducer/result'](o); - } - var _a = _curry2(function bind(s, o) { - return _arity(s.length, function () { - return s.apply(o, arguments); - }); - }); - const Ea = _a; - function _xIterableReduce(s, o, i) { - for (var u = i.next(); !u.done; ) { - if ((o = s['@@transducer/step'](o, u.value)) && o['@@transducer/reduced']) { - o = o['@@transducer/value']; - break; - } - u = i.next(); - } - return s['@@transducer/result'](o); - } - function _xMethodReduce(s, o, i, u) { - return s['@@transducer/result'](i[u](Ea(s['@@transducer/step'], s), o)); - } - const wa = _createReduce(_xArrayReduce, _xMethodReduce, _xIterableReduce); - var xa = (function () { - function XWrap(s) { - this.f = s; - } - return ( - (XWrap.prototype['@@transducer/init'] = function () { - throw new Error('init not implemented on XWrap'); - }), - (XWrap.prototype['@@transducer/result'] = function (s) { - return s; - }), - (XWrap.prototype['@@transducer/step'] = function (s, o) { - return this.f(s, o); - }), - XWrap - ); - })(); - function _xwrap(s) { - return new xa(s); - } - var ka = _curry3(function (s, o, i) { - return wa('function' == typeof s ? _xwrap(s) : s, o, i); - }); - const Ca = ka; - function _checkForMethod(s, o) { - return function () { - var i = arguments.length; - if (0 === i) return o(); - var u = arguments[i - 1]; - return aa(u) || 'function' != typeof u[s] - ? o.apply(this, arguments) - : u[s].apply(u, Array.prototype.slice.call(arguments, 0, i - 1)); - }; - } - var Aa = _curry3( - _checkForMethod('slice', function slice(s, o, i) { - return Array.prototype.slice.call(i, s, o); - }) - ); - const ja = Aa; - const Ia = _curry1(_checkForMethod('tail', ja(1, 1 / 0))); - function pipe() { - if (0 === arguments.length) throw new Error('pipe requires at least one argument'); - return _arity(arguments[0].length, Ca(_pipe, arguments[0], Ia(arguments))); - } - const Na = _curry2(function defaultTo(s, o) { - return null == o || o != o ? s : o; - }); - const Da = _curry2(function prop(s, o) { - if (null != o) return Yo(s) ? _nth(s, o) : o[s]; - }); - const La = _curry3(function propOr(s, o, i) { - return Na(s, Da(o, i)); - }); - var Ba = _curry1(function (s) { - return _nth(-1, s); - }); - const Fa = Ba; - function _curryN(s, o, i) { - return function () { - for (var u = [], _ = 0, w = s, x = 0, C = !1; x < o.length || _ < arguments.length; ) { - var j; - x < o.length && (!_isPlaceholder(o[x]) || _ >= arguments.length) - ? (j = o[x]) - : ((j = arguments[_]), (_ += 1)), - (u[x] = j), - _isPlaceholder(j) ? (C = !0) : (w -= 1), - (x += 1); - } - return !C && w <= 0 ? i.apply(this, u) : _arity(Math.max(0, w), _curryN(s, u, i)); - }; - } - var $a = _curry2(function curryN(s, o) { - return 1 === s ? _curry1(o) : _arity(s, _curryN(s, [], o)); - }); - const za = $a; - var Ha = _curry1(function curry(s) { - return za(s.length, s); - }); - const Ja = Ha; - function _isFunction(s) { - var o = Object.prototype.toString.call(s); - return ( - '[object Function]' === o || - '[object AsyncFunction]' === o || - '[object GeneratorFunction]' === o || - '[object AsyncGeneratorFunction]' === o - ); - } - const Ga = _curry2(function invoker(s, o) { - return za(s + 1, function () { - var i = arguments[s]; - if (null != i && _isFunction(i[o])) - return i[o].apply(i, Array.prototype.slice.call(arguments, 0, s)); - throw new TypeError(ma(i) + ' does not have a method named "' + o + '"'); - }); - }); - const tl = Ga(1, 'split'); - function dropLastWhile(s, o) { - for (var i = o.length - 1; i >= 0 && s(o[i]); ) i -= 1; - return ja(0, i + 1, o); - } - var sl = (function () { - function XDropLastWhile(s, o) { - (this.f = s), (this.retained = []), (this.xf = o); - } - return ( - (XDropLastWhile.prototype['@@transducer/init'] = _xfBase_init), - (XDropLastWhile.prototype['@@transducer/result'] = function (s) { - return (this.retained = null), this.xf['@@transducer/result'](s); - }), - (XDropLastWhile.prototype['@@transducer/step'] = function (s, o) { - return this.f(o) ? this.retain(s, o) : this.flush(s, o); - }), - (XDropLastWhile.prototype.flush = function (s, o) { - return ( - (s = wa(this.xf, s, this.retained)), - (this.retained = []), - this.xf['@@transducer/step'](s, o) - ); - }), - (XDropLastWhile.prototype.retain = function (s, o) { - return this.retained.push(o), s; - }), - XDropLastWhile - ); - })(); - function _xdropLastWhile(s) { - return function (o) { - return new sl(s, o); - }; - } - const ul = _curry2(_dispatchable([], _xdropLastWhile, dropLastWhile)); - const yl = Ga(1, 'join'); - var vl = _curry1(function flip(s) { - return za(s.length, function (o, i) { - var u = Array.prototype.slice.call(arguments, 0); - return (u[0] = i), (u[1] = o), s.apply(this, u); - }); - }); - const _l = vl(_curry2(_includes)); - const El = Ja(function (s, o) { - return pipe(tl(''), ul(_l(s)), yl(''))(o); - }); - function _iterableReduce(s, o, i) { - for (var u = i.next(); !u.done; ) (o = s(o, u.value)), (u = i.next()); - return o; - } - function _methodReduce(s, o, i, u) { - return i[u](s, o); - } - const wl = _createReduce(_arrayReduce, _methodReduce, _iterableReduce); - var Sl = (function () { - function XMap(s, o) { - (this.xf = o), (this.f = s); - } - return ( - (XMap.prototype['@@transducer/init'] = _xfBase_init), - (XMap.prototype['@@transducer/result'] = _xfBase_result), - (XMap.prototype['@@transducer/step'] = function (s, o) { - return this.xf['@@transducer/step'](s, this.f(o)); - }), - XMap - ); - })(); - var xl = _curry2( - _dispatchable( - ['fantasy-land/map', 'map'], - function _xmap(s) { - return function (o) { - return new Sl(s, o); - }; - }, - function map(s, o) { - switch (Object.prototype.toString.call(o)) { - case '[object Function]': - return za(o.length, function () { - return s.call(this, o.apply(this, arguments)); - }); - case '[object Object]': - return _arrayReduce( - function (i, u) { - return (i[u] = s(o[u])), i; - }, - {}, - Wi(o) - ); - default: - return _map(s, o); - } - } - ) - ); - const kl = xl; - const Cl = _curry2(function ap(s, o) { - return 'function' == typeof o['fantasy-land/ap'] - ? o['fantasy-land/ap'](s) - : 'function' == typeof s.ap - ? s.ap(o) - : 'function' == typeof s - ? function (i) { - return s(i)(o(i)); - } - : wl( - function (s, i) { - return (function _concat(s, o) { - var i; - o = o || []; - var u = (s = s || []).length, - _ = o.length, - w = []; - for (i = 0; i < u; ) (w[w.length] = s[i]), (i += 1); - for (i = 0; i < _; ) (w[w.length] = o[i]), (i += 1); - return w; - })(s, kl(i, o)); - }, - [], - s - ); - }); - var Ol = _curry2(function liftN(s, o) { - var i = za(s, o); - return za(s, function () { - return _arrayReduce(Cl, kl(i, arguments[0]), Array.prototype.slice.call(arguments, 1)); - }); - }); - const Al = Ol; - var Il = _curry1(function lift(s) { - return Al(s.length, s); - }); - const Pl = Il; - const Ml = Pl( - _curry1(function not(s) { - return !s; - }) - ); - const Tl = _curry1(function always(s) { - return function () { - return s; - }; - }); - const Nl = Tl(void 0); - const Rl = ra(Nl()); - const Dl = Ml(Rl); - const Ll = _curry2(function max(s, o) { - if (s === o) return o; - function safeMax(s, o) { - if (s > o != o > s) return o > s ? o : s; - } - var i = safeMax(s, o); - if (void 0 !== i) return i; - var u = safeMax(typeof s, typeof o); - if (void 0 !== u) return u === typeof s ? s : o; - var _ = ma(s), - w = safeMax(_, ma(o)); - return void 0 !== w && w === _ ? s : o; - }); - var Bl = _curry2(function pluck(s, o) { - return kl(Da(s), o); - }); - const Fl = Bl; - const $l = _curry1(function anyPass(s) { - return za(Ca(Ll, 0, Fl('length', s)), function () { - for (var o = 0, i = s.length; o < i; ) { - if (s[o].apply(this, arguments)) return !0; - o += 1; - } - return !1; - }); - }); - var identical = function (s, o) { - switch (arguments.length) { - case 0: - return identical; - case 1: - return function unaryIdentical(o) { - return 0 === arguments.length ? unaryIdentical : Zo(s, o); - }; - default: - return Zo(s, o); - } - }; - const Vl = identical; - const Ul = za(1, pipe(ea, Vl('GeneratorFunction'))); - const zl = za(1, pipe(ea, Vl('AsyncFunction'))); - const Wl = $l([pipe(ea, Vl('Function')), Ul, zl]); - var Kl = _curry3(function replace(s, o, i) { - return i.replace(s, o); - }); - const Hl = Kl; - const Jl = za(1, pipe(ea, Vl('RegExp'))); - const Gl = _curry3(function when(s, o, i) { - return s(i) ? o(i) : i; - }); - const Yl = za(1, pipe(ea, Vl('String'))); - const Xl = Gl(Yl, Hl(/[.*+?^${}()|[\]\\-]/g, '\\$&')); - var Zl = function checkValue(s, o) { - if ('string' != typeof s && !(s instanceof String)) - throw TypeError('`'.concat(o, '` must be a string')); - }; - const Ql = function replaceAll(s, o, i) { - !(function checkArguments(s, o, i) { - if (null == i || null == s || null == o) - throw TypeError('Input values must not be `null` or `undefined`'); - })(s, o, i), - Zl(i, 'str'), - Zl(o, 'replaceValue'), - (function checkSearchValue(s) { - if (!('string' == typeof s || s instanceof String || s instanceof RegExp)) - throw TypeError('`searchValue` must be a string or an regexp'); - })(s); - var u = new RegExp(Jl(s) ? s : Xl(s), 'g'); - return Hl(u, o, i); - }; - var ec = za(3, Ql), - rc = Ga(2, 'replaceAll'); - const sc = Wl(String.prototype.replaceAll) ? rc : ec, - isWindows = () => Xo(ya(/^win/), ['platform'], Go), - getProtocol = (s) => { - try { - const o = new URL(s); - return El(':', o.protocol); - } catch { - return; - } - }, - oc = - (pipe(getProtocol, Dl), - (s) => { - if (Go.browser) return !1; - const o = getProtocol(s); - return Rl(o) || 'file' === o || /^[a-zA-Z]$/.test(o); - }), - isHttpUrl = (s) => { - const o = getProtocol(s); - return 'http' === o || 'https' === o; - }, - toFileSystemPath = (s, o) => { - const i = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@'], - u = La(!1, 'keepFileProtocol', o), - _ = La(isWindows, 'isWindows', o); - let w = decodeURI(s); - for (let s = 0; s < i.length; s += 2) w = w.replace(i[s], i[s + 1]); - let x = 'file://' === w.substring(0, 7).toLowerCase(); - return ( - x && - ((w = '/' === w[7] ? w.substring(8) : w.substring(7)), - _() && '/' === w[1] && (w = `${w[0]}:${w.substring(1)}`), - u ? (w = `file:///${w}`) : ((x = !1), (w = _() ? w : `/${w}`))), - _() && - !x && - ((w = sc('/', '\\', w)), - ':\\' === w.substring(1, 3) && (w = w[0].toUpperCase() + w.substring(1))), - w - ); - }, - getHash = (s) => { - const o = s.indexOf('#'); - return -1 !== o ? s.substring(o) : '#'; - }, - stripHash = (s) => { - const o = s.indexOf('#'); - let i = s; - return o >= 0 && (i = s.substring(0, o)), i; - }, - url_cwd = () => { - if (Go.browser) return stripHash(globalThis.location.href); - const s = Go.cwd(), - o = Fa(s); - return ['/', '\\'].includes(o) ? s : s + (isWindows() ? '\\' : '/'); - }, - resolve = (s, o) => { - const i = new URL(o, new URL(s, 'resolve://')); - if ('resolve:' === i.protocol) { - const { pathname: s, search: o, hash: u } = i; - return s + o + u; - } - return i.toString(); - }, - sanitize = (s) => { - if (oc(s)) - return ((s) => { - const o = [/\?/g, '%3F', /#/g, '%23']; - let i = s; - isWindows() && (i = i.replace(/\\/g, '/')), (i = encodeURI(i)); - for (let s = 0; s < o.length; s += 2) i = i.replace(o[s], o[s + 1]); - return i; - })(toFileSystemPath(s)); - try { - return new URL(s).toString(); - } catch { - return encodeURI(decodeURI(s)).replace(/%5B/g, '[').replace(/%5D/g, ']'); - } - }, - unsanitize = (s) => (oc(s) ? toFileSystemPath(s) : decodeURI(s)), - { - fetch: ic, - Response: ac, - Headers: lc, - Request: cc, - FormData: pc, - File: hc, - Blob: dc - } = globalThis; - function _array_like_to_array(s, o) { - (null == o || o > s.length) && (o = s.length); - for (var i = 0, u = new Array(o); i < o; i++) u[i] = s[i]; - return u; - } - function legacy_defineProperties(s, o) { - for (var i = 0; i < o.length; i++) { - var u = o[i]; - (u.enumerable = u.enumerable || !1), - (u.configurable = !0), - 'value' in u && (u.writable = !0), - Object.defineProperty(s, u.key, u); - } - } - function _instanceof(s, o) { - return null != o && 'undefined' != typeof Symbol && o[Symbol.hasInstance] - ? !!o[Symbol.hasInstance](s) - : s instanceof o; - } - function _sliced_to_array(s, o) { - return ( - (function _array_with_holes(s) { - if (Array.isArray(s)) return s; - })(s) || - (function _iterable_to_array_limit(s, o) { - var i = - null == s - ? null - : ('undefined' != typeof Symbol && s[Symbol.iterator]) || s['@@iterator']; - if (null != i) { - var u, - _, - w = [], - x = !0, - C = !1; - try { - for ( - i = i.call(s); - !(x = (u = i.next()).done) && (w.push(u.value), !o || w.length !== o); - x = !0 - ); - } catch (s) { - (C = !0), (_ = s); - } finally { - try { - x || null == i.return || i.return(); - } finally { - if (C) throw _; - } - } - return w; - } - })(s, o) || - (function _unsupported_iterable_to_array(s, o) { - if (!s) return; - if ('string' == typeof s) return _array_like_to_array(s, o); - var i = Object.prototype.toString.call(s).slice(8, -1); - 'Object' === i && s.constructor && (i = s.constructor.name); - if ('Map' === i || 'Set' === i) return Array.from(i); - if ('Arguments' === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)) - return _array_like_to_array(s, o); - })(s, o) || - (function _non_iterable_rest() { - throw new TypeError( - 'Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })() - ); - } - function _type_of(s) { - return s && 'undefined' != typeof Symbol && s.constructor === Symbol - ? 'symbol' - : typeof s; - } - void 0 === globalThis.fetch && (globalThis.fetch = ic), - void 0 === globalThis.Headers && (globalThis.Headers = lc), - void 0 === globalThis.Request && (globalThis.Request = cc), - void 0 === globalThis.Response && (globalThis.Response = ac), - void 0 === globalThis.FormData && (globalThis.FormData = pc), - void 0 === globalThis.File && (globalThis.File = hc), - void 0 === globalThis.Blob && (globalThis.Blob = dc); - var __typeError = function (s) { - throw TypeError(s); - }, - __accessCheck = function (s, o, i) { - return o.has(s) || __typeError('Cannot ' + i); - }, - __privateGet = function (s, o, i) { - return __accessCheck(s, o, 'read from private field'), i ? i.call(s) : o.get(s); - }, - __privateAdd = function (s, o, i) { - return o.has(s) - ? __typeError('Cannot add the same private member more than once') - : _instanceof(o, WeakSet) - ? o.add(s) - : o.set(s, i); - }, - __privateSet = function (s, o, i, u) { - return __accessCheck(s, o, 'write to private field'), u ? u.call(s, i) : o.set(s, i), i; - }, - to_string = function (s) { - return Object.prototype.toString.call(s); - }, - is_typed_array = function (s) { - return ArrayBuffer.isView(s) && !_instanceof(s, DataView); - }, - fc = Array.isArray, - gc = Object.getOwnPropertyDescriptor, - bc = Object.prototype.propertyIsEnumerable, - _c = Object.getOwnPropertySymbols, - Ec = Object.prototype.hasOwnProperty; - function own_enumerable_keys(s) { - for (var o = Object.keys(s), i = _c(s), u = 0; u < i.length; u++) - bc.call(s, i[u]) && o.push(i[u]); - return o; - } - function is_writable(s, o) { - var i; - return !(null === (i = gc(s, o)) || void 0 === i ? void 0 : i.writable); - } - function legacy_copy(s, o) { - if ('object' === (void 0 === s ? 'undefined' : _type_of(s)) && null !== s) { - var i; - if (fc(s)) i = []; - else if ('[object Date]' === to_string(s)) i = new Date(s.getTime ? s.getTime() : s); - else if ( - (function (s) { - return '[object RegExp]' === to_string(s); - })(s) - ) - i = new RegExp(s); - else if ( - (function (s) { - return '[object Error]' === to_string(s); - })(s) - ) - i = { message: s.message }; - else if ( - (function (s) { - return '[object Boolean]' === to_string(s); - })(s) || - (function (s) { - return '[object Number]' === to_string(s); - })(s) || - (function (s) { - return '[object String]' === to_string(s); - })(s) - ) - i = Object(s); - else { - if (is_typed_array(s)) return s.slice(); - i = Object.create(Object.getPrototypeOf(s)); - } - var u = o.includeSymbols ? own_enumerable_keys : Object.keys, - _ = !0, - w = !1, - x = void 0; - try { - for (var C, j = u(s)[Symbol.iterator](); !(_ = (C = j.next()).done); _ = !0) { - var L = C.value; - i[L] = s[L]; - } - } catch (s) { - (w = !0), (x = s); - } finally { - try { - _ || null == j.return || j.return(); - } finally { - if (w) throw x; - } - } - return i; - } - return s; - } - var kc, - Oc, - jc = { includeSymbols: !1, immutable: !1 }; - function walk(s, o) { - var i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : jc, - u = [], - _ = [], - w = !0, - x = i.includeSymbols ? own_enumerable_keys : Object.keys, - C = !!i.immutable; - return (function walker(s) { - var j = C ? legacy_copy(s, i) : s, - L = {}, - B = !0, - $ = { - node: j, - node_: s, - path: [].concat(u), - parent: _[_.length - 1], - parents: _, - key: u[u.length - 1], - isRoot: 0 === u.length, - level: u.length, - circular: void 0, - isLeaf: !1, - notLeaf: !0, - notRoot: !0, - isFirst: !1, - isLast: !1, - update: function update(s) { - var o = arguments.length > 1 && void 0 !== arguments[1] && arguments[1]; - $.isRoot || ($.parent.node[$.key] = s), ($.node = s), o && (B = !1); - }, - delete: function _delete(s) { - delete $.parent.node[$.key], s && (B = !1); - }, - remove: function remove(s) { - fc($.parent.node) ? $.parent.node.splice($.key, 1) : delete $.parent.node[$.key], - s && (B = !1); - }, - keys: null, - before: function before(s) { - L.before = s; - }, - after: function after(s) { - L.after = s; - }, - pre: function pre(s) { - L.pre = s; - }, - post: function post(s) { - L.post = s; - }, - stop: function stop() { - w = !1; - }, - block: function block() { - B = !1; - } - }; - if (!w) return $; - function update_state() { - if ('object' === _type_of($.node) && null !== $.node) { - ($.keys && $.node_ === $.node) || ($.keys = x($.node)), - ($.isLeaf = 0 === $.keys.length); - for (var o = 0; o < _.length; o++) - if (_[o].node_ === s) { - $.circular = _[o]; - break; - } - } else ($.isLeaf = !0), ($.keys = null); - ($.notLeaf = !$.isLeaf), ($.notRoot = !$.isRoot); - } - update_state(); - var V = o.call($, $.node); - if ((void 0 !== V && $.update && $.update(V), L.before && L.before.call($, $.node), !B)) - return $; - if ('object' === _type_of($.node) && null !== $.node && !$.circular) { - var U; - _.push($), update_state(); - var z = !0, - Y = !1, - Z = void 0; - try { - for ( - var ee, - ie = Object.entries(null !== (U = $.keys) && void 0 !== U ? U : [])[ - Symbol.iterator - ](); - !(z = (ee = ie.next()).done); - z = !0 - ) { - var ae, - le = _sliced_to_array(ee.value, 2), - ce = le[0], - pe = le[1]; - u.push(pe), L.pre && L.pre.call($, $.node[pe], pe); - var de = walker($.node[pe]); - C && Ec.call($.node, pe) && !is_writable($.node, pe) && ($.node[pe] = de.node), - (de.isLast = - !!(null === (ae = $.keys) || void 0 === ae ? void 0 : ae.length) && - +ce == $.keys.length - 1), - (de.isFirst = 0 == +ce), - L.post && L.post.call($, de), - u.pop(); - } - } catch (s) { - (Y = !0), (Z = s); - } finally { - try { - z || null == ie.return || ie.return(); - } finally { - if (Y) throw Z; - } - } - _.pop(); - } - return L.after && L.after.call($, $.node), $; - })(s).node; - } - var Ic = (function () { - function Traverse(s) { - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : jc; - !(function _class_call_check(s, o) { - if (!(s instanceof o)) throw new TypeError('Cannot call a class as a function'); - })(this, Traverse), - __privateAdd(this, kc), - __privateAdd(this, Oc), - __privateSet(this, kc, s), - __privateSet(this, Oc, o); - } - return ( - (function _create_class(s, o, i) { - return ( - o && legacy_defineProperties(s.prototype, o), i && legacy_defineProperties(s, i), s - ); - })(Traverse, [ - { - key: 'get', - value: function get(s) { - for (var o = __privateGet(this, kc), i = 0; o && i < s.length; i++) { - var u = s[i]; - if ( - !Ec.call(o, u) || - (!__privateGet(this, Oc).includeSymbols && - 'symbol' === (void 0 === u ? 'undefined' : _type_of(u))) - ) - return; - o = o[u]; - } - return o; - } - }, - { - key: 'has', - value: function has(s) { - for (var o = __privateGet(this, kc), i = 0; o && i < s.length; i++) { - var u = s[i]; - if ( - !Ec.call(o, u) || - (!__privateGet(this, Oc).includeSymbols && - 'symbol' === (void 0 === u ? 'undefined' : _type_of(u))) - ) - return !1; - o = o[u]; - } - return !0; - } - }, - { - key: 'set', - value: function set(s, o) { - var i = __privateGet(this, kc), - u = 0; - for (u = 0; u < s.length - 1; u++) { - var _ = s[u]; - Ec.call(i, _) || (i[_] = {}), (i = i[_]); - } - return (i[s[u]] = o), o; - } - }, - { - key: 'map', - value: function map(s) { - return walk(__privateGet(this, kc), s, { - immutable: !0, - includeSymbols: !!__privateGet(this, Oc).includeSymbols - }); - } - }, - { - key: 'forEach', - value: function forEach(s) { - return ( - __privateSet(this, kc, walk(__privateGet(this, kc), s, __privateGet(this, Oc))), - __privateGet(this, kc) - ); - } - }, - { - key: 'reduce', - value: function reduce(s, o) { - var i = 1 === arguments.length, - u = i ? __privateGet(this, kc) : o; - return ( - this.forEach(function (o) { - (this.isRoot && i) || (u = s.call(this, u, o)); - }), - u - ); - } - }, - { - key: 'paths', - value: function paths() { - var s = []; - return ( - this.forEach(function () { - s.push(this.path); - }), - s - ); - } - }, - { - key: 'nodes', - value: function nodes() { - var s = []; - return ( - this.forEach(function () { - s.push(this.node); - }), - s - ); - } - }, - { - key: 'clone', - value: function clone() { - var s = [], - o = [], - i = __privateGet(this, Oc); - return is_typed_array(__privateGet(this, kc)) - ? __privateGet(this, kc).slice() - : (function clone(u) { - for (var _ = 0; _ < s.length; _++) if (s[_] === u) return o[_]; - if ('object' === (void 0 === u ? 'undefined' : _type_of(u)) && null !== u) { - var w = legacy_copy(u, i); - s.push(u), o.push(w); - var x = i.includeSymbols ? own_enumerable_keys : Object.keys, - C = !0, - j = !1, - L = void 0; - try { - for ( - var B, $ = x(u)[Symbol.iterator](); - !(C = (B = $.next()).done); - C = !0 - ) { - var V = B.value; - w[V] = clone(u[V]); - } - } catch (s) { - (j = !0), (L = s); - } finally { - try { - C || null == $.return || $.return(); - } finally { - if (j) throw L; - } - } - return s.pop(), o.pop(), w; - } - return u; - })(__privateGet(this, kc)); - } - } - ]), - Traverse - ); - })(); - (kc = new WeakMap()), (Oc = new WeakMap()); - var traverse = function (s, o) { - return new Ic(s, o); - }; - (traverse.get = function (s, o, i) { - return new Ic(s, i).get(o); - }), - (traverse.set = function (s, o, i, u) { - return new Ic(s, u).set(o, i); - }), - (traverse.has = function (s, o, i) { - return new Ic(s, i).has(o); - }), - (traverse.map = function (s, o, i) { - return new Ic(s, i).map(o); - }), - (traverse.forEach = function (s, o, i) { - return new Ic(s, i).forEach(o); - }), - (traverse.reduce = function (s, o, i, u) { - return new Ic(s, u).reduce(o, i); - }), - (traverse.paths = function (s, o) { - return new Ic(s, o).paths(); - }), - (traverse.nodes = function (s, o) { - return new Ic(s, o).nodes(); - }), - (traverse.clone = function (s, o) { - return new Ic(s, o).clone(); - }); - var Pc = traverse; - const Mc = 'application/json, application/yaml', - Nc = 'https://swagger.io', - Rc = Object.freeze({ url: '/' }), - Lc = ['properties'], - Fc = ['properties'], - qc = [ - 'definitions', - 'parameters', - 'responses', - 'securityDefinitions', - 'components/schemas', - 'components/responses', - 'components/parameters', - 'components/securitySchemes' - ], - Kc = ['schema/example', 'items/example']; - function isFreelyNamed(s) { - const o = s[s.length - 1], - i = s[s.length - 2], - u = s.join('/'); - return ( - (Lc.indexOf(o) > -1 && -1 === Fc.indexOf(i)) || - qc.indexOf(u) > -1 || - Kc.some((s) => u.indexOf(s) > -1) - ); - } - function absolutifyPointer(s, o) { - const [i, u] = s.split('#'), - _ = null != o ? o : '', - w = null != i ? i : ''; - let x; - if (isHttpUrl(_)) x = resolve(_, w); - else { - const s = resolve(Nc, _), - o = resolve(s, w).replace(Nc, ''); - x = w.startsWith('/') ? o : o.substring(1); - } - return u ? `${x}#${u}` : x; - } - const Hc = /^([a-z]+:\/\/|\/\/)/i; - class JSONRefError extends Jo {} - const Jc = {}, - Gc = new WeakMap(), - Qc = [ - (s) => 'paths' === s[0] && 'responses' === s[3] && 'examples' === s[5], - (s) => - 'paths' === s[0] && 'responses' === s[3] && 'content' === s[5] && 'example' === s[7], - (s) => - 'paths' === s[0] && - 'responses' === s[3] && - 'content' === s[5] && - 'examples' === s[7] && - 'value' === s[9], - (s) => - 'paths' === s[0] && - 'requestBody' === s[3] && - 'content' === s[4] && - 'example' === s[6], - (s) => - 'paths' === s[0] && - 'requestBody' === s[3] && - 'content' === s[4] && - 'examples' === s[6] && - 'value' === s[8], - (s) => 'paths' === s[0] && 'parameters' === s[2] && 'example' === s[4], - (s) => 'paths' === s[0] && 'parameters' === s[3] && 'example' === s[5], - (s) => - 'paths' === s[0] && 'parameters' === s[2] && 'examples' === s[4] && 'value' === s[6], - (s) => - 'paths' === s[0] && 'parameters' === s[3] && 'examples' === s[5] && 'value' === s[7], - (s) => - 'paths' === s[0] && 'parameters' === s[2] && 'content' === s[4] && 'example' === s[6], - (s) => - 'paths' === s[0] && - 'parameters' === s[2] && - 'content' === s[4] && - 'examples' === s[6] && - 'value' === s[8], - (s) => - 'paths' === s[0] && 'parameters' === s[3] && 'content' === s[4] && 'example' === s[7], - (s) => - 'paths' === s[0] && - 'parameters' === s[3] && - 'content' === s[5] && - 'examples' === s[7] && - 'value' === s[9] - ], - eu = { - key: '$ref', - plugin: (s, o, i, u) => { - const _ = u.getInstance(), - w = i.slice(0, -1); - if (isFreelyNamed(w) || ((s) => Qc.some((o) => o(s)))(w)) return; - const { baseDoc: x } = u.getContext(i); - if ('string' != typeof s) - return new JSONRefError('$ref: must be a string (JSON-Ref)', { - $ref: s, - baseDoc: x, - fullPath: i - }); - const C = refs_split(s), - j = C[0], - L = C[1] || ''; - let B, $, V; - try { - B = x || j ? absoluteify(j, x) : null; - } catch (o) { - return wrapError(o, { pointer: L, $ref: s, basePath: B, fullPath: i }); - } - if ( - (function pointerAlreadyInPath(s, o, i, u) { - let _ = Gc.get(u); - _ || ((_ = {}), Gc.set(u, _)); - const w = (function arrayToJsonPointer(s) { - if (0 === s.length) return ''; - return `/${s.map(escapeJsonPointerToken).join('/')}`; - })(i), - x = `${o || ''}#${s}`, - C = w.replace(/allOf\/\d+\/?/g, ''), - j = u.contextTree.get([]).baseDoc; - if (o === j && pointerIsAParent(C, s)) return !0; - let L = ''; - const B = i.some( - (s) => ( - (L = `${L}/${escapeJsonPointerToken(s)}`), - _[L] && _[L].some((s) => pointerIsAParent(s, x) || pointerIsAParent(x, s)) - ) - ); - if (B) return !0; - return void (_[C] = (_[C] || []).concat(x)); - })(L, B, w, u) && - !_.useCircularStructures - ) { - const o = absolutifyPointer(s, B); - return s === o ? null : zo.replace(i, o); - } - if ( - (null == B - ? ((V = jsonPointerToArray(L)), - ($ = u.get(V)), - void 0 === $ && - ($ = new JSONRefError(`Could not resolve reference: ${s}`, { - pointer: L, - $ref: s, - baseDoc: x, - fullPath: i - }))) - : (($ = extractFromDoc(B, L)), - ($ = - null != $.__value - ? $.__value - : $.catch((o) => { - throw wrapError(o, { pointer: L, $ref: s, baseDoc: x, fullPath: i }); - }))), - $ instanceof Error) - ) - return [zo.remove(i), $]; - const U = absolutifyPointer(s, B), - z = zo.replace(w, $, { $$ref: U }); - if (B && B !== x) return [z, zo.context(w, { baseDoc: B })]; - try { - if ( - !(function patchValueAlreadyInPath(s, o) { - const i = [s]; - return ( - o.path.reduce((s, o) => (i.push(s[o]), s[o]), s), pointToAncestor(o.value) - ); - function pointToAncestor(s) { - return ( - zo.isObject(s) && - (i.indexOf(s) >= 0 || Object.keys(s).some((o) => pointToAncestor(s[o]))) - ); - } - })(u.state, z) || - _.useCircularStructures - ) - return z; - } catch (s) { - return null; - } - } - }, - tu = Object.assign(eu, { - docCache: Jc, - absoluteify, - clearCache: function clearCache(s) { - void 0 !== s - ? delete Jc[s] - : Object.keys(Jc).forEach((s) => { - delete Jc[s]; - }); - }, - JSONRefError, - wrapError, - getDoc, - split: refs_split, - extractFromDoc, - fetchJSON: function fetchJSON(s) { - return fetch(s, { headers: { Accept: Mc }, loadSpec: !0 }) - .then((s) => s.text()) - .then((s) => mn.load(s)); - }, - extract, - jsonPointerToArray, - unescapeJsonPointerToken - }), - ru = tu; - function absoluteify(s, o) { - if (!Hc.test(s)) { - if (!o) - throw new JSONRefError( - `Tried to resolve a relative URL, without having a basePath. path: '${s}' basePath: '${o}'` - ); - return resolve(o, s); - } - return s; - } - function wrapError(s, o) { - let i; - return ( - (i = - s && s.response && s.response.body - ? `${s.response.body.code} ${s.response.body.message}` - : s.message), - new JSONRefError(`Could not resolve reference: ${i}`, { ...o, cause: s }) - ); - } - function refs_split(s) { - return (s + '').split('#'); - } - function extractFromDoc(s, o) { - const i = Jc[s]; - if (i && !zo.isPromise(i)) - try { - const s = extract(o, i); - return Object.assign(Promise.resolve(s), { __value: s }); - } catch (s) { - return Promise.reject(s); - } - return getDoc(s).then((s) => extract(o, s)); - } - function getDoc(s) { - const o = Jc[s]; - return o - ? zo.isPromise(o) - ? o - : Promise.resolve(o) - : ((Jc[s] = tu.fetchJSON(s).then((o) => ((Jc[s] = o), o))), Jc[s]); - } - function extract(s, o) { - const i = jsonPointerToArray(s); - if (i.length < 1) return o; - const u = zo.getIn(o, i); - if (void 0 === u) - throw new JSONRefError(`Could not resolve pointer: ${s} does not exist in document`, { - pointer: s - }); - return u; - } - function jsonPointerToArray(s) { - if ('string' != typeof s) throw new TypeError('Expected a string, got a ' + typeof s); - return ( - '/' === s[0] && (s = s.substr(1)), - '' === s ? [] : s.split('/').map(unescapeJsonPointerToken) - ); - } - function unescapeJsonPointerToken(s) { - if ('string' != typeof s) return s; - return new URLSearchParams(`=${s.replace(/~1/g, '/').replace(/~0/g, '~')}`).get(''); - } - function escapeJsonPointerToken(s) { - return new URLSearchParams([['', s.replace(/~/g, '~0').replace(/\//g, '~1')]]) - .toString() - .slice(1); - } - const pointerBoundaryChar = (s) => !s || '/' === s || '#' === s; - function pointerIsAParent(s, o) { - if (pointerBoundaryChar(o)) return !0; - const i = s.charAt(o.length), - u = o.slice(-1); - return 0 === s.indexOf(o) && (!i || '/' === i || '#' === i) && '#' !== u; - } - const nu = { - key: 'allOf', - plugin: (s, o, i, u, _) => { - if (_.meta && _.meta.$$ref) return; - const w = i.slice(0, -1); - if (isFreelyNamed(w)) return; - if (!Array.isArray(s)) { - const s = new TypeError('allOf must be an array'); - return (s.fullPath = i), s; - } - let x = !1, - C = _.value; - if ( - (w.forEach((s) => { - C && (C = C[s]); - }), - (C = { ...C }), - 0 === Object.keys(C).length) - ) - return; - delete C.allOf; - const j = []; - return ( - j.push(u.replace(w, {})), - s.forEach((s, o) => { - if (!u.isObject(s)) { - if (x) return null; - x = !0; - const s = new TypeError('Elements in allOf must be objects'); - return (s.fullPath = i), j.push(s); - } - j.push(u.mergeDeep(w, s)); - const _ = (function generateAbsoluteRefPatches( - s, - o, - { - specmap: i, - getBaseUrlForNodePath: u = (s) => i.getContext([...o, ...s]).baseDoc, - targetKeys: _ = ['$ref', '$$ref'] - } = {} - ) { - const w = []; - return ( - Pc(s).forEach(function callback() { - if (_.includes(this.key) && 'string' == typeof this.node) { - const s = this.path, - _ = o.concat(this.path), - x = absolutifyPointer(this.node, u(s)); - w.push(i.replace(_, x)); - } - }), - w - ); - })(s, i.slice(0, -1), { - getBaseUrlForNodePath: (s) => u.getContext([...i, o, ...s]).baseDoc, - specmap: u - }); - j.push(..._); - }), - C.example && j.push(u.remove([].concat(w, 'example'))), - j.push(u.mergeDeep(w, C)), - C.$$ref || j.push(u.remove([].concat(w, '$$ref'))), - j - ); - } - }, - su = { - key: 'parameters', - plugin: (s, o, i, u) => { - if (Array.isArray(s) && s.length) { - const o = Object.assign([], s), - _ = i.slice(0, -1), - w = { ...zo.getIn(u.spec, _) }; - for (let _ = 0; _ < s.length; _ += 1) { - const x = s[_]; - try { - o[_].default = u.parameterMacro(w, x); - } catch (s) { - const o = new Error(s); - return (o.fullPath = i), o; - } - } - return zo.replace(i, o); - } - return zo.replace(i, s); - } - }, - ou = { - key: 'properties', - plugin: (s, o, i, u) => { - const _ = { ...s }; - for (const o in s) - try { - _[o].default = u.modelPropertyMacro(_[o]); - } catch (s) { - const o = new Error(s); - return (o.fullPath = i), o; - } - return zo.replace(i, _); - } - }; - class ContextTree { - constructor(s) { - this.root = context_tree_createNode(s || {}); - } - set(s, o) { - const i = this.getParent(s, !0); - if (!i) return void context_tree_updateNode(this.root, o, null); - const u = s[s.length - 1], - { children: _ } = i; - _[u] ? context_tree_updateNode(_[u], o, i) : (_[u] = context_tree_createNode(o, i)); - } - get(s) { - if ((s = s || []).length < 1) return this.root.value; - let o, - i, - u = this.root; - for (let _ = 0; _ < s.length && ((i = s[_]), (o = u.children), o[i]); _ += 1) u = o[i]; - return u && u.protoValue; - } - getParent(s, o) { - return !s || s.length < 1 - ? null - : s.length < 2 - ? this.root - : s.slice(0, -1).reduce((s, i) => { - if (!s) return s; - const { children: u } = s; - return !u[i] && o && (u[i] = context_tree_createNode(null, s)), u[i]; - }, this.root); - } - } - function context_tree_createNode(s, o) { - return context_tree_updateNode({ children: {} }, s, o); - } - function context_tree_updateNode(s, o, i) { - return ( - (s.value = o || {}), - (s.protoValue = i ? { ...i.protoValue, ...s.value } : s.value), - Object.keys(s.children).forEach((o) => { - const i = s.children[o]; - s.children[o] = context_tree_updateNode(i, i.value, s); - }), - s - ); - } - const specmap_noop = () => {}; - class SpecMap { - static getPluginName(s) { - return s.pluginName; - } - static getPatchesOfType(s, o) { - return s.filter(o); - } - constructor(s) { - Object.assign( - this, - { - spec: '', - debugLevel: 'info', - plugins: [], - pluginHistory: {}, - errors: [], - mutations: [], - promisedPatches: [], - state: {}, - patches: [], - context: {}, - contextTree: new ContextTree(), - showDebug: !1, - allPatches: [], - pluginProp: 'specMap', - libMethods: Object.assign(Object.create(this), zo, { getInstance: () => this }), - allowMetaPatches: !1 - }, - s - ), - (this.get = this._get.bind(this)), - (this.getContext = this._getContext.bind(this)), - (this.hasRun = this._hasRun.bind(this)), - (this.wrappedPlugins = this.plugins - .map(this.wrapPlugin.bind(this)) - .filter(zo.isFunction)), - this.patches.push(zo.add([], this.spec)), - this.patches.push(zo.context([], this.context)), - this.updatePatches(this.patches); - } - debug(s, ...o) { - this.debugLevel === s && console.log(...o); - } - verbose(s, ...o) { - 'verbose' === this.debugLevel && console.log(`[${s}] `, ...o); - } - wrapPlugin(s, o) { - const { pathDiscriminator: i } = this; - let u, - _ = null; - return ( - s[this.pluginProp] - ? ((_ = s), (u = s[this.pluginProp])) - : zo.isFunction(s) - ? (u = s) - : zo.isObject(s) && - (u = (function createKeyBasedPlugin(s) { - const isSubPath = (s, o) => - !Array.isArray(s) || s.every((s, i) => s === o[i]); - return function* generator(o, u) { - const _ = {}; - for (const [s, i] of o.filter(zo.isAdditiveMutation).entries()) { - if (!(s < 3e3)) return; - yield* traverse(i.value, i.path, i); - } - function* traverse(o, w, x) { - if (zo.isObject(o)) { - const C = w.length - 1, - j = w[C], - L = w.indexOf('properties'), - B = 'properties' === j && C === L, - $ = u.allowMetaPatches && _[o.$$ref]; - for (const C of Object.keys(o)) { - const j = o[C], - L = w.concat(C), - V = zo.isObject(j), - U = o.$$ref; - if ( - ($ || - (V && - (u.allowMetaPatches && U && (_[U] = !0), - yield* traverse(j, L, x))), - !B && C === s.key) - ) { - const o = isSubPath(i, w); - (i && !o) || (yield s.plugin(j, C, L, u, x)); - } - } - } else s.key === w[w.length - 1] && (yield s.plugin(o, s.key, w, u)); - } - }; - })(s)), - Object.assign(u.bind(_), { pluginName: s.name || o, isGenerator: zo.isGenerator(u) }) - ); - } - nextPlugin() { - return this.wrappedPlugins.find((s) => this.getMutationsForPlugin(s).length > 0); - } - nextPromisedPatch() { - if (this.promisedPatches.length > 0) - return Promise.race(this.promisedPatches.map((s) => s.value)); - } - getPluginHistory(s) { - const o = this.constructor.getPluginName(s); - return this.pluginHistory[o] || []; - } - getPluginRunCount(s) { - return this.getPluginHistory(s).length; - } - getPluginHistoryTip(s) { - const o = this.getPluginHistory(s); - return (o && o[o.length - 1]) || {}; - } - getPluginMutationIndex(s) { - const o = this.getPluginHistoryTip(s).mutationIndex; - return 'number' != typeof o ? -1 : o; - } - updatePluginHistory(s, o) { - const i = this.constructor.getPluginName(s); - (this.pluginHistory[i] = this.pluginHistory[i] || []), this.pluginHistory[i].push(o); - } - updatePatches(s) { - zo.normalizeArray(s).forEach((s) => { - if (s instanceof Error) this.errors.push(s); - else - try { - if (!zo.isObject(s)) - return void this.debug('updatePatches', 'Got a non-object patch', s); - if ((this.showDebug && this.allPatches.push(s), zo.isPromise(s.value))) - return this.promisedPatches.push(s), void this.promisedPatchThen(s); - if (zo.isContextPatch(s)) return void this.setContext(s.path, s.value); - zo.isMutation(s) && this.updateMutations(s); - } catch (s) { - console.error(s), this.errors.push(s); - } - }); - } - updateMutations(s) { - 'object' == typeof s.value && - !Array.isArray(s.value) && - this.allowMetaPatches && - (s.value = { ...s.value }); - const o = zo.applyPatch(this.state, s, { allowMetaPatches: this.allowMetaPatches }); - o && (this.mutations.push(s), (this.state = o)); - } - removePromisedPatch(s) { - const o = this.promisedPatches.indexOf(s); - o < 0 - ? this.debug("Tried to remove a promisedPatch that isn't there!") - : this.promisedPatches.splice(o, 1); - } - promisedPatchThen(s) { - return ( - (s.value = s.value - .then((o) => { - const i = { ...s, value: o }; - this.removePromisedPatch(s), this.updatePatches(i); - }) - .catch((o) => { - this.removePromisedPatch(s), this.updatePatches(o); - })), - s.value - ); - } - getMutations(s, o) { - return ( - (s = s || 0), - 'number' != typeof o && (o = this.mutations.length), - this.mutations.slice(s, o) - ); - } - getCurrentMutations() { - return this.getMutationsForPlugin(this.getCurrentPlugin()); - } - getMutationsForPlugin(s) { - const o = this.getPluginMutationIndex(s); - return this.getMutations(o + 1); - } - getCurrentPlugin() { - return this.currentPlugin; - } - getLib() { - return this.libMethods; - } - _get(s) { - return zo.getIn(this.state, s); - } - _getContext(s) { - return this.contextTree.get(s); - } - setContext(s, o) { - return this.contextTree.set(s, o); - } - _hasRun(s) { - return this.getPluginRunCount(this.getCurrentPlugin()) > (s || 0); - } - dispatch() { - const s = this, - o = this.nextPlugin(); - if (!o) { - const s = this.nextPromisedPatch(); - if (s) return s.then(() => this.dispatch()).catch(() => this.dispatch()); - const o = { spec: this.state, errors: this.errors }; - return this.showDebug && (o.patches = this.allPatches), Promise.resolve(o); - } - if ( - ((s.pluginCount = s.pluginCount || new WeakMap()), - s.pluginCount.set(o, (s.pluginCount.get(o) || 0) + 1), - s.pluginCount[o] > 100) - ) - return Promise.resolve({ - spec: s.state, - errors: s.errors.concat(new Error("We've reached a hard limit of 100 plugin runs")) - }); - if (o !== this.currentPlugin && this.promisedPatches.length) { - const s = this.promisedPatches.map((s) => s.value); - return Promise.all(s.map((s) => s.then(specmap_noop, specmap_noop))).then(() => - this.dispatch() - ); - } - return (function executePlugin() { - s.currentPlugin = o; - const i = s.getCurrentMutations(), - u = s.mutations.length - 1; - try { - if (o.isGenerator) for (const u of o(i, s.getLib())) updatePatches(u); - else { - updatePatches(o(i, s.getLib())); - } - } catch (s) { - console.error(s), updatePatches([Object.assign(Object.create(s), { plugin: o })]); - } finally { - s.updatePluginHistory(o, { mutationIndex: u }); - } - return s.dispatch(); - })(); - function updatePatches(i) { - i && ((i = zo.fullyNormalizeArray(i)), s.updatePatches(i, o)); - } - } - } - const iu = { refs: ru, allOf: nu, parameters: su, properties: ou }; - function makeFetchJSON(s, o = {}) { - const { requestInterceptor: i, responseInterceptor: u } = o, - _ = s.withCredentials ? 'include' : 'same-origin'; - return (o) => - s({ - url: o, - loadSpec: !0, - requestInterceptor: i, - responseInterceptor: u, - headers: { Accept: Mc }, - credentials: _ - }).then((s) => s.body); - } - function isFile(s, o) { - return ( - o || 'undefined' == typeof navigator || (o = navigator), - o && 'ReactNative' === o.product - ? !(!s || 'object' != typeof s || 'string' != typeof s.uri) - : ('undefined' != typeof File && s instanceof File) || - ('undefined' != typeof Blob && s instanceof Blob) || - !!ArrayBuffer.isView(s) || - (null !== s && 'object' == typeof s && 'function' == typeof s.pipe) - ); - } - function isArrayOfFile(s, o) { - return Array.isArray(s) && s.some((s) => isFile(s, o)); - } - class FileWithData extends File { - constructor(s, o = '', i = {}) { - super([s], o, i), (this.data = s); - } - valueOf() { - return this.data; - } - toString() { - return this.valueOf(); - } - } - const isRfc3986Reserved = (s) => ":/?#[]@!$&'()*+,;=".indexOf(s) > -1, - isRfc3986Unreserved = (s) => /^[a-z0-9\-._~]+$/i.test(s); - function encodeCharacters(s, o = 'reserved') { - return [...s] - .map((s) => { - if (isRfc3986Unreserved(s)) return s; - if (isRfc3986Reserved(s) && 'unsafe' === o) return s; - const i = new TextEncoder(); - return Array.from(i.encode(s)) - .map((s) => `0${s.toString(16).toUpperCase()}`.slice(-2)) - .map((s) => `%${s}`) - .join(''); - }) - .join(''); - } - function stylize(s) { - const { value: o } = s; - return Array.isArray(o) - ? (function encodeArray({ key: s, value: o, style: i, explode: u, escape: _ }) { - if ('simple' === i) return o.map((s) => valueEncoder(s, _)).join(','); - if ('label' === i) return `.${o.map((s) => valueEncoder(s, _)).join('.')}`; - if ('matrix' === i) - return o - .map((s) => valueEncoder(s, _)) - .reduce((o, i) => (!o || u ? `${o || ''};${s}=${i}` : `${o},${i}`), ''); - if ('form' === i) { - const i = u ? `&${s}=` : ','; - return o.map((s) => valueEncoder(s, _)).join(i); - } - if ('spaceDelimited' === i) { - const i = u ? `${s}=` : ''; - return o.map((s) => valueEncoder(s, _)).join(` ${i}`); - } - if ('pipeDelimited' === i) { - const i = u ? `${s}=` : ''; - return o.map((s) => valueEncoder(s, _)).join(`|${i}`); - } - return; - })(s) - : 'object' == typeof o - ? (function encodeObject({ key: s, value: o, style: i, explode: u, escape: _ }) { - const w = Object.keys(o); - if ('simple' === i) - return w.reduce((s, i) => { - const w = valueEncoder(o[i], _); - return `${s ? `${s},` : ''}${i}${u ? '=' : ','}${w}`; - }, ''); - if ('label' === i) - return w.reduce((s, i) => { - const w = valueEncoder(o[i], _); - return `${s ? `${s}.` : '.'}${i}${u ? '=' : '.'}${w}`; - }, ''); - if ('matrix' === i && u) - return w.reduce( - (s, i) => `${s ? `${s};` : ';'}${i}=${valueEncoder(o[i], _)}`, - '' - ); - if ('matrix' === i) - return w.reduce((i, u) => { - const w = valueEncoder(o[u], _); - return `${i ? `${i},` : `;${s}=`}${u},${w}`; - }, ''); - if ('form' === i) - return w.reduce((s, i) => { - const w = valueEncoder(o[i], _); - return `${s ? `${s}${u ? '&' : ','}` : ''}${i}${u ? '=' : ','}${w}`; - }, ''); - return; - })(s) - : (function encodePrimitive({ key: s, value: o, style: i, escape: u }) { - if ('simple' === i) return valueEncoder(o, u); - if ('label' === i) return `.${valueEncoder(o, u)}`; - if ('matrix' === i) return `;${s}=${valueEncoder(o, u)}`; - if ('form' === i) return valueEncoder(o, u); - if ('deepObject' === i) return valueEncoder(o, u); - return; - })(s); - } - function valueEncoder(s, o = !1) { - return ( - Array.isArray(s) || (null !== s && 'object' == typeof s) - ? (s = JSON.stringify(s)) - : ('number' != typeof s && 'boolean' != typeof s) || (s = String(s)), - o && s.length > 0 ? encodeCharacters(s, o) : s - ); - } - const au = { form: ',', spaceDelimited: '%20', pipeDelimited: '|' }, - lu = { csv: ',', ssv: '%20', tsv: '%09', pipes: '|' }; - function formatKeyValue(s, o, i = !1) { - const { - collectionFormat: u, - allowEmptyValue: _, - serializationOption: w, - encoding: x - } = o, - C = 'object' != typeof o || Array.isArray(o) ? o : o.value, - j = i ? (s) => s.toString() : (s) => encodeURIComponent(s), - L = j(s); - if (void 0 === C && _) return [[L, '']]; - if (isFile(C) || isArrayOfFile(C)) return [[L, C]]; - if (w) return formatKeyValueBySerializationOption(s, C, i, w); - if (x) { - if ( - [typeof x.style, typeof x.explode, typeof x.allowReserved].some( - (s) => 'undefined' !== s - ) - ) { - const { style: o, explode: u, allowReserved: _ } = x; - return formatKeyValueBySerializationOption(s, C, i, { - style: o, - explode: u, - allowReserved: _ - }); - } - if ('string' == typeof x.contentType) { - if (x.contentType.startsWith('application/json')) { - const s = j('string' == typeof C ? C : JSON.stringify(C)); - return [[L, new FileWithData(s, 'blob', { type: x.contentType })]]; - } - const s = j(String(C)); - return [[L, new FileWithData(s, 'blob', { type: x.contentType })]]; - } - return 'object' != typeof C - ? [[L, j(C)]] - : Array.isArray(C) && C.every((s) => 'object' != typeof s) - ? [[L, C.map(j).join(',')]] - : [[L, j(JSON.stringify(C))]]; - } - return 'object' != typeof C - ? [[L, j(C)]] - : Array.isArray(C) - ? 'multi' === u - ? [[L, C.map(j)]] - : [[L, C.map(j).join(lu[u || 'csv'])]] - : [[L, '']]; - } - function formatKeyValueBySerializationOption(s, o, i, u) { - const _ = u.style || 'form', - w = void 0 === u.explode ? 'form' === _ : u.explode, - x = !i && (u && u.allowReserved ? 'unsafe' : 'reserved'), - encodeFn = (s) => valueEncoder(s, x), - C = i ? (s) => s : (s) => encodeFn(s); - return 'object' != typeof o - ? [[C(s), encodeFn(o)]] - : Array.isArray(o) - ? w - ? [[C(s), o.map(encodeFn)]] - : [[C(s), o.map(encodeFn).join(au[_])]] - : 'deepObject' === _ - ? Object.keys(o).map((i) => [C(`${s}[${i}]`), encodeFn(o[i])]) - : w - ? Object.keys(o).map((s) => [C(s), encodeFn(o[s])]) - : [ - [ - C(s), - Object.keys(o) - .map((s) => [`${C(s)},${encodeFn(o[s])}`]) - .join(',') - ] - ]; - } - function encodeFormOrQuery(s) { - return ((s, { encode: o = !0 } = {}) => { - const buildNestedParams = (s, o, i) => ( - null == i - ? s.append(o, '') - : Array.isArray(i) - ? i.reduce((i, u) => buildNestedParams(s, o, u), s) - : i instanceof Date - ? s.append(o, i.toISOString()) - : 'object' == typeof i - ? Object.entries(i).reduce( - (i, [u, _]) => buildNestedParams(s, `${o}[${u}]`, _), - s - ) - : s.append(o, i), - s - ), - i = Object.entries(s).reduce( - (s, [o, i]) => buildNestedParams(s, o, i), - new URLSearchParams() - ), - u = String(i); - return o ? u : decodeURIComponent(u); - })( - Object.keys(s).reduce((o, i) => { - for (const [u, _] of formatKeyValue(i, s[i])) - o[u] = _ instanceof FileWithData ? _.valueOf() : _; - return o; - }, {}), - { encode: !1 } - ); - } - function serializeRequest(s = {}) { - const { url: o = '', query: i, form: u } = s; - if (u) { - const o = Object.keys(u).some((s) => { - const { value: o } = u[s]; - return isFile(o) || isArrayOfFile(o); - }), - i = s.headers['content-type'] || s.headers['Content-Type']; - if (o || /multipart\/form-data/i.test(i)) { - const o = (function request_buildFormData(s) { - return Object.entries(s).reduce((s, [o, i]) => { - for (const [u, _] of formatKeyValue(o, i, !0)) - if (Array.isArray(_)) - for (const o of _) - if (ArrayBuffer.isView(o)) { - const i = new Blob([o]); - s.append(u, i); - } else s.append(u, o); - else if (ArrayBuffer.isView(_)) { - const o = new Blob([_]); - s.append(u, o); - } else s.append(u, _); - return s; - }, new FormData()); - })(s.form); - (s.formdata = o), (s.body = o); - } else s.body = encodeFormOrQuery(u); - delete s.form; - } - if (i) { - const [u, _] = o.split('?'); - let w = ''; - if (_) { - const s = new URLSearchParams(_); - Object.keys(i).forEach((o) => s.delete(o)), (w = String(s)); - } - const x = ((...s) => { - const o = s.filter((s) => s).join('&'); - return o ? `?${o}` : ''; - })(w, encodeFormOrQuery(i)); - (s.url = u + x), delete s.query; - } - return s; - } - function serializeHeaders(s = {}) { - return 'function' != typeof s.entries - ? {} - : Array.from(s.entries()).reduce( - (s, [o, i]) => ( - (s[o] = (function serializeHeaderValue(s) { - return s.includes(', ') ? s.split(', ') : s; - })(i)), - s - ), - {} - ); - } - function serializeResponse(s, o, { loadSpec: i = !1 } = {}) { - const u = { - ok: s.ok, - url: s.url || o, - status: s.status, - statusText: s.statusText, - headers: serializeHeaders(s.headers) - }, - _ = u.headers['content-type'], - w = i || ((s = '') => /(json|xml|yaml|text)\b/.test(s))(_); - return (w ? s.text : s.blob || s.buffer).call(s).then((s) => { - if (((u.text = s), (u.data = s), w)) - try { - const o = (function parseBody(s, o) { - return o && (0 === o.indexOf('application/json') || o.indexOf('+json') > 0) - ? JSON.parse(s) - : mn.load(s); - })(s, _); - (u.body = o), (u.obj = o); - } catch (s) { - u.parseError = s; - } - return u; - }); - } - async function http_http(s, o = {}) { - 'object' == typeof s && (s = (o = s).url), - (o.headers = o.headers || {}), - (o = serializeRequest(o)).headers && - Object.keys(o.headers).forEach((s) => { - const i = o.headers[s]; - 'string' == typeof i && (o.headers[s] = i.replace(/\n+/g, ' ')); - }), - o.requestInterceptor && (o = (await o.requestInterceptor(o)) || o); - const i = o.headers['content-type'] || o.headers['Content-Type']; - let u; - /multipart\/form-data/i.test(i) && - (delete o.headers['content-type'], delete o.headers['Content-Type']); - try { - (u = await (o.userFetch || fetch)(o.url, o)), - (u = await serializeResponse(u, s, o)), - o.responseInterceptor && (u = (await o.responseInterceptor(u)) || u); - } catch (s) { - if (!u) throw s; - const o = new Error(u.statusText || `response status is ${u.status}`); - throw ((o.status = u.status), (o.statusCode = u.status), (o.responseError = s), o); - } - if (!u.ok) { - const s = new Error(u.statusText || `response status is ${u.status}`); - throw ((s.status = u.status), (s.statusCode = u.status), (s.response = u), s); - } - return u; - } - const options_retrievalURI = (s) => { - var o, i; - const { baseDoc: u, url: _ } = s, - w = null !== (o = null != u ? u : _) && void 0 !== o ? o : ''; - return 'string' == - typeof (null === (i = globalThis.document) || void 0 === i ? void 0 : i.baseURI) - ? String(new URL(w, globalThis.document.baseURI)) - : w; - }, - options_httpClient = (s) => { - const { fetch: o, http: i } = s; - return o || i || http_http; - }; - async function resolveGenericStrategy(s) { - const { - spec: o, - mode: i, - allowMetaPatches: u = !0, - pathDiscriminator: _, - modelPropertyMacro: w, - parameterMacro: x, - requestInterceptor: C, - responseInterceptor: j, - skipNormalization: L = !1, - useCircularStructures: B, - strategies: $ - } = s, - V = options_retrievalURI(s), - U = options_httpClient(s), - z = $.find((s) => s.match(o)); - return (async function doResolve(s) { - V && (iu.refs.docCache[V] = s); - iu.refs.fetchJSON = makeFetchJSON(U, { requestInterceptor: C, responseInterceptor: j }); - const o = [iu.refs]; - 'function' == typeof x && o.push(iu.parameters); - 'function' == typeof w && o.push(iu.properties); - 'strict' !== i && o.push(iu.allOf); - const $ = await (function mapSpec(s) { - return new SpecMap(s).dispatch(); - })({ - spec: s, - context: { baseDoc: V }, - plugins: o, - allowMetaPatches: u, - pathDiscriminator: _, - parameterMacro: x, - modelPropertyMacro: w, - useCircularStructures: B - }); - L || ($.spec = z.normalize($.spec)); - return $; - })(o); - } - const replace_special_chars_with_underscore = (s) => s.replace(/\W/gi, '_'); - function opId(s, o, i = '', { v2OperationIdCompatibilityMode: u } = {}) { - if (!s || 'object' != typeof s) return null; - return (s.operationId || '').replace(/\s/g, '').length - ? replace_special_chars_with_underscore(s.operationId) - : (function idFromPathMethod(s, o, { v2OperationIdCompatibilityMode: i } = {}) { - if (i) { - let i = `${o.toLowerCase()}_${s}`.replace( - /[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g, - '_' - ); - return ( - (i = i || `${s.substring(1)}_${o}`), - i - .replace(/((_){2,})/g, '_') - .replace(/^(_)*/g, '') - .replace(/([_])*$/g, '') - ); - } - return `${o.toLowerCase()}${replace_special_chars_with_underscore(s)}`; - })(o, i, { v2OperationIdCompatibilityMode: u }); - } - function normalize(s) { - const { spec: o } = s, - { paths: i } = o, - u = {}; - if (!i || o.$$normalized) return s; - for (const s in i) { - const _ = i[s]; - if (null == _ || !['object', 'function'].includes(typeof _)) continue; - const w = _.parameters; - for (const i in _) { - const x = _[i]; - if (null == x || !['object', 'function'].includes(typeof x)) continue; - const C = opId(x, s, i); - if (C) { - u[C] ? u[C].push(x) : (u[C] = [x]); - const s = u[C]; - if (s.length > 1) - s.forEach((s, o) => { - (s.__originalOperationId = s.__originalOperationId || s.operationId), - (s.operationId = `${C}${o + 1}`); - }); - else if (void 0 !== x.operationId) { - const o = s[0]; - (o.__originalOperationId = o.__originalOperationId || x.operationId), - (o.operationId = C); - } - } - if ('parameters' !== i) { - const s = [], - i = {}; - for (const u in o) - ('produces' !== u && 'consumes' !== u && 'security' !== u) || - ((i[u] = o[u]), s.push(i)); - if ((w && ((i.parameters = w), s.push(i)), s.length)) - for (const o of s) - for (const s in o) - if (x[s]) { - if ('parameters' === s) - for (const i of o[s]) { - x[s].some( - (s) => - (s.name && s.name === i.name) || - (s.$ref && s.$ref === i.$ref) || - (s.$$ref && s.$$ref === i.$$ref) || - s === i - ) || x[s].push(i); - } - } else x[s] = o[s]; - } - } - } - return (o.$$normalized = !0), s; - } - const cu = { - name: 'generic', - match: () => !0, - normalize(s) { - const { spec: o } = normalize({ spec: s }); - return o; - }, - resolve: async (s) => resolveGenericStrategy(s) - }, - uu = cu; - const isOpenAPI30 = (s) => { - try { - const { openapi: o } = s; - return 'string' == typeof o && /^3\.0\.([0123])(?:-rc[012])?$/.test(o); - } catch { - return !1; - } - }, - isOpenAPI31 = (s) => { - try { - const { openapi: o } = s; - return 'string' == typeof o && /^3\.1\.(?:[1-9]\d*|0)$/.test(o); - } catch { - return !1; - } - }, - isOpenAPI3 = (s) => isOpenAPI30(s) || isOpenAPI31(s), - pu = { - name: 'openapi-2', - match: (s) => - ((s) => { - try { - const { swagger: o } = s; - return '2.0' === o; - } catch { - return !1; - } - })(s), - normalize(s) { - const { spec: o } = normalize({ spec: s }); - return o; - }, - resolve: async (s) => - (async function resolveOpenAPI2Strategy(s) { - return resolveGenericStrategy(s); - })(s) - }, - hu = pu; - const du = { - name: 'openapi-3-0', - match: (s) => isOpenAPI30(s), - normalize(s) { - const { spec: o } = normalize({ spec: s }); - return o; - }, - resolve: async (s) => - (async function resolveOpenAPI30Strategy(s) { - return resolveGenericStrategy(s); - })(s) - }, - fu = du; - const mu = _curry2(function and(s, o) { - return s && o; - }); - const gu = _curry2(function both(s, o) { - return _isFunction(s) - ? function _both() { - return s.apply(this, arguments) && o.apply(this, arguments); - } - : Pl(mu)(s, o); - }); - const yu = ra(null); - const vu = Ml(yu); - function isOfTypeObject_typeof(s) { - return ( - (isOfTypeObject_typeof = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (s) { - return typeof s; - } - : function (s) { - return s && - 'function' == typeof Symbol && - s.constructor === Symbol && - s !== Symbol.prototype - ? 'symbol' - : typeof s; - }), - isOfTypeObject_typeof(s) - ); - } - const bu = function isOfTypeObject(s) { - return 'object' === isOfTypeObject_typeof(s); - }; - const _u = za(1, gu(vu, bu)); - var Eu = pipe(ea, Vl('Object')), - wu = pipe(ma, ra(ma(Object))), - Su = Xo(gu(Wl, wu), ['constructor']), - xu = za(1, function (s) { - if (!_u(s) || !Eu(s)) return !1; - var o = Object.getPrototypeOf(s); - return !!yu(o) || Su(o); - }); - const ku = xu; - var Cu = __webpack_require__(34035); - function _reduced(s) { - return s && s['@@transducer/reduced'] - ? s - : { '@@transducer/value': s, '@@transducer/reduced': !0 }; - } - var Ou = (function () { - function XAll(s, o) { - (this.xf = o), (this.f = s), (this.all = !0); - } - return ( - (XAll.prototype['@@transducer/init'] = _xfBase_init), - (XAll.prototype['@@transducer/result'] = function (s) { - return ( - this.all && (s = this.xf['@@transducer/step'](s, !0)), - this.xf['@@transducer/result'](s) - ); - }), - (XAll.prototype['@@transducer/step'] = function (s, o) { - return ( - this.f(o) || ((this.all = !1), (s = _reduced(this.xf['@@transducer/step'](s, !1)))), - s - ); - }), - XAll - ); - })(); - function _xall(s) { - return function (o) { - return new Ou(s, o); - }; - } - var Au = _curry2( - _dispatchable(['all'], _xall, function all(s, o) { - for (var i = 0; i < o.length; ) { - if (!s(o[i])) return !1; - i += 1; - } - return !0; - }) - ); - const ju = Au; - class Annotation extends Cu.Om { - constructor(s, o, i) { - super(s, o, i), (this.element = 'annotation'); - } - get code() { - return this.attributes.get('code'); - } - set code(s) { - this.attributes.set('code', s); - } - } - const Iu = Annotation; - class Comment extends Cu.Om { - constructor(s, o, i) { - super(s, o, i), (this.element = 'comment'); - } - } - const Pu = Comment; - class ParseResult extends Cu.wE { - constructor(s, o, i) { - super(s, o, i), (this.element = 'parseResult'); - } - get api() { - return this.children.filter((s) => s.classes.contains('api')).first; - } - get results() { - return this.children.filter((s) => s.classes.contains('result')); - } - get result() { - return this.results.first; - } - get annotations() { - return this.children.filter((s) => 'annotation' === s.element); - } - get warnings() { - return this.children.filter( - (s) => 'annotation' === s.element && s.classes.contains('warning') - ); - } - get errors() { - return this.children.filter( - (s) => 'annotation' === s.element && s.classes.contains('error') - ); - } - get isEmpty() { - return this.children.reject((s) => 'annotation' === s.element).isEmpty; - } - replaceResult(s) { - const { result: o } = this; - if (Rl(o)) return !1; - const i = this.content.findIndex((s) => s === o); - return -1 !== i && ((this.content[i] = s), !0); - } - } - const Mu = ParseResult; - class SourceMap extends Cu.wE { - constructor(s, o, i) { - super(s, o, i), (this.element = 'sourceMap'); - } - get positionStart() { - return this.children.filter((s) => s.classes.contains('position')).get(0); - } - get positionEnd() { - return this.children.filter((s) => s.classes.contains('position')).get(1); - } - set position(s) { - if (void 0 === s) return; - const o = new Cu.wE([s.start.row, s.start.column, s.start.char]), - i = new Cu.wE([s.end.row, s.end.column, s.end.char]); - o.classes.push('position'), i.classes.push('position'), this.push(o).push(i); - } - } - const Tu = SourceMap, - hasMethod = (s, o) => - 'object' == typeof o && null !== o && s in o && 'function' == typeof o[s], - hasBasicElementProps = (s) => - 'object' == typeof s && - null != s && - '_storedElement' in s && - 'string' == typeof s._storedElement && - '_content' in s, - primitiveEq = (s, o) => - 'object' == typeof o && - null !== o && - 'primitive' in o && - 'function' == typeof o.primitive && - o.primitive() === s, - hasClass = (s, o) => - 'object' == typeof o && - null !== o && - 'classes' in o && - (Array.isArray(o.classes) || o.classes instanceof Cu.wE) && - o.classes.includes(s), - isElementType = (s, o) => - 'object' == typeof o && null !== o && 'element' in o && o.element === s, - helpers = (s) => - s({ hasMethod, hasBasicElementProps, primitiveEq, isElementType, hasClass }), - Nu = helpers( - ({ hasBasicElementProps: s, primitiveEq: o }) => - (i) => - i instanceof Cu.Hg || (s(i) && o(void 0, i)) - ), - Ru = helpers( - ({ hasBasicElementProps: s, primitiveEq: o }) => - (i) => - i instanceof Cu.Om || (s(i) && o('string', i)) - ), - Du = helpers( - ({ hasBasicElementProps: s, primitiveEq: o }) => - (i) => - i instanceof Cu.kT || (s(i) && o('number', i)) - ), - Lu = helpers( - ({ hasBasicElementProps: s, primitiveEq: o }) => - (i) => - i instanceof Cu.Os || (s(i) && o('null', i)) - ), - Bu = helpers( - ({ hasBasicElementProps: s, primitiveEq: o }) => - (i) => - i instanceof Cu.bd || (s(i) && o('boolean', i)) - ), - Fu = helpers( - ({ hasBasicElementProps: s, primitiveEq: o, hasMethod: i }) => - (u) => - u instanceof Cu.Sh || - (s(u) && o('object', u) && i('keys', u) && i('values', u) && i('items', u)) - ), - qu = helpers( - ({ hasBasicElementProps: s, primitiveEq: o, hasMethod: i }) => - (u) => - (u instanceof Cu.wE && !(u instanceof Cu.Sh)) || - (s(u) && - o('array', u) && - i('push', u) && - i('unshift', u) && - i('map', u) && - i('reduce', u)) - ), - $u = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Cu.Pr || (s(u) && o('member', u) && i(void 0, u)) - ), - Vu = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Cu.Ft || (s(u) && o('link', u) && i(void 0, u)) - ), - Uu = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Cu.sI || (s(u) && o('ref', u) && i(void 0, u)) - ), - zu = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Iu || (s(u) && o('annotation', u) && i('array', u)) - ), - Wu = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Pu || (s(u) && o('comment', u) && i('string', u)) - ), - Ku = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Mu || (s(u) && o('parseResult', u) && i('array', u)) - ), - Hu = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Tu || (s(u) && o('sourceMap', u) && i('array', u)) - ), - isPrimitiveElement = (s) => - isElementType('object', s) || - isElementType('array', s) || - isElementType('boolean', s) || - isElementType('number', s) || - isElementType('string', s) || - isElementType('null', s) || - isElementType('member', s), - hasElementSourceMap = (s) => Hu(s.meta.get('sourceMap')), - includesSymbols = (s, o) => { - if (0 === s.length) return !0; - const i = o.attributes.get('symbols'); - return !!qu(i) && ju(_l(i.toValue()), s); - }, - includesClasses = (s, o) => 0 === s.length || ju(_l(o.classes.toValue()), s); - const es_T = function () { - return !0; - }; - const es_F = function () { - return !1; - }, - getVisitFn = (s, o, i) => { - const u = s[o]; - if (null != u) { - if (!i && 'function' == typeof u) return u; - const s = i ? u.leave : u.enter; - if ('function' == typeof s) return s; - } else { - const u = i ? s.leave : s.enter; - if (null != u) { - if ('function' == typeof u) return u; - const s = u[o]; - if ('function' == typeof s) return s; - } - } - return null; - }, - Ju = {}, - getNodeType = (s) => (null == s ? void 0 : s.type), - isNode = (s) => 'string' == typeof getNodeType(s), - cloneNode = (s) => - Object.create(Object.getPrototypeOf(s), Object.getOwnPropertyDescriptors(s)), - mergeAll = ( - s, - { - visitFnGetter: o = getVisitFn, - nodeTypeGetter: i = getNodeType, - breakSymbol: u = Ju, - deleteNodeSymbol: _ = null, - skipVisitingNodeSymbol: w = !1, - exposeEdits: x = !1 - } = {} - ) => { - const C = Symbol('skip'), - j = new Array(s.length).fill(C); - return { - enter(L, B, $, V, U, z) { - let Y = L, - Z = !1; - const ee = { - ...z, - replaceWith(s, o) { - z.replaceWith(s, o), (Y = s); - } - }; - for (let L = 0; L < s.length; L += 1) - if (j[L] === C) { - const C = o(s[L], i(Y), !1); - if ('function' == typeof C) { - const o = C.call(s[L], Y, B, $, V, U, ee); - if ('function' == typeof (null == o ? void 0 : o.then)) - throw new Jo('Async visitor not supported in sync mode', { - visitor: s[L], - visitFn: C - }); - if (o === w) j[L] = Y; - else if (o === u) j[L] = u; - else { - if (o === _) return o; - if (void 0 !== o) { - if (!x) return o; - (Y = o), (Z = !0); - } - } - } - } - return Z ? Y : void 0; - }, - leave(_, x, L, B, $, V) { - let U = _; - const z = { - ...V, - replaceWith(s, o) { - V.replaceWith(s, o), (U = s); - } - }; - for (let _ = 0; _ < s.length; _ += 1) - if (j[_] === C) { - const C = o(s[_], i(U), !0); - if ('function' == typeof C) { - const o = C.call(s[_], U, x, L, B, $, z); - if ('function' == typeof (null == o ? void 0 : o.then)) - throw new Jo('Async visitor not supported in sync mode', { - visitor: s[_], - visitFn: C - }); - if (o === u) j[_] = u; - else if (void 0 !== o && o !== w) return o; - } - } else j[_] === U && (j[_] = C); - } - }; - }; - mergeAll[Symbol.for('nodejs.util.promisify.custom')] = ( - s, - { - visitFnGetter: o = getVisitFn, - nodeTypeGetter: i = getNodeType, - breakSymbol: u = Ju, - deleteNodeSymbol: _ = null, - skipVisitingNodeSymbol: w = !1, - exposeEdits: x = !1 - } = {} - ) => { - const C = Symbol('skip'), - j = new Array(s.length).fill(C); - return { - async enter(L, B, $, V, U, z) { - let Y = L, - Z = !1; - const ee = { - ...z, - replaceWith(s, o) { - z.replaceWith(s, o), (Y = s); - } - }; - for (let L = 0; L < s.length; L += 1) - if (j[L] === C) { - const C = o(s[L], i(Y), !1); - if ('function' == typeof C) { - const o = await C.call(s[L], Y, B, $, V, U, ee); - if (o === w) j[L] = Y; - else if (o === u) j[L] = u; - else { - if (o === _) return o; - if (void 0 !== o) { - if (!x) return o; - (Y = o), (Z = !0); - } - } - } - } - return Z ? Y : void 0; - }, - async leave(_, x, L, B, $, V) { - let U = _; - const z = { - ...V, - replaceWith(s, o) { - V.replaceWith(s, o), (U = s); - } - }; - for (let _ = 0; _ < s.length; _ += 1) - if (j[_] === C) { - const C = o(s[_], i(U), !0); - if ('function' == typeof C) { - const o = await C.call(s[_], U, x, L, B, $, z); - if (o === u) j[_] = u; - else if (void 0 !== o && o !== w) return o; - } - } else j[_] === U && (j[_] = C); - } - }; - }; - const visit = ( - s, - o, - { - keyMap: i = null, - state: u = {}, - breakSymbol: _ = Ju, - deleteNodeSymbol: w = null, - skipVisitingNodeSymbol: x = !1, - visitFnGetter: C = getVisitFn, - nodeTypeGetter: j = getNodeType, - nodePredicate: L = isNode, - nodeCloneFn: B = cloneNode, - detectCycles: $ = !0 - } = {} - ) => { - const V = i || {}; - let U, - z, - Y = Array.isArray(s), - Z = [s], - ee = -1, - ie = [], - ae = s; - const le = [], - ce = []; - do { - ee += 1; - const s = ee === Z.length; - let i; - const fe = s && 0 !== ie.length; - if (s) { - if (((i = 0 === ce.length ? void 0 : le.pop()), (ae = z), (z = ce.pop()), fe)) - if (Y) { - ae = ae.slice(); - let s = 0; - for (const [o, i] of ie) { - const u = o - s; - i === w ? (ae.splice(u, 1), (s += 1)) : (ae[u] = i); - } - } else { - ae = B(ae); - for (const [s, o] of ie) ae[s] = o; - } - (ee = U.index), (Z = U.keys), (ie = U.edits), (Y = U.inArray), (U = U.prev); - } else if (z !== w && void 0 !== z) { - if (((i = Y ? ee : Z[ee]), (ae = z[i]), ae === w || void 0 === ae)) continue; - le.push(i); - } - let ye; - if (!Array.isArray(ae)) { - var pe; - if (!L(ae)) throw new Jo(`Invalid AST Node: ${String(ae)}`, { node: ae }); - if ($ && ce.includes(ae)) { - le.pop(); - continue; - } - const w = C(o, j(ae), s); - if (w) { - for (const [s, i] of Object.entries(u)) o[s] = i; - const _ = { - replaceWith(o, u) { - 'function' == typeof u ? u(o, ae, i, z, le, ce) : z && (z[i] = o), - s || (ae = o); - } - }; - ye = w.call(o, ae, i, z, le, ce, _); - } - if ('function' == typeof (null === (pe = ye) || void 0 === pe ? void 0 : pe.then)) - throw new Jo('Async visitor not supported in sync mode', { - visitor: o, - visitFn: w - }); - if (ye === _) break; - if (ye === x) { - if (!s) { - le.pop(); - continue; - } - } else if (void 0 !== ye && (ie.push([i, ye]), !s)) { - if (!L(ye)) { - le.pop(); - continue; - } - ae = ye; - } - } - var de; - if ((void 0 === ye && fe && ie.push([i, ae]), !s)) - (U = { inArray: Y, index: ee, keys: Z, edits: ie, prev: U }), - (Y = Array.isArray(ae)), - (Z = Y ? ae : null !== (de = V[j(ae)]) && void 0 !== de ? de : []), - (ee = -1), - (ie = []), - z !== w && void 0 !== z && ce.push(z), - (z = ae); - } while (void 0 !== U); - return 0 !== ie.length ? ie[ie.length - 1][1] : s; - }; - visit[Symbol.for('nodejs.util.promisify.custom')] = async ( - s, - o, - { - keyMap: i = null, - state: u = {}, - breakSymbol: _ = Ju, - deleteNodeSymbol: w = null, - skipVisitingNodeSymbol: x = !1, - visitFnGetter: C = getVisitFn, - nodeTypeGetter: j = getNodeType, - nodePredicate: L = isNode, - nodeCloneFn: B = cloneNode, - detectCycles: $ = !0 - } = {} - ) => { - const V = i || {}; - let U, - z, - Y = Array.isArray(s), - Z = [s], - ee = -1, - ie = [], - ae = s; - const le = [], - ce = []; - do { - ee += 1; - const s = ee === Z.length; - let i; - const de = s && 0 !== ie.length; - if (s) { - if (((i = 0 === ce.length ? void 0 : le.pop()), (ae = z), (z = ce.pop()), de)) - if (Y) { - ae = ae.slice(); - let s = 0; - for (const [o, i] of ie) { - const u = o - s; - i === w ? (ae.splice(u, 1), (s += 1)) : (ae[u] = i); - } - } else { - ae = B(ae); - for (const [s, o] of ie) ae[s] = o; - } - (ee = U.index), (Z = U.keys), (ie = U.edits), (Y = U.inArray), (U = U.prev); - } else if (z !== w && void 0 !== z) { - if (((i = Y ? ee : Z[ee]), (ae = z[i]), ae === w || void 0 === ae)) continue; - le.push(i); - } - let fe; - if (!Array.isArray(ae)) { - if (!L(ae)) throw new Jo(`Invalid AST Node: ${String(ae)}`, { node: ae }); - if ($ && ce.includes(ae)) { - le.pop(); - continue; - } - const w = C(o, j(ae), s); - if (w) { - for (const [s, i] of Object.entries(u)) o[s] = i; - const _ = { - replaceWith(o, u) { - 'function' == typeof u ? u(o, ae, i, z, le, ce) : z && (z[i] = o), - s || (ae = o); - } - }; - fe = await w.call(o, ae, i, z, le, ce, _); - } - if (fe === _) break; - if (fe === x) { - if (!s) { - le.pop(); - continue; - } - } else if (void 0 !== fe && (ie.push([i, fe]), !s)) { - if (!L(fe)) { - le.pop(); - continue; - } - ae = fe; - } - } - var pe; - if ((void 0 === fe && de && ie.push([i, ae]), !s)) - (U = { inArray: Y, index: ee, keys: Z, edits: ie, prev: U }), - (Y = Array.isArray(ae)), - (Z = Y ? ae : null !== (pe = V[j(ae)]) && void 0 !== pe ? pe : []), - (ee = -1), - (ie = []), - z !== w && void 0 !== z && ce.push(z), - (z = ae); - } while (void 0 !== U); - return 0 !== ie.length ? ie[ie.length - 1][1] : s; - }; - const Gu = class CloneError extends Jo { - value; - constructor(s, o) { - super(s, o), void 0 !== o && (this.value = o.value); - } - }; - const Yu = class DeepCloneError extends Gu {}; - const Xu = class ShallowCloneError extends Gu {}, - cloneDeep = (s, o = {}) => { - const { visited: i = new WeakMap() } = o, - u = { ...o, visited: i }; - if (i.has(s)) return i.get(s); - if (s instanceof Cu.KeyValuePair) { - const { key: o, value: _ } = s, - w = Nu(o) ? cloneDeep(o, u) : o, - x = Nu(_) ? cloneDeep(_, u) : _, - C = new Cu.KeyValuePair(w, x); - return i.set(s, C), C; - } - if (s instanceof Cu.ot) { - const mapper = (s) => cloneDeep(s, u), - o = [...s].map(mapper), - _ = new Cu.ot(o); - return i.set(s, _), _; - } - if (s instanceof Cu.G6) { - const mapper = (s) => cloneDeep(s, u), - o = [...s].map(mapper), - _ = new Cu.G6(o); - return i.set(s, _), _; - } - if (Nu(s)) { - const o = cloneShallow(s); - if ((i.set(s, o), s.content)) - if (Nu(s.content)) o.content = cloneDeep(s.content, u); - else if (s.content instanceof Cu.KeyValuePair) o.content = cloneDeep(s.content, u); - else if (Array.isArray(s.content)) { - const mapper = (s) => cloneDeep(s, u); - o.content = s.content.map(mapper); - } else o.content = s.content; - else o.content = s.content; - return o; - } - throw new Yu("Value provided to cloneDeep function couldn't be cloned", { value: s }); - }; - cloneDeep.safe = (s) => { - try { - return cloneDeep(s); - } catch { - return s; - } - }; - const cloneShallowKeyValuePair = (s) => { - const { key: o, value: i } = s; - return new Cu.KeyValuePair(o, i); - }, - cloneShallowElement = (s) => { - const o = new s.constructor(); - if ( - ((o.element = s.element), - s.meta.length > 0 && (o._meta = cloneDeep(s.meta)), - s.attributes.length > 0 && (o._attributes = cloneDeep(s.attributes)), - Nu(s.content)) - ) { - const i = s.content; - o.content = cloneShallowElement(i); - } else - Array.isArray(s.content) - ? (o.content = [...s.content]) - : s.content instanceof Cu.KeyValuePair - ? (o.content = cloneShallowKeyValuePair(s.content)) - : (o.content = s.content); - return o; - }, - cloneShallow = (s) => { - if (s instanceof Cu.KeyValuePair) return cloneShallowKeyValuePair(s); - if (s instanceof Cu.ot) - return ((s) => { - const o = [...s]; - return new Cu.ot(o); - })(s); - if (s instanceof Cu.G6) - return ((s) => { - const o = [...s]; - return new Cu.G6(o); - })(s); - if (Nu(s)) return cloneShallowElement(s); - throw new Xu("Value provided to cloneShallow function couldn't be cloned", { - value: s - }); - }; - cloneShallow.safe = (s) => { - try { - return cloneShallow(s); - } catch { - return s; - } - }; - const visitor_getNodeType = (s) => - Fu(s) - ? 'ObjectElement' - : qu(s) - ? 'ArrayElement' - : $u(s) - ? 'MemberElement' - : Ru(s) - ? 'StringElement' - : Bu(s) - ? 'BooleanElement' - : Du(s) - ? 'NumberElement' - : Lu(s) - ? 'NullElement' - : Vu(s) - ? 'LinkElement' - : Uu(s) - ? 'RefElement' - : void 0, - visitor_cloneNode = (s) => (Nu(s) ? cloneShallow(s) : cloneNode(s)), - Zu = pipe(visitor_getNodeType, Yl), - Qu = { - ObjectElement: ['content'], - ArrayElement: ['content'], - MemberElement: ['key', 'value'], - StringElement: [], - BooleanElement: [], - NumberElement: [], - NullElement: [], - RefElement: [], - LinkElement: [], - Annotation: [], - Comment: [], - ParseResultElement: ['content'], - SourceMap: ['content'] - }; - class PredicateVisitor { - result; - predicate; - returnOnTrue; - returnOnFalse; - constructor({ predicate: s = es_F, returnOnTrue: o, returnOnFalse: i } = {}) { - (this.result = []), - (this.predicate = s), - (this.returnOnTrue = o), - (this.returnOnFalse = i); - } - enter(s) { - return this.predicate(s) - ? (this.result.push(s), this.returnOnTrue) - : this.returnOnFalse; - } - } - const visitor_visit = (s, o, { keyMap: i = Qu, ...u } = {}) => - visit(s, o, { - keyMap: i, - nodeTypeGetter: visitor_getNodeType, - nodePredicate: Zu, - nodeCloneFn: visitor_cloneNode, - ...u - }); - visitor_visit[Symbol.for('nodejs.util.promisify.custom')] = async ( - s, - o, - { keyMap: i = Qu, ...u } = {} - ) => - visit[Symbol.for('nodejs.util.promisify.custom')](s, o, { - keyMap: i, - nodeTypeGetter: visitor_getNodeType, - nodePredicate: Zu, - nodeCloneFn: visitor_cloneNode, - ...u - }); - const nodeTypeGetter = (s) => - 'string' == typeof (null == s ? void 0 : s.type) ? s.type : visitor_getNodeType(s), - ep = { EphemeralObject: ['content'], EphemeralArray: ['content'], ...Qu }, - value_visitor_visit = (s, o, { keyMap: i = ep, ...u } = {}) => - visitor_visit(s, o, { - keyMap: i, - nodeTypeGetter, - nodePredicate: es_T, - detectCycles: !1, - deleteNodeSymbol: Symbol.for('delete-node'), - skipVisitingNodeSymbol: Symbol.for('skip-visiting-node'), - ...u - }); - value_visitor_visit[Symbol.for('nodejs.util.promisify.custom')] = async ( - s, - { keyMap: o = ep, ...i } = {} - ) => - visitor_visit[Symbol.for('nodejs.util.promisify.custom')](s, visitor, { - keyMap: o, - nodeTypeGetter, - nodePredicate: es_T, - detectCycles: !1, - deleteNodeSymbol: Symbol.for('delete-node'), - skipVisitingNodeSymbol: Symbol.for('skip-visiting-node'), - ...i - }); - const tp = class EphemeralArray { - type = 'EphemeralArray'; - content = []; - reference = void 0; - constructor(s) { - (this.content = s), (this.reference = []); - } - toReference() { - return this.reference; - } - toArray() { - return this.reference.push(...this.content), this.reference; - } - }; - const rp = class EphemeralObject { - type = 'EphemeralObject'; - content = []; - reference = void 0; - constructor(s) { - (this.content = s), (this.reference = {}); - } - toReference() { - return this.reference; - } - toObject() { - return Object.assign(this.reference, Object.fromEntries(this.content)); - } - }; - class Visitor { - ObjectElement = { - enter: (s) => { - if (this.references.has(s)) return this.references.get(s).toReference(); - const o = new rp(s.content); - return this.references.set(s, o), o; - } - }; - EphemeralObject = { leave: (s) => s.toObject() }; - MemberElement = { enter: (s) => [s.key, s.value] }; - ArrayElement = { - enter: (s) => { - if (this.references.has(s)) return this.references.get(s).toReference(); - const o = new tp(s.content); - return this.references.set(s, o), o; - } - }; - EphemeralArray = { leave: (s) => s.toArray() }; - references = new WeakMap(); - BooleanElement(s) { - return s.toValue(); - } - NumberElement(s) { - return s.toValue(); - } - StringElement(s) { - return s.toValue(); - } - NullElement() { - return null; - } - RefElement(s, ...o) { - var i; - const u = o[3]; - return 'EphemeralObject' === - (null === (i = u[u.length - 1]) || void 0 === i ? void 0 : i.type) - ? Symbol.for('delete-node') - : String(s.toValue()); - } - LinkElement(s) { - return Ru(s.href) ? s.href.toValue() : ''; - } - } - const serializers_value = (s) => - Nu(s) - ? Ru(s) || Du(s) || Bu(s) || Lu(s) - ? s.toValue() - : value_visitor_visit(s, new Visitor()) - : s; - var np = _curry3(function mergeWithKey(s, o, i) { - var u, - _ = {}; - for (u in ((i = i || {}), (o = o || {}))) - _has(u, o) && (_[u] = _has(u, i) ? s(u, o[u], i[u]) : o[u]); - for (u in i) _has(u, i) && !_has(u, _) && (_[u] = i[u]); - return _; - }); - const sp = np; - var op = _curry3(function mergeDeepWithKey(s, o, i) { - return sp( - function (o, i, u) { - return _isObject(i) && _isObject(u) ? mergeDeepWithKey(s, i, u) : s(o, i, u); - }, - o, - i - ); - }); - const ip = op; - const lp = _curry2(function mergeDeepRight(s, o) { - return ip( - function (s, o, i) { - return i; - }, - s, - o - ); - }); - const cp = _curry2(_path); - const up = ja(0, -1); - var pp = _curry2(function apply(s, o) { - return s.apply(this, o); - }); - const hp = pp; - const dp = Ml(Wl); - var fp = _curry1(function empty(s) { - return null != s && 'function' == typeof s['fantasy-land/empty'] - ? s['fantasy-land/empty']() - : null != s && - null != s.constructor && - 'function' == typeof s.constructor['fantasy-land/empty'] - ? s.constructor['fantasy-land/empty']() - : null != s && 'function' == typeof s.empty - ? s.empty() - : null != s && null != s.constructor && 'function' == typeof s.constructor.empty - ? s.constructor.empty() - : aa(s) - ? [] - : _isString(s) - ? '' - : _isObject(s) - ? {} - : _i(s) - ? (function () { - return arguments; - })() - : (function _isTypedArray(s) { - var o = Object.prototype.toString.call(s); - return ( - '[object Uint8ClampedArray]' === o || - '[object Int8Array]' === o || - '[object Uint8Array]' === o || - '[object Int16Array]' === o || - '[object Uint16Array]' === o || - '[object Int32Array]' === o || - '[object Uint32Array]' === o || - '[object Float32Array]' === o || - '[object Float64Array]' === o || - '[object BigInt64Array]' === o || - '[object BigUint64Array]' === o - ); - })(s) - ? s.constructor.from('') - : void 0; - }); - const mp = fp; - const gp = _curry1(function isEmpty(s) { - return null != s && ra(s, mp(s)); - }); - const yp = za(1, Wl(Array.isArray) ? Array.isArray : pipe(ea, Vl('Array'))); - const vp = gu(yp, gp); - var bp = za(3, function (s, o, i) { - var u = cp(s, i), - _ = cp(up(s), i); - if (!dp(u) && !vp(s)) { - var w = Ea(u, _); - return hp(w, o); - } - }); - const _p = bp; - class Namespace extends Cu.g$ { - constructor() { - super(), - this.register('annotation', Iu), - this.register('comment', Pu), - this.register('parseResult', Mu), - this.register('sourceMap', Tu); - } - } - const Ep = new Namespace(), - createNamespace = (s) => { - const o = new Namespace(); - return ku(s) && o.use(s), o; - }, - wp = Ep, - toolbox = () => ({ predicates: { ...le }, namespace: wp }), - Sp = { - toolboxCreator: toolbox, - visitorOptions: { nodeTypeGetter: visitor_getNodeType, exposeEdits: !0 } - }, - dispatchPluginsSync = (s, o, i = {}) => { - if (0 === o.length) return s; - const u = lp(Sp, i), - { toolboxCreator: _, visitorOptions: w } = u, - x = _(), - C = o.map((s) => s(x)), - j = mergeAll(C.map(La({}, 'visitor')), { ...w }); - C.forEach(_p(['pre'], [])); - const L = visitor_visit(s, j, w); - return C.forEach(_p(['post'], [])), L; - }; - dispatchPluginsSync[Symbol.for('nodejs.util.promisify.custom')] = async (s, o, i = {}) => { - if (0 === o.length) return s; - const u = lp(Sp, i), - { toolboxCreator: _, visitorOptions: w } = u, - x = _(), - C = o.map((s) => s(x)), - j = mergeAll[Symbol.for('nodejs.util.promisify.custom')], - L = visitor_visit[Symbol.for('nodejs.util.promisify.custom')], - B = j(C.map(La({}, 'visitor')), { ...w }); - await Promise.allSettled(C.map(_p(['pre'], []))); - const $ = await L(s, B, w); - return await Promise.allSettled(C.map(_p(['post'], []))), $; - }; - const refract = (s, { Type: o, plugins: i = [] }) => { - const u = new o(s); - return ( - Nu(s) && - (s.meta.length > 0 && (u.meta = cloneDeep(s.meta)), - s.attributes.length > 0 && (u.attributes = cloneDeep(s.attributes))), - dispatchPluginsSync(u, i, { - toolboxCreator: toolbox, - visitorOptions: { nodeTypeGetter: visitor_getNodeType } - }) - ); - }, - createRefractor = - (s) => - (o, i = {}) => - refract(o, { ...i, Type: s }); - (Cu.Sh.refract = createRefractor(Cu.Sh)), - (Cu.wE.refract = createRefractor(Cu.wE)), - (Cu.Om.refract = createRefractor(Cu.Om)), - (Cu.bd.refract = createRefractor(Cu.bd)), - (Cu.Os.refract = createRefractor(Cu.Os)), - (Cu.kT.refract = createRefractor(Cu.kT)), - (Cu.Ft.refract = createRefractor(Cu.Ft)), - (Cu.sI.refract = createRefractor(Cu.sI)), - (Iu.refract = createRefractor(Iu)), - (Pu.refract = createRefractor(Pu)), - (Mu.refract = createRefractor(Mu)), - (Tu.refract = createRefractor(Tu)); - const computeEdges = (s, o = new WeakMap()) => ( - $u(s) - ? (o.set(s.key, s), computeEdges(s.key, o), o.set(s.value, s), computeEdges(s.value, o)) - : s.children.forEach((i) => { - o.set(i, s), computeEdges(i, o); - }), - o - ); - const xp = class Transcluder_Transcluder { - element; - edges; - constructor({ element: s }) { - this.element = s; - } - transclude(s, o) { - var i; - if (s === this.element) return o; - if (s === o) return this.element; - this.edges = - null !== (i = this.edges) && void 0 !== i ? i : computeEdges(this.element); - const u = this.edges.get(s); - return Rl(u) - ? void 0 - : (Fu(u) - ? ((s, o, i) => { - const u = i.get(s); - Fu(u) && - (u.content = u.map((_, w, x) => - x === s ? (i.delete(s), i.set(o, u), o) : x - )); - })(s, o, this.edges) - : qu(u) - ? ((s, o, i) => { - const u = i.get(s); - qu(u) && - (u.content = u.map((_) => - _ === s ? (i.delete(s), i.set(o, u), o) : _ - )); - })(s, o, this.edges) - : $u(u) && - ((s, o, i) => { - const u = i.get(s); - $u(u) && - (u.key === s && ((u.key = o), i.delete(s), i.set(o, u)), - u.value === s && ((u.value = o), i.delete(s), i.set(o, u))); - })(s, o, this.edges), - this.element); - } - }, - kp = pipe(Hl(/~/g, '~0'), Hl(/\//g, '~1'), encodeURIComponent); - const Cp = class JsonPointerError extends Jo {}; - const Op = class CompilationJsonPointerError extends Cp { - tokens; - constructor(s, o) { - super(s, o), void 0 !== o && (this.tokens = [...o.tokens]); - } - }, - es_compile = (s) => { - try { - return 0 === s.length ? '' : `/${s.map(kp).join('/')}`; - } catch (o) { - throw new Op('JSON Pointer compilation of tokens encountered an error.', { - tokens: s, - cause: o - }); - } - }; - var Ap = _curry2(function converge(s, o) { - return za(Ca(Ll, 0, Fl('length', o)), function () { - var i = arguments, - u = this; - return s.apply( - u, - _map(function (s) { - return s.apply(u, i); - }, o) - ); - }); - }); - const jp = Ap; - function _identity(s) { - return s; - } - const Ip = _curry1(_identity); - var Pp = gu(za(1, pipe(ea, Vl('Number'))), isFinite); - var Mp = za(1, Pp); - var Tp = gu( - Wl(Number.isFinite) ? za(1, Ea(Number.isFinite, Number)) : Mp, - jp(ra, [Math.floor, Ip]) - ); - var Np = za(1, Tp); - const Rp = Wl(Number.isInteger) ? za(1, Ea(Number.isInteger, Number)) : Np; - var Dp = (function () { - function XTake(s, o) { - (this.xf = o), (this.n = s), (this.i = 0); - } - return ( - (XTake.prototype['@@transducer/init'] = _xfBase_init), - (XTake.prototype['@@transducer/result'] = _xfBase_result), - (XTake.prototype['@@transducer/step'] = function (s, o) { - this.i += 1; - var i = 0 === this.n ? s : this.xf['@@transducer/step'](s, o); - return this.n >= 0 && this.i >= this.n ? _reduced(i) : i; - }), - XTake - ); - })(); - function _xtake(s) { - return function (o) { - return new Dp(s, o); - }; - } - const Lp = _curry2( - _dispatchable(['take'], _xtake, function take(s, o) { - return ja(0, s < 0 ? 1 / 0 : s, o); - }) - ); - var Bp = _curry2(function (s, o) { - return ra(Lp(s.length, o), s); - }); - const Fp = Bp; - const qp = ra(''); - var $p = (function () { - function XDropWhile(s, o) { - (this.xf = o), (this.f = s); - } - return ( - (XDropWhile.prototype['@@transducer/init'] = _xfBase_init), - (XDropWhile.prototype['@@transducer/result'] = _xfBase_result), - (XDropWhile.prototype['@@transducer/step'] = function (s, o) { - if (this.f) { - if (this.f(o)) return s; - this.f = null; - } - return this.xf['@@transducer/step'](s, o); - }), - XDropWhile - ); - })(); - function _xdropWhile(s) { - return function (o) { - return new $p(s, o); - }; - } - const Vp = _curry2( - _dispatchable(['dropWhile'], _xdropWhile, function dropWhile(s, o) { - for (var i = 0, u = o.length; i < u && s(o[i]); ) i += 1; - return ja(i, 1 / 0, o); - }) - ); - const Up = Ja(function (s, o) { - return pipe(tl(''), Vp(_l(s)), yl(''))(o); - }), - zp = pipe(Hl(/~1/g, '/'), Hl(/~0/g, '~'), (s) => { - try { - return decodeURIComponent(s); - } catch { - return s; - } - }); - const Wp = class InvalidJsonPointerError extends Cp { - pointer; - constructor(s, o) { - super(s, o), void 0 !== o && (this.pointer = o.pointer); - } - }, - uriToPointer = (s) => { - const o = ((s) => { - const o = s.indexOf('#'); - return -1 !== o ? s.substring(o) : '#'; - })(s); - return Up('#', o); - }, - es_parse = (s) => { - if (qp(s)) return []; - if (!Fp('/', s)) - throw new Wp(`Invalid JSON Pointer "${s}". JSON Pointers must begin with "/"`, { - pointer: s - }); - try { - const o = pipe(tl('/'), kl(zp))(s); - return Ia(o); - } catch (o) { - throw new Wp(`JSON Pointer parsing of "${s}" encountered an error.`, { - pointer: s, - cause: o - }); - } - }; - const Kp = class EvaluationJsonPointerError extends Cp { - pointer; - tokens; - failedToken; - failedTokenPosition; - element; - constructor(s, o) { - super(s, o), - void 0 !== o && - ((this.pointer = o.pointer), - Array.isArray(o.tokens) && (this.tokens = [...o.tokens]), - (this.failedToken = o.failedToken), - (this.failedTokenPosition = o.failedTokenPosition), - (this.element = o.element)); - } - }, - es_evaluate = (s, o) => { - let i; - try { - i = es_parse(s); - } catch (i) { - throw new Kp(`JSON Pointer evaluation failed while parsing the pointer "${s}".`, { - pointer: s, - element: cloneDeep(o), - cause: i - }); - } - return i.reduce((o, u, _) => { - if (Fu(o)) { - if (!o.hasKey(u)) - throw new Kp( - `JSON Pointer evaluation failed while evaluating token "${u}" against an ObjectElement`, - { - pointer: s, - tokens: i, - failedToken: u, - failedTokenPosition: _, - element: cloneDeep(o) - } - ); - return o.get(u); - } - if (qu(o)) { - if (!(u in o.content) || !Rp(Number(u))) - throw new Kp( - `JSON Pointer evaluation failed while evaluating token "${u}" against an ArrayElement`, - { - pointer: s, - tokens: i, - failedToken: u, - failedTokenPosition: _, - element: cloneDeep(o) - } - ); - return o.get(Number(u)); - } - throw new Kp( - `JSON Pointer evaluation failed while evaluating token "${u}" against an unexpected Element`, - { - pointer: s, - tokens: i, - failedToken: u, - failedTokenPosition: _, - element: cloneDeep(o) - } - ); - }, o); - }; - class Callback extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'callback'); - } - } - const Hp = Callback; - class Components extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'components'); - } - get schemas() { - return this.get('schemas'); - } - set schemas(s) { - this.set('schemas', s); - } - get responses() { - return this.get('responses'); - } - set responses(s) { - this.set('responses', s); - } - get parameters() { - return this.get('parameters'); - } - set parameters(s) { - this.set('parameters', s); - } - get examples() { - return this.get('examples'); - } - set examples(s) { - this.set('examples', s); - } - get requestBodies() { - return this.get('requestBodies'); - } - set requestBodies(s) { - this.set('requestBodies', s); - } - get headers() { - return this.get('headers'); - } - set headers(s) { - this.set('headers', s); - } - get securitySchemes() { - return this.get('securitySchemes'); - } - set securitySchemes(s) { - this.set('securitySchemes', s); - } - get links() { - return this.get('links'); - } - set links(s) { - this.set('links', s); - } - get callbacks() { - return this.get('callbacks'); - } - set callbacks(s) { - this.set('callbacks', s); - } - } - const Jp = Components; - class Contact extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'contact'); - } - get name() { - return this.get('name'); - } - set name(s) { - this.set('name', s); - } - get url() { - return this.get('url'); - } - set url(s) { - this.set('url', s); - } - get email() { - return this.get('email'); - } - set email(s) { - this.set('email', s); - } - } - const Gp = Contact; - class Discriminator extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'discriminator'); - } - get propertyName() { - return this.get('propertyName'); - } - set propertyName(s) { - this.set('propertyName', s); - } - get mapping() { - return this.get('mapping'); - } - set mapping(s) { - this.set('mapping', s); - } - } - const Yp = Discriminator; - class Encoding extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'encoding'); - } - get contentType() { - return this.get('contentType'); - } - set contentType(s) { - this.set('contentType', s); - } - get headers() { - return this.get('headers'); - } - set headers(s) { - this.set('headers', s); - } - get style() { - return this.get('style'); - } - set style(s) { - this.set('style', s); - } - get explode() { - return this.get('explode'); - } - set explode(s) { - this.set('explode', s); - } - get allowedReserved() { - return this.get('allowedReserved'); - } - set allowedReserved(s) { - this.set('allowedReserved', s); - } - } - const Xp = Encoding; - class Example extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'example'); - } - get summary() { - return this.get('summary'); - } - set summary(s) { - this.set('summary', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get value() { - return this.get('value'); - } - set value(s) { - this.set('value', s); - } - get externalValue() { - return this.get('externalValue'); - } - set externalValue(s) { - this.set('externalValue', s); - } - } - const Zp = Example; - class ExternalDocumentation extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'externalDocumentation'); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get url() { - return this.get('url'); - } - set url(s) { - this.set('url', s); - } - } - const Qp = ExternalDocumentation; - class Header extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'header'); - } - get required() { - return this.hasKey('required') ? this.get('required') : new Cu.bd(!1); - } - set required(s) { - this.set('required', s); - } - get deprecated() { - return this.hasKey('deprecated') ? this.get('deprecated') : new Cu.bd(!1); - } - set deprecated(s) { - this.set('deprecated', s); - } - get allowEmptyValue() { - return this.get('allowEmptyValue'); - } - set allowEmptyValue(s) { - this.set('allowEmptyValue', s); - } - get style() { - return this.get('style'); - } - set style(s) { - this.set('style', s); - } - get explode() { - return this.get('explode'); - } - set explode(s) { - this.set('explode', s); - } - get allowReserved() { - return this.get('allowReserved'); - } - set allowReserved(s) { - this.set('allowReserved', s); - } - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - get example() { - return this.get('example'); - } - set example(s) { - this.set('example', s); - } - get examples() { - return this.get('examples'); - } - set examples(s) { - this.set('examples', s); - } - get contentProp() { - return this.get('content'); - } - set contentProp(s) { - this.set('content', s); - } - } - Object.defineProperty(Header.prototype, 'description', { - get() { - return this.get('description'); - }, - set(s) { - this.set('description', s); - }, - enumerable: !0 - }); - const th = Header; - class Info extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'info'), this.classes.push('info'); - } - get title() { - return this.get('title'); - } - set title(s) { - this.set('title', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get termsOfService() { - return this.get('termsOfService'); - } - set termsOfService(s) { - this.set('termsOfService', s); - } - get contact() { - return this.get('contact'); - } - set contact(s) { - this.set('contact', s); - } - get license() { - return this.get('license'); - } - set license(s) { - this.set('license', s); - } - get version() { - return this.get('version'); - } - set version(s) { - this.set('version', s); - } - } - const rh = Info; - class License extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'license'); - } - get name() { - return this.get('name'); - } - set name(s) { - this.set('name', s); - } - get url() { - return this.get('url'); - } - set url(s) { - this.set('url', s); - } - } - const uh = License; - class Link extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'link'); - } - get operationRef() { - return this.get('operationRef'); - } - set operationRef(s) { - this.set('operationRef', s); - } - get operationId() { - return this.get('operationId'); - } - set operationId(s) { - this.set('operationId', s); - } - get operation() { - var s, o; - return Ru(this.operationRef) - ? null === (s = this.operationRef) || void 0 === s - ? void 0 - : s.meta.get('operation') - : Ru(this.operationId) - ? null === (o = this.operationId) || void 0 === o - ? void 0 - : o.meta.get('operation') - : void 0; - } - set operation(s) { - this.set('operation', s); - } - get parameters() { - return this.get('parameters'); - } - set parameters(s) { - this.set('parameters', s); - } - get requestBody() { - return this.get('requestBody'); - } - set requestBody(s) { - this.set('requestBody', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get server() { - return this.get('server'); - } - set server(s) { - this.set('server', s); - } - } - const dh = Link; - class MediaType extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'mediaType'); - } - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - get example() { - return this.get('example'); - } - set example(s) { - this.set('example', s); - } - get examples() { - return this.get('examples'); - } - set examples(s) { - this.set('examples', s); - } - get encoding() { - return this.get('encoding'); - } - set encoding(s) { - this.set('encoding', s); - } - } - const fh = MediaType; - class OAuthFlow extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'oAuthFlow'); - } - get authorizationUrl() { - return this.get('authorizationUrl'); - } - set authorizationUrl(s) { - this.set('authorizationUrl', s); - } - get tokenUrl() { - return this.get('tokenUrl'); - } - set tokenUrl(s) { - this.set('tokenUrl', s); - } - get refreshUrl() { - return this.get('refreshUrl'); - } - set refreshUrl(s) { - this.set('refreshUrl', s); - } - get scopes() { - return this.get('scopes'); - } - set scopes(s) { - this.set('scopes', s); - } - } - const vh = OAuthFlow; - class OAuthFlows extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'oAuthFlows'); - } - get implicit() { - return this.get('implicit'); - } - set implicit(s) { - this.set('implicit', s); - } - get password() { - return this.get('password'); - } - set password(s) { - this.set('password', s); - } - get clientCredentials() { - return this.get('clientCredentials'); - } - set clientCredentials(s) { - this.set('clientCredentials', s); - } - get authorizationCode() { - return this.get('authorizationCode'); - } - set authorizationCode(s) { - this.set('authorizationCode', s); - } - } - const _h = OAuthFlows; - class Openapi extends Cu.Om { - constructor(s, o, i) { - super(s, o, i), - (this.element = 'openapi'), - this.classes.push('spec-version'), - this.classes.push('version'); - } - } - const wh = Openapi; - class OpenApi3_0 extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'openApi3_0'), this.classes.push('api'); - } - get openapi() { - return this.get('openapi'); - } - set openapi(s) { - this.set('openapi', s); - } - get info() { - return this.get('info'); - } - set info(s) { - this.set('info', s); - } - get servers() { - return this.get('servers'); - } - set servers(s) { - this.set('servers', s); - } - get paths() { - return this.get('paths'); - } - set paths(s) { - this.set('paths', s); - } - get components() { - return this.get('components'); - } - set components(s) { - this.set('components', s); - } - get security() { - return this.get('security'); - } - set security(s) { - this.set('security', s); - } - get tags() { - return this.get('tags'); - } - set tags(s) { - this.set('tags', s); - } - get externalDocs() { - return this.get('externalDocs'); - } - set externalDocs(s) { - this.set('externalDocs', s); - } - } - const Oh = OpenApi3_0; - class Operation extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'operation'); - } - get tags() { - return this.get('tags'); - } - set tags(s) { - this.set('tags', s); - } - get summary() { - return this.get('summary'); - } - set summary(s) { - this.set('summary', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - set externalDocs(s) { - this.set('externalDocs', s); - } - get externalDocs() { - return this.get('externalDocs'); - } - get operationId() { - return this.get('operationId'); - } - set operationId(s) { - this.set('operationId', s); - } - get parameters() { - return this.get('parameters'); - } - set parameters(s) { - this.set('parameters', s); - } - get requestBody() { - return this.get('requestBody'); - } - set requestBody(s) { - this.set('requestBody', s); - } - get responses() { - return this.get('responses'); - } - set responses(s) { - this.set('responses', s); - } - get callbacks() { - return this.get('callbacks'); - } - set callbacks(s) { - this.set('callbacks', s); - } - get deprecated() { - return this.hasKey('deprecated') ? this.get('deprecated') : new Cu.bd(!1); - } - set deprecated(s) { - this.set('deprecated', s); - } - get security() { - return this.get('security'); - } - set security(s) { - this.set('security', s); - } - get servers() { - return this.get('severs'); - } - set servers(s) { - this.set('servers', s); - } - } - const jh = Operation; - class Parameter extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'parameter'); - } - get name() { - return this.get('name'); - } - set name(s) { - this.set('name', s); - } - get in() { - return this.get('in'); - } - set in(s) { - this.set('in', s); - } - get required() { - return this.hasKey('required') ? this.get('required') : new Cu.bd(!1); - } - set required(s) { - this.set('required', s); - } - get deprecated() { - return this.hasKey('deprecated') ? this.get('deprecated') : new Cu.bd(!1); - } - set deprecated(s) { - this.set('deprecated', s); - } - get allowEmptyValue() { - return this.get('allowEmptyValue'); - } - set allowEmptyValue(s) { - this.set('allowEmptyValue', s); - } - get style() { - return this.get('style'); - } - set style(s) { - this.set('style', s); - } - get explode() { - return this.get('explode'); - } - set explode(s) { - this.set('explode', s); - } - get allowReserved() { - return this.get('allowReserved'); - } - set allowReserved(s) { - this.set('allowReserved', s); - } - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - get example() { - return this.get('example'); - } - set example(s) { - this.set('example', s); - } - get examples() { - return this.get('examples'); - } - set examples(s) { - this.set('examples', s); - } - get contentProp() { - return this.get('content'); - } - set contentProp(s) { - this.set('content', s); - } - } - Object.defineProperty(Parameter.prototype, 'description', { - get() { - return this.get('description'); - }, - set(s) { - this.set('description', s); - }, - enumerable: !0 - }); - const Ih = Parameter; - class PathItem extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'pathItem'); - } - get $ref() { - return this.get('$ref'); - } - set $ref(s) { - this.set('$ref', s); - } - get summary() { - return this.get('summary'); - } - set summary(s) { - this.set('summary', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get GET() { - return this.get('get'); - } - set GET(s) { - this.set('GET', s); - } - get PUT() { - return this.get('put'); - } - set PUT(s) { - this.set('PUT', s); - } - get POST() { - return this.get('post'); - } - set POST(s) { - this.set('POST', s); - } - get DELETE() { - return this.get('delete'); - } - set DELETE(s) { - this.set('DELETE', s); - } - get OPTIONS() { - return this.get('options'); - } - set OPTIONS(s) { - this.set('OPTIONS', s); - } - get HEAD() { - return this.get('head'); - } - set HEAD(s) { - this.set('HEAD', s); - } - get PATCH() { - return this.get('patch'); - } - set PATCH(s) { - this.set('PATCH', s); - } - get TRACE() { - return this.get('trace'); - } - set TRACE(s) { - this.set('TRACE', s); - } - get servers() { - return this.get('servers'); - } - set servers(s) { - this.set('servers', s); - } - get parameters() { - return this.get('parameters'); - } - set parameters(s) { - this.set('parameters', s); - } - } - const Ph = PathItem; - class Paths extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'paths'); - } - } - const Rh = Paths; - class Reference extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'reference'), this.classes.push('openapi-reference'); - } - get $ref() { - return this.get('$ref'); - } - set $ref(s) { - this.set('$ref', s); - } - } - const Dh = Reference; - class RequestBody extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'requestBody'); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get contentProp() { - return this.get('content'); - } - set contentProp(s) { - this.set('content', s); - } - get required() { - return this.hasKey('required') ? this.get('required') : new Cu.bd(!1); - } - set required(s) { - this.set('required', s); - } - } - const Lh = RequestBody; - class Response_Response extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'response'); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get headers() { - return this.get('headers'); - } - set headers(s) { - this.set('headers', s); - } - get contentProp() { - return this.get('content'); - } - set contentProp(s) { - this.set('content', s); - } - get links() { - return this.get('links'); - } - set links(s) { - this.set('links', s); - } - } - const Fh = Response_Response; - class Responses extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'responses'); - } - get default() { - return this.get('default'); - } - set default(s) { - this.set('default', s); - } - } - const Kh = Responses; - const Hh = class UnsupportedOperationError extends Ho {}; - class JSONSchema extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'JSONSchemaDraft4'); - } - get idProp() { - return this.get('id'); - } - set idProp(s) { - this.set('id', s); - } - get $schema() { - return this.get('$schema'); - } - set $schema(s) { - this.set('$schema', s); - } - get multipleOf() { - return this.get('multipleOf'); - } - set multipleOf(s) { - this.set('multipleOf', s); - } - get maximum() { - return this.get('maximum'); - } - set maximum(s) { - this.set('maximum', s); - } - get exclusiveMaximum() { - return this.get('exclusiveMaximum'); - } - set exclusiveMaximum(s) { - this.set('exclusiveMaximum', s); - } - get minimum() { - return this.get('minimum'); - } - set minimum(s) { - this.set('minimum', s); - } - get exclusiveMinimum() { - return this.get('exclusiveMinimum'); - } - set exclusiveMinimum(s) { - this.set('exclusiveMinimum', s); - } - get maxLength() { - return this.get('maxLength'); - } - set maxLength(s) { - this.set('maxLength', s); - } - get minLength() { - return this.get('minLength'); - } - set minLength(s) { - this.set('minLength', s); - } - get pattern() { - return this.get('pattern'); - } - set pattern(s) { - this.set('pattern', s); - } - get additionalItems() { - return this.get('additionalItems'); - } - set additionalItems(s) { - this.set('additionalItems', s); - } - get items() { - return this.get('items'); - } - set items(s) { - this.set('items', s); - } - get maxItems() { - return this.get('maxItems'); - } - set maxItems(s) { - this.set('maxItems', s); - } - get minItems() { - return this.get('minItems'); - } - set minItems(s) { - this.set('minItems', s); - } - get uniqueItems() { - return this.get('uniqueItems'); - } - set uniqueItems(s) { - this.set('uniqueItems', s); - } - get maxProperties() { - return this.get('maxProperties'); - } - set maxProperties(s) { - this.set('maxProperties', s); - } - get minProperties() { - return this.get('minProperties'); - } - set minProperties(s) { - this.set('minProperties', s); - } - get required() { - return this.get('required'); - } - set required(s) { - this.set('required', s); - } - get properties() { - return this.get('properties'); - } - set properties(s) { - this.set('properties', s); - } - get additionalProperties() { - return this.get('additionalProperties'); - } - set additionalProperties(s) { - this.set('additionalProperties', s); - } - get patternProperties() { - return this.get('patternProperties'); - } - set patternProperties(s) { - this.set('patternProperties', s); - } - get dependencies() { - return this.get('dependencies'); - } - set dependencies(s) { - this.set('dependencies', s); - } - get enum() { - return this.get('enum'); - } - set enum(s) { - this.set('enum', s); - } - get type() { - return this.get('type'); - } - set type(s) { - this.set('type', s); - } - get allOf() { - return this.get('allOf'); - } - set allOf(s) { - this.set('allOf', s); - } - get anyOf() { - return this.get('anyOf'); - } - set anyOf(s) { - this.set('anyOf', s); - } - get oneOf() { - return this.get('oneOf'); - } - set oneOf(s) { - this.set('oneOf', s); - } - get not() { - return this.get('not'); - } - set not(s) { - this.set('not', s); - } - get definitions() { - return this.get('definitions'); - } - set definitions(s) { - this.set('definitions', s); - } - get title() { - return this.get('title'); - } - set title(s) { - this.set('title', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get default() { - return this.get('default'); - } - set default(s) { - this.set('default', s); - } - get format() { - return this.get('format'); - } - set format(s) { - this.set('format', s); - } - get base() { - return this.get('base'); - } - set base(s) { - this.set('base', s); - } - get links() { - return this.get('links'); - } - set links(s) { - this.set('links', s); - } - get media() { - return this.get('media'); - } - set media(s) { - this.set('media', s); - } - get readOnly() { - return this.get('readOnly'); - } - set readOnly(s) { - this.set('readOnly', s); - } - } - const Jh = JSONSchema; - class JSONReference extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'JSONReference'), this.classes.push('json-reference'); - } - get $ref() { - return this.get('$ref'); - } - set $ref(s) { - this.set('$ref', s); - } - } - const Gh = JSONReference; - class Media extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'media'); - } - get binaryEncoding() { - return this.get('binaryEncoding'); - } - set binaryEncoding(s) { - this.set('binaryEncoding', s); - } - get type() { - return this.get('type'); - } - set type(s) { - this.set('type', s); - } - } - const Qh = Media; - class LinkDescription extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'linkDescription'); - } - get href() { - return this.get('href'); - } - set href(s) { - this.set('href', s); - } - get rel() { - return this.get('rel'); - } - set rel(s) { - this.set('rel', s); - } - get title() { - return this.get('title'); - } - set title(s) { - this.set('title', s); - } - get targetSchema() { - return this.get('targetSchema'); - } - set targetSchema(s) { - this.set('targetSchema', s); - } - get mediaType() { - return this.get('mediaType'); - } - set mediaType(s) { - this.set('mediaType', s); - } - get method() { - return this.get('method'); - } - set method(s) { - this.set('method', s); - } - get encType() { - return this.get('encType'); - } - set encType(s) { - this.set('encType', s); - } - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - } - const td = LinkDescription; - var sd = _curry2(function mapObjIndexed(s, o) { - return _arrayReduce( - function (i, u) { - return (i[u] = s(o[u], u, o)), i; - }, - {}, - Wi(o) - ); - }); - const id = sd; - const ld = _curry1(function isNil(s) { - return null == s; - }); - var cd = _curry2(function hasPath(s, o) { - if (0 === s.length || ld(o)) return !1; - for (var i = o, u = 0; u < s.length; ) { - if (ld(i) || !_has(s[u], i)) return !1; - (i = i[s[u]]), (u += 1); - } - return !0; - }); - const ud = cd; - var dd = _curry2(function has(s, o) { - return ud([s], o); - }); - const md = dd; - const yd = _curry3(function propSatisfies(s, o, i) { - return s(Da(o, i)); - }), - dereference = (s, o) => { - const i = Na(s, o); - return id((s) => { - if (ku(s) && md('$ref', s) && yd(Yl, '$ref', s)) { - const o = cp(['$ref'], s), - u = Up('#/', o); - return cp(u.split('/'), i); - } - return ku(s) ? dereference(s, i) : s; - }, s); - }, - emptyElement = (s) => { - const o = s.meta.length > 0 ? cloneDeep(s.meta) : void 0, - i = s.attributes.length > 0 ? cloneDeep(s.attributes) : void 0; - return new s.constructor(void 0, o, i); - }, - cloneUnlessOtherwiseSpecified = (s, o) => - o.clone && o.isMergeableElement(s) ? deepmerge(emptyElement(s), s, o) : s, - getMetaMergeFunction = (s) => - 'function' != typeof s.customMetaMerge ? (s) => cloneDeep(s) : s.customMetaMerge, - getAttributesMergeFunction = (s) => - 'function' != typeof s.customAttributesMerge - ? (s) => cloneDeep(s) - : s.customAttributesMerge, - vd = { - clone: !0, - isMergeableElement: (s) => Fu(s) || qu(s), - arrayElementMerge: (s, o, i) => - s.concat(o)['fantasy-land/map']((s) => cloneUnlessOtherwiseSpecified(s, i)), - objectElementMerge: (s, o, i) => { - const u = Fu(s) ? emptyElement(s) : emptyElement(o); - return ( - Fu(s) && - s.forEach((s, o, _) => { - const w = cloneShallow(_); - (w.value = cloneUnlessOtherwiseSpecified(s, i)), u.content.push(w); - }), - o.forEach((o, _, w) => { - const x = serializers_value(_); - let C; - if (Fu(s) && s.hasKey(x) && i.isMergeableElement(o)) { - const u = s.get(x); - (C = cloneShallow(w)), - (C.value = ((s, o) => { - if ('function' != typeof o.customMerge) return deepmerge; - const i = o.customMerge(s, o); - return 'function' == typeof i ? i : deepmerge; - })(_, i)(u, o)); - } else (C = cloneShallow(w)), (C.value = cloneUnlessOtherwiseSpecified(o, i)); - u.remove(x), u.content.push(C); - }), - u - ); - }, - customMerge: void 0, - customMetaMerge: void 0, - customAttributesMerge: void 0 - }; - function deepmerge(s, o, i) { - var u, _, w; - const x = { ...vd, ...i }; - (x.isMergeableElement = - null !== (u = x.isMergeableElement) && void 0 !== u ? u : vd.isMergeableElement), - (x.arrayElementMerge = - null !== (_ = x.arrayElementMerge) && void 0 !== _ ? _ : vd.arrayElementMerge), - (x.objectElementMerge = - null !== (w = x.objectElementMerge) && void 0 !== w ? w : vd.objectElementMerge); - const C = qu(o); - if (!(C === qu(s))) return cloneUnlessOtherwiseSpecified(o, x); - const j = - C && 'function' == typeof x.arrayElementMerge - ? x.arrayElementMerge(s, o, x) - : x.objectElementMerge(s, o, x); - return ( - (j.meta = getMetaMergeFunction(x)(s.meta, o.meta)), - (j.attributes = getAttributesMergeFunction(x)(s.attributes, o.attributes)), - j - ); - } - deepmerge.all = (s, o) => { - if (!Array.isArray(s)) - throw new TypeError('First argument of deepmerge should be an array.'); - return 0 === s.length - ? new Cu.Sh() - : s.reduce((s, i) => deepmerge(s, i, o), emptyElement(s[0])); - }; - const _d = class Visitor_Visitor { - element; - constructor(s) { - Object.assign(this, s); - } - copyMetaAndAttributes(s, o) { - (s.meta.length > 0 || o.meta.length > 0) && - ((o.meta = deepmerge(o.meta, s.meta)), - hasElementSourceMap(s) && o.meta.set('sourceMap', s.meta.get('sourceMap'))), - (s.attributes.length > 0 || s.meta.length > 0) && - (o.attributes = deepmerge(o.attributes, s.attributes)); - } - }; - const Ed = class FallbackVisitor extends _d { - enter(s) { - return (this.element = cloneDeep(s)), Ju; - } - }, - copyProps = (s, o, i = []) => { - const u = Object.getOwnPropertyDescriptors(o); - for (let s of i) delete u[s]; - Object.defineProperties(s, u); - }, - protoChain = (s, o = [s]) => { - const i = Object.getPrototypeOf(s); - return null === i ? o : protoChain(i, [...o, i]); - }, - hardMixProtos = (s, o, i = []) => { - var u; - const _ = - null !== - (u = ((...s) => { - if (0 === s.length) return; - let o; - const i = s.map((s) => protoChain(s)); - for (; i.every((s) => s.length > 0); ) { - const s = i.map((s) => s.pop()), - u = s[0]; - if (!s.every((s) => s === u)) break; - o = u; - } - return o; - })(...s)) && void 0 !== u - ? u - : Object.prototype, - w = Object.create(_), - x = protoChain(_); - for (let o of s) { - let s = protoChain(o); - for (let o = s.length - 1; o >= 0; o--) { - let u = s[o]; - -1 === x.indexOf(u) && (copyProps(w, u, ['constructor', ...i]), x.push(u)); - } - } - return (w.constructor = o), w; - }, - unique = (s) => s.filter((o, i) => s.indexOf(o) == i), - getIngredientWithProp = (s, o) => { - const i = o.map((s) => protoChain(s)); - let u = 0, - _ = !0; - for (; _; ) { - _ = !1; - for (let w = o.length - 1; w >= 0; w--) { - const o = i[w][u]; - if (null != o && ((_ = !0), null != Object.getOwnPropertyDescriptor(o, s))) - return i[w][0]; - } - u++; - } - }, - proxyMix = (s, o = Object.prototype) => - new Proxy( - {}, - { - getPrototypeOf: () => o, - setPrototypeOf() { - throw Error('Cannot set prototype of Proxies created by ts-mixer'); - }, - getOwnPropertyDescriptor: (o, i) => - Object.getOwnPropertyDescriptor(getIngredientWithProp(i, s) || {}, i), - defineProperty() { - throw new Error('Cannot define new properties on Proxies created by ts-mixer'); - }, - has: (i, u) => void 0 !== getIngredientWithProp(u, s) || void 0 !== o[u], - get: (i, u) => (getIngredientWithProp(u, s) || o)[u], - set(o, i, u) { - const _ = getIngredientWithProp(i, s); - if (void 0 === _) - throw new Error('Cannot set new properties on Proxies created by ts-mixer'); - return (_[i] = u), !0; - }, - deleteProperty() { - throw new Error('Cannot delete properties on Proxies created by ts-mixer'); - }, - ownKeys: () => - s - .map(Object.getOwnPropertyNames) - .reduce((s, o) => o.concat(s.filter((s) => o.indexOf(s) < 0))) - } - ), - wd = null, - Sd = 'copy', - xd = 'copy', - kd = 'deep', - Cd = new WeakMap(), - getMixinsForClass = (s) => Cd.get(s), - mergeObjectsOfDecorators = (s, o) => { - var i, u; - const _ = unique([...Object.getOwnPropertyNames(s), ...Object.getOwnPropertyNames(o)]), - w = {}; - for (let x of _) - w[x] = unique([ - ...(null !== (i = null == s ? void 0 : s[x]) && void 0 !== i ? i : []), - ...(null !== (u = null == o ? void 0 : o[x]) && void 0 !== u ? u : []) - ]); - return w; - }, - mergePropertyAndMethodDecorators = (s, o) => { - var i, u, _, w; - return { - property: mergeObjectsOfDecorators( - null !== (i = null == s ? void 0 : s.property) && void 0 !== i ? i : {}, - null !== (u = null == o ? void 0 : o.property) && void 0 !== u ? u : {} - ), - method: mergeObjectsOfDecorators( - null !== (_ = null == s ? void 0 : s.method) && void 0 !== _ ? _ : {}, - null !== (w = null == o ? void 0 : o.method) && void 0 !== w ? w : {} - ) - }; - }, - mergeDecorators = (s, o) => { - var i, u, _, w, x, C; - return { - class: unique([ - ...(null !== (i = null == s ? void 0 : s.class) && void 0 !== i ? i : []), - ...(null !== (u = null == o ? void 0 : o.class) && void 0 !== u ? u : []) - ]), - static: mergePropertyAndMethodDecorators( - null !== (_ = null == s ? void 0 : s.static) && void 0 !== _ ? _ : {}, - null !== (w = null == o ? void 0 : o.static) && void 0 !== w ? w : {} - ), - instance: mergePropertyAndMethodDecorators( - null !== (x = null == s ? void 0 : s.instance) && void 0 !== x ? x : {}, - null !== (C = null == o ? void 0 : o.instance) && void 0 !== C ? C : {} - ) - }; - }, - Od = new Map(), - deepDecoratorSearch = (...s) => { - const o = ((...s) => { - var o; - const i = new Set(), - u = new Set([...s]); - for (; u.size > 0; ) - for (let s of u) { - const _ = protoChain(s.prototype).map((s) => s.constructor), - w = [ - ..._, - ...(null !== (o = getMixinsForClass(s)) && void 0 !== o ? o : []) - ].filter((s) => !i.has(s)); - for (let s of w) u.add(s); - i.add(s), u.delete(s); - } - return [...i]; - })(...s) - .map((s) => Od.get(s)) - .filter((s) => !!s); - return 0 == o.length - ? {} - : 1 == o.length - ? o[0] - : o.reduce((s, o) => mergeDecorators(s, o)); - }, - getDecoratorsForClass = (s) => { - let o = Od.get(s); - return o || ((o = {}), Od.set(s, o)), o; - }; - function Mixin(...s) { - var o, i, u; - const _ = s.map((s) => s.prototype), - w = wd; - if (null !== w) { - const s = _.map((s) => s[w]).filter((s) => 'function' == typeof s), - combinedInitFunction = function (...o) { - for (let i of s) i.apply(this, o); - }, - o = { [w]: combinedInitFunction }; - _.push(o); - } - function MixedClass(...o) { - for (const i of s) copyProps(this, new i(...o)); - null !== w && 'function' == typeof this[w] && this[w].apply(this, o); - } - var x, C; - (MixedClass.prototype = - 'copy' === xd - ? hardMixProtos(_, MixedClass) - : ((x = _), (C = MixedClass), proxyMix([...x, { constructor: C }]))), - Object.setPrototypeOf( - MixedClass, - 'copy' === Sd - ? hardMixProtos(s, null, ['prototype']) - : proxyMix(s, Function.prototype) - ); - let j = MixedClass; - if ('none' !== kd) { - const _ = - 'deep' === kd - ? deepDecoratorSearch(...s) - : ((...s) => { - const o = s.map((s) => getDecoratorsForClass(s)); - return 0 === o.length - ? {} - : 1 === o.length - ? o[0] - : o.reduce((s, o) => mergeDecorators(s, o)); - })(...s); - for (let s of null !== (o = null == _ ? void 0 : _.class) && void 0 !== o ? o : []) { - const o = s(j); - o && (j = o); - } - applyPropAndMethodDecorators( - null !== (i = null == _ ? void 0 : _.static) && void 0 !== i ? i : {}, - j - ), - applyPropAndMethodDecorators( - null !== (u = null == _ ? void 0 : _.instance) && void 0 !== u ? u : {}, - j.prototype - ); - } - var L, B; - return (L = j), (B = s), Cd.set(L, B), j; - } - const applyPropAndMethodDecorators = (s, o) => { - const i = s.property, - u = s.method; - if (i) for (let s in i) for (let u of i[s]) u(o, s); - if (u) - for (let s in u) for (let i of u[s]) i(o, s, Object.getOwnPropertyDescriptor(o, s)); - }; - const Ad = _curry2(function pick(s, o) { - for (var i = {}, u = 0; u < s.length; ) s[u] in o && (i[s[u]] = o[s[u]]), (u += 1); - return i; - }); - const Id = class SpecificationVisitor extends _d { - specObj; - passingOptionsNames = ['specObj']; - constructor({ specObj: s, ...o }) { - super({ ...o }), (this.specObj = s); - } - retrievePassingOptions() { - return Ad(this.passingOptionsNames, this); - } - retrieveFixedFields(s) { - const o = cp(['visitors', ...s, 'fixedFields'], this.specObj); - return 'object' == typeof o && null !== o ? Object.keys(o) : []; - } - retrieveVisitor(s) { - return Xo(Wl, ['visitors', ...s], this.specObj) - ? cp(['visitors', ...s], this.specObj) - : cp(['visitors', ...s, '$visitor'], this.specObj); - } - retrieveVisitorInstance(s, o = {}) { - const i = this.retrievePassingOptions(); - return new (this.retrieveVisitor(s))({ ...i, ...o }); - } - toRefractedElement(s, o, i = {}) { - const u = this.retrieveVisitorInstance(s, i); - return u instanceof Ed && (null == u ? void 0 : u.constructor) === Ed - ? cloneDeep(o) - : (visitor_visit(o, u, i), u.element); - } - }; - const Md = class FixedFieldsVisitor extends Id { - specPath; - ignoredFields; - constructor({ specPath: s, ignoredFields: o, ...i }) { - super({ ...i }), (this.specPath = s), (this.ignoredFields = o || []); - } - ObjectElement(s) { - const o = this.specPath(s), - i = this.retrieveFixedFields(o); - return ( - s.forEach((s, u, _) => { - if ( - Ru(u) && - i.includes(serializers_value(u)) && - !this.ignoredFields.includes(serializers_value(u)) - ) { - const i = this.toRefractedElement([...o, 'fixedFields', serializers_value(u)], s), - w = new Cu.Pr(cloneDeep(u), i); - this.copyMetaAndAttributes(_, w), - w.classes.push('fixed-field'), - this.element.content.push(w); - } else - this.ignoredFields.includes(serializers_value(u)) || - this.element.content.push(cloneDeep(_)); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - }; - class JSONSchemaVisitor extends Mixin(Md, Ed) { - constructor(s) { - super(s), - (this.element = new Jh()), - (this.specPath = Tl(['document', 'objects', 'JSONSchema'])); - } - } - const Td = JSONSchemaVisitor; - const Nd = class ParentSchemaAwareVisitor { - parent; - constructor({ parent: s }) { - this.parent = s; - } - }, - isJSONReferenceLikeElement = (s) => Fu(s) && s.hasKey('$ref'); - class ItemsVisitor extends Mixin(Id, Nd, Ed) { - ObjectElement(s) { - const o = isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema']; - return (this.element = this.toRefractedElement(o, s)), Ju; - } - ArrayElement(s) { - return ( - (this.element = new Cu.wE()), - this.element.classes.push('json-schema-items'), - s.forEach((s) => { - const o = isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const Rd = ItemsVisitor; - const Dd = class RequiredVisitor extends Ed { - ArrayElement(s) { - const o = this.enter(s); - return this.element.classes.push('json-schema-required'), o; - } - }; - const Ld = _curry1(function allPass(s) { - return za(Ca(Ll, 0, Fl('length', s)), function () { - for (var o = 0, i = s.length; o < i; ) { - if (!s[o].apply(this, arguments)) return !1; - o += 1; - } - return !0; - }); - }); - const Bd = _curry1(function isNotEmpty(s) { - return !gp(s); - }); - const Fd = _curry2(function or(s, o) { - return s || o; - }); - var $d = Ml( - za( - 1, - gu( - vu, - _curry2(function either(s, o) { - return _isFunction(s) - ? function _either() { - return s.apply(this, arguments) || o.apply(this, arguments); - } - : Pl(Fd)(s, o); - })(bu, Wl) - ) - ) - ); - const Vd = Ld([Yl, $d, Bd]); - const Ud = class PatternedFieldsVisitor extends Id { - specPath; - ignoredFields; - fieldPatternPredicate = es_F; - constructor({ specPath: s, ignoredFields: o, fieldPatternPredicate: i, ...u }) { - super({ ...u }), - (this.specPath = s), - (this.ignoredFields = o || []), - 'function' == typeof i && (this.fieldPatternPredicate = i); - } - ObjectElement(s) { - return ( - s.forEach((s, o, i) => { - if ( - !this.ignoredFields.includes(serializers_value(o)) && - this.fieldPatternPredicate(serializers_value(o)) - ) { - const u = this.specPath(s), - _ = this.toRefractedElement(u, s), - w = new Cu.Pr(cloneDeep(o), _); - this.copyMetaAndAttributes(i, w), - w.classes.push('patterned-field'), - this.element.content.push(w); - } else - this.ignoredFields.includes(serializers_value(o)) || - this.element.content.push(cloneDeep(i)); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - }; - const Wd = class MapVisitor extends Ud { - constructor(s) { - super(s), (this.fieldPatternPredicate = Vd); - } - }; - class PropertiesVisitor extends Mixin(Wd, Nd, Ed) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-properties'), - (this.specPath = (s) => - isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema']); - } - } - const Kd = PropertiesVisitor; - class PatternPropertiesVisitor extends Mixin(Wd, Nd, Ed) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-patternProperties'), - (this.specPath = (s) => - isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema']); - } - } - const Hd = PatternPropertiesVisitor; - class DependenciesVisitor extends Mixin(Wd, Nd, Ed) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-dependencies'), - (this.specPath = (s) => - isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema']); - } - } - const Jd = DependenciesVisitor; - const Gd = class EnumVisitor extends Ed { - ArrayElement(s) { - const o = this.enter(s); - return this.element.classes.push('json-schema-enum'), o; - } - }; - const Yd = class TypeVisitor extends Ed { - StringElement(s) { - const o = this.enter(s); - return this.element.classes.push('json-schema-type'), o; - } - ArrayElement(s) { - const o = this.enter(s); - return this.element.classes.push('json-schema-type'), o; - } - }; - class AllOfVisitor extends Mixin(Id, Nd, Ed) { - constructor(s) { - super(s), (this.element = new Cu.wE()), this.element.classes.push('json-schema-allOf'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const Xd = AllOfVisitor; - class AnyOfVisitor extends Mixin(Id, Nd, Ed) { - constructor(s) { - super(s), (this.element = new Cu.wE()), this.element.classes.push('json-schema-anyOf'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const Zd = AnyOfVisitor; - class OneOfVisitor extends Mixin(Id, Nd, Ed) { - constructor(s) { - super(s), (this.element = new Cu.wE()), this.element.classes.push('json-schema-oneOf'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const Qd = OneOfVisitor; - class DefinitionsVisitor extends Mixin(Wd, Nd, Ed) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-definitions'), - (this.specPath = (s) => - isJSONReferenceLikeElement(s) - ? ['document', 'objects', 'JSONReference'] - : ['document', 'objects', 'JSONSchema']); - } - } - const ef = DefinitionsVisitor; - class LinksVisitor extends Mixin(Id, Nd, Ed) { - constructor(s) { - super(s), (this.element = new Cu.wE()), this.element.classes.push('json-schema-links'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = this.toRefractedElement(['document', 'objects', 'LinkDescription'], s); - this.element.push(o); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const rf = LinksVisitor; - class JSONReferenceVisitor extends Mixin(Md, Ed) { - constructor(s) { - super(s), - (this.element = new Gh()), - (this.specPath = Tl(['document', 'objects', 'JSONReference'])); - } - ObjectElement(s) { - const o = Md.prototype.ObjectElement.call(this, s); - return Ru(this.element.$ref) && this.element.classes.push('reference-element'), o; - } - } - const of = JSONReferenceVisitor; - const af = class $RefVisitor extends Ed { - StringElement(s) { - const o = this.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - const lf = _curry3(function ifElse(s, o, i) { - return za(Math.max(s.length, o.length, i.length), function _ifElse() { - return s.apply(this, arguments) ? o.apply(this, arguments) : i.apply(this, arguments); - }); - }); - const cf = _curry1(function comparator(s) { - return function (o, i) { - return s(o, i) ? -1 : s(i, o) ? 1 : 0; - }; - }); - var uf = _curry2(function sort(s, o) { - return Array.prototype.slice.call(o, 0).sort(s); - }); - const hf = uf; - var df = _curry1(function (s) { - return _nth(0, s); - }); - const mf = df; - const gf = _curry1(_reduced); - const yf = Ml(ld); - const bf = gu(yp, Bd); - function _toConsumableArray(s) { - return ( - (function _arrayWithoutHoles(s) { - if (Array.isArray(s)) return _arrayLikeToArray(s); - })(s) || - (function _iterableToArray(s) { - if ( - ('undefined' != typeof Symbol && null != s[Symbol.iterator]) || - null != s['@@iterator'] - ) - return Array.from(s); - })(s) || - (function _unsupportedIterableToArray(s, o) { - if (s) { - if ('string' == typeof s) return _arrayLikeToArray(s, o); - var i = {}.toString.call(s).slice(8, -1); - return ( - 'Object' === i && s.constructor && (i = s.constructor.name), - 'Map' === i || 'Set' === i - ? Array.from(s) - : 'Arguments' === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i) - ? _arrayLikeToArray(s, o) - : void 0 - ); - } - })(s) || - (function _nonIterableSpread() { - throw new TypeError( - 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })() - ); - } - function _arrayLikeToArray(s, o) { - (null == o || o > s.length) && (o = s.length); - for (var i = 0, u = Array(o); i < o; i++) u[i] = s[i]; - return u; - } - var _f = pipe( - hf( - cf(function (s, o) { - return s.length > o.length; - }) - ), - mf, - Da('length') - ), - Sf = Ja(function (s, o, i) { - var u = i.apply(void 0, _toConsumableArray(s)); - return yf(u) ? gf(u) : o; - }); - const xf = lf( - bf, - function dispatchImpl(s) { - var o = _f(s); - return za(o, function () { - for (var o = arguments.length, i = new Array(o), u = 0; u < o; u++) - i[u] = arguments[u]; - return Ca(Sf(i), void 0, s); - }); - }, - Nl - ); - const kf = class AlternatingVisitor extends Id { - alternator; - constructor({ alternator: s, ...o }) { - super({ ...o }), (this.alternator = s); - } - enter(s) { - const o = this.alternator.map(({ predicate: s, specPath: o }) => lf(s, Tl(o), Nl)), - i = xf(o)(s); - return (this.element = this.toRefractedElement(i, s)), Ju; - } - }; - const Cf = class SchemaOrReferenceVisitor extends kf { - constructor(s) { - super(s), - (this.alternator = [ - { - predicate: isJSONReferenceLikeElement, - specPath: ['document', 'objects', 'JSONReference'] - }, - { predicate: es_T, specPath: ['document', 'objects', 'JSONSchema'] } - ]); - } - }; - class MediaVisitor extends Mixin(Md, Ed) { - constructor(s) { - super(s), - (this.element = new Qh()), - (this.specPath = Tl(['document', 'objects', 'Media'])); - } - } - const Of = MediaVisitor; - class LinkDescriptionVisitor extends Mixin(Md, Ed) { - constructor(s) { - super(s), - (this.element = new td()), - (this.specPath = Tl(['document', 'objects', 'LinkDescription'])); - } - } - const jf = { - visitors: { - value: Ed, - JSONSchemaOrJSONReferenceVisitor: Cf, - document: { - objects: { - JSONSchema: { - $visitor: Td, - fixedFields: { - id: { $ref: '#/visitors/value' }, - $schema: { $ref: '#/visitors/value' }, - multipleOf: { $ref: '#/visitors/value' }, - maximum: { $ref: '#/visitors/value' }, - exclusiveMaximum: { $ref: '#/visitors/value' }, - minimum: { $ref: '#/visitors/value' }, - exclusiveMinimum: { $ref: '#/visitors/value' }, - maxLength: { $ref: '#/visitors/value' }, - minLength: { $ref: '#/visitors/value' }, - pattern: { $ref: '#/visitors/value' }, - additionalItems: Cf, - items: Rd, - maxItems: { $ref: '#/visitors/value' }, - minItems: { $ref: '#/visitors/value' }, - uniqueItems: { $ref: '#/visitors/value' }, - maxProperties: { $ref: '#/visitors/value' }, - minProperties: { $ref: '#/visitors/value' }, - required: Dd, - properties: Kd, - additionalProperties: Cf, - patternProperties: Hd, - dependencies: Jd, - enum: Gd, - type: Yd, - allOf: Xd, - anyOf: Zd, - oneOf: Qd, - not: Cf, - definitions: ef, - title: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - default: { $ref: '#/visitors/value' }, - format: { $ref: '#/visitors/value' }, - base: { $ref: '#/visitors/value' }, - links: rf, - media: { $ref: '#/visitors/document/objects/Media' }, - readOnly: { $ref: '#/visitors/value' } - } - }, - JSONReference: { $visitor: of, fixedFields: { $ref: af } }, - Media: { - $visitor: Of, - fixedFields: { - binaryEncoding: { $ref: '#/visitors/value' }, - type: { $ref: '#/visitors/value' } - } - }, - LinkDescription: { - $visitor: LinkDescriptionVisitor, - fixedFields: { - href: { $ref: '#/visitors/value' }, - rel: { $ref: '#/visitors/value' }, - title: { $ref: '#/visitors/value' }, - targetSchema: Cf, - mediaType: { $ref: '#/visitors/value' }, - method: { $ref: '#/visitors/value' }, - encType: { $ref: '#/visitors/value' }, - schema: Cf - } - } - } - } - } - }, - traversal_visitor_getNodeType = (s) => { - if (Nu(s)) return `${s.element.charAt(0).toUpperCase() + s.element.slice(1)}Element`; - }, - Pf = { - JSONSchemaDraft4Element: ['content'], - JSONReferenceElement: ['content'], - MediaElement: ['content'], - LinkDescriptionElement: ['content'], - ...Qu - }, - Tf = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Jh || (s(u) && o('JSONSchemaDraft4', u) && i('object', u)) - ), - Nf = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Gh || (s(u) && o('JSONReference', u) && i('object', u)) - ), - Rf = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Qh || (s(u) && o('media', u) && i('object', u)) - ), - Df = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof td || (s(u) && o('linkDescription', u) && i('object', u)) - ), - Ff = { - namespace: (s) => { - const { base: o } = s; - return ( - o.register('jSONSchemaDraft4', Jh), - o.register('jSONReference', Gh), - o.register('media', Qh), - o.register('linkDescription', td), - o - ); - } - }, - Vf = Ff, - refractor_toolbox = () => { - const s = createNamespace(Vf); - return { predicates: { ...ce, isStringElement: Ru }, namespace: s }; - }, - refractor_refract = ( - s, - { - specPath: o = ['visitors', 'document', 'objects', 'JSONSchema', '$visitor'], - plugins: i = [], - specificationObj: u = jf - } = {} - ) => { - const _ = (0, Cu.e)(s), - w = dereference(u), - x = new (cp(o, w))({ specObj: w }); - return ( - visitor_visit(_, x), - dispatchPluginsSync(x.element, i, { - toolboxCreator: refractor_toolbox, - visitorOptions: { keyMap: Pf, nodeTypeGetter: traversal_visitor_getNodeType } - }) - ); - }, - refractor_createRefractor = - (s) => - (o, i = {}) => - refractor_refract(o, { specPath: s, ...i }); - (Jh.refract = refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'JSONSchema', - '$visitor' - ])), - (Gh.refract = refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'JSONReference', - '$visitor' - ])), - (Qh.refract = refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Media', - '$visitor' - ])), - (td.refract = refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'LinkDescription', - '$visitor' - ])); - const Wf = class Schema_Schema extends Jh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'schema'), this.classes.push('json-schema-draft-4'); - } - get idProp() { - throw new Hh('idProp getter in Schema class is not not supported.'); - } - set idProp(s) { - throw new Hh('idProp setter in Schema class is not not supported.'); - } - get $schema() { - throw new Hh('$schema getter in Schema class is not not supported.'); - } - set $schema(s) { - throw new Hh('$schema setter in Schema class is not not supported.'); - } - get additionalItems() { - return this.get('additionalItems'); - } - set additionalItems(s) { - this.set('additionalItems', s); - } - get items() { - return this.get('items'); - } - set items(s) { - this.set('items', s); - } - get additionalProperties() { - return this.get('additionalProperties'); - } - set additionalProperties(s) { - this.set('additionalProperties', s); - } - get patternProperties() { - throw new Hh('patternProperties getter in Schema class is not not supported.'); - } - set patternProperties(s) { - throw new Hh('patternProperties setter in Schema class is not not supported.'); - } - get dependencies() { - throw new Hh('dependencies getter in Schema class is not not supported.'); - } - set dependencies(s) { - throw new Hh('dependencies setter in Schema class is not not supported.'); - } - get type() { - return this.get('type'); - } - set type(s) { - this.set('type', s); - } - get not() { - return this.get('not'); - } - set not(s) { - this.set('not', s); - } - get definitions() { - throw new Hh('definitions getter in Schema class is not not supported.'); - } - set definitions(s) { - throw new Hh('definitions setter in Schema class is not not supported.'); - } - get base() { - throw new Hh('base getter in Schema class is not not supported.'); - } - set base(s) { - throw new Hh('base setter in Schema class is not not supported.'); - } - get links() { - throw new Hh('links getter in Schema class is not not supported.'); - } - set links(s) { - throw new Hh('links setter in Schema class is not not supported.'); - } - get media() { - throw new Hh('media getter in Schema class is not not supported.'); - } - set media(s) { - throw new Hh('media setter in Schema class is not not supported.'); - } - get nullable() { - return this.get('nullable'); - } - set nullable(s) { - this.set('nullable', s); - } - get discriminator() { - return this.get('discriminator'); - } - set discriminator(s) { - this.set('discriminator', s); - } - get writeOnly() { - return this.get('writeOnly'); - } - set writeOnly(s) { - this.set('writeOnly', s); - } - get xml() { - return this.get('xml'); - } - set xml(s) { - this.set('xml', s); - } - get externalDocs() { - return this.get('externalDocs'); - } - set externalDocs(s) { - this.set('externalDocs', s); - } - get example() { - return this.get('example'); - } - set example(s) { - this.set('example', s); - } - get deprecated() { - return this.get('deprecated'); - } - set deprecated(s) { - this.set('deprecated', s); - } - }; - class SecurityRequirement extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'securityRequirement'); - } - } - const Hf = SecurityRequirement; - class SecurityScheme extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'securityScheme'); - } - get type() { - return this.get('type'); - } - set type(s) { - this.set('type', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get name() { - return this.get('name'); - } - set name(s) { - this.set('name', s); - } - get in() { - return this.get('in'); - } - set in(s) { - this.set('in', s); - } - get scheme() { - return this.get('scheme'); - } - set scheme(s) { - this.set('scheme', s); - } - get bearerFormat() { - return this.get('bearerFormat'); - } - set bearerFormat(s) { - this.set('bearerFormat', s); - } - get flows() { - return this.get('flows'); - } - set flows(s) { - this.set('flows', s); - } - get openIdConnectUrl() { - return this.get('openIdConnectUrl'); - } - set openIdConnectUrl(s) { - this.set('openIdConnectUrl', s); - } - } - const Jf = SecurityScheme; - class Server extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'server'); - } - get url() { - return this.get('url'); - } - set url(s) { - this.set('url', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get variables() { - return this.get('variables'); - } - set variables(s) { - this.set('variables', s); - } - } - const Gf = Server; - class ServerVariable extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'serverVariable'); - } - get enum() { - return this.get('enum'); - } - set enum(s) { - this.set('enum', s); - } - get default() { - return this.get('default'); - } - set default(s) { - this.set('default', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - } - const Xf = ServerVariable; - class Tag extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'tag'); - } - get name() { - return this.get('name'); - } - set name(s) { - this.set('name', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get externalDocs() { - return this.get('externalDocs'); - } - set externalDocs(s) { - this.set('externalDocs', s); - } - } - const Qf = Tag; - class Xml extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'xml'); - } - get name() { - return this.get('name'); - } - set name(s) { - this.set('name', s); - } - get namespace() { - return this.get('namespace'); - } - set namespace(s) { - this.set('namespace', s); - } - get prefix() { - return this.get('prefix'); - } - set prefix(s) { - this.set('prefix', s); - } - get attribute() { - return this.get('attribute'); - } - set attribute(s) { - this.set('attribute', s); - } - get wrapped() { - return this.get('wrapped'); - } - set wrapped(s) { - this.set('wrapped', s); - } - } - const em = Xml; - const tm = class visitors_Visitor_Visitor { - element; - constructor(s = {}) { - Object.assign(this, s); - } - copyMetaAndAttributes(s, o) { - (s.meta.length > 0 || o.meta.length > 0) && - ((o.meta = deepmerge(o.meta, s.meta)), - hasElementSourceMap(s) && o.meta.set('sourceMap', s.meta.get('sourceMap'))), - (s.attributes.length > 0 || s.meta.length > 0) && - (o.attributes = deepmerge(o.attributes, s.attributes)); - } - }; - const rm = class FallbackVisitor_FallbackVisitor extends tm { - enter(s) { - return (this.element = cloneDeep(s)), Ju; - } - }; - const nm = class SpecificationVisitor_SpecificationVisitor extends tm { - specObj; - passingOptionsNames = ['specObj', 'openApiGenericElement', 'openApiSemanticElement']; - openApiGenericElement; - openApiSemanticElement; - constructor({ - specObj: s, - passingOptionsNames: o, - openApiGenericElement: i, - openApiSemanticElement: u, - ..._ - }) { - super({ ..._ }), - (this.specObj = s), - (this.openApiGenericElement = i), - (this.openApiSemanticElement = u), - Array.isArray(o) && (this.passingOptionsNames = o); - } - retrievePassingOptions() { - return Ad(this.passingOptionsNames, this); - } - retrieveFixedFields(s) { - const o = cp(['visitors', ...s, 'fixedFields'], this.specObj); - return 'object' == typeof o && null !== o ? Object.keys(o) : []; - } - retrieveVisitor(s) { - return Xo(Wl, ['visitors', ...s], this.specObj) - ? cp(['visitors', ...s], this.specObj) - : cp(['visitors', ...s, '$visitor'], this.specObj); - } - retrieveVisitorInstance(s, o = {}) { - const i = this.retrievePassingOptions(); - return new (this.retrieveVisitor(s))({ ...i, ...o }); - } - toRefractedElement(s, o, i = {}) { - const u = this.retrieveVisitorInstance(s, i); - return u instanceof rm && (null == u ? void 0 : u.constructor) === rm - ? cloneDeep(o) - : (visitor_visit(o, u, i), u.element); - } - }, - isReferenceLikeElement = (s) => Fu(s) && s.hasKey('$ref'), - sm = Fu, - om = Fu, - isOpenApiExtension = (s) => Ru(s.key) && Fp('x-', serializers_value(s.key)); - const im = class FixedFieldsVisitor_FixedFieldsVisitor extends nm { - specPath; - ignoredFields; - canSupportSpecificationExtensions = !0; - specificationExtensionPredicate = isOpenApiExtension; - constructor({ - specPath: s, - ignoredFields: o, - canSupportSpecificationExtensions: i, - specificationExtensionPredicate: u, - ..._ - }) { - super({ ..._ }), - (this.specPath = s), - (this.ignoredFields = o || []), - 'boolean' == typeof i && (this.canSupportSpecificationExtensions = i), - 'function' == typeof u && (this.specificationExtensionPredicate = u); - } - ObjectElement(s) { - const o = this.specPath(s), - i = this.retrieveFixedFields(o); - return ( - s.forEach((s, u, _) => { - if ( - Ru(u) && - i.includes(serializers_value(u)) && - !this.ignoredFields.includes(serializers_value(u)) - ) { - const i = this.toRefractedElement([...o, 'fixedFields', serializers_value(u)], s), - w = new Cu.Pr(cloneDeep(u), i); - this.copyMetaAndAttributes(_, w), - w.classes.push('fixed-field'), - this.element.content.push(w); - } else if ( - this.canSupportSpecificationExtensions && - this.specificationExtensionPredicate(_) - ) { - const s = this.toRefractedElement(['document', 'extension'], _); - this.element.content.push(s); - } else - this.ignoredFields.includes(serializers_value(u)) || - this.element.content.push(cloneDeep(_)); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - }; - class OpenApi3_0Visitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Oh()), - (this.specPath = Tl(['document', 'objects', 'OpenApi'])), - (this.canSupportSpecificationExtensions = !0); - } - ObjectElement(s) { - return im.prototype.ObjectElement.call(this, s); - } - } - const am = OpenApi3_0Visitor; - class OpenapiVisitor extends Mixin(nm, rm) { - StringElement(s) { - const o = new wh(serializers_value(s)); - return this.copyMetaAndAttributes(s, o), (this.element = o), Ju; - } - } - const lm = OpenapiVisitor; - const cm = class SpecificationExtensionVisitor extends nm { - MemberElement(s) { - return ( - (this.element = cloneDeep(s)), - this.element.classes.push('specification-extension'), - Ju - ); - } - }; - class InfoVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new rh()), - (this.specPath = Tl(['document', 'objects', 'Info'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const um = InfoVisitor; - const pm = class VersionVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return ( - this.element.classes.push('api-version'), this.element.classes.push('version'), o - ); - } - }; - class ContactVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Gp()), - (this.specPath = Tl(['document', 'objects', 'Contact'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const hm = ContactVisitor; - class LicenseVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new uh()), - (this.specPath = Tl(['document', 'objects', 'License'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const dm = LicenseVisitor; - class LinkVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new dh()), - (this.specPath = Tl(['document', 'objects', 'Link'])), - (this.canSupportSpecificationExtensions = !0); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - (Ru(this.element.operationId) || Ru(this.element.operationRef)) && - this.element.classes.push('reference-element'), - o - ); - } - } - const fm = LinkVisitor; - const mm = class OperationRefVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - const gm = class OperationIdVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - const ym = class PatternedFieldsVisitor_PatternedFieldsVisitor extends nm { - specPath; - ignoredFields; - fieldPatternPredicate = es_F; - canSupportSpecificationExtensions = !1; - specificationExtensionPredicate = isOpenApiExtension; - constructor({ - specPath: s, - ignoredFields: o, - fieldPatternPredicate: i, - canSupportSpecificationExtensions: u, - specificationExtensionPredicate: _, - ...w - }) { - super({ ...w }), - (this.specPath = s), - (this.ignoredFields = o || []), - 'function' == typeof i && (this.fieldPatternPredicate = i), - 'boolean' == typeof u && (this.canSupportSpecificationExtensions = u), - 'function' == typeof _ && (this.specificationExtensionPredicate = _); - } - ObjectElement(s) { - return ( - s.forEach((s, o, i) => { - if ( - this.canSupportSpecificationExtensions && - this.specificationExtensionPredicate(i) - ) { - const s = this.toRefractedElement(['document', 'extension'], i); - this.element.content.push(s); - } else if ( - !this.ignoredFields.includes(serializers_value(o)) && - this.fieldPatternPredicate(serializers_value(o)) - ) { - const u = this.specPath(s), - _ = this.toRefractedElement(u, s), - w = new Cu.Pr(cloneDeep(o), _); - this.copyMetaAndAttributes(i, w), - w.classes.push('patterned-field'), - this.element.content.push(w); - } else - this.ignoredFields.includes(serializers_value(o)) || - this.element.content.push(cloneDeep(i)); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - }; - const vm = class MapVisitor_MapVisitor extends ym { - constructor(s) { - super(s), (this.fieldPatternPredicate = Vd); - } - }; - class LinkParameters extends Cu.Sh { - static primaryClass = 'link-parameters'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(LinkParameters.primaryClass); - } - } - const bm = LinkParameters; - class ParametersVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), (this.element = new bm()), (this.specPath = Tl(['value'])); - } - } - const _m = ParametersVisitor; - class ServerVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Gf()), - (this.specPath = Tl(['document', 'objects', 'Server'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const Em = ServerVisitor; - const wm = class UrlVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('server-url'), o; - } - }; - class Servers extends Cu.wE { - static primaryClass = 'servers'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(Servers.primaryClass); - } - } - const Sm = Servers; - class ServersVisitor extends Mixin(nm, rm) { - constructor(s) { - super(s), (this.element = new Sm()); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = sm(s) ? ['document', 'objects', 'Server'] : ['value'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const xm = ServersVisitor; - class ServerVariableVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Xf()), - (this.specPath = Tl(['document', 'objects', 'ServerVariable'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const km = ServerVariableVisitor; - class ServerVariables extends Cu.Sh { - static primaryClass = 'server-variables'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ServerVariables.primaryClass); - } - } - const Cm = ServerVariables; - class VariablesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Cm()), - (this.specPath = Tl(['document', 'objects', 'ServerVariable'])); - } - } - const Om = VariablesVisitor; - class MediaTypeVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new fh()), - (this.specPath = Tl(['document', 'objects', 'MediaType'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const Am = MediaTypeVisitor; - const jm = class AlternatingVisitor_AlternatingVisitor extends nm { - alternator; - constructor({ alternator: s, ...o }) { - super({ ...o }), (this.alternator = s || []); - } - enter(s) { - const o = this.alternator.map(({ predicate: s, specPath: o }) => lf(s, Tl(o), Nl)), - i = xf(o)(s); - return (this.element = this.toRefractedElement(i, s)), Ju; - } - }, - Im = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Hp || (s(u) && o('callback', u) && i('object', u)) - ), - Pm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Jp || (s(u) && o('components', u) && i('object', u)) - ), - Mm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Gp || (s(u) && o('contact', u) && i('object', u)) - ), - Tm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Zp || (s(u) && o('example', u) && i('object', u)) - ), - Nm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Qp || (s(u) && o('externalDocumentation', u) && i('object', u)) - ), - Rm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof th || (s(u) && o('header', u) && i('object', u)) - ), - Dm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof rh || (s(u) && o('info', u) && i('object', u)) - ), - Lm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof uh || (s(u) && o('license', u) && i('object', u)) - ), - Bm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof dh || (s(u) && o('link', u) && i('object', u)) - ), - Fm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof wh || (s(u) && o('openapi', u) && i('string', u)) - ), - qm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i, hasClass: u }) => - (_) => - _ instanceof Oh || (s(_) && o('openApi3_0', _) && i('object', _) && u('api', _)) - ), - $m = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof jh || (s(u) && o('operation', u) && i('object', u)) - ), - Vm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Ih || (s(u) && o('parameter', u) && i('object', u)) - ), - Um = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Ph || (s(u) && o('pathItem', u) && i('object', u)) - ), - zm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Rh || (s(u) && o('paths', u) && i('object', u)) - ), - Wm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Dh || (s(u) && o('reference', u) && i('object', u)) - ), - Km = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Lh || (s(u) && o('requestBody', u) && i('object', u)) - ), - Hm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Fh || (s(u) && o('response', u) && i('object', u)) - ), - Jm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Kh || (s(u) && o('responses', u) && i('object', u)) - ), - Gm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Wf || (s(u) && o('schema', u) && i('object', u)) - ), - isBooleanJsonSchemaElement = (s) => Bu(s) && s.classes.includes('boolean-json-schema'), - Ym = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Hf || (s(u) && o('securityRequirement', u) && i('object', u)) - ), - Xm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Jf || (s(u) && o('securityScheme', u) && i('object', u)) - ), - Zm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Gf || (s(u) && o('server', u) && i('object', u)) - ), - Qm = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Xf || (s(u) && o('serverVariable', u) && i('object', u)) - ), - eg = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof fh || (s(u) && o('mediaType', u) && i('object', u)) - ), - rg = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i, hasClass: u }) => - (_) => - _ instanceof Sm || (s(_) && o('array', _) && i('array', _) && u('servers', _)) - ); - class SchemaVisitor extends Mixin(jm, rm) { - constructor(s) { - super(s), - (this.alternator = [ - { - predicate: isReferenceLikeElement, - specPath: ['document', 'objects', 'Reference'] - }, - { predicate: es_T, specPath: ['document', 'objects', 'Schema'] } - ]); - } - ObjectElement(s) { - const o = jm.prototype.enter.call(this, s); - return ( - Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), o - ); - } - } - const ng = SchemaVisitor; - class ExamplesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('examples'), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Example']), - (this.canSupportSpecificationExtensions = !0); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'example'); - }), - o - ); - } - } - const sg = ExamplesVisitor; - class MediaTypeExamples extends Cu.Sh { - static primaryClass = 'media-type-examples'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(MediaTypeExamples.primaryClass), - this.classes.push('examples'); - } - } - const og = MediaTypeExamples; - const lg = class ExamplesVisitor_ExamplesVisitor extends sg { - constructor(s) { - super(s), (this.element = new og()); - } - }; - class MediaTypeEncoding extends Cu.Sh { - static primaryClass = 'media-type-encoding'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(MediaTypeEncoding.primaryClass); - } - } - const pg = MediaTypeEncoding; - class EncodingVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new pg()), - (this.specPath = Tl(['document', 'objects', 'Encoding'])); - } - } - const fg = EncodingVisitor; - class SecurityRequirementVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), (this.element = new Hf()), (this.specPath = Tl(['value'])); - } - } - const mg = SecurityRequirementVisitor; - class Security extends Cu.wE { - static primaryClass = 'security'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(Security.primaryClass); - } - } - const gg = Security; - class SecurityVisitor extends Mixin(nm, rm) { - constructor(s) { - super(s), (this.element = new gg()); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - if (Fu(s)) { - const o = this.toRefractedElement( - ['document', 'objects', 'SecurityRequirement'], - s - ); - this.element.push(o); - } else this.element.push(cloneDeep(s)); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const yg = SecurityVisitor; - class ComponentsVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Jp()), - (this.specPath = Tl(['document', 'objects', 'Components'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const _g = ComponentsVisitor; - class TagVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Qf()), - (this.specPath = Tl(['document', 'objects', 'Tag'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const xg = TagVisitor; - class ReferenceVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Dh()), - (this.specPath = Tl(['document', 'objects', 'Reference'])), - (this.canSupportSpecificationExtensions = !1); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return Ru(this.element.$ref) && this.element.classes.push('reference-element'), o; - } - } - const kg = ReferenceVisitor; - const qg = class $RefVisitor_$RefVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - class ParameterVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Ih()), - (this.specPath = Tl(['document', 'objects', 'Parameter'])), - (this.canSupportSpecificationExtensions = !0); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - Fu(this.element.contentProp) && - this.element.contentProp.filter(eg).forEach((s, o) => { - s.setMetaProperty('media-type', serializers_value(o)); - }), - o - ); - } - } - const Vg = ParameterVisitor; - class SchemaVisitor_SchemaVisitor extends Mixin(jm, rm) { - constructor(s) { - super(s), - (this.alternator = [ - { - predicate: isReferenceLikeElement, - specPath: ['document', 'objects', 'Reference'] - }, - { predicate: es_T, specPath: ['document', 'objects', 'Schema'] } - ]); - } - ObjectElement(s) { - const o = jm.prototype.enter.call(this, s); - return ( - Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), o - ); - } - } - const Ug = SchemaVisitor_SchemaVisitor; - class HeaderVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new th()), - (this.specPath = Tl(['document', 'objects', 'Header'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const zg = HeaderVisitor; - class header_SchemaVisitor_SchemaVisitor extends Mixin(jm, rm) { - constructor(s) { - super(s), - (this.alternator = [ - { - predicate: isReferenceLikeElement, - specPath: ['document', 'objects', 'Reference'] - }, - { predicate: es_T, specPath: ['document', 'objects', 'Schema'] } - ]); - } - ObjectElement(s) { - const o = jm.prototype.enter.call(this, s); - return ( - Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), o - ); - } - } - const Wg = header_SchemaVisitor_SchemaVisitor; - class HeaderExamples extends Cu.Sh { - static primaryClass = 'header-examples'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(HeaderExamples.primaryClass), - this.classes.push('examples'); - } - } - const Kg = HeaderExamples; - const Yg = class header_ExamplesVisitor_ExamplesVisitor extends sg { - constructor(s) { - super(s), (this.element = new Kg()); - } - }; - class ContentVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('content'), - (this.specPath = Tl(['document', 'objects', 'MediaType'])); - } - } - const Xg = ContentVisitor; - class HeaderContent extends Cu.Sh { - static primaryClass = 'header-content'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(HeaderContent.primaryClass), - this.classes.push('content'); - } - } - const Zg = HeaderContent; - const ey = class ContentVisitor_ContentVisitor extends Xg { - constructor(s) { - super(s), (this.element = new Zg()); - } - }; - class schema_SchemaVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Wf()), - (this.specPath = Tl(['document', 'objects', 'Schema'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const ty = schema_SchemaVisitor, - { allOf: ry } = jf.visitors.document.objects.JSONSchema.fixedFields; - const ny = class AllOfVisitor_AllOfVisitor extends ry { - ArrayElement(s) { - const o = ry.prototype.ArrayElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'schema'); - }), - o - ); - } - }, - { anyOf: sy } = jf.visitors.document.objects.JSONSchema.fixedFields; - const oy = class AnyOfVisitor_AnyOfVisitor extends sy { - ArrayElement(s) { - const o = sy.prototype.ArrayElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'schema'); - }), - o - ); - } - }, - { oneOf: iy } = jf.visitors.document.objects.JSONSchema.fixedFields; - const ay = class OneOfVisitor_OneOfVisitor extends iy { - ArrayElement(s) { - const o = iy.prototype.ArrayElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'schema'); - }), - o - ); - } - }, - { items: ly } = jf.visitors.document.objects.JSONSchema.fixedFields; - const cy = class ItemsVisitor_ItemsVisitor extends ly { - ObjectElement(s) { - const o = ly.prototype.ObjectElement.call(this, s); - return ( - Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), o - ); - } - ArrayElement(s) { - return this.enter(s); - } - }, - { properties: uy } = jf.visitors.document.objects.JSONSchema.fixedFields; - const py = class PropertiesVisitor_PropertiesVisitor extends uy { - ObjectElement(s) { - const o = uy.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'schema'); - }), - o - ); - } - }, - { type: hy } = jf.visitors.document.objects.JSONSchema.fixedFields; - const dy = class TypeVisitor_TypeVisitor extends hy { - ArrayElement(s) { - return this.enter(s); - } - }, - { JSONSchemaOrJSONReferenceVisitor: fy } = jf.visitors; - const my = class SchemaOrReferenceVisitor_SchemaOrReferenceVisitor extends fy { - ObjectElement(s) { - const o = fy.prototype.enter.call(this, s); - return ( - Wm(this.element) && this.element.setMetaProperty('referenced-element', 'schema'), o - ); - } - }; - class DiscriminatorVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Yp()), - (this.specPath = Tl(['document', 'objects', 'Discriminator'])), - (this.canSupportSpecificationExtensions = !1); - } - } - const gy = DiscriminatorVisitor; - class DiscriminatorMapping extends Cu.Sh { - static primaryClass = 'discriminator-mapping'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(DiscriminatorMapping.primaryClass); - } - } - const yy = DiscriminatorMapping; - class MappingVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), (this.element = new yy()), (this.specPath = Tl(['value'])); - } - } - const vy = MappingVisitor; - class XmlVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new em()), - (this.specPath = Tl(['document', 'objects', 'XML'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const by = XmlVisitor; - class ParameterExamples extends Cu.Sh { - static primaryClass = 'parameter-examples'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(ParameterExamples.primaryClass), - this.classes.push('examples'); - } - } - const _y = ParameterExamples; - const Ey = class parameter_ExamplesVisitor_ExamplesVisitor extends sg { - constructor(s) { - super(s), (this.element = new _y()); - } - }; - class ParameterContent extends Cu.Sh { - static primaryClass = 'parameter-content'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(ParameterContent.primaryClass), - this.classes.push('content'); - } - } - const wy = ParameterContent; - const Sy = class parameter_ContentVisitor_ContentVisitor extends Xg { - constructor(s) { - super(s), (this.element = new wy()); - } - }; - class ComponentsSchemas extends Cu.Sh { - static primaryClass = 'components-schemas'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsSchemas.primaryClass); - } - } - const xy = ComponentsSchemas; - class SchemasVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new xy()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Schema']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'schema'); - }), - o - ); - } - } - const ky = SchemasVisitor; - class ComponentsResponses extends Cu.Sh { - static primaryClass = 'components-responses'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsResponses.primaryClass); - } - } - const Cy = ComponentsResponses; - class ResponsesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Cy()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Response']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'response'); - }), - this.element.filter(Hm).forEach((s, o) => { - s.setMetaProperty('http-status-code', serializers_value(o)); - }), - o - ); - } - } - const Oy = ResponsesVisitor; - class ComponentsParameters extends Cu.Sh { - static primaryClass = 'components-parameters'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(ComponentsParameters.primaryClass), - this.classes.push('parameters'); - } - } - const Ay = ComponentsParameters; - class ParametersVisitor_ParametersVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Ay()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Parameter']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'parameter'); - }), - o - ); - } - } - const jy = ParametersVisitor_ParametersVisitor; - class ComponentsExamples extends Cu.Sh { - static primaryClass = 'components-examples'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(ComponentsExamples.primaryClass), - this.classes.push('examples'); - } - } - const Iy = ComponentsExamples; - class components_ExamplesVisitor_ExamplesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Iy()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Example']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'example'); - }), - o - ); - } - } - const Py = components_ExamplesVisitor_ExamplesVisitor; - class ComponentsRequestBodies extends Cu.Sh { - static primaryClass = 'components-request-bodies'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsRequestBodies.primaryClass); - } - } - const My = ComponentsRequestBodies; - class RequestBodiesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new My()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'RequestBody']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'requestBody'); - }), - o - ); - } - } - const Ty = RequestBodiesVisitor; - class ComponentsHeaders extends Cu.Sh { - static primaryClass = 'components-headers'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsHeaders.primaryClass); - } - } - const Ny = ComponentsHeaders; - class HeadersVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Ny()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Header']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'header'); - }), - this.element.filter(Rm).forEach((s, o) => { - s.setMetaProperty('header-name', serializers_value(o)); - }), - o - ); - } - } - const Ry = HeadersVisitor; - class ComponentsSecuritySchemes extends Cu.Sh { - static primaryClass = 'components-security-schemes'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsSecuritySchemes.primaryClass); - } - } - const Dy = ComponentsSecuritySchemes; - class SecuritySchemesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Dy()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'SecurityScheme']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'securityScheme'); - }), - o - ); - } - } - const Ly = SecuritySchemesVisitor; - class ComponentsLinks extends Cu.Sh { - static primaryClass = 'components-links'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsLinks.primaryClass); - } - } - const By = ComponentsLinks; - class LinksVisitor_LinksVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new By()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Link']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'link'); - }), - o - ); - } - } - const Fy = LinksVisitor_LinksVisitor; - class ComponentsCallbacks extends Cu.Sh { - static primaryClass = 'components-callbacks'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsCallbacks.primaryClass); - } - } - const qy = ComponentsCallbacks; - class CallbacksVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new qy()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Callback']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'callback'); - }), - o - ); - } - } - const $y = CallbacksVisitor; - class ExampleVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Zp()), - (this.specPath = Tl(['document', 'objects', 'Example'])), - (this.canSupportSpecificationExtensions = !0); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - Ru(this.element.externalValue) && this.element.classes.push('reference-element'), o - ); - } - } - const Vy = ExampleVisitor; - const Uy = class ExternalValueVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - class ExternalDocumentationVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Qp()), - (this.specPath = Tl(['document', 'objects', 'ExternalDocumentation'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const zy = ExternalDocumentationVisitor; - class encoding_EncodingVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Xp()), - (this.specPath = Tl(['document', 'objects', 'Encoding'])), - (this.canSupportSpecificationExtensions = !0); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - Fu(this.element.headers) && - this.element.headers.filter(Rm).forEach((s, o) => { - s.setMetaProperty('header-name', serializers_value(o)); - }), - o - ); - } - } - const Wy = encoding_EncodingVisitor; - class EncodingHeaders extends Cu.Sh { - static primaryClass = 'encoding-headers'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(EncodingHeaders.primaryClass); - } - } - const Ky = EncodingHeaders; - class HeadersVisitor_HeadersVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new Ky()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Header']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'header'); - }), - this.element.forEach((s, o) => { - if (!Rm(s)) return; - const i = serializers_value(o); - s.setMetaProperty('headerName', i); - }), - o - ); - } - } - const Hy = HeadersVisitor_HeadersVisitor; - class PathsVisitor extends Mixin(ym, rm) { - constructor(s) { - super(s), - (this.element = new Rh()), - (this.specPath = Tl(['document', 'objects', 'PathItem'])), - (this.canSupportSpecificationExtensions = !0), - (this.fieldPatternPredicate = es_T); - } - ObjectElement(s) { - const o = ym.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Um).forEach((s, o) => { - o.classes.push('openapi-path-template'), - o.classes.push('path-template'), - s.setMetaProperty('path', cloneDeep(o)); - }), - o - ); - } - } - const Jy = PathsVisitor; - class RequestBodyVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Lh()), - (this.specPath = Tl(['document', 'objects', 'RequestBody'])); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - Fu(this.element.contentProp) && - this.element.contentProp.filter(eg).forEach((s, o) => { - s.setMetaProperty('media-type', serializers_value(o)); - }), - o - ); - } - } - const Gy = RequestBodyVisitor; - class RequestBodyContent extends Cu.Sh { - static primaryClass = 'request-body-content'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(RequestBodyContent.primaryClass), - this.classes.push('content'); - } - } - const Yy = RequestBodyContent; - const Xy = class request_body_ContentVisitor_ContentVisitor extends Xg { - constructor(s) { - super(s), (this.element = new Yy()); - } - }; - class CallbackVisitor extends Mixin(ym, rm) { - constructor(s) { - super(s), - (this.element = new Hp()), - (this.specPath = Tl(['document', 'objects', 'PathItem'])), - (this.canSupportSpecificationExtensions = !0), - (this.fieldPatternPredicate = (s) => /{(?[^}]{1,2083})}/.test(String(s))); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Um).forEach((s, o) => { - s.setMetaProperty('runtime-expression', serializers_value(o)); - }), - o - ); - } - } - const Zy = CallbackVisitor; - class ResponseVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Fh()), - (this.specPath = Tl(['document', 'objects', 'Response'])); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - Fu(this.element.contentProp) && - this.element.contentProp.filter(eg).forEach((s, o) => { - s.setMetaProperty('media-type', serializers_value(o)); - }), - Fu(this.element.headers) && - this.element.headers.filter(Rm).forEach((s, o) => { - s.setMetaProperty('header-name', serializers_value(o)); - }), - o - ); - } - } - const Qy = ResponseVisitor; - class ResponseHeaders extends Cu.Sh { - static primaryClass = 'response-headers'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ResponseHeaders.primaryClass); - } - } - const ev = ResponseHeaders; - class response_HeadersVisitor_HeadersVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new ev()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Header']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'header'); - }), - this.element.forEach((s, o) => { - if (!Rm(s)) return; - const i = serializers_value(o); - s.setMetaProperty('header-name', i); - }), - o - ); - } - } - const tv = response_HeadersVisitor_HeadersVisitor; - class ResponseContent extends Cu.Sh { - static primaryClass = 'response-content'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(ResponseContent.primaryClass), - this.classes.push('content'); - } - } - const rv = ResponseContent; - const nv = class response_ContentVisitor_ContentVisitor extends Xg { - constructor(s) { - super(s), (this.element = new rv()); - } - }; - class ResponseLinks extends Cu.Sh { - static primaryClass = 'response-links'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ResponseLinks.primaryClass); - } - } - const sv = ResponseLinks; - class response_LinksVisitor_LinksVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new sv()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Link']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'link'); - }), - o - ); - } - } - const ov = response_LinksVisitor_LinksVisitor; - function _isNumber(s) { - return '[object Number]' === Object.prototype.toString.call(s); - } - var iv = _curry2(function range(s, o) { - if (!_isNumber(s) || !_isNumber(o)) - throw new TypeError('Both arguments to range must be numbers'); - for ( - var i = Array(s < o ? o - s : 0), u = s < 0 ? o + Math.abs(s) : o - s, _ = 0; - _ < u; - - ) - (i[_] = _ + s), (_ += 1); - return i; - }); - const av = iv; - function hasOrAdd(s, o, i) { - var u, - _ = typeof s; - switch (_) { - case 'string': - case 'number': - return 0 === s && 1 / s == -1 / 0 - ? !!i._items['-0'] || (o && (i._items['-0'] = !0), !1) - : null !== i._nativeSet - ? o - ? ((u = i._nativeSet.size), i._nativeSet.add(s), i._nativeSet.size === u) - : i._nativeSet.has(s) - : _ in i._items - ? s in i._items[_] || (o && (i._items[_][s] = !0), !1) - : (o && ((i._items[_] = {}), (i._items[_][s] = !0)), !1); - case 'boolean': - if (_ in i._items) { - var w = s ? 1 : 0; - return !!i._items[_][w] || (o && (i._items[_][w] = !0), !1); - } - return o && (i._items[_] = s ? [!1, !0] : [!0, !1]), !1; - case 'function': - return null !== i._nativeSet - ? o - ? ((u = i._nativeSet.size), i._nativeSet.add(s), i._nativeSet.size === u) - : i._nativeSet.has(s) - : _ in i._items - ? !!_includes(s, i._items[_]) || (o && i._items[_].push(s), !1) - : (o && (i._items[_] = [s]), !1); - case 'undefined': - return !!i._items[_] || (o && (i._items[_] = !0), !1); - case 'object': - if (null === s) return !!i._items.null || (o && (i._items.null = !0), !1); - default: - return (_ = Object.prototype.toString.call(s)) in i._items - ? !!_includes(s, i._items[_]) || (o && i._items[_].push(s), !1) - : (o && (i._items[_] = [s]), !1); - } - } - const lv = (function () { - function _Set() { - (this._nativeSet = 'function' == typeof Set ? new Set() : null), (this._items = {}); - } - return ( - (_Set.prototype.add = function (s) { - return !hasOrAdd(s, !0, this); - }), - (_Set.prototype.has = function (s) { - return hasOrAdd(s, !1, this); - }), - _Set - ); - })(); - var cv = _curry2(function difference(s, o) { - for (var i = [], u = 0, _ = s.length, w = o.length, x = new lv(), C = 0; C < w; C += 1) - x.add(o[C]); - for (; u < _; ) x.add(s[u]) && (i[i.length] = s[u]), (u += 1); - return i; - }); - const uv = cv; - class MixedFieldsVisitor extends Mixin(im, ym) { - specPathFixedFields; - specPathPatternedFields; - constructor({ specPathFixedFields: s, specPathPatternedFields: o, ...i }) { - super({ ...i }), (this.specPathFixedFields = s), (this.specPathPatternedFields = o); - } - ObjectElement(s) { - const { specPath: o, ignoredFields: i } = this; - try { - this.specPath = this.specPathFixedFields; - const o = this.retrieveFixedFields(this.specPath(s)); - (this.ignoredFields = [...i, ...uv(s.keys(), o)]), - im.prototype.ObjectElement.call(this, s), - (this.specPath = this.specPathPatternedFields), - (this.ignoredFields = o), - ym.prototype.ObjectElement.call(this, s); - } catch (s) { - throw ((this.specPath = o), s); - } - return Ju; - } - } - const pv = MixedFieldsVisitor; - class responses_ResponsesVisitor extends Mixin(pv, rm) { - constructor(s) { - super(s), - (this.element = new Kh()), - (this.specPathFixedFields = Tl(['document', 'objects', 'Responses'])), - (this.canSupportSpecificationExtensions = !0), - (this.specPathPatternedFields = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Response']), - (this.fieldPatternPredicate = (s) => - new RegExp(`^(1XX|2XX|3XX|4XX|5XX|${av(100, 600).join('|')})$`).test(String(s))); - } - ObjectElement(s) { - const o = pv.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'response'); - }), - this.element.filter(Hm).forEach((s, o) => { - const i = cloneDeep(o); - this.fieldPatternPredicate(serializers_value(i)) && - s.setMetaProperty('http-status-code', i); - }), - o - ); - } - } - const hv = responses_ResponsesVisitor; - class DefaultVisitor extends Mixin(jm, rm) { - constructor(s) { - super(s), - (this.alternator = [ - { - predicate: isReferenceLikeElement, - specPath: ['document', 'objects', 'Reference'] - }, - { predicate: es_T, specPath: ['document', 'objects', 'Response'] } - ]); - } - ObjectElement(s) { - const o = jm.prototype.enter.call(this, s); - return ( - Wm(this.element) - ? this.element.setMetaProperty('referenced-element', 'response') - : Hm(this.element) && this.element.setMetaProperty('http-status-code', 'default'), - o - ); - } - } - const dv = DefaultVisitor; - class OperationVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new jh()), - (this.specPath = Tl(['document', 'objects', 'Operation'])); - } - } - const fv = OperationVisitor; - class OperationTags extends Cu.wE { - static primaryClass = 'operation-tags'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(OperationTags.primaryClass); - } - } - const mv = OperationTags; - const gv = class TagsVisitor extends rm { - constructor(s) { - super(s), (this.element = new mv()); - } - ArrayElement(s) { - return (this.element = this.element.concat(cloneDeep(s))), Ju; - } - }; - class OperationParameters extends Cu.wE { - static primaryClass = 'operation-parameters'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(OperationParameters.primaryClass), - this.classes.push('parameters'); - } - } - const yv = OperationParameters; - class open_api_3_0_ParametersVisitor_ParametersVisitor extends Mixin(nm, rm) { - constructor(s) { - super(s), (this.element = new Cu.wE()), this.element.classes.push('parameters'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Parameter'], - i = this.toRefractedElement(o, s); - Wm(i) && i.setMetaProperty('referenced-element', 'parameter'), this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const vv = open_api_3_0_ParametersVisitor_ParametersVisitor; - const bv = class operation_ParametersVisitor_ParametersVisitor extends vv { - constructor(s) { - super(s), (this.element = new yv()); - } - }; - const _v = class RequestBodyVisitor_RequestBodyVisitor extends jm { - constructor(s) { - super(s), - (this.alternator = [ - { - predicate: isReferenceLikeElement, - specPath: ['document', 'objects', 'Reference'] - }, - { predicate: es_T, specPath: ['document', 'objects', 'RequestBody'] } - ]); - } - ObjectElement(s) { - const o = jm.prototype.enter.call(this, s); - return ( - Wm(this.element) && this.element.setMetaProperty('referenced-element', 'requestBody'), - o - ); - } - }; - class OperationCallbacks extends Cu.Sh { - static primaryClass = 'operation-callbacks'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(OperationCallbacks.primaryClass); - } - } - const Ev = OperationCallbacks; - class CallbacksVisitor_CallbacksVisitor extends Mixin(vm, rm) { - specPath; - constructor(s) { - super(s), - (this.element = new Ev()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'Callback']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(Wm).forEach((s) => { - s.setMetaProperty('referenced-element', 'callback'); - }), - o - ); - } - } - const wv = CallbacksVisitor_CallbacksVisitor; - class OperationSecurity extends Cu.wE { - static primaryClass = 'operation-security'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(OperationSecurity.primaryClass), - this.classes.push('security'); - } - } - const Sv = OperationSecurity; - class SecurityVisitor_SecurityVisitor extends Mixin(nm, rm) { - constructor(s) { - super(s), (this.element = new Sv()); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = Fu(s) ? ['document', 'objects', 'SecurityRequirement'] : ['value'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const xv = SecurityVisitor_SecurityVisitor; - class OperationServers extends Cu.wE { - static primaryClass = 'operation-servers'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(OperationServers.primaryClass), - this.classes.push('servers'); - } - } - const kv = OperationServers; - const Cv = class ServersVisitor_ServersVisitor extends xm { - constructor(s) { - super(s), (this.element = new kv()); - } - }; - class PathItemVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Ph()), - (this.specPath = Tl(['document', 'objects', 'PathItem'])); - } - ObjectElement(s) { - const o = im.prototype.ObjectElement.call(this, s); - return ( - this.element.filter($m).forEach((s, o) => { - const i = cloneDeep(o); - (i.content = serializers_value(i).toUpperCase()), - s.setMetaProperty('http-method', i); - }), - Ru(this.element.$ref) && this.element.classes.push('reference-element'), - o - ); - } - } - const Ov = PathItemVisitor; - const Av = class path_item_$RefVisitor_$RefVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - class PathItemServers extends Cu.wE { - static primaryClass = 'path-item-servers'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(PathItemServers.primaryClass), - this.classes.push('servers'); - } - } - const jv = PathItemServers; - const Iv = class path_item_ServersVisitor_ServersVisitor extends xm { - constructor(s) { - super(s), (this.element = new jv()); - } - }; - class PathItemParameters extends Cu.wE { - static primaryClass = 'path-item-parameters'; - constructor(s, o, i) { - super(s, o, i), - this.classes.push(PathItemParameters.primaryClass), - this.classes.push('parameters'); - } - } - const Pv = PathItemParameters; - const Mv = class path_item_ParametersVisitor_ParametersVisitor extends vv { - constructor(s) { - super(s), (this.element = new Pv()); - } - }; - class SecuritySchemeVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new Jf()), - (this.specPath = Tl(['document', 'objects', 'SecurityScheme'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const Tv = SecuritySchemeVisitor; - class OAuthFlowsVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new _h()), - (this.specPath = Tl(['document', 'objects', 'OAuthFlows'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const Nv = OAuthFlowsVisitor; - class OAuthFlowVisitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new vh()), - (this.specPath = Tl(['document', 'objects', 'OAuthFlow'])), - (this.canSupportSpecificationExtensions = !0); - } - } - const Rv = OAuthFlowVisitor; - class OAuthFlowScopes extends Cu.Sh { - static primaryClass = 'oauth-flow-scopes'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(OAuthFlowScopes.primaryClass); - } - } - const Dv = OAuthFlowScopes; - class ScopesVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), (this.element = new Dv()), (this.specPath = Tl(['value'])); - } - } - const Lv = ScopesVisitor; - class Tags extends Cu.wE { - static primaryClass = 'tags'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(Tags.primaryClass); - } - } - const Bv = Tags; - class TagsVisitor_TagsVisitor extends Mixin(nm, rm) { - constructor(s) { - super(s), (this.element = new Bv()); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - const o = om(s) ? ['document', 'objects', 'Tag'] : ['value'], - i = this.toRefractedElement(o, s); - this.element.push(i); - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const Fv = TagsVisitor_TagsVisitor, - { fixedFields: qv } = jf.visitors.document.objects.JSONSchema, - $v = { - visitors: { - value: rm, - document: { - objects: { - OpenApi: { - $visitor: am, - fixedFields: { - openapi: lm, - info: { $ref: '#/visitors/document/objects/Info' }, - servers: xm, - paths: { $ref: '#/visitors/document/objects/Paths' }, - components: { $ref: '#/visitors/document/objects/Components' }, - security: yg, - tags: Fv, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } - } - }, - Info: { - $visitor: um, - fixedFields: { - title: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - termsOfService: { $ref: '#/visitors/value' }, - contact: { $ref: '#/visitors/document/objects/Contact' }, - license: { $ref: '#/visitors/document/objects/License' }, - version: pm - } - }, - Contact: { - $visitor: hm, - fixedFields: { - name: { $ref: '#/visitors/value' }, - url: { $ref: '#/visitors/value' }, - email: { $ref: '#/visitors/value' } - } - }, - License: { - $visitor: dm, - fixedFields: { - name: { $ref: '#/visitors/value' }, - url: { $ref: '#/visitors/value' } - } - }, - Server: { - $visitor: Em, - fixedFields: { - url: wm, - description: { $ref: '#/visitors/value' }, - variables: Om - } - }, - ServerVariable: { - $visitor: km, - fixedFields: { - enum: { $ref: '#/visitors/value' }, - default: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' } - } - }, - Components: { - $visitor: _g, - fixedFields: { - schemas: ky, - responses: Oy, - parameters: jy, - examples: Py, - requestBodies: Ty, - headers: Ry, - securitySchemes: Ly, - links: Fy, - callbacks: $y - } - }, - Paths: { $visitor: Jy }, - PathItem: { - $visitor: Ov, - fixedFields: { - $ref: Av, - summary: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - get: { $ref: '#/visitors/document/objects/Operation' }, - put: { $ref: '#/visitors/document/objects/Operation' }, - post: { $ref: '#/visitors/document/objects/Operation' }, - delete: { $ref: '#/visitors/document/objects/Operation' }, - options: { $ref: '#/visitors/document/objects/Operation' }, - head: { $ref: '#/visitors/document/objects/Operation' }, - patch: { $ref: '#/visitors/document/objects/Operation' }, - trace: { $ref: '#/visitors/document/objects/Operation' }, - servers: Iv, - parameters: Mv - } - }, - Operation: { - $visitor: fv, - fixedFields: { - tags: gv, - summary: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, - operationId: { $ref: '#/visitors/value' }, - parameters: bv, - requestBody: _v, - responses: { $ref: '#/visitors/document/objects/Responses' }, - callbacks: wv, - deprecated: { $ref: '#/visitors/value' }, - security: xv, - servers: Cv - } - }, - ExternalDocumentation: { - $visitor: zy, - fixedFields: { - description: { $ref: '#/visitors/value' }, - url: { $ref: '#/visitors/value' } - } - }, - Parameter: { - $visitor: Vg, - fixedFields: { - name: { $ref: '#/visitors/value' }, - in: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - required: { $ref: '#/visitors/value' }, - deprecated: { $ref: '#/visitors/value' }, - allowEmptyValue: { $ref: '#/visitors/value' }, - style: { $ref: '#/visitors/value' }, - explode: { $ref: '#/visitors/value' }, - allowReserved: { $ref: '#/visitors/value' }, - schema: Ug, - example: { $ref: '#/visitors/value' }, - examples: Ey, - content: Sy - } - }, - RequestBody: { - $visitor: Gy, - fixedFields: { - description: { $ref: '#/visitors/value' }, - content: Xy, - required: { $ref: '#/visitors/value' } - } - }, - MediaType: { - $visitor: Am, - fixedFields: { - schema: ng, - example: { $ref: '#/visitors/value' }, - examples: lg, - encoding: fg - } - }, - Encoding: { - $visitor: Wy, - fixedFields: { - contentType: { $ref: '#/visitors/value' }, - headers: Hy, - style: { $ref: '#/visitors/value' }, - explode: { $ref: '#/visitors/value' }, - allowReserved: { $ref: '#/visitors/value' } - } - }, - Responses: { $visitor: hv, fixedFields: { default: dv } }, - Response: { - $visitor: Qy, - fixedFields: { - description: { $ref: '#/visitors/value' }, - headers: tv, - content: nv, - links: ov - } - }, - Callback: { $visitor: Zy }, - Example: { - $visitor: Vy, - fixedFields: { - summary: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - value: { $ref: '#/visitors/value' }, - externalValue: Uy - } - }, - Link: { - $visitor: fm, - fixedFields: { - operationRef: mm, - operationId: gm, - parameters: _m, - requestBody: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - server: { $ref: '#/visitors/document/objects/Server' } - } - }, - Header: { - $visitor: zg, - fixedFields: { - description: { $ref: '#/visitors/value' }, - required: { $ref: '#/visitors/value' }, - deprecated: { $ref: '#/visitors/value' }, - allowEmptyValue: { $ref: '#/visitors/value' }, - style: { $ref: '#/visitors/value' }, - explode: { $ref: '#/visitors/value' }, - allowReserved: { $ref: '#/visitors/value' }, - schema: Wg, - example: { $ref: '#/visitors/value' }, - examples: Yg, - content: ey - } - }, - Tag: { - $visitor: xg, - fixedFields: { - name: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } - } - }, - Reference: { $visitor: kg, fixedFields: { $ref: qg } }, - JSONSchema: { $ref: '#/visitors/document/objects/Schema' }, - JSONReference: { $ref: '#/visitors/document/objects/Reference' }, - Schema: { - $visitor: ty, - fixedFields: { - title: qv.title, - multipleOf: qv.multipleOf, - maximum: qv.maximum, - exclusiveMaximum: qv.exclusiveMaximum, - minimum: qv.minimum, - exclusiveMinimum: qv.exclusiveMinimum, - maxLength: qv.maxLength, - minLength: qv.minLength, - pattern: qv.pattern, - maxItems: qv.maxItems, - minItems: qv.minItems, - uniqueItems: qv.uniqueItems, - maxProperties: qv.maxProperties, - minProperties: qv.minProperties, - required: qv.required, - enum: qv.enum, - type: dy, - allOf: ny, - anyOf: oy, - oneOf: ay, - not: my, - items: cy, - properties: py, - additionalProperties: my, - description: qv.description, - format: qv.format, - default: qv.default, - nullable: { $ref: '#/visitors/value' }, - discriminator: { $ref: '#/visitors/document/objects/Discriminator' }, - writeOnly: { $ref: '#/visitors/value' }, - xml: { $ref: '#/visitors/document/objects/XML' }, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, - example: { $ref: '#/visitors/value' }, - deprecated: { $ref: '#/visitors/value' } - } - }, - Discriminator: { - $visitor: gy, - fixedFields: { propertyName: { $ref: '#/visitors/value' }, mapping: vy } - }, - XML: { - $visitor: by, - fixedFields: { - name: { $ref: '#/visitors/value' }, - namespace: { $ref: '#/visitors/value' }, - prefix: { $ref: '#/visitors/value' }, - attribute: { $ref: '#/visitors/value' }, - wrapped: { $ref: '#/visitors/value' } - } - }, - SecurityScheme: { - $visitor: Tv, - fixedFields: { - type: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - name: { $ref: '#/visitors/value' }, - in: { $ref: '#/visitors/value' }, - scheme: { $ref: '#/visitors/value' }, - bearerFormat: { $ref: '#/visitors/value' }, - flows: { $ref: '#/visitors/document/objects/OAuthFlows' }, - openIdConnectUrl: { $ref: '#/visitors/value' } - } - }, - OAuthFlows: { - $visitor: Nv, - fixedFields: { - implicit: { $ref: '#/visitors/document/objects/OAuthFlow' }, - password: { $ref: '#/visitors/document/objects/OAuthFlow' }, - clientCredentials: { $ref: '#/visitors/document/objects/OAuthFlow' }, - authorizationCode: { $ref: '#/visitors/document/objects/OAuthFlow' } - } - }, - OAuthFlow: { - $visitor: Rv, - fixedFields: { - authorizationUrl: { $ref: '#/visitors/value' }, - tokenUrl: { $ref: '#/visitors/value' }, - refreshUrl: { $ref: '#/visitors/value' }, - scopes: Lv - } - }, - SecurityRequirement: { $visitor: mg } - }, - extension: { $visitor: cm } - } - } - }, - es_traversal_visitor_getNodeType = (s) => { - if (Nu(s)) return `${s.element.charAt(0).toUpperCase() + s.element.slice(1)}Element`; - }, - Vv = { - CallbackElement: ['content'], - ComponentsElement: ['content'], - ContactElement: ['content'], - DiscriminatorElement: ['content'], - Encoding: ['content'], - Example: ['content'], - ExternalDocumentationElement: ['content'], - HeaderElement: ['content'], - InfoElement: ['content'], - LicenseElement: ['content'], - MediaTypeElement: ['content'], - OAuthFlowElement: ['content'], - OAuthFlowsElement: ['content'], - OpenApi3_0Element: ['content'], - OperationElement: ['content'], - ParameterElement: ['content'], - PathItemElement: ['content'], - PathsElement: ['content'], - ReferenceElement: ['content'], - RequestBodyElement: ['content'], - ResponseElement: ['content'], - ResponsesElement: ['content'], - SchemaElement: ['content'], - SecurityRequirementElement: ['content'], - SecuritySchemeElement: ['content'], - ServerElement: ['content'], - ServerVariableElement: ['content'], - TagElement: ['content'], - ...Qu - }, - Uv = { - namespace: (s) => { - const { base: o } = s; - return ( - o.register('callback', Hp), - o.register('components', Jp), - o.register('contact', Gp), - o.register('discriminator', Yp), - o.register('encoding', Xp), - o.register('example', Zp), - o.register('externalDocumentation', Qp), - o.register('header', th), - o.register('info', rh), - o.register('license', uh), - o.register('link', dh), - o.register('mediaType', fh), - o.register('oAuthFlow', vh), - o.register('oAuthFlows', _h), - o.register('openapi', wh), - o.register('openApi3_0', Oh), - o.register('operation', jh), - o.register('parameter', Ih), - o.register('pathItem', Ph), - o.register('paths', Rh), - o.register('reference', Dh), - o.register('requestBody', Lh), - o.register('response', Fh), - o.register('responses', Kh), - o.register('schema', Wf), - o.register('securityRequirement', Hf), - o.register('securityScheme', Jf), - o.register('server', Gf), - o.register('serverVariable', Xf), - o.register('tag', Qf), - o.register('xml', em), - o - ); - } - }, - zv = Uv, - es_refractor_toolbox = () => { - const s = createNamespace(zv); - return { - predicates: { - ...pe, - isElement: Nu, - isStringElement: Ru, - isArrayElement: qu, - isObjectElement: Fu, - isMemberElement: $u, - includesClasses, - hasElementSourceMap - }, - namespace: s - }; - }, - es_refractor_refract = ( - s, - { - specPath: o = ['visitors', 'document', 'objects', 'OpenApi', '$visitor'], - plugins: i = [] - } = {} - ) => { - const u = (0, Cu.e)(s), - _ = dereference($v), - w = new (cp(o, _))({ specObj: _ }); - return ( - visitor_visit(u, w), - dispatchPluginsSync(w.element, i, { - toolboxCreator: es_refractor_toolbox, - visitorOptions: { keyMap: Vv, nodeTypeGetter: es_traversal_visitor_getNodeType } - }) - ); - }, - es_refractor_createRefractor = - (s) => - (o, i = {}) => - es_refractor_refract(o, { specPath: s, ...i }); - (Hp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Callback', - '$visitor' - ])), - (Jp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Components', - '$visitor' - ])), - (Gp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Contact', - '$visitor' - ])), - (Zp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Example', - '$visitor' - ])), - (Yp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Discriminator', - '$visitor' - ])), - (Xp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Encoding', - '$visitor' - ])), - (Qp.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'ExternalDocumentation', - '$visitor' - ])), - (th.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Header', - '$visitor' - ])), - (rh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Info', - '$visitor' - ])), - (uh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'License', - '$visitor' - ])), - (dh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Link', - '$visitor' - ])), - (fh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'MediaType', - '$visitor' - ])), - (vh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OAuthFlow', - '$visitor' - ])), - (_h.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OAuthFlows', - '$visitor' - ])), - (wh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OpenApi', - 'fixedFields', - 'openapi' - ])), - (Oh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OpenApi', - '$visitor' - ])), - (jh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Operation', - '$visitor' - ])), - (Ih.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Parameter', - '$visitor' - ])), - (Ph.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'PathItem', - '$visitor' - ])), - (Rh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Paths', - '$visitor' - ])), - (Dh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Reference', - '$visitor' - ])), - (Lh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'RequestBody', - '$visitor' - ])), - (Fh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Response', - '$visitor' - ])), - (Kh.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Responses', - '$visitor' - ])), - (Wf.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Schema', - '$visitor' - ])), - (Hf.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'SecurityRequirement', - '$visitor' - ])), - (Jf.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'SecurityScheme', - '$visitor' - ])), - (Gf.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Server', - '$visitor' - ])), - (Xf.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'ServerVariable', - '$visitor' - ])), - (Qf.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Tag', - '$visitor' - ])), - (em.refract = es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'XML', - '$visitor' - ])); - const Wv = class Callback_Callback extends Hp {}; - const Kv = class Components_Components extends Jp { - get pathItems() { - return this.get('pathItems'); - } - set pathItems(s) { - this.set('pathItems', s); - } - }; - const Hv = class Contact_Contact extends Gp {}; - const Jv = class Discriminator_Discriminator extends Yp {}; - const Gv = class Encoding_Encoding extends Xp {}; - const Yv = class Example_Example extends Zp {}; - const Xv = class ExternalDocumentation_ExternalDocumentation extends Qp {}; - const Zv = class Header_Header extends th { - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - }; - const Qv = class Info_Info extends rh { - get license() { - return this.get('license'); - } - set license(s) { - this.set('license', s); - } - get summary() { - return this.get('summary'); - } - set summary(s) { - this.set('summary', s); - } - }; - class JsonSchemaDialect extends Cu.Om { - static default = new JsonSchemaDialect('https://spec.openapis.org/oas/3.1/dialect/base'); - constructor(s, o, i) { - super(s, o, i), (this.element = 'jsonSchemaDialect'); - } - } - const eb = JsonSchemaDialect; - const tb = class License_License extends uh { - get identifier() { - return this.get('identifier'); - } - set identifier(s) { - this.set('identifier', s); - } - }; - const nb = class Link_Link extends dh {}; - const pb = class MediaType_MediaType extends fh { - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - }; - const mb = class OAuthFlow_OAuthFlow extends vh {}; - const yb = class OAuthFlows_OAuthFlows extends _h {}; - const _b = class Openapi_Openapi extends wh {}; - class OpenApi3_1 extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'openApi3_1'), this.classes.push('api'); - } - get openapi() { - return this.get('openapi'); - } - set openapi(s) { - this.set('openapi', s); - } - get info() { - return this.get('info'); - } - set info(s) { - this.set('info', s); - } - get jsonSchemaDialect() { - return this.get('jsonSchemaDialect'); - } - set jsonSchemaDialect(s) { - this.set('jsonSchemaDialect', s); - } - get servers() { - return this.get('servers'); - } - set servers(s) { - this.set('servers', s); - } - get paths() { - return this.get('paths'); - } - set paths(s) { - this.set('paths', s); - } - get components() { - return this.get('components'); - } - set components(s) { - this.set('components', s); - } - get security() { - return this.get('security'); - } - set security(s) { - this.set('security', s); - } - get tags() { - return this.get('tags'); - } - set tags(s) { - this.set('tags', s); - } - get externalDocs() { - return this.get('externalDocs'); - } - set externalDocs(s) { - this.set('externalDocs', s); - } - get webhooks() { - return this.get('webhooks'); - } - set webhooks(s) { - this.set('webhooks', s); - } - } - const wb = OpenApi3_1; - const Sb = class Operation_Operation extends jh { - get requestBody() { - return this.get('requestBody'); - } - set requestBody(s) { - this.set('requestBody', s); - } - }; - const Ob = class Parameter_Parameter extends Ih { - get schema() { - return this.get('schema'); - } - set schema(s) { - this.set('schema', s); - } - }; - const Ab = class PathItem_PathItem extends Ph { - get GET() { - return this.get('get'); - } - set GET(s) { - this.set('GET', s); - } - get PUT() { - return this.get('put'); - } - set PUT(s) { - this.set('PUT', s); - } - get POST() { - return this.get('post'); - } - set POST(s) { - this.set('POST', s); - } - get DELETE() { - return this.get('delete'); - } - set DELETE(s) { - this.set('DELETE', s); - } - get OPTIONS() { - return this.get('options'); - } - set OPTIONS(s) { - this.set('OPTIONS', s); - } - get HEAD() { - return this.get('head'); - } - set HEAD(s) { - this.set('HEAD', s); - } - get PATCH() { - return this.get('patch'); - } - set PATCH(s) { - this.set('PATCH', s); - } - get TRACE() { - return this.get('trace'); - } - set TRACE(s) { - this.set('TRACE', s); - } - }; - const Ib = class Paths_Paths extends Rh {}; - class Reference_Reference extends Dh {} - Object.defineProperty(Reference_Reference.prototype, 'description', { - get() { - return this.get('description'); - }, - set(s) { - this.set('description', s); - }, - enumerable: !0 - }), - Object.defineProperty(Reference_Reference.prototype, 'summary', { - get() { - return this.get('summary'); - }, - set(s) { - this.set('summary', s); - }, - enumerable: !0 - }); - const Pb = Reference_Reference; - const Mb = class RequestBody_RequestBody extends Lh {}; - const Rb = class elements_Response_Response extends Fh {}; - const Lb = class Responses_Responses extends Kh {}; - class elements_Schema_Schema extends Cu.Sh { - constructor(s, o, i) { - super(s, o, i), (this.element = 'schema'); - } - get $schema() { - return this.get('$schema'); - } - set $schema(s) { - this.set('$schema', s); - } - get $vocabulary() { - return this.get('$vocabulary'); - } - set $vocabulary(s) { - this.set('$vocabulary', s); - } - get $id() { - return this.get('$id'); - } - set $id(s) { - this.set('$id', s); - } - get $anchor() { - return this.get('$anchor'); - } - set $anchor(s) { - this.set('$anchor', s); - } - get $dynamicAnchor() { - return this.get('$dynamicAnchor'); - } - set $dynamicAnchor(s) { - this.set('$dynamicAnchor', s); - } - get $dynamicRef() { - return this.get('$dynamicRef'); - } - set $dynamicRef(s) { - this.set('$dynamicRef', s); - } - get $ref() { - return this.get('$ref'); - } - set $ref(s) { - this.set('$ref', s); - } - get $defs() { - return this.get('$defs'); - } - set $defs(s) { - this.set('$defs', s); - } - get $comment() { - return this.get('$comment'); - } - set $comment(s) { - this.set('$comment', s); - } - get allOf() { - return this.get('allOf'); - } - set allOf(s) { - this.set('allOf', s); - } - get anyOf() { - return this.get('anyOf'); - } - set anyOf(s) { - this.set('anyOf', s); - } - get oneOf() { - return this.get('oneOf'); - } - set oneOf(s) { - this.set('oneOf', s); - } - get not() { - return this.get('not'); - } - set not(s) { - this.set('not', s); - } - get if() { - return this.get('if'); - } - set if(s) { - this.set('if', s); - } - get then() { - return this.get('then'); - } - set then(s) { - this.set('then', s); - } - get else() { - return this.get('else'); - } - set else(s) { - this.set('else', s); - } - get dependentSchemas() { - return this.get('dependentSchemas'); - } - set dependentSchemas(s) { - this.set('dependentSchemas', s); - } - get prefixItems() { - return this.get('prefixItems'); - } - set prefixItems(s) { - this.set('prefixItems', s); - } - get items() { - return this.get('items'); - } - set items(s) { - this.set('items', s); - } - get containsProp() { - return this.get('contains'); - } - set containsProp(s) { - this.set('contains', s); - } - get properties() { - return this.get('properties'); - } - set properties(s) { - this.set('properties', s); - } - get patternProperties() { - return this.get('patternProperties'); - } - set patternProperties(s) { - this.set('patternProperties', s); - } - get additionalProperties() { - return this.get('additionalProperties'); - } - set additionalProperties(s) { - this.set('additionalProperties', s); - } - get propertyNames() { - return this.get('propertyNames'); - } - set propertyNames(s) { - this.set('propertyNames', s); - } - get unevaluatedItems() { - return this.get('unevaluatedItems'); - } - set unevaluatedItems(s) { - this.set('unevaluatedItems', s); - } - get unevaluatedProperties() { - return this.get('unevaluatedProperties'); - } - set unevaluatedProperties(s) { - this.set('unevaluatedProperties', s); - } - get type() { - return this.get('type'); - } - set type(s) { - this.set('type', s); - } - get enum() { - return this.get('enum'); - } - set enum(s) { - this.set('enum', s); - } - get const() { - return this.get('const'); - } - set const(s) { - this.set('const', s); - } - get multipleOf() { - return this.get('multipleOf'); - } - set multipleOf(s) { - this.set('multipleOf', s); - } - get maximum() { - return this.get('maximum'); - } - set maximum(s) { - this.set('maximum', s); - } - get exclusiveMaximum() { - return this.get('exclusiveMaximum'); - } - set exclusiveMaximum(s) { - this.set('exclusiveMaximum', s); - } - get minimum() { - return this.get('minimum'); - } - set minimum(s) { - this.set('minimum', s); - } - get exclusiveMinimum() { - return this.get('exclusiveMinimum'); - } - set exclusiveMinimum(s) { - this.set('exclusiveMinimum', s); - } - get maxLength() { - return this.get('maxLength'); - } - set maxLength(s) { - this.set('maxLength', s); - } - get minLength() { - return this.get('minLength'); - } - set minLength(s) { - this.set('minLength', s); - } - get pattern() { - return this.get('pattern'); - } - set pattern(s) { - this.set('pattern', s); - } - get maxItems() { - return this.get('maxItems'); - } - set maxItems(s) { - this.set('maxItems', s); - } - get minItems() { - return this.get('minItems'); - } - set minItems(s) { - this.set('minItems', s); - } - get uniqueItems() { - return this.get('uniqueItems'); - } - set uniqueItems(s) { - this.set('uniqueItems', s); - } - get maxContains() { - return this.get('maxContains'); - } - set maxContains(s) { - this.set('maxContains', s); - } - get minContains() { - return this.get('minContains'); - } - set minContains(s) { - this.set('minContains', s); - } - get maxProperties() { - return this.get('maxProperties'); - } - set maxProperties(s) { - this.set('maxProperties', s); - } - get minProperties() { - return this.get('minProperties'); - } - set minProperties(s) { - this.set('minProperties', s); - } - get required() { - return this.get('required'); - } - set required(s) { - this.set('required', s); - } - get dependentRequired() { - return this.get('dependentRequired'); - } - set dependentRequired(s) { - this.set('dependentRequired', s); - } - get title() { - return this.get('title'); - } - set title(s) { - this.set('title', s); - } - get description() { - return this.get('description'); - } - set description(s) { - this.set('description', s); - } - get default() { - return this.get('default'); - } - set default(s) { - this.set('default', s); - } - get deprecated() { - return this.get('deprecated'); - } - set deprecated(s) { - this.set('deprecated', s); - } - get readOnly() { - return this.get('readOnly'); - } - set readOnly(s) { - this.set('readOnly', s); - } - get writeOnly() { - return this.get('writeOnly'); - } - set writeOnly(s) { - this.set('writeOnly', s); - } - get examples() { - return this.get('examples'); - } - set examples(s) { - this.set('examples', s); - } - get format() { - return this.get('format'); - } - set format(s) { - this.set('format', s); - } - get contentEncoding() { - return this.get('contentEncoding'); - } - set contentEncoding(s) { - this.set('contentEncoding', s); - } - get contentMediaType() { - return this.get('contentMediaType'); - } - set contentMediaType(s) { - this.set('contentMediaType', s); - } - get contentSchema() { - return this.get('contentSchema'); - } - set contentSchema(s) { - this.set('contentSchema', s); - } - get discriminator() { - return this.get('discriminator'); - } - set discriminator(s) { - this.set('discriminator', s); - } - get xml() { - return this.get('xml'); - } - set xml(s) { - this.set('xml', s); - } - get externalDocs() { - return this.get('externalDocs'); - } - set externalDocs(s) { - this.set('externalDocs', s); - } - get example() { - return this.get('example'); - } - set example(s) { - this.set('example', s); - } - } - const qb = elements_Schema_Schema; - const zb = class SecurityRequirement_SecurityRequirement extends Hf {}; - const Qb = class SecurityScheme_SecurityScheme extends Jf {}; - const e_ = class Server_Server extends Gf {}; - const t_ = class ServerVariable_ServerVariable extends Xf {}; - const r_ = class Tag_Tag extends Qf {}; - const n_ = class Xml_Xml extends em {}; - class OpenApi3_1Visitor extends Mixin(im, rm) { - constructor(s) { - super(s), - (this.element = new wb()), - (this.specPath = Tl(['document', 'objects', 'OpenApi'])), - (this.canSupportSpecificationExtensions = !0), - (this.openApiSemanticElement = this.element); - } - ObjectElement(s) { - return (this.openApiGenericElement = s), im.prototype.ObjectElement.call(this, s); - } - } - const s_ = OpenApi3_1Visitor, - { - visitors: { - document: { - objects: { - Info: { $visitor: o_ } - } - } - } - } = $v; - const i_ = class info_InfoVisitor extends o_ { - constructor(s) { - super(s), (this.element = new Qv()); - } - }, - { - visitors: { - document: { - objects: { - Contact: { $visitor: a_ } - } - } - } - } = $v; - const l_ = class contact_ContactVisitor extends a_ { - constructor(s) { - super(s), (this.element = new Hv()); - } - }, - { - visitors: { - document: { - objects: { - License: { $visitor: c_ } - } - } - } - } = $v; - const u_ = class license_LicenseVisitor extends c_ { - constructor(s) { - super(s), (this.element = new tb()); - } - }, - { - visitors: { - document: { - objects: { - Link: { $visitor: p_ } - } - } - } - } = $v; - const h_ = class link_LinkVisitor extends p_ { - constructor(s) { - super(s), (this.element = new nb()); - } - }; - class JsonSchemaDialectVisitor extends Mixin(nm, rm) { - StringElement(s) { - const o = new eb(serializers_value(s)); - return this.copyMetaAndAttributes(s, o), (this.element = o), Ju; - } - } - const d_ = JsonSchemaDialectVisitor, - { - visitors: { - document: { - objects: { - Server: { $visitor: f_ } - } - } - } - } = $v; - const m_ = class server_ServerVisitor extends f_ { - constructor(s) { - super(s), (this.element = new e_()); - } - }, - { - visitors: { - document: { - objects: { - ServerVariable: { $visitor: g_ } - } - } - } - } = $v; - const y_ = class server_variable_ServerVariableVisitor extends g_ { - constructor(s) { - super(s), (this.element = new t_()); - } - }, - { - visitors: { - document: { - objects: { - MediaType: { $visitor: v_ } - } - } - } - } = $v; - const b_ = class media_type_MediaTypeVisitor extends v_ { - constructor(s) { - super(s), (this.element = new pb()); - } - }, - { - visitors: { - document: { - objects: { - SecurityRequirement: { $visitor: E_ } - } - } - } - } = $v; - const w_ = class security_requirement_SecurityRequirementVisitor extends E_ { - constructor(s) { - super(s), (this.element = new zb()); - } - }, - { - visitors: { - document: { - objects: { - Components: { $visitor: S_ } - } - } - } - } = $v; - const x_ = class components_ComponentsVisitor extends S_ { - constructor(s) { - super(s), (this.element = new Kv()); - } - }, - { - visitors: { - document: { - objects: { - Tag: { $visitor: k_ } - } - } - } - } = $v; - const C_ = class tag_TagVisitor extends k_ { - constructor(s) { - super(s), (this.element = new r_()); - } - }, - { - visitors: { - document: { - objects: { - Reference: { $visitor: O_ } - } - } - } - } = $v; - const A_ = class reference_ReferenceVisitor extends O_ { - constructor(s) { - super(s), (this.element = new Pb()); - } - }, - { - visitors: { - document: { - objects: { - Parameter: { $visitor: j_ } - } - } - } - } = $v; - const I_ = class parameter_ParameterVisitor extends j_ { - constructor(s) { - super(s), (this.element = new Ob()); - } - }, - { - visitors: { - document: { - objects: { - Header: { $visitor: P_ } - } - } - } - } = $v; - const M_ = class header_HeaderVisitor extends P_ { - constructor(s) { - super(s), (this.element = new Zv()); - } - }, - T_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Wv || (s(u) && o('callback', u) && i('object', u)) - ), - N_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Kv || (s(u) && o('components', u) && i('object', u)) - ), - R_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Hv || (s(u) && o('contact', u) && i('object', u)) - ), - D_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Yv || (s(u) && o('example', u) && i('object', u)) - ), - L_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Xv || (s(u) && o('externalDocumentation', u) && i('object', u)) - ), - B_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Zv || (s(u) && o('header', u) && i('object', u)) - ), - F_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Qv || (s(u) && o('info', u) && i('object', u)) - ), - q_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof eb || (s(u) && o('jsonSchemaDialect', u) && i('string', u)) - ), - $_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof tb || (s(u) && o('license', u) && i('object', u)) - ), - V_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof nb || (s(u) && o('link', u) && i('object', u)) - ), - U_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof _b || (s(u) && o('openapi', u) && i('string', u)) - ), - z_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i, hasClass: u }) => - (_) => - _ instanceof wb || (s(_) && o('openApi3_1', _) && i('object', _) && u('api', _)) - ), - W_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Sb || (s(u) && o('operation', u) && i('object', u)) - ), - K_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Ob || (s(u) && o('parameter', u) && i('object', u)) - ), - H_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Ab || (s(u) && o('pathItem', u) && i('object', u)) - ), - isPathItemElementExternal = (s) => { - if (!H_(s)) return !1; - if (!Ru(s.$ref)) return !1; - const o = serializers_value(s.$ref); - return 'string' == typeof o && o.length > 0 && !o.startsWith('#'); - }, - J_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Ib || (s(u) && o('paths', u) && i('object', u)) - ), - G_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Pb || (s(u) && o('reference', u) && i('object', u)) - ), - isReferenceElementExternal = (s) => { - if (!G_(s)) return !1; - if (!Ru(s.$ref)) return !1; - const o = serializers_value(s.$ref); - return 'string' == typeof o && o.length > 0 && !o.startsWith('#'); - }, - Y_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Mb || (s(u) && o('requestBody', u) && i('object', u)) - ), - X_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Rb || (s(u) && o('response', u) && i('object', u)) - ), - Z_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Lb || (s(u) && o('responses', u) && i('object', u)) - ), - Q_ = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof qb || (s(u) && o('schema', u) && i('object', u)) - ), - predicates_isBooleanJsonSchemaElement = (s) => - Bu(s) && s.classes.includes('boolean-json-schema'), - eE = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof zb || (s(u) && o('securityRequirement', u) && i('object', u)) - ), - tE = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof Qb || (s(u) && o('securityScheme', u) && i('object', u)) - ), - rE = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof e_ || (s(u) && o('server', u) && i('object', u)) - ), - nE = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof t_ || (s(u) && o('serverVariable', u) && i('object', u)) - ), - sE = helpers( - ({ hasBasicElementProps: s, isElementType: o, primitiveEq: i }) => - (u) => - u instanceof pb || (s(u) && o('mediaType', u) && i('object', u)) - ); - const oE = class ParentSchemaAwareVisitor_ParentSchemaAwareVisitor { - parent; - constructor({ parent: s }) { - this.parent = s; - } - }; - class open_api_3_1_schema_SchemaVisitor extends Mixin(im, oE, rm) { - constructor(s) { - super(s), - (this.element = new qb()), - (this.specPath = Tl(['document', 'objects', 'Schema'])), - (this.canSupportSpecificationExtensions = !0), - (this.jsonSchemaDefaultDialect = eb.default), - this.passingOptionsNames.push('parent'); - } - ObjectElement(s) { - this.handle$schema(s), this.handle$id(s), (this.parent = this.element); - const o = im.prototype.ObjectElement.call(this, s); - return ( - Ru(this.element.$ref) && - (this.element.classes.push('reference-element'), - this.element.setMetaProperty('referenced-element', 'schema')), - o - ); - } - BooleanElement(s) { - const o = super.enter(s); - return this.element.classes.push('boolean-json-schema'), o; - } - getJsonSchemaDialect() { - let s; - return ( - (s = - void 0 !== this.openApiSemanticElement && - q_(this.openApiSemanticElement.jsonSchemaDialect) - ? serializers_value(this.openApiSemanticElement.jsonSchemaDialect) - : void 0 !== this.openApiGenericElement && - Ru(this.openApiGenericElement.get('jsonSchemaDialect')) - ? serializers_value(this.openApiGenericElement.get('jsonSchemaDialect')) - : serializers_value(this.jsonSchemaDefaultDialect)), - s - ); - } - handle$schema(s) { - if (Rl(this.parent) && !Ru(s.get('$schema'))) - this.element.setMetaProperty('inherited$schema', this.getJsonSchemaDialect()); - else if (Q_(this.parent) && !Ru(s.get('$schema'))) { - const s = Na( - serializers_value(this.parent.meta.get('inherited$schema')), - serializers_value(this.parent.$schema) - ); - this.element.setMetaProperty('inherited$schema', s); - } - } - handle$id(s) { - const o = - void 0 !== this.parent - ? cloneDeep(this.parent.getMetaProperty('inherited$id', [])) - : new Cu.wE(), - i = serializers_value(s.get('$id')); - Vd(i) && o.push(i), this.element.setMetaProperty('inherited$id', o); - } - } - const iE = open_api_3_1_schema_SchemaVisitor; - const aE = class $vocabularyVisitor extends rm { - ObjectElement(s) { - const o = super.enter(s); - return this.element.classes.push('json-schema-$vocabulary'), o; - } - }; - const lE = class $refVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('reference-value'), o; - } - }; - class $defsVisitor extends Mixin(vm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-$defs'), - (this.specPath = Tl(['document', 'objects', 'Schema'])), - this.passingOptionsNames.push('parent'); - } - } - const cE = $defsVisitor; - class schema_AllOfVisitor_AllOfVisitor extends Mixin(nm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.wE()), - this.element.classes.push('json-schema-allOf'), - this.passingOptionsNames.push('parent'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - if (Fu(s)) { - const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); - this.element.push(o); - } else { - const o = cloneDeep(s); - this.element.push(o); - } - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const uE = schema_AllOfVisitor_AllOfVisitor; - class schema_AnyOfVisitor_AnyOfVisitor extends Mixin(nm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.wE()), - this.element.classes.push('json-schema-anyOf'), - this.passingOptionsNames.push('parent'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - if (Fu(s)) { - const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); - this.element.push(o); - } else { - const o = cloneDeep(s); - this.element.push(o); - } - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const pE = schema_AnyOfVisitor_AnyOfVisitor; - class schema_OneOfVisitor_OneOfVisitor extends Mixin(nm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.wE()), - this.element.classes.push('json-schema-oneOf'), - this.passingOptionsNames.push('parent'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - if (Fu(s)) { - const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); - this.element.push(o); - } else { - const o = cloneDeep(s); - this.element.push(o); - } - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const hE = schema_OneOfVisitor_OneOfVisitor; - class DependentSchemasVisitor extends Mixin(vm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-dependentSchemas'), - (this.specPath = Tl(['document', 'objects', 'Schema'])), - this.passingOptionsNames.push('parent'); - } - } - const dE = DependentSchemasVisitor; - class PrefixItemsVisitor extends Mixin(nm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.wE()), - this.element.classes.push('json-schema-prefixItems'), - this.passingOptionsNames.push('parent'); - } - ArrayElement(s) { - return ( - s.forEach((s) => { - if (Fu(s)) { - const o = this.toRefractedElement(['document', 'objects', 'Schema'], s); - this.element.push(o); - } else { - const o = cloneDeep(s); - this.element.push(o); - } - }), - this.copyMetaAndAttributes(s, this.element), - Ju - ); - } - } - const fE = PrefixItemsVisitor; - class schema_PropertiesVisitor_PropertiesVisitor extends Mixin(vm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-properties'), - (this.specPath = Tl(['document', 'objects', 'Schema'])), - this.passingOptionsNames.push('parent'); - } - } - const mE = schema_PropertiesVisitor_PropertiesVisitor; - class PatternPropertiesVisitor_PatternPropertiesVisitor extends Mixin(vm, oE, rm) { - constructor(s) { - super(s), - (this.element = new Cu.Sh()), - this.element.classes.push('json-schema-patternProperties'), - (this.specPath = Tl(['document', 'objects', 'Schema'])), - this.passingOptionsNames.push('parent'); - } - } - const gE = PatternPropertiesVisitor_PatternPropertiesVisitor; - const yE = class schema_TypeVisitor_TypeVisitor extends rm { - StringElement(s) { - const o = super.enter(s); - return this.element.classes.push('json-schema-type'), o; - } - ArrayElement(s) { - const o = super.enter(s); - return this.element.classes.push('json-schema-type'), o; - } - }; - const vE = class EnumVisitor_EnumVisitor extends rm { - ArrayElement(s) { - const o = super.enter(s); - return this.element.classes.push('json-schema-enum'), o; - } - }; - const bE = class DependentRequiredVisitor extends rm { - ObjectElement(s) { - const o = super.enter(s); - return this.element.classes.push('json-schema-dependentRequired'), o; - } - }; - const _E = class schema_ExamplesVisitor_ExamplesVisitor extends rm { - ArrayElement(s) { - const o = super.enter(s); - return this.element.classes.push('json-schema-examples'), o; - } - }, - { - visitors: { - document: { - objects: { - Discriminator: { $visitor: EE } - } - } - } - } = $v; - const wE = class distriminator_DiscriminatorVisitor extends EE { - constructor(s) { - super(s), (this.element = new Jv()), (this.canSupportSpecificationExtensions = !0); - } - }, - { - visitors: { - document: { - objects: { - XML: { $visitor: SE } - } - } - } - } = $v; - const xE = class xml_XmlVisitor extends SE { - constructor(s) { - super(s), (this.element = new n_()); - } - }; - class SchemasVisitor_SchemasVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new xy()), - (this.specPath = Tl(['document', 'objects', 'Schema'])); - } - } - const kE = SchemasVisitor_SchemasVisitor; - class ComponentsPathItems extends Cu.Sh { - static primaryClass = 'components-path-items'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(ComponentsPathItems.primaryClass); - } - } - const CE = ComponentsPathItems; - class PathItemsVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new CE()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'PathItem']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(G_).forEach((s) => { - s.setMetaProperty('referenced-element', 'pathItem'); - }), - o - ); - } - } - const OE = PathItemsVisitor, - { - visitors: { - document: { - objects: { - Example: { $visitor: AE } - } - } - } - } = $v; - const jE = class example_ExampleVisitor extends AE { - constructor(s) { - super(s), (this.element = new Yv()); - } - }, - { - visitors: { - document: { - objects: { - ExternalDocumentation: { $visitor: IE } - } - } - } - } = $v; - const PE = class external_documentation_ExternalDocumentationVisitor extends IE { - constructor(s) { - super(s), (this.element = new Xv()); - } - }, - { - visitors: { - document: { - objects: { - Encoding: { $visitor: ME } - } - } - } - } = $v; - const TE = class open_api_3_1_encoding_EncodingVisitor extends ME { - constructor(s) { - super(s), (this.element = new Gv()); - } - }, - { - visitors: { - document: { - objects: { - Paths: { $visitor: NE } - } - } - } - } = $v; - const RE = class paths_PathsVisitor extends NE { - constructor(s) { - super(s), (this.element = new Ib()); - } - }, - { - visitors: { - document: { - objects: { - RequestBody: { $visitor: DE } - } - } - } - } = $v; - const LE = class request_body_RequestBodyVisitor extends DE { - constructor(s) { - super(s), (this.element = new Mb()); - } - }, - { - visitors: { - document: { - objects: { - Callback: { $visitor: BE } - } - } - } - } = $v; - const FE = class callback_CallbackVisitor extends BE { - constructor(s) { - super(s), - (this.element = new Wv()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'PathItem']); - } - ObjectElement(s) { - const o = BE.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(G_).forEach((s) => { - s.setMetaProperty('referenced-element', 'pathItem'); - }), - o - ); - } - }, - { - visitors: { - document: { - objects: { - Response: { $visitor: qE } - } - } - } - } = $v; - const $E = class response_ResponseVisitor extends qE { - constructor(s) { - super(s), (this.element = new Rb()); - } - }, - { - visitors: { - document: { - objects: { - Responses: { $visitor: VE } - } - } - } - } = $v; - const UE = class open_api_3_1_responses_ResponsesVisitor extends VE { - constructor(s) { - super(s), (this.element = new Lb()); - } - }, - { - visitors: { - document: { - objects: { - Operation: { $visitor: zE } - } - } - } - } = $v; - const WE = class operation_OperationVisitor extends zE { - constructor(s) { - super(s), (this.element = new Sb()); - } - }, - { - visitors: { - document: { - objects: { - PathItem: { $visitor: KE } - } - } - } - } = $v; - const HE = class path_item_PathItemVisitor extends KE { - constructor(s) { - super(s), (this.element = new Ab()); - } - }, - { - visitors: { - document: { - objects: { - SecurityScheme: { $visitor: JE } - } - } - } - } = $v; - const GE = class security_scheme_SecuritySchemeVisitor extends JE { - constructor(s) { - super(s), (this.element = new Qb()); - } - }, - { - visitors: { - document: { - objects: { - OAuthFlows: { $visitor: YE } - } - } - } - } = $v; - const XE = class oauth_flows_OAuthFlowsVisitor extends YE { - constructor(s) { - super(s), (this.element = new yb()); - } - }, - { - visitors: { - document: { - objects: { - OAuthFlow: { $visitor: ZE } - } - } - } - } = $v; - const QE = class oauth_flow_OAuthFlowVisitor extends ZE { - constructor(s) { - super(s), (this.element = new mb()); - } - }; - class Webhooks extends Cu.Sh { - static primaryClass = 'webhooks'; - constructor(s, o, i) { - super(s, o, i), this.classes.push(Webhooks.primaryClass); - } - } - const ew = Webhooks; - class WebhooksVisitor extends Mixin(vm, rm) { - constructor(s) { - super(s), - (this.element = new ew()), - (this.specPath = (s) => - isReferenceLikeElement(s) - ? ['document', 'objects', 'Reference'] - : ['document', 'objects', 'PathItem']); - } - ObjectElement(s) { - const o = vm.prototype.ObjectElement.call(this, s); - return ( - this.element.filter(G_).forEach((s) => { - s.setMetaProperty('referenced-element', 'pathItem'); - }), - this.element.filter(H_).forEach((s, o) => { - s.setMetaProperty('webhook-name', serializers_value(o)); - }), - o - ); - } - } - const tw = WebhooksVisitor, - rw = { - visitors: { - value: $v.visitors.value, - document: { - objects: { - OpenApi: { - $visitor: s_, - fixedFields: { - openapi: $v.visitors.document.objects.OpenApi.fixedFields.openapi, - info: { $ref: '#/visitors/document/objects/Info' }, - jsonSchemaDialect: d_, - servers: $v.visitors.document.objects.OpenApi.fixedFields.servers, - paths: { $ref: '#/visitors/document/objects/Paths' }, - webhooks: tw, - components: { $ref: '#/visitors/document/objects/Components' }, - security: $v.visitors.document.objects.OpenApi.fixedFields.security, - tags: $v.visitors.document.objects.OpenApi.fixedFields.tags, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } - } - }, - Info: { - $visitor: i_, - fixedFields: { - title: $v.visitors.document.objects.Info.fixedFields.title, - description: $v.visitors.document.objects.Info.fixedFields.description, - summary: { $ref: '#/visitors/value' }, - termsOfService: $v.visitors.document.objects.Info.fixedFields.termsOfService, - contact: { $ref: '#/visitors/document/objects/Contact' }, - license: { $ref: '#/visitors/document/objects/License' }, - version: $v.visitors.document.objects.Info.fixedFields.version - } - }, - Contact: { - $visitor: l_, - fixedFields: { - name: $v.visitors.document.objects.Contact.fixedFields.name, - url: $v.visitors.document.objects.Contact.fixedFields.url, - email: $v.visitors.document.objects.Contact.fixedFields.email - } - }, - License: { - $visitor: u_, - fixedFields: { - name: $v.visitors.document.objects.License.fixedFields.name, - identifier: { $ref: '#/visitors/value' }, - url: $v.visitors.document.objects.License.fixedFields.url - } - }, - Server: { - $visitor: m_, - fixedFields: { - url: $v.visitors.document.objects.Server.fixedFields.url, - description: $v.visitors.document.objects.Server.fixedFields.description, - variables: $v.visitors.document.objects.Server.fixedFields.variables - } - }, - ServerVariable: { - $visitor: y_, - fixedFields: { - enum: $v.visitors.document.objects.ServerVariable.fixedFields.enum, - default: $v.visitors.document.objects.ServerVariable.fixedFields.default, - description: - $v.visitors.document.objects.ServerVariable.fixedFields.description - } - }, - Components: { - $visitor: x_, - fixedFields: { - schemas: kE, - responses: $v.visitors.document.objects.Components.fixedFields.responses, - parameters: $v.visitors.document.objects.Components.fixedFields.parameters, - examples: $v.visitors.document.objects.Components.fixedFields.examples, - requestBodies: - $v.visitors.document.objects.Components.fixedFields.requestBodies, - headers: $v.visitors.document.objects.Components.fixedFields.headers, - securitySchemes: - $v.visitors.document.objects.Components.fixedFields.securitySchemes, - links: $v.visitors.document.objects.Components.fixedFields.links, - callbacks: $v.visitors.document.objects.Components.fixedFields.callbacks, - pathItems: OE - } - }, - Paths: { $visitor: RE }, - PathItem: { - $visitor: HE, - fixedFields: { - $ref: $v.visitors.document.objects.PathItem.fixedFields.$ref, - summary: $v.visitors.document.objects.PathItem.fixedFields.summary, - description: $v.visitors.document.objects.PathItem.fixedFields.description, - get: { $ref: '#/visitors/document/objects/Operation' }, - put: { $ref: '#/visitors/document/objects/Operation' }, - post: { $ref: '#/visitors/document/objects/Operation' }, - delete: { $ref: '#/visitors/document/objects/Operation' }, - options: { $ref: '#/visitors/document/objects/Operation' }, - head: { $ref: '#/visitors/document/objects/Operation' }, - patch: { $ref: '#/visitors/document/objects/Operation' }, - trace: { $ref: '#/visitors/document/objects/Operation' }, - servers: $v.visitors.document.objects.PathItem.fixedFields.servers, - parameters: $v.visitors.document.objects.PathItem.fixedFields.parameters - } - }, - Operation: { - $visitor: WE, - fixedFields: { - tags: $v.visitors.document.objects.Operation.fixedFields.tags, - summary: $v.visitors.document.objects.Operation.fixedFields.summary, - description: $v.visitors.document.objects.Operation.fixedFields.description, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, - operationId: $v.visitors.document.objects.Operation.fixedFields.operationId, - parameters: $v.visitors.document.objects.Operation.fixedFields.parameters, - requestBody: $v.visitors.document.objects.Operation.fixedFields.requestBody, - responses: { $ref: '#/visitors/document/objects/Responses' }, - callbacks: $v.visitors.document.objects.Operation.fixedFields.callbacks, - deprecated: $v.visitors.document.objects.Operation.fixedFields.deprecated, - security: $v.visitors.document.objects.Operation.fixedFields.security, - servers: $v.visitors.document.objects.Operation.fixedFields.servers - } - }, - ExternalDocumentation: { - $visitor: PE, - fixedFields: { - description: - $v.visitors.document.objects.ExternalDocumentation.fixedFields.description, - url: $v.visitors.document.objects.ExternalDocumentation.fixedFields.url - } - }, - Parameter: { - $visitor: I_, - fixedFields: { - name: $v.visitors.document.objects.Parameter.fixedFields.name, - in: $v.visitors.document.objects.Parameter.fixedFields.in, - description: $v.visitors.document.objects.Parameter.fixedFields.description, - required: $v.visitors.document.objects.Parameter.fixedFields.required, - deprecated: $v.visitors.document.objects.Parameter.fixedFields.deprecated, - allowEmptyValue: - $v.visitors.document.objects.Parameter.fixedFields.allowEmptyValue, - style: $v.visitors.document.objects.Parameter.fixedFields.style, - explode: $v.visitors.document.objects.Parameter.fixedFields.explode, - allowReserved: - $v.visitors.document.objects.Parameter.fixedFields.allowReserved, - schema: { $ref: '#/visitors/document/objects/Schema' }, - example: $v.visitors.document.objects.Parameter.fixedFields.example, - examples: $v.visitors.document.objects.Parameter.fixedFields.examples, - content: $v.visitors.document.objects.Parameter.fixedFields.content - } - }, - RequestBody: { - $visitor: LE, - fixedFields: { - description: $v.visitors.document.objects.RequestBody.fixedFields.description, - content: $v.visitors.document.objects.RequestBody.fixedFields.content, - required: $v.visitors.document.objects.RequestBody.fixedFields.required - } - }, - MediaType: { - $visitor: b_, - fixedFields: { - schema: { $ref: '#/visitors/document/objects/Schema' }, - example: $v.visitors.document.objects.MediaType.fixedFields.example, - examples: $v.visitors.document.objects.MediaType.fixedFields.examples, - encoding: $v.visitors.document.objects.MediaType.fixedFields.encoding - } - }, - Encoding: { - $visitor: TE, - fixedFields: { - contentType: $v.visitors.document.objects.Encoding.fixedFields.contentType, - headers: $v.visitors.document.objects.Encoding.fixedFields.headers, - style: $v.visitors.document.objects.Encoding.fixedFields.style, - explode: $v.visitors.document.objects.Encoding.fixedFields.explode, - allowReserved: $v.visitors.document.objects.Encoding.fixedFields.allowReserved - } - }, - Responses: { - $visitor: UE, - fixedFields: { - default: $v.visitors.document.objects.Responses.fixedFields.default - } - }, - Response: { - $visitor: $E, - fixedFields: { - description: $v.visitors.document.objects.Response.fixedFields.description, - headers: $v.visitors.document.objects.Response.fixedFields.headers, - content: $v.visitors.document.objects.Response.fixedFields.content, - links: $v.visitors.document.objects.Response.fixedFields.links - } - }, - Callback: { $visitor: FE }, - Example: { - $visitor: jE, - fixedFields: { - summary: $v.visitors.document.objects.Example.fixedFields.summary, - description: $v.visitors.document.objects.Example.fixedFields.description, - value: $v.visitors.document.objects.Example.fixedFields.value, - externalValue: $v.visitors.document.objects.Example.fixedFields.externalValue - } - }, - Link: { - $visitor: h_, - fixedFields: { - operationRef: $v.visitors.document.objects.Link.fixedFields.operationRef, - operationId: $v.visitors.document.objects.Link.fixedFields.operationId, - parameters: $v.visitors.document.objects.Link.fixedFields.parameters, - requestBody: $v.visitors.document.objects.Link.fixedFields.requestBody, - description: $v.visitors.document.objects.Link.fixedFields.description, - server: { $ref: '#/visitors/document/objects/Server' } - } - }, - Header: { - $visitor: M_, - fixedFields: { - description: $v.visitors.document.objects.Header.fixedFields.description, - required: $v.visitors.document.objects.Header.fixedFields.required, - deprecated: $v.visitors.document.objects.Header.fixedFields.deprecated, - allowEmptyValue: - $v.visitors.document.objects.Header.fixedFields.allowEmptyValue, - style: $v.visitors.document.objects.Header.fixedFields.style, - explode: $v.visitors.document.objects.Header.fixedFields.explode, - allowReserved: $v.visitors.document.objects.Header.fixedFields.allowReserved, - schema: { $ref: '#/visitors/document/objects/Schema' }, - example: $v.visitors.document.objects.Header.fixedFields.example, - examples: $v.visitors.document.objects.Header.fixedFields.examples, - content: $v.visitors.document.objects.Header.fixedFields.content - } - }, - Tag: { - $visitor: C_, - fixedFields: { - name: $v.visitors.document.objects.Tag.fixedFields.name, - description: $v.visitors.document.objects.Tag.fixedFields.description, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' } - } - }, - Reference: { - $visitor: A_, - fixedFields: { - $ref: $v.visitors.document.objects.Reference.fixedFields.$ref, - summary: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' } - } - }, - Schema: { - $visitor: iE, - fixedFields: { - $schema: { $ref: '#/visitors/value' }, - $vocabulary: aE, - $id: { $ref: '#/visitors/value' }, - $anchor: { $ref: '#/visitors/value' }, - $dynamicAnchor: { $ref: '#/visitors/value' }, - $dynamicRef: { $ref: '#/visitors/value' }, - $ref: lE, - $defs: cE, - $comment: { $ref: '#/visitors/value' }, - allOf: uE, - anyOf: pE, - oneOf: hE, - not: { $ref: '#/visitors/document/objects/Schema' }, - if: { $ref: '#/visitors/document/objects/Schema' }, - then: { $ref: '#/visitors/document/objects/Schema' }, - else: { $ref: '#/visitors/document/objects/Schema' }, - dependentSchemas: dE, - prefixItems: fE, - items: { $ref: '#/visitors/document/objects/Schema' }, - contains: { $ref: '#/visitors/document/objects/Schema' }, - properties: mE, - patternProperties: gE, - additionalProperties: { $ref: '#/visitors/document/objects/Schema' }, - propertyNames: { $ref: '#/visitors/document/objects/Schema' }, - unevaluatedItems: { $ref: '#/visitors/document/objects/Schema' }, - unevaluatedProperties: { $ref: '#/visitors/document/objects/Schema' }, - type: yE, - enum: vE, - const: { $ref: '#/visitors/value' }, - multipleOf: { $ref: '#/visitors/value' }, - maximum: { $ref: '#/visitors/value' }, - exclusiveMaximum: { $ref: '#/visitors/value' }, - minimum: { $ref: '#/visitors/value' }, - exclusiveMinimum: { $ref: '#/visitors/value' }, - maxLength: { $ref: '#/visitors/value' }, - minLength: { $ref: '#/visitors/value' }, - pattern: { $ref: '#/visitors/value' }, - maxItems: { $ref: '#/visitors/value' }, - minItems: { $ref: '#/visitors/value' }, - uniqueItems: { $ref: '#/visitors/value' }, - maxContains: { $ref: '#/visitors/value' }, - minContains: { $ref: '#/visitors/value' }, - maxProperties: { $ref: '#/visitors/value' }, - minProperties: { $ref: '#/visitors/value' }, - required: { $ref: '#/visitors/value' }, - dependentRequired: bE, - title: { $ref: '#/visitors/value' }, - description: { $ref: '#/visitors/value' }, - default: { $ref: '#/visitors/value' }, - deprecated: { $ref: '#/visitors/value' }, - readOnly: { $ref: '#/visitors/value' }, - writeOnly: { $ref: '#/visitors/value' }, - examples: _E, - format: { $ref: '#/visitors/value' }, - contentEncoding: { $ref: '#/visitors/value' }, - contentMediaType: { $ref: '#/visitors/value' }, - contentSchema: { $ref: '#/visitors/document/objects/Schema' }, - discriminator: { $ref: '#/visitors/document/objects/Discriminator' }, - xml: { $ref: '#/visitors/document/objects/XML' }, - externalDocs: { $ref: '#/visitors/document/objects/ExternalDocumentation' }, - example: { $ref: '#/visitors/value' } - } - }, - Discriminator: { - $visitor: wE, - fixedFields: { - propertyName: - $v.visitors.document.objects.Discriminator.fixedFields.propertyName, - mapping: $v.visitors.document.objects.Discriminator.fixedFields.mapping - } - }, - XML: { - $visitor: xE, - fixedFields: { - name: $v.visitors.document.objects.XML.fixedFields.name, - namespace: $v.visitors.document.objects.XML.fixedFields.namespace, - prefix: $v.visitors.document.objects.XML.fixedFields.prefix, - attribute: $v.visitors.document.objects.XML.fixedFields.attribute, - wrapped: $v.visitors.document.objects.XML.fixedFields.wrapped - } - }, - SecurityScheme: { - $visitor: GE, - fixedFields: { - type: $v.visitors.document.objects.SecurityScheme.fixedFields.type, - description: - $v.visitors.document.objects.SecurityScheme.fixedFields.description, - name: $v.visitors.document.objects.SecurityScheme.fixedFields.name, - in: $v.visitors.document.objects.SecurityScheme.fixedFields.in, - scheme: $v.visitors.document.objects.SecurityScheme.fixedFields.scheme, - bearerFormat: - $v.visitors.document.objects.SecurityScheme.fixedFields.bearerFormat, - flows: { $ref: '#/visitors/document/objects/OAuthFlows' }, - openIdConnectUrl: - $v.visitors.document.objects.SecurityScheme.fixedFields.openIdConnectUrl - } - }, - OAuthFlows: { - $visitor: XE, - fixedFields: { - implicit: { $ref: '#/visitors/document/objects/OAuthFlow' }, - password: { $ref: '#/visitors/document/objects/OAuthFlow' }, - clientCredentials: { $ref: '#/visitors/document/objects/OAuthFlow' }, - authorizationCode: { $ref: '#/visitors/document/objects/OAuthFlow' } - } - }, - OAuthFlow: { - $visitor: QE, - fixedFields: { - authorizationUrl: - $v.visitors.document.objects.OAuthFlow.fixedFields.authorizationUrl, - tokenUrl: $v.visitors.document.objects.OAuthFlow.fixedFields.tokenUrl, - refreshUrl: $v.visitors.document.objects.OAuthFlow.fixedFields.refreshUrl, - scopes: $v.visitors.document.objects.OAuthFlow.fixedFields.scopes - } - }, - SecurityRequirement: { $visitor: w_ } - }, - extension: { $visitor: $v.visitors.document.extension.$visitor } - } - } - }, - apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType = (s) => { - if (Nu(s)) return `${s.element.charAt(0).toUpperCase() + s.element.slice(1)}Element`; - }, - nw = { - CallbackElement: ['content'], - ComponentsElement: ['content'], - ContactElement: ['content'], - DiscriminatorElement: ['content'], - Encoding: ['content'], - Example: ['content'], - ExternalDocumentationElement: ['content'], - HeaderElement: ['content'], - InfoElement: ['content'], - LicenseElement: ['content'], - MediaTypeElement: ['content'], - OAuthFlowElement: ['content'], - OAuthFlowsElement: ['content'], - OpenApi3_1Element: ['content'], - OperationElement: ['content'], - ParameterElement: ['content'], - PathItemElement: ['content'], - PathsElement: ['content'], - ReferenceElement: ['content'], - RequestBodyElement: ['content'], - ResponseElement: ['content'], - ResponsesElement: ['content'], - SchemaElement: ['content'], - SecurityRequirementElement: ['content'], - SecuritySchemeElement: ['content'], - ServerElement: ['content'], - ServerVariableElement: ['content'], - TagElement: ['content'], - ...Qu - }, - sw = { - namespace: (s) => { - const { base: o } = s; - return ( - o.register('callback', Wv), - o.register('components', Kv), - o.register('contact', Hv), - o.register('discriminator', Jv), - o.register('encoding', Gv), - o.register('example', Yv), - o.register('externalDocumentation', Xv), - o.register('header', Zv), - o.register('info', Qv), - o.register('jsonSchemaDialect', eb), - o.register('license', tb), - o.register('link', nb), - o.register('mediaType', pb), - o.register('oAuthFlow', mb), - o.register('oAuthFlows', yb), - o.register('openapi', _b), - o.register('openApi3_1', wb), - o.register('operation', Sb), - o.register('parameter', Ob), - o.register('pathItem', Ab), - o.register('paths', Ib), - o.register('reference', Pb), - o.register('requestBody', Mb), - o.register('response', Rb), - o.register('responses', Lb), - o.register('schema', qb), - o.register('securityRequirement', zb), - o.register('securityScheme', Qb), - o.register('server', e_), - o.register('serverVariable', t_), - o.register('tag', r_), - o.register('xml', n_), - o - ); - } - }, - ow = sw, - ancestorLineageToJSONPointer = (s) => { - const o = s.reduce((o, i, u) => { - if ($u(i)) { - const s = String(serializers_value(i.key)); - o.push(s); - } else if (qu(s[u - 2])) { - const _ = String(s[u - 2].content.indexOf(i)); - o.push(_); - } - return o; - }, []); - return es_compile(o); - }, - apidom_ns_openapi_3_1_es_refractor_toolbox = () => { - const s = createNamespace(ow); - return { - predicates: { - ...de, - isElement: Nu, - isStringElement: Ru, - isArrayElement: qu, - isObjectElement: Fu, - isMemberElement: $u, - isServersElement: rg, - includesClasses, - hasElementSourceMap - }, - ancestorLineageToJSONPointer, - namespace: s - }; - }, - apidom_ns_openapi_3_1_es_refractor_refract = ( - s, - { - specPath: o = ['visitors', 'document', 'objects', 'OpenApi', '$visitor'], - plugins: i = [] - } = {} - ) => { - const u = (0, Cu.e)(s), - _ = dereference(rw), - w = new (cp(o, _))({ specObj: _ }); - return ( - visitor_visit(u, w), - dispatchPluginsSync(w.element, i, { - toolboxCreator: apidom_ns_openapi_3_1_es_refractor_toolbox, - visitorOptions: { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - } - }) - ); - }, - apidom_ns_openapi_3_1_es_refractor_createRefractor = - (s) => - (o, i = {}) => - apidom_ns_openapi_3_1_es_refractor_refract(o, { specPath: s, ...i }); - (Wv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Callback', - '$visitor' - ])), - (Kv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Components', - '$visitor' - ])), - (Hv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Contact', - '$visitor' - ])), - (Yv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Example', - '$visitor' - ])), - (Jv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Discriminator', - '$visitor' - ])), - (Gv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Encoding', - '$visitor' - ])), - (Xv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'ExternalDocumentation', - '$visitor' - ])), - (Zv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Header', - '$visitor' - ])), - (Qv.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Info', - '$visitor' - ])), - (eb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OpenApi', - 'fixedFields', - 'jsonSchemaDialect' - ])), - (tb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'License', - '$visitor' - ])), - (nb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Link', - '$visitor' - ])), - (pb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'MediaType', - '$visitor' - ])), - (mb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OAuthFlow', - '$visitor' - ])), - (yb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OAuthFlows', - '$visitor' - ])), - (_b.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OpenApi', - 'fixedFields', - 'openapi' - ])), - (wb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'OpenApi', - '$visitor' - ])), - (Sb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Operation', - '$visitor' - ])), - (Ob.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Parameter', - '$visitor' - ])), - (Ab.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'PathItem', - '$visitor' - ])), - (Ib.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Paths', - '$visitor' - ])), - (Pb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Reference', - '$visitor' - ])), - (Mb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'RequestBody', - '$visitor' - ])), - (Rb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Response', - '$visitor' - ])), - (Lb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Responses', - '$visitor' - ])), - (qb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Schema', - '$visitor' - ])), - (zb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'SecurityRequirement', - '$visitor' - ])), - (Qb.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'SecurityScheme', - '$visitor' - ])), - (e_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Server', - '$visitor' - ])), - (t_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'ServerVariable', - '$visitor' - ])), - (r_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'Tag', - '$visitor' - ])), - (n_.refract = apidom_ns_openapi_3_1_es_refractor_createRefractor([ - 'visitors', - 'document', - 'objects', - 'XML', - '$visitor' - ])); - const iw = class NotImplementedError extends Hh {}; - const aw = class MediaTypes extends Array { - unknownMediaType = 'application/octet-stream'; - filterByFormat() { - throw new iw('filterByFormat method in MediaTypes class is not yet implemented.'); - } - findBy() { - throw new iw('findBy method in MediaTypes class is not yet implemented.'); - } - latest() { - throw new iw('latest method in MediaTypes class is not yet implemented.'); - } - }; - class OpenAPIMediaTypes extends aw { - filterByFormat(s = 'generic') { - const o = 'generic' === s ? 'openapi;version' : s; - return this.filter((s) => s.includes(o)); - } - findBy(s = '3.1.0', o = 'generic') { - const i = - 'generic' === o - ? `vnd.oai.openapi;version=${s}` - : `vnd.oai.openapi+${o};version=${s}`; - return this.find((s) => s.includes(i)) || this.unknownMediaType; - } - latest(s = 'generic') { - return Fa(this.filterByFormat(s)); - } - } - const lw = new OpenAPIMediaTypes( - 'application/vnd.oai.openapi;version=3.1.0', - 'application/vnd.oai.openapi+json;version=3.1.0', - 'application/vnd.oai.openapi+yaml;version=3.1.0' - ); - const cw = class es_Reference_Reference { - uri; - depth; - value; - refSet; - errors; - constructor({ uri: s, depth: o = 0, refSet: i, value: u }) { - (this.uri = s), - (this.value = u), - (this.depth = o), - (this.refSet = i), - (this.errors = []); - } - }; - const uw = class ReferenceSet { - rootRef; - refs; - circular; - constructor({ refs: s = [], circular: o = !1 } = {}) { - (this.refs = []), (this.circular = o), s.forEach(this.add.bind(this)); - } - get size() { - return this.refs.length; - } - add(s) { - return ( - this.has(s) || - (this.refs.push(s), - (this.rootRef = void 0 === this.rootRef ? s : this.rootRef), - (s.refSet = this)), - this - ); - } - merge(s) { - for (const o of s.values()) this.add(o); - return this; - } - has(s) { - const o = Yl(s) ? s : s.uri; - return Dl(this.find((s) => s.uri === o)); - } - find(s) { - return this.refs.find(s); - } - *values() { - yield* this.refs; - } - clean() { - this.refs.forEach((s) => { - s.refSet = void 0; - }), - (this.rootRef = void 0), - (this.refs.length = 0); - } - }, - pw = { - parse: { mediaType: 'text/plain', parsers: [], parserOpts: {} }, - resolve: { - baseURI: '', - resolvers: [], - resolverOpts: {}, - strategies: [], - strategyOpts: {}, - internal: !0, - external: !0, - maxDepth: 1 / 0 - }, - dereference: { - strategies: [], - strategyOpts: {}, - refSet: null, - maxDepth: 1 / 0, - circular: 'ignore', - circularReplacer: Ip, - immutable: !0 - }, - bundle: { strategies: [], refSet: null, maxDepth: 1 / 0 } - }; - const hw = _curry2(function lens(s, o) { - return function (i) { - return function (u) { - return kl( - function (s) { - return o(s, u); - }, - i(s(u)) - ); - }; - }; - }); - var dw = _curry3(function assocPath(s, o, i) { - if (0 === s.length) return o; - var u = s[0]; - if (s.length > 1) { - var _ = !ld(i) && _has(u, i) && 'object' == typeof i[u] ? i[u] : Yo(s[1]) ? [] : {}; - o = assocPath(Array.prototype.slice.call(s, 1), o, _); - } - return (function _assoc(s, o, i) { - if (Yo(s) && aa(i)) { - var u = [].concat(i); - return (u[s] = o), u; - } - var _ = {}; - for (var w in i) _[w] = i[w]; - return (_[s] = o), _; - })(u, o, i); - }); - const fw = dw; - var Identity = function (s) { - return { - value: s, - map: function (o) { - return Identity(o(s)); - } - }; - }, - mw = _curry3(function over(s, o, i) { - return s(function (s) { - return Identity(o(s)); - })(i).value; - }); - const gw = mw, - yw = hw(cp(['resolve', 'baseURI']), fw(['resolve', 'baseURI'])), - baseURIDefault = (s) => (qp(s) ? url_cwd() : s), - util_merge = (s, o) => { - const i = lp(s, o); - return gw(yw, baseURIDefault, i); - }; - const vw = class File_File { - uri; - mediaType; - data; - parseResult; - constructor({ uri: s, mediaType: o = 'text/plain', data: i, parseResult: u }) { - (this.uri = s), (this.mediaType = o), (this.data = i), (this.parseResult = u); - } - get extension() { - return Yl(this.uri) - ? ((s) => { - const o = s.lastIndexOf('.'); - return o >= 0 ? s.substring(o).toLowerCase() : ''; - })(this.uri) - : ''; - } - toString() { - if ('string' == typeof this.data) return this.data; - if ( - this.data instanceof ArrayBuffer || - ['ArrayBuffer'].includes(ea(this.data)) || - ArrayBuffer.isView(this.data) - ) { - return new TextDecoder('utf-8').decode(this.data); - } - return String(this.data); - } - }; - const bw = class PluginError extends Ho { - plugin; - constructor(s, o) { - super(s, { cause: o.cause }), (this.plugin = o.plugin); - } - }, - plugins_filter = async (s, o, i) => { - const u = await Promise.all(i.map(_p([s], o))); - return i.filter((s, o) => u[o]); - }, - run = async (s, o, i) => { - let u; - for (const _ of i) - try { - const i = await _[s].call(_, ...o); - return { plugin: _, result: i }; - } catch (s) { - u = new bw('Error while running plugin', { cause: s, plugin: _ }); - } - return Promise.reject(u); - }; - const _w = class DereferenceError extends Ho {}; - const Ew = class UnmatchedDereferenceStrategyError extends _w {}, - dereferenceApiDOM = async (s, o) => { - let i = s, - u = !1; - if (!Ku(s)) { - const o = cloneShallow(s); - o.classes.push('result'), (i = new Mu([o])), (u = !0); - } - const _ = new vw({ - uri: o.resolve.baseURI, - parseResult: i, - mediaType: o.parse.mediaType - }), - w = await plugins_filter('canDereference', [_, o], o.dereference.strategies); - if (gp(w)) throw new Ew(_.uri); - try { - const { result: s } = await run('dereference', [_, o], w); - return u ? s.get(0) : s; - } catch (s) { - throw new _w(`Error while dereferencing file "${_.uri}"`, { cause: s }); - } - }; - const ww = class ParseError extends Ho {}; - const Sw = class ParserError extends ww {}; - const xw = class Parser { - name; - allowEmpty; - sourceMap; - fileExtensions; - mediaTypes; - constructor({ - name: s, - allowEmpty: o = !0, - sourceMap: i = !1, - fileExtensions: u = [], - mediaTypes: _ = [] - }) { - (this.name = s), - (this.allowEmpty = o), - (this.sourceMap = i), - (this.fileExtensions = u), - (this.mediaTypes = _); - } - }; - const kw = class BinaryParser extends xw { - constructor(s) { - super({ ...(null != s ? s : {}), name: 'binary' }); - } - canParse(s) { - return 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension); - } - parse(s) { - try { - const o = unescape(encodeURIComponent(s.toString())), - i = btoa(o), - u = new Mu(); - if (0 !== i.length) { - const s = new Cu.Om(i); - s.classes.push('result'), u.push(s); - } - return u; - } catch (o) { - throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); - } - } - }; - const Cw = class ResolveStrategy { - name; - constructor({ name: s }) { - this.name = s; - } - }; - const Ow = class OpenAPI3_1ResolveStrategy extends Cw { - constructor(s) { - super({ ...(null != s ? s : {}), name: 'openapi-3-1' }); - } - canResolve(s, o) { - const i = o.dereference.strategies.find((s) => 'openapi-3-1' === s.name); - return void 0 !== i && i.canDereference(s, o); - } - async resolve(s, o) { - const i = o.dereference.strategies.find((s) => 'openapi-3-1' === s.name); - if (void 0 === i) throw new Ew('"openapi-3-1" dereference strategy is not available.'); - const u = new uw(), - _ = util_merge(o, { resolve: { internal: !1 }, dereference: { refSet: u } }); - return await i.dereference(s, _), u; - } - }; - const Aw = class Resolver { - name; - constructor({ name: s }) { - this.name = s; - } - }; - const jw = class HTTPResolver extends Aw { - timeout; - redirects; - withCredentials; - constructor(s) { - const { - name: o = 'http-resolver', - timeout: i = 5e3, - redirects: u = 5, - withCredentials: _ = !1 - } = null != s ? s : {}; - super({ name: o }), - (this.timeout = i), - (this.redirects = u), - (this.withCredentials = _); - } - canRead(s) { - return isHttpUrl(s.uri); - } - }; - const Iw = class ResolveError extends Ho {}; - const Pw = class ResolverError extends Iw {}, - { AbortController: Mw, AbortSignal: Tw } = globalThis; - void 0 === globalThis.AbortController && (globalThis.AbortController = Mw), - void 0 === globalThis.AbortSignal && (globalThis.AbortSignal = Tw); - const Nw = class HTTPResolverSwaggerClient extends jw { - swaggerHTTPClient = http_http; - swaggerHTTPClientConfig; - constructor({ - swaggerHTTPClient: s = http_http, - swaggerHTTPClientConfig: o = {}, - ...i - } = {}) { - super({ ...i, name: 'http-swagger-client' }), - (this.swaggerHTTPClient = s), - (this.swaggerHTTPClientConfig = o); - } - getHttpClient() { - return this.swaggerHTTPClient; - } - async read(s) { - const o = this.getHttpClient(), - i = new AbortController(), - { signal: u } = i, - _ = setTimeout(() => { - i.abort(); - }, this.timeout), - w = - this.getHttpClient().withCredentials || this.withCredentials - ? 'include' - : 'same-origin', - x = 0 === this.redirects ? 'error' : 'follow', - C = this.redirects > 0 ? this.redirects : void 0; - try { - return ( - await o({ - url: s.uri, - signal: u, - userFetch: async (s, o) => { - let i = await fetch(s, o); - try { - i.headers.delete('Content-Type'); - } catch { - (i = new Response(i.body, { ...i, headers: new Headers(i.headers) })), - i.headers.delete('Content-Type'); - } - return i; - }, - credentials: w, - redirect: x, - follow: C, - ...this.swaggerHTTPClientConfig - }) - ).text.arrayBuffer(); - } catch (o) { - throw new Pw(`Error downloading "${s.uri}"`, { cause: o }); - } finally { - clearTimeout(_); - } - } - }, - from = (s, o = wp) => { - if (Yl(s)) - try { - return o.fromRefract(JSON.parse(s)); - } catch {} - return ku(s) && md('element', s) ? o.fromRefract(s) : o.toElement(s); - }; - const Rw = class JSONParser extends xw { - constructor(s = {}) { - super({ name: 'json-swagger-client', mediaTypes: ['application/json'], ...s }); - } - async canParse(s) { - const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), - i = this.mediaTypes.includes(s.mediaType); - if (!o) return !1; - if (i) return !0; - if (!i) - try { - return JSON.parse(s.toString()), !0; - } catch (s) { - return !1; - } - return !1; - } - async parse(s) { - if (this.sourceMap) - throw new Sw("json-swagger-client parser plugin doesn't support sourceMaps option"); - const o = new Mu(), - i = s.toString(); - if (this.allowEmpty && '' === i.trim()) return o; - try { - const s = from(JSON.parse(i)); - return s.classes.push('result'), o.push(s), o; - } catch (o) { - throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); - } - } - }; - const Dw = class YAMLParser extends xw { - constructor(s = {}) { - super({ - name: 'yaml-1-2-swagger-client', - mediaTypes: ['text/yaml', 'application/yaml'], - ...s - }); - } - async canParse(s) { - const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), - i = this.mediaTypes.includes(s.mediaType); - if (!o) return !1; - if (i) return !0; - if (!i) - try { - return mn.load(s.toString(), { schema: nn }), !0; - } catch (s) { - return !1; - } - return !1; - } - async parse(s) { - if (this.sourceMap) - throw new Sw( - "yaml-1-2-swagger-client parser plugin doesn't support sourceMaps option" - ); - const o = new Mu(), - i = s.toString(); - try { - const s = mn.load(i, { schema: nn }); - if (this.allowEmpty && void 0 === s) return o; - const u = from(s); - return u.classes.push('result'), o.push(u), o; - } catch (o) { - throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); - } - } - }; - const Lw = class OpenAPIJSON3_1Parser extends xw { - detectionRegExp = /"openapi"\s*:\s*"(?3\.1\.(?:[1-9]\d*|0))"/; - constructor(s = {}) { - super({ - name: 'openapi-json-3-1-swagger-client', - mediaTypes: new OpenAPIMediaTypes( - ...lw.filterByFormat('generic'), - ...lw.filterByFormat('json') - ), - ...s - }); - } - async canParse(s) { - const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), - i = this.mediaTypes.includes(s.mediaType); - if (!o) return !1; - if (i) return !0; - if (!i) - try { - const o = s.toString(); - return JSON.parse(o), this.detectionRegExp.test(o); - } catch (s) { - return !1; - } - return !1; - } - async parse(s) { - if (this.sourceMap) - throw new Sw( - "openapi-json-3-1-swagger-client parser plugin doesn't support sourceMaps option" - ); - const o = new Mu(), - i = s.toString(); - if (this.allowEmpty && '' === i.trim()) return o; - try { - const s = JSON.parse(i), - u = wb.refract(s, this.refractorOpts); - return u.classes.push('result'), o.push(u), o; - } catch (o) { - throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); - } - } - }; - const Bw = class OpenAPIYAML31Parser extends xw { - detectionRegExp = - /(?^(["']?)openapi\2\s*:\s*(["']?)(?3\.1\.(?:[1-9]\d*|0))\3(?:\s+|$))|(?"openapi"\s*:\s*"(?3\.1\.(?:[1-9]\d*|0))")/m; - constructor(s = {}) { - super({ - name: 'openapi-yaml-3-1-swagger-client', - mediaTypes: new OpenAPIMediaTypes( - ...lw.filterByFormat('generic'), - ...lw.filterByFormat('yaml') - ), - ...s - }); - } - async canParse(s) { - const o = 0 === this.fileExtensions.length || this.fileExtensions.includes(s.extension), - i = this.mediaTypes.includes(s.mediaType); - if (!o) return !1; - if (i) return !0; - if (!i) - try { - const o = s.toString(); - return mn.load(o), this.detectionRegExp.test(o); - } catch (s) { - return !1; - } - return !1; - } - async parse(s) { - if (this.sourceMap) - throw new Sw( - "openapi-yaml-3-1-swagger-client parser plugin doesn't support sourceMaps option" - ); - const o = new Mu(), - i = s.toString(); - try { - const s = mn.load(i, { schema: nn }); - if (this.allowEmpty && void 0 === s) return o; - const u = wb.refract(s, this.refractorOpts); - return u.classes.push('result'), o.push(u), o; - } catch (o) { - throw new Sw(`Error parsing "${s.uri}"`, { cause: o }); - } - } - }; - const Fw = _curry3(function propEq(s, o, i) { - return ra(s, Da(o, i)); - }); - const qw = class DereferenceStrategy { - name; - constructor({ name: s }) { - this.name = s; - } - }; - var $w = _curry2(function none(s, o) { - return ju(_complement(s), o); - }); - const Vw = $w; - var Uw = __webpack_require__(8068); - const zw = class ElementIdentityError extends Jo { - value; - constructor(s, o) { - super(s, o), void 0 !== o && (this.value = o.value); - } - }; - class IdentityManager { - uuid; - identityMap; - constructor({ length: s = 6 } = {}) { - (this.uuid = new Uw({ length: s })), (this.identityMap = new WeakMap()); - } - identify(s) { - if (!Nu(s)) - throw new zw( - 'Cannot not identify the element. `element` is neither structurally compatible nor a subclass of an Element class.', - { value: s } - ); - if (s.meta.hasKey('id') && Ru(s.meta.get('id')) && !s.meta.get('id').equals('')) - return s.id; - if (this.identityMap.has(s)) return this.identityMap.get(s); - const o = new Cu.Om(this.generateId()); - return this.identityMap.set(s, o), o; - } - forget(s) { - return !!this.identityMap.has(s) && (this.identityMap.delete(s), !0); - } - generateId() { - return this.uuid.randomUUID(); - } - } - new IdentityManager(); - const Ww = _curry3(function pathOr(s, o, i) { - return Na(s, _path(o, i)); - }), - traversal_find = (s, o) => { - const i = new PredicateVisitor({ predicate: s, returnOnTrue: Ju }); - return visitor_visit(o, i), Ww(void 0, [0], i.result); - }; - const Kw = class JsonSchema$anchorError extends Ho {}; - const Hw = class EvaluationJsonSchema$anchorError extends Kw {}; - const Jw = class InvalidJsonSchema$anchorError extends Kw { - constructor(s) { - super(`Invalid JSON Schema $anchor "${s}".`); - } - }, - isAnchor = (s) => /^[A-Za-z_][A-Za-z_0-9.-]*$/.test(s), - uriToAnchor = (s) => { - const o = getHash(s); - return Up('#', o); - }, - $anchor_evaluate = (s, o) => { - const i = ((s) => { - if (!isAnchor(s)) throw new Jw(s); - return s; - })(s), - u = traversal_find((s) => Q_(s) && serializers_value(s.$anchor) === i, o); - if (Rl(u)) throw new Hw(`Evaluation failed on token: "${i}"`); - return u; - }, - traversal_filter = (s, o) => { - const i = new PredicateVisitor({ predicate: s }); - return visitor_visit(o, i), new Cu.G6(i.result); - }; - const Gw = class JsonSchemaUriError extends Ho {}; - const Yw = class EvaluationJsonSchemaUriError extends Gw {}, - resolveSchema$refField = (s, o) => { - if (void 0 === o.$ref) return; - const i = getHash(serializers_value(o.$ref)), - u = serializers_value(o.meta.get('inherited$id')), - _ = Ca((s, o) => resolve(s, sanitize(stripHash(o))), s, [ - ...u, - serializers_value(o.$ref) - ]); - return `${_}${'#' === i ? '' : i}`; - }, - refractToSchemaElement = (s) => { - if (refractToSchemaElement.cache.has(s)) return refractToSchemaElement.cache.get(s); - const o = qb.refract(s); - return refractToSchemaElement.cache.set(s, o), o; - }; - refractToSchemaElement.cache = new WeakMap(); - const maybeRefractToSchemaElement = (s) => - isPrimitiveElement(s) ? refractToSchemaElement(s) : s, - uri_evaluate = (s, o) => { - const { cache: i } = uri_evaluate, - u = stripHash(s), - isSchemaElementWith$id = (s) => Q_(s) && void 0 !== s.$id; - if (!i.has(o)) { - const s = traversal_filter(isSchemaElementWith$id, o); - i.set(o, Array.from(s)); - } - const _ = i.get(o).find((s) => { - const o = ((s, o) => { - if (void 0 === o.$id) return; - const i = serializers_value(o.meta.get('inherited$id')); - return Ca((s, o) => resolve(s, sanitize(stripHash(o))), s, [ - ...i, - serializers_value(o.$id) - ]); - })(u, s); - return o === u; - }); - if (Rl(_)) throw new Yw(`Evaluation failed on URI: "${s}"`); - let w, x; - return ( - isAnchor(uriToAnchor(s)) - ? ((w = $anchor_evaluate), (x = uriToAnchor(s))) - : ((w = es_evaluate), (x = uriToPointer(s))), - w(x, _) - ); - }; - uri_evaluate.cache = new WeakMap(); - const Xw = class MaximumDereferenceDepthError extends _w {}; - const Zw = class MaximumResolveDepthError extends Iw {}; - const Qw = class UnmatchedResolverError extends Pw {}, - _swagger_api_apidom_reference_es_parse = async (s, o) => { - const i = new vw({ uri: sanitize(stripHash(s)), mediaType: o.parse.mediaType }), - u = await (async (s, o) => { - const i = o.resolve.resolvers.map((s) => { - const i = Object.create(s); - return Object.assign(i, o.resolve.resolverOpts); - }), - u = await plugins_filter('canRead', [s, o], i); - if (gp(u)) throw new Qw(s.uri); - try { - const { result: o } = await run('read', [s], u); - return o; - } catch (o) { - throw new Iw(`Error while reading file "${s.uri}"`, { cause: o }); - } - })(i, o); - return (async (s, o) => { - const i = o.parse.parsers.map((s) => { - const i = Object.create(s); - return Object.assign(i, o.parse.parserOpts); - }), - u = await plugins_filter('canParse', [s, o], i); - if (gp(u)) throw new Qw(s.uri); - try { - const { plugin: i, result: _ } = await run('parse', [s, o], u); - return !i.allowEmpty && _.isEmpty - ? Promise.reject(new ww(`Error while parsing file "${s.uri}". File is empty.`)) - : _; - } catch (o) { - throw new ww(`Error while parsing file "${s.uri}"`, { cause: o }); - } - })(new vw({ ...i, data: u }), o); - }; - class AncestorLineage extends Array { - includesCycle(s) { - return this.filter((o) => o.has(s)).length > 1; - } - includes(s, o) { - return s instanceof Set ? super.includes(s, o) : this.some((o) => o.has(s)); - } - findItem(s) { - for (const o of this) for (const i of o) if (Nu(i) && s(i)) return i; - } - } - const eS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')], - tS = new IdentityManager(), - mutationReplacer = (s, o, i, u) => { - $u(u) ? (u.value = s) : Array.isArray(u) && (u[i] = s); - }; - class OpenAPI3_1DereferenceVisitor { - indirections; - namespace; - reference; - options; - ancestors; - refractCache; - constructor({ - reference: s, - namespace: o, - options: i, - indirections: u = [], - ancestors: _ = new AncestorLineage(), - refractCache: w = new Map() - }) { - (this.indirections = u), - (this.namespace = o), - (this.reference = s), - (this.options = i), - (this.ancestors = new AncestorLineage(..._)), - (this.refractCache = w); - } - toBaseURI(s) { - return resolve(this.reference.uri, sanitize(stripHash(s))); - } - async toReference(s) { - if (this.reference.depth >= this.options.resolve.maxDepth) - throw new Zw( - `Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"` - ); - const o = this.toBaseURI(s), - { refSet: i } = this.reference; - if (i.has(o)) return i.find(Fw(o, 'uri')); - const u = await _swagger_api_apidom_reference_es_parse(unsanitize(o), { - ...this.options, - parse: { ...this.options.parse, mediaType: 'text/plain' } - }), - _ = new cw({ uri: o, value: cloneDeep(u), depth: this.reference.depth + 1 }); - if ((i.add(_), this.options.dereference.immutable)) { - const s = new cw({ - uri: `immutable://${o}`, - value: u, - depth: this.reference.depth + 1 - }); - i.add(s); - } - return _; - } - toAncestorLineage(s) { - const o = new Set(s.filter(Nu)); - return [new AncestorLineage(...this.ancestors, o), o]; - } - async ReferenceElement(s, o, i, u, _, w) { - if (this.indirections.includes(s)) return !1; - const [x, C] = this.toAncestorLineage([..._, i]), - j = this.toBaseURI(serializers_value(s.$ref)), - L = stripHash(this.reference.uri) === j, - B = !L; - if (!this.options.resolve.internal && L) return !1; - if (!this.options.resolve.external && B) return !1; - const $ = await this.toReference(serializers_value(s.$ref)), - V = resolve(j, serializers_value(s.$ref)); - this.indirections.push(s); - const U = uriToPointer(V); - let z = es_evaluate(U, $.value.result); - if (((z.id = tS.identify(z)), isPrimitiveElement(z))) { - const o = serializers_value(s.meta.get('referenced-element')), - i = `${o}-${serializers_value(tS.identify(z))}`; - if (this.refractCache.has(i)) z = this.refractCache.get(i); - else if (isReferenceLikeElement(z)) - (z = Pb.refract(z)), - z.setMetaProperty('referenced-element', o), - this.refractCache.set(i, z); - else { - (z = this.namespace.getElementClass(o).refract(z)), this.refractCache.set(i, z); - } - } - if (s === z) throw new Ho('Recursive Reference Object detected'); - if (this.indirections.length > this.options.dereference.maxDepth) - throw new Xw( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); - if (x.includes(z)) { - if ((($.refSet.circular = !0), 'error' === this.options.dereference.circular)) - throw new Ho('Circular reference detected'); - if ('replace' === this.options.dereference.circular) { - var Y, Z; - const o = new Cu.sI(z.id, { - type: 'reference', - uri: $.uri, - $ref: serializers_value(s.$ref) - }), - u = ( - null !== - (Y = - null === (Z = this.options.dereference.strategyOpts['openapi-3-1']) || - void 0 === Z - ? void 0 - : Z.circularReplacer) && void 0 !== Y - ? Y - : this.options.dereference.circularReplacer - )(o); - return w.replaceWith(u, mutationReplacer), !i && u; - } - } - const ee = stripHash($.refSet.rootRef.uri) !== $.uri, - ie = ['error', 'replace'].includes(this.options.dereference.circular); - if ((B || ee || G_(z) || ie) && !x.includesCycle(z)) { - C.add(s); - const o = new OpenAPI3_1DereferenceVisitor({ - reference: $, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - refractCache: this.refractCache, - ancestors: x - }); - (z = await eS(z, o, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - })), - C.delete(s); - } - this.indirections.pop(); - const ae = cloneShallow(z); - return ( - ae.setMetaProperty('id', tS.generateId()), - ae.setMetaProperty('ref-fields', { - $ref: serializers_value(s.$ref), - description: serializers_value(s.description), - summary: serializers_value(s.summary) - }), - ae.setMetaProperty('ref-origin', $.uri), - ae.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), - Fu(z) && - Fu(ae) && - (s.hasKey('description') && - 'description' in z && - (ae.remove('description'), ae.set('description', s.get('description'))), - s.hasKey('summary') && - 'summary' in z && - (ae.remove('summary'), ae.set('summary', s.get('summary')))), - w.replaceWith(ae, mutationReplacer), - !i && ae - ); - } - async PathItemElement(s, o, i, u, _, w) { - if (!Ru(s.$ref)) return; - if (this.indirections.includes(s)) return !1; - const [x, C] = this.toAncestorLineage([..._, i]), - j = this.toBaseURI(serializers_value(s.$ref)), - L = stripHash(this.reference.uri) === j, - B = !L; - if (!this.options.resolve.internal && L) return; - if (!this.options.resolve.external && B) return; - const $ = await this.toReference(serializers_value(s.$ref)), - V = resolve(j, serializers_value(s.$ref)); - this.indirections.push(s); - const U = uriToPointer(V); - let z = es_evaluate(U, $.value.result); - if (((z.id = tS.identify(z)), isPrimitiveElement(z))) { - const s = `path-item-${serializers_value(tS.identify(z))}`; - this.refractCache.has(s) - ? (z = this.refractCache.get(s)) - : ((z = Ab.refract(z)), this.refractCache.set(s, z)); - } - if (s === z) throw new Ho('Recursive Path Item Object reference detected'); - if (this.indirections.length > this.options.dereference.maxDepth) - throw new Xw( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); - if (x.includes(z)) { - if ((($.refSet.circular = !0), 'error' === this.options.dereference.circular)) - throw new Ho('Circular reference detected'); - if ('replace' === this.options.dereference.circular) { - var Y, Z; - const o = new Cu.sI(z.id, { - type: 'path-item', - uri: $.uri, - $ref: serializers_value(s.$ref) - }), - u = ( - null !== - (Y = - null === (Z = this.options.dereference.strategyOpts['openapi-3-1']) || - void 0 === Z - ? void 0 - : Z.circularReplacer) && void 0 !== Y - ? Y - : this.options.dereference.circularReplacer - )(o); - return w.replaceWith(u, mutationReplacer), !i && u; - } - } - const ee = stripHash($.refSet.rootRef.uri) !== $.uri, - ie = ['error', 'replace'].includes(this.options.dereference.circular); - if ((B || ee || (H_(z) && Ru(z.$ref)) || ie) && !x.includesCycle(z)) { - C.add(s); - const o = new OpenAPI3_1DereferenceVisitor({ - reference: $, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - refractCache: this.refractCache, - ancestors: x - }); - (z = await eS(z, o, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - })), - C.delete(s); - } - if ((this.indirections.pop(), H_(z))) { - const o = new Ab([...z.content], cloneDeep(z.meta), cloneDeep(z.attributes)); - o.setMetaProperty('id', tS.generateId()), - s.forEach((s, i, u) => { - o.remove(serializers_value(i)), o.content.push(u); - }), - o.remove('$ref'), - o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), - o.setMetaProperty('ref-origin', $.uri), - o.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), - (z = o); - } - return w.replaceWith(z, mutationReplacer), i ? void 0 : z; - } - async LinkElement(s, o, i, u, _, w) { - if (!Ru(s.operationRef) && !Ru(s.operationId)) return; - if (Ru(s.operationRef) && Ru(s.operationId)) - throw new Ho( - 'LinkElement operationRef and operationId fields are mutually exclusive.' - ); - let x; - if (Ru(s.operationRef)) { - var C; - const o = uriToPointer(serializers_value(s.operationRef)), - u = this.toBaseURI(serializers_value(s.operationRef)), - _ = stripHash(this.reference.uri) === u, - j = !_; - if (!this.options.resolve.internal && _) return; - if (!this.options.resolve.external && j) return; - const L = await this.toReference(serializers_value(s.operationRef)); - if (((x = es_evaluate(o, L.value.result)), isPrimitiveElement(x))) { - const s = `operation-${serializers_value(tS.identify(x))}`; - this.refractCache.has(s) - ? (x = this.refractCache.get(s)) - : ((x = Sb.refract(x)), this.refractCache.set(s, x)); - } - (x = cloneShallow(x)), x.setMetaProperty('ref-origin', L.uri); - const B = cloneShallow(s); - return ( - null === (C = B.operationRef) || void 0 === C || C.meta.set('operation', x), - w.replaceWith(B, mutationReplacer), - i ? void 0 : B - ); - } - if (Ru(s.operationId)) { - var j; - const o = serializers_value(s.operationId), - u = await this.toReference(unsanitize(this.reference.uri)); - if ( - ((x = traversal_find( - (s) => W_(s) && Nu(s.operationId) && s.operationId.equals(o), - u.value.result - )), - Rl(x)) - ) - throw new Ho(`OperationElement(operationId=${o}) not found.`); - const _ = cloneShallow(s); - return ( - null === (j = _.operationId) || void 0 === j || j.meta.set('operation', x), - w.replaceWith(_, mutationReplacer), - i ? void 0 : _ - ); - } - } - async ExampleElement(s, o, i, u, _, w) { - if (!Ru(s.externalValue)) return; - if (s.hasKey('value') && Ru(s.externalValue)) - throw new Ho('ExampleElement value and externalValue fields are mutually exclusive.'); - const x = this.toBaseURI(serializers_value(s.externalValue)), - C = stripHash(this.reference.uri) === x, - j = !C; - if (!this.options.resolve.internal && C) return; - if (!this.options.resolve.external && j) return; - const L = await this.toReference(serializers_value(s.externalValue)), - B = cloneShallow(L.value.result); - B.setMetaProperty('ref-origin', L.uri); - const $ = cloneShallow(s); - return ($.value = B), w.replaceWith($, mutationReplacer), i ? void 0 : $; - } - async SchemaElement(s, o, i, u, _, w) { - if (!Ru(s.$ref)) return; - if (this.indirections.includes(s)) return !1; - const [x, C] = this.toAncestorLineage([..._, i]); - let j = await this.toReference(unsanitize(this.reference.uri)), - { uri: L } = j; - const B = resolveSchema$refField(L, s), - $ = stripHash(B), - V = new vw({ uri: $ }), - U = Vw((s) => s.canRead(V), this.options.resolve.resolvers), - z = !U; - let Y, - Z = stripHash(this.reference.uri) === B, - ee = !Z; - this.indirections.push(s); - try { - if (U || z) { - L = this.toBaseURI(B); - const s = B, - o = maybeRefractToSchemaElement(j.value.result); - if ( - ((Y = uri_evaluate(s, o)), - (Y = maybeRefractToSchemaElement(Y)), - (Y.id = tS.identify(Y)), - !this.options.resolve.internal && Z) - ) - return; - if (!this.options.resolve.external && ee) return; - } else { - if ( - ((L = this.toBaseURI(B)), - (Z = stripHash(this.reference.uri) === L), - (ee = !Z), - !this.options.resolve.internal && Z) - ) - return; - if (!this.options.resolve.external && ee) return; - j = await this.toReference(unsanitize(B)); - const s = uriToPointer(B), - o = maybeRefractToSchemaElement(j.value.result); - (Y = es_evaluate(s, o)), - (Y = maybeRefractToSchemaElement(Y)), - (Y.id = tS.identify(Y)); - } - } catch (s) { - if (!(z && s instanceof Yw)) throw s; - if (isAnchor(uriToAnchor(B))) { - if ( - ((Z = stripHash(this.reference.uri) === L), - (ee = !Z), - !this.options.resolve.internal && Z) - ) - return; - if (!this.options.resolve.external && ee) return; - j = await this.toReference(unsanitize(B)); - const s = uriToAnchor(B), - o = maybeRefractToSchemaElement(j.value.result); - (Y = $anchor_evaluate(s, o)), - (Y = maybeRefractToSchemaElement(Y)), - (Y.id = tS.identify(Y)); - } else { - if ( - ((L = this.toBaseURI(B)), - (Z = stripHash(this.reference.uri) === L), - (ee = !Z), - !this.options.resolve.internal && Z) - ) - return; - if (!this.options.resolve.external && ee) return; - j = await this.toReference(unsanitize(B)); - const s = uriToPointer(B), - o = maybeRefractToSchemaElement(j.value.result); - (Y = es_evaluate(s, o)), - (Y = maybeRefractToSchemaElement(Y)), - (Y.id = tS.identify(Y)); - } - } - if (s === Y) throw new Ho('Recursive Schema Object reference detected'); - if (this.indirections.length > this.options.dereference.maxDepth) - throw new Xw( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); - if (x.includes(Y)) { - if (((j.refSet.circular = !0), 'error' === this.options.dereference.circular)) - throw new Ho('Circular reference detected'); - if ('replace' === this.options.dereference.circular) { - var ie, ae; - const o = new Cu.sI(Y.id, { - type: 'json-schema', - uri: j.uri, - $ref: serializers_value(s.$ref) - }), - u = ( - null !== - (ie = - null === (ae = this.options.dereference.strategyOpts['openapi-3-1']) || - void 0 === ae - ? void 0 - : ae.circularReplacer) && void 0 !== ie - ? ie - : this.options.dereference.circularReplacer - )(o); - return w.replaceWith(u, mutationReplacer), !i && u; - } - } - const le = stripHash(j.refSet.rootRef.uri) !== j.uri, - ce = ['error', 'replace'].includes(this.options.dereference.circular); - if ((ee || le || (Q_(Y) && Ru(Y.$ref)) || ce) && !x.includesCycle(Y)) { - C.add(s); - const o = new OpenAPI3_1DereferenceVisitor({ - reference: j, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - refractCache: this.refractCache, - ancestors: x - }); - (Y = await eS(Y, o, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - })), - C.delete(s); - } - if ((this.indirections.pop(), predicates_isBooleanJsonSchemaElement(Y))) { - const o = cloneDeep(Y); - return ( - o.setMetaProperty('id', tS.generateId()), - o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), - o.setMetaProperty('ref-origin', j.uri), - o.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), - w.replaceWith(o, mutationReplacer), - !i && o - ); - } - if (Q_(Y)) { - const o = new qb([...Y.content], cloneDeep(Y.meta), cloneDeep(Y.attributes)); - o.setMetaProperty('id', tS.generateId()), - s.forEach((s, i, u) => { - o.remove(serializers_value(i)), o.content.push(u); - }), - o.remove('$ref'), - o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), - o.setMetaProperty('ref-origin', j.uri), - o.setMetaProperty('ref-referencing-element-id', cloneDeep(tS.identify(s))), - (Y = o); - } - return w.replaceWith(Y, mutationReplacer), i ? void 0 : Y; - } - } - const rS = OpenAPI3_1DereferenceVisitor, - nS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')]; - const sS = class OpenAPI3_1DereferenceStrategy extends qw { - constructor(s) { - super({ ...(null != s ? s : {}), name: 'openapi-3-1' }); - } - canDereference(s) { - var o; - return 'text/plain' !== s.mediaType - ? lw.includes(s.mediaType) - : z_(null === (o = s.parseResult) || void 0 === o ? void 0 : o.result); - } - async dereference(s, o) { - var i; - const u = createNamespace(ow), - _ = null !== (i = o.dereference.refSet) && void 0 !== i ? i : new uw(), - w = new uw(); - let x, - C = _; - _.has(s.uri) - ? (x = _.find(Fw(s.uri, 'uri'))) - : ((x = new cw({ uri: s.uri, value: s.parseResult })), _.add(x)), - o.dereference.immutable && - (_.refs - .map((s) => new cw({ ...s, value: cloneDeep(s.value) })) - .forEach((s) => w.add(s)), - (x = w.find((o) => o.uri === s.uri)), - (C = w)); - const j = new rS({ reference: x, namespace: u, options: o }), - L = await nS(C.rootRef.value, j, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - }); - return ( - o.dereference.immutable && - w.refs - .filter((s) => s.uri.startsWith('immutable://')) - .map((s) => new cw({ ...s, uri: s.uri.replace(/^immutable:\/\//, '') })) - .forEach((s) => _.add(s)), - null === o.dereference.refSet && _.clean(), - w.clean(), - L - ); - } - }, - to_path = (s) => { - const o = ((s) => s.slice(2))(s); - return o.reduce((s, i, u) => { - if ($u(i)) { - const o = String(serializers_value(i.key)); - s.push(o); - } else if (qu(o[u - 2])) { - const _ = o[u - 2].content.indexOf(i); - s.push(_); - } - return s; - }, []); - }; - const oS = class ModelPropertyMacroVisitor { - modelPropertyMacro; - options; - SchemaElement = { - leave: (s, o, i, u, _) => { - void 0 !== s.properties && - Fu(s.properties) && - s.properties.forEach((o) => { - if (Fu(o)) - try { - const s = this.modelPropertyMacro(serializers_value(o)); - o.set('default', s); - } catch (o) { - var u, w; - const x = new Error(o, { cause: o }); - (x.fullPath = [...to_path([..._, i, s]), 'properties']), - null === (u = this.options.dereference.dereferenceOpts) || - void 0 === u || - null === (u = u.errors) || - void 0 === u || - null === (w = u.push) || - void 0 === w || - w.call(u, x); - } - }); - } - }; - constructor({ modelPropertyMacro: s, options: o }) { - (this.modelPropertyMacro = s), (this.options = o); - } - }; - const iS = class all_of_AllOfVisitor { - options; - SchemaElement = { - leave(s, o, i, u, _) { - if (void 0 === s.allOf) return; - if (!qu(s.allOf)) { - var w, x; - const o = new TypeError('allOf must be an array'); - return ( - (o.fullPath = [...to_path([..._, i, s]), 'allOf']), - void ( - null === (w = this.options.dereference.dereferenceOpts) || - void 0 === w || - null === (w = w.errors) || - void 0 === w || - null === (x = w.push) || - void 0 === x || - x.call(w, o) - ) - ); - } - if (s.allOf.isEmpty) return void s.remove('allOf'); - if (!s.allOf.content.every(Q_)) { - var C, j; - const o = new TypeError('Elements in allOf must be objects'); - return ( - (o.fullPath = [...to_path([..._, i, s]), 'allOf']), - void ( - null === (C = this.options.dereference.dereferenceOpts) || - void 0 === C || - null === (C = C.errors) || - void 0 === C || - null === (j = C.push) || - void 0 === j || - j.call(C, o) - ) - ); - } - for (; s.hasKey('allOf'); ) { - const { allOf: o } = s; - s.remove('allOf'); - const i = deepmerge.all([...o.content, s]); - if ((s.hasKey('$$ref') || i.remove('$$ref'), s.hasKey('example'))) { - const o = i.getMember('example'); - o && (o.value = s.get('example')); - } - if (s.hasKey('examples')) { - const o = i.getMember('examples'); - o && (o.value = s.get('examples')); - } - s.content = i.content; - } - } - }; - constructor({ options: s }) { - this.options = s; - } - }; - const aS = class ParameterMacroVisitor { - parameterMacro; - options; - #e; - OperationElement = { - enter: (s) => { - this.#e = s; - }, - leave: () => { - this.#e = void 0; - } - }; - ParameterElement = { - leave: (s, o, i, u, _) => { - const w = this.#e ? serializers_value(this.#e) : null, - x = serializers_value(s); - try { - const o = this.parameterMacro(w, x); - s.set('default', o); - } catch (s) { - var C, j; - const o = new Error(s, { cause: s }); - (o.fullPath = to_path([..._, i])), - null === (C = this.options.dereference.dereferenceOpts) || - void 0 === C || - null === (C = C.errors) || - void 0 === C || - null === (j = C.push) || - void 0 === j || - j.call(C, o); - } - } - }; - constructor({ parameterMacro: s, options: o }) { - (this.parameterMacro = s), (this.options = o); - } - }, - get_root_cause = (s) => { - if (null == s.cause) return s; - let { cause: o } = s; - for (; null != o.cause; ) o = o.cause; - return o; - }; - const lS = class SchemaRefError extends Jo {}, - { wrapError: cS } = ru, - uS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')], - pS = new IdentityManager(), - dereference_mutationReplacer = (s, o, i, u) => { - $u(u) ? (u.value = s) : Array.isArray(u) && (u[i] = s); - }; - class OpenAPI3_1SwaggerClientDereferenceVisitor extends rS { - useCircularStructures; - allowMetaPatches; - basePath; - constructor({ - allowMetaPatches: s = !0, - useCircularStructures: o = !1, - basePath: i = null, - ...u - }) { - super(u), - (this.allowMetaPatches = s), - (this.useCircularStructures = o), - (this.basePath = i); - } - async ReferenceElement(s, o, i, u, _, w) { - try { - if (this.indirections.includes(s)) return !1; - const [o, u] = this.toAncestorLineage([..._, i]), - L = this.toBaseURI(serializers_value(s.$ref)), - B = stripHash(this.reference.uri) === L, - $ = !B; - if (!this.options.resolve.internal && B) return !1; - if (!this.options.resolve.external && $) return !1; - const V = await this.toReference(serializers_value(s.$ref)), - U = resolve(L, serializers_value(s.$ref)); - this.indirections.push(s); - const z = uriToPointer(U); - let Y = es_evaluate(z, V.value.result); - if (((Y.id = pS.identify(Y)), isPrimitiveElement(Y))) { - const o = serializers_value(s.meta.get('referenced-element')), - i = `${o}-${serializers_value(pS.identify(Y))}`; - if (this.refractCache.has(i)) Y = this.refractCache.get(i); - else if (isReferenceLikeElement(Y)) - (Y = Pb.refract(Y)), - Y.setMetaProperty('referenced-element', o), - this.refractCache.set(i, Y); - else { - (Y = this.namespace.getElementClass(o).refract(Y)), this.refractCache.set(i, Y); - } - } - if (s === Y) throw new Ho('Recursive Reference Object detected'); - if (this.indirections.length > this.options.dereference.maxDepth) - throw new Xw( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); - if (o.includes(Y)) { - if (((V.refSet.circular = !0), 'error' === this.options.dereference.circular)) - throw new Ho('Circular reference detected'); - if ('replace' === this.options.dereference.circular) { - var x, C; - const o = new Cu.sI(Y.id, { - type: 'reference', - uri: V.uri, - $ref: serializers_value(s.$ref), - baseURI: U, - referencingElement: s - }), - u = ( - null !== - (x = - null === (C = this.options.dereference.strategyOpts['openapi-3-1']) || - void 0 === C - ? void 0 - : C.circularReplacer) && void 0 !== x - ? x - : this.options.dereference.circularReplacer - )(o); - return w.replaceWith(o, dereference_mutationReplacer), !i && u; - } - } - const Z = stripHash(V.refSet.rootRef.uri) !== V.uri, - ee = ['error', 'replace'].includes(this.options.dereference.circular); - if (($ || Z || G_(Y) || ee) && !o.includesCycle(Y)) { - var j; - u.add(s); - const w = new OpenAPI3_1SwaggerClientDereferenceVisitor({ - reference: V, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - refractCache: this.refractCache, - ancestors: o, - allowMetaPatches: this.allowMetaPatches, - useCircularStructures: this.useCircularStructures, - basePath: - null !== (j = this.basePath) && void 0 !== j - ? j - : [...to_path([..._, i, s]), '$ref'] - }); - (Y = await uS(Y, w, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - })), - u.delete(s); - } - this.indirections.pop(); - const ie = cloneShallow(Y); - if ( - (ie.setMetaProperty('ref-fields', { - $ref: serializers_value(s.$ref), - description: serializers_value(s.description), - summary: serializers_value(s.summary) - }), - ie.setMetaProperty('ref-origin', V.uri), - ie.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), - Fu(Y) && - (s.hasKey('description') && - 'description' in Y && - (ie.remove('description'), ie.set('description', s.get('description'))), - s.hasKey('summary') && - 'summary' in Y && - (ie.remove('summary'), ie.set('summary', s.get('summary')))), - this.allowMetaPatches && Fu(ie) && !ie.hasKey('$$ref')) - ) { - const s = resolve(L, U); - ie.set('$$ref', s); - } - return w.replaceWith(ie, dereference_mutationReplacer), !i && ie; - } catch (o) { - var L, B, $; - const u = get_root_cause(o), - w = cS(u, { - baseDoc: this.reference.uri, - $ref: serializers_value(s.$ref), - pointer: uriToPointer(serializers_value(s.$ref)), - fullPath: - null !== (L = this.basePath) && void 0 !== L - ? L - : [...to_path([..._, i, s]), '$ref'] - }); - return void ( - null === (B = this.options.dereference.dereferenceOpts) || - void 0 === B || - null === (B = B.errors) || - void 0 === B || - null === ($ = B.push) || - void 0 === $ || - $.call(B, w) - ); - } - } - async PathItemElement(s, o, i, u, _, w) { - try { - if (!Ru(s.$ref)) return; - if (this.indirections.includes(s)) return !1; - if (includesClasses(['cycle'], s.$ref)) return !1; - const [o, u] = this.toAncestorLineage([..._, i]), - L = this.toBaseURI(serializers_value(s.$ref)), - B = stripHash(this.reference.uri) === L, - $ = !B; - if (!this.options.resolve.internal && B) return; - if (!this.options.resolve.external && $) return; - const V = await this.toReference(serializers_value(s.$ref)), - U = resolve(L, serializers_value(s.$ref)); - this.indirections.push(s); - const z = uriToPointer(U); - let Y = es_evaluate(z, V.value.result); - if (((Y.id = pS.identify(Y)), isPrimitiveElement(Y))) { - const s = `path-item-${serializers_value(pS.identify(Y))}`; - this.refractCache.has(s) - ? (Y = this.refractCache.get(s)) - : ((Y = Ab.refract(Y)), this.refractCache.set(s, Y)); - } - if (s === Y) throw new Ho('Recursive Path Item Object reference detected'); - if (this.indirections.length > this.options.dereference.maxDepth) - throw new Xw( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); - if (o.includes(Y)) { - if (((V.refSet.circular = !0), 'error' === this.options.dereference.circular)) - throw new Ho('Circular reference detected'); - if ('replace' === this.options.dereference.circular) { - var x, C; - const o = new Cu.sI(Y.id, { - type: 'path-item', - uri: V.uri, - $ref: serializers_value(s.$ref), - baseURI: U, - referencingElement: s - }), - u = ( - null !== - (x = - null === (C = this.options.dereference.strategyOpts['openapi-3-1']) || - void 0 === C - ? void 0 - : C.circularReplacer) && void 0 !== x - ? x - : this.options.dereference.circularReplacer - )(o); - return w.replaceWith(o, dereference_mutationReplacer), !i && u; - } - } - const Z = stripHash(V.refSet.rootRef.uri) !== V.uri, - ee = ['error', 'replace'].includes(this.options.dereference.circular); - if (($ || Z || (H_(Y) && Ru(Y.$ref)) || ee) && !o.includesCycle(Y)) { - var j; - u.add(s); - const w = new OpenAPI3_1SwaggerClientDereferenceVisitor({ - reference: V, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - ancestors: o, - allowMetaPatches: this.allowMetaPatches, - useCircularStructures: this.useCircularStructures, - basePath: - null !== (j = this.basePath) && void 0 !== j - ? j - : [...to_path([..._, i, s]), '$ref'] - }); - (Y = await uS(Y, w, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - })), - u.delete(s); - } - if ((this.indirections.pop(), H_(Y))) { - const o = new Ab([...Y.content], cloneDeep(Y.meta), cloneDeep(Y.attributes)); - if ( - (s.forEach((s, i, u) => { - o.remove(serializers_value(i)), o.content.push(u); - }), - o.remove('$ref'), - o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), - o.setMetaProperty('ref-origin', V.uri), - o.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), - this.allowMetaPatches && void 0 === o.get('$$ref')) - ) { - const s = resolve(L, U); - o.set('$$ref', s); - } - Y = o; - } - return w.replaceWith(Y, dereference_mutationReplacer), i ? void 0 : Y; - } catch (o) { - var L, B, $; - const u = get_root_cause(o), - w = cS(u, { - baseDoc: this.reference.uri, - $ref: serializers_value(s.$ref), - pointer: uriToPointer(serializers_value(s.$ref)), - fullPath: - null !== (L = this.basePath) && void 0 !== L - ? L - : [...to_path([..._, i, s]), '$ref'] - }); - return void ( - null === (B = this.options.dereference.dereferenceOpts) || - void 0 === B || - null === (B = B.errors) || - void 0 === B || - null === ($ = B.push) || - void 0 === $ || - $.call(B, w) - ); - } - } - async SchemaElement(s, o, i, u, _, w) { - try { - if (!Ru(s.$ref)) return; - if (this.indirections.includes(s)) return !1; - const [o, u] = this.toAncestorLineage([..._, i]); - let L = await this.toReference(unsanitize(this.reference.uri)), - { uri: B } = L; - const $ = resolveSchema$refField(B, s), - V = stripHash($), - U = new vw({ uri: V }), - z = !this.options.resolve.resolvers.some((s) => s.canRead(U)), - Y = !z; - let Z, - ee = stripHash(this.reference.uri) === $, - ie = !ee; - this.indirections.push(s); - try { - if (z || Y) { - B = this.toBaseURI($); - const s = $, - o = maybeRefractToSchemaElement(L.value.result); - if ( - ((Z = uri_evaluate(s, o)), - (Z = maybeRefractToSchemaElement(Z)), - (Z.id = pS.identify(Z)), - !this.options.resolve.internal && ee) - ) - return; - if (!this.options.resolve.external && ie) return; - } else { - if ( - ((B = this.toBaseURI($)), - (ee = stripHash(this.reference.uri) === B), - (ie = !ee), - !this.options.resolve.internal && ee) - ) - return; - if (!this.options.resolve.external && ie) return; - L = await this.toReference(unsanitize($)); - const s = uriToPointer($), - o = maybeRefractToSchemaElement(L.value.result); - (Z = es_evaluate(s, o)), - (Z = maybeRefractToSchemaElement(Z)), - (Z.id = pS.identify(Z)); - } - } catch (s) { - if (!(Y && s instanceof Yw)) throw s; - if (isAnchor(uriToAnchor($))) { - if ( - ((ee = stripHash(this.reference.uri) === B), - (ie = !ee), - !this.options.resolve.internal && ee) - ) - return; - if (!this.options.resolve.external && ie) return; - L = await this.toReference(unsanitize($)); - const s = uriToAnchor($), - o = maybeRefractToSchemaElement(L.value.result); - (Z = $anchor_evaluate(s, o)), - (Z = maybeRefractToSchemaElement(Z)), - (Z.id = pS.identify(Z)); - } else { - if ( - ((B = this.toBaseURI(serializers_value($))), - (ee = stripHash(this.reference.uri) === B), - (ie = !ee), - !this.options.resolve.internal && ee) - ) - return; - if (!this.options.resolve.external && ie) return; - L = await this.toReference(unsanitize($)); - const s = uriToPointer($), - o = maybeRefractToSchemaElement(L.value.result); - (Z = es_evaluate(s, o)), - (Z = maybeRefractToSchemaElement(Z)), - (Z.id = pS.identify(Z)); - } - } - if (s === Z) throw new Ho('Recursive Schema Object reference detected'); - if (this.indirections.length > this.options.dereference.maxDepth) - throw new Xw( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); - if (o.includes(Z)) { - if (((L.refSet.circular = !0), 'error' === this.options.dereference.circular)) - throw new Ho('Circular reference detected'); - if ('replace' === this.options.dereference.circular) { - var x, C; - const o = new Cu.sI(Z.id, { - type: 'json-schema', - uri: L.uri, - $ref: serializers_value(s.$ref), - baseURI: resolve(B, $), - referencingElement: s - }), - u = ( - null !== - (x = - null === (C = this.options.dereference.strategyOpts['openapi-3-1']) || - void 0 === C - ? void 0 - : C.circularReplacer) && void 0 !== x - ? x - : this.options.dereference.circularReplacer - )(o); - return w.replaceWith(u, dereference_mutationReplacer), !i && u; - } - } - const ae = stripHash(L.refSet.rootRef.uri) !== L.uri, - le = ['error', 'replace'].includes(this.options.dereference.circular); - if ((ie || ae || (Q_(Z) && Ru(Z.$ref)) || le) && !o.includesCycle(Z)) { - var j; - u.add(s); - const w = new OpenAPI3_1SwaggerClientDereferenceVisitor({ - reference: L, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - useCircularStructures: this.useCircularStructures, - allowMetaPatches: this.allowMetaPatches, - ancestors: o, - basePath: - null !== (j = this.basePath) && void 0 !== j - ? j - : [...to_path([..._, i, s]), '$ref'] - }); - (Z = await uS(Z, w, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - })), - u.delete(s); - } - if ((this.indirections.pop(), predicates_isBooleanJsonSchemaElement(Z))) { - const o = cloneDeep(Z); - return ( - o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), - o.setMetaProperty('ref-origin', L.uri), - o.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), - w.replaceWith(o, dereference_mutationReplacer), - !i && o - ); - } - if (Q_(Z)) { - const o = new qb([...Z.content], cloneDeep(Z.meta), cloneDeep(Z.attributes)); - if ( - (s.forEach((s, i, u) => { - o.remove(serializers_value(i)), o.content.push(u); - }), - o.remove('$ref'), - o.setMetaProperty('ref-fields', { $ref: serializers_value(s.$ref) }), - o.setMetaProperty('ref-origin', L.uri), - o.setMetaProperty('ref-referencing-element-id', cloneDeep(pS.identify(s))), - this.allowMetaPatches && void 0 === o.get('$$ref')) - ) { - const s = resolve(B, $); - o.set('$$ref', s); - } - Z = o; - } - return w.replaceWith(Z, dereference_mutationReplacer), i ? void 0 : Z; - } catch (o) { - var L, B, $; - const u = get_root_cause(o), - w = new lS(`Could not resolve reference: ${u.message}`, { - baseDoc: this.reference.uri, - $ref: serializers_value(s.$ref), - fullPath: - null !== (L = this.basePath) && void 0 !== L - ? L - : [...to_path([..._, i, s]), '$ref'], - cause: u - }); - return void ( - null === (B = this.options.dereference.dereferenceOpts) || - void 0 === B || - null === (B = B.errors) || - void 0 === B || - null === ($ = B.push) || - void 0 === $ || - $.call(B, w) - ); - } - } - async LinkElement() {} - async ExampleElement(s, o, i, u, _, w) { - try { - return await super.ExampleElement(s, o, i, u, _, w); - } catch (o) { - var x, C, j; - const u = get_root_cause(o), - w = cS(u, { - baseDoc: this.reference.uri, - externalValue: serializers_value(s.externalValue), - fullPath: - null !== (x = this.basePath) && void 0 !== x - ? x - : [...to_path([..._, i, s]), 'externalValue'] - }); - return void ( - null === (C = this.options.dereference.dereferenceOpts) || - void 0 === C || - null === (C = C.errors) || - void 0 === C || - null === (j = C.push) || - void 0 === j || - j.call(C, w) - ); - } - } - } - const hS = OpenAPI3_1SwaggerClientDereferenceVisitor, - dS = mergeAll[Symbol.for('nodejs.util.promisify.custom')]; - const fS = class RootVisitor { - constructor({ parameterMacro: s, modelPropertyMacro: o, mode: i, options: u, ..._ }) { - const w = []; - w.push(new hS({ ..._, options: u })), - 'function' == typeof o && w.push(new oS({ modelPropertyMacro: o, options: u })), - 'strict' !== i && w.push(new iS({ options: u })), - 'function' == typeof s && w.push(new aS({ parameterMacro: s, options: u })); - const x = dS(w, { - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - }); - Object.assign(this, x); - } - }, - mS = visitor_visit[Symbol.for('nodejs.util.promisify.custom')]; - const gS = class OpenAPI3_1SwaggerClientDereferenceStrategy extends sS { - allowMetaPatches; - parameterMacro; - modelPropertyMacro; - mode; - ancestors; - constructor({ - allowMetaPatches: s = !1, - parameterMacro: o = null, - modelPropertyMacro: i = null, - mode: u = 'non-strict', - ancestors: _ = [], - ...w - } = {}) { - super({ ...w }), - (this.name = 'openapi-3-1-swagger-client'), - (this.allowMetaPatches = s), - (this.parameterMacro = o), - (this.modelPropertyMacro = i), - (this.mode = u), - (this.ancestors = [..._]); - } - async dereference(s, o) { - var i; - const u = createNamespace(ow), - _ = null !== (i = o.dereference.refSet) && void 0 !== i ? i : new uw(), - w = new uw(); - let x, - C = _; - _.has(s.uri) - ? (x = _.find((o) => o.uri === s.uri)) - : ((x = new cw({ uri: s.uri, value: s.parseResult })), _.add(x)), - o.dereference.immutable && - (_.refs - .map((s) => new cw({ ...s, value: cloneDeep(s.value) })) - .forEach((s) => w.add(s)), - (x = w.find((o) => o.uri === s.uri)), - (C = w)); - const j = new fS({ - reference: x, - namespace: u, - options: o, - allowMetaPatches: this.allowMetaPatches, - ancestors: this.ancestors, - modelPropertyMacro: this.modelPropertyMacro, - mode: this.mode, - parameterMacro: this.parameterMacro - }), - L = await mS(C.rootRef.value, j, { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - }); - return ( - o.dereference.immutable && - w.refs - .filter((s) => s.uri.startsWith('immutable://')) - .map((s) => new cw({ ...s, uri: s.uri.replace(/^immutable:\/\//, '') })) - .forEach((s) => _.add(s)), - null === o.dereference.refSet && _.clean(), - w.clean(), - L - ); - } - }, - circularReplacer = (s) => { - const o = serializers_value(s.meta.get('baseURI')), - i = s.meta.get('referencingElement'); - return new Cu.Sh({ $ref: o }, cloneDeep(i.meta), cloneDeep(i.attributes)); - }, - resolveOpenAPI31Strategy = async (s) => { - const { - spec: o, - timeout: i, - redirects: u, - requestInterceptor: _, - responseInterceptor: w, - pathDiscriminator: x = [], - allowMetaPatches: C = !1, - useCircularStructures: j = !1, - skipNormalization: L = !1, - parameterMacro: B = null, - modelPropertyMacro: $ = null, - mode: V = 'non-strict', - strategies: U - } = s; - try { - const { cache: z } = resolveOpenAPI31Strategy, - Y = U.find((s) => s.match(o)), - Z = isHttpUrl(url_cwd()) ? url_cwd() : Nc, - ee = options_retrievalURI(s), - ie = resolve(Z, ee); - let ae; - z.has(o) - ? (ae = z.get(o)) - : ((ae = wb.refract(o)), ae.classes.push('result'), z.set(o, ae)); - const le = new Mu([ae]), - ce = es_compile(x), - pe = '' === ce ? '' : `#${ce}`, - de = es_evaluate(ce, ae), - fe = new cw({ uri: ie, value: le }), - ye = new uw({ refs: [fe] }); - '' !== ce && (ye.rootRef = void 0); - const be = [new Set([de])], - _e = [], - we = await (async (s, o = {}) => { - const i = util_merge(pw, o); - return dereferenceApiDOM(s, i); - })(de, { - resolve: { - baseURI: `${ie}${pe}`, - resolvers: [new Nw({ timeout: i || 1e4, redirects: u || 10 })], - resolverOpts: { - swaggerHTTPClientConfig: { requestInterceptor: _, responseInterceptor: w } - }, - strategies: [new Ow()] - }, - parse: { - mediaType: lw.latest(), - parsers: [ - new Lw({ allowEmpty: !1, sourceMap: !1 }), - new Bw({ allowEmpty: !1, sourceMap: !1 }), - new Rw({ allowEmpty: !1, sourceMap: !1 }), - new Dw({ allowEmpty: !1, sourceMap: !1 }), - new kw({ allowEmpty: !1, sourceMap: !1 }) - ] - }, - dereference: { - maxDepth: 100, - strategies: [ - new gS({ - allowMetaPatches: C, - useCircularStructures: j, - parameterMacro: B, - modelPropertyMacro: $, - mode: V, - ancestors: be - }) - ], - refSet: ye, - dereferenceOpts: { errors: _e }, - immutable: !1, - circular: j ? 'ignore' : 'replace', - circularReplacer: j ? pw.dereference.circularReplacer : circularReplacer - } - }), - Se = ((s, o, i) => new xp({ element: i }).transclude(s, o))(de, we, ae), - xe = L ? Se : Y.normalize(Se); - return { spec: serializers_value(xe), errors: _e }; - } catch (s) { - if (s instanceof Wp || s instanceof Kp) return { spec: null, errors: [] }; - throw s; - } - }; - resolveOpenAPI31Strategy.cache = new WeakMap(); - const yS = resolveOpenAPI31Strategy; - function _clone(s, o, i) { - if ( - (i || (i = new vS()), - (function _isPrimitive(s) { - var o = typeof s; - return null == s || ('object' != o && 'function' != o); - })(s)) - ) - return s; - var u = function copy(u) { - var _ = i.get(s); - if (_) return _; - for (var w in (i.set(s, u), s)) - Object.prototype.hasOwnProperty.call(s, w) && (u[w] = o ? _clone(s[w], !0, i) : s[w]); - return u; - }; - switch (ea(s)) { - case 'Object': - return u(Object.create(Object.getPrototypeOf(s))); - case 'Array': - return u(Array(s.length)); - case 'Date': - return new Date(s.valueOf()); - case 'RegExp': - return _cloneRegExp(s); - case 'Int8Array': - case 'Uint8Array': - case 'Uint8ClampedArray': - case 'Int16Array': - case 'Uint16Array': - case 'Int32Array': - case 'Uint32Array': - case 'Float32Array': - case 'Float64Array': - case 'BigInt64Array': - case 'BigUint64Array': - return s.slice(); - default: - return s; - } - } - var vS = (function () { - function _ObjectMap() { - (this.map = {}), (this.length = 0); - } - return ( - (_ObjectMap.prototype.set = function (s, o) { - var i = this.hash(s), - u = this.map[i]; - u || (this.map[i] = u = []), u.push([s, o]), (this.length += 1); - }), - (_ObjectMap.prototype.hash = function (s) { - var o = []; - for (var i in s) o.push(Object.prototype.toString.call(s[i])); - return o.join(); - }), - (_ObjectMap.prototype.get = function (s) { - if (this.length <= 180) - for (var o in this.map) - for (var i = this.map[o], u = 0; u < i.length; u += 1) { - if ((w = i[u])[0] === s) return w[1]; - } - else { - var _ = this.hash(s); - if ((i = this.map[_])) - for (u = 0; u < i.length; u += 1) { - var w; - if ((w = i[u])[0] === s) return w[1]; - } - } - }), - _ObjectMap - ); - })(), - bS = (function () { - function XReduceBy(s, o, i, u) { - (this.valueFn = s), - (this.valueAcc = o), - (this.keyFn = i), - (this.xf = u), - (this.inputs = {}); - } - return ( - (XReduceBy.prototype['@@transducer/init'] = _xfBase_init), - (XReduceBy.prototype['@@transducer/result'] = function (s) { - var o; - for (o in this.inputs) - if ( - _has(o, this.inputs) && - (s = this.xf['@@transducer/step'](s, this.inputs[o]))['@@transducer/reduced'] - ) { - s = s['@@transducer/value']; - break; - } - return (this.inputs = null), this.xf['@@transducer/result'](s); - }), - (XReduceBy.prototype['@@transducer/step'] = function (s, o) { - var i = this.keyFn(o); - return ( - (this.inputs[i] = this.inputs[i] || [i, _clone(this.valueAcc, !1)]), - (this.inputs[i][1] = this.valueFn(this.inputs[i][1], o)), - s - ); - }), - XReduceBy - ); - })(); - function _xreduceBy(s, o, i) { - return function (u) { - return new bS(s, o, i, u); - }; - } - var _S = _curryN( - 4, - [], - _dispatchable([], _xreduceBy, function reduceBy(s, o, i, u) { - var _ = _xwrap(function (u, _) { - var w = i(_), - x = s(_has(w, u) ? u[w] : _clone(o, !1), _); - return x && x['@@transducer/reduced'] ? _reduced(u) : ((u[w] = x), u); - }); - return wa(_, {}, u); - }) - ); - const ES = _curry2( - _checkForMethod( - 'groupBy', - _S(function (s, o) { - return s.push(o), s; - }, []) - ) - ); - const wS = class NormalizeStorage { - internalStore; - constructor(s, o, i) { - (this.storageElement = s), (this.storageField = o), (this.storageSubField = i); - } - get store() { - if (!this.internalStore) { - let s = this.storageElement.get(this.storageField); - Fu(s) || ((s = new Cu.Sh()), this.storageElement.set(this.storageField, s)); - let o = s.get(this.storageSubField); - qu(o) || ((o = new Cu.wE()), s.set(this.storageSubField, o)), - (this.internalStore = o); - } - return this.internalStore; - } - append(s) { - this.includes(s) || this.store.push(s); - } - includes(s) { - return this.store.includes(s); - } - }, - removeSpaces = (s) => s.replace(/\s/g, ''), - normalize_operation_ids_replaceSpecialCharsWithUnderscore = (s) => s.replace(/\W/gi, '_'), - normalizeOperationId = (s, o, i) => { - const u = removeSpaces(s); - return u.length > 0 - ? normalize_operation_ids_replaceSpecialCharsWithUnderscore(u) - : ((s, o) => - `${normalize_operation_ids_replaceSpecialCharsWithUnderscore(removeSpaces(o.toLowerCase()))}${normalize_operation_ids_replaceSpecialCharsWithUnderscore(removeSpaces(s))}`)( - o, - i - ); - }, - normalize_operation_ids = - ({ - storageField: s = 'x-normalized', - operationIdNormalizer: o = normalizeOperationId - } = {}) => - (i) => { - const { predicates: u, ancestorLineageToJSONPointer: _, namespace: w } = i, - x = [], - C = [], - j = []; - let L; - return { - visitor: { - OpenApi3_1Element: { - enter(o) { - L = new wS(o, s, 'operation-ids'); - }, - leave() { - const s = ES((s) => serializers_value(s.operationId), C); - Object.entries(s).forEach(([s, o]) => { - Array.isArray(o) && - (o.length <= 1 || - o.forEach((o, i) => { - const u = `${s}${i + 1}`; - o.operationId = new w.elements.String(u); - })); - }), - j.forEach((s) => { - if (void 0 === s.operationId) return; - const o = String(serializers_value(s.operationId)), - i = C.find( - (s) => serializers_value(s.meta.get('originalOperationId')) === o - ); - void 0 !== i && - ((s.operationId = cloneDeep.safe(i.operationId)), - s.meta.set('originalOperationId', o), - s.set('__originalOperationId', o)); - }), - (C.length = 0), - (j.length = 0), - (L = void 0); - } - }, - PathItemElement: { - enter(s) { - const o = Na('path', serializers_value(s.meta.get('path'))); - x.push(o); - }, - leave() { - x.pop(); - } - }, - OperationElement: { - enter(s, i, u, j, B) { - if (void 0 === s.operationId) return; - const $ = _([...B, u, s]); - if (L.includes($)) return; - const V = String(serializers_value(s.operationId)), - U = Fa(x), - z = Na('method', serializers_value(s.meta.get('http-method'))), - Y = o(V, U, z); - V !== Y && - ((s.operationId = new w.elements.String(Y)), - s.set('__originalOperationId', V), - s.meta.set('originalOperationId', V), - C.push(s), - L.append($)); - } - }, - LinkElement: { - leave(s) { - u.isLinkElement(s) && void 0 !== s.operationId && j.push(s); - } - } - } - }; - }; - var SS = (function () { - function XUniqWith(s, o) { - (this.xf = o), (this.pred = s), (this.items = []); - } - return ( - (XUniqWith.prototype['@@transducer/init'] = _xfBase_init), - (XUniqWith.prototype['@@transducer/result'] = _xfBase_result), - (XUniqWith.prototype['@@transducer/step'] = function (s, o) { - return _includesWith(this.pred, o, this.items) - ? s - : (this.items.push(o), this.xf['@@transducer/step'](s, o)); - }), - XUniqWith - ); - })(); - function _xuniqWith(s) { - return function (o) { - return new SS(s, o); - }; - } - var xS = _curry2( - _dispatchable([], _xuniqWith, function (s, o) { - for (var i, u = 0, _ = o.length, w = []; u < _; ) - _includesWith(s, (i = o[u]), w) || (w[w.length] = i), (u += 1); - return w; - }) - ); - const kS = xS, - normalize_parameters = - ({ storageField: s = 'x-normalized' } = {}) => - (o) => { - const { predicates: i, ancestorLineageToJSONPointer: u } = o, - parameterEquals = (s, o) => - !!i.isParameterElement(s) && - !!i.isParameterElement(o) && - !!i.isStringElement(s.name) && - !!i.isStringElement(s.in) && - !!i.isStringElement(o.name) && - !!i.isStringElement(o.in) && - serializers_value(s.name) === serializers_value(o.name) && - serializers_value(s.in) === serializers_value(o.in), - _ = []; - let w; - return { - visitor: { - OpenApi3_1Element: { - enter(o) { - w = new wS(o, s, 'parameters'); - }, - leave() { - w = void 0; - } - }, - PathItemElement: { - enter(s, o, u, w, x) { - if (x.some(i.isComponentsElement)) return; - const { parameters: C } = s; - i.isArrayElement(C) ? _.push([...C.content]) : _.push([]); - }, - leave() { - _.pop(); - } - }, - OperationElement: { - leave(s, o, i, x, C) { - const j = Fa(_); - if (!Array.isArray(j) || 0 === j.length) return; - const L = u([...C, i, s]); - if (w.includes(L)) return; - const B = Ww([], ['parameters', 'content'], s), - $ = kS(parameterEquals, [...B, ...j]); - (s.parameters = new yv($)), w.append(L); - } - } - } - }; - }, - normalize_security_requirements = - ({ storageField: s = 'x-normalized' } = {}) => - (o) => { - const { predicates: i, ancestorLineageToJSONPointer: u } = o; - let _, w; - return { - visitor: { - OpenApi3_1Element: { - enter(o) { - (w = new wS(o, s, 'security-requirements')), - i.isArrayElement(o.security) && (_ = o.security); - }, - leave() { - (w = void 0), (_ = void 0); - } - }, - OperationElement: { - leave(s, o, x, C, j) { - if (j.some(i.isComponentsElement)) return; - const L = u([...j, x, s]); - if (w.includes(L)) return; - var B; - void 0 === s.security && - void 0 !== _ && - ((s.security = new Sv( - null === (B = _) || void 0 === B ? void 0 : B.content - )), - w.append(L)); - } - } - } - }; - }, - normalize_parameter_examples = - ({ storageField: s = 'x-normalized' } = {}) => - (o) => { - const { predicates: i, ancestorLineageToJSONPointer: u } = o; - let _; - return { - visitor: { - OpenApi3_1Element: { - enter(o) { - _ = new wS(o, s, 'parameter-examples'); - }, - leave() { - _ = void 0; - } - }, - ParameterElement: { - leave(s, o, w, x, C) { - var j, L; - if (C.some(i.isComponentsElement)) return; - if (void 0 === s.schema || !i.isSchemaElement(s.schema)) return; - if ( - void 0 === (null === (j = s.schema) || void 0 === j ? void 0 : j.example) && - void 0 === (null === (L = s.schema) || void 0 === L ? void 0 : L.examples) - ) - return; - const B = u([...C, w, s]); - if (!_.includes(B)) { - if (void 0 !== s.examples && i.isObjectElement(s.examples)) { - const o = s.examples.map((s) => cloneDeep.safe(s.value)); - return ( - void 0 !== s.schema.examples && - (s.schema.set('examples', o), _.append(B)), - void ( - void 0 !== s.schema.example && - (s.schema.set('example', o[0]), _.append(B)) - ) - ); - } - void 0 !== s.example && - (void 0 !== s.schema.examples && - (s.schema.set('examples', [cloneDeep(s.example)]), _.append(B)), - void 0 !== s.schema.example && - (s.schema.set('example', cloneDeep(s.example)), _.append(B))); - } - } - } - } - }; - }, - normalize_header_examples = - ({ storageField: s = 'x-normalized' } = {}) => - (o) => { - const { predicates: i, ancestorLineageToJSONPointer: u } = o; - let _; - return { - visitor: { - OpenApi3_1Element: { - enter(o) { - _ = new wS(o, s, 'header-examples'); - }, - leave() { - _ = void 0; - } - }, - HeaderElement: { - leave(s, o, w, x, C) { - var j, L; - if (C.some(i.isComponentsElement)) return; - if (void 0 === s.schema || !i.isSchemaElement(s.schema)) return; - if ( - void 0 === (null === (j = s.schema) || void 0 === j ? void 0 : j.example) && - void 0 === (null === (L = s.schema) || void 0 === L ? void 0 : L.examples) - ) - return; - const B = u([...C, w, s]); - if (!_.includes(B)) { - if (void 0 !== s.examples && i.isObjectElement(s.examples)) { - const o = s.examples.map((s) => cloneDeep.safe(s.value)); - return ( - void 0 !== s.schema.examples && - (s.schema.set('examples', o), _.append(B)), - void ( - void 0 !== s.schema.example && - (s.schema.set('example', o[0]), _.append(B)) - ) - ); - } - void 0 !== s.example && - (void 0 !== s.schema.examples && - (s.schema.set('examples', [cloneDeep(s.example)]), _.append(B)), - void 0 !== s.schema.example && - (s.schema.set('example', cloneDeep(s.example)), _.append(B))); - } - } - } - } - }; - }, - openapi_3_1_apidom_normalize = (s) => { - if (!Fu(s)) return s; - const o = [ - normalize_operation_ids({ - operationIdNormalizer: (s, o, i) => - opId({ operationId: s }, o, i, { v2OperationIdCompatibilityMode: !1 }) - }), - normalize_parameters(), - normalize_security_requirements(), - normalize_parameter_examples(), - normalize_header_examples() - ]; - return dispatchPluginsSync(s, o, { - toolboxCreator: apidom_ns_openapi_3_1_es_refractor_toolbox, - visitorOptions: { - keyMap: nw, - nodeTypeGetter: apidom_ns_openapi_3_1_es_traversal_visitor_getNodeType - } - }); - }, - CS = { - name: 'openapi-3-1-apidom', - match: (s) => isOpenAPI31(s), - normalize(s) { - if (!Nu(s) && ku(s) && !s.$$normalized) { - const i = ((o = openapi_3_1_apidom_normalize), - (s) => { - const i = wb.refract(s); - i.classes.push('result'); - const u = o(i), - _ = serializers_value(u); - return yS.cache.set(_, u), serializers_value(u); - })(s); - return (i.$$normalized = !0), i; - } - var o; - return Nu(s) ? openapi_3_1_apidom_normalize(s) : s; - }, - resolve: async (s) => yS(s) - }, - OS = CS, - makeResolve = (s) => async (o) => - (async (s) => { - const { spec: o, requestInterceptor: i, responseInterceptor: u } = s, - _ = options_retrievalURI(s), - w = options_httpClient(s), - x = - o || - (await makeFetchJSON(w, { requestInterceptor: i, responseInterceptor: u })(_)), - C = { ...s, spec: x }; - return s.strategies.find((s) => s.match(x)).resolve(C); - })({ ...s, ...o }), - AS = makeResolve({ strategies: [fu, hu, uu] }); - var jS = __webpack_require__(69883); - const IS = function fnparser() { - const s = TS, - o = MS, - i = this, - u = 'parser.js: Parser(): '; - (i.ast = void 0), (i.stats = void 0), (i.trace = void 0), (i.callbacks = []); - let _, - w, - x, - C, - j, - L, - B, - $ = 0, - V = 0, - U = 0, - z = 0, - Y = 0, - Z = new (function systemData() { - (this.state = s.ACTIVE), - (this.phraseLength = 0), - (this.refresh = () => { - (this.state = s.ACTIVE), (this.phraseLength = 0); - }); - })(); - i.parse = (ee, ie, ae, le) => { - const ce = `${u}parse(): `; - ($ = 0), - (V = 0), - (U = 0), - (z = 0), - (Y = 0), - (_ = void 0), - (w = void 0), - (x = void 0), - (C = void 0), - Z.refresh(), - (j = void 0), - (L = void 0), - (B = void 0), - (C = o.stringToChars(ae)), - (_ = ee.rules), - (w = ee.udts); - const pe = ie.toLowerCase(); - let de; - for (const s in _) - if (_.hasOwnProperty(s) && pe === _[s].lower) { - de = _[s].index; - break; - } - if (void 0 === de) - throw new Error(`${ce}start rule name '${startRule}' not recognized`); - (() => { - const s = `${u}initializeCallbacks(): `; - let o, x; - for (j = [], L = [], o = 0; o < _.length; o += 1) j[o] = void 0; - for (o = 0; o < w.length; o += 1) L[o] = void 0; - const C = []; - for (o = 0; o < _.length; o += 1) C.push(_[o].lower); - for (o = 0; o < w.length; o += 1) C.push(w[o].lower); - for (const u in i.callbacks) - if (i.callbacks.hasOwnProperty(u)) { - if (((o = C.indexOf(u.toLowerCase())), o < 0)) - throw new Error(`${s}syntax callback '${u}' not a rule or udt name`); - if ( - ((x = i.callbacks[u] ? i.callbacks[u] : void 0), - 'function' != typeof x && void 0 !== x) - ) - throw new Error( - `${s}syntax callback[${u}] must be function reference or falsy)` - ); - o < _.length ? (j[o] = x) : (L[o - _.length] = x); - } - })(), - i.trace && i.trace.init(_, w, C), - i.stats && i.stats.init(_, w), - i.ast && i.ast.init(_, w, C), - (B = le), - (x = [{ type: s.RNM, index: de }]), - opExecute(0, 0), - (x = void 0); - let fe = !1; - switch (Z.state) { - case s.ACTIVE: - throw new Error(`${ce}final state should never be 'ACTIVE'`); - case s.NOMATCH: - fe = !1; - break; - case s.EMPTY: - case s.MATCH: - fe = Z.phraseLength === C.length; - break; - default: - throw new Error('unrecognized state'); - } - return { - success: fe, - state: Z.state, - stateName: s.idName(Z.state), - length: C.length, - matched: Z.phraseLength, - maxMatched: Y, - maxTreeDepth: U, - nodeHits: z - }; - }; - const validateRnmCallbackResult = (o, i, _, w) => { - if (i.phraseLength > _) { - let s = `${u}opRNM(${o.name}): callback function error: `; - throw ( - ((s += `sysData.phraseLength: ${i.phraseLength}`), - (s += ` must be <= remaining chars: ${_}`), - new Error(s)) - ); - } - switch (i.state) { - case s.ACTIVE: - if (!w) - throw new Error( - `${u}opRNM(${o.name}): callback function return error. ACTIVE state not allowed.` - ); - break; - case s.EMPTY: - i.phraseLength = 0; - break; - case s.MATCH: - 0 === i.phraseLength && (i.state = s.EMPTY); - break; - case s.NOMATCH: - i.phraseLength = 0; - break; - default: - throw new Error( - `${u}opRNM(${o.name}): callback function return error. Unrecognized return state: ${i.state}` - ); - } - }, - opUDT = (o, j) => { - let V, U, z; - const Y = x[o], - ee = w[Y.index]; - (Z.UdtIndex = ee.index), - $ || - ((z = i.ast && i.ast.udtDefined(Y.index)), - z && - ((U = _.length + Y.index), (V = i.ast.getLength()), i.ast.down(U, ee.name))); - const ie = C.length - j; - L[Y.index](Z, C, j, B), - ((o, i, _) => { - if (i.phraseLength > _) { - let s = `${u}opUDT(${o.name}): callback function error: `; - throw ( - ((s += `sysData.phraseLength: ${i.phraseLength}`), - (s += ` must be <= remaining chars: ${_}`), - new Error(s)) - ); - } - switch (i.state) { - case s.ACTIVE: - throw new Error(`${u}opUDT(${o.name}) ACTIVE state return not allowed.`); - case s.EMPTY: - if (!o.empty) throw new Error(`${u}opUDT(${o.name}) may not return EMPTY.`); - i.phraseLength = 0; - break; - case s.MATCH: - if (0 === i.phraseLength) { - if (!o.empty) - throw new Error(`${u}opUDT(${o.name}) may not return EMPTY.`); - i.state = s.EMPTY; - } - break; - case s.NOMATCH: - i.phraseLength = 0; - break; - default: - throw new Error( - `${u}opUDT(${o.name}): callback function return error. Unrecognized return state: ${i.state}` - ); - } - })(ee, Z, ie), - $ || - (z && - (Z.state === s.NOMATCH - ? i.ast.setLength(V) - : i.ast.up(U, ee.name, j, Z.phraseLength))); - }, - opExecute = (o, w) => { - const L = `${u}opExecute(): `, - ee = x[o]; - switch ( - ((z += 1), - V > U && (U = V), - (V += 1), - Z.refresh(), - i.trace && i.trace.down(ee, w), - ee.type) - ) { - case s.ALT: - ((o, i) => { - const u = x[o]; - for ( - let o = 0; - o < u.children.length && - (opExecute(u.children[o], i), Z.state === s.NOMATCH); - o += 1 - ); - })(o, w); - break; - case s.CAT: - ((o, u) => { - let _, w, C, j; - const L = x[o]; - i.ast && (w = i.ast.getLength()), (_ = !0), (C = u), (j = 0); - for (let o = 0; o < L.children.length; o += 1) { - if ((opExecute(L.children[o], C), Z.state === s.NOMATCH)) { - _ = !1; - break; - } - (C += Z.phraseLength), (j += Z.phraseLength); - } - _ - ? ((Z.state = 0 === j ? s.EMPTY : s.MATCH), (Z.phraseLength = j)) - : ((Z.state = s.NOMATCH), - (Z.phraseLength = 0), - i.ast && i.ast.setLength(w)); - })(o, w); - break; - case s.REP: - ((o, u) => { - let _, w, j, L; - const B = x[o]; - if (0 === B.max) return (Z.state = s.EMPTY), void (Z.phraseLength = 0); - for ( - w = u, j = 0, L = 0, i.ast && (_ = i.ast.getLength()); - !(w >= C.length) && - (opExecute(o + 1, w), Z.state !== s.NOMATCH) && - Z.state !== s.EMPTY && - ((L += 1), (j += Z.phraseLength), (w += Z.phraseLength), L !== B.max); - - ); - Z.state === s.EMPTY || L >= B.min - ? ((Z.state = 0 === j ? s.EMPTY : s.MATCH), (Z.phraseLength = j)) - : ((Z.state = s.NOMATCH), - (Z.phraseLength = 0), - i.ast && i.ast.setLength(_)); - })(o, w); - break; - case s.RNM: - ((o, u) => { - let w, L, V; - const U = x[o], - z = _[U.index], - Y = j[z.index]; - if ( - ($ || - ((L = i.ast && i.ast.ruleDefined(U.index)), - L && ((w = i.ast.getLength()), i.ast.down(U.index, _[U.index].name))), - Y) - ) { - const o = C.length - u; - Y(Z, C, u, B), - validateRnmCallbackResult(z, Z, o, !0), - Z.state === s.ACTIVE && - ((V = x), - (x = z.opcodes), - opExecute(0, u), - (x = V), - Y(Z, C, u, B), - validateRnmCallbackResult(z, Z, o, !1)); - } else (V = x), (x = z.opcodes), opExecute(0, u, Z), (x = V); - $ || - (L && - (Z.state === s.NOMATCH - ? i.ast.setLength(w) - : i.ast.up(U.index, z.name, u, Z.phraseLength))); - })(o, w); - break; - case s.TRG: - ((o, i) => { - const u = x[o]; - (Z.state = s.NOMATCH), - i < C.length && - u.min <= C[i] && - C[i] <= u.max && - ((Z.state = s.MATCH), (Z.phraseLength = 1)); - })(o, w); - break; - case s.TBS: - ((o, i) => { - const u = x[o], - _ = u.string.length; - if (((Z.state = s.NOMATCH), i + _ <= C.length)) { - for (let s = 0; s < _; s += 1) if (C[i + s] !== u.string[s]) return; - (Z.state = s.MATCH), (Z.phraseLength = _); - } - })(o, w); - break; - case s.TLS: - ((o, i) => { - let u; - const _ = x[o]; - Z.state = s.NOMATCH; - const w = _.string.length; - if (0 !== w) { - if (i + w <= C.length) { - for (let s = 0; s < w; s += 1) - if ( - ((u = C[i + s]), u >= 65 && u <= 90 && (u += 32), u !== _.string[s]) - ) - return; - (Z.state = s.MATCH), (Z.phraseLength = w); - } - } else Z.state = s.EMPTY; - })(o, w); - break; - case s.UDT: - opUDT(o, w); - break; - case s.AND: - ((o, i) => { - switch ( - (($ += 1), opExecute(o + 1, i), ($ -= 1), (Z.phraseLength = 0), Z.state) - ) { - case s.EMPTY: - case s.MATCH: - Z.state = s.EMPTY; - break; - case s.NOMATCH: - Z.state = s.NOMATCH; - break; - default: - throw new Error(`opAND: invalid state ${Z.state}`); - } - })(o, w); - break; - case s.NOT: - ((o, i) => { - switch ( - (($ += 1), opExecute(o + 1, i), ($ -= 1), (Z.phraseLength = 0), Z.state) - ) { - case s.EMPTY: - case s.MATCH: - Z.state = s.NOMATCH; - break; - case s.NOMATCH: - Z.state = s.EMPTY; - break; - default: - throw new Error(`opNOT: invalid state ${Z.state}`); - } - })(o, w); - break; - default: - throw new Error(`${L}unrecognized operator`); - } - $ || (w + Z.phraseLength > Y && (Y = w + Z.phraseLength)), - i.stats && i.stats.collect(ee, Z), - i.trace && i.trace.up(ee, Z.state, w, Z.phraseLength), - (V -= 1); - }; - }, - PS = function fnast() { - const s = TS, - o = MS, - i = this; - let u, - _, - w, - x = 0; - const C = [], - j = [], - L = []; - function indent(s) { - let o = ''; - for (; s-- > 0; ) o += ' '; - return o; - } - (i.callbacks = []), - (i.init = (s, o, B) => { - let $; - (j.length = 0), (L.length = 0), (x = 0), (u = s), (_ = o), (w = B); - const V = []; - for ($ = 0; $ < u.length; $ += 1) V.push(u[$].lower); - for ($ = 0; $ < _.length; $ += 1) V.push(_[$].lower); - for (x = u.length + _.length, $ = 0; $ < x; $ += 1) C[$] = void 0; - for (const s in i.callbacks) - if (i.callbacks.hasOwnProperty(s)) { - const o = s.toLowerCase(); - if ((($ = V.indexOf(o)), $ < 0)) - throw new Error( - `parser.js: Ast()): init: node '${s}' not a rule or udt name` - ); - C[$] = i.callbacks[s]; - } - }), - (i.ruleDefined = (s) => !!C[s]), - (i.udtDefined = (s) => !!C[u.length + s]), - (i.down = (o, i) => { - const u = L.length; - return ( - j.push(u), - L.push({ - name: i, - thisIndex: u, - thatIndex: void 0, - state: s.SEM_PRE, - callbackIndex: o, - phraseIndex: void 0, - phraseLength: void 0, - stack: j.length - }), - u - ); - }), - (i.up = (o, i, u, _) => { - const w = L.length, - x = j.pop(); - return ( - L.push({ - name: i, - thisIndex: w, - thatIndex: x, - state: s.SEM_POST, - callbackIndex: o, - phraseIndex: u, - phraseLength: _, - stack: j.length - }), - (L[x].thatIndex = w), - (L[x].phraseIndex = u), - (L[x].phraseLength = _), - w - ); - }), - (i.translate = (o) => { - let i, u; - for (let _ = 0; _ < L.length; _ += 1) - (u = L[_]), - (i = C[u.callbackIndex]), - i && - (u.state === s.SEM_PRE - ? i(s.SEM_PRE, w, u.phraseIndex, u.phraseLength, o) - : i && i(s.SEM_POST, w, u.phraseIndex, u.phraseLength, o)); - }), - (i.setLength = (s) => { - (L.length = s), (j.length = s > 0 ? L[s - 1].stack : 0); - }), - (i.getLength = () => L.length), - (i.toXml = () => { - let i = '', - u = 0; - return ( - (i += '\n'), - (i += `\n`), - (i += '\x3c!-- input string --\x3e\n'), - (i += indent(u + 2)), - (i += o.charsToString(w)), - (i += '\n'), - L.forEach((_) => { - _.state === s.SEM_PRE - ? ((u += 1), - (i += indent(u)), - (i += `\n`), - (i += indent(u + 2)), - (i += o.charsToString(w, _.phraseIndex, _.phraseLength)), - (i += '\n')) - : ((i += indent(u)), - (i += `\x3c!-- name="${_.name}" --\x3e\n`), - (u -= 1)); - }), - (i += '\n'), - i - ); - }); - }, - MS = { - stringToChars: (s) => [...s].map((s) => s.codePointAt(0)), - charsToString: (s, o, i) => { - let u = s; - for (; !(void 0 === o || o < 0); ) { - if (void 0 === i) { - u = s.slice(o); - break; - } - if (i <= 0) return ''; - u = s.slice(o, o + i); - break; - } - return String.fromCodePoint(...u); - } - }, - TS = { - ALT: 1, - CAT: 2, - REP: 3, - RNM: 4, - TRG: 5, - TBS: 6, - TLS: 7, - UDT: 11, - AND: 12, - NOT: 13, - ACTIVE: 100, - MATCH: 101, - EMPTY: 102, - NOMATCH: 103, - SEM_PRE: 200, - SEM_POST: 201, - SEM_OK: 300, - idName: (s) => { - switch (s) { - case TS.ALT: - return 'ALT'; - case TS.CAT: - return 'CAT'; - case TS.REP: - return 'REP'; - case TS.RNM: - return 'RNM'; - case TS.TRG: - return 'TRG'; - case TS.TBS: - return 'TBS'; - case TS.TLS: - return 'TLS'; - case TS.UDT: - return 'UDT'; - case TS.AND: - return 'AND'; - case TS.NOT: - return 'NOT'; - case TS.ACTIVE: - return 'ACTIVE'; - case TS.EMPTY: - return 'EMPTY'; - case TS.MATCH: - return 'MATCH'; - case TS.NOMATCH: - return 'NOMATCH'; - case TS.SEM_PRE: - return 'SEM_PRE'; - case TS.SEM_POST: - return 'SEM_POST'; - case TS.SEM_OK: - return 'SEM_OK'; - default: - return 'UNRECOGNIZED STATE'; - } - } - }; - const server_url_template = (s, o, i, u, _) => { - if (s === TS.SEM_PRE) { - if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); - _.push(['server-url-template', MS.charsToString(o, i, u)]); - } - return TS.SEM_OK; - }, - callbacks_server_variable = (s, o, i, u, _) => { - if (s === TS.SEM_PRE) { - if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); - _.push(['server-variable', MS.charsToString(o, i, u)]); - } - return TS.SEM_OK; - }, - server_variable_name = (s, o, i, u, _) => { - if (s === TS.SEM_PRE) { - if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); - _.push(['server-variable-name', MS.charsToString(o, i, u)]); - } - return TS.SEM_OK; - }, - callbacks_literals = (s, o, i, u, _) => { - if (s === TS.SEM_PRE) { - if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); - _.push(['literals', MS.charsToString(o, i, u)]); - } - return TS.SEM_OK; - }, - NS = new (function grammar() { - (this.grammarObject = 'grammarObject'), - (this.rules = []), - (this.rules[0] = { - name: 'server-url-template', - lower: 'server-url-template', - index: 0, - isBkr: !1 - }), - (this.rules[1] = { - name: 'server-variable', - lower: 'server-variable', - index: 1, - isBkr: !1 - }), - (this.rules[2] = { - name: 'server-variable-name', - lower: 'server-variable-name', - index: 2, - isBkr: !1 - }), - (this.rules[3] = { name: 'literals', lower: 'literals', index: 3, isBkr: !1 }), - (this.rules[4] = { name: 'ALPHA', lower: 'alpha', index: 4, isBkr: !1 }), - (this.rules[5] = { name: 'DIGIT', lower: 'digit', index: 5, isBkr: !1 }), - (this.rules[6] = { name: 'HEXDIG', lower: 'hexdig', index: 6, isBkr: !1 }), - (this.rules[7] = { name: 'pct-encoded', lower: 'pct-encoded', index: 7, isBkr: !1 }), - (this.rules[8] = { name: 'unreserved', lower: 'unreserved', index: 8, isBkr: !1 }), - (this.rules[9] = { name: 'sub-delims', lower: 'sub-delims', index: 9, isBkr: !1 }), - (this.rules[10] = { name: 'ucschar', lower: 'ucschar', index: 10, isBkr: !1 }), - (this.rules[11] = { name: 'iprivate', lower: 'iprivate', index: 11, isBkr: !1 }), - (this.udts = []), - (this.rules[0].opcodes = []), - (this.rules[0].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[0].opcodes[1] = { type: 1, children: [2, 3] }), - (this.rules[0].opcodes[2] = { type: 4, index: 3 }), - (this.rules[0].opcodes[3] = { type: 4, index: 1 }), - (this.rules[1].opcodes = []), - (this.rules[1].opcodes[0] = { type: 2, children: [1, 2, 3] }), - (this.rules[1].opcodes[1] = { type: 7, string: [123] }), - (this.rules[1].opcodes[2] = { type: 4, index: 2 }), - (this.rules[1].opcodes[3] = { type: 7, string: [125] }), - (this.rules[2].opcodes = []), - (this.rules[2].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[2].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6] }), - (this.rules[2].opcodes[2] = { type: 4, index: 8 }), - (this.rules[2].opcodes[3] = { type: 4, index: 7 }), - (this.rules[2].opcodes[4] = { type: 4, index: 9 }), - (this.rules[2].opcodes[5] = { type: 7, string: [58] }), - (this.rules[2].opcodes[6] = { type: 7, string: [64] }), - (this.rules[3].opcodes = []), - (this.rules[3].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[3].opcodes[1] = { - type: 1, - children: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] - }), - (this.rules[3].opcodes[2] = { type: 6, string: [33] }), - (this.rules[3].opcodes[3] = { type: 5, min: 35, max: 36 }), - (this.rules[3].opcodes[4] = { type: 6, string: [38] }), - (this.rules[3].opcodes[5] = { type: 5, min: 40, max: 59 }), - (this.rules[3].opcodes[6] = { type: 6, string: [61] }), - (this.rules[3].opcodes[7] = { type: 5, min: 63, max: 91 }), - (this.rules[3].opcodes[8] = { type: 6, string: [93] }), - (this.rules[3].opcodes[9] = { type: 6, string: [95] }), - (this.rules[3].opcodes[10] = { type: 5, min: 97, max: 122 }), - (this.rules[3].opcodes[11] = { type: 6, string: [126] }), - (this.rules[3].opcodes[12] = { type: 4, index: 10 }), - (this.rules[3].opcodes[13] = { type: 4, index: 11 }), - (this.rules[3].opcodes[14] = { type: 4, index: 7 }), - (this.rules[4].opcodes = []), - (this.rules[4].opcodes[0] = { type: 1, children: [1, 2] }), - (this.rules[4].opcodes[1] = { type: 5, min: 65, max: 90 }), - (this.rules[4].opcodes[2] = { type: 5, min: 97, max: 122 }), - (this.rules[5].opcodes = []), - (this.rules[5].opcodes[0] = { type: 5, min: 48, max: 57 }), - (this.rules[6].opcodes = []), - (this.rules[6].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6, 7] }), - (this.rules[6].opcodes[1] = { type: 4, index: 5 }), - (this.rules[6].opcodes[2] = { type: 7, string: [97] }), - (this.rules[6].opcodes[3] = { type: 7, string: [98] }), - (this.rules[6].opcodes[4] = { type: 7, string: [99] }), - (this.rules[6].opcodes[5] = { type: 7, string: [100] }), - (this.rules[6].opcodes[6] = { type: 7, string: [101] }), - (this.rules[6].opcodes[7] = { type: 7, string: [102] }), - (this.rules[7].opcodes = []), - (this.rules[7].opcodes[0] = { type: 2, children: [1, 2, 3] }), - (this.rules[7].opcodes[1] = { type: 7, string: [37] }), - (this.rules[7].opcodes[2] = { type: 4, index: 6 }), - (this.rules[7].opcodes[3] = { type: 4, index: 6 }), - (this.rules[8].opcodes = []), - (this.rules[8].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6] }), - (this.rules[8].opcodes[1] = { type: 4, index: 4 }), - (this.rules[8].opcodes[2] = { type: 4, index: 5 }), - (this.rules[8].opcodes[3] = { type: 7, string: [45] }), - (this.rules[8].opcodes[4] = { type: 7, string: [46] }), - (this.rules[8].opcodes[5] = { type: 7, string: [95] }), - (this.rules[8].opcodes[6] = { type: 7, string: [126] }), - (this.rules[9].opcodes = []), - (this.rules[9].opcodes[0] = { - type: 1, - children: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }), - (this.rules[9].opcodes[1] = { type: 7, string: [33] }), - (this.rules[9].opcodes[2] = { type: 7, string: [36] }), - (this.rules[9].opcodes[3] = { type: 7, string: [38] }), - (this.rules[9].opcodes[4] = { type: 7, string: [39] }), - (this.rules[9].opcodes[5] = { type: 7, string: [40] }), - (this.rules[9].opcodes[6] = { type: 7, string: [41] }), - (this.rules[9].opcodes[7] = { type: 7, string: [42] }), - (this.rules[9].opcodes[8] = { type: 7, string: [43] }), - (this.rules[9].opcodes[9] = { type: 7, string: [44] }), - (this.rules[9].opcodes[10] = { type: 7, string: [59] }), - (this.rules[9].opcodes[11] = { type: 7, string: [61] }), - (this.rules[10].opcodes = []), - (this.rules[10].opcodes[0] = { - type: 1, - children: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] - }), - (this.rules[10].opcodes[1] = { type: 5, min: 160, max: 55295 }), - (this.rules[10].opcodes[2] = { type: 5, min: 63744, max: 64975 }), - (this.rules[10].opcodes[3] = { type: 5, min: 65008, max: 65519 }), - (this.rules[10].opcodes[4] = { type: 5, min: 65536, max: 131069 }), - (this.rules[10].opcodes[5] = { type: 5, min: 131072, max: 196605 }), - (this.rules[10].opcodes[6] = { type: 5, min: 196608, max: 262141 }), - (this.rules[10].opcodes[7] = { type: 5, min: 262144, max: 327677 }), - (this.rules[10].opcodes[8] = { type: 5, min: 327680, max: 393213 }), - (this.rules[10].opcodes[9] = { type: 5, min: 393216, max: 458749 }), - (this.rules[10].opcodes[10] = { type: 5, min: 458752, max: 524285 }), - (this.rules[10].opcodes[11] = { type: 5, min: 524288, max: 589821 }), - (this.rules[10].opcodes[12] = { type: 5, min: 589824, max: 655357 }), - (this.rules[10].opcodes[13] = { type: 5, min: 655360, max: 720893 }), - (this.rules[10].opcodes[14] = { type: 5, min: 720896, max: 786429 }), - (this.rules[10].opcodes[15] = { type: 5, min: 786432, max: 851965 }), - (this.rules[10].opcodes[16] = { type: 5, min: 851968, max: 917501 }), - (this.rules[10].opcodes[17] = { type: 5, min: 921600, max: 983037 }), - (this.rules[11].opcodes = []), - (this.rules[11].opcodes[0] = { type: 1, children: [1, 2, 3] }), - (this.rules[11].opcodes[1] = { type: 5, min: 57344, max: 63743 }), - (this.rules[11].opcodes[2] = { type: 5, min: 983040, max: 1048573 }), - (this.rules[11].opcodes[3] = { type: 5, min: 1048576, max: 1114109 }), - (this.toString = function toString() { - let s = ''; - return ( - (s += '; OpenAPI Server URL templating ABNF syntax\n'), - (s += 'server-url-template = 1*( literals / server-variable )\n'), - (s += 'server-variable = "{" server-variable-name "}"\n'), - (s += - 'server-variable-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n'), - (s += - 'literals = 1*( %x21 / %x23-24 / %x26 / %x28-3B / %x3D / %x3F-5B\n'), - (s += - ' / %x5D / %x5F / %x61-7A / %x7E / ucschar / iprivate\n'), - (s += ' / pct-encoded)\n'), - (s += ' ; any Unicode character except: CTL, SP,\n'), - (s += - ' ; DQUOTE, "\'", "%" (aside from pct-encoded),\n'), - (s += ' ; "<", ">", "\\", "^", "`", "{", "|", "}"\n'), - (s += '\n'), - (s += '; Characters definitions (from RFC 6570)\n'), - (s += 'ALPHA = %x41-5A / %x61-7A ; A-Z / a-z\n'), - (s += 'DIGIT = %x30-39 ; 0-9\n'), - (s += 'HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n'), - (s += ' ; case-insensitive\n'), - (s += '\n'), - (s += 'pct-encoded = "%" HEXDIG HEXDIG\n'), - (s += 'unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\n'), - (s += 'sub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n'), - (s += ' / "*" / "+" / "," / ";" / "="\n'), - (s += '\n'), - (s += 'ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF\n'), - (s += ' / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD\n'), - (s += ' / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD\n'), - (s += ' / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD\n'), - (s += ' / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD\n'), - (s += ' / %xD0000-DFFFD / %xE1000-EFFFD\n'), - (s += '\n'), - (s += 'iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD\n'), - '; OpenAPI Server URL templating ABNF syntax\nserver-url-template = 1*( literals / server-variable )\nserver-variable = "{" server-variable-name "}"\nserver-variable-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\nliterals = 1*( %x21 / %x23-24 / %x26 / %x28-3B / %x3D / %x3F-5B\n / %x5D / %x5F / %x61-7A / %x7E / ucschar / iprivate\n / pct-encoded)\n ; any Unicode character except: CTL, SP,\n ; DQUOTE, "\'", "%" (aside from pct-encoded),\n ; "<", ">", "\\", "^", "`", "{", "|", "}"\n\n; Characters definitions (from RFC 6570)\nALPHA = %x41-5A / %x61-7A ; A-Z / a-z\nDIGIT = %x30-39 ; 0-9\nHEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n ; case-insensitive\n\npct-encoded = "%" HEXDIG HEXDIG\nunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\nsub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n / "*" / "+" / "," / ";" / "="\n\nucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF\n / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD\n / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD\n / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD\n / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD\n / %xD0000-DFFFD / %xE1000-EFFFD\n\niprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD\n' - ); - }); - })(), - openapi_server_url_templating_es_parse = (s) => { - const o = new IS(); - (o.ast = new PS()), - (o.ast.callbacks['server-url-template'] = server_url_template), - (o.ast.callbacks['server-variable'] = callbacks_server_variable), - (o.ast.callbacks['server-variable-name'] = server_variable_name), - (o.ast.callbacks.literals = callbacks_literals); - return { result: o.parse(NS, 'server-url-template', s), ast: o.ast }; - }, - openapi_server_url_templating_es_test = (s, { strict: o = !1 } = {}) => { - try { - const i = openapi_server_url_templating_es_parse(s); - if (!i.result.success) return !1; - const u = []; - i.ast.translate(u); - const _ = u.some(([s]) => 'server-variable' === s); - if (!o && !_) - try { - return new URL(s, 'https://vladimirgorej.com'), !0; - } catch { - return !1; - } - return !o || _; - } catch { - return !1; - } - }, - encodeServerVariable = (s) => - ((s) => { - try { - return 'string' == typeof s && decodeURIComponent(s) !== s; - } catch { - return !1; - } - })(s) - ? s - : encodeURIComponent(s).replace(/%5B/g, '[').replace(/%5D/g, ']'), - RS = ['literals', 'server-variable-name'], - es_substitute = (s, o, i = {}) => { - const u = { ...{ encoder: encodeServerVariable }, ...i }, - _ = openapi_server_url_templating_es_parse(s); - if (!_.result.success) return s; - const w = []; - _.ast.translate(w); - const x = w - .filter(([s]) => RS.includes(s)) - .map(([s, i]) => - 'server-variable-name' === s - ? Object.hasOwn(o, i) - ? u.encoder(o[i], i) - : `{${i}}` - : i - ); - return x.join(''); - }; - const callbacks_slash = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['slash', MS.charsToString(o, i, u)]) : TS.SEM_POST, TS.SEM_OK - ), - path_template = (s, o, i, u, _) => { - if (s === TS.SEM_PRE) { - if (!1 === Array.isArray(_)) throw new Error("parser's user data must be an array"); - _.push(['path-template', MS.charsToString(o, i, u)]); - } - return TS.SEM_OK; - }, - callbacks_path = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['path', MS.charsToString(o, i, u)]) : TS.SEM_POST, TS.SEM_OK - ), - path_literal = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['path-literal', MS.charsToString(o, i, u)]) : TS.SEM_POST, - TS.SEM_OK - ), - callbacks_query = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['query', MS.charsToString(o, i, u)]) : TS.SEM_POST, TS.SEM_OK - ), - query_marker = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['query-marker', MS.charsToString(o, i, u)]) : TS.SEM_POST, - TS.SEM_OK - ), - callbacks_fragment = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['fragment', MS.charsToString(o, i, u)]) : TS.SEM_POST, - TS.SEM_OK - ), - fragment_marker = (s, o, i, u, _) => ( - s === TS.SEM_PRE ? _.push(['fragment-marker', MS.charsToString(o, i, u)]) : TS.SEM_POST, - TS.SEM_OK - ), - template_expression = (s, o, i, u, _) => ( - s === TS.SEM_PRE - ? _.push(['template-expression', MS.charsToString(o, i, u)]) - : TS.SEM_POST, - TS.SEM_OK - ), - template_expression_param_name = (s, o, i, u, _) => ( - s === TS.SEM_PRE - ? _.push(['template-expression-param-name', MS.charsToString(o, i, u)]) - : TS.SEM_POST, - TS.SEM_OK - ), - DS = new (function path_templating_grammar() { - (this.grammarObject = 'grammarObject'), - (this.rules = []), - (this.rules[0] = { - name: 'path-template', - lower: 'path-template', - index: 0, - isBkr: !1 - }), - (this.rules[1] = { name: 'path', lower: 'path', index: 1, isBkr: !1 }), - (this.rules[2] = { - name: 'path-segment', - lower: 'path-segment', - index: 2, - isBkr: !1 - }), - (this.rules[3] = { name: 'query', lower: 'query', index: 3, isBkr: !1 }), - (this.rules[4] = { - name: 'query-literal', - lower: 'query-literal', - index: 4, - isBkr: !1 - }), - (this.rules[5] = { - name: 'query-marker', - lower: 'query-marker', - index: 5, - isBkr: !1 - }), - (this.rules[6] = { name: 'fragment', lower: 'fragment', index: 6, isBkr: !1 }), - (this.rules[7] = { - name: 'fragment-literal', - lower: 'fragment-literal', - index: 7, - isBkr: !1 - }), - (this.rules[8] = { - name: 'fragment-marker', - lower: 'fragment-marker', - index: 8, - isBkr: !1 - }), - (this.rules[9] = { name: 'slash', lower: 'slash', index: 9, isBkr: !1 }), - (this.rules[10] = { - name: 'path-literal', - lower: 'path-literal', - index: 10, - isBkr: !1 - }), - (this.rules[11] = { - name: 'template-expression', - lower: 'template-expression', - index: 11, - isBkr: !1 - }), - (this.rules[12] = { - name: 'template-expression-param-name', - lower: 'template-expression-param-name', - index: 12, - isBkr: !1 - }), - (this.rules[13] = { name: 'unreserved', lower: 'unreserved', index: 13, isBkr: !1 }), - (this.rules[14] = { - name: 'pct-encoded', - lower: 'pct-encoded', - index: 14, - isBkr: !1 - }), - (this.rules[15] = { name: 'sub-delims', lower: 'sub-delims', index: 15, isBkr: !1 }), - (this.rules[16] = { name: 'ALPHA', lower: 'alpha', index: 16, isBkr: !1 }), - (this.rules[17] = { name: 'DIGIT', lower: 'digit', index: 17, isBkr: !1 }), - (this.rules[18] = { name: 'HEXDIG', lower: 'hexdig', index: 18, isBkr: !1 }), - (this.udts = []), - (this.rules[0].opcodes = []), - (this.rules[0].opcodes[0] = { type: 2, children: [1, 2, 6] }), - (this.rules[0].opcodes[1] = { type: 4, index: 1 }), - (this.rules[0].opcodes[2] = { type: 3, min: 0, max: 1 }), - (this.rules[0].opcodes[3] = { type: 2, children: [4, 5] }), - (this.rules[0].opcodes[4] = { type: 4, index: 5 }), - (this.rules[0].opcodes[5] = { type: 4, index: 3 }), - (this.rules[0].opcodes[6] = { type: 3, min: 0, max: 1 }), - (this.rules[0].opcodes[7] = { type: 2, children: [8, 9] }), - (this.rules[0].opcodes[8] = { type: 4, index: 8 }), - (this.rules[0].opcodes[9] = { type: 4, index: 6 }), - (this.rules[1].opcodes = []), - (this.rules[1].opcodes[0] = { type: 2, children: [1, 2, 6] }), - (this.rules[1].opcodes[1] = { type: 4, index: 9 }), - (this.rules[1].opcodes[2] = { type: 3, min: 0, max: 1 / 0 }), - (this.rules[1].opcodes[3] = { type: 2, children: [4, 5] }), - (this.rules[1].opcodes[4] = { type: 4, index: 2 }), - (this.rules[1].opcodes[5] = { type: 4, index: 9 }), - (this.rules[1].opcodes[6] = { type: 3, min: 0, max: 1 }), - (this.rules[1].opcodes[7] = { type: 4, index: 2 }), - (this.rules[2].opcodes = []), - (this.rules[2].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[2].opcodes[1] = { type: 1, children: [2, 3] }), - (this.rules[2].opcodes[2] = { type: 4, index: 10 }), - (this.rules[2].opcodes[3] = { type: 4, index: 11 }), - (this.rules[3].opcodes = []), - (this.rules[3].opcodes[0] = { type: 3, min: 0, max: 1 / 0 }), - (this.rules[3].opcodes[1] = { type: 4, index: 4 }), - (this.rules[4].opcodes = []), - (this.rules[4].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[4].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6, 7, 8, 9, 10] }), - (this.rules[4].opcodes[2] = { type: 4, index: 13 }), - (this.rules[4].opcodes[3] = { type: 4, index: 14 }), - (this.rules[4].opcodes[4] = { type: 4, index: 15 }), - (this.rules[4].opcodes[5] = { type: 7, string: [58] }), - (this.rules[4].opcodes[6] = { type: 7, string: [64] }), - (this.rules[4].opcodes[7] = { type: 7, string: [47] }), - (this.rules[4].opcodes[8] = { type: 7, string: [63] }), - (this.rules[4].opcodes[9] = { type: 7, string: [38] }), - (this.rules[4].opcodes[10] = { type: 7, string: [61] }), - (this.rules[5].opcodes = []), - (this.rules[5].opcodes[0] = { type: 7, string: [63] }), - (this.rules[6].opcodes = []), - (this.rules[6].opcodes[0] = { type: 3, min: 0, max: 1 / 0 }), - (this.rules[6].opcodes[1] = { type: 4, index: 7 }), - (this.rules[7].opcodes = []), - (this.rules[7].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[7].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6, 7, 8] }), - (this.rules[7].opcodes[2] = { type: 4, index: 13 }), - (this.rules[7].opcodes[3] = { type: 4, index: 14 }), - (this.rules[7].opcodes[4] = { type: 4, index: 15 }), - (this.rules[7].opcodes[5] = { type: 7, string: [58] }), - (this.rules[7].opcodes[6] = { type: 7, string: [64] }), - (this.rules[7].opcodes[7] = { type: 7, string: [47] }), - (this.rules[7].opcodes[8] = { type: 7, string: [63] }), - (this.rules[8].opcodes = []), - (this.rules[8].opcodes[0] = { type: 7, string: [35] }), - (this.rules[9].opcodes = []), - (this.rules[9].opcodes[0] = { type: 7, string: [47] }), - (this.rules[10].opcodes = []), - (this.rules[10].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[10].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6] }), - (this.rules[10].opcodes[2] = { type: 4, index: 13 }), - (this.rules[10].opcodes[3] = { type: 4, index: 14 }), - (this.rules[10].opcodes[4] = { type: 4, index: 15 }), - (this.rules[10].opcodes[5] = { type: 7, string: [58] }), - (this.rules[10].opcodes[6] = { type: 7, string: [64] }), - (this.rules[11].opcodes = []), - (this.rules[11].opcodes[0] = { type: 2, children: [1, 2, 3] }), - (this.rules[11].opcodes[1] = { type: 7, string: [123] }), - (this.rules[11].opcodes[2] = { type: 4, index: 12 }), - (this.rules[11].opcodes[3] = { type: 7, string: [125] }), - (this.rules[12].opcodes = []), - (this.rules[12].opcodes[0] = { type: 3, min: 1, max: 1 / 0 }), - (this.rules[12].opcodes[1] = { type: 1, children: [2, 3, 4, 5, 6] }), - (this.rules[12].opcodes[2] = { type: 4, index: 13 }), - (this.rules[12].opcodes[3] = { type: 4, index: 14 }), - (this.rules[12].opcodes[4] = { type: 4, index: 15 }), - (this.rules[12].opcodes[5] = { type: 7, string: [58] }), - (this.rules[12].opcodes[6] = { type: 7, string: [64] }), - (this.rules[13].opcodes = []), - (this.rules[13].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6] }), - (this.rules[13].opcodes[1] = { type: 4, index: 16 }), - (this.rules[13].opcodes[2] = { type: 4, index: 17 }), - (this.rules[13].opcodes[3] = { type: 7, string: [45] }), - (this.rules[13].opcodes[4] = { type: 7, string: [46] }), - (this.rules[13].opcodes[5] = { type: 7, string: [95] }), - (this.rules[13].opcodes[6] = { type: 7, string: [126] }), - (this.rules[14].opcodes = []), - (this.rules[14].opcodes[0] = { type: 2, children: [1, 2, 3] }), - (this.rules[14].opcodes[1] = { type: 7, string: [37] }), - (this.rules[14].opcodes[2] = { type: 4, index: 18 }), - (this.rules[14].opcodes[3] = { type: 4, index: 18 }), - (this.rules[15].opcodes = []), - (this.rules[15].opcodes[0] = { - type: 1, - children: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - }), - (this.rules[15].opcodes[1] = { type: 7, string: [33] }), - (this.rules[15].opcodes[2] = { type: 7, string: [36] }), - (this.rules[15].opcodes[3] = { type: 7, string: [38] }), - (this.rules[15].opcodes[4] = { type: 7, string: [39] }), - (this.rules[15].opcodes[5] = { type: 7, string: [40] }), - (this.rules[15].opcodes[6] = { type: 7, string: [41] }), - (this.rules[15].opcodes[7] = { type: 7, string: [42] }), - (this.rules[15].opcodes[8] = { type: 7, string: [43] }), - (this.rules[15].opcodes[9] = { type: 7, string: [44] }), - (this.rules[15].opcodes[10] = { type: 7, string: [59] }), - (this.rules[15].opcodes[11] = { type: 7, string: [61] }), - (this.rules[16].opcodes = []), - (this.rules[16].opcodes[0] = { type: 1, children: [1, 2] }), - (this.rules[16].opcodes[1] = { type: 5, min: 65, max: 90 }), - (this.rules[16].opcodes[2] = { type: 5, min: 97, max: 122 }), - (this.rules[17].opcodes = []), - (this.rules[17].opcodes[0] = { type: 5, min: 48, max: 57 }), - (this.rules[18].opcodes = []), - (this.rules[18].opcodes[0] = { type: 1, children: [1, 2, 3, 4, 5, 6, 7] }), - (this.rules[18].opcodes[1] = { type: 4, index: 17 }), - (this.rules[18].opcodes[2] = { type: 7, string: [97] }), - (this.rules[18].opcodes[3] = { type: 7, string: [98] }), - (this.rules[18].opcodes[4] = { type: 7, string: [99] }), - (this.rules[18].opcodes[5] = { type: 7, string: [100] }), - (this.rules[18].opcodes[6] = { type: 7, string: [101] }), - (this.rules[18].opcodes[7] = { type: 7, string: [102] }), - (this.toString = function toString() { - let s = ''; - return ( - (s += '; OpenAPI Path Templating ABNF syntax\n'), - (s += - 'path-template = path [ query-marker query ] [ fragment-marker fragment ]\n'), - (s += - 'path = slash *( path-segment slash ) [ path-segment ]\n'), - (s += - 'path-segment = 1*( path-literal / template-expression )\n'), - (s += 'query = *( query-literal )\n'), - (s += - 'query-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" / "&" / "=" )\n'), - (s += 'query-marker = "?"\n'), - (s += 'fragment = *( fragment-literal )\n'), - (s += - 'fragment-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" )\n'), - (s += 'fragment-marker = "#"\n'), - (s += 'slash = "/"\n'), - (s += - 'path-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n'), - (s += - 'template-expression = "{" template-expression-param-name "}"\n'), - (s += - 'template-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n'), - (s += '\n'), - (s += '; Characters definitions (from RFC 3986)\n'), - (s += 'unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\n'), - (s += 'pct-encoded = "%" HEXDIG HEXDIG\n'), - (s += 'sub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n'), - (s += ' / "*" / "+" / "," / ";" / "="\n'), - (s += 'ALPHA = %x41-5A / %x61-7A ; A-Z / a-z\n'), - (s += 'DIGIT = %x30-39 ; 0-9\n'), - (s += 'HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n'), - '; OpenAPI Path Templating ABNF syntax\npath-template = path [ query-marker query ] [ fragment-marker fragment ]\npath = slash *( path-segment slash ) [ path-segment ]\npath-segment = 1*( path-literal / template-expression )\nquery = *( query-literal )\nquery-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" / "&" / "=" )\nquery-marker = "?"\nfragment = *( fragment-literal )\nfragment-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" )\nfragment-marker = "#"\nslash = "/"\npath-literal = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\ntemplate-expression = "{" template-expression-param-name "}"\ntemplate-expression-param-name = 1*( unreserved / pct-encoded / sub-delims / ":" / "@" )\n\n; Characters definitions (from RFC 3986)\nunreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"\npct-encoded = "%" HEXDIG HEXDIG\nsub-delims = "!" / "$" / "&" / "\'" / "(" / ")"\n / "*" / "+" / "," / ";" / "="\nALPHA = %x41-5A / %x61-7A ; A-Z / a-z\nDIGIT = %x30-39 ; 0-9\nHEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"\n' - ); - }); - })(), - openapi_path_templating_es_parse = (s) => { - const o = new IS(); - (o.ast = new PS()), - (o.ast.callbacks['path-template'] = path_template), - (o.ast.callbacks.path = callbacks_path), - (o.ast.callbacks.query = callbacks_query), - (o.ast.callbacks['query-marker'] = query_marker), - (o.ast.callbacks.fragment = callbacks_fragment), - (o.ast.callbacks['fragment-marker'] = fragment_marker), - (o.ast.callbacks.slash = callbacks_slash), - (o.ast.callbacks['path-literal'] = path_literal), - (o.ast.callbacks['template-expression'] = template_expression), - (o.ast.callbacks['template-expression-param-name'] = template_expression_param_name); - return { result: o.parse(DS, 'path-template', s), ast: o.ast }; - }, - encodePathComponent = (s) => - ((s) => { - try { - return 'string' == typeof s && decodeURIComponent(s) !== s; - } catch { - return !1; - } - })(s) - ? s - : encodeURIComponent(s).replace(/%5B/g, '[').replace(/%5D/g, ']'), - LS = [ - 'slash', - 'path-literal', - 'query-marker', - 'query-literal', - 'template-expression-param-name' - ], - openapi_path_templating_es_resolve = (s, o, i = {}) => { - const u = { ...{ encoder: encodePathComponent }, ...i }, - _ = openapi_path_templating_es_parse(s); - if (!_.result.success) return s; - const w = []; - _.ast.translate(w); - const x = w - .filter(([s]) => LS.includes(s)) - .map(([s, i]) => - 'template-expression-param-name' === s - ? Object.hasOwn(o, i) - ? u.encoder(o[i], i) - : `{${i}}` - : i - ); - return x.join(''); - }, - BS = { - body: function bodyBuilder({ req: s, value: o }) { - void 0 !== o && (s.body = o); - }, - header: function headerBuilder({ req: s, parameter: o, value: i }) { - (s.headers = s.headers || {}), void 0 !== i && (s.headers[o.name] = i); - }, - query: function queryBuilder({ req: s, value: o, parameter: i }) { - (s.query = s.query || {}), !1 === o && 'boolean' === i.type && (o = 'false'); - 0 === o && ['number', 'integer'].indexOf(i.type) > -1 && (o = '0'); - if (o) s.query[i.name] = { collectionFormat: i.collectionFormat, value: o }; - else if (i.allowEmptyValue && void 0 !== o) { - const o = i.name; - (s.query[o] = s.query[o] || {}), (s.query[o].allowEmptyValue = !0); - } - }, - path: function pathBuilder({ req: s, value: o, parameter: i, baseURL: u }) { - if (void 0 !== o) { - const _ = s.url.replace(u, ''), - w = openapi_path_templating_es_resolve(_, { [i.name]: o }); - s.url = u + w; - } - }, - formData: function formDataBuilder({ req: s, value: o, parameter: i }) { - !1 === o && 'boolean' === i.type && (o = 'false'); - 0 === o && ['number', 'integer'].indexOf(i.type) > -1 && (o = '0'); - if (o) - (s.form = s.form || {}), - (s.form[i.name] = { collectionFormat: i.collectionFormat, value: o }); - else if (i.allowEmptyValue && void 0 !== o) { - s.form = s.form || {}; - const o = i.name; - (s.form[o] = s.form[o] || {}), (s.form[o].allowEmptyValue = !0); - } - } - }; - function serialize(s, o) { - return o.includes('application/json') - ? 'string' == typeof s - ? s - : (Array.isArray(s) && - (s = s.map((s) => { - try { - return JSON.parse(s); - } catch (o) { - return s; - } - })), - JSON.stringify(s)) - : String(s); - } - function parameter_builders_path({ req: s, value: o, parameter: i, baseURL: u }) { - const { name: _, style: w, explode: x, content: C } = i; - if (void 0 === o) return; - const j = s.url.replace(u, ''); - let L; - if (C) { - const s = Object.keys(C)[0]; - L = openapi_path_templating_es_resolve( - j, - { [_]: o }, - { encoder: (o) => encodeCharacters(serialize(o, s)) } - ); - } else - L = openapi_path_templating_es_resolve( - j, - { [_]: o }, - { - encoder: (s) => - stylize({ - key: i.name, - value: s, - style: w || 'simple', - explode: x || !1, - escape: 'reserved' - }) - } - ); - s.url = u + L; - } - function parameter_builders_query({ req: s, value: o, parameter: i }) { - if (((s.query = s.query || {}), void 0 !== o && i.content)) { - const u = serialize(o, Object.keys(i.content)[0]); - if (u) s.query[i.name] = u; - else if (i.allowEmptyValue) { - const o = i.name; - (s.query[o] = s.query[o] || {}), (s.query[o].allowEmptyValue = !0); - } - } else if ((!1 === o && (o = 'false'), 0 === o && (o = '0'), o)) { - const { style: u, explode: _, allowReserved: w } = i; - s.query[i.name] = { - value: o, - serializationOption: { style: u, explode: _, allowReserved: w } - }; - } else if (i.allowEmptyValue && void 0 !== o) { - const o = i.name; - (s.query[o] = s.query[o] || {}), (s.query[o].allowEmptyValue = !0); - } - } - const FS = ['accept', 'authorization', 'content-type']; - function parameter_builders_header({ req: s, parameter: o, value: i }) { - if (((s.headers = s.headers || {}), !(FS.indexOf(o.name.toLowerCase()) > -1))) - if (void 0 !== i && o.content) { - const u = Object.keys(o.content)[0]; - s.headers[o.name] = serialize(i, u); - } else - void 0 === i || - (Array.isArray(i) && 0 === i.length) || - (s.headers[o.name] = stylize({ - key: o.name, - value: i, - style: o.style || 'simple', - explode: void 0 !== o.explode && o.explode, - escape: !1 - })); - } - function parameter_builders_cookie({ req: s, parameter: o, value: i }) { - s.headers = s.headers || {}; - const u = typeof i; - if (void 0 !== i && o.content) { - const u = Object.keys(o.content)[0]; - s.headers.Cookie = `${o.name}=${serialize(i, u)}`; - } else if (void 0 !== i && (!Array.isArray(i) || 0 !== i.length)) { - const _ = 'object' === u && !Array.isArray(i) && o.explode ? '' : `${o.name}=`; - s.headers.Cookie = - _ + - stylize({ - key: o.name, - value: i, - escape: !1, - style: o.style || 'form', - explode: void 0 !== o.explode && o.explode - }); - } - } - const qS = - 'undefined' != typeof globalThis - ? globalThis - : 'undefined' != typeof self - ? self - : window, - { btoa: $S } = qS, - VS = $S; - function buildRequest(s, o) { - const { - operation: i, - requestBody: u, - securities: _, - spec: w, - attachContentTypeForEmptyPayload: x - } = s; - let { requestContentType: C } = s; - o = (function applySecurities({ - request: s, - securities: o = {}, - operation: i = {}, - spec: u - }) { - var _; - const w = { ...s }, - { authorized: x = {} } = o, - C = i.security || u.security || [], - j = x && !!Object.keys(x).length, - L = - (null == u || null === (_ = u.components) || void 0 === _ - ? void 0 - : _.securitySchemes) || {}; - if ( - ((w.headers = w.headers || {}), - (w.query = w.query || {}), - !Object.keys(o).length || - !j || - !C || - (Array.isArray(i.security) && !i.security.length)) - ) - return s; - return ( - C.forEach((s) => { - Object.keys(s).forEach((s) => { - const o = x[s], - i = L[s]; - if (!o) return; - const u = o.value || o, - { type: _ } = i; - if (o) - if ('apiKey' === _) - 'query' === i.in && (w.query[i.name] = u), - 'header' === i.in && (w.headers[i.name] = u), - 'cookie' === i.in && (w.cookies[i.name] = u); - else if ('http' === _) { - if (/^basic$/i.test(i.scheme)) { - const s = u.username || '', - o = u.password || '', - i = VS(`${s}:${o}`); - w.headers.Authorization = `Basic ${i}`; - } - /^bearer$/i.test(i.scheme) && (w.headers.Authorization = `Bearer ${u}`); - } else if ('oauth2' === _ || 'openIdConnect' === _) { - const s = o.token || {}, - u = s[i['x-tokenName'] || 'access_token']; - let _ = s.token_type; - (_ && 'bearer' !== _.toLowerCase()) || (_ = 'Bearer'), - (w.headers.Authorization = `${_} ${u}`); - } - }); - }), - w - ); - })({ request: o, securities: _, operation: i, spec: w }); - const j = i.requestBody || {}, - L = Object.keys(j.content || {}), - B = C && L.indexOf(C) > -1; - if (u || x) { - if (C && B) o.headers['Content-Type'] = C; - else if (!C) { - const s = L[0]; - s && ((o.headers['Content-Type'] = s), (C = s)); - } - } else C && B && (o.headers['Content-Type'] = C); - if (!s.responseContentType && i.responses) { - const s = Object.entries(i.responses) - .filter(([s, o]) => { - const i = parseInt(s, 10); - return i >= 200 && i < 300 && ku(o.content); - }) - .reduce((s, [, o]) => s.concat(Object.keys(o.content)), []); - s.length > 0 && (o.headers.accept = s.join(', ')); - } - if (u) - if (C) { - if (L.indexOf(C) > -1) - if ('application/x-www-form-urlencoded' === C || 'multipart/form-data' === C) - if ('object' == typeof u) { - var $, V; - const s = - null !== - ($ = null === (V = j.content[C]) || void 0 === V ? void 0 : V.encoding) && - void 0 !== $ - ? $ - : {}; - (o.form = {}), - Object.keys(u).forEach((i) => { - let _; - try { - _ = JSON.parse(u[i]); - } catch { - _ = u[i]; - } - o.form[i] = { value: _, encoding: s[i] || {} }; - }); - } else if ('string' == typeof u) { - var U, z; - const s = - null !== - (U = null === (z = j.content[C]) || void 0 === z ? void 0 : z.encoding) && - void 0 !== U - ? U - : {}; - try { - o.form = {}; - const i = JSON.parse(u); - Object.entries(i).forEach(([i, u]) => { - o.form[i] = { value: u, encoding: s[i] || {} }; - }); - } catch { - o.form = u; - } - } else o.form = u; - else o.body = u; - } else o.body = u; - return o; - } - function build_request_buildRequest(s, o) { - const { - spec: i, - operation: u, - securities: _, - requestContentType: w, - responseContentType: x, - attachContentTypeForEmptyPayload: C - } = s; - if ( - ((o = (function build_request_applySecurities({ - request: s, - securities: o = {}, - operation: i = {}, - spec: u - }) { - const _ = { ...s }, - { authorized: w = {}, specSecurity: x = [] } = o, - C = i.security || x, - j = w && !!Object.keys(w).length, - L = u.securityDefinitions; - if ( - ((_.headers = _.headers || {}), - (_.query = _.query || {}), - !Object.keys(o).length || - !j || - !C || - (Array.isArray(i.security) && !i.security.length)) - ) - return s; - return ( - C.forEach((s) => { - Object.keys(s).forEach((s) => { - const o = w[s]; - if (!o) return; - const { token: i } = o, - u = o.value || o, - x = L[s], - { type: C } = x, - j = x['x-tokenName'] || 'access_token', - B = i && i[j]; - let $ = i && i.token_type; - if (o) - if ('apiKey' === C) { - const s = 'query' === x.in ? 'query' : 'headers'; - (_[s] = _[s] || {}), (_[s][x.name] = u); - } else if ('basic' === C) - if (u.header) _.headers.authorization = u.header; - else { - const s = u.username || '', - o = u.password || ''; - (u.base64 = VS(`${s}:${o}`)), - (_.headers.authorization = `Basic ${u.base64}`); - } - else - 'oauth2' === C && - B && - (($ = $ && 'bearer' !== $.toLowerCase() ? $ : 'Bearer'), - (_.headers.authorization = `${$} ${B}`)); - }); - }), - _ - ); - })({ request: o, securities: _, operation: u, spec: i })), - o.body || o.form || C) - ) - w - ? (o.headers['Content-Type'] = w) - : Array.isArray(u.consumes) - ? ([o.headers['Content-Type']] = u.consumes) - : Array.isArray(i.consumes) - ? ([o.headers['Content-Type']] = i.consumes) - : u.parameters && u.parameters.filter((s) => 'file' === s.type).length - ? (o.headers['Content-Type'] = 'multipart/form-data') - : u.parameters && - u.parameters.filter((s) => 'formData' === s.in).length && - (o.headers['Content-Type'] = 'application/x-www-form-urlencoded'); - else if (w) { - const s = u.parameters && u.parameters.filter((s) => 'body' === s.in).length > 0, - i = u.parameters && u.parameters.filter((s) => 'formData' === s.in).length > 0; - (s || i) && (o.headers['Content-Type'] = w); - } - return ( - !x && - Array.isArray(u.produces) && - u.produces.length > 0 && - (o.headers.accept = u.produces.join(', ')), - o - ); - } - function idFromPathMethodLegacy(s, o) { - return `${o.toLowerCase()}-${s}`; - } - const arrayOrEmpty = (s) => (Array.isArray(s) ? s : []), - parseURIReference = (s) => { - try { - return new URL(s); - } catch { - const o = new URL(s, Nc), - i = String(s).startsWith('/') ? o.pathname : o.pathname.substring(1); - return { - hash: o.hash, - host: '', - hostname: '', - href: '', - origin: '', - password: '', - pathname: i, - port: '', - protocol: '', - search: o.search, - searchParams: o.searchParams - }; - } - }; - class OperationNotFoundError extends Jo {} - const US = { buildRequest: execute_buildRequest }; - function execute_execute({ - http: s, - fetch: o, - spec: i, - operationId: u, - pathName: _, - method: w, - parameters: x, - securities: C, - ...j - }) { - const L = s || o || http_http; - _ && w && !u && (u = idFromPathMethodLegacy(_, w)); - const B = US.buildRequest({ - spec: i, - operationId: u, - parameters: x, - securities: C, - http: L, - ...j - }); - return ( - B.body && (ku(B.body) || Array.isArray(B.body)) && (B.body = JSON.stringify(B.body)), - L(B) - ); - } - function execute_buildRequest(s) { - var o; - const { - spec: i, - operationId: u, - responseContentType: _, - scheme: w, - requestInterceptor: x, - responseInterceptor: C, - contextUrl: j, - userFetch: L, - server: B, - serverVariables: $, - http: V, - signal: U, - serverVariableEncoder: z - } = s; - let { parameters: Y, parameterBuilders: Z, baseURL: ee } = s; - const ie = isOpenAPI3(i); - Z || (Z = ie ? fe : BS); - let ae = { - url: '', - credentials: V && V.withCredentials ? 'include' : 'same-origin', - headers: {}, - cookies: {} - }; - U && (ae.signal = U), - x && (ae.requestInterceptor = x), - C && (ae.responseInterceptor = C), - L && (ae.userFetch = L); - const le = (function getOperationRaw(s, o) { - return s && s.paths - ? (function findOperation(s, o) { - return ( - (function eachOperation(s, o, i) { - if (!s || 'object' != typeof s || !s.paths || 'object' != typeof s.paths) - return null; - const { paths: u } = s; - for (const _ in u) - for (const w in u[_]) { - if ('PARAMETERS' === w.toUpperCase()) continue; - const x = u[_][w]; - if (!x || 'object' != typeof x) continue; - const C = { spec: s, pathName: _, method: w.toUpperCase(), operation: x }, - j = o(C); - if (i && j) return C; - } - })(s, o, !0) || null - ); - })(s, ({ pathName: s, method: i, operation: u }) => { - if (!u || 'object' != typeof u) return !1; - const _ = u.operationId; - return [opId(u, s, i), idFromPathMethodLegacy(s, i), _].some((s) => s && s === o); - }) - : null; - })(i, u); - if (!le) throw new OperationNotFoundError(`Operation ${u} not found`); - const { operation: ce = {}, method: pe, pathName: de } = le; - if ( - ((ee = - null !== (o = ee) && void 0 !== o - ? o - : (function baseUrl(s) { - const o = isOpenAPI3(s.spec); - return o - ? (function oas3BaseUrl({ - spec: s, - pathName: o, - method: i, - server: u, - contextUrl: _, - serverVariables: w = {}, - serverVariableEncoder: x - }) { - var C, j; - let L, - B = [], - $ = ''; - const V = - null == s || - null === (C = s.paths) || - void 0 === C || - null === (C = C[o]) || - void 0 === C || - null === (C = C[(i || '').toLowerCase()]) || - void 0 === C - ? void 0 - : C.servers, - U = - null == s || - null === (j = s.paths) || - void 0 === j || - null === (j = j[o]) || - void 0 === j - ? void 0 - : j.servers, - z = null == s ? void 0 : s.servers; - (B = isNonEmptyServerList(V) - ? V - : isNonEmptyServerList(U) - ? U - : isNonEmptyServerList(z) - ? z - : [Rc]), - u && ((L = B.find((s) => s.url === u)), L && ($ = u)); - $ || (([L] = B), ($ = L.url)); - if (openapi_server_url_templating_es_test($, { strict: !0 })) { - const s = Object.entries({ ...L.variables }).reduce( - (s, [o, i]) => ((s[o] = i.default), s), - {} - ); - $ = es_substitute( - $, - { ...s, ...w }, - { encoder: 'function' == typeof x ? x : Ip } - ); - } - return (function buildOas3UrlWithContext(s = '', o = '') { - const i = parseURIReference(s && o ? resolve(o, s) : s), - u = parseURIReference(o), - _ = stripNonAlpha(i.protocol) || stripNonAlpha(u.protocol), - w = i.host || u.host, - x = i.pathname; - let C; - C = _ && w ? `${_}://${w + x}` : x; - return '/' === C[C.length - 1] ? C.slice(0, -1) : C; - })($, _); - })(s) - : (function swagger2BaseUrl({ spec: s, scheme: o, contextUrl: i = '' }) { - const u = parseURIReference(i), - _ = Array.isArray(s.schemes) ? s.schemes[0] : null, - w = o || _ || stripNonAlpha(u.protocol) || 'http', - x = s.host || u.host || '', - C = s.basePath || ''; - let j; - j = w && x ? `${w}://${x + C}` : C; - return '/' === j[j.length - 1] ? j.slice(0, -1) : j; - })(s); - })({ - spec: i, - scheme: w, - contextUrl: j, - server: B, - serverVariables: $, - pathName: de, - method: pe, - serverVariableEncoder: z - })), - (ae.url += ee), - !u) - ) - return delete ae.cookies, ae; - (ae.url += de), (ae.method = `${pe}`.toUpperCase()), (Y = Y || {}); - const ye = i.paths[de] || {}; - _ && (ae.headers.accept = _); - const be = ((s) => { - const o = {}; - s.forEach((s) => { - o[s.in] || (o[s.in] = {}), (o[s.in][s.name] = s); - }); - const i = []; - return ( - Object.keys(o).forEach((s) => { - Object.keys(o[s]).forEach((u) => { - i.push(o[s][u]); - }); - }), - i - ); - })([].concat(arrayOrEmpty(ce.parameters)).concat(arrayOrEmpty(ye.parameters))); - be.forEach((s) => { - const o = Z[s.in]; - let u; - if ( - ('body' === s.in && s.schema && s.schema.properties && (u = Y), - (u = s && s.name && Y[s.name]), - void 0 === u - ? (u = s && s.name && Y[`${s.in}.${s.name}`]) - : ((s, o) => o.filter((o) => o.name === s))(s.name, be).length > 1 && - console.warn( - `Parameter '${s.name}' is ambiguous because the defined spec has more than one parameter with the name: '${s.name}' and the passed-in parameter values did not define an 'in' value.` - ), - null !== u) - ) { - if ( - (void 0 !== s.default && void 0 === u && (u = s.default), - void 0 === u && s.required && !s.allowEmptyValue) - ) - throw new Error(`Required parameter ${s.name} is not provided`); - if (ie && s.schema && 'object' === s.schema.type && 'string' == typeof u) - try { - u = JSON.parse(u); - } catch (s) { - throw new Error('Could not parse object parameter value string as JSON'); - } - o && o({ req: ae, parameter: s, value: u, operation: ce, spec: i, baseURL: ee }); - } - }); - const _e = { ...s, operation: ce }; - if ( - ((ae = ie ? buildRequest(_e, ae) : build_request_buildRequest(_e, ae)), - ae.cookies && Object.keys(ae.cookies).length) - ) { - const s = Object.keys(ae.cookies).reduce((s, o) => { - const i = ae.cookies[o]; - return s + (s ? '&' : '') + jS.serialize(o, i); - }, ''); - ae.headers.Cookie = s; - } - return ae.cookies && delete ae.cookies, serializeRequest(ae); - } - const stripNonAlpha = (s) => (s ? s.replace(/\W/g, '') : null); - const isNonEmptyServerList = (s) => Array.isArray(s) && s.length > 0; - const makeResolveSubtree = - (s) => - async (o, i, u = {}) => - (async (s, o, i = {}) => { - const { - returnEntireTree: u, - baseDoc: _, - requestInterceptor: w, - responseInterceptor: x, - parameterMacro: C, - modelPropertyMacro: j, - useCircularStructures: L, - strategies: B - } = i, - $ = { - spec: s, - pathDiscriminator: o, - baseDoc: _, - requestInterceptor: w, - responseInterceptor: x, - parameterMacro: C, - modelPropertyMacro: j, - useCircularStructures: L, - strategies: B - }, - V = B.find((o) => o.match(s)).normalize(s), - U = await AS({ - spec: V, - ...$, - allowMetaPatches: !0, - skipNormalization: !isOpenAPI31(s) - }); - return ( - !u && - Array.isArray(o) && - o.length && - (U.spec = o.reduce((s, o) => (null == s ? void 0 : s[o]), U.spec) || null), - U - ); - })(o, i, { ...s, ...u }), - zS = - (makeResolveSubtree({ strategies: [fu, hu, uu] }), - (s, o) => - (...i) => { - s(...i); - const u = o.getConfigs().withCredentials; - o.fn.fetch.withCredentials = u; - }); - function swagger_client({ configs: s, getConfigs: o }) { - return { - fn: { - fetch: - ((i = http_http), - (u = s.preFetch), - (_ = s.postFetch), - (_ = _ || ((s) => s)), - (u = u || ((s) => s)), - (s) => ( - 'string' == typeof s && (s = { url: s }), - (s = serializeRequest(s)), - (s = u(s)), - _(i(s)) - )), - buildRequest: execute_buildRequest, - execute: execute_execute, - resolve: makeResolve({ strategies: [OS, fu, hu, uu] }), - resolveSubtree: async (s, i, u = {}) => { - const _ = o(), - w = { - modelPropertyMacro: _.modelPropertyMacro, - parameterMacro: _.parameterMacro, - requestInterceptor: _.requestInterceptor, - responseInterceptor: _.responseInterceptor, - strategies: [OS, fu, hu, uu] - }; - return makeResolveSubtree(w)(s, i, u); - }, - serializeRes: serializeResponse, - opId - }, - statePlugins: { configs: { wrapActions: { loaded: zS } } } - }; - var i, u, _; - } - function util() { - return { fn: { shallowEqualKeys } }; - } - var WS = __webpack_require__(40961), - KS = __webpack_require__(78418), - HS = Pe, - JS = Symbol.for('react-redux-context'), - GS = 'undefined' != typeof globalThis ? globalThis : {}; - function getContext() { - if (!HS.createContext) return {}; - const s = GS[JS] ?? (GS[JS] = new Map()); - let o = s.get(HS.createContext); - return o || ((o = HS.createContext(null)), s.set(HS.createContext, o)), o; - } - var YS = getContext(), - notInitialized = () => { - throw new Error('uSES not initialized!'); - }; - var XS = Symbol.for('react.element'), - ZS = Symbol.for('react.portal'), - QS = Symbol.for('react.fragment'), - ex = Symbol.for('react.strict_mode'), - tx = Symbol.for('react.profiler'), - rx = Symbol.for('react.provider'), - nx = Symbol.for('react.context'), - sx = Symbol.for('react.server_context'), - ox = Symbol.for('react.forward_ref'), - ix = Symbol.for('react.suspense'), - ax = Symbol.for('react.suspense_list'), - lx = Symbol.for('react.memo'), - cx = Symbol.for('react.lazy'), - ux = (Symbol.for('react.offscreen'), Symbol.for('react.client.reference'), ox), - px = lx; - function typeOf(s) { - if ('object' == typeof s && null !== s) { - const o = s.$$typeof; - switch (o) { - case XS: { - const i = s.type; - switch (i) { - case QS: - case tx: - case ex: - case ix: - case ax: - return i; - default: { - const s = i && i.$$typeof; - switch (s) { - case sx: - case nx: - case ox: - case cx: - case lx: - case rx: - return s; - default: - return o; - } - } - } - } - case ZS: - return o; - } - } - } - function pureFinalPropsSelectorFactory( - s, - o, - i, - u, - { areStatesEqual: _, areOwnPropsEqual: w, areStatePropsEqual: x } - ) { - let C, - j, - L, - B, - $, - V = !1; - function handleSubsequentCalls(V, U) { - const z = !w(U, j), - Y = !_(V, C, U, j); - return ( - (C = V), - (j = U), - z && Y - ? (function handleNewPropsAndNewState() { - return (L = s(C, j)), o.dependsOnOwnProps && (B = o(u, j)), ($ = i(L, B, j)), $; - })() - : z - ? (function handleNewProps() { - return ( - s.dependsOnOwnProps && (L = s(C, j)), - o.dependsOnOwnProps && (B = o(u, j)), - ($ = i(L, B, j)), - $ - ); - })() - : Y - ? (function handleNewState() { - const o = s(C, j), - u = !x(o, L); - return (L = o), u && ($ = i(L, B, j)), $; - })() - : $ - ); - } - return function pureFinalPropsSelector(_, w) { - return V - ? handleSubsequentCalls(_, w) - : (function handleFirstCall(_, w) { - return ( - (C = _), (j = w), (L = s(C, j)), (B = o(u, j)), ($ = i(L, B, j)), (V = !0), $ - ); - })(_, w); - }; - } - function wrapMapToPropsConstant(s) { - return function initConstantSelector(o) { - const i = s(o); - function constantSelector() { - return i; - } - return (constantSelector.dependsOnOwnProps = !1), constantSelector; - }; - } - function getDependsOnOwnProps(s) { - return s.dependsOnOwnProps ? Boolean(s.dependsOnOwnProps) : 1 !== s.length; - } - function wrapMapToPropsFunc(s, o) { - return function initProxySelector(o, { displayName: i }) { - const u = function mapToPropsProxy(s, o) { - return u.dependsOnOwnProps ? u.mapToProps(s, o) : u.mapToProps(s, void 0); - }; - return ( - (u.dependsOnOwnProps = !0), - (u.mapToProps = function detectFactoryAndVerify(o, i) { - (u.mapToProps = s), (u.dependsOnOwnProps = getDependsOnOwnProps(s)); - let _ = u(o, i); - return ( - 'function' == typeof _ && - ((u.mapToProps = _), - (u.dependsOnOwnProps = getDependsOnOwnProps(_)), - (_ = u(o, i))), - _ - ); - }), - u - ); - }; - } - function createInvalidArgFactory(s, o) { - return (i, u) => { - throw new Error( - `Invalid value of type ${typeof s} for ${o} argument when connecting component ${u.wrappedComponentName}.` - ); - }; - } - function defaultMergeProps(s, o, i) { - return { ...i, ...s, ...o }; - } - function defaultNoopBatch(s) { - s(); - } - var hx = { notify() {}, get: () => [] }; - function createSubscription(s, o) { - let i, - u = hx, - _ = 0, - w = !1; - function handleChangeWrapper() { - x.onStateChange && x.onStateChange(); - } - function trySubscribe() { - _++, - i || - ((i = o ? o.addNestedSub(handleChangeWrapper) : s.subscribe(handleChangeWrapper)), - (u = (function createListenerCollection() { - let s = null, - o = null; - return { - clear() { - (s = null), (o = null); - }, - notify() { - defaultNoopBatch(() => { - let o = s; - for (; o; ) o.callback(), (o = o.next); - }); - }, - get() { - const o = []; - let i = s; - for (; i; ) o.push(i), (i = i.next); - return o; - }, - subscribe(i) { - let u = !0; - const _ = (o = { callback: i, next: null, prev: o }); - return ( - _.prev ? (_.prev.next = _) : (s = _), - function unsubscribe() { - u && - null !== s && - ((u = !1), - _.next ? (_.next.prev = _.prev) : (o = _.prev), - _.prev ? (_.prev.next = _.next) : (s = _.next)); - } - ); - } - }; - })())); - } - function tryUnsubscribe() { - _--, i && 0 === _ && (i(), (i = void 0), u.clear(), (u = hx)); - } - const x = { - addNestedSub: function addNestedSub(s) { - trySubscribe(); - const o = u.subscribe(s); - let i = !1; - return () => { - i || ((i = !0), o(), tryUnsubscribe()); - }; - }, - notifyNestedSubs: function notifyNestedSubs() { - u.notify(); - }, - handleChangeWrapper, - isSubscribed: function isSubscribed() { - return w; - }, - trySubscribe: function trySubscribeSelf() { - w || ((w = !0), trySubscribe()); - }, - tryUnsubscribe: function tryUnsubscribeSelf() { - w && ((w = !1), tryUnsubscribe()); - }, - getListeners: () => u - }; - return x; - } - var dx = !( - 'undefined' == typeof window || - void 0 === window.document || - void 0 === window.document.createElement - ), - fx = 'undefined' != typeof navigator && 'ReactNative' === navigator.product, - mx = dx || fx ? HS.useLayoutEffect : HS.useEffect; - function is(s, o) { - return s === o ? 0 !== s || 0 !== o || 1 / s == 1 / o : s != s && o != o; - } - function shallowEqual(s, o) { - if (is(s, o)) return !0; - if ('object' != typeof s || null === s || 'object' != typeof o || null === o) return !1; - const i = Object.keys(s), - u = Object.keys(o); - if (i.length !== u.length) return !1; - for (let u = 0; u < i.length; u++) - if (!Object.prototype.hasOwnProperty.call(o, i[u]) || !is(s[i[u]], o[i[u]])) return !1; - return !0; - } - var gx = { - childContextTypes: !0, - contextType: !0, - contextTypes: !0, - defaultProps: !0, - displayName: !0, - getDefaultProps: !0, - getDerivedStateFromError: !0, - getDerivedStateFromProps: !0, - mixins: !0, - propTypes: !0, - type: !0 - }, - yx = { - name: !0, - length: !0, - prototype: !0, - caller: !0, - callee: !0, - arguments: !0, - arity: !0 - }, - vx = { - $$typeof: !0, - compare: !0, - defaultProps: !0, - displayName: !0, - propTypes: !0, - type: !0 - }, - bx = { - [ux]: { $$typeof: !0, render: !0, defaultProps: !0, displayName: !0, propTypes: !0 }, - [px]: vx - }; - function getStatics(s) { - return (function isMemo(s) { - return typeOf(s) === lx; - })(s) - ? vx - : bx[s.$$typeof] || gx; - } - var _x = Object.defineProperty, - Ex = Object.getOwnPropertyNames, - wx = Object.getOwnPropertySymbols, - Sx = Object.getOwnPropertyDescriptor, - xx = Object.getPrototypeOf, - kx = Object.prototype; - function hoistNonReactStatics(s, o) { - if ('string' != typeof o) { - if (kx) { - const i = xx(o); - i && i !== kx && hoistNonReactStatics(s, i); - } - let i = Ex(o); - wx && (i = i.concat(wx(o))); - const u = getStatics(s), - _ = getStatics(o); - for (let w = 0; w < i.length; ++w) { - const x = i[w]; - if (!(yx[x] || (_ && _[x]) || (u && u[x]))) { - const i = Sx(o, x); - try { - _x(s, x, i); - } catch (s) {} - } - } - } - return s; - } - var Cx = notInitialized, - Ox = [null, null]; - function captureWrapperProps(s, o, i, u, _, w) { - (s.current = u), (i.current = !1), _.current && ((_.current = null), w()); - } - function strictEqual(s, o) { - return s === o; - } - var Ax = function connect( - s, - o, - i, - { - pure: u, - areStatesEqual: _ = strictEqual, - areOwnPropsEqual: w = shallowEqual, - areStatePropsEqual: x = shallowEqual, - areMergedPropsEqual: C = shallowEqual, - forwardRef: j = !1, - context: L = YS - } = {} - ) { - const B = L, - $ = (function mapStateToPropsFactory(s) { - return s - ? 'function' == typeof s - ? wrapMapToPropsFunc(s) - : createInvalidArgFactory(s, 'mapStateToProps') - : wrapMapToPropsConstant(() => ({})); - })(s), - V = (function mapDispatchToPropsFactory(s) { - return s && 'object' == typeof s - ? wrapMapToPropsConstant((o) => - (function react_redux_bindActionCreators(s, o) { - const i = {}; - for (const u in s) { - const _ = s[u]; - 'function' == typeof _ && (i[u] = (...s) => o(_(...s))); - } - return i; - })(s, o) - ) - : s - ? 'function' == typeof s - ? wrapMapToPropsFunc(s) - : createInvalidArgFactory(s, 'mapDispatchToProps') - : wrapMapToPropsConstant((s) => ({ dispatch: s })); - })(o), - U = (function mergePropsFactory(s) { - return s - ? 'function' == typeof s - ? (function wrapMergePropsFunc(s) { - return function initMergePropsProxy( - o, - { displayName: i, areMergedPropsEqual: u } - ) { - let _, - w = !1; - return function mergePropsProxy(o, i, x) { - const C = s(o, i, x); - return w ? u(C, _) || (_ = C) : ((w = !0), (_ = C)), _; - }; - }; - })(s) - : createInvalidArgFactory(s, 'mergeProps') - : () => defaultMergeProps; - })(i), - z = Boolean(s); - return (s) => { - const o = s.displayName || s.name || 'Component', - i = `Connect(${o})`, - u = { - shouldHandleStateChanges: z, - displayName: i, - wrappedComponentName: o, - WrappedComponent: s, - initMapStateToProps: $, - initMapDispatchToProps: V, - initMergeProps: U, - areStatesEqual: _, - areStatePropsEqual: x, - areOwnPropsEqual: w, - areMergedPropsEqual: C - }; - function ConnectFunction(o) { - const [i, _, w] = HS.useMemo(() => { - const { reactReduxForwardedRef: s, ...i } = o; - return [o.context, s, i]; - }, [o]), - x = HS.useMemo(() => B, [i, B]), - C = HS.useContext(x), - j = Boolean(o.store) && Boolean(o.store.getState) && Boolean(o.store.dispatch), - L = Boolean(C) && Boolean(C.store); - const $ = j ? o.store : C.store, - V = L ? C.getServerState : $.getState, - U = HS.useMemo( - () => - (function finalPropsSelectorFactory( - s, - { initMapStateToProps: o, initMapDispatchToProps: i, initMergeProps: u, ..._ } - ) { - return pureFinalPropsSelectorFactory(o(s, _), i(s, _), u(s, _), s, _); - })($.dispatch, u), - [$] - ), - [Y, Z] = HS.useMemo(() => { - if (!z) return Ox; - const s = createSubscription($, j ? void 0 : C.subscription), - o = s.notifyNestedSubs.bind(s); - return [s, o]; - }, [$, j, C]), - ee = HS.useMemo(() => (j ? C : { ...C, subscription: Y }), [j, C, Y]), - ie = HS.useRef(void 0), - ae = HS.useRef(w), - le = HS.useRef(void 0), - ce = HS.useRef(!1), - pe = HS.useRef(!1), - de = HS.useRef(void 0); - mx( - () => ( - (pe.current = !0), - () => { - pe.current = !1; - } - ), - [] - ); - const fe = HS.useMemo( - () => () => (le.current && w === ae.current ? le.current : U($.getState(), w)), - [$, w] - ), - ye = HS.useMemo( - () => (s) => - Y - ? (function subscribeUpdates(s, o, i, u, _, w, x, C, j, L, B) { - if (!s) return () => {}; - let $ = !1, - V = null; - const checkForUpdates = () => { - if ($ || !C.current) return; - const s = o.getState(); - let i, U; - try { - i = u(s, _.current); - } catch (s) { - (U = s), (V = s); - } - U || (V = null), - i === w.current - ? x.current || L() - : ((w.current = i), (j.current = i), (x.current = !0), B()); - }; - return ( - (i.onStateChange = checkForUpdates), - i.trySubscribe(), - checkForUpdates(), - () => { - if ((($ = !0), i.tryUnsubscribe(), (i.onStateChange = null), V)) - throw V; - } - ); - })(z, $, Y, U, ae, ie, ce, pe, le, Z, s) - : () => {}, - [Y] - ); - let be; - !(function useIsomorphicLayoutEffectWithArgs(s, o, i) { - mx(() => s(...o), i); - })(captureWrapperProps, [ae, ie, ce, w, le, Z]); - try { - be = Cx(ye, fe, V ? () => U(V(), w) : fe); - } catch (s) { - throw ( - (de.current && - (s.message += `\nThe error may be correlated with this previous error:\n${de.current.stack}\n\n`), - s) - ); - } - mx(() => { - (de.current = void 0), (le.current = void 0), (ie.current = be); - }); - const _e = HS.useMemo(() => HS.createElement(s, { ...be, ref: _ }), [_, s, be]); - return HS.useMemo( - () => (z ? HS.createElement(x.Provider, { value: ee }, _e) : _e), - [x, _e, ee] - ); - } - const L = HS.memo(ConnectFunction); - if (((L.WrappedComponent = s), (L.displayName = ConnectFunction.displayName = i), j)) { - const o = HS.forwardRef(function forwardConnectRef(s, o) { - return HS.createElement(L, { ...s, reactReduxForwardedRef: o }); - }); - return (o.displayName = i), (o.WrappedComponent = s), hoistNonReactStatics(o, s); - } - return hoistNonReactStatics(L, s); - }; - }; - var jx = function Provider({ - store: s, - context: o, - children: i, - serverState: u, - stabilityCheck: _ = 'once', - identityFunctionCheck: w = 'once' - }) { - const x = HS.useMemo(() => { - const o = createSubscription(s); - return { - store: s, - subscription: o, - getServerState: u ? () => u : void 0, - stabilityCheck: _, - identityFunctionCheck: w - }; - }, [s, u, _, w]), - C = HS.useMemo(() => s.getState(), [s]); - mx(() => { - const { subscription: o } = x; - return ( - (o.onStateChange = o.notifyNestedSubs), - o.trySubscribe(), - C !== s.getState() && o.notifyNestedSubs(), - () => { - o.tryUnsubscribe(), (o.onStateChange = void 0); - } - ); - }, [x, C]); - const j = o || YS; - return HS.createElement(j.Provider, { value: x }, i); - }; - var Ix; - (Ix = KS.useSyncExternalStoreWithSelector), - ((s) => { - Cx = s; - })(Pe.useSyncExternalStore); - var Px = __webpack_require__(83488), - Mx = __webpack_require__.n(Px); - const withSystem = (s) => (o) => { - const { fn: i } = s(); - class WithSystem extends Pe.Component { - render() { - return Pe.createElement(o, Rn()({}, s(), this.props, this.context)); - } - } - return (WithSystem.displayName = `WithSystem(${i.getDisplayName(o)})`), WithSystem; - }, - withRoot = (s, o) => (i) => { - const { fn: u } = s(); - class WithRoot extends Pe.Component { - render() { - return Pe.createElement( - jx, - { store: o }, - Pe.createElement(i, Rn()({}, this.props, this.context)) - ); - } - } - return (WithRoot.displayName = `WithRoot(${u.getDisplayName(i)})`), WithRoot; - }, - withConnect = (s, o, i) => - compose( - i ? withRoot(s, i) : Mx(), - Ax((i, u) => { - const _ = { ...u, ...s() }, - w = o.prototype?.mapStateToProps || ((s) => ({ state: s })); - return w(i, _); - }), - withSystem(s) - )(o), - handleProps = (s, o, i, u) => { - for (const _ in o) { - const w = o[_]; - 'function' == typeof w && w(i[_], u[_], s()); - } - }, - withMappedContainer = (s, o, i) => (o, u) => { - const { fn: _ } = s(), - w = i(o, 'root'); - class WithMappedContainer extends Pe.Component { - constructor(o, i) { - super(o, i), handleProps(s, u, o, {}); - } - UNSAFE_componentWillReceiveProps(o) { - handleProps(s, u, o, this.props); - } - render() { - const s = Yt()(this.props, u ? Object.keys(u) : []); - return Pe.createElement(w, s); - } - } - return ( - (WithMappedContainer.displayName = `WithMappedContainer(${_.getDisplayName(w)})`), - WithMappedContainer - ); - }, - render = (s, o, i, u) => (_) => { - const w = i(s, o, u)('App', 'root'), - { createRoot: x } = WS; - x(_).render(Pe.createElement(w, null)); - }, - getComponent = - (s, o, i) => - (u, _, w = {}) => { - if ('string' != typeof u) - throw new TypeError('Need a string, to fetch a component. Was given a ' + typeof u); - const x = i(u); - return x - ? _ - ? 'root' === _ - ? withConnect(s, x, o()) - : withConnect(s, x) - : x - : (w.failSilently || s().log.warn('Could not find component:', u), null); - }, - getDisplayName = (s) => s.displayName || s.name || 'Component', - view = ({ getComponents: s, getStore: o, getSystem: i }) => { - const u = ((s) => jt(s, (...s) => JSON.stringify(s)))(getComponent(i, o, s)), - _ = ((s) => utils_memoizeN(s, (...s) => s))(withMappedContainer(i, 0, u)); - return { - rootInjects: { - getComponent: u, - makeMappedContainer: _, - render: render(i, o, getComponent, s) - }, - fn: { getDisplayName } - }; - }, - view_legacy = ({ React: s, getSystem: o, getStore: i, getComponents: u }) => { - const _ = {}, - w = parseInt(s?.version, 10); - return ( - w >= 16 && - w < 18 && - (_.render = ((s, o, i, u) => (_) => { - const w = i(s, o, u)('App', 'root'); - WS.render(Pe.createElement(w, null), _); - })(o, i, getComponent, u)), - { rootInjects: _ } - ); - }; - function downloadUrlPlugin(s) { - let { fn: o } = s; - const i = { - download: - (s) => - ({ errActions: i, specSelectors: u, specActions: _, getConfigs: w }) => { - let { fetch: x } = o; - const C = w(); - function next(o) { - if (o instanceof Error || o.status >= 400) - return ( - _.updateLoadingStatus('failed'), - i.newThrownErr( - Object.assign(new Error((o.message || o.statusText) + ' ' + s), { - source: 'fetch' - }) - ), - void ( - !o.status && - o instanceof Error && - (function checkPossibleFailReasons() { - try { - let o; - if ( - ('URL' in at - ? (o = new URL(s)) - : ((o = document.createElement('a')), (o.href = s)), - 'https:' !== o.protocol && 'https:' === at.location.protocol) - ) { - const s = Object.assign( - new Error( - `Possible mixed-content issue? The page was loaded over https:// but a ${o.protocol}// URL was specified. Check that you are not attempting to load mixed content.` - ), - { source: 'fetch' } - ); - return void i.newThrownErr(s); - } - if (o.origin !== at.location.origin) { - const s = Object.assign( - new Error( - `Possible cross-origin (CORS) issue? The URL origin (${o.origin}) does not match the page (${at.location.origin}). Check the server returns the correct 'Access-Control-Allow-*' headers.` - ), - { source: 'fetch' } - ); - i.newThrownErr(s); - } - } catch (s) { - return; - } - })() - ) - ); - _.updateLoadingStatus('success'), - _.updateSpec(o.text), - u.url() !== s && _.updateUrl(s); - } - (s = s || u.url()), - _.updateLoadingStatus('loading'), - i.clear({ source: 'fetch' }), - x({ - url: s, - loadSpec: !0, - requestInterceptor: C.requestInterceptor || ((s) => s), - responseInterceptor: C.responseInterceptor || ((s) => s), - credentials: 'same-origin', - headers: { Accept: 'application/json,*/*' } - }).then(next, next); - }, - updateLoadingStatus: (s) => { - let o = [null, 'loading', 'failed', 'success', 'failedConfig']; - return ( - -1 === o.indexOf(s) && - console.error(`Error: ${s} is not one of ${JSON.stringify(o)}`), - { type: 'spec_update_loading_status', payload: s } - ); - } - }; - let u = { - loadingStatus: Ut( - (s) => s || (0, qe.Map)(), - (s) => s.get('loadingStatus') || null - ) - }; - return { - statePlugins: { - spec: { - actions: i, - reducers: { - spec_update_loading_status: (s, o) => - 'string' == typeof o.payload ? s.set('loadingStatus', o.payload) : s - }, - selectors: u - } - } - }; - } - function arrayLikeToArray_arrayLikeToArray(s, o) { - (null == o || o > s.length) && (o = s.length); - for (var i = 0, u = Array(o); i < o; i++) u[i] = s[i]; - return u; - } - function toConsumableArray_toConsumableArray(s) { - return ( - (function arrayWithoutHoles_arrayWithoutHoles(s) { - if (Array.isArray(s)) return arrayLikeToArray_arrayLikeToArray(s); - })(s) || - (function iterableToArray_iterableToArray(s) { - if ( - ('undefined' != typeof Symbol && null != s[Symbol.iterator]) || - null != s['@@iterator'] - ) - return Array.from(s); - })(s) || - (function unsupportedIterableToArray_unsupportedIterableToArray(s, o) { - if (s) { - if ('string' == typeof s) return arrayLikeToArray_arrayLikeToArray(s, o); - var i = {}.toString.call(s).slice(8, -1); - return ( - 'Object' === i && s.constructor && (i = s.constructor.name), - 'Map' === i || 'Set' === i - ? Array.from(s) - : 'Arguments' === i || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i) - ? arrayLikeToArray_arrayLikeToArray(s, o) - : void 0 - ); - } - })(s) || - (function nonIterableSpread_nonIterableSpread() { - throw new TypeError( - 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })() - ); - } - function typeof_typeof(s) { - return ( - (typeof_typeof = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (s) { - return typeof s; - } - : function (s) { - return s && - 'function' == typeof Symbol && - s.constructor === Symbol && - s !== Symbol.prototype - ? 'symbol' - : typeof s; - }), - typeof_typeof(s) - ); - } - function toPropertyKey(s) { - var o = (function toPrimitive(s, o) { - if ('object' != typeof_typeof(s) || !s) return s; - var i = s[Symbol.toPrimitive]; - if (void 0 !== i) { - var u = i.call(s, o || 'default'); - if ('object' != typeof_typeof(u)) return u; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return ('string' === o ? String : Number)(s); - })(s, 'string'); - return 'symbol' == typeof_typeof(o) ? o : o + ''; - } - function defineProperty_defineProperty(s, o, i) { - return ( - (o = toPropertyKey(o)) in s - ? Object.defineProperty(s, o, { - value: i, - enumerable: !0, - configurable: !0, - writable: !0 - }) - : (s[o] = i), - s - ); - } - function extends_extends() { - return ( - (extends_extends = Object.assign - ? Object.assign.bind() - : function (s) { - for (var o = 1; o < arguments.length; o++) { - var i = arguments[o]; - for (var u in i) ({}).hasOwnProperty.call(i, u) && (s[u] = i[u]); - } - return s; - }), - extends_extends.apply(null, arguments) - ); - } - function create_element_ownKeys(s, o) { - var i = Object.keys(s); - if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); - o && - (u = u.filter(function (o) { - return Object.getOwnPropertyDescriptor(s, o).enumerable; - })), - i.push.apply(i, u); - } - return i; - } - function _objectSpread(s) { - for (var o = 1; o < arguments.length; o++) { - var i = null != arguments[o] ? arguments[o] : {}; - o % 2 - ? create_element_ownKeys(Object(i), !0).forEach(function (o) { - defineProperty_defineProperty(s, o, i[o]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) - : create_element_ownKeys(Object(i)).forEach(function (o) { - Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); - }); - } - return s; - } - var Tx = {}; - function createStyleObject(s) { - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}, - i = arguments.length > 2 ? arguments[2] : void 0; - return (function getClassNameCombinations(s) { - if (0 === s.length || 1 === s.length) return s; - var o = s.join('.'); - return ( - Tx[o] || - (Tx[o] = (function powerSetPermutations(s) { - var o = s.length; - return 0 === o || 1 === o - ? s - : 2 === o - ? [ - s[0], - s[1], - ''.concat(s[0], '.').concat(s[1]), - ''.concat(s[1], '.').concat(s[0]) - ] - : 3 === o - ? [ - s[0], - s[1], - s[2], - ''.concat(s[0], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[0]), - ''.concat(s[1], '.').concat(s[2]), - ''.concat(s[2], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[1], '.').concat(s[2]), - ''.concat(s[0], '.').concat(s[2], '.').concat(s[1]), - ''.concat(s[1], '.').concat(s[0], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[2], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[0], '.').concat(s[1]), - ''.concat(s[2], '.').concat(s[1], '.').concat(s[0]) - ] - : o >= 4 - ? [ - s[0], - s[1], - s[2], - s[3], - ''.concat(s[0], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[2]), - ''.concat(s[0], '.').concat(s[3]), - ''.concat(s[1], '.').concat(s[0]), - ''.concat(s[1], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[3]), - ''.concat(s[2], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[1]), - ''.concat(s[2], '.').concat(s[3]), - ''.concat(s[3], '.').concat(s[0]), - ''.concat(s[3], '.').concat(s[1]), - ''.concat(s[3], '.').concat(s[2]), - ''.concat(s[0], '.').concat(s[1], '.').concat(s[2]), - ''.concat(s[0], '.').concat(s[1], '.').concat(s[3]), - ''.concat(s[0], '.').concat(s[2], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[2], '.').concat(s[3]), - ''.concat(s[0], '.').concat(s[3], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[3], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[0], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[0], '.').concat(s[3]), - ''.concat(s[1], '.').concat(s[2], '.').concat(s[0]), - ''.concat(s[1], '.').concat(s[2], '.').concat(s[3]), - ''.concat(s[1], '.').concat(s[3], '.').concat(s[0]), - ''.concat(s[1], '.').concat(s[3], '.').concat(s[2]), - ''.concat(s[2], '.').concat(s[0], '.').concat(s[1]), - ''.concat(s[2], '.').concat(s[0], '.').concat(s[3]), - ''.concat(s[2], '.').concat(s[1], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[1], '.').concat(s[3]), - ''.concat(s[2], '.').concat(s[3], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[3], '.').concat(s[1]), - ''.concat(s[3], '.').concat(s[0], '.').concat(s[1]), - ''.concat(s[3], '.').concat(s[0], '.').concat(s[2]), - ''.concat(s[3], '.').concat(s[1], '.').concat(s[0]), - ''.concat(s[3], '.').concat(s[1], '.').concat(s[2]), - ''.concat(s[3], '.').concat(s[2], '.').concat(s[0]), - ''.concat(s[3], '.').concat(s[2], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[1], '.').concat(s[2], '.').concat(s[3]), - ''.concat(s[0], '.').concat(s[1], '.').concat(s[3], '.').concat(s[2]), - ''.concat(s[0], '.').concat(s[2], '.').concat(s[1], '.').concat(s[3]), - ''.concat(s[0], '.').concat(s[2], '.').concat(s[3], '.').concat(s[1]), - ''.concat(s[0], '.').concat(s[3], '.').concat(s[1], '.').concat(s[2]), - ''.concat(s[0], '.').concat(s[3], '.').concat(s[2], '.').concat(s[1]), - ''.concat(s[1], '.').concat(s[0], '.').concat(s[2], '.').concat(s[3]), - ''.concat(s[1], '.').concat(s[0], '.').concat(s[3], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[2], '.').concat(s[0], '.').concat(s[3]), - ''.concat(s[1], '.').concat(s[2], '.').concat(s[3], '.').concat(s[0]), - ''.concat(s[1], '.').concat(s[3], '.').concat(s[0], '.').concat(s[2]), - ''.concat(s[1], '.').concat(s[3], '.').concat(s[2], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[0], '.').concat(s[1], '.').concat(s[3]), - ''.concat(s[2], '.').concat(s[0], '.').concat(s[3], '.').concat(s[1]), - ''.concat(s[2], '.').concat(s[1], '.').concat(s[0], '.').concat(s[3]), - ''.concat(s[2], '.').concat(s[1], '.').concat(s[3], '.').concat(s[0]), - ''.concat(s[2], '.').concat(s[3], '.').concat(s[0], '.').concat(s[1]), - ''.concat(s[2], '.').concat(s[3], '.').concat(s[1], '.').concat(s[0]), - ''.concat(s[3], '.').concat(s[0], '.').concat(s[1], '.').concat(s[2]), - ''.concat(s[3], '.').concat(s[0], '.').concat(s[2], '.').concat(s[1]), - ''.concat(s[3], '.').concat(s[1], '.').concat(s[0], '.').concat(s[2]), - ''.concat(s[3], '.').concat(s[1], '.').concat(s[2], '.').concat(s[0]), - ''.concat(s[3], '.').concat(s[2], '.').concat(s[0], '.').concat(s[1]), - ''.concat(s[3], '.').concat(s[2], '.').concat(s[1], '.').concat(s[0]) - ] - : void 0; - })(s)), - Tx[o] - ); - })( - s.filter(function (s) { - return 'token' !== s; - }) - ).reduce(function (s, o) { - return _objectSpread(_objectSpread({}, s), i[o]); - }, o); - } - function createClassNameString(s) { - return s.join(' '); - } - function createElement(s) { - var o = s.node, - i = s.stylesheet, - u = s.style, - _ = void 0 === u ? {} : u, - w = s.useInlineStyles, - x = s.key, - C = o.properties, - j = o.type, - L = o.tagName, - B = o.value; - if ('text' === j) return B; - if (L) { - var $, - V = (function createChildren(s, o) { - var i = 0; - return function (u) { - return ( - (i += 1), - u.map(function (u, _) { - return createElement({ - node: u, - stylesheet: s, - useInlineStyles: o, - key: 'code-segment-'.concat(i, '-').concat(_) - }); - }) - ); - }; - })(i, w); - if (w) { - var U = Object.keys(i).reduce(function (s, o) { - return ( - o.split('.').forEach(function (o) { - s.includes(o) || s.push(o); - }), - s - ); - }, []), - z = C.className && C.className.includes('token') ? ['token'] : [], - Y = - C.className && - z.concat( - C.className.filter(function (s) { - return !U.includes(s); - }) - ); - $ = _objectSpread( - _objectSpread({}, C), - {}, - { - className: createClassNameString(Y) || void 0, - style: createStyleObject(C.className, Object.assign({}, C.style, _), i) - } - ); - } else - $ = _objectSpread( - _objectSpread({}, C), - {}, - { className: createClassNameString(C.className) } - ); - var Z = V(o.children); - return Pe.createElement(L, extends_extends({ key: x }, $), Z); - } - } - var Nx = [ - 'language', - 'children', - 'style', - 'customStyle', - 'codeTagProps', - 'useInlineStyles', - 'showLineNumbers', - 'showInlineLineNumbers', - 'startingLineNumber', - 'lineNumberContainerStyle', - 'lineNumberStyle', - 'wrapLines', - 'wrapLongLines', - 'lineProps', - 'renderer', - 'PreTag', - 'CodeTag', - 'code', - 'astGenerator' - ]; - function highlight_ownKeys(s, o) { - var i = Object.keys(s); - if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); - o && - (u = u.filter(function (o) { - return Object.getOwnPropertyDescriptor(s, o).enumerable; - })), - i.push.apply(i, u); - } - return i; - } - function highlight_objectSpread(s) { - for (var o = 1; o < arguments.length; o++) { - var i = null != arguments[o] ? arguments[o] : {}; - o % 2 - ? highlight_ownKeys(Object(i), !0).forEach(function (o) { - defineProperty_defineProperty(s, o, i[o]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(s, Object.getOwnPropertyDescriptors(i)) - : highlight_ownKeys(Object(i)).forEach(function (o) { - Object.defineProperty(s, o, Object.getOwnPropertyDescriptor(i, o)); - }); - } - return s; - } - var Rx = /\n/g; - function AllLineNumbers(s) { - var o = s.codeString, - i = s.codeStyle, - u = s.containerStyle, - _ = void 0 === u ? { float: 'left', paddingRight: '10px' } : u, - w = s.numberStyle, - x = void 0 === w ? {} : w, - C = s.startingLineNumber; - return Pe.createElement( - 'code', - { style: Object.assign({}, i, _) }, - (function getAllLineNumbers(s) { - var o = s.lines, - i = s.startingLineNumber, - u = s.style; - return o.map(function (s, o) { - var _ = o + i; - return Pe.createElement( - 'span', - { - key: 'line-'.concat(o), - className: 'react-syntax-highlighter-line-number', - style: 'function' == typeof u ? u(_) : u - }, - ''.concat(_, '\n') - ); - }); - })({ lines: o.replace(/\n$/, '').split('\n'), style: x, startingLineNumber: C }) - ); - } - function getInlineLineNumber(s, o) { - return { - type: 'element', - tagName: 'span', - properties: { - key: 'line-number--'.concat(s), - className: ['comment', 'linenumber', 'react-syntax-highlighter-line-number'], - style: o - }, - children: [{ type: 'text', value: s }] - }; - } - function assembleLineNumberStyles(s, o, i) { - var u, - _ = { - display: 'inline-block', - minWidth: ((u = i), ''.concat(u.toString().length, '.25em')), - paddingRight: '1em', - textAlign: 'right', - userSelect: 'none' - }, - w = 'function' == typeof s ? s(o) : s; - return highlight_objectSpread(highlight_objectSpread({}, _), w); - } - function createLineElement(s) { - var o = s.children, - i = s.lineNumber, - u = s.lineNumberStyle, - _ = s.largestLineNumber, - w = s.showInlineLineNumbers, - x = s.lineProps, - C = void 0 === x ? {} : x, - j = s.className, - L = void 0 === j ? [] : j, - B = s.showLineNumbers, - $ = s.wrapLongLines, - V = s.wrapLines, - U = - void 0 !== V && V - ? highlight_objectSpread({}, 'function' == typeof C ? C(i) : C) - : {}; - if ( - ((U.className = U.className - ? [].concat( - toConsumableArray_toConsumableArray(U.className.trim().split(/\s+/)), - toConsumableArray_toConsumableArray(L) - ) - : L), - i && w) - ) { - var z = assembleLineNumberStyles(u, i, _); - o.unshift(getInlineLineNumber(i, z)); - } - return ( - $ & B && (U.style = highlight_objectSpread({ display: 'flex' }, U.style)), - { type: 'element', tagName: 'span', properties: U, children: o } - ); - } - function flattenCodeTree(s) { - for ( - var o = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : [], - i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : [], - u = 0; - u < s.length; - u++ - ) { - var _ = s[u]; - if ('text' === _.type) - i.push( - createLineElement({ - children: [_], - className: toConsumableArray_toConsumableArray(new Set(o)) - }) - ); - else if (_.children) { - var w = o.concat(_.properties.className); - flattenCodeTree(_.children, w).forEach(function (s) { - return i.push(s); - }); - } - } - return i; - } - function processLines(s, o, i, u, _, w, x, C, j) { - var L, - B = flattenCodeTree(s.value), - $ = [], - V = -1, - U = 0; - function createLine(s, w) { - var L = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : []; - return o || L.length > 0 - ? (function createWrappedLine(s, w) { - return createLineElement({ - children: s, - lineNumber: w, - lineNumberStyle: C, - largestLineNumber: x, - showInlineLineNumbers: _, - lineProps: i, - className: arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : [], - showLineNumbers: u, - wrapLongLines: j, - wrapLines: o - }); - })(s, w, L) - : (function createUnwrappedLine(s, o) { - if (u && o && _) { - var i = assembleLineNumberStyles(C, o, x); - s.unshift(getInlineLineNumber(o, i)); - } - return s; - })(s, w); - } - for ( - var z = function _loop() { - var s = B[U], - o = s.children[0].value, - i = (function getNewLines(s) { - return s.match(Rx); - })(o); - if (i) { - var _ = o.split('\n'); - _.forEach(function (o, i) { - var x = u && $.length + w, - C = { type: 'text', value: ''.concat(o, '\n') }; - if (0 === i) { - var j = createLine( - B.slice(V + 1, U).concat( - createLineElement({ children: [C], className: s.properties.className }) - ), - x - ); - $.push(j); - } else if (i === _.length - 1) { - var L = B[U + 1] && B[U + 1].children && B[U + 1].children[0], - z = { type: 'text', value: ''.concat(o) }; - if (L) { - var Y = createLineElement({ - children: [z], - className: s.properties.className - }); - B.splice(U + 1, 0, Y); - } else { - var Z = createLine([z], x, s.properties.className); - $.push(Z); - } - } else { - var ee = createLine([C], x, s.properties.className); - $.push(ee); - } - }), - (V = U); - } - U++; - }; - U < B.length; - - ) - z(); - if (V !== B.length - 1) { - var Y = B.slice(V + 1, B.length); - if (Y && Y.length) { - var Z = createLine(Y, u && $.length + w); - $.push(Z); - } - } - return o ? $ : (L = []).concat.apply(L, $); - } - function defaultRenderer(s) { - var o = s.rows, - i = s.stylesheet, - u = s.useInlineStyles; - return o.map(function (s, o) { - return createElement({ - node: s, - stylesheet: i, - useInlineStyles: u, - key: 'code-segement'.concat(o) - }); - }); - } - function isHighlightJs(s) { - return s && void 0 !== s.highlightAuto; - } - var Dx = __webpack_require__(43768), - Lx = (function highlight(s, o) { - return function SyntaxHighlighter(i) { - var u = i.language, - _ = i.children, - w = i.style, - x = void 0 === w ? o : w, - C = i.customStyle, - j = void 0 === C ? {} : C, - L = i.codeTagProps, - B = - void 0 === L - ? { - className: u ? 'language-'.concat(u) : void 0, - style: highlight_objectSpread( - highlight_objectSpread({}, x['code[class*="language-"]']), - x['code[class*="language-'.concat(u, '"]')] - ) - } - : L, - $ = i.useInlineStyles, - V = void 0 === $ || $, - U = i.showLineNumbers, - z = void 0 !== U && U, - Y = i.showInlineLineNumbers, - Z = void 0 === Y || Y, - ee = i.startingLineNumber, - ie = void 0 === ee ? 1 : ee, - ae = i.lineNumberContainerStyle, - le = i.lineNumberStyle, - ce = void 0 === le ? {} : le, - pe = i.wrapLines, - de = i.wrapLongLines, - fe = void 0 !== de && de, - ye = i.lineProps, - be = void 0 === ye ? {} : ye, - _e = i.renderer, - we = i.PreTag, - Se = void 0 === we ? 'pre' : we, - xe = i.CodeTag, - Te = void 0 === xe ? 'code' : xe, - Re = i.code, - qe = void 0 === Re ? (Array.isArray(_) ? _[0] : _) || '' : Re, - $e = i.astGenerator, - ze = (function _objectWithoutProperties(s, o) { - if (null == s) return {}; - var i, - u, - _ = (function _objectWithoutPropertiesLoose(s, o) { - if (null == s) return {}; - var i = {}; - for (var u in s) - if ({}.hasOwnProperty.call(s, u)) { - if (o.includes(u)) continue; - i[u] = s[u]; - } - return i; - })(s, o); - if (Object.getOwnPropertySymbols) { - var w = Object.getOwnPropertySymbols(s); - for (u = 0; u < w.length; u++) - (i = w[u]), - o.includes(i) || ({}.propertyIsEnumerable.call(s, i) && (_[i] = s[i])); - } - return _; - })(i, Nx); - $e = $e || s; - var We = z - ? Pe.createElement(AllLineNumbers, { - containerStyle: ae, - codeStyle: B.style || {}, - numberStyle: ce, - startingLineNumber: ie, - codeString: qe - }) - : null, - He = x.hljs || x['pre[class*="language-"]'] || { backgroundColor: '#fff' }, - Ye = isHighlightJs($e) ? 'hljs' : 'prismjs', - Xe = V - ? Object.assign({}, ze, { style: Object.assign({}, He, j) }) - : Object.assign({}, ze, { - className: ze.className ? ''.concat(Ye, ' ').concat(ze.className) : Ye, - style: Object.assign({}, j) - }); - if ( - ((B.style = highlight_objectSpread( - fe ? { whiteSpace: 'pre-wrap' } : { whiteSpace: 'pre' }, - B.style - )), - !$e) - ) - return Pe.createElement(Se, Xe, We, Pe.createElement(Te, B, qe)); - ((void 0 === pe && _e) || fe) && (pe = !0), (_e = _e || defaultRenderer); - var Qe = [{ type: 'text', value: qe }], - et = (function getCodeTree(s) { - var o = s.astGenerator, - i = s.language, - u = s.code, - _ = s.defaultCodeValue; - if (isHighlightJs(o)) { - var w = (function (s, o) { - return -1 !== s.listLanguages().indexOf(o); - })(o, i); - return 'text' === i - ? { value: _, language: 'text' } - : w - ? o.highlight(i, u) - : o.highlightAuto(u); - } - try { - return i && 'text' !== i ? { value: o.highlight(u, i) } : { value: _ }; - } catch (s) { - return { value: _ }; - } - })({ astGenerator: $e, language: u, code: qe, defaultCodeValue: Qe }); - null === et.language && (et.value = Qe); - var tt = et.value.length; - 1 === tt && - 'text' === et.value[0].type && - (tt = et.value[0].value.split('\n').length); - var rt = processLines(et, pe, be, z, Z, ie, tt + ie, ce, fe); - return Pe.createElement( - Se, - Xe, - Pe.createElement( - Te, - B, - !Z && We, - _e({ rows: rt, stylesheet: x, useInlineStyles: V }) - ) - ); - }; - })(Dx, {}); - Lx.registerLanguage = Dx.registerLanguage; - const Bx = Lx; - var Fx = __webpack_require__(95089); - const qx = __webpack_require__.n(Fx)(); - var $x = __webpack_require__(65772); - const Vx = __webpack_require__.n($x)(); - var Ux = __webpack_require__(17285); - const zx = __webpack_require__.n(Ux)(); - var Wx = __webpack_require__(35344); - const Kx = __webpack_require__.n(Wx)(); - var Hx = __webpack_require__(17533); - const Jx = __webpack_require__.n(Hx)(); - var Gx = __webpack_require__(73402); - const Yx = __webpack_require__.n(Gx)(); - var Xx = __webpack_require__(26571); - const Zx = __webpack_require__.n(Xx)(), - after_load = () => { - Bx.registerLanguage('json', Vx), - Bx.registerLanguage('js', qx), - Bx.registerLanguage('xml', zx), - Bx.registerLanguage('yaml', Jx), - Bx.registerLanguage('http', Yx), - Bx.registerLanguage('bash', Kx), - Bx.registerLanguage('powershell', Zx), - Bx.registerLanguage('javascript', qx); - }, - Qx = { - hljs: { - display: 'block', - overflowX: 'auto', - padding: '0.5em', - background: '#333', - color: 'white' - }, - 'hljs-name': { fontWeight: 'bold' }, - 'hljs-strong': { fontWeight: 'bold' }, - 'hljs-code': { fontStyle: 'italic', color: '#888' }, - 'hljs-emphasis': { fontStyle: 'italic' }, - 'hljs-tag': { color: '#62c8f3' }, - 'hljs-variable': { color: '#ade5fc' }, - 'hljs-template-variable': { color: '#ade5fc' }, - 'hljs-selector-id': { color: '#ade5fc' }, - 'hljs-selector-class': { color: '#ade5fc' }, - 'hljs-string': { color: '#a2fca2' }, - 'hljs-bullet': { color: '#d36363' }, - 'hljs-type': { color: '#ffa' }, - 'hljs-title': { color: '#ffa' }, - 'hljs-section': { color: '#ffa' }, - 'hljs-attribute': { color: '#ffa' }, - 'hljs-quote': { color: '#ffa' }, - 'hljs-built_in': { color: '#ffa' }, - 'hljs-builtin-name': { color: '#ffa' }, - 'hljs-number': { color: '#d36363' }, - 'hljs-symbol': { color: '#d36363' }, - 'hljs-keyword': { color: '#fcc28c' }, - 'hljs-selector-tag': { color: '#fcc28c' }, - 'hljs-literal': { color: '#fcc28c' }, - 'hljs-comment': { color: '#888' }, - 'hljs-deletion': { color: '#333', backgroundColor: '#fc9b9b' }, - 'hljs-regexp': { color: '#c6b4f0' }, - 'hljs-link': { color: '#c6b4f0' }, - 'hljs-meta': { color: '#fc9b9b' }, - 'hljs-addition': { backgroundColor: '#a2fca2', color: '#333' } - }, - tk = { - agate: Qx, - arta: { - hljs: { - display: 'block', - overflowX: 'auto', - padding: '0.5em', - background: '#222', - color: '#aaa' - }, - 'hljs-subst': { color: '#aaa' }, - 'hljs-section': { color: '#fff', fontWeight: 'bold' }, - 'hljs-comment': { color: '#444' }, - 'hljs-quote': { color: '#444' }, - 'hljs-meta': { color: '#444' }, - 'hljs-string': { color: '#ffcc33' }, - 'hljs-symbol': { color: '#ffcc33' }, - 'hljs-bullet': { color: '#ffcc33' }, - 'hljs-regexp': { color: '#ffcc33' }, - 'hljs-number': { color: '#00cc66' }, - 'hljs-addition': { color: '#00cc66' }, - 'hljs-built_in': { color: '#32aaee' }, - 'hljs-builtin-name': { color: '#32aaee' }, - 'hljs-literal': { color: '#32aaee' }, - 'hljs-type': { color: '#32aaee' }, - 'hljs-template-variable': { color: '#32aaee' }, - 'hljs-attribute': { color: '#32aaee' }, - 'hljs-link': { color: '#32aaee' }, - 'hljs-keyword': { color: '#6644aa' }, - 'hljs-selector-tag': { color: '#6644aa' }, - 'hljs-name': { color: '#6644aa' }, - 'hljs-selector-id': { color: '#6644aa' }, - 'hljs-selector-class': { color: '#6644aa' }, - 'hljs-title': { color: '#bb1166' }, - 'hljs-variable': { color: '#bb1166' }, - 'hljs-deletion': { color: '#bb1166' }, - 'hljs-template-tag': { color: '#bb1166' }, - 'hljs-doctag': { fontWeight: 'bold' }, - 'hljs-strong': { fontWeight: 'bold' }, - 'hljs-emphasis': { fontStyle: 'italic' } - }, - monokai: { - hljs: { - display: 'block', - overflowX: 'auto', - padding: '0.5em', - background: '#272822', - color: '#ddd' - }, - 'hljs-tag': { color: '#f92672' }, - 'hljs-keyword': { color: '#f92672', fontWeight: 'bold' }, - 'hljs-selector-tag': { color: '#f92672', fontWeight: 'bold' }, - 'hljs-literal': { color: '#f92672', fontWeight: 'bold' }, - 'hljs-strong': { color: '#f92672' }, - 'hljs-name': { color: '#f92672' }, - 'hljs-code': { color: '#66d9ef' }, - 'hljs-class .hljs-title': { color: 'white' }, - 'hljs-attribute': { color: '#bf79db' }, - 'hljs-symbol': { color: '#bf79db' }, - 'hljs-regexp': { color: '#bf79db' }, - 'hljs-link': { color: '#bf79db' }, - 'hljs-string': { color: '#a6e22e' }, - 'hljs-bullet': { color: '#a6e22e' }, - 'hljs-subst': { color: '#a6e22e' }, - 'hljs-title': { color: '#a6e22e', fontWeight: 'bold' }, - 'hljs-section': { color: '#a6e22e', fontWeight: 'bold' }, - 'hljs-emphasis': { color: '#a6e22e' }, - 'hljs-type': { color: '#a6e22e', fontWeight: 'bold' }, - 'hljs-built_in': { color: '#a6e22e' }, - 'hljs-builtin-name': { color: '#a6e22e' }, - 'hljs-selector-attr': { color: '#a6e22e' }, - 'hljs-selector-pseudo': { color: '#a6e22e' }, - 'hljs-addition': { color: '#a6e22e' }, - 'hljs-variable': { color: '#a6e22e' }, - 'hljs-template-tag': { color: '#a6e22e' }, - 'hljs-template-variable': { color: '#a6e22e' }, - 'hljs-comment': { color: '#75715e' }, - 'hljs-quote': { color: '#75715e' }, - 'hljs-deletion': { color: '#75715e' }, - 'hljs-meta': { color: '#75715e' }, - 'hljs-doctag': { fontWeight: 'bold' }, - 'hljs-selector-id': { fontWeight: 'bold' } - }, - nord: { - hljs: { - display: 'block', - overflowX: 'auto', - padding: '0.5em', - background: '#2E3440', - color: '#D8DEE9' - }, - 'hljs-subst': { color: '#D8DEE9' }, - 'hljs-selector-tag': { color: '#81A1C1' }, - 'hljs-selector-id': { color: '#8FBCBB', fontWeight: 'bold' }, - 'hljs-selector-class': { color: '#8FBCBB' }, - 'hljs-selector-attr': { color: '#8FBCBB' }, - 'hljs-selector-pseudo': { color: '#88C0D0' }, - 'hljs-addition': { backgroundColor: 'rgba(163, 190, 140, 0.5)' }, - 'hljs-deletion': { backgroundColor: 'rgba(191, 97, 106, 0.5)' }, - 'hljs-built_in': { color: '#8FBCBB' }, - 'hljs-type': { color: '#8FBCBB' }, - 'hljs-class': { color: '#8FBCBB' }, - 'hljs-function': { color: '#88C0D0' }, - 'hljs-function > .hljs-title': { color: '#88C0D0' }, - 'hljs-keyword': { color: '#81A1C1' }, - 'hljs-literal': { color: '#81A1C1' }, - 'hljs-symbol': { color: '#81A1C1' }, - 'hljs-number': { color: '#B48EAD' }, - 'hljs-regexp': { color: '#EBCB8B' }, - 'hljs-string': { color: '#A3BE8C' }, - 'hljs-title': { color: '#8FBCBB' }, - 'hljs-params': { color: '#D8DEE9' }, - 'hljs-bullet': { color: '#81A1C1' }, - 'hljs-code': { color: '#8FBCBB' }, - 'hljs-emphasis': { fontStyle: 'italic' }, - 'hljs-formula': { color: '#8FBCBB' }, - 'hljs-strong': { fontWeight: 'bold' }, - 'hljs-link:hover': { textDecoration: 'underline' }, - 'hljs-quote': { color: '#4C566A' }, - 'hljs-comment': { color: '#4C566A' }, - 'hljs-doctag': { color: '#8FBCBB' }, - 'hljs-meta': { color: '#5E81AC' }, - 'hljs-meta-keyword': { color: '#5E81AC' }, - 'hljs-meta-string': { color: '#A3BE8C' }, - 'hljs-attr': { color: '#8FBCBB' }, - 'hljs-attribute': { color: '#D8DEE9' }, - 'hljs-builtin-name': { color: '#81A1C1' }, - 'hljs-name': { color: '#81A1C1' }, - 'hljs-section': { color: '#88C0D0' }, - 'hljs-tag': { color: '#81A1C1' }, - 'hljs-variable': { color: '#D8DEE9' }, - 'hljs-template-variable': { color: '#D8DEE9' }, - 'hljs-template-tag': { color: '#5E81AC' }, - 'abnf .hljs-attribute': { color: '#88C0D0' }, - 'abnf .hljs-symbol': { color: '#EBCB8B' }, - 'apache .hljs-attribute': { color: '#88C0D0' }, - 'apache .hljs-section': { color: '#81A1C1' }, - 'arduino .hljs-built_in': { color: '#88C0D0' }, - 'aspectj .hljs-meta': { color: '#D08770' }, - 'aspectj > .hljs-title': { color: '#88C0D0' }, - 'bnf .hljs-attribute': { color: '#8FBCBB' }, - 'clojure .hljs-name': { color: '#88C0D0' }, - 'clojure .hljs-symbol': { color: '#EBCB8B' }, - 'coq .hljs-built_in': { color: '#88C0D0' }, - 'cpp .hljs-meta-string': { color: '#8FBCBB' }, - 'css .hljs-built_in': { color: '#88C0D0' }, - 'css .hljs-keyword': { color: '#D08770' }, - 'diff .hljs-meta': { color: '#8FBCBB' }, - 'ebnf .hljs-attribute': { color: '#8FBCBB' }, - 'glsl .hljs-built_in': { color: '#88C0D0' }, - 'groovy .hljs-meta:not(:first-child)': { color: '#D08770' }, - 'haxe .hljs-meta': { color: '#D08770' }, - 'java .hljs-meta': { color: '#D08770' }, - 'ldif .hljs-attribute': { color: '#8FBCBB' }, - 'lisp .hljs-name': { color: '#88C0D0' }, - 'lua .hljs-built_in': { color: '#88C0D0' }, - 'moonscript .hljs-built_in': { color: '#88C0D0' }, - 'nginx .hljs-attribute': { color: '#88C0D0' }, - 'nginx .hljs-section': { color: '#5E81AC' }, - 'pf .hljs-built_in': { color: '#88C0D0' }, - 'processing .hljs-built_in': { color: '#88C0D0' }, - 'scss .hljs-keyword': { color: '#81A1C1' }, - 'stylus .hljs-keyword': { color: '#81A1C1' }, - 'swift .hljs-meta': { color: '#D08770' }, - 'vim .hljs-built_in': { color: '#88C0D0', fontStyle: 'italic' }, - 'yaml .hljs-meta': { color: '#D08770' } - }, - obsidian: { - hljs: { - display: 'block', - overflowX: 'auto', - padding: '0.5em', - background: '#282b2e', - color: '#e0e2e4' - }, - 'hljs-keyword': { color: '#93c763', fontWeight: 'bold' }, - 'hljs-selector-tag': { color: '#93c763', fontWeight: 'bold' }, - 'hljs-literal': { color: '#93c763', fontWeight: 'bold' }, - 'hljs-selector-id': { color: '#93c763' }, - 'hljs-number': { color: '#ffcd22' }, - 'hljs-attribute': { color: '#668bb0' }, - 'hljs-code': { color: 'white' }, - 'hljs-class .hljs-title': { color: 'white' }, - 'hljs-section': { color: 'white', fontWeight: 'bold' }, - 'hljs-regexp': { color: '#d39745' }, - 'hljs-link': { color: '#d39745' }, - 'hljs-meta': { color: '#557182' }, - 'hljs-tag': { color: '#8cbbad' }, - 'hljs-name': { color: '#8cbbad', fontWeight: 'bold' }, - 'hljs-bullet': { color: '#8cbbad' }, - 'hljs-subst': { color: '#8cbbad' }, - 'hljs-emphasis': { color: '#8cbbad' }, - 'hljs-type': { color: '#8cbbad', fontWeight: 'bold' }, - 'hljs-built_in': { color: '#8cbbad' }, - 'hljs-selector-attr': { color: '#8cbbad' }, - 'hljs-selector-pseudo': { color: '#8cbbad' }, - 'hljs-addition': { color: '#8cbbad' }, - 'hljs-variable': { color: '#8cbbad' }, - 'hljs-template-tag': { color: '#8cbbad' }, - 'hljs-template-variable': { color: '#8cbbad' }, - 'hljs-string': { color: '#ec7600' }, - 'hljs-symbol': { color: '#ec7600' }, - 'hljs-comment': { color: '#818e96' }, - 'hljs-quote': { color: '#818e96' }, - 'hljs-deletion': { color: '#818e96' }, - 'hljs-selector-class': { color: '#A082BD' }, - 'hljs-doctag': { fontWeight: 'bold' }, - 'hljs-title': { fontWeight: 'bold' }, - 'hljs-strong': { fontWeight: 'bold' } - }, - 'tomorrow-night': { - 'hljs-comment': { color: '#969896' }, - 'hljs-quote': { color: '#969896' }, - 'hljs-variable': { color: '#cc6666' }, - 'hljs-template-variable': { color: '#cc6666' }, - 'hljs-tag': { color: '#cc6666' }, - 'hljs-name': { color: '#cc6666' }, - 'hljs-selector-id': { color: '#cc6666' }, - 'hljs-selector-class': { color: '#cc6666' }, - 'hljs-regexp': { color: '#cc6666' }, - 'hljs-deletion': { color: '#cc6666' }, - 'hljs-number': { color: '#de935f' }, - 'hljs-built_in': { color: '#de935f' }, - 'hljs-builtin-name': { color: '#de935f' }, - 'hljs-literal': { color: '#de935f' }, - 'hljs-type': { color: '#de935f' }, - 'hljs-params': { color: '#de935f' }, - 'hljs-meta': { color: '#de935f' }, - 'hljs-link': { color: '#de935f' }, - 'hljs-attribute': { color: '#f0c674' }, - 'hljs-string': { color: '#b5bd68' }, - 'hljs-symbol': { color: '#b5bd68' }, - 'hljs-bullet': { color: '#b5bd68' }, - 'hljs-addition': { color: '#b5bd68' }, - 'hljs-title': { color: '#81a2be' }, - 'hljs-section': { color: '#81a2be' }, - 'hljs-keyword': { color: '#b294bb' }, - 'hljs-selector-tag': { color: '#b294bb' }, - hljs: { - display: 'block', - overflowX: 'auto', - background: '#1d1f21', - color: '#c5c8c6', - padding: '0.5em' - }, - 'hljs-emphasis': { fontStyle: 'italic' }, - 'hljs-strong': { fontWeight: 'bold' } - }, - idea: { - hljs: { - display: 'block', - overflowX: 'auto', - padding: '0.5em', - color: '#000', - background: '#fff' - }, - 'hljs-subst': { fontWeight: 'normal', color: '#000' }, - 'hljs-title': { fontWeight: 'normal', color: '#000' }, - 'hljs-comment': { color: '#808080', fontStyle: 'italic' }, - 'hljs-quote': { color: '#808080', fontStyle: 'italic' }, - 'hljs-meta': { color: '#808000' }, - 'hljs-tag': { background: '#efefef' }, - 'hljs-section': { fontWeight: 'bold', color: '#000080' }, - 'hljs-name': { fontWeight: 'bold', color: '#000080' }, - 'hljs-literal': { fontWeight: 'bold', color: '#000080' }, - 'hljs-keyword': { fontWeight: 'bold', color: '#000080' }, - 'hljs-selector-tag': { fontWeight: 'bold', color: '#000080' }, - 'hljs-type': { fontWeight: 'bold', color: '#000080' }, - 'hljs-selector-id': { fontWeight: 'bold', color: '#000080' }, - 'hljs-selector-class': { fontWeight: 'bold', color: '#000080' }, - 'hljs-attribute': { fontWeight: 'bold', color: '#0000ff' }, - 'hljs-number': { fontWeight: 'normal', color: '#0000ff' }, - 'hljs-regexp': { fontWeight: 'normal', color: '#0000ff' }, - 'hljs-link': { fontWeight: 'normal', color: '#0000ff' }, - 'hljs-string': { color: '#008000', fontWeight: 'bold' }, - 'hljs-symbol': { color: '#000', background: '#d0eded', fontStyle: 'italic' }, - 'hljs-bullet': { color: '#000', background: '#d0eded', fontStyle: 'italic' }, - 'hljs-formula': { color: '#000', background: '#d0eded', fontStyle: 'italic' }, - 'hljs-doctag': { textDecoration: 'underline' }, - 'hljs-variable': { color: '#660e7a' }, - 'hljs-template-variable': { color: '#660e7a' }, - 'hljs-addition': { background: '#baeeba' }, - 'hljs-deletion': { background: '#ffc8bd' }, - 'hljs-emphasis': { fontStyle: 'italic' }, - 'hljs-strong': { fontWeight: 'bold' } - } - }, - rk = Qx, - components_SyntaxHighlighter = ({ - language: s, - className: o = '', - getConfigs: i, - syntaxHighlighting: u = {}, - children: _ = '' - }) => { - const w = i().syntaxHighlight.theme, - { styles: x, defaultStyle: C } = u, - j = x?.[w] ?? C; - return Pe.createElement(Bx, { language: s, className: o, style: j }, _); - }; - var nk = __webpack_require__(5419), - sk = __webpack_require__.n(nk); - const components_HighlightCode = ({ - fileName: s = 'response.txt', - className: o, - downloadable: i, - getComponent: u, - canCopy: _, - language: w, - children: x - }) => { - const C = (0, Pe.useRef)(null), - j = u('SyntaxHighlighter', !0), - handlePreventYScrollingBeyondElement = (s) => { - const { target: o, deltaY: i } = s, - { scrollHeight: u, offsetHeight: _, scrollTop: w } = o; - u > _ && ((0 === w && i < 0) || (_ + w >= u && i > 0)) && s.preventDefault(); - }; - return ( - (0, Pe.useEffect)(() => { - const s = Array.from(C.current.childNodes).filter( - (s) => !!s.nodeType && s.classList.contains('microlight') - ); - return ( - s.forEach((s) => - s.addEventListener('mousewheel', handlePreventYScrollingBeyondElement, { - passive: !1 - }) - ), - () => { - s.forEach((s) => - s.removeEventListener('mousewheel', handlePreventYScrollingBeyondElement) - ); - } - ); - }, [x, o, w]), - Pe.createElement( - 'div', - { className: 'highlight-code', ref: C }, - _ && - Pe.createElement( - 'div', - { className: 'copy-to-clipboard' }, - Pe.createElement( - Jn.CopyToClipboard, - { text: x }, - Pe.createElement('button', null) - ) - ), - i - ? Pe.createElement( - 'button', - { - className: 'download-contents', - onClick: () => { - sk()(x, s); - } - }, - 'Download' - ) - : null, - Pe.createElement( - j, - { - language: w, - className: Hn()(o, 'microlight'), - renderPlainText: ({ children: s, PlainTextViewer: i }) => - Pe.createElement(i, { className: o }, s) - }, - x - ) - ) - ); - }, - components_PlainTextViewer = ({ className: s = '', children: o }) => - Pe.createElement('pre', { className: Hn()('microlight', s) }, o), - wrap_components_SyntaxHighlighter = - (s, o) => - ({ renderPlainText: i, children: u, ..._ }) => { - const w = o.getConfigs().syntaxHighlight.activated, - x = o.getComponent('PlainTextViewer'); - return w || 'function' != typeof i - ? w - ? Pe.createElement(s, _, u) - : Pe.createElement(x, null, u) - : i({ children: u, PlainTextViewer: x }); - }, - SyntaxHighlightingPlugin1 = () => ({ - afterLoad: after_load, - rootInjects: { syntaxHighlighting: { styles: tk, defaultStyle: rk } }, - components: { - SyntaxHighlighter: components_SyntaxHighlighter, - HighlightCode: components_HighlightCode, - PlainTextViewer: components_PlainTextViewer - } - }), - SyntaxHighlightingPlugin2 = () => ({ - wrapComponents: { SyntaxHighlighter: wrap_components_SyntaxHighlighter } - }), - syntax_highlighting = () => [SyntaxHighlightingPlugin1, SyntaxHighlightingPlugin2], - versions_after_load = () => { - const { - GIT_DIRTY: s, - GIT_COMMIT: o, - PACKAGE_VERSION: i, - BUILD_TIME: u - } = { - PACKAGE_VERSION: '5.18.2', - GIT_COMMIT: 'g1dd1f7cc', - GIT_DIRTY: !0, - BUILD_TIME: 'Thu, 07 Nov 2024 14:01:17 GMT' - }; - (at.versions = at.versions || {}), - (at.versions.swaggerUI = { - version: i, - gitRevision: o, - gitDirty: s, - buildTimestamp: u - }); - }, - versions = () => ({ afterLoad: versions_after_load }); - var ok = __webpack_require__(47248), - lk = __webpack_require__.n(ok); - const uk = console.error, - withErrorBoundary = (s) => (o) => { - const { getComponent: i, fn: u } = s(), - _ = i('ErrorBoundary'), - w = u.getDisplayName(o); - class WithErrorBoundary extends Pe.Component { - render() { - return Pe.createElement( - _, - { targetName: w, getComponent: i, fn: u }, - Pe.createElement(o, Rn()({}, this.props, this.context)) - ); - } - } - var x; - return ( - (WithErrorBoundary.displayName = `WithErrorBoundary(${w})`), - (x = o).prototype && - x.prototype.isReactComponent && - (WithErrorBoundary.prototype.mapStateToProps = o.prototype.mapStateToProps), - WithErrorBoundary - ); - }, - fallback = ({ name: s }) => - Pe.createElement( - 'div', - { className: 'fallback' }, - '😱 ', - Pe.createElement( - 'i', - null, - 'Could not render ', - 't' === s ? 'this component' : s, - ', see the console.' - ) - ); - class ErrorBoundary extends Pe.Component { - static defaultProps = { - targetName: 'this component', - getComponent: () => fallback, - fn: { componentDidCatch: uk }, - children: null - }; - static getDerivedStateFromError(s) { - return { hasError: !0, error: s }; - } - constructor(...s) { - super(...s), (this.state = { hasError: !1, error: null }); - } - componentDidCatch(s, o) { - this.props.fn.componentDidCatch(s, o); - } - render() { - const { getComponent: s, targetName: o, children: i } = this.props; - if (this.state.hasError) { - const i = s('Fallback'); - return Pe.createElement(i, { name: o }); - } - return i; - } - } - const pk = ErrorBoundary, - safe_render = - ({ componentList: s = [], fullOverride: o = !1 } = {}) => - ({ getSystem: i }) => { - const u = o - ? s - : [ - 'App', - 'BaseLayout', - 'VersionPragmaFilter', - 'InfoContainer', - 'ServersContainer', - 'SchemesContainer', - 'AuthorizeBtnContainer', - 'FilterContainer', - 'Operations', - 'OperationContainer', - 'parameters', - 'responses', - 'OperationServers', - 'Models', - 'ModelWrapper', - ...s - ], - _ = lk()( - u, - Array(u.length).fill((s, { fn: o }) => o.withErrorBoundary(s)) - ); - return { - fn: { componentDidCatch: uk, withErrorBoundary: withErrorBoundary(i) }, - components: { ErrorBoundary: pk, Fallback: fallback }, - wrapComponents: _ - }; - }; - class App extends Pe.Component { - getLayout() { - const { getComponent: s, layoutSelectors: o } = this.props, - i = o.current(), - u = s(i, !0); - return u || (() => Pe.createElement('h1', null, ' No layout defined for "', i, '" ')); - } - render() { - const s = this.getLayout(); - return Pe.createElement(s, null); - } - } - const fk = App; - class AuthorizationPopup extends Pe.Component { - close = () => { - let { authActions: s } = this.props; - s.showDefinitions(!1); - }; - render() { - let { - authSelectors: s, - authActions: o, - getComponent: i, - errSelectors: u, - specSelectors: _, - fn: { AST: w = {} } - } = this.props, - x = s.shownDefinitions(); - const C = i('auths'), - j = i('CloseIcon'); - return Pe.createElement( - 'div', - { className: 'dialog-ux' }, - Pe.createElement('div', { className: 'backdrop-ux' }), - Pe.createElement( - 'div', - { className: 'modal-ux' }, - Pe.createElement( - 'div', - { className: 'modal-dialog-ux' }, - Pe.createElement( - 'div', - { className: 'modal-ux-inner' }, - Pe.createElement( - 'div', - { className: 'modal-ux-header' }, - Pe.createElement('h3', null, 'Available authorizations'), - Pe.createElement( - 'button', - { type: 'button', className: 'close-modal', onClick: this.close }, - Pe.createElement(j, null) - ) - ), - Pe.createElement( - 'div', - { className: 'modal-ux-content' }, - x.valueSeq().map((x, j) => - Pe.createElement(C, { - key: j, - AST: w, - definitions: x, - getComponent: i, - errSelectors: u, - authSelectors: s, - authActions: o, - specSelectors: _ - }) - ) - ) - ) - ) - ) - ); - } - } - class AuthorizeBtn extends Pe.Component { - render() { - let { isAuthorized: s, showPopup: o, onClick: i, getComponent: u } = this.props; - const _ = u('authorizationPopup', !0), - w = u('LockAuthIcon', !0), - x = u('UnlockAuthIcon', !0); - return Pe.createElement( - 'div', - { className: 'auth-wrapper' }, - Pe.createElement( - 'button', - { className: s ? 'btn authorize locked' : 'btn authorize unlocked', onClick: i }, - Pe.createElement('span', null, 'Authorize'), - s ? Pe.createElement(w, null) : Pe.createElement(x, null) - ), - o && Pe.createElement(_, null) - ); - } - } - class AuthorizeBtnContainer extends Pe.Component { - render() { - const { - authActions: s, - authSelectors: o, - specSelectors: i, - getComponent: u - } = this.props, - _ = i.securityDefinitions(), - w = o.definitionsToAuthorize(), - x = u('authorizeBtn'); - return _ - ? Pe.createElement(x, { - onClick: () => s.showDefinitions(w), - isAuthorized: !!o.authorized().size, - showPopup: !!o.shownDefinitions(), - getComponent: u - }) - : null; - } - } - class AuthorizeOperationBtn extends Pe.Component { - onClick = (s) => { - s.stopPropagation(); - let { onClick: o } = this.props; - o && o(); - }; - render() { - let { isAuthorized: s, getComponent: o } = this.props; - const i = o('LockAuthOperationIcon', !0), - u = o('UnlockAuthOperationIcon', !0); - return Pe.createElement( - 'button', - { - className: 'authorization__btn', - 'aria-label': s ? 'authorization button locked' : 'authorization button unlocked', - onClick: this.onClick - }, - s - ? Pe.createElement(i, { className: 'locked' }) - : Pe.createElement(u, { className: 'unlocked' }) - ); - } - } - class Auths extends Pe.Component { - constructor(s, o) { - super(s, o), (this.state = {}); - } - onAuthChange = (s) => { - let { name: o } = s; - this.setState({ [o]: s }); - }; - submitAuth = (s) => { - s.preventDefault(); - let { authActions: o } = this.props; - o.authorizeWithPersistOption(this.state); - }; - logoutClick = (s) => { - s.preventDefault(); - let { authActions: o, definitions: i } = this.props, - u = i.map((s, o) => o).toArray(); - this.setState(u.reduce((s, o) => ((s[o] = ''), s), {})), o.logoutWithPersistOption(u); - }; - close = (s) => { - s.preventDefault(); - let { authActions: o } = this.props; - o.showDefinitions(!1); - }; - render() { - let { definitions: s, getComponent: o, authSelectors: i, errSelectors: u } = this.props; - const _ = o('AuthItem'), - w = o('oauth2', !0), - x = o('Button'); - let C = i.authorized(), - j = s.filter((s, o) => !!C.get(o)), - L = s.filter((s) => 'oauth2' !== s.get('type')), - B = s.filter((s) => 'oauth2' === s.get('type')); - return Pe.createElement( - 'div', - { className: 'auth-container' }, - !!L.size && - Pe.createElement( - 'form', - { onSubmit: this.submitAuth }, - L.map((s, i) => - Pe.createElement(_, { - key: i, - schema: s, - name: i, - getComponent: o, - onAuthChange: this.onAuthChange, - authorized: C, - errSelectors: u - }) - ).toArray(), - Pe.createElement( - 'div', - { className: 'auth-btn-wrapper' }, - L.size === j.size - ? Pe.createElement( - x, - { - className: 'btn modal-btn auth', - onClick: this.logoutClick, - 'aria-label': 'Remove authorization' - }, - 'Logout' - ) - : Pe.createElement( - x, - { - type: 'submit', - className: 'btn modal-btn auth authorize', - 'aria-label': 'Apply credentials' - }, - 'Authorize' - ), - Pe.createElement( - x, - { className: 'btn modal-btn auth btn-done', onClick: this.close }, - 'Close' - ) - ) - ), - B && B.size - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'div', - { className: 'scope-def' }, - Pe.createElement( - 'p', - null, - 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.' - ), - Pe.createElement( - 'p', - null, - 'API requires the following scopes. Select which ones you want to grant to Swagger UI.' - ) - ), - s - .filter((s) => 'oauth2' === s.get('type')) - .map((s, o) => - Pe.createElement( - 'div', - { key: o }, - Pe.createElement(w, { authorized: C, schema: s, name: o }) - ) - ) - .toArray() - ) - : null - ); - } - } - class auth_item_Auths extends Pe.Component { - render() { - let { - schema: s, - name: o, - getComponent: i, - onAuthChange: u, - authorized: _, - errSelectors: w - } = this.props; - const x = i('apiKeyAuth'), - C = i('basicAuth'); - let j; - const L = s.get('type'); - switch (L) { - case 'apiKey': - j = Pe.createElement(x, { - key: o, - schema: s, - name: o, - errSelectors: w, - authorized: _, - getComponent: i, - onChange: u - }); - break; - case 'basic': - j = Pe.createElement(C, { - key: o, - schema: s, - name: o, - errSelectors: w, - authorized: _, - getComponent: i, - onChange: u - }); - break; - default: - j = Pe.createElement('div', { key: o }, 'Unknown security definition type ', L); - } - return Pe.createElement('div', { key: `${o}-jump` }, j); - } - } - class AuthError extends Pe.Component { - render() { - let { error: s } = this.props, - o = s.get('level'), - i = s.get('message'), - u = s.get('source'); - return Pe.createElement( - 'div', - { className: 'errors' }, - Pe.createElement('b', null, u, ' ', o), - Pe.createElement('span', null, i) - ); - } - } - class ApiKeyAuth extends Pe.Component { - constructor(s, o) { - super(s, o); - let { name: i, schema: u } = this.props, - _ = this.getValue(); - this.state = { name: i, schema: u, value: _ }; - } - getValue() { - let { name: s, authorized: o } = this.props; - return o && o.getIn([s, 'value']); - } - onChange = (s) => { - let { onChange: o } = this.props, - i = s.target.value, - u = Object.assign({}, this.state, { value: i }); - this.setState(u), o(u); - }; - render() { - let { schema: s, getComponent: o, errSelectors: i, name: u } = this.props; - const _ = o('Input'), - w = o('Row'), - x = o('Col'), - C = o('authError'), - j = o('Markdown', !0), - L = o('JumpToPath', !0); - let B = this.getValue(), - $ = i.allErrors().filter((s) => s.get('authId') === u); - return Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - Pe.createElement('code', null, u || s.get('name')), - ' (apiKey)', - Pe.createElement(L, { path: ['securityDefinitions', u] }) - ), - B && Pe.createElement('h6', null, 'Authorized'), - Pe.createElement(w, null, Pe.createElement(j, { source: s.get('description') })), - Pe.createElement( - w, - null, - Pe.createElement('p', null, 'Name: ', Pe.createElement('code', null, s.get('name'))) - ), - Pe.createElement( - w, - null, - Pe.createElement('p', null, 'In: ', Pe.createElement('code', null, s.get('in'))) - ), - Pe.createElement( - w, - null, - Pe.createElement('label', { htmlFor: 'api_key_value' }, 'Value:'), - B - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - x, - null, - Pe.createElement(_, { - id: 'api_key_value', - type: 'text', - onChange: this.onChange, - autoFocus: !0 - }) - ) - ), - $.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) - ); - } - } - class BasicAuth extends Pe.Component { - constructor(s, o) { - super(s, o); - let { schema: i, name: u } = this.props, - _ = this.getValue().username; - this.state = { name: u, schema: i, value: _ ? { username: _ } : {} }; - } - getValue() { - let { authorized: s, name: o } = this.props; - return (s && s.getIn([o, 'value'])) || {}; - } - onChange = (s) => { - let { onChange: o } = this.props, - { value: i, name: u } = s.target, - _ = this.state.value; - (_[u] = i), this.setState({ value: _ }), o(this.state); - }; - render() { - let { schema: s, getComponent: o, name: i, errSelectors: u } = this.props; - const _ = o('Input'), - w = o('Row'), - x = o('Col'), - C = o('authError'), - j = o('JumpToPath', !0), - L = o('Markdown', !0); - let B = this.getValue().username, - $ = u.allErrors().filter((s) => s.get('authId') === i); - return Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - 'Basic authorization', - Pe.createElement(j, { path: ['securityDefinitions', i] }) - ), - B && Pe.createElement('h6', null, 'Authorized'), - Pe.createElement(w, null, Pe.createElement(L, { source: s.get('description') })), - Pe.createElement( - w, - null, - Pe.createElement('label', { htmlFor: 'auth_username' }, 'Username:'), - B - ? Pe.createElement('code', null, ' ', B, ' ') - : Pe.createElement( - x, - null, - Pe.createElement(_, { - id: 'auth_username', - type: 'text', - required: 'required', - name: 'username', - onChange: this.onChange, - autoFocus: !0 - }) - ) - ), - Pe.createElement( - w, - null, - Pe.createElement('label', { htmlFor: 'auth_password' }, 'Password:'), - B - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - x, - null, - Pe.createElement(_, { - id: 'auth_password', - autoComplete: 'new-password', - name: 'password', - type: 'password', - onChange: this.onChange - }) - ) - ), - $.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) - ); - } - } - function example_Example(s) { - const { example: o, showValue: i, getComponent: u } = s, - _ = u('Markdown', !0), - w = u('HighlightCode', !0); - return o - ? Pe.createElement( - 'div', - { className: 'example' }, - o.get('description') - ? Pe.createElement( - 'section', - { className: 'example__section' }, - Pe.createElement( - 'div', - { className: 'example__section-header' }, - 'Example Description' - ), - Pe.createElement( - 'p', - null, - Pe.createElement(_, { source: o.get('description') }) - ) - ) - : null, - i && o.has('value') - ? Pe.createElement( - 'section', - { className: 'example__section' }, - Pe.createElement( - 'div', - { className: 'example__section-header' }, - 'Example Value' - ), - Pe.createElement(w, null, stringify(o.get('value'))) - ) - : null - ) - : null; - } - class ExamplesSelect extends Pe.PureComponent { - static defaultProps = { - examples: $e().Map({}), - onSelect: (...s) => - console.log('DEBUG: ExamplesSelect was not given an onSelect callback', ...s), - currentExampleKey: null, - showLabels: !0 - }; - _onSelect = (s, { isSyntheticChange: o = !1 } = {}) => { - 'function' == typeof this.props.onSelect && - this.props.onSelect(s, { isSyntheticChange: o }); - }; - _onDomSelect = (s) => { - if ('function' == typeof this.props.onSelect) { - const o = s.target.selectedOptions[0].getAttribute('value'); - this._onSelect(o, { isSyntheticChange: !1 }); - } - }; - getCurrentExample = () => { - const { examples: s, currentExampleKey: o } = this.props, - i = s.get(o), - u = s.keySeq().first(), - _ = s.get(u); - return i || _ || Map({}); - }; - componentDidMount() { - const { onSelect: s, examples: o } = this.props; - if ('function' == typeof s) { - const s = o.first(), - i = o.keyOf(s); - this._onSelect(i, { isSyntheticChange: !0 }); - } - } - UNSAFE_componentWillReceiveProps(s) { - const { currentExampleKey: o, examples: i } = s; - if (i !== this.props.examples && !i.has(o)) { - const s = i.first(), - o = i.keyOf(s); - this._onSelect(o, { isSyntheticChange: !0 }); - } - } - render() { - const { - examples: s, - currentExampleKey: o, - isValueModified: i, - isModifiedValueAvailable: u, - showLabels: _ - } = this.props; - return Pe.createElement( - 'div', - { className: 'examples-select' }, - _ - ? Pe.createElement( - 'span', - { className: 'examples-select__section-label' }, - 'Examples: ' - ) - : null, - Pe.createElement( - 'select', - { - className: 'examples-select-element', - onChange: this._onDomSelect, - value: u && i ? '__MODIFIED__VALUE__' : o || '' - }, - u - ? Pe.createElement('option', { value: '__MODIFIED__VALUE__' }, '[Modified value]') - : null, - s - .map((s, o) => - Pe.createElement('option', { key: o, value: o }, s.get('summary') || o) - ) - .valueSeq() - ) - ); - } - } - const stringifyUnlessList = (s) => (qe.List.isList(s) ? s : stringify(s)); - class ExamplesSelectValueRetainer extends Pe.PureComponent { - static defaultProps = { - userHasEditedBody: !1, - examples: (0, qe.Map)({}), - currentNamespace: '__DEFAULT__NAMESPACE__', - setRetainRequestBodyValueFlag: () => {}, - onSelect: (...s) => - console.log('ExamplesSelectValueRetainer: no `onSelect` function was provided', ...s), - updateValue: (...s) => - console.log( - 'ExamplesSelectValueRetainer: no `updateValue` function was provided', - ...s - ) - }; - constructor(s) { - super(s); - const o = this._getCurrentExampleValue(); - this.state = { - [s.currentNamespace]: (0, qe.Map)({ - lastUserEditedValue: this.props.currentUserInputValue, - lastDownstreamValue: o, - isModifiedValueSelected: - this.props.userHasEditedBody || this.props.currentUserInputValue !== o - }) - }; - } - componentWillUnmount() { - this.props.setRetainRequestBodyValueFlag(!1); - } - _getStateForCurrentNamespace = () => { - const { currentNamespace: s } = this.props; - return (this.state[s] || (0, qe.Map)()).toObject(); - }; - _setStateForCurrentNamespace = (s) => { - const { currentNamespace: o } = this.props; - return this._setStateForNamespace(o, s); - }; - _setStateForNamespace = (s, o) => { - const i = (this.state[s] || (0, qe.Map)()).mergeDeep(o); - return this.setState({ [s]: i }); - }; - _isCurrentUserInputSameAsExampleValue = () => { - const { currentUserInputValue: s } = this.props; - return this._getCurrentExampleValue() === s; - }; - _getValueForExample = (s, o) => { - const { examples: i } = o || this.props; - return stringifyUnlessList((i || (0, qe.Map)({})).getIn([s, 'value'])); - }; - _getCurrentExampleValue = (s) => { - const { currentKey: o } = s || this.props; - return this._getValueForExample(o, s || this.props); - }; - _onExamplesSelect = (s, { isSyntheticChange: o } = {}, ...i) => { - const { - onSelect: u, - updateValue: _, - currentUserInputValue: w, - userHasEditedBody: x - } = this.props, - { lastUserEditedValue: C } = this._getStateForCurrentNamespace(), - j = this._getValueForExample(s); - if ('__MODIFIED__VALUE__' === s) - return ( - _(stringifyUnlessList(C)), - this._setStateForCurrentNamespace({ isModifiedValueSelected: !0 }) - ); - 'function' == typeof u && u(s, { isSyntheticChange: o }, ...i), - this._setStateForCurrentNamespace({ - lastDownstreamValue: j, - isModifiedValueSelected: (o && x) || (!!w && w !== j) - }), - o || ('function' == typeof _ && _(stringifyUnlessList(j))); - }; - UNSAFE_componentWillReceiveProps(s) { - const { currentUserInputValue: o, examples: i, onSelect: u, userHasEditedBody: _ } = s, - { lastUserEditedValue: w, lastDownstreamValue: x } = - this._getStateForCurrentNamespace(), - C = this._getValueForExample(s.currentKey, s), - j = i.filter((s) => s.get('value') === o || stringify(s.get('value')) === o); - if (j.size) { - let o; - (o = j.has(s.currentKey) ? s.currentKey : j.keySeq().first()), - u(o, { isSyntheticChange: !0 }); - } else - o !== this.props.currentUserInputValue && - o !== w && - o !== x && - (this.props.setRetainRequestBodyValueFlag(!0), - this._setStateForNamespace(s.currentNamespace, { - lastUserEditedValue: s.currentUserInputValue, - isModifiedValueSelected: _ || o !== C - })); - } - render() { - const { - currentUserInputValue: s, - examples: o, - currentKey: i, - getComponent: u, - userHasEditedBody: _ - } = this.props, - { - lastDownstreamValue: w, - lastUserEditedValue: x, - isModifiedValueSelected: C - } = this._getStateForCurrentNamespace(), - j = u('ExamplesSelect'); - return Pe.createElement(j, { - examples: o, - currentExampleKey: i, - onSelect: this._onExamplesSelect, - isModifiedValueAvailable: !!x && x !== w, - isValueModified: (void 0 !== s && C && s !== this._getCurrentExampleValue()) || _ - }); - } - } - function oauth2_authorize_authorize({ - auth: s, - authActions: o, - errActions: i, - configs: u, - authConfigs: _ = {}, - currentServer: w - }) { - let { schema: x, scopes: C, name: j, clientId: L } = s, - B = x.get('flow'), - $ = []; - switch (B) { - case 'password': - return void o.authorizePassword(s); - case 'application': - case 'clientCredentials': - case 'client_credentials': - return void o.authorizeApplication(s); - case 'accessCode': - case 'authorizationCode': - case 'authorization_code': - $.push('response_type=code'); - break; - case 'implicit': - $.push('response_type=token'); - } - 'string' == typeof L && $.push('client_id=' + encodeURIComponent(L)); - let V = u.oauth2RedirectUrl; - if (void 0 === V) - return void i.newAuthErr({ - authId: j, - source: 'validation', - level: 'error', - message: - 'oauth2RedirectUrl configuration is not passed. Oauth2 authorization cannot be performed.' - }); - $.push('redirect_uri=' + encodeURIComponent(V)); - let U = []; - if ( - (Array.isArray(C) ? (U = C) : $e().List.isList(C) && (U = C.toArray()), U.length > 0) - ) { - let s = _.scopeSeparator || ' '; - $.push('scope=' + encodeURIComponent(U.join(s))); - } - let z = utils_btoa(new Date()); - if ( - ($.push('state=' + encodeURIComponent(z)), - void 0 !== _.realm && $.push('realm=' + encodeURIComponent(_.realm)), - ('authorizationCode' === B || 'authorization_code' === B || 'accessCode' === B) && - _.usePkceWithAuthorizationCodeGrant) - ) { - const o = (function generateCodeVerifier() { - return b64toB64UrlEncoded(St()(32).toString('base64')); - })(), - i = (function createCodeChallenge(s) { - return b64toB64UrlEncoded(kt()('sha256').update(s).digest('base64')); - })(o); - $.push('code_challenge=' + i), - $.push('code_challenge_method=S256'), - (s.codeVerifier = o); - } - let { additionalQueryStringParams: Y } = _; - for (let s in Y) void 0 !== Y[s] && $.push([s, Y[s]].map(encodeURIComponent).join('=')); - const Z = x.get('authorizationUrl'); - let ee; - ee = w ? Mt()(sanitizeUrl(Z), w, !0).toString() : sanitizeUrl(Z); - let ie, - ae = [ee, $.join('&')].join(-1 === Z.indexOf('?') ? '?' : '&'); - (ie = - 'implicit' === B - ? o.preAuthorizeImplicit - : _.useBasicAuthenticationWithAccessCodeGrant - ? o.authorizeAccessCodeWithBasicAuthentication - : o.authorizeAccessCodeWithFormParams), - o.authPopup(ae, { - auth: s, - state: z, - redirectUrl: V, - callback: ie, - errCb: i.newAuthErr - }); - } - class Oauth2 extends Pe.Component { - constructor(s, o) { - super(s, o); - let { name: i, schema: u, authorized: _, authSelectors: w } = this.props, - x = _ && _.get(i), - C = w.getConfigs() || {}, - j = (x && x.get('username')) || '', - L = (x && x.get('clientId')) || C.clientId || '', - B = (x && x.get('clientSecret')) || C.clientSecret || '', - $ = (x && x.get('passwordType')) || 'basic', - V = (x && x.get('scopes')) || C.scopes || []; - 'string' == typeof V && (V = V.split(C.scopeSeparator || ' ')), - (this.state = { - appName: C.appName, - name: i, - schema: u, - scopes: V, - clientId: L, - clientSecret: B, - username: j, - password: '', - passwordType: $ - }); - } - close = (s) => { - s.preventDefault(); - let { authActions: o } = this.props; - o.showDefinitions(!1); - }; - authorize = () => { - let { - authActions: s, - errActions: o, - getConfigs: i, - authSelectors: u, - oas3Selectors: _ - } = this.props, - w = i(), - x = u.getConfigs(); - o.clear({ authId: name, type: 'auth', source: 'auth' }), - oauth2_authorize_authorize({ - auth: this.state, - currentServer: _.serverEffectiveValue(_.selectedServer()), - authActions: s, - errActions: o, - configs: w, - authConfigs: x - }); - }; - onScopeChange = (s) => { - let { target: o } = s, - { checked: i } = o, - u = o.dataset.value; - if (i && -1 === this.state.scopes.indexOf(u)) { - let s = this.state.scopes.concat([u]); - this.setState({ scopes: s }); - } else - !i && - this.state.scopes.indexOf(u) > -1 && - this.setState({ scopes: this.state.scopes.filter((s) => s !== u) }); - }; - onInputChange = (s) => { - let { - target: { - dataset: { name: o }, - value: i - } - } = s, - u = { [o]: i }; - this.setState(u); - }; - selectScopes = (s) => { - s.target.dataset.all - ? this.setState({ - scopes: Array.from( - ( - this.props.schema.get('allowedScopes') || this.props.schema.get('scopes') - ).keys() - ) - }) - : this.setState({ scopes: [] }); - }; - logout = (s) => { - s.preventDefault(); - let { authActions: o, errActions: i, name: u } = this.props; - i.clear({ authId: u, type: 'auth', source: 'auth' }), o.logoutWithPersistOption([u]); - }; - render() { - let { - schema: s, - getComponent: o, - authSelectors: i, - errSelectors: u, - name: _, - specSelectors: w - } = this.props; - const x = o('Input'), - C = o('Row'), - j = o('Col'), - L = o('Button'), - B = o('authError'), - $ = o('JumpToPath', !0), - V = o('Markdown', !0), - U = o('InitializedInput'), - { isOAS3: z } = w; - let Y = z() ? s.get('openIdConnectUrl') : null; - const Z = 'implicit', - ee = 'password', - ie = z() ? (Y ? 'authorization_code' : 'authorizationCode') : 'accessCode', - ae = z() ? (Y ? 'client_credentials' : 'clientCredentials') : 'application'; - let le = !!(i.getConfigs() || {}).usePkceWithAuthorizationCodeGrant, - ce = s.get('flow'), - pe = ce === ie && le ? ce + ' with PKCE' : ce, - de = s.get('allowedScopes') || s.get('scopes'), - fe = !!i.authorized().get(_), - ye = u.allErrors().filter((s) => s.get('authId') === _), - be = !ye.filter((s) => 'validation' === s.get('source')).size, - _e = s.get('description'); - return Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - _, - ' (OAuth2, ', - pe, - ') ', - Pe.createElement($, { path: ['securityDefinitions', _] }) - ), - this.state.appName - ? Pe.createElement('h5', null, 'Application: ', this.state.appName, ' ') - : null, - _e && Pe.createElement(V, { source: s.get('description') }), - fe && Pe.createElement('h6', null, 'Authorized'), - Y && - Pe.createElement( - 'p', - null, - 'OpenID Connect URL: ', - Pe.createElement('code', null, Y) - ), - (ce === Z || ce === ie) && - Pe.createElement( - 'p', - null, - 'Authorization URL: ', - Pe.createElement('code', null, s.get('authorizationUrl')) - ), - (ce === ee || ce === ie || ce === ae) && - Pe.createElement( - 'p', - null, - 'Token URL:', - Pe.createElement('code', null, ' ', s.get('tokenUrl')) - ), - Pe.createElement( - 'p', - { className: 'flow' }, - 'Flow: ', - Pe.createElement('code', null, pe) - ), - ce !== ee - ? null - : Pe.createElement( - C, - null, - Pe.createElement( - C, - null, - Pe.createElement('label', { htmlFor: 'oauth_username' }, 'username:'), - fe - ? Pe.createElement('code', null, ' ', this.state.username, ' ') - : Pe.createElement( - j, - { tablet: 10, desktop: 10 }, - Pe.createElement('input', { - id: 'oauth_username', - type: 'text', - 'data-name': 'username', - onChange: this.onInputChange, - autoFocus: !0 - }) - ) - ), - Pe.createElement( - C, - null, - Pe.createElement('label', { htmlFor: 'oauth_password' }, 'password:'), - fe - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - j, - { tablet: 10, desktop: 10 }, - Pe.createElement('input', { - id: 'oauth_password', - type: 'password', - 'data-name': 'password', - onChange: this.onInputChange - }) - ) - ), - Pe.createElement( - C, - null, - Pe.createElement( - 'label', - { htmlFor: 'password_type' }, - 'Client credentials location:' - ), - fe - ? Pe.createElement('code', null, ' ', this.state.passwordType, ' ') - : Pe.createElement( - j, - { tablet: 10, desktop: 10 }, - Pe.createElement( - 'select', - { - id: 'password_type', - 'data-name': 'passwordType', - onChange: this.onInputChange - }, - Pe.createElement( - 'option', - { value: 'basic' }, - 'Authorization header' - ), - Pe.createElement('option', { value: 'request-body' }, 'Request body') - ) - ) - ) - ), - (ce === ae || ce === Z || ce === ie || ce === ee) && - (!fe || (fe && this.state.clientId)) && - Pe.createElement( - C, - null, - Pe.createElement('label', { htmlFor: `client_id_${ce}` }, 'client_id:'), - fe - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - j, - { tablet: 10, desktop: 10 }, - Pe.createElement(U, { - id: `client_id_${ce}`, - type: 'text', - required: ce === ee, - initialValue: this.state.clientId, - 'data-name': 'clientId', - onChange: this.onInputChange - }) - ) - ), - (ce === ae || ce === ie || ce === ee) && - Pe.createElement( - C, - null, - Pe.createElement('label', { htmlFor: `client_secret_${ce}` }, 'client_secret:'), - fe - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - j, - { tablet: 10, desktop: 10 }, - Pe.createElement(U, { - id: `client_secret_${ce}`, - initialValue: this.state.clientSecret, - type: 'password', - 'data-name': 'clientSecret', - onChange: this.onInputChange - }) - ) - ), - !fe && de && de.size - ? Pe.createElement( - 'div', - { className: 'scopes' }, - Pe.createElement( - 'h2', - null, - 'Scopes:', - Pe.createElement( - 'a', - { onClick: this.selectScopes, 'data-all': !0 }, - 'select all' - ), - Pe.createElement('a', { onClick: this.selectScopes }, 'select none') - ), - de - .map((s, o) => - Pe.createElement( - C, - { key: o }, - Pe.createElement( - 'div', - { className: 'checkbox' }, - Pe.createElement(x, { - 'data-value': o, - id: `${o}-${ce}-checkbox-${this.state.name}`, - disabled: fe, - checked: this.state.scopes.includes(o), - type: 'checkbox', - onChange: this.onScopeChange - }), - Pe.createElement( - 'label', - { htmlFor: `${o}-${ce}-checkbox-${this.state.name}` }, - Pe.createElement('span', { className: 'item' }), - Pe.createElement( - 'div', - { className: 'text' }, - Pe.createElement('p', { className: 'name' }, o), - Pe.createElement('p', { className: 'description' }, s) - ) - ) - ) - ) - ) - .toArray() - ) - : null, - ye.valueSeq().map((s, o) => Pe.createElement(B, { error: s, key: o })), - Pe.createElement( - 'div', - { className: 'auth-btn-wrapper' }, - be && - (fe - ? Pe.createElement( - L, - { - className: 'btn modal-btn auth authorize', - onClick: this.logout, - 'aria-label': 'Remove authorization' - }, - 'Logout' - ) - : Pe.createElement( - L, - { - className: 'btn modal-btn auth authorize', - onClick: this.authorize, - 'aria-label': 'Apply given OAuth2 credentials' - }, - 'Authorize' - )), - Pe.createElement( - L, - { className: 'btn modal-btn auth btn-done', onClick: this.close }, - 'Close' - ) - ) - ); - } - } - class Clear extends Pe.Component { - onClick = () => { - let { specActions: s, path: o, method: i } = this.props; - s.clearResponse(o, i), s.clearRequest(o, i); - }; - render() { - return Pe.createElement( - 'button', - { className: 'btn btn-clear opblock-control__btn', onClick: this.onClick }, - 'Clear' - ); - } - } - const live_response_Headers = ({ headers: s }) => - Pe.createElement( - 'div', - null, - Pe.createElement('h5', null, 'Response headers'), - Pe.createElement('pre', { className: 'microlight' }, s) - ), - Duration = ({ duration: s }) => - Pe.createElement( - 'div', - null, - Pe.createElement('h5', null, 'Request duration'), - Pe.createElement('pre', { className: 'microlight' }, s, ' ms') - ); - class LiveResponse extends Pe.Component { - shouldComponentUpdate(s) { - return ( - this.props.response !== s.response || - this.props.path !== s.path || - this.props.method !== s.method || - this.props.displayRequestDuration !== s.displayRequestDuration - ); - } - render() { - const { - response: s, - getComponent: o, - getConfigs: i, - displayRequestDuration: u, - specSelectors: _, - path: w, - method: x - } = this.props, - { showMutatedRequest: C, requestSnippetsEnabled: j } = i(), - L = C ? _.mutatedRequestFor(w, x) : _.requestFor(w, x), - B = s.get('status'), - $ = L.get('url'), - V = s.get('headers').toJS(), - U = s.get('notDocumented'), - z = s.get('error'), - Y = s.get('text'), - Z = s.get('duration'), - ee = Object.keys(V), - ie = V['content-type'] || V['Content-Type'], - ae = o('responseBody'), - le = ee.map((s) => { - var o = Array.isArray(V[s]) ? V[s].join() : V[s]; - return Pe.createElement( - 'span', - { className: 'headerline', key: s }, - ' ', - s, - ': ', - o, - ' ' - ); - }), - ce = 0 !== le.length, - pe = o('Markdown', !0), - de = o('RequestSnippets', !0), - fe = o('curl', !0); - return Pe.createElement( - 'div', - null, - L && j ? Pe.createElement(de, { request: L }) : Pe.createElement(fe, { request: L }), - $ && - Pe.createElement( - 'div', - null, - Pe.createElement( - 'div', - { className: 'request-url' }, - Pe.createElement('h4', null, 'Request URL'), - Pe.createElement('pre', { className: 'microlight' }, $) - ) - ), - Pe.createElement('h4', null, 'Server response'), - Pe.createElement( - 'table', - { className: 'responses-table live-responses-table' }, - Pe.createElement( - 'thead', - null, - Pe.createElement( - 'tr', - { className: 'responses-header' }, - Pe.createElement('td', { className: 'col_header response-col_status' }, 'Code'), - Pe.createElement( - 'td', - { className: 'col_header response-col_description' }, - 'Details' - ) - ) - ), - Pe.createElement( - 'tbody', - null, - Pe.createElement( - 'tr', - { className: 'response' }, - Pe.createElement( - 'td', - { className: 'response-col_status' }, - B, - U - ? Pe.createElement( - 'div', - { className: 'response-undocumented' }, - Pe.createElement('i', null, ' Undocumented ') - ) - : null - ), - Pe.createElement( - 'td', - { className: 'response-col_description' }, - z - ? Pe.createElement(pe, { - source: `${'' !== s.get('name') ? `${s.get('name')}: ` : ''}${s.get('message')}` - }) - : null, - Y - ? Pe.createElement(ae, { - content: Y, - contentType: ie, - url: $, - headers: V, - getConfigs: i, - getComponent: o - }) - : null, - ce ? Pe.createElement(live_response_Headers, { headers: le }) : null, - u && Z ? Pe.createElement(Duration, { duration: Z }) : null - ) - ) - ) - ) - ); - } - } - class OnlineValidatorBadge extends Pe.Component { - constructor(s, o) { - super(s, o); - let { getConfigs: i } = s, - { validatorUrl: u } = i(); - this.state = { - url: this.getDefinitionUrl(), - validatorUrl: void 0 === u ? 'https://validator.swagger.io/validator' : u - }; - } - getDefinitionUrl = () => { - let { specSelectors: s } = this.props; - return new (Mt())(s.url(), at.location).toString(); - }; - UNSAFE_componentWillReceiveProps(s) { - let { getConfigs: o } = s, - { validatorUrl: i } = o(); - this.setState({ - url: this.getDefinitionUrl(), - validatorUrl: void 0 === i ? 'https://validator.swagger.io/validator' : i - }); - } - render() { - let { getConfigs: s } = this.props, - { spec: o } = s(), - i = sanitizeUrl(this.state.validatorUrl); - return 'object' == typeof o && Object.keys(o).length - ? null - : this.state.url && - requiresValidationURL(this.state.validatorUrl) && - requiresValidationURL(this.state.url) - ? Pe.createElement( - 'span', - { className: 'float-right' }, - Pe.createElement( - 'a', - { - target: '_blank', - rel: 'noopener noreferrer', - href: `${i}/debug?url=${encodeURIComponent(this.state.url)}` - }, - Pe.createElement(ValidatorImage, { - src: `${i}?url=${encodeURIComponent(this.state.url)}`, - alt: 'Online validator badge' - }) - ) - ) - : null; - } - } - class ValidatorImage extends Pe.Component { - constructor(s) { - super(s), (this.state = { loaded: !1, error: !1 }); - } - componentDidMount() { - const s = new Image(); - (s.onload = () => { - this.setState({ loaded: !0 }); - }), - (s.onerror = () => { - this.setState({ error: !0 }); - }), - (s.src = this.props.src); - } - UNSAFE_componentWillReceiveProps(s) { - if (s.src !== this.props.src) { - const o = new Image(); - (o.onload = () => { - this.setState({ loaded: !0 }); - }), - (o.onerror = () => { - this.setState({ error: !0 }); - }), - (o.src = s.src); - } - } - render() { - return this.state.error - ? Pe.createElement('img', { alt: 'Error' }) - : this.state.loaded - ? Pe.createElement('img', { src: this.props.src, alt: this.props.alt }) - : null; - } - } - class Operations extends Pe.Component { - render() { - let { specSelectors: s } = this.props; - const o = s.taggedOperations(); - return 0 === o.size - ? Pe.createElement('h3', null, ' No operations defined in spec!') - : Pe.createElement( - 'div', - null, - o.map(this.renderOperationTag).toArray(), - o.size < 1 - ? Pe.createElement('h3', null, ' No operations defined in spec! ') - : null - ); - } - renderOperationTag = (s, o) => { - const { - specSelectors: i, - getComponent: u, - oas3Selectors: _, - layoutSelectors: w, - layoutActions: x, - getConfigs: C - } = this.props, - j = i.validOperationMethods(), - L = u('OperationContainer', !0), - B = u('OperationTag'), - $ = s.get('operations'); - return Pe.createElement( - B, - { - key: 'operation-' + o, - tagObj: s, - tag: o, - oas3Selectors: _, - layoutSelectors: w, - layoutActions: x, - getConfigs: C, - getComponent: u, - specUrl: i.url() - }, - Pe.createElement( - 'div', - { className: 'operation-tag-content' }, - $.map((s) => { - const i = s.get('path'), - u = s.get('method'), - _ = $e().List(['paths', i, u]); - return -1 === j.indexOf(u) - ? null - : Pe.createElement(L, { - key: `${i}-${u}`, - specPath: _, - op: s, - path: i, - method: u, - tag: o - }); - }).toArray() - ) - ); - }; - } - function isAbsoluteUrl(s) { - return s.match(/^(?:[a-z]+:)?\/\//i); - } - function buildBaseUrl(s, o) { - return s - ? isAbsoluteUrl(s) - ? (function addProtocol(s) { - return s.match(/^\/\//i) ? `${window.location.protocol}${s}` : s; - })(s) - : new URL(s, o).href - : o; - } - function safeBuildUrl(s, o, { selectedServer: i = '' } = {}) { - try { - return (function buildUrl(s, o, { selectedServer: i = '' } = {}) { - if (!s) return; - if (isAbsoluteUrl(s)) return s; - const u = buildBaseUrl(i, o); - return isAbsoluteUrl(u) ? new URL(s, u).href : new URL(s, window.location.href).href; - })(s, o, { selectedServer: i }); - } catch { - return; - } - } - class OperationTag extends Pe.Component { - static defaultProps = { tagObj: $e().fromJS({}), tag: '' }; - render() { - const { - tagObj: s, - tag: o, - children: i, - oas3Selectors: u, - layoutSelectors: _, - layoutActions: w, - getConfigs: x, - getComponent: C, - specUrl: j - } = this.props; - let { docExpansion: L, deepLinking: B } = x(); - const $ = C('Collapse'), - V = C('Markdown', !0), - U = C('DeepLink'), - z = C('Link'), - Y = C('ArrowUpIcon'), - Z = C('ArrowDownIcon'); - let ee, - ie = s.getIn(['tagDetails', 'description'], null), - ae = s.getIn(['tagDetails', 'externalDocs', 'description']), - le = s.getIn(['tagDetails', 'externalDocs', 'url']); - ee = - isFunc(u) && isFunc(u.selectedServer) - ? safeBuildUrl(le, j, { selectedServer: u.selectedServer() }) - : le; - let ce = ['operations-tag', o], - pe = _.isShown(ce, 'full' === L || 'list' === L); - return Pe.createElement( - 'div', - { className: pe ? 'opblock-tag-section is-open' : 'opblock-tag-section' }, - Pe.createElement( - 'h3', - { - onClick: () => w.show(ce, !pe), - className: ie ? 'opblock-tag' : 'opblock-tag no-desc', - id: ce.map((s) => escapeDeepLinkPath(s)).join('-'), - 'data-tag': o, - 'data-is-open': pe - }, - Pe.createElement(U, { - enabled: B, - isShown: pe, - path: createDeepLinkPath(o), - text: o - }), - ie - ? Pe.createElement('small', null, Pe.createElement(V, { source: ie })) - : Pe.createElement('small', null), - ee - ? Pe.createElement( - 'div', - { className: 'info__externaldocs' }, - Pe.createElement( - 'small', - null, - Pe.createElement( - z, - { - href: sanitizeUrl(ee), - onClick: (s) => s.stopPropagation(), - target: '_blank' - }, - ae || ee - ) - ) - ) - : null, - Pe.createElement( - 'button', - { - 'aria-expanded': pe, - className: 'expand-operation', - title: pe ? 'Collapse operation' : 'Expand operation', - onClick: () => w.show(ce, !pe) - }, - pe - ? Pe.createElement(Y, { className: 'arrow' }) - : Pe.createElement(Z, { className: 'arrow' }) - ) - ), - Pe.createElement($, { isOpened: pe }, i) - ); - } - } - class operation_Operation extends Pe.PureComponent { - static defaultProps = { - operation: null, - response: null, - request: null, - specPath: (0, qe.List)(), - summary: '' - }; - render() { - let { - specPath: s, - response: o, - request: i, - toggleShown: u, - onTryoutClick: _, - onResetClick: w, - onCancelClick: x, - onExecute: C, - fn: j, - getComponent: L, - getConfigs: B, - specActions: $, - specSelectors: V, - authActions: U, - authSelectors: z, - oas3Actions: Y, - oas3Selectors: Z - } = this.props, - ee = this.props.operation, - { - deprecated: ie, - isShown: ae, - path: le, - method: ce, - op: pe, - tag: de, - operationId: fe, - allowTryItOut: ye, - displayRequestDuration: be, - tryItOutEnabled: _e, - executeInProgress: we - } = ee.toJS(), - { description: Se, externalDocs: xe, schemes: Te } = pe; - const Re = xe - ? safeBuildUrl(xe.url, V.url(), { selectedServer: Z.selectedServer() }) - : ''; - let qe = ee.getIn(['op']), - ze = qe.get('responses'), - We = (function getList(s, o) { - if (!$e().Iterable.isIterable(s)) return $e().List(); - let i = s.getIn(Array.isArray(o) ? o : [o]); - return $e().List.isList(i) ? i : $e().List(); - })(qe, ['parameters']), - He = V.operationScheme(le, ce), - Ye = ['operations', de, fe], - Xe = getExtensions(qe); - const Qe = L('responses'), - et = L('parameters'), - tt = L('execute'), - rt = L('clear'), - nt = L('Collapse'), - st = L('Markdown', !0), - ot = L('schemes'), - it = L('OperationServers'), - at = L('OperationExt'), - lt = L('OperationSummary'), - ct = L('Link'), - { showExtensions: ut } = B(); - if (ze && o && o.size > 0) { - let s = !ze.get(String(o.get('status'))) && !ze.get('default'); - o = o.set('notDocumented', s); - } - let pt = [le, ce]; - const ht = V.validationErrors([le, ce]); - return Pe.createElement( - 'div', - { - className: ie - ? 'opblock opblock-deprecated' - : ae - ? `opblock opblock-${ce} is-open` - : `opblock opblock-${ce}`, - id: escapeDeepLinkPath(Ye.join('-')) - }, - Pe.createElement(lt, { - operationProps: ee, - isShown: ae, - toggleShown: u, - getComponent: L, - authActions: U, - authSelectors: z, - specPath: s - }), - Pe.createElement( - nt, - { isOpened: ae }, - Pe.createElement( - 'div', - { className: 'opblock-body' }, - (qe && qe.size) || null === qe - ? null - : Pe.createElement(rolling_load, { - height: '32px', - width: '32px', - className: 'opblock-loading-animation' - }), - ie && - Pe.createElement( - 'h4', - { className: 'opblock-title_normal' }, - ' Warning: Deprecated' - ), - Se && - Pe.createElement( - 'div', - { className: 'opblock-description-wrapper' }, - Pe.createElement( - 'div', - { className: 'opblock-description' }, - Pe.createElement(st, { source: Se }) - ) - ), - Re - ? Pe.createElement( - 'div', - { className: 'opblock-external-docs-wrapper' }, - Pe.createElement( - 'h4', - { className: 'opblock-title_normal' }, - 'Find more details' - ), - Pe.createElement( - 'div', - { className: 'opblock-external-docs' }, - xe.description && - Pe.createElement( - 'span', - { className: 'opblock-external-docs__description' }, - Pe.createElement(st, { source: xe.description }) - ), - Pe.createElement( - ct, - { - target: '_blank', - className: 'opblock-external-docs__link', - href: sanitizeUrl(Re) - }, - Re - ) - ) - ) - : null, - qe && qe.size - ? Pe.createElement(et, { - parameters: We, - specPath: s.push('parameters'), - operation: qe, - onChangeKey: pt, - onTryoutClick: _, - onResetClick: w, - onCancelClick: x, - tryItOutEnabled: _e, - allowTryItOut: ye, - fn: j, - getComponent: L, - specActions: $, - specSelectors: V, - pathMethod: [le, ce], - getConfigs: B, - oas3Actions: Y, - oas3Selectors: Z - }) - : null, - _e - ? Pe.createElement(it, { - getComponent: L, - path: le, - method: ce, - operationServers: qe.get('servers'), - pathServers: V.paths().getIn([le, 'servers']), - getSelectedServer: Z.selectedServer, - setSelectedServer: Y.setSelectedServer, - setServerVariableValue: Y.setServerVariableValue, - getServerVariable: Z.serverVariableValue, - getEffectiveServerValue: Z.serverEffectiveValue - }) - : null, - _e && ye && Te && Te.size - ? Pe.createElement( - 'div', - { className: 'opblock-schemes' }, - Pe.createElement(ot, { - schemes: Te, - path: le, - method: ce, - specActions: $, - currentScheme: He - }) - ) - : null, - !_e || !ye || ht.length <= 0 - ? null - : Pe.createElement( - 'div', - { className: 'validation-errors errors-wrapper' }, - 'Please correct the following validation errors and try again.', - Pe.createElement( - 'ul', - null, - ht.map((s, o) => Pe.createElement('li', { key: o }, ' ', s, ' ')) - ) - ), - Pe.createElement( - 'div', - { className: _e && o && ye ? 'btn-group' : 'execute-wrapper' }, - _e && ye - ? Pe.createElement(tt, { - operation: qe, - specActions: $, - specSelectors: V, - oas3Selectors: Z, - oas3Actions: Y, - path: le, - method: ce, - onExecute: C, - disabled: we - }) - : null, - _e && o && ye - ? Pe.createElement(rt, { specActions: $, path: le, method: ce }) - : null - ), - we - ? Pe.createElement( - 'div', - { className: 'loading-container' }, - Pe.createElement('div', { className: 'loading' }) - ) - : null, - ze - ? Pe.createElement(Qe, { - responses: ze, - request: i, - tryItOutResponse: o, - getComponent: L, - getConfigs: B, - specSelectors: V, - oas3Actions: Y, - oas3Selectors: Z, - specActions: $, - produces: V.producesOptionsFor([le, ce]), - producesValue: V.currentProducesFor([le, ce]), - specPath: s.push('responses'), - path: le, - method: ce, - displayRequestDuration: be, - fn: j - }) - : null, - ut && Xe.size ? Pe.createElement(at, { extensions: Xe, getComponent: L }) : null - ) - ) - ); - } - } - class OperationContainer extends Pe.PureComponent { - constructor(s, o) { - super(s, o); - const { tryItOutEnabled: i } = s.getConfigs(); - this.state = { tryItOutEnabled: i, executeInProgress: !1 }; - } - static defaultProps = { - showSummary: !0, - response: null, - allowTryItOut: !0, - displayOperationId: !1, - displayRequestDuration: !1 - }; - mapStateToProps(s, o) { - const { op: i, layoutSelectors: u, getConfigs: _ } = o, - { - docExpansion: w, - deepLinking: x, - displayOperationId: C, - displayRequestDuration: j, - supportedSubmitMethods: L - } = _(), - B = u.showSummary(), - $ = - i.getIn(['operation', '__originalOperationId']) || - i.getIn(['operation', 'operationId']) || - opId(i.get('operation'), o.path, o.method) || - i.get('id'), - V = ['operations', o.tag, $], - U = - L.indexOf(o.method) >= 0 && - (void 0 === o.allowTryItOut - ? o.specSelectors.allowTryItOutFor(o.path, o.method) - : o.allowTryItOut), - z = i.getIn(['operation', 'security']) || o.specSelectors.security(); - return { - operationId: $, - isDeepLinkingEnabled: x, - showSummary: B, - displayOperationId: C, - displayRequestDuration: j, - allowTryItOut: U, - security: z, - isAuthorized: o.authSelectors.isAuthorized(z), - isShown: u.isShown(V, 'full' === w), - jumpToKey: `paths.${o.path}.${o.method}`, - response: o.specSelectors.responseFor(o.path, o.method), - request: o.specSelectors.requestFor(o.path, o.method) - }; - } - componentDidMount() { - const { isShown: s } = this.props, - o = this.getResolvedSubtree(); - s && void 0 === o && this.requestResolvedSubtree(); - } - UNSAFE_componentWillReceiveProps(s) { - const { response: o, isShown: i } = s, - u = this.getResolvedSubtree(); - o !== this.props.response && this.setState({ executeInProgress: !1 }), - i && void 0 === u && this.requestResolvedSubtree(); - } - toggleShown = () => { - let { layoutActions: s, tag: o, operationId: i, isShown: u } = this.props; - const _ = this.getResolvedSubtree(); - u || void 0 !== _ || this.requestResolvedSubtree(), s.show(['operations', o, i], !u); - }; - onCancelClick = () => { - this.setState({ tryItOutEnabled: !this.state.tryItOutEnabled }); - }; - onTryoutClick = () => { - this.setState({ tryItOutEnabled: !this.state.tryItOutEnabled }); - }; - onResetClick = (s) => { - const o = this.props.oas3Selectors.selectDefaultRequestBodyValue(...s); - this.props.oas3Actions.setRequestBodyValue({ value: o, pathMethod: s }); - }; - onExecute = () => { - this.setState({ executeInProgress: !0 }); - }; - getResolvedSubtree = () => { - const { specSelectors: s, path: o, method: i, specPath: u } = this.props; - return u ? s.specResolvedSubtree(u.toJS()) : s.specResolvedSubtree(['paths', o, i]); - }; - requestResolvedSubtree = () => { - const { specActions: s, path: o, method: i, specPath: u } = this.props; - return u - ? s.requestResolvedSubtree(u.toJS()) - : s.requestResolvedSubtree(['paths', o, i]); - }; - render() { - let { - op: s, - tag: o, - path: i, - method: u, - security: _, - isAuthorized: w, - operationId: x, - showSummary: C, - isShown: j, - jumpToKey: L, - allowTryItOut: B, - response: $, - request: V, - displayOperationId: U, - displayRequestDuration: z, - isDeepLinkingEnabled: Y, - specPath: Z, - specSelectors: ee, - specActions: ie, - getComponent: ae, - getConfigs: le, - layoutSelectors: ce, - layoutActions: pe, - authActions: de, - authSelectors: fe, - oas3Actions: ye, - oas3Selectors: be, - fn: _e - } = this.props; - const we = ae('operation'), - Se = this.getResolvedSubtree() || (0, qe.Map)(), - xe = (0, qe.fromJS)({ - op: Se, - tag: o, - path: i, - summary: s.getIn(['operation', 'summary']) || '', - deprecated: Se.get('deprecated') || s.getIn(['operation', 'deprecated']) || !1, - method: u, - security: _, - isAuthorized: w, - operationId: x, - originalOperationId: Se.getIn(['operation', '__originalOperationId']), - showSummary: C, - isShown: j, - jumpToKey: L, - allowTryItOut: B, - request: V, - displayOperationId: U, - displayRequestDuration: z, - isDeepLinkingEnabled: Y, - executeInProgress: this.state.executeInProgress, - tryItOutEnabled: this.state.tryItOutEnabled - }); - return Pe.createElement(we, { - operation: xe, - response: $, - request: V, - isShown: j, - toggleShown: this.toggleShown, - onTryoutClick: this.onTryoutClick, - onResetClick: this.onResetClick, - onCancelClick: this.onCancelClick, - onExecute: this.onExecute, - specPath: Z, - specActions: ie, - specSelectors: ee, - oas3Actions: ye, - oas3Selectors: be, - layoutActions: pe, - layoutSelectors: ce, - authActions: de, - authSelectors: fe, - getComponent: ae, - getConfigs: le, - fn: _e - }); - } - } - var mk = __webpack_require__(13222), - yk = __webpack_require__.n(mk); - class OperationSummary extends Pe.PureComponent { - static defaultProps = { operationProps: null, specPath: (0, qe.List)(), summary: '' }; - render() { - let { - isShown: s, - toggleShown: o, - getComponent: i, - authActions: u, - authSelectors: _, - operationProps: w, - specPath: x - } = this.props, - { - summary: C, - isAuthorized: j, - method: L, - op: B, - showSummary: $, - path: V, - operationId: U, - originalOperationId: z, - displayOperationId: Y - } = w.toJS(), - { summary: Z } = B, - ee = w.get('security'); - const ie = i('authorizeOperationBtn', !0), - ae = i('OperationSummaryMethod'), - le = i('OperationSummaryPath'), - ce = i('JumpToPath', !0), - pe = i('CopyToClipboardBtn', !0), - de = i('ArrowUpIcon'), - fe = i('ArrowDownIcon'), - ye = ee && !!ee.count(), - be = ye && 1 === ee.size && ee.first().isEmpty(), - _e = !ye || be; - return Pe.createElement( - 'div', - { className: `opblock-summary opblock-summary-${L}` }, - Pe.createElement( - 'button', - { 'aria-expanded': s, className: 'opblock-summary-control', onClick: o }, - Pe.createElement(ae, { method: L }), - Pe.createElement( - 'div', - { className: 'opblock-summary-path-description-wrapper' }, - Pe.createElement(le, { getComponent: i, operationProps: w, specPath: x }), - $ - ? Pe.createElement( - 'div', - { className: 'opblock-summary-description' }, - yk()(Z || C) - ) - : null - ), - Y && (z || U) - ? Pe.createElement('span', { className: 'opblock-summary-operation-id' }, z || U) - : null - ), - Pe.createElement(pe, { textToCopy: `${x.get(1)}` }), - _e - ? null - : Pe.createElement(ie, { - isAuthorized: j, - onClick: () => { - const s = _.definitionsForRequirements(ee); - u.showDefinitions(s); - } - }), - Pe.createElement(ce, { path: x }), - Pe.createElement( - 'button', - { - 'aria-label': `${L} ${V.replace(/\//g, '​/')}`, - className: 'opblock-control-arrow', - 'aria-expanded': s, - tabIndex: '-1', - onClick: o - }, - s - ? Pe.createElement(de, { className: 'arrow' }) - : Pe.createElement(fe, { className: 'arrow' }) - ) - ); - } - } - class OperationSummaryMethod extends Pe.PureComponent { - static defaultProps = { operationProps: null }; - render() { - let { method: s } = this.props; - return Pe.createElement( - 'span', - { className: 'opblock-summary-method' }, - s.toUpperCase() - ); - } - } - class OperationSummaryPath extends Pe.PureComponent { - render() { - let { getComponent: s, operationProps: o } = this.props, - { - deprecated: i, - isShown: u, - path: _, - tag: w, - operationId: x, - isDeepLinkingEnabled: C - } = o.toJS(); - const j = _.split(/(?=\/)/g); - for (let s = 1; s < j.length; s += 2) - j.splice(s, 0, Pe.createElement('wbr', { key: s })); - const L = s('DeepLink'); - return Pe.createElement( - 'span', - { - className: i ? 'opblock-summary-path__deprecated' : 'opblock-summary-path', - 'data-path': _ - }, - Pe.createElement(L, { - enabled: C, - isShown: u, - path: createDeepLinkPath(`${w}/${x}`), - text: j - }) - ); - } - } - const operation_extensions = ({ extensions: s, getComponent: o }) => { - let i = o('OperationExtRow'); - return Pe.createElement( - 'div', - { className: 'opblock-section' }, - Pe.createElement( - 'div', - { className: 'opblock-section-header' }, - Pe.createElement('h4', null, 'Extensions') - ), - Pe.createElement( - 'div', - { className: 'table-container' }, - Pe.createElement( - 'table', - null, - Pe.createElement( - 'thead', - null, - Pe.createElement( - 'tr', - null, - Pe.createElement('td', { className: 'col_header' }, 'Field'), - Pe.createElement('td', { className: 'col_header' }, 'Value') - ) - ), - Pe.createElement( - 'tbody', - null, - s - .entrySeq() - .map(([s, o]) => Pe.createElement(i, { key: `${s}-${o}`, xKey: s, xVal: o })) - ) - ) - ) - ); - }, - operation_extension_row = ({ xKey: s, xVal: o }) => { - const i = o ? (o.toJS ? o.toJS() : o) : null; - return Pe.createElement( - 'tr', - null, - Pe.createElement('td', null, s), - Pe.createElement('td', null, JSON.stringify(i)) - ); - }; - function createHtmlReadyId(s, o = '_') { - return s.replace(/[^\w-]/g, o); - } - class responses_Responses extends Pe.Component { - static defaultProps = { - tryItOutResponse: null, - produces: (0, qe.fromJS)(['application/json']), - displayRequestDuration: !1 - }; - onChangeProducesWrapper = (s) => - this.props.specActions.changeProducesValue([this.props.path, this.props.method], s); - onResponseContentTypeChange = ({ controlsAcceptHeader: s, value: o }) => { - const { oas3Actions: i, path: u, method: _ } = this.props; - s && i.setResponseContentType({ value: o, path: u, method: _ }); - }; - render() { - let { - responses: s, - tryItOutResponse: o, - getComponent: i, - getConfigs: u, - specSelectors: _, - fn: w, - producesValue: x, - displayRequestDuration: C, - specPath: j, - path: L, - method: B, - oas3Selectors: $, - oas3Actions: V - } = this.props, - U = (function defaultStatusCode(s) { - let o = s.keySeq(); - return o.contains(At) - ? At - : o - .filter((s) => '2' === (s + '')[0]) - .sort() - .first(); - })(s); - const z = i('contentType'), - Y = i('liveResponse'), - Z = i('response'); - let ee = - this.props.produces && this.props.produces.size - ? this.props.produces - : responses_Responses.defaultProps.produces; - const ie = _.isOAS3() - ? (function getAcceptControllingResponse(s) { - if (!$e().OrderedMap.isOrderedMap(s)) return null; - if (!s.size) return null; - const o = s.find( - (s, o) => - o.startsWith('2') && Object.keys(s.get('content') || {}).length > 0 - ), - i = s.get('default') || $e().OrderedMap(), - u = (i.get('content') || $e().OrderedMap()).keySeq().toJS().length ? i : null; - return o || u; - })(s) - : null, - ae = createHtmlReadyId(`${B}${L}_responses`), - le = `${ae}_select`; - return Pe.createElement( - 'div', - { className: 'responses-wrapper' }, - Pe.createElement( - 'div', - { className: 'opblock-section-header' }, - Pe.createElement('h4', null, 'Responses'), - _.isOAS3() - ? null - : Pe.createElement( - 'label', - { htmlFor: le }, - Pe.createElement('span', null, 'Response content type'), - Pe.createElement(z, { - value: x, - ariaControls: ae, - ariaLabel: 'Response content type', - className: 'execute-content-type', - contentTypes: ee, - controlId: le, - onChange: this.onChangeProducesWrapper - }) - ) - ), - Pe.createElement( - 'div', - { className: 'responses-inner' }, - o - ? Pe.createElement( - 'div', - null, - Pe.createElement(Y, { - response: o, - getComponent: i, - getConfigs: u, - specSelectors: _, - path: this.props.path, - method: this.props.method, - displayRequestDuration: C - }), - Pe.createElement('h4', null, 'Responses') - ) - : null, - Pe.createElement( - 'table', - { 'aria-live': 'polite', className: 'responses-table', id: ae, role: 'region' }, - Pe.createElement( - 'thead', - null, - Pe.createElement( - 'tr', - { className: 'responses-header' }, - Pe.createElement( - 'td', - { className: 'col_header response-col_status' }, - 'Code' - ), - Pe.createElement( - 'td', - { className: 'col_header response-col_description' }, - 'Description' - ), - _.isOAS3() - ? Pe.createElement( - 'td', - { className: 'col col_header response-col_links' }, - 'Links' - ) - : null - ) - ), - Pe.createElement( - 'tbody', - null, - s - .entrySeq() - .map(([s, C]) => { - let z = o && o.get('status') == s ? 'response_current' : ''; - return Pe.createElement(Z, { - key: s, - path: L, - method: B, - specPath: j.push(s), - isDefault: U === s, - fn: w, - className: z, - code: s, - response: C, - specSelectors: _, - controlsAcceptHeader: C === ie, - onContentTypeChange: this.onResponseContentTypeChange, - contentType: x, - getConfigs: u, - activeExamplesKey: $.activeExamplesMember(L, B, 'responses', s), - oas3Actions: V, - getComponent: i - }); - }) - .toArray() - ) - ) - ) - ); - } - } - function getKnownSyntaxHighlighterLanguage(s) { - const o = (function canJsonParse(s) { - try { - return !!JSON.parse(s); - } catch (s) { - return null; - } - })(s); - return o ? 'json' : null; - } - class response_Response extends Pe.Component { - constructor(s, o) { - super(s, o), (this.state = { responseContentType: '' }); - } - static defaultProps = { response: (0, qe.fromJS)({}), onContentTypeChange: () => {} }; - _onContentTypeChange = (s) => { - const { onContentTypeChange: o, controlsAcceptHeader: i } = this.props; - this.setState({ responseContentType: s }), o({ value: s, controlsAcceptHeader: i }); - }; - getTargetExamplesKey = () => { - const { response: s, contentType: o, activeExamplesKey: i } = this.props, - u = this.state.responseContentType || o, - _ = s - .getIn(['content', u], (0, qe.Map)({})) - .get('examples', null) - .keySeq() - .first(); - return i || _; - }; - render() { - let { - path: s, - method: o, - code: i, - response: u, - className: _, - specPath: w, - fn: x, - getComponent: C, - getConfigs: j, - specSelectors: L, - contentType: B, - controlsAcceptHeader: $, - oas3Actions: V - } = this.props, - { inferSchema: U, getSampleSchema: z } = x, - Y = L.isOAS3(); - const { showExtensions: Z } = j(); - let ee = Z ? getExtensions(u) : null, - ie = u.get('headers'), - ae = u.get('links'); - const le = C('ResponseExtension'), - ce = C('headers'), - pe = C('HighlightCode', !0), - de = C('modelExample'), - fe = C('Markdown', !0), - ye = C('operationLink'), - be = C('contentType'), - _e = C('ExamplesSelect'), - we = C('Example'); - var Se, xe; - const Te = this.state.responseContentType || B, - Re = u.getIn(['content', Te], (0, qe.Map)({})), - $e = Re.get('examples', null); - if (Y) { - const s = Re.get('schema'); - (Se = s ? U(s.toJS()) : null), - (xe = s ? (0, qe.List)(['content', this.state.responseContentType, 'schema']) : w); - } else (Se = u.get('schema')), (xe = u.has('schema') ? w.push('schema') : w); - let ze, - We, - He = !1, - Ye = { includeReadOnly: !0 }; - if (Y) - if (((We = Re.get('schema')?.toJS()), qe.Map.isMap($e) && !$e.isEmpty())) { - const s = this.getTargetExamplesKey(), - getMediaTypeExample = (s) => s.get('value'); - (ze = getMediaTypeExample($e.get(s, (0, qe.Map)({})))), - void 0 === ze && (ze = getMediaTypeExample($e.values().next().value)), - (He = !0); - } else void 0 !== Re.get('example') && ((ze = Re.get('example')), (He = !0)); - else { - (We = Se), (Ye = { ...Ye, includeWriteOnly: !0 }); - const s = u.getIn(['examples', Te]); - s && ((ze = s), (He = !0)); - } - const Xe = ((s, o) => { - if (null == s) return null; - const i = getKnownSyntaxHighlighterLanguage(s) ? 'json' : null; - return Pe.createElement( - 'div', - null, - Pe.createElement(o, { className: 'example', language: i }, stringify(s)) - ); - })(z(We, Te, Ye, He ? ze : void 0), pe); - return Pe.createElement( - 'tr', - { className: 'response ' + (_ || ''), 'data-code': i }, - Pe.createElement('td', { className: 'response-col_status' }, i), - Pe.createElement( - 'td', - { className: 'response-col_description' }, - Pe.createElement( - 'div', - { className: 'response-col_description__inner' }, - Pe.createElement(fe, { source: u.get('description') }) - ), - Z && ee.size - ? ee - .entrySeq() - .map(([s, o]) => Pe.createElement(le, { key: `${s}-${o}`, xKey: s, xVal: o })) - : null, - Y && u.get('content') - ? Pe.createElement( - 'section', - { className: 'response-controls' }, - Pe.createElement( - 'div', - { - className: Hn()('response-control-media-type', { - 'response-control-media-type--accept-controller': $ - }) - }, - Pe.createElement( - 'small', - { className: 'response-control-media-type__title' }, - 'Media type' - ), - Pe.createElement(be, { - value: this.state.responseContentType, - contentTypes: u.get('content') - ? u.get('content').keySeq() - : (0, qe.Seq)(), - onChange: this._onContentTypeChange, - ariaLabel: 'Media Type' - }), - $ - ? Pe.createElement( - 'small', - { className: 'response-control-media-type__accept-message' }, - 'Controls ', - Pe.createElement('code', null, 'Accept'), - ' header.' - ) - : null - ), - qe.Map.isMap($e) && !$e.isEmpty() - ? Pe.createElement( - 'div', - { className: 'response-control-examples' }, - Pe.createElement( - 'small', - { className: 'response-control-examples__title' }, - 'Examples' - ), - Pe.createElement(_e, { - examples: $e, - currentExampleKey: this.getTargetExamplesKey(), - onSelect: (u) => - V.setActiveExamplesMember({ - name: u, - pathMethod: [s, o], - contextType: 'responses', - contextName: i - }), - showLabels: !1 - }) - ) - : null - ) - : null, - Xe || Se - ? Pe.createElement(de, { - specPath: xe, - getComponent: C, - getConfigs: j, - specSelectors: L, - schema: fromJSOrdered(Se), - example: Xe, - includeReadOnly: !0 - }) - : null, - Y && $e - ? Pe.createElement(we, { - example: $e.get(this.getTargetExamplesKey(), (0, qe.Map)({})), - getComponent: C, - getConfigs: j, - omitValue: !0 - }) - : null, - ie ? Pe.createElement(ce, { headers: ie, getComponent: C }) : null - ), - Y - ? Pe.createElement( - 'td', - { className: 'response-col_links' }, - ae - ? ae - .toSeq() - .entrySeq() - .map(([s, o]) => - Pe.createElement(ye, { key: s, name: s, link: o, getComponent: C }) - ) - : Pe.createElement('i', null, 'No links') - ) - : null - ); - } - } - const response_extension = ({ xKey: s, xVal: o }) => - Pe.createElement('div', { className: 'response__extension' }, s, ': ', String(o)); - var vk = __webpack_require__(26657), - _k = __webpack_require__.n(vk), - wk = __webpack_require__(80218), - xk = __webpack_require__.n(wk); - class ResponseBody extends Pe.PureComponent { - state = { parsedContent: null }; - updateParsedContent = (s) => { - const { content: o } = this.props; - if (s !== o) - if (o && o instanceof Blob) { - var i = new FileReader(); - (i.onload = () => { - this.setState({ parsedContent: i.result }); - }), - i.readAsText(o); - } else this.setState({ parsedContent: o.toString() }); - }; - componentDidMount() { - this.updateParsedContent(null); - } - componentDidUpdate(s) { - this.updateParsedContent(s.content); - } - render() { - let { - content: s, - contentType: o, - url: i, - headers: u = {}, - getComponent: _ - } = this.props; - const { parsedContent: w } = this.state, - x = _('HighlightCode', !0), - C = 'response_' + new Date().getTime(); - let j, L; - if ( - ((i = i || ''), - (/^application\/octet-stream/i.test(o) || - (u['Content-Disposition'] && /attachment/i.test(u['Content-Disposition'])) || - (u['content-disposition'] && /attachment/i.test(u['content-disposition'])) || - (u['Content-Description'] && /File Transfer/i.test(u['Content-Description'])) || - (u['content-description'] && /File Transfer/i.test(u['content-description']))) && - (s.size > 0 || s.length > 0)) - ) - if ('Blob' in window) { - let _ = o || 'text/html', - w = s instanceof Blob ? s : new Blob([s], { type: _ }), - x = window.URL.createObjectURL(w), - C = [_, i.substr(i.lastIndexOf('/') + 1), x].join(':'), - j = u['content-disposition'] || u['Content-Disposition']; - if (void 0 !== j) { - let s = (function extractFileNameFromContentDispositionHeader(s) { - let o; - if ( - ([ - /filename\*=[^']+'\w*'"([^"]+)";?/i, - /filename\*=[^']+'\w*'([^;]+);?/i, - /filename="([^;]*);?"/i, - /filename=([^;]*);?/i - ].some((i) => ((o = i.exec(s)), null !== o)), - null !== o && o.length > 1) - ) - try { - return decodeURIComponent(o[1]); - } catch (s) { - console.error(s); - } - return null; - })(j); - null !== s && (C = s); - } - L = - at.navigator && at.navigator.msSaveOrOpenBlob - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'a', - { href: x, onClick: () => at.navigator.msSaveOrOpenBlob(w, C) }, - 'Download file' - ) - ) - : Pe.createElement( - 'div', - null, - Pe.createElement('a', { href: x, download: C }, 'Download file') - ); - } else - L = Pe.createElement( - 'pre', - { className: 'microlight' }, - 'Download headers detected but your browser does not support downloading binary via XHR (Blob).' - ); - else if (/json/i.test(o)) { - let o = null; - getKnownSyntaxHighlighterLanguage(s) && (o = 'json'); - try { - j = JSON.stringify(JSON.parse(s), null, ' '); - } catch (o) { - j = "can't parse JSON. Raw result:\n\n" + s; - } - L = Pe.createElement( - x, - { language: o, downloadable: !0, fileName: `${C}.json`, canCopy: !0 }, - j - ); - } else - /xml/i.test(o) - ? ((j = _k()(s, { textNodesOnSameLine: !0, indentor: ' ' })), - (L = Pe.createElement( - x, - { downloadable: !0, fileName: `${C}.xml`, canCopy: !0 }, - j - ))) - : (L = - 'text/html' === xk()(o) || /text\/plain/.test(o) - ? Pe.createElement( - x, - { downloadable: !0, fileName: `${C}.html`, canCopy: !0 }, - s - ) - : 'text/csv' === xk()(o) || /text\/csv/.test(o) - ? Pe.createElement( - x, - { downloadable: !0, fileName: `${C}.csv`, canCopy: !0 }, - s - ) - : /^image\//i.test(o) - ? o.includes('svg') - ? Pe.createElement('div', null, ' ', s, ' ') - : Pe.createElement('img', { src: window.URL.createObjectURL(s) }) - : /^audio\//i.test(o) - ? Pe.createElement( - 'pre', - { className: 'microlight' }, - Pe.createElement( - 'audio', - { controls: !0, key: i }, - Pe.createElement('source', { src: i, type: o }) - ) - ) - : 'string' == typeof s - ? Pe.createElement( - x, - { downloadable: !0, fileName: `${C}.txt`, canCopy: !0 }, - s - ) - : s.size > 0 - ? w - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'p', - { className: 'i' }, - 'Unrecognized response type; displaying content as text.' - ), - Pe.createElement( - x, - { downloadable: !0, fileName: `${C}.txt`, canCopy: !0 }, - w - ) - ) - : Pe.createElement( - 'p', - { className: 'i' }, - 'Unrecognized response type; unable to display.' - ) - : null); - return L - ? Pe.createElement('div', null, Pe.createElement('h5', null, 'Response body'), L) - : null; - } - } - class Parameters extends Pe.Component { - constructor(s) { - super(s), (this.state = { callbackVisible: !1, parametersVisible: !0 }); - } - static defaultProps = { - onTryoutClick: Function.prototype, - onCancelClick: Function.prototype, - tryItOutEnabled: !1, - allowTryItOut: !0, - onChangeKey: [], - specPath: [] - }; - onChange = (s, o, i) => { - let { - specActions: { changeParamByIdentity: u }, - onChangeKey: _ - } = this.props; - u(_, s, o, i); - }; - onChangeConsumesWrapper = (s) => { - let { - specActions: { changeConsumesValue: o }, - onChangeKey: i - } = this.props; - o(i, s); - }; - toggleTab = (s) => - 'parameters' === s - ? this.setState({ parametersVisible: !0, callbackVisible: !1 }) - : 'callbacks' === s - ? this.setState({ callbackVisible: !0, parametersVisible: !1 }) - : void 0; - onChangeMediaType = ({ value: s, pathMethod: o }) => { - let { specActions: i, oas3Selectors: u, oas3Actions: _ } = this.props; - const w = u.hasUserEditedBody(...o), - x = u.shouldRetainRequestBodyValue(...o); - _.setRequestContentType({ value: s, pathMethod: o }), - _.initRequestBodyValidateError({ pathMethod: o }), - w || - (x || _.setRequestBodyValue({ value: void 0, pathMethod: o }), - i.clearResponse(...o), - i.clearRequest(...o), - i.clearValidateParams(o)); - }; - render() { - let { - onTryoutClick: s, - onResetClick: o, - parameters: i, - allowTryItOut: u, - tryItOutEnabled: _, - specPath: w, - fn: x, - getComponent: C, - getConfigs: j, - specSelectors: L, - specActions: B, - pathMethod: $, - oas3Actions: V, - oas3Selectors: U, - operation: z - } = this.props; - const Y = C('parameterRow'), - Z = C('TryItOutButton'), - ee = C('contentType'), - ie = C('Callbacks', !0), - ae = C('RequestBody', !0), - le = _ && u, - ce = L.isOAS3(), - pe = `${createHtmlReadyId(`${$[1]}${$[0]}_requests`)}_select`, - de = z.get('requestBody'), - fe = Object.values( - i.reduce((s, o) => { - const i = o.get('in'); - return (s[i] ??= []), s[i].push(o), s; - }, {}) - ).reduce((s, o) => s.concat(o), []); - return Pe.createElement( - 'div', - { className: 'opblock-section' }, - Pe.createElement( - 'div', - { className: 'opblock-section-header' }, - ce - ? Pe.createElement( - 'div', - { className: 'tab-header' }, - Pe.createElement( - 'div', - { - onClick: () => this.toggleTab('parameters'), - className: `tab-item ${this.state.parametersVisible && 'active'}` - }, - Pe.createElement( - 'h4', - { className: 'opblock-title' }, - Pe.createElement('span', null, 'Parameters') - ) - ), - z.get('callbacks') - ? Pe.createElement( - 'div', - { - onClick: () => this.toggleTab('callbacks'), - className: `tab-item ${this.state.callbackVisible && 'active'}` - }, - Pe.createElement( - 'h4', - { className: 'opblock-title' }, - Pe.createElement('span', null, 'Callbacks') - ) - ) - : null - ) - : Pe.createElement( - 'div', - { className: 'tab-header' }, - Pe.createElement('h4', { className: 'opblock-title' }, 'Parameters') - ), - u - ? Pe.createElement(Z, { - isOAS3: L.isOAS3(), - hasUserEditedBody: U.hasUserEditedBody(...$), - enabled: _, - onCancelClick: this.props.onCancelClick, - onTryoutClick: s, - onResetClick: () => o($) - }) - : null - ), - this.state.parametersVisible - ? Pe.createElement( - 'div', - { className: 'parameters-container' }, - fe.length - ? Pe.createElement( - 'div', - { className: 'table-container' }, - Pe.createElement( - 'table', - { className: 'parameters' }, - Pe.createElement( - 'thead', - null, - Pe.createElement( - 'tr', - null, - Pe.createElement( - 'th', - { className: 'col_header parameters-col_name' }, - 'Name' - ), - Pe.createElement( - 'th', - { className: 'col_header parameters-col_description' }, - 'Description' - ) - ) - ), - Pe.createElement( - 'tbody', - null, - fe.map((s, o) => - Pe.createElement(Y, { - fn: x, - specPath: w.push(o.toString()), - getComponent: C, - getConfigs: j, - rawParam: s, - param: L.parameterWithMetaByIdentity($, s), - key: `${s.get('in')}.${s.get('name')}`, - onChange: this.onChange, - onChangeConsumes: this.onChangeConsumesWrapper, - specSelectors: L, - specActions: B, - oas3Actions: V, - oas3Selectors: U, - pathMethod: $, - isExecute: le - }) - ) - ) - ) - ) - : Pe.createElement( - 'div', - { className: 'opblock-description-wrapper' }, - Pe.createElement('p', null, 'No parameters') - ) - ) - : null, - this.state.callbackVisible - ? Pe.createElement( - 'div', - { className: 'callbacks-container opblock-description-wrapper' }, - Pe.createElement(ie, { - callbacks: (0, qe.Map)(z.get('callbacks')), - specPath: w.slice(0, -1).push('callbacks') - }) - ) - : null, - ce && - de && - this.state.parametersVisible && - Pe.createElement( - 'div', - { className: 'opblock-section opblock-section-request-body' }, - Pe.createElement( - 'div', - { className: 'opblock-section-header' }, - Pe.createElement( - 'h4', - { - className: `opblock-title parameter__name ${de.get('required') && 'required'}` - }, - 'Request body' - ), - Pe.createElement( - 'label', - { id: pe }, - Pe.createElement(ee, { - value: U.requestContentType(...$), - contentTypes: de.get('content', (0, qe.List)()).keySeq(), - onChange: (s) => { - this.onChangeMediaType({ value: s, pathMethod: $ }); - }, - className: 'body-param-content-type', - ariaLabel: 'Request content type', - controlId: pe - }) - ) - ), - Pe.createElement( - 'div', - { className: 'opblock-description-wrapper' }, - Pe.createElement(ae, { - setRetainRequestBodyValueFlag: (s) => - V.setRetainRequestBodyValueFlag({ value: s, pathMethod: $ }), - userHasEditedBody: U.hasUserEditedBody(...$), - specPath: w.slice(0, -1).push('requestBody'), - requestBody: de, - requestBodyValue: U.requestBodyValue(...$), - requestBodyInclusionSetting: U.requestBodyInclusionSetting(...$), - requestBodyErrors: U.requestBodyErrors(...$), - isExecute: le, - getConfigs: j, - activeExamplesKey: U.activeExamplesMember(...$, 'requestBody', 'requestBody'), - updateActiveExamplesKey: (s) => { - this.props.oas3Actions.setActiveExamplesMember({ - name: s, - pathMethod: this.props.pathMethod, - contextType: 'requestBody', - contextName: 'requestBody' - }); - }, - onChange: (s, o) => { - if (o) { - const i = U.requestBodyValue(...$), - u = qe.Map.isMap(i) ? i : (0, qe.Map)(); - return V.setRequestBodyValue({ pathMethod: $, value: u.setIn(o, s) }); - } - V.setRequestBodyValue({ value: s, pathMethod: $ }); - }, - onChangeIncludeEmpty: (s, o) => { - V.setRequestBodyInclusion({ pathMethod: $, value: o, name: s }); - }, - contentType: U.requestContentType(...$) - }) - ) - ) - ); - } - } - const parameter_extension = ({ xKey: s, xVal: o }) => - Pe.createElement('div', { className: 'parameter__extension' }, s, ': ', String(o)), - Ak = { onChange: () => {}, isIncludedOptions: {} }; - class ParameterIncludeEmpty extends Pe.Component { - static defaultProps = Ak; - componentDidMount() { - const { isIncludedOptions: s, onChange: o } = this.props, - { shouldDispatchInit: i, defaultValue: u } = s; - i && o(u); - } - onCheckboxChange = (s) => { - const { onChange: o } = this.props; - o(s.target.checked); - }; - render() { - let { isIncluded: s, isDisabled: o } = this.props; - return Pe.createElement( - 'div', - null, - Pe.createElement( - 'label', - { - htmlFor: 'include_empty_value', - className: Hn()('parameter__empty_value_toggle', { disabled: o }) - }, - Pe.createElement('input', { - id: 'include_empty_value', - type: 'checkbox', - disabled: o, - checked: !o && s, - onChange: this.onCheckboxChange - }), - 'Send empty value' - ) - ); - } - } - class ParameterRow extends Pe.Component { - constructor(s, o) { - super(s, o), this.setDefaultValue(); - } - UNSAFE_componentWillReceiveProps(s) { - let o, - { specSelectors: i, pathMethod: u, rawParam: _ } = s, - w = i.isOAS3(), - x = i.parameterWithMetaByIdentity(u, _) || new qe.Map(); - if (((x = x.isEmpty() ? _ : x), w)) { - let { schema: s } = getParameterSchema(x, { isOAS3: w }); - o = s ? s.get('enum') : void 0; - } else o = x ? x.get('enum') : void 0; - let C, - j = x ? x.get('value') : void 0; - void 0 !== j ? (C = j) : _.get('required') && o && o.size && (C = o.first()), - void 0 !== C && - C !== j && - this.onChangeWrapper( - (function numberToString(s) { - return 'number' == typeof s ? s.toString() : s; - })(C) - ), - this.setDefaultValue(); - } - onChangeWrapper = (s, o = !1) => { - let i, - { onChange: u, rawParam: _ } = this.props; - return (i = '' === s || (s && 0 === s.size) ? null : s), u(_, i, o); - }; - _onExampleSelect = (s) => { - this.props.oas3Actions.setActiveExamplesMember({ - name: s, - pathMethod: this.props.pathMethod, - contextType: 'parameters', - contextName: this.getParamKey() - }); - }; - onChangeIncludeEmpty = (s) => { - let { specActions: o, param: i, pathMethod: u } = this.props; - const _ = i.get('name'), - w = i.get('in'); - return o.updateEmptyParamInclusion(u, _, w, s); - }; - setDefaultValue = () => { - let { - specSelectors: s, - pathMethod: o, - rawParam: i, - oas3Selectors: u, - fn: _ - } = this.props; - const w = s.parameterWithMetaByIdentity(o, i) || (0, qe.Map)(); - let { schema: x } = getParameterSchema(w, { isOAS3: s.isOAS3() }); - const C = w - .get('content', (0, qe.Map)()) - .keySeq() - .first(), - j = x ? _.getSampleSchema(x.toJS(), C, { includeWriteOnly: !0 }) : null; - if (w && void 0 === w.get('value') && 'body' !== w.get('in')) { - let i; - if (s.isSwagger2()) - i = - void 0 !== w.get('x-example') - ? w.get('x-example') - : void 0 !== w.getIn(['schema', 'example']) - ? w.getIn(['schema', 'example']) - : x && x.getIn(['default']); - else if (s.isOAS3()) { - x = this.composeJsonSchema(x); - const s = u.activeExamplesMember(...o, 'parameters', this.getParamKey()); - i = - void 0 !== w.getIn(['examples', s, 'value']) - ? w.getIn(['examples', s, 'value']) - : void 0 !== w.getIn(['content', C, 'example']) - ? w.getIn(['content', C, 'example']) - : void 0 !== w.get('example') - ? w.get('example') - : void 0 !== (x && x.get('example')) - ? x && x.get('example') - : void 0 !== (x && x.get('default')) - ? x && x.get('default') - : w.get('default'); - } - void 0 === i || qe.List.isList(i) || (i = stringify(i)), - void 0 !== i - ? this.onChangeWrapper(i) - : x && - 'object' === x.get('type') && - j && - !w.get('examples') && - this.onChangeWrapper(qe.List.isList(j) ? j : stringify(j)); - } - }; - getParamKey() { - const { param: s } = this.props; - return s ? `${s.get('name')}-${s.get('in')}` : null; - } - composeJsonSchema(s) { - const { fn: o } = this.props, - i = s.get('oneOf')?.get(0)?.toJS(), - u = s.get('anyOf')?.get(0)?.toJS(); - return (0, qe.fromJS)(o.mergeJsonSchema(s.toJS(), i ?? u ?? {})); - } - render() { - let { - param: s, - rawParam: o, - getComponent: i, - getConfigs: u, - isExecute: _, - fn: w, - onChangeConsumes: x, - specSelectors: C, - pathMethod: j, - specPath: L, - oas3Selectors: B - } = this.props, - $ = C.isOAS3(); - const { showExtensions: V, showCommonExtensions: U } = u(); - if ((s || (s = o), !o)) return null; - const z = i('JsonSchemaForm'), - Y = i('ParamBody'); - let Z = s.get('in'), - ee = - 'body' !== Z - ? null - : Pe.createElement(Y, { - getComponent: i, - getConfigs: u, - fn: w, - param: s, - consumes: C.consumesOptionsFor(j), - consumesValue: C.contentTypeValues(j).get('requestContentType'), - onChange: this.onChangeWrapper, - onChangeConsumes: x, - isExecute: _, - specSelectors: C, - pathMethod: j - }); - const ie = i('modelExample'), - ae = i('Markdown', !0), - le = i('ParameterExt'), - ce = i('ParameterIncludeEmpty'), - pe = i('ExamplesSelectValueRetainer'), - de = i('Example'); - let { schema: fe } = getParameterSchema(s, { isOAS3: $ }), - ye = C.parameterWithMetaByIdentity(j, o) || (0, qe.Map)(); - $ && (fe = this.composeJsonSchema(fe)); - let be, - _e, - we, - Se, - xe = fe ? fe.get('format') : null, - Te = fe ? fe.get('type') : null, - Re = fe ? fe.getIn(['items', 'type']) : null, - $e = 'formData' === Z, - ze = 'FormData' in at, - We = s.get('required'), - He = ye ? ye.get('value') : '', - Ye = U ? getCommonExtensions(fe) : null, - Xe = V ? getExtensions(s) : null, - Qe = !1; - return ( - void 0 !== s && fe && (be = fe.get('items')), - void 0 !== be - ? ((_e = be.get('enum')), (we = be.get('default'))) - : fe && (_e = fe.get('enum')), - _e && _e.size && _e.size > 0 && (Qe = !0), - void 0 !== s && - (fe && (we = fe.get('default')), - void 0 === we && (we = s.get('default')), - (Se = s.get('example')), - void 0 === Se && (Se = s.get('x-example'))), - Pe.createElement( - 'tr', - { 'data-param-name': s.get('name'), 'data-param-in': s.get('in') }, - Pe.createElement( - 'td', - { className: 'parameters-col_name' }, - Pe.createElement( - 'div', - { className: We ? 'parameter__name required' : 'parameter__name' }, - s.get('name'), - We ? Pe.createElement('span', null, ' *') : null - ), - Pe.createElement( - 'div', - { className: 'parameter__type' }, - Te, - Re && `[${Re}]`, - xe && Pe.createElement('span', { className: 'prop-format' }, '($', xe, ')') - ), - Pe.createElement( - 'div', - { className: 'parameter__deprecated' }, - $ && s.get('deprecated') ? 'deprecated' : null - ), - Pe.createElement('div', { className: 'parameter__in' }, '(', s.get('in'), ')') - ), - Pe.createElement( - 'td', - { className: 'parameters-col_description' }, - s.get('description') - ? Pe.createElement(ae, { source: s.get('description') }) - : null, - (!ee && _) || !Qe - ? null - : Pe.createElement(ae, { - className: 'parameter__enum', - source: - 'Available values : ' + - _e - .map(function (s) { - return s; - }) - .toArray() - .map(String) - .join(', ') - }), - (!ee && _) || void 0 === we - ? null - : Pe.createElement(ae, { - className: 'parameter__default', - source: 'Default value : ' + we - }), - (!ee && _) || void 0 === Se - ? null - : Pe.createElement(ae, { source: 'Example : ' + Se }), - $e && - !ze && - Pe.createElement('div', null, 'Error: your browser does not support FormData'), - $ && s.get('examples') - ? Pe.createElement( - 'section', - { className: 'parameter-controls' }, - Pe.createElement(pe, { - examples: s.get('examples'), - onSelect: this._onExampleSelect, - updateValue: this.onChangeWrapper, - getComponent: i, - defaultToFirstExample: !0, - currentKey: B.activeExamplesMember( - ...j, - 'parameters', - this.getParamKey() - ), - currentUserInputValue: He - }) - ) - : null, - ee - ? null - : Pe.createElement(z, { - fn: w, - getComponent: i, - value: He, - required: We, - disabled: !_, - description: s.get('name'), - onChange: this.onChangeWrapper, - errors: ye.get('errors'), - schema: fe - }), - ee && fe - ? Pe.createElement(ie, { - getComponent: i, - specPath: L.push('schema'), - getConfigs: u, - isExecute: _, - specSelectors: C, - schema: fe, - example: ee, - includeWriteOnly: !0 - }) - : null, - !ee && _ && s.get('allowEmptyValue') - ? Pe.createElement(ce, { - onChange: this.onChangeIncludeEmpty, - isIncluded: C.parameterInclusionSettingFor(j, s.get('name'), s.get('in')), - isDisabled: !isEmptyValue(He) - }) - : null, - $ && s.get('examples') - ? Pe.createElement(de, { - example: s.getIn([ - 'examples', - B.activeExamplesMember(...j, 'parameters', this.getParamKey()) - ]), - getComponent: i, - getConfigs: u - }) - : null, - U && Ye.size - ? Ye.entrySeq().map(([s, o]) => - Pe.createElement(le, { key: `${s}-${o}`, xKey: s, xVal: o }) - ) - : null, - V && Xe.size - ? Xe.entrySeq().map(([s, o]) => - Pe.createElement(le, { key: `${s}-${o}`, xKey: s, xVal: o }) - ) - : null - ) - ) - ); - } - } - class Execute extends Pe.Component { - handleValidateParameters = () => { - let { specSelectors: s, specActions: o, path: i, method: u } = this.props; - return o.validateParams([i, u]), s.validateBeforeExecute([i, u]); - }; - handleValidateRequestBody = () => { - let { - path: s, - method: o, - specSelectors: i, - oas3Selectors: u, - oas3Actions: _ - } = this.props, - w = { missingBodyValue: !1, missingRequiredKeys: [] }; - _.clearRequestBodyValidateError({ path: s, method: o }); - let x = i.getOAS3RequiredRequestBodyContentType([s, o]), - C = u.requestBodyValue(s, o), - j = u.validateBeforeExecute([s, o]), - L = u.requestContentType(s, o); - if (!j) - return ( - (w.missingBodyValue = !0), - _.setRequestBodyValidateError({ path: s, method: o, validationErrors: w }), - !1 - ); - if (!x) return !0; - let B = u.validateShallowRequired({ - oas3RequiredRequestBodyContentType: x, - oas3RequestContentType: L, - oas3RequestBodyValue: C - }); - return ( - !B || - B.length < 1 || - (B.forEach((s) => { - w.missingRequiredKeys.push(s); - }), - _.setRequestBodyValidateError({ path: s, method: o, validationErrors: w }), - !1) - ); - }; - handleValidationResultPass = () => { - let { specActions: s, operation: o, path: i, method: u } = this.props; - this.props.onExecute && this.props.onExecute(), - s.execute({ operation: o, path: i, method: u }); - }; - handleValidationResultFail = () => { - let { specActions: s, path: o, method: i } = this.props; - s.clearValidateParams([o, i]), - setTimeout(() => { - s.validateParams([o, i]); - }, 40); - }; - handleValidationResult = (s) => { - s ? this.handleValidationResultPass() : this.handleValidationResultFail(); - }; - onClick = () => { - let s = this.handleValidateParameters(), - o = this.handleValidateRequestBody(), - i = s && o; - this.handleValidationResult(i); - }; - onChangeProducesWrapper = (s) => - this.props.specActions.changeProducesValue([this.props.path, this.props.method], s); - render() { - const { disabled: s } = this.props; - return Pe.createElement( - 'button', - { className: 'btn execute opblock-control__btn', onClick: this.onClick, disabled: s }, - 'Execute' - ); - } - } - class headers_Headers extends Pe.Component { - render() { - let { headers: s, getComponent: o } = this.props; - const i = o('Property'), - u = o('Markdown', !0); - return s && s.size - ? Pe.createElement( - 'div', - { className: 'headers-wrapper' }, - Pe.createElement('h4', { className: 'headers__title' }, 'Headers:'), - Pe.createElement( - 'table', - { className: 'headers' }, - Pe.createElement( - 'thead', - null, - Pe.createElement( - 'tr', - { className: 'header-row' }, - Pe.createElement('th', { className: 'header-col' }, 'Name'), - Pe.createElement('th', { className: 'header-col' }, 'Description'), - Pe.createElement('th', { className: 'header-col' }, 'Type') - ) - ), - Pe.createElement( - 'tbody', - null, - s - .entrySeq() - .map(([s, o]) => { - if (!$e().Map.isMap(o)) return null; - const _ = o.get('description'), - w = o.getIn(['schema']) - ? o.getIn(['schema', 'type']) - : o.getIn(['type']), - x = o.getIn(['schema', 'example']); - return Pe.createElement( - 'tr', - { key: s }, - Pe.createElement('td', { className: 'header-col' }, s), - Pe.createElement( - 'td', - { className: 'header-col' }, - _ ? Pe.createElement(u, { source: _ }) : null - ), - Pe.createElement( - 'td', - { className: 'header-col' }, - w, - ' ', - x - ? Pe.createElement(i, { - propKey: 'Example', - propVal: x, - propClass: 'header-example' - }) - : null - ) - ); - }) - .toArray() - ) - ) - ) - : null; - } - } - class Errors extends Pe.Component { - render() { - let { - editorActions: s, - errSelectors: o, - layoutSelectors: i, - layoutActions: u, - getComponent: _ - } = this.props; - const w = _('Collapse'); - if (s && s.jumpToLine) var x = s.jumpToLine; - let C = o - .allErrors() - .filter((s) => 'thrown' === s.get('type') || 'error' === s.get('level')); - if (!C || C.count() < 1) return null; - let j = i.isShown(['errorPane'], !0), - L = C.sortBy((s) => s.get('line')); - return Pe.createElement( - 'pre', - { className: 'errors-wrapper' }, - Pe.createElement( - 'hgroup', - { className: 'error' }, - Pe.createElement('h4', { className: 'errors__title' }, 'Errors'), - Pe.createElement( - 'button', - { className: 'btn errors__clear-btn', onClick: () => u.show(['errorPane'], !j) }, - j ? 'Hide' : 'Show' - ) - ), - Pe.createElement( - w, - { isOpened: j, animated: !0 }, - Pe.createElement( - 'div', - { className: 'errors' }, - L.map((s, o) => { - let i = s.get('type'); - return 'thrown' === i || 'auth' === i - ? Pe.createElement(ThrownErrorItem, { - key: o, - error: s.get('error') || s, - jumpToLine: x - }) - : 'spec' === i - ? Pe.createElement(SpecErrorItem, { key: o, error: s, jumpToLine: x }) - : void 0; - }) - ) - ) - ); - } - } - const ThrownErrorItem = ({ error: s, jumpToLine: o }) => { - if (!s) return null; - let i = s.get('line'); - return Pe.createElement( - 'div', - { className: 'error-wrapper' }, - s - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - s.get('source') && s.get('level') - ? toTitleCase(s.get('source')) + ' ' + s.get('level') - : '', - s.get('path') ? Pe.createElement('small', null, ' at ', s.get('path')) : null - ), - Pe.createElement('span', { className: 'message thrown' }, s.get('message')), - Pe.createElement( - 'div', - { className: 'error-line' }, - i && o - ? Pe.createElement('a', { onClick: o.bind(null, i) }, 'Jump to line ', i) - : null - ) - ) - : null - ); - }, - SpecErrorItem = ({ error: s, jumpToLine: o = null }) => { - let i = null; - return ( - s.get('path') - ? (i = qe.List.isList(s.get('path')) - ? Pe.createElement('small', null, 'at ', s.get('path').join('.')) - : Pe.createElement('small', null, 'at ', s.get('path'))) - : s.get('line') && - !o && - (i = Pe.createElement('small', null, 'on line ', s.get('line'))), - Pe.createElement( - 'div', - { className: 'error-wrapper' }, - s - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - toTitleCase(s.get('source')) + ' ' + s.get('level'), - ' ', - i - ), - Pe.createElement('span', { className: 'message' }, s.get('message')), - Pe.createElement( - 'div', - { className: 'error-line' }, - o - ? Pe.createElement( - 'a', - { onClick: o.bind(null, s.get('line')) }, - 'Jump to line ', - s.get('line') - ) - : null - ) - ) - : null - ) - ); - }; - function toTitleCase(s) { - return (s || '') - .split(' ') - .map((s) => s[0].toUpperCase() + s.slice(1)) - .join(' '); - } - const content_type_noop = () => {}; - class ContentType extends Pe.Component { - static defaultProps = { - onChange: content_type_noop, - value: null, - contentTypes: (0, qe.fromJS)(['application/json']) - }; - componentDidMount() { - this.props.contentTypes && this.props.onChange(this.props.contentTypes.first()); - } - UNSAFE_componentWillReceiveProps(s) { - s.contentTypes && - s.contentTypes.size && - (s.contentTypes.includes(s.value) || s.onChange(s.contentTypes.first())); - } - onChangeWrapper = (s) => this.props.onChange(s.target.value); - render() { - let { - ariaControls: s, - ariaLabel: o, - className: i, - contentTypes: u, - controlId: _, - value: w - } = this.props; - return u && u.size - ? Pe.createElement( - 'div', - { className: 'content-type-wrapper ' + (i || '') }, - Pe.createElement( - 'select', - { - 'aria-controls': s, - 'aria-label': o, - className: 'content-type', - id: _, - onChange: this.onChangeWrapper, - value: w || '' - }, - u.map((s) => Pe.createElement('option', { key: s, value: s }, s)).toArray() - ) - ) - : null; - } - } - function xclass(...s) { - return s - .filter((s) => !!s) - .join(' ') - .trim(); - } - class Container extends Pe.Component { - render() { - let { fullscreen: s, full: o, ...i } = this.props; - if (s) return Pe.createElement('section', i); - let u = 'swagger-container' + (o ? '-full' : ''); - return Pe.createElement('section', Rn()({}, i, { className: xclass(i.className, u) })); - } - } - const Bk = { mobile: '', tablet: '-tablet', desktop: '-desktop', large: '-hd' }; - class Col extends Pe.Component { - render() { - const { - hide: s, - keepContents: o, - mobile: i, - tablet: u, - desktop: _, - large: w, - ...x - } = this.props; - if (s && !o) return Pe.createElement('span', null); - let C = []; - for (let s in Bk) { - if (!Object.prototype.hasOwnProperty.call(Bk, s)) continue; - let o = Bk[s]; - if (s in this.props) { - let i = this.props[s]; - if (i < 1) { - C.push('none' + o); - continue; - } - C.push('block' + o), C.push('col-' + i + o); - } - } - s && C.push('hidden'); - let j = xclass(x.className, ...C); - return Pe.createElement('section', Rn()({}, x, { className: j })); - } - } - class Row extends Pe.Component { - render() { - return Pe.createElement( - 'div', - Rn()({}, this.props, { className: xclass(this.props.className, 'wrapper') }) - ); - } - } - class Button extends Pe.Component { - static defaultProps = { className: '' }; - render() { - return Pe.createElement( - 'button', - Rn()({}, this.props, { className: xclass(this.props.className, 'button') }) - ); - } - } - const TextArea = (s) => Pe.createElement('textarea', s), - Input = (s) => Pe.createElement('input', s); - class Select extends Pe.Component { - static defaultProps = { multiple: !1, allowEmptyValue: !0 }; - constructor(s, o) { - let i; - super(s, o), - (i = s.value ? s.value : s.multiple ? [''] : ''), - (this.state = { value: i }); - } - onChange = (s) => { - let o, - { onChange: i, multiple: u } = this.props, - _ = [].slice.call(s.target.options); - (o = u - ? _.filter(function (s) { - return s.selected; - }).map(function (s) { - return s.value; - }) - : s.target.value), - this.setState({ value: o }), - i && i(o); - }; - UNSAFE_componentWillReceiveProps(s) { - s.value !== this.props.value && this.setState({ value: s.value }); - } - render() { - let { allowedValues: s, multiple: o, allowEmptyValue: i, disabled: u } = this.props, - _ = this.state.value?.toJS?.() || this.state.value; - return Pe.createElement( - 'select', - { - className: this.props.className, - multiple: o, - value: _, - onChange: this.onChange, - disabled: u - }, - i ? Pe.createElement('option', { value: '' }, '--') : null, - s.map(function (s, o) { - return Pe.createElement('option', { key: o, value: String(s) }, String(s)); - }) - ); - } - } - class layout_utils_Link extends Pe.Component { - render() { - return Pe.createElement( - 'a', - Rn()({}, this.props, { - rel: 'noopener noreferrer', - className: xclass(this.props.className, 'link') - }) - ); - } - } - const NoMargin = ({ children: s }) => - Pe.createElement('div', { className: 'no-margin' }, ' ', s, ' '); - class Collapse extends Pe.Component { - static defaultProps = { isOpened: !1, animated: !1 }; - renderNotAnimated() { - return this.props.isOpened - ? Pe.createElement(NoMargin, null, this.props.children) - : Pe.createElement('noscript', null); - } - render() { - let { animated: s, isOpened: o, children: i } = this.props; - return s - ? ((i = o ? i : null), Pe.createElement(NoMargin, null, i)) - : this.renderNotAnimated(); - } - } - class Overview extends Pe.Component { - constructor(...s) { - super(...s), (this.setTagShown = this._setTagShown.bind(this)); - } - _setTagShown(s, o) { - this.props.layoutActions.show(s, o); - } - showOp(s, o) { - let { layoutActions: i } = this.props; - i.show(s, o); - } - render() { - let { - specSelectors: s, - layoutSelectors: o, - layoutActions: i, - getComponent: u - } = this.props, - _ = s.taggedOperations(); - const w = u('Collapse'); - return Pe.createElement( - 'div', - null, - Pe.createElement('h4', { className: 'overview-title' }, 'Overview'), - _.map((s, u) => { - let _ = s.get('operations'), - x = ['overview-tags', u], - C = o.isShown(x, !0); - return Pe.createElement( - 'div', - { key: 'overview-' + u }, - Pe.createElement( - 'h4', - { onClick: () => i.show(x, !C), className: 'link overview-tag' }, - ' ', - C ? '-' : '+', - u - ), - Pe.createElement( - w, - { isOpened: C, animated: !0 }, - _.map((s) => { - let { path: u, method: _, id: w } = s.toObject(), - x = 'operations', - C = w, - j = o.isShown([x, C]); - return Pe.createElement(OperationLink, { - key: w, - path: u, - method: _, - id: u + '-' + _, - shown: j, - showOpId: C, - showOpIdPrefix: x, - href: `#operation-${C}`, - onClick: i.show - }); - }).toArray() - ) - ); - }).toArray(), - _.size < 1 && Pe.createElement('h3', null, ' No operations defined in spec! ') - ); - } - } - class OperationLink extends Pe.Component { - constructor(s) { - super(s), (this.onClick = this._onClick.bind(this)); - } - _onClick() { - let { showOpId: s, showOpIdPrefix: o, onClick: i, shown: u } = this.props; - i([o, s], !u); - } - render() { - let { id: s, method: o, shown: i, href: u } = this.props; - return Pe.createElement( - layout_utils_Link, - { - href: u, - onClick: this.onClick, - className: 'block opblock-link ' + (i ? 'shown' : '') - }, - Pe.createElement( - 'div', - null, - Pe.createElement('small', { className: `bold-label-${o}` }, o.toUpperCase()), - Pe.createElement('span', { className: 'bold-label' }, s) - ) - ); - } - } - class InitializedInput extends Pe.Component { - componentDidMount() { - this.props.initialValue && (this.inputRef.value = this.props.initialValue); - } - render() { - const { value: s, defaultValue: o, initialValue: i, ...u } = this.props; - return Pe.createElement('input', Rn()({}, u, { ref: (s) => (this.inputRef = s) })); - } - } - class InfoBasePath extends Pe.Component { - render() { - const { host: s, basePath: o } = this.props; - return Pe.createElement('pre', { className: 'base-url' }, '[ Base URL: ', s, o, ' ]'); - } - } - class InfoUrl extends Pe.PureComponent { - render() { - const { url: s, getComponent: o } = this.props, - i = o('Link'); - return Pe.createElement( - i, - { target: '_blank', href: sanitizeUrl(s) }, - Pe.createElement('span', { className: 'url' }, ' ', s) - ); - } - } - class info_Info extends Pe.Component { - render() { - const { - info: s, - url: o, - host: i, - basePath: u, - getComponent: _, - externalDocs: w, - selectedServer: x, - url: C - } = this.props, - j = s.get('version'), - L = s.get('description'), - B = s.get('title'), - $ = safeBuildUrl(s.get('termsOfService'), C, { selectedServer: x }), - V = s.get('contact'), - U = s.get('license'), - z = safeBuildUrl(w && w.get('url'), C, { selectedServer: x }), - Y = w && w.get('description'), - Z = _('Markdown', !0), - ee = _('Link'), - ie = _('VersionStamp'), - ae = _('OpenAPIVersion'), - le = _('InfoUrl'), - ce = _('InfoBasePath'), - pe = _('License'), - de = _('Contact'); - return Pe.createElement( - 'div', - { className: 'info' }, - Pe.createElement( - 'hgroup', - { className: 'main' }, - Pe.createElement( - 'h2', - { className: 'title' }, - B, - Pe.createElement( - 'span', - null, - j && Pe.createElement(ie, { version: j }), - Pe.createElement(ae, { oasVersion: '2.0' }) - ) - ), - i || u ? Pe.createElement(ce, { host: i, basePath: u }) : null, - o && Pe.createElement(le, { getComponent: _, url: o }) - ), - Pe.createElement( - 'div', - { className: 'description' }, - Pe.createElement(Z, { source: L }) - ), - $ && - Pe.createElement( - 'div', - { className: 'info__tos' }, - Pe.createElement( - ee, - { target: '_blank', href: sanitizeUrl($) }, - 'Terms of service' - ) - ), - V?.size > 0 && - Pe.createElement(de, { getComponent: _, data: V, selectedServer: x, url: o }), - U?.size > 0 && - Pe.createElement(pe, { getComponent: _, license: U, selectedServer: x, url: o }), - z - ? Pe.createElement( - ee, - { className: 'info__extdocs', target: '_blank', href: sanitizeUrl(z) }, - Y || z - ) - : null - ); - } - } - const qk = info_Info; - class InfoContainer extends Pe.Component { - render() { - const { specSelectors: s, getComponent: o, oas3Selectors: i } = this.props, - u = s.info(), - _ = s.url(), - w = s.basePath(), - x = s.host(), - C = s.externalDocs(), - j = i.selectedServer(), - L = o('info'); - return Pe.createElement( - 'div', - null, - u && u.count() - ? Pe.createElement(L, { - info: u, - url: _, - host: x, - basePath: w, - externalDocs: C, - getComponent: o, - selectedServer: j - }) - : null - ); - } - } - class contact_Contact extends Pe.Component { - render() { - const { data: s, getComponent: o, selectedServer: i, url: u } = this.props, - _ = s.get('name', 'the developer'), - w = safeBuildUrl(s.get('url'), u, { selectedServer: i }), - x = s.get('email'), - C = o('Link'); - return Pe.createElement( - 'div', - { className: 'info__contact' }, - w && - Pe.createElement( - 'div', - null, - Pe.createElement(C, { href: sanitizeUrl(w), target: '_blank' }, _, ' - Website') - ), - x && - Pe.createElement( - C, - { href: sanitizeUrl(`mailto:${x}`) }, - w ? `Send email to ${_}` : `Contact ${_}` - ) - ); - } - } - const Vk = contact_Contact; - class license_License extends Pe.Component { - render() { - const { license: s, getComponent: o, selectedServer: i, url: u } = this.props, - _ = s.get('name', 'License'), - w = safeBuildUrl(s.get('url'), u, { selectedServer: i }), - x = o('Link'); - return Pe.createElement( - 'div', - { className: 'info__license' }, - w - ? Pe.createElement( - 'div', - { className: 'info__license__url' }, - Pe.createElement(x, { target: '_blank', href: sanitizeUrl(w) }, _) - ) - : Pe.createElement('span', null, _) - ); - } - } - const zk = license_License; - class JumpToPath extends Pe.Component { - render() { - return null; - } - } - class CopyToClipboardBtn extends Pe.Component { - render() { - let { getComponent: s } = this.props; - const o = s('CopyIcon'); - return Pe.createElement( - 'div', - { className: 'view-line-link copy-to-clipboard', title: 'Copy to clipboard' }, - Pe.createElement( - Jn.CopyToClipboard, - { text: this.props.textToCopy }, - Pe.createElement(o, null) - ) - ); - } - } - class Footer extends Pe.Component { - render() { - return Pe.createElement('div', { className: 'footer' }); - } - } - class FilterContainer extends Pe.Component { - onFilterChange = (s) => { - const { - target: { value: o } - } = s; - this.props.layoutActions.updateFilter(o); - }; - render() { - const { specSelectors: s, layoutSelectors: o, getComponent: i } = this.props, - u = i('Col'), - _ = 'loading' === s.loadingStatus(), - w = 'failed' === s.loadingStatus(), - x = o.currentFilter(), - C = ['operation-filter-input']; - return ( - w && C.push('failed'), - _ && C.push('loading'), - Pe.createElement( - 'div', - null, - !1 === x - ? null - : Pe.createElement( - 'div', - { className: 'filter-container' }, - Pe.createElement( - u, - { className: 'filter wrapper', mobile: 12 }, - Pe.createElement('input', { - className: C.join(' '), - placeholder: 'Filter by tag', - type: 'text', - onChange: this.onFilterChange, - value: 'string' == typeof x ? x : '', - disabled: _ - }) - ) - ) - ) - ); - } - } - const eC = Function.prototype; - class ParamBody extends Pe.PureComponent { - static defaultProp = { - consumes: (0, qe.fromJS)(['application/json']), - param: (0, qe.fromJS)({}), - onChange: eC, - onChangeConsumes: eC - }; - constructor(s, o) { - super(s, o), (this.state = { isEditBox: !1, value: '' }); - } - componentDidMount() { - this.updateValues.call(this, this.props); - } - UNSAFE_componentWillReceiveProps(s) { - this.updateValues.call(this, s); - } - updateValues = (s) => { - let { param: o, isExecute: i, consumesValue: u = '' } = s, - _ = /xml/i.test(u), - w = /json/i.test(u), - x = _ ? o.get('value_xml') : o.get('value'); - if (void 0 !== x) { - let s = !x && w ? '{}' : x; - this.setState({ value: s }), this.onChange(s, { isXml: _, isEditBox: i }); - } else - _ - ? this.onChange(this.sample('xml'), { isXml: _, isEditBox: i }) - : this.onChange(this.sample(), { isEditBox: i }); - }; - sample = (s) => { - let { param: o, fn: i } = this.props, - u = i.inferSchema(o.toJS()); - return i.getSampleSchema(u, s, { includeWriteOnly: !0 }); - }; - onChange = (s, { isEditBox: o, isXml: i }) => { - this.setState({ value: s, isEditBox: o }), this._onChange(s, i); - }; - _onChange = (s, o) => { - (this.props.onChange || eC)(s, o); - }; - handleOnChange = (s) => { - const { consumesValue: o } = this.props, - i = /xml/i.test(o), - u = s.target.value; - this.onChange(u, { isXml: i, isEditBox: this.state.isEditBox }); - }; - toggleIsEditBox = () => this.setState((s) => ({ isEditBox: !s.isEditBox })); - render() { - let { - onChangeConsumes: s, - param: o, - isExecute: i, - specSelectors: u, - pathMethod: _, - getComponent: w - } = this.props; - const x = w('Button'), - C = w('TextArea'), - j = w('HighlightCode', !0), - L = w('contentType'); - let B = (u ? u.parameterWithMetaByIdentity(_, o) : o).get('errors', (0, qe.List)()), - $ = u.contentTypeValues(_).get('requestContentType'), - V = - this.props.consumes && this.props.consumes.size - ? this.props.consumes - : ParamBody.defaultProp.consumes, - { value: U, isEditBox: z } = this.state, - Y = null; - getKnownSyntaxHighlighterLanguage(U) && (Y = 'json'); - const Z = `${createHtmlReadyId(`${_[1]}${_[0]}_parameters`)}_select`; - return Pe.createElement( - 'div', - { - className: 'body-param', - 'data-param-name': o.get('name'), - 'data-param-in': o.get('in') - }, - z && i - ? Pe.createElement(C, { - className: 'body-param__text' + (B.count() ? ' invalid' : ''), - value: U, - onChange: this.handleOnChange - }) - : U && Pe.createElement(j, { className: 'body-param__example', language: Y }, U), - Pe.createElement( - 'div', - { className: 'body-param-options' }, - i - ? Pe.createElement( - 'div', - { className: 'body-param-edit' }, - Pe.createElement( - x, - { - className: z - ? 'btn cancel body-param__example-edit' - : 'btn edit body-param__example-edit', - onClick: this.toggleIsEditBox - }, - z ? 'Cancel' : 'Edit' - ) - ) - : null, - Pe.createElement( - 'label', - { htmlFor: Z }, - Pe.createElement('span', null, 'Parameter content type'), - Pe.createElement(L, { - value: $, - contentTypes: V, - onChange: s, - className: 'body-param-content-type', - ariaLabel: 'Parameter content type', - controlId: Z - }) - ) - ) - ); - } - } - class Curl extends Pe.Component { - render() { - const { request: s, getComponent: o } = this.props, - i = requestSnippetGenerator_curl_bash(s), - u = o('SyntaxHighlighter', !0); - return Pe.createElement( - 'div', - { className: 'curl-command' }, - Pe.createElement('h4', null, 'Curl'), - Pe.createElement( - 'div', - { className: 'copy-to-clipboard' }, - Pe.createElement(Jn.CopyToClipboard, { text: i }, Pe.createElement('button', null)) - ), - Pe.createElement( - 'div', - null, - Pe.createElement( - u, - { - language: 'bash', - className: 'curl microlight', - renderPlainText: ({ children: s, PlainTextViewer: o }) => - Pe.createElement(o, { className: 'curl' }, s) - }, - i - ) - ) - ); - } - } - const property = ({ propKey: s, propVal: o, propClass: i }) => - Pe.createElement( - 'span', - { className: i }, - Pe.createElement('br', null), - s, - ': ', - String(o) - ); - class TryItOutButton extends Pe.Component { - static defaultProps = { - onTryoutClick: Function.prototype, - onCancelClick: Function.prototype, - onResetClick: Function.prototype, - enabled: !1, - hasUserEditedBody: !1, - isOAS3: !1 - }; - render() { - const { - onTryoutClick: s, - onCancelClick: o, - onResetClick: i, - enabled: u, - hasUserEditedBody: _, - isOAS3: w - } = this.props, - x = w && _; - return Pe.createElement( - 'div', - { className: x ? 'try-out btn-group' : 'try-out' }, - u - ? Pe.createElement( - 'button', - { className: 'btn try-out__btn cancel', onClick: o }, - 'Cancel' - ) - : Pe.createElement( - 'button', - { className: 'btn try-out__btn', onClick: s }, - 'Try it out ' - ), - x && - Pe.createElement( - 'button', - { className: 'btn try-out__btn reset', onClick: i }, - 'Reset' - ) - ); - } - } - class VersionPragmaFilter extends Pe.PureComponent { - static defaultProps = { alsoShow: null, children: null, bypass: !1 }; - render() { - const { bypass: s, isSwagger2: o, isOAS3: i, alsoShow: u } = this.props; - return s - ? Pe.createElement('div', null, this.props.children) - : o && i - ? Pe.createElement( - 'div', - { className: 'version-pragma' }, - u, - Pe.createElement( - 'div', - { className: 'version-pragma__message version-pragma__message--ambiguous' }, - Pe.createElement( - 'div', - null, - Pe.createElement('h3', null, 'Unable to render this definition'), - Pe.createElement( - 'p', - null, - Pe.createElement('code', null, 'swagger'), - ' and ', - Pe.createElement('code', null, 'openapi'), - ' fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.' - ), - Pe.createElement( - 'p', - null, - 'Supported version fields are ', - Pe.createElement('code', null, 'swagger: ', '"2.0"'), - ' and those that match ', - Pe.createElement('code', null, 'openapi: 3.0.n'), - ' (for example, ', - Pe.createElement('code', null, 'openapi: 3.0.0'), - ').' - ) - ) - ) - ) - : o || i - ? Pe.createElement('div', null, this.props.children) - : Pe.createElement( - 'div', - { className: 'version-pragma' }, - u, - Pe.createElement( - 'div', - { className: 'version-pragma__message version-pragma__message--missing' }, - Pe.createElement( - 'div', - null, - Pe.createElement('h3', null, 'Unable to render this definition'), - Pe.createElement( - 'p', - null, - 'The provided definition does not specify a valid version field.' - ), - Pe.createElement( - 'p', - null, - 'Please indicate a valid Swagger or OpenAPI version field. Supported version fields are ', - Pe.createElement('code', null, 'swagger: ', '"2.0"'), - ' and those that match ', - Pe.createElement('code', null, 'openapi: 3.0.n'), - ' (for example, ', - Pe.createElement('code', null, 'openapi: 3.0.0'), - ').' - ) - ) - ) - ); - } - } - const version_stamp = ({ version: s }) => - Pe.createElement( - 'small', - null, - Pe.createElement('pre', { className: 'version' }, ' ', s, ' ') - ), - openapi_version = ({ oasVersion: s }) => - Pe.createElement( - 'small', - { className: 'version-stamp' }, - Pe.createElement('pre', { className: 'version' }, 'OAS ', s) - ), - deep_link = ({ enabled: s, path: o, text: i }) => - Pe.createElement( - 'a', - { - className: 'nostyle', - onClick: s ? (s) => s.preventDefault() : null, - href: s ? `#/${o}` : null - }, - Pe.createElement('span', null, i) - ), - svg_assets = () => - Pe.createElement( - 'div', - null, - Pe.createElement( - 'svg', - { - xmlns: 'http://www.w3.org/2000/svg', - xmlnsXlink: 'http://www.w3.org/1999/xlink', - className: 'svg-assets' - }, - Pe.createElement( - 'defs', - null, - Pe.createElement( - 'symbol', - { viewBox: '0 0 20 20', id: 'unlocked' }, - Pe.createElement('path', { - d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 20 20', id: 'locked' }, - Pe.createElement('path', { - d: 'M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 20 20', id: 'close' }, - Pe.createElement('path', { - d: 'M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 20 20', id: 'large-arrow' }, - Pe.createElement('path', { - d: 'M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 20 20', id: 'large-arrow-down' }, - Pe.createElement('path', { - d: 'M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 20 20', id: 'large-arrow-up' }, - Pe.createElement('path', { - d: 'M 17.418 14.908 C 17.69 15.176 18.127 15.176 18.397 14.908 C 18.667 14.64 18.668 14.207 18.397 13.939 L 10.489 6.109 C 10.219 5.841 9.782 5.841 9.51 6.109 L 1.602 13.939 C 1.332 14.207 1.332 14.64 1.602 14.908 C 1.873 15.176 2.311 15.176 2.581 14.908 L 10 7.767 L 17.418 14.908 Z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 24 24', id: 'jump-to' }, - Pe.createElement('path', { - d: 'M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 24 24', id: 'expand' }, - Pe.createElement('path', { - d: 'M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z' - }) - ), - Pe.createElement( - 'symbol', - { viewBox: '0 0 15 16', id: 'copy' }, - Pe.createElement( - 'g', - { transform: 'translate(2, -1)' }, - Pe.createElement('path', { - fill: '#ffffff', - fillRule: 'evenodd', - d: 'M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z' - }) - ) - ) - ) - ) - ); - var tC; - function decodeEntity(s) { - return ( - ((tC = tC || document.createElement('textarea')).innerHTML = '&' + s + ';'), tC.value - ); - } - var rC = Object.prototype.hasOwnProperty; - function index_browser_has(s, o) { - return !!s && rC.call(s, o); - } - function index_browser_assign(s) { - return ( - [].slice.call(arguments, 1).forEach(function (o) { - if (o) { - if ('object' != typeof o) throw new TypeError(o + 'must be object'); - Object.keys(o).forEach(function (i) { - s[i] = o[i]; - }); - } - }), - s - ); - } - var nC = /\\([\\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; - function unescapeMd(s) { - return s.indexOf('\\') < 0 ? s : s.replace(nC, '$1'); - } - function isValidEntityCode(s) { - return ( - !(s >= 55296 && s <= 57343) && - !(s >= 64976 && s <= 65007) && - !!(65535 & ~s && 65534 != (65535 & s)) && - !(s >= 0 && s <= 8) && - 11 !== s && - !(s >= 14 && s <= 31) && - !(s >= 127 && s <= 159) && - !(s > 1114111) - ); - } - function fromCodePoint(s) { - if (s > 65535) { - var o = 55296 + ((s -= 65536) >> 10), - i = 56320 + (1023 & s); - return String.fromCharCode(o, i); - } - return String.fromCharCode(s); - } - var sC = /&([a-z#][a-z0-9]{1,31});/gi, - oC = /^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))/i; - function replaceEntityPattern(s, o) { - var i = 0, - u = decodeEntity(o); - return o !== u - ? u - : 35 === o.charCodeAt(0) && - oC.test(o) && - isValidEntityCode( - (i = - 'x' === o[1].toLowerCase() - ? parseInt(o.slice(2), 16) - : parseInt(o.slice(1), 10)) - ) - ? fromCodePoint(i) - : s; - } - function replaceEntities(s) { - return s.indexOf('&') < 0 ? s : s.replace(sC, replaceEntityPattern); - } - var iC = /[&<>"]/, - aC = /[&<>"]/g, - lC = { '&': '&', '<': '<', '>': '>', '"': '"' }; - function replaceUnsafeChar(s) { - return lC[s]; - } - function escapeHtml(s) { - return iC.test(s) ? s.replace(aC, replaceUnsafeChar) : s; - } - var cC = {}; - function nextToken(s, o) { - return ++o >= s.length - 2 - ? o - : 'paragraph_open' === s[o].type && - s[o].tight && - 'inline' === s[o + 1].type && - 0 === s[o + 1].content.length && - 'paragraph_close' === s[o + 2].type && - s[o + 2].tight - ? nextToken(s, o + 2) - : o; - } - (cC.blockquote_open = function () { - return '
\n'; - }), - (cC.blockquote_close = function (s, o) { - return '
' + uC(s, o); - }), - (cC.code = function (s, o) { - return s[o].block - ? '
' + escapeHtml(s[o].content) + '
' + uC(s, o) - : '' + escapeHtml(s[o].content) + ''; - }), - (cC.fence = function (s, o, i, u, _) { - var w, - x, - C = s[o], - j = '', - L = i.langPrefix; - if (C.params) { - if ( - ((x = (w = C.params.split(/\s+/g)).join(' ')), - index_browser_has(_.rules.fence_custom, w[0])) - ) - return _.rules.fence_custom[w[0]](s, o, i, u, _); - j = ' class="' + L + escapeHtml(replaceEntities(unescapeMd(x))) + '"'; - } - return ( - '
' +
-							((i.highlight && i.highlight.apply(i.highlight, [C.content].concat(w))) ||
-								escapeHtml(C.content)) +
-							'
' + - uC(s, o) - ); - }), - (cC.fence_custom = {}), - (cC.heading_open = function (s, o) { - return ''; - }), - (cC.heading_close = function (s, o) { - return '\n'; - }), - (cC.hr = function (s, o, i) { - return (i.xhtmlOut ? '
' : '
') + uC(s, o); - }), - (cC.bullet_list_open = function () { - return '
    \n'; - }), - (cC.bullet_list_close = function (s, o) { - return '
' + uC(s, o); - }), - (cC.list_item_open = function () { - return '
  • '; - }), - (cC.list_item_close = function () { - return '
  • \n'; - }), - (cC.ordered_list_open = function (s, o) { - var i = s[o]; - return ' 1 ? ' start="' + i.order + '"' : '') + '>\n'; - }), - (cC.ordered_list_close = function (s, o) { - return '' + uC(s, o); - }), - (cC.paragraph_open = function (s, o) { - return s[o].tight ? '' : '

    '; - }), - (cC.paragraph_close = function (s, o) { - var i = !(s[o].tight && o && 'inline' === s[o - 1].type && !s[o - 1].content); - return (s[o].tight ? '' : '

    ') + (i ? uC(s, o) : ''); - }), - (cC.link_open = function (s, o, i) { - var u = s[o].title ? ' title="' + escapeHtml(replaceEntities(s[o].title)) + '"' : '', - _ = i.linkTarget ? ' target="' + i.linkTarget + '"' : ''; - return ''; - }), - (cC.link_close = function () { - return ''; - }), - (cC.image = function (s, o, i) { - var u = ' src="' + escapeHtml(s[o].src) + '"', - _ = s[o].title ? ' title="' + escapeHtml(replaceEntities(s[o].title)) + '"' : ''; - return ( - '' - ); - }), - (cC.table_open = function () { - return '\n'; - }), - (cC.table_close = function () { - return '
    \n'; - }), - (cC.thead_open = function () { - return '\n'; - }), - (cC.thead_close = function () { - return '\n'; - }), - (cC.tbody_open = function () { - return '\n'; - }), - (cC.tbody_close = function () { - return '\n'; - }), - (cC.tr_open = function () { - return ''; - }), - (cC.tr_close = function () { - return '\n'; - }), - (cC.th_open = function (s, o) { - var i = s[o]; - return ''; - }), - (cC.th_close = function () { - return ''; - }), - (cC.td_open = function (s, o) { - var i = s[o]; - return ''; - }), - (cC.td_close = function () { - return ''; - }), - (cC.strong_open = function () { - return ''; - }), - (cC.strong_close = function () { - return ''; - }), - (cC.em_open = function () { - return ''; - }), - (cC.em_close = function () { - return ''; - }), - (cC.del_open = function () { - return ''; - }), - (cC.del_close = function () { - return ''; - }), - (cC.ins_open = function () { - return ''; - }), - (cC.ins_close = function () { - return ''; - }), - (cC.mark_open = function () { - return ''; - }), - (cC.mark_close = function () { - return ''; - }), - (cC.sub = function (s, o) { - return '' + escapeHtml(s[o].content) + ''; - }), - (cC.sup = function (s, o) { - return '' + escapeHtml(s[o].content) + ''; - }), - (cC.hardbreak = function (s, o, i) { - return i.xhtmlOut ? '
    \n' : '
    \n'; - }), - (cC.softbreak = function (s, o, i) { - return i.breaks ? (i.xhtmlOut ? '
    \n' : '
    \n') : '\n'; - }), - (cC.text = function (s, o) { - return escapeHtml(s[o].content); - }), - (cC.htmlblock = function (s, o) { - return s[o].content; - }), - (cC.htmltag = function (s, o) { - return s[o].content; - }), - (cC.abbr_open = function (s, o) { - return ''; - }), - (cC.abbr_close = function () { - return ''; - }), - (cC.footnote_ref = function (s, o) { - var i = Number(s[o].id + 1).toString(), - u = 'fnref' + i; - return ( - s[o].subId > 0 && (u += ':' + s[o].subId), - '[' + - i + - ']' - ); - }), - (cC.footnote_block_open = function (s, o, i) { - return ( - (i.xhtmlOut ? '
    \n' : '
    \n') + - '
    \n
      \n' - ); - }), - (cC.footnote_block_close = function () { - return '
    \n
    \n'; - }), - (cC.footnote_open = function (s, o) { - return '
  • '; - }), - (cC.footnote_close = function () { - return '
  • \n'; - }), - (cC.footnote_anchor = function (s, o) { - var i = 'fnref' + Number(s[o].id + 1).toString(); - return ( - s[o].subId > 0 && (i += ':' + s[o].subId), - ' ↩' - ); - }), - (cC.dl_open = function () { - return '
    \n'; - }), - (cC.dt_open = function () { - return '
    '; - }), - (cC.dd_open = function () { - return '
    '; - }), - (cC.dl_close = function () { - return '
    \n'; - }), - (cC.dt_close = function () { - return '\n'; - }), - (cC.dd_close = function () { - return '\n'; - }); - var uC = (cC.getBreak = function getBreak(s, o) { - return (o = nextToken(s, o)) < s.length && 'list_item_close' === s[o].type ? '' : '\n'; - }); - function Renderer() { - (this.rules = index_browser_assign({}, cC)), (this.getBreak = cC.getBreak); - } - function Ruler() { - (this.__rules__ = []), (this.__cache__ = null); - } - function StateInline(s, o, i, u, _) { - (this.src = s), - (this.env = u), - (this.options = i), - (this.parser = o), - (this.tokens = _), - (this.pos = 0), - (this.posMax = this.src.length), - (this.level = 0), - (this.pending = ''), - (this.pendingLevel = 0), - (this.cache = []), - (this.isInLabel = !1), - (this.linkLevel = 0), - (this.linkContent = ''), - (this.labelUnmatchedScopes = 0); - } - function parseLinkLabel(s, o) { - var i, - u, - _, - w = -1, - x = s.posMax, - C = s.pos, - j = s.isInLabel; - if (s.isInLabel) return -1; - if (s.labelUnmatchedScopes) return s.labelUnmatchedScopes--, -1; - for (s.pos = o + 1, s.isInLabel = !0, i = 1; s.pos < x; ) { - if (91 === (_ = s.src.charCodeAt(s.pos))) i++; - else if (93 === _ && 0 === --i) { - u = !0; - break; - } - s.parser.skipToken(s); - } - return ( - u ? ((w = s.pos), (s.labelUnmatchedScopes = 0)) : (s.labelUnmatchedScopes = i - 1), - (s.pos = C), - (s.isInLabel = j), - w - ); - } - function parseAbbr(s, o, i, u) { - var _, w, x, C, j, L; - if (42 !== s.charCodeAt(0)) return -1; - if (91 !== s.charCodeAt(1)) return -1; - if (-1 === s.indexOf(']:')) return -1; - if ( - (w = parseLinkLabel((_ = new StateInline(s, o, i, u, [])), 1)) < 0 || - 58 !== s.charCodeAt(w + 1) - ) - return -1; - for (C = _.posMax, x = w + 2; x < C && 10 !== _.src.charCodeAt(x); x++); - return ( - (j = s.slice(2, w)), - 0 === (L = s.slice(w + 2, x).trim()).length - ? -1 - : (u.abbreviations || (u.abbreviations = {}), - void 0 === u.abbreviations[':' + j] && (u.abbreviations[':' + j] = L), - x) - ); - } - function normalizeLink(s) { - var o = replaceEntities(s); - try { - o = decodeURI(o); - } catch (s) {} - return encodeURI(o); - } - function parseLinkDestination(s, o) { - var i, - u, - _, - w = o, - x = s.posMax; - if (60 === s.src.charCodeAt(o)) { - for (o++; o < x; ) { - if (10 === (i = s.src.charCodeAt(o))) return !1; - if (62 === i) - return ( - (_ = normalizeLink(unescapeMd(s.src.slice(w + 1, o)))), - !!s.parser.validateLink(_) && ((s.pos = o + 1), (s.linkContent = _), !0) - ); - 92 === i && o + 1 < x ? (o += 2) : o++; - } - return !1; - } - for (u = 0; o < x && 32 !== (i = s.src.charCodeAt(o)) && !(i < 32 || 127 === i); ) - if (92 === i && o + 1 < x) o += 2; - else { - if (40 === i && ++u > 1) break; - if (41 === i && --u < 0) break; - o++; - } - return ( - w !== o && - ((_ = unescapeMd(s.src.slice(w, o))), - !!s.parser.validateLink(_) && ((s.linkContent = _), (s.pos = o), !0)) - ); - } - function parseLinkTitle(s, o) { - var i, - u = o, - _ = s.posMax, - w = s.src.charCodeAt(o); - if (34 !== w && 39 !== w && 40 !== w) return !1; - for (o++, 40 === w && (w = 41); o < _; ) { - if ((i = s.src.charCodeAt(o)) === w) - return (s.pos = o + 1), (s.linkContent = unescapeMd(s.src.slice(u + 1, o))), !0; - 92 === i && o + 1 < _ ? (o += 2) : o++; - } - return !1; - } - function normalizeReference(s) { - return s.trim().replace(/\s+/g, ' ').toUpperCase(); - } - function parseReference(s, o, i, u) { - var _, w, x, C, j, L, B, $, V; - if (91 !== s.charCodeAt(0)) return -1; - if (-1 === s.indexOf(']:')) return -1; - if ( - (w = parseLinkLabel((_ = new StateInline(s, o, i, u, [])), 0)) < 0 || - 58 !== s.charCodeAt(w + 1) - ) - return -1; - for ( - C = _.posMax, x = w + 2; - x < C && (32 === (j = _.src.charCodeAt(x)) || 10 === j); - x++ - ); - if (!parseLinkDestination(_, x)) return -1; - for ( - B = _.linkContent, L = x = _.pos, x += 1; - x < C && (32 === (j = _.src.charCodeAt(x)) || 10 === j); - x++ - ); - for ( - x < C && L !== x && parseLinkTitle(_, x) - ? (($ = _.linkContent), (x = _.pos)) - : (($ = ''), (x = L)); - x < C && 32 === _.src.charCodeAt(x); - - ) - x++; - return x < C && 10 !== _.src.charCodeAt(x) - ? -1 - : ((V = normalizeReference(s.slice(1, w))), - void 0 === u.references[V] && (u.references[V] = { title: $, href: B }), - x); - } - (Renderer.prototype.renderInline = function (s, o, i) { - for (var u = this.rules, _ = s.length, w = 0, x = ''; _--; ) - x += u[s[w].type](s, w++, o, i, this); - return x; - }), - (Renderer.prototype.render = function (s, o, i) { - for (var u = this.rules, _ = s.length, w = -1, x = ''; ++w < _; ) - 'inline' === s[w].type - ? (x += this.renderInline(s[w].children, o, i)) - : (x += u[s[w].type](s, w, o, i, this)); - return x; - }), - (Ruler.prototype.__find__ = function (s) { - for (var o = this.__rules__.length, i = -1; o--; ) - if (this.__rules__[++i].name === s) return i; - return -1; - }), - (Ruler.prototype.__compile__ = function () { - var s = this, - o = ['']; - s.__rules__.forEach(function (s) { - s.enabled && - s.alt.forEach(function (s) { - o.indexOf(s) < 0 && o.push(s); - }); - }), - (s.__cache__ = {}), - o.forEach(function (o) { - (s.__cache__[o] = []), - s.__rules__.forEach(function (i) { - i.enabled && ((o && i.alt.indexOf(o) < 0) || s.__cache__[o].push(i.fn)); - }); - }); - }), - (Ruler.prototype.at = function (s, o, i) { - var u = this.__find__(s), - _ = i || {}; - if (-1 === u) throw new Error('Parser rule not found: ' + s); - (this.__rules__[u].fn = o), - (this.__rules__[u].alt = _.alt || []), - (this.__cache__ = null); - }), - (Ruler.prototype.before = function (s, o, i, u) { - var _ = this.__find__(s), - w = u || {}; - if (-1 === _) throw new Error('Parser rule not found: ' + s); - this.__rules__.splice(_, 0, { name: o, enabled: !0, fn: i, alt: w.alt || [] }), - (this.__cache__ = null); - }), - (Ruler.prototype.after = function (s, o, i, u) { - var _ = this.__find__(s), - w = u || {}; - if (-1 === _) throw new Error('Parser rule not found: ' + s); - this.__rules__.splice(_ + 1, 0, { name: o, enabled: !0, fn: i, alt: w.alt || [] }), - (this.__cache__ = null); - }), - (Ruler.prototype.push = function (s, o, i) { - var u = i || {}; - this.__rules__.push({ name: s, enabled: !0, fn: o, alt: u.alt || [] }), - (this.__cache__ = null); - }), - (Ruler.prototype.enable = function (s, o) { - (s = Array.isArray(s) ? s : [s]), - o && - this.__rules__.forEach(function (s) { - s.enabled = !1; - }), - s.forEach(function (s) { - var o = this.__find__(s); - if (o < 0) throw new Error('Rules manager: invalid rule name ' + s); - this.__rules__[o].enabled = !0; - }, this), - (this.__cache__ = null); - }), - (Ruler.prototype.disable = function (s) { - (s = Array.isArray(s) ? s : [s]).forEach(function (s) { - var o = this.__find__(s); - if (o < 0) throw new Error('Rules manager: invalid rule name ' + s); - this.__rules__[o].enabled = !1; - }, this), - (this.__cache__ = null); - }), - (Ruler.prototype.getRules = function (s) { - return null === this.__cache__ && this.__compile__(), this.__cache__[s] || []; - }), - (StateInline.prototype.pushPending = function () { - this.tokens.push({ type: 'text', content: this.pending, level: this.pendingLevel }), - (this.pending = ''); - }), - (StateInline.prototype.push = function (s) { - this.pending && this.pushPending(), - this.tokens.push(s), - (this.pendingLevel = this.level); - }), - (StateInline.prototype.cacheSet = function (s, o) { - for (var i = this.cache.length; i <= s; i++) this.cache.push(0); - this.cache[s] = o; - }), - (StateInline.prototype.cacheGet = function (s) { - return s < this.cache.length ? this.cache[s] : 0; - }); - var pC = ' \n()[]\'".,!?-'; - function regEscape(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:#= s.length) && !yC.test(s[o]); - } - function replaceAt(s, o, i) { - return s.substr(0, o) + i + s.substr(o + 1); - } - var vC = [ - [ - 'block', - function block(s) { - s.inlineMode - ? s.tokens.push({ - type: 'inline', - content: s.src.replace(/\n/g, ' ').trim(), - level: 0, - lines: [0, 1], - children: [] - }) - : s.block.parse(s.src, s.options, s.env, s.tokens); - } - ], - [ - 'abbr', - function abbr(s) { - var o, - i, - u, - _, - w = s.tokens; - if (!s.inlineMode) - for (o = 1, i = w.length - 1; o < i; o++) - if ( - 'paragraph_open' === w[o - 1].type && - 'inline' === w[o].type && - 'paragraph_close' === w[o + 1].type - ) { - for ( - u = w[o].content; - u.length && !((_ = parseAbbr(u, s.inline, s.options, s.env)) < 0); - - ) - u = u.slice(_).trim(); - (w[o].content = u), u.length || ((w[o - 1].tight = !0), (w[o + 1].tight = !0)); - } - } - ], - [ - 'references', - function references(s) { - var o, - i, - u, - _, - w = s.tokens; - if (((s.env.references = s.env.references || {}), !s.inlineMode)) - for (o = 1, i = w.length - 1; o < i; o++) - if ( - 'inline' === w[o].type && - 'paragraph_open' === w[o - 1].type && - 'paragraph_close' === w[o + 1].type - ) { - for ( - u = w[o].content; - u.length && !((_ = parseReference(u, s.inline, s.options, s.env)) < 0); - - ) - u = u.slice(_).trim(); - (w[o].content = u), u.length || ((w[o - 1].tight = !0), (w[o + 1].tight = !0)); - } - } - ], - [ - 'inline', - function inline(s) { - var o, - i, - u, - _ = s.tokens; - for (i = 0, u = _.length; i < u; i++) - 'inline' === (o = _[i]).type && - s.inline.parse(o.content, s.options, s.env, o.children); - } - ], - [ - 'footnote_tail', - function footnote_block(s) { - var o, - i, - u, - _, - w, - x, - C, - j, - L, - B = 0, - $ = !1, - V = {}; - if ( - s.env.footnotes && - ((s.tokens = s.tokens.filter(function (s) { - return 'footnote_reference_open' === s.type - ? (($ = !0), (j = []), (L = s.label), !1) - : 'footnote_reference_close' === s.type - ? (($ = !1), (V[':' + L] = j), !1) - : ($ && j.push(s), !$); - })), - s.env.footnotes.list) - ) { - for ( - x = s.env.footnotes.list, - s.tokens.push({ type: 'footnote_block_open', level: B++ }), - o = 0, - i = x.length; - o < i; - o++ - ) { - for ( - s.tokens.push({ type: 'footnote_open', id: o, level: B++ }), - x[o].tokens - ? ((C = []).push({ type: 'paragraph_open', tight: !1, level: B++ }), - C.push({ type: 'inline', content: '', level: B, children: x[o].tokens }), - C.push({ type: 'paragraph_close', tight: !1, level: --B })) - : x[o].label && (C = V[':' + x[o].label]), - s.tokens = s.tokens.concat(C), - w = - 'paragraph_close' === s.tokens[s.tokens.length - 1].type - ? s.tokens.pop() - : null, - _ = x[o].count > 0 ? x[o].count : 1, - u = 0; - u < _; - u++ - ) - s.tokens.push({ type: 'footnote_anchor', id: o, subId: u, level: B }); - w && s.tokens.push(w), s.tokens.push({ type: 'footnote_close', level: --B }); - } - s.tokens.push({ type: 'footnote_block_close', level: --B }); - } - } - ], - [ - 'abbr2', - function abbr2(s) { - var o, - i, - u, - _, - w, - x, - C, - j, - L, - B, - $, - V, - U = s.tokens; - if (s.env.abbreviations) - for ( - s.env.abbrRegExp || - ((V = - '(^|[' + - pC.split('').map(regEscape).join('') + - '])(' + - Object.keys(s.env.abbreviations) - .map(function (s) { - return s.substr(1); - }) - .sort(function (s, o) { - return o.length - s.length; - }) - .map(regEscape) - .join('|') + - ')($|[' + - pC.split('').map(regEscape).join('') + - '])'), - (s.env.abbrRegExp = new RegExp(V, 'g'))), - B = s.env.abbrRegExp, - i = 0, - u = U.length; - i < u; - i++ - ) - if ('inline' === U[i].type) - for (o = (_ = U[i].children).length - 1; o >= 0; o--) - if ('text' === (w = _[o]).type) { - for ( - j = 0, x = w.content, B.lastIndex = 0, L = w.level, C = []; - ($ = B.exec(x)); - - ) - B.lastIndex > j && - C.push({ - type: 'text', - content: x.slice(j, $.index + $[1].length), - level: L - }), - C.push({ - type: 'abbr_open', - title: s.env.abbreviations[':' + $[2]], - level: L++ - }), - C.push({ type: 'text', content: $[2], level: L }), - C.push({ type: 'abbr_close', level: --L }), - (j = B.lastIndex - $[3].length); - C.length && - (j < x.length && C.push({ type: 'text', content: x.slice(j), level: L }), - (U[i].children = _ = [].concat(_.slice(0, o), C, _.slice(o + 1)))); - } - } - ], - [ - 'replacements', - function index_browser_replace(s) { - var o, i, u, _, w; - if (s.options.typographer) - for (w = s.tokens.length - 1; w >= 0; w--) - if ('inline' === s.tokens[w].type) - for (o = (_ = s.tokens[w].children).length - 1; o >= 0; o--) - 'text' === (i = _[o]).type && - ((u = replaceScopedAbbr((u = i.content))), - hC.test(u) && - (u = u - .replace(/\+-/g, '±') - .replace(/\.{2,}/g, '…') - .replace(/([?!])…/g, '$1..') - .replace(/([?!]){4,}/g, '$1$1$1') - .replace(/,{2,}/g, ',') - .replace(/(^|[^-])---([^-]|$)/gm, '$1—$2') - .replace(/(^|\s)--(\s|$)/gm, '$1–$2') - .replace(/(^|[^-\s])--([^-\s]|$)/gm, '$1–$2')), - (i.content = u)); - } - ], - [ - 'smartquotes', - function smartquotes(s) { - var o, i, u, _, w, x, C, j, L, B, $, V, U, z, Y, Z, ee; - if (s.options.typographer) - for (ee = [], Y = s.tokens.length - 1; Y >= 0; Y--) - if ('inline' === s.tokens[Y].type) - for (Z = s.tokens[Y].children, ee.length = 0, o = 0; o < Z.length; o++) - if ('text' === (i = Z[o]).type && !mC.test(i.text)) { - for (C = Z[o].level, U = ee.length - 1; U >= 0 && !(ee[U].level <= C); U--); - (ee.length = U + 1), (w = 0), (x = (u = i.content).length); - e: for (; w < x && ((gC.lastIndex = w), (_ = gC.exec(u))); ) - if ( - ((j = !isLetter(u, _.index - 1)), - (w = _.index + 1), - (z = "'" === _[0]), - (L = !isLetter(u, w)) || j) - ) { - if ((($ = !L), (V = !j))) - for ( - U = ee.length - 1; - U >= 0 && ((B = ee[U]), !(ee[U].level < C)); - U-- - ) - if (B.single === z && ee[U].level === C) { - (B = ee[U]), - z - ? ((Z[B.token].content = replaceAt( - Z[B.token].content, - B.pos, - s.options.quotes[2] - )), - (i.content = replaceAt( - i.content, - _.index, - s.options.quotes[3] - ))) - : ((Z[B.token].content = replaceAt( - Z[B.token].content, - B.pos, - s.options.quotes[0] - )), - (i.content = replaceAt( - i.content, - _.index, - s.options.quotes[1] - ))), - (ee.length = U); - continue e; - } - $ - ? ee.push({ token: o, pos: _.index, single: z, level: C }) - : V && z && (i.content = replaceAt(i.content, _.index, '’')); - } else z && (i.content = replaceAt(i.content, _.index, '’')); - } - } - ] - ]; - function Core() { - (this.options = {}), (this.ruler = new Ruler()); - for (var s = 0; s < vC.length; s++) this.ruler.push(vC[s][0], vC[s][1]); - } - function StateBlock(s, o, i, u, _) { - var w, x, C, j, L, B, $; - for ( - this.src = s, - this.parser = o, - this.options = i, - this.env = u, - this.tokens = _, - this.bMarks = [], - this.eMarks = [], - this.tShift = [], - this.blkIndent = 0, - this.line = 0, - this.lineMax = 0, - this.tight = !1, - this.parentType = 'root', - this.ddIndent = -1, - this.level = 0, - this.result = '', - B = 0, - $ = !1, - C = j = B = 0, - L = (x = this.src).length; - j < L; - j++ - ) { - if (((w = x.charCodeAt(j)), !$)) { - if (32 === w) { - B++; - continue; - } - $ = !0; - } - (10 !== w && j !== L - 1) || - (10 !== w && j++, - this.bMarks.push(C), - this.eMarks.push(j), - this.tShift.push(B), - ($ = !1), - (B = 0), - (C = j + 1)); - } - this.bMarks.push(x.length), - this.eMarks.push(x.length), - this.tShift.push(0), - (this.lineMax = this.bMarks.length - 1); - } - function skipBulletListMarker(s, o) { - var i, u, _; - return (u = s.bMarks[o] + s.tShift[o]) >= (_ = s.eMarks[o]) || - (42 !== (i = s.src.charCodeAt(u++)) && 45 !== i && 43 !== i) || - (u < _ && 32 !== s.src.charCodeAt(u)) - ? -1 - : u; - } - function skipOrderedListMarker(s, o) { - var i, - u = s.bMarks[o] + s.tShift[o], - _ = s.eMarks[o]; - if (u + 1 >= _) return -1; - if ((i = s.src.charCodeAt(u++)) < 48 || i > 57) return -1; - for (;;) { - if (u >= _) return -1; - if (!((i = s.src.charCodeAt(u++)) >= 48 && i <= 57)) { - if (41 === i || 46 === i) break; - return -1; - } - } - return u < _ && 32 !== s.src.charCodeAt(u) ? -1 : u; - } - (Core.prototype.process = function (s) { - var o, i, u; - for (o = 0, i = (u = this.ruler.getRules('')).length; o < i; o++) u[o](s); - }), - (StateBlock.prototype.isEmpty = function isEmpty(s) { - return this.bMarks[s] + this.tShift[s] >= this.eMarks[s]; - }), - (StateBlock.prototype.skipEmptyLines = function skipEmptyLines(s) { - for ( - var o = this.lineMax; - s < o && !(this.bMarks[s] + this.tShift[s] < this.eMarks[s]); - s++ - ); - return s; - }), - (StateBlock.prototype.skipSpaces = function skipSpaces(s) { - for (var o = this.src.length; s < o && 32 === this.src.charCodeAt(s); s++); - return s; - }), - (StateBlock.prototype.skipChars = function skipChars(s, o) { - for (var i = this.src.length; s < i && this.src.charCodeAt(s) === o; s++); - return s; - }), - (StateBlock.prototype.skipCharsBack = function skipCharsBack(s, o, i) { - if (s <= i) return s; - for (; s > i; ) if (o !== this.src.charCodeAt(--s)) return s + 1; - return s; - }), - (StateBlock.prototype.getLines = function getLines(s, o, i, u) { - var _, - w, - x, - C, - j, - L = s; - if (s >= o) return ''; - if (L + 1 === o) - return ( - (w = this.bMarks[L] + Math.min(this.tShift[L], i)), - (x = u ? this.eMarks[L] + 1 : this.eMarks[L]), - this.src.slice(w, x) - ); - for (C = new Array(o - s), _ = 0; L < o; L++, _++) - (j = this.tShift[L]) > i && (j = i), - j < 0 && (j = 0), - (w = this.bMarks[L] + j), - (x = L + 1 < o || u ? this.eMarks[L] + 1 : this.eMarks[L]), - (C[_] = this.src.slice(w, x)); - return C.join(''); - }); - var bC = {}; - [ - 'article', - 'aside', - 'button', - 'blockquote', - 'body', - 'canvas', - 'caption', - 'col', - 'colgroup', - 'dd', - 'div', - 'dl', - 'dt', - 'embed', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'header', - 'hgroup', - 'hr', - 'iframe', - 'li', - 'map', - 'object', - 'ol', - 'output', - 'p', - 'pre', - 'progress', - 'script', - 'section', - 'style', - 'table', - 'tbody', - 'td', - 'textarea', - 'tfoot', - 'th', - 'tr', - 'thead', - 'ul', - 'video' - ].forEach(function (s) { - bC[s] = !0; - }); - var _C = /^<([a-zA-Z]{1,15})[\s\/>]/, - EC = /^<\/([a-zA-Z]{1,15})[\s>]/; - function index_browser_getLine(s, o) { - var i = s.bMarks[o] + s.blkIndent, - u = s.eMarks[o]; - return s.src.substr(i, u - i); - } - function skipMarker(s, o) { - var i, - u, - _ = s.bMarks[o] + s.tShift[o], - w = s.eMarks[o]; - return _ >= w || - (126 !== (u = s.src.charCodeAt(_++)) && 58 !== u) || - _ === (i = s.skipSpaces(_)) || - i >= w - ? -1 - : i; - } - var wC = [ - [ - 'code', - function code(s, o, i) { - var u, _; - if (s.tShift[o] - s.blkIndent < 4) return !1; - for (_ = u = o + 1; u < i; ) - if (s.isEmpty(u)) u++; - else { - if (!(s.tShift[u] - s.blkIndent >= 4)) break; - _ = ++u; - } - return ( - (s.line = u), - s.tokens.push({ - type: 'code', - content: s.getLines(o, _, 4 + s.blkIndent, !0), - block: !0, - lines: [o, s.line], - level: s.level - }), - !0 - ); - } - ], - [ - 'fences', - function fences(s, o, i, u) { - var _, - w, - x, - C, - j, - L = !1, - B = s.bMarks[o] + s.tShift[o], - $ = s.eMarks[o]; - if (B + 3 > $) return !1; - if (126 !== (_ = s.src.charCodeAt(B)) && 96 !== _) return !1; - if (((j = B), (w = (B = s.skipChars(B, _)) - j) < 3)) return !1; - if ((x = s.src.slice(B, $).trim()).indexOf('`') >= 0) return !1; - if (u) return !0; - for ( - C = o; - !(++C >= i) && - !( - (B = j = s.bMarks[C] + s.tShift[C]) < ($ = s.eMarks[C]) && - s.tShift[C] < s.blkIndent - ); - - ) - if ( - s.src.charCodeAt(B) === _ && - !( - s.tShift[C] - s.blkIndent >= 4 || - (B = s.skipChars(B, _)) - j < w || - (B = s.skipSpaces(B)) < $ - ) - ) { - L = !0; - break; - } - return ( - (w = s.tShift[o]), - (s.line = C + (L ? 1 : 0)), - s.tokens.push({ - type: 'fence', - params: x, - content: s.getLines(o + 1, C, w, !0), - lines: [o, s.line], - level: s.level - }), - !0 - ); - }, - ['paragraph', 'blockquote', 'list'] - ], - [ - 'blockquote', - function blockquote(s, o, i, u) { - var _, - w, - x, - C, - j, - L, - B, - $, - V, - U, - z, - Y = s.bMarks[o] + s.tShift[o], - Z = s.eMarks[o]; - if (Y > Z) return !1; - if (62 !== s.src.charCodeAt(Y++)) return !1; - if (s.level >= s.options.maxNesting) return !1; - if (u) return !0; - for ( - 32 === s.src.charCodeAt(Y) && Y++, - j = s.blkIndent, - s.blkIndent = 0, - C = [s.bMarks[o]], - s.bMarks[o] = Y, - w = (Y = Y < Z ? s.skipSpaces(Y) : Y) >= Z, - x = [s.tShift[o]], - s.tShift[o] = Y - s.bMarks[o], - $ = s.parser.ruler.getRules('blockquote'), - _ = o + 1; - _ < i && !((Y = s.bMarks[_] + s.tShift[_]) >= (Z = s.eMarks[_])); - _++ - ) - if (62 !== s.src.charCodeAt(Y++)) { - if (w) break; - for (z = !1, V = 0, U = $.length; V < U; V++) - if ($[V](s, _, i, !0)) { - z = !0; - break; - } - if (z) break; - C.push(s.bMarks[_]), x.push(s.tShift[_]), (s.tShift[_] = -1337); - } else - 32 === s.src.charCodeAt(Y) && Y++, - C.push(s.bMarks[_]), - (s.bMarks[_] = Y), - (w = (Y = Y < Z ? s.skipSpaces(Y) : Y) >= Z), - x.push(s.tShift[_]), - (s.tShift[_] = Y - s.bMarks[_]); - for ( - L = s.parentType, - s.parentType = 'blockquote', - s.tokens.push({ type: 'blockquote_open', lines: (B = [o, 0]), level: s.level++ }), - s.parser.tokenize(s, o, _), - s.tokens.push({ type: 'blockquote_close', level: --s.level }), - s.parentType = L, - B[1] = s.line, - V = 0; - V < x.length; - V++ - ) - (s.bMarks[V + o] = C[V]), (s.tShift[V + o] = x[V]); - return (s.blkIndent = j), !0; - }, - ['paragraph', 'blockquote', 'list'] - ], - [ - 'hr', - function hr(s, o, i, u) { - var _, - w, - x, - C = s.bMarks[o], - j = s.eMarks[o]; - if ((C += s.tShift[o]) > j) return !1; - if (42 !== (_ = s.src.charCodeAt(C++)) && 45 !== _ && 95 !== _) return !1; - for (w = 1; C < j; ) { - if ((x = s.src.charCodeAt(C++)) !== _ && 32 !== x) return !1; - x === _ && w++; - } - return ( - !(w < 3) && - (u || - ((s.line = o + 1), - s.tokens.push({ type: 'hr', lines: [o, s.line], level: s.level })), - !0) - ); - }, - ['paragraph', 'blockquote', 'list'] - ], - [ - 'list', - function index_browser_list(s, o, i, u) { - var _, - w, - x, - C, - j, - L, - B, - $, - V, - U, - z, - Y, - Z, - ee, - ie, - ae, - le, - ce, - pe, - de, - fe, - ye = !0; - if (($ = skipOrderedListMarker(s, o)) >= 0) Y = !0; - else { - if (!(($ = skipBulletListMarker(s, o)) >= 0)) return !1; - Y = !1; - } - if (s.level >= s.options.maxNesting) return !1; - if (((z = s.src.charCodeAt($ - 1)), u)) return !0; - for ( - ee = s.tokens.length, - Y - ? ((B = s.bMarks[o] + s.tShift[o]), - (U = Number(s.src.substr(B, $ - B - 1))), - s.tokens.push({ - type: 'ordered_list_open', - order: U, - lines: (ae = [o, 0]), - level: s.level++ - })) - : s.tokens.push({ - type: 'bullet_list_open', - lines: (ae = [o, 0]), - level: s.level++ - }), - _ = o, - ie = !1, - ce = s.parser.ruler.getRules('list'); - !( - !(_ < i) || - ((V = (Z = s.skipSpaces($)) >= s.eMarks[_] ? 1 : Z - $) > 4 && (V = 1), - V < 1 && (V = 1), - (w = $ - s.bMarks[_] + V), - s.tokens.push({ type: 'list_item_open', lines: (le = [o, 0]), level: s.level++ }), - (C = s.blkIndent), - (j = s.tight), - (x = s.tShift[o]), - (L = s.parentType), - (s.tShift[o] = Z - s.bMarks[o]), - (s.blkIndent = w), - (s.tight = !0), - (s.parentType = 'list'), - s.parser.tokenize(s, o, i, !0), - (s.tight && !ie) || (ye = !1), - (ie = s.line - o > 1 && s.isEmpty(s.line - 1)), - (s.blkIndent = C), - (s.tShift[o] = x), - (s.tight = j), - (s.parentType = L), - s.tokens.push({ type: 'list_item_close', level: --s.level }), - (_ = o = s.line), - (le[1] = _), - (Z = s.bMarks[o]), - _ >= i) || - s.isEmpty(_) || - s.tShift[_] < s.blkIndent - ); - - ) { - for (fe = !1, pe = 0, de = ce.length; pe < de; pe++) - if (ce[pe](s, _, i, !0)) { - fe = !0; - break; - } - if (fe) break; - if (Y) { - if (($ = skipOrderedListMarker(s, _)) < 0) break; - } else if (($ = skipBulletListMarker(s, _)) < 0) break; - if (z !== s.src.charCodeAt($ - 1)) break; - } - return ( - s.tokens.push({ - type: Y ? 'ordered_list_close' : 'bullet_list_close', - level: --s.level - }), - (ae[1] = _), - (s.line = _), - ye && - (function markTightParagraphs(s, o) { - var i, - u, - _ = s.level + 2; - for (i = o + 2, u = s.tokens.length - 2; i < u; i++) - s.tokens[i].level === _ && - 'paragraph_open' === s.tokens[i].type && - ((s.tokens[i + 2].tight = !0), (s.tokens[i].tight = !0), (i += 2)); - })(s, ee), - !0 - ); - }, - ['paragraph', 'blockquote'] - ], - [ - 'footnote', - function footnote(s, o, i, u) { - var _, - w, - x, - C, - j, - L = s.bMarks[o] + s.tShift[o], - B = s.eMarks[o]; - if (L + 4 > B) return !1; - if (91 !== s.src.charCodeAt(L)) return !1; - if (94 !== s.src.charCodeAt(L + 1)) return !1; - if (s.level >= s.options.maxNesting) return !1; - for (C = L + 2; C < B; C++) { - if (32 === s.src.charCodeAt(C)) return !1; - if (93 === s.src.charCodeAt(C)) break; - } - return ( - C !== L + 2 && - !(C + 1 >= B || 58 !== s.src.charCodeAt(++C)) && - (u || - (C++, - s.env.footnotes || (s.env.footnotes = {}), - s.env.footnotes.refs || (s.env.footnotes.refs = {}), - (j = s.src.slice(L + 2, C - 2)), - (s.env.footnotes.refs[':' + j] = -1), - s.tokens.push({ type: 'footnote_reference_open', label: j, level: s.level++ }), - (_ = s.bMarks[o]), - (w = s.tShift[o]), - (x = s.parentType), - (s.tShift[o] = s.skipSpaces(C) - C), - (s.bMarks[o] = C), - (s.blkIndent += 4), - (s.parentType = 'footnote'), - s.tShift[o] < s.blkIndent && - ((s.tShift[o] += s.blkIndent), (s.bMarks[o] -= s.blkIndent)), - s.parser.tokenize(s, o, i, !0), - (s.parentType = x), - (s.blkIndent -= 4), - (s.tShift[o] = w), - (s.bMarks[o] = _), - s.tokens.push({ type: 'footnote_reference_close', level: --s.level })), - !0) - ); - }, - ['paragraph'] - ], - [ - 'heading', - function heading(s, o, i, u) { - var _, - w, - x, - C = s.bMarks[o] + s.tShift[o], - j = s.eMarks[o]; - if (C >= j) return !1; - if (35 !== (_ = s.src.charCodeAt(C)) || C >= j) return !1; - for (w = 1, _ = s.src.charCodeAt(++C); 35 === _ && C < j && w <= 6; ) - w++, (_ = s.src.charCodeAt(++C)); - return ( - !(w > 6 || (C < j && 32 !== _)) && - (u || - ((j = s.skipCharsBack(j, 32, C)), - (x = s.skipCharsBack(j, 35, C)) > C && 32 === s.src.charCodeAt(x - 1) && (j = x), - (s.line = o + 1), - s.tokens.push({ - type: 'heading_open', - hLevel: w, - lines: [o, s.line], - level: s.level - }), - C < j && - s.tokens.push({ - type: 'inline', - content: s.src.slice(C, j).trim(), - level: s.level + 1, - lines: [o, s.line], - children: [] - }), - s.tokens.push({ type: 'heading_close', hLevel: w, level: s.level })), - !0) - ); - }, - ['paragraph', 'blockquote'] - ], - [ - 'lheading', - function lheading(s, o, i) { - var u, - _, - w, - x = o + 1; - return ( - !(x >= i) && - !(s.tShift[x] < s.blkIndent) && - !(s.tShift[x] - s.blkIndent > 3) && - !((_ = s.bMarks[x] + s.tShift[x]) >= (w = s.eMarks[x])) && - (45 === (u = s.src.charCodeAt(_)) || 61 === u) && - ((_ = s.skipChars(_, u)), - !((_ = s.skipSpaces(_)) < w) && - ((_ = s.bMarks[o] + s.tShift[o]), - (s.line = x + 1), - s.tokens.push({ - type: 'heading_open', - hLevel: 61 === u ? 1 : 2, - lines: [o, s.line], - level: s.level - }), - s.tokens.push({ - type: 'inline', - content: s.src.slice(_, s.eMarks[o]).trim(), - level: s.level + 1, - lines: [o, s.line - 1], - children: [] - }), - s.tokens.push({ - type: 'heading_close', - hLevel: 61 === u ? 1 : 2, - level: s.level - }), - !0)) - ); - } - ], - [ - 'htmlblock', - function htmlblock(s, o, i, u) { - var _, - w, - x, - C = s.bMarks[o], - j = s.eMarks[o], - L = s.tShift[o]; - if (((C += L), !s.options.html)) return !1; - if (L > 3 || C + 2 >= j) return !1; - if (60 !== s.src.charCodeAt(C)) return !1; - if (33 === (_ = s.src.charCodeAt(C + 1)) || 63 === _) { - if (u) return !0; - } else { - if ( - 47 !== _ && - !(function isLetter$1(s) { - var o = 32 | s; - return o >= 97 && o <= 122; - })(_) - ) - return !1; - if (47 === _) { - if (!(w = s.src.slice(C, j).match(EC))) return !1; - } else if (!(w = s.src.slice(C, j).match(_C))) return !1; - if (!0 !== bC[w[1].toLowerCase()]) return !1; - if (u) return !0; - } - for (x = o + 1; x < s.lineMax && !s.isEmpty(x); ) x++; - return ( - (s.line = x), - s.tokens.push({ - type: 'htmlblock', - level: s.level, - lines: [o, s.line], - content: s.getLines(o, x, 0, !0) - }), - !0 - ); - }, - ['paragraph', 'blockquote'] - ], - [ - 'table', - function table(s, o, i, u) { - var _, w, x, C, j, L, B, $, V, U, z; - if (o + 2 > i) return !1; - if (((j = o + 1), s.tShift[j] < s.blkIndent)) return !1; - if ((x = s.bMarks[j] + s.tShift[j]) >= s.eMarks[j]) return !1; - if (124 !== (_ = s.src.charCodeAt(x)) && 45 !== _ && 58 !== _) return !1; - if (((w = index_browser_getLine(s, o + 1)), !/^[-:| ]+$/.test(w))) return !1; - if ((L = w.split('|')) <= 2) return !1; - for ($ = [], C = 0; C < L.length; C++) { - if (!(V = L[C].trim())) { - if (0 === C || C === L.length - 1) continue; - return !1; - } - if (!/^:?-+:?$/.test(V)) return !1; - 58 === V.charCodeAt(V.length - 1) - ? $.push(58 === V.charCodeAt(0) ? 'center' : 'right') - : 58 === V.charCodeAt(0) - ? $.push('left') - : $.push(''); - } - if (-1 === (w = index_browser_getLine(s, o).trim()).indexOf('|')) return !1; - if (((L = w.replace(/^\||\|$/g, '').split('|')), $.length !== L.length)) return !1; - if (u) return !0; - for ( - s.tokens.push({ type: 'table_open', lines: (U = [o, 0]), level: s.level++ }), - s.tokens.push({ type: 'thead_open', lines: [o, o + 1], level: s.level++ }), - s.tokens.push({ type: 'tr_open', lines: [o, o + 1], level: s.level++ }), - C = 0; - C < L.length; - C++ - ) - s.tokens.push({ - type: 'th_open', - align: $[C], - lines: [o, o + 1], - level: s.level++ - }), - s.tokens.push({ - type: 'inline', - content: L[C].trim(), - lines: [o, o + 1], - level: s.level, - children: [] - }), - s.tokens.push({ type: 'th_close', level: --s.level }); - for ( - s.tokens.push({ type: 'tr_close', level: --s.level }), - s.tokens.push({ type: 'thead_close', level: --s.level }), - s.tokens.push({ type: 'tbody_open', lines: (z = [o + 2, 0]), level: s.level++ }), - j = o + 2; - j < i && - !(s.tShift[j] < s.blkIndent) && - -1 !== (w = index_browser_getLine(s, j).trim()).indexOf('|'); - j++ - ) { - for ( - L = w.replace(/^\||\|$/g, '').split('|'), - s.tokens.push({ type: 'tr_open', level: s.level++ }), - C = 0; - C < L.length; - C++ - ) - s.tokens.push({ type: 'td_open', align: $[C], level: s.level++ }), - (B = L[C].substring( - 124 === L[C].charCodeAt(0) ? 1 : 0, - 124 === L[C].charCodeAt(L[C].length - 1) ? L[C].length - 1 : L[C].length - ).trim()), - s.tokens.push({ type: 'inline', content: B, level: s.level, children: [] }), - s.tokens.push({ type: 'td_close', level: --s.level }); - s.tokens.push({ type: 'tr_close', level: --s.level }); - } - return ( - s.tokens.push({ type: 'tbody_close', level: --s.level }), - s.tokens.push({ type: 'table_close', level: --s.level }), - (U[1] = z[1] = j), - (s.line = j), - !0 - ); - }, - ['paragraph'] - ], - [ - 'deflist', - function deflist(s, o, i, u) { - var _, w, x, C, j, L, B, $, V, U, z, Y, Z, ee; - if (u) return !(s.ddIndent < 0) && skipMarker(s, o) >= 0; - if (((B = o + 1), s.isEmpty(B) && ++B > i)) return !1; - if (s.tShift[B] < s.blkIndent) return !1; - if ((_ = skipMarker(s, B)) < 0) return !1; - if (s.level >= s.options.maxNesting) return !1; - (L = s.tokens.length), - s.tokens.push({ type: 'dl_open', lines: (j = [o, 0]), level: s.level++ }), - (x = o), - (w = B); - e: for (;;) { - for ( - ee = !0, - Z = !1, - s.tokens.push({ type: 'dt_open', lines: [x, x], level: s.level++ }), - s.tokens.push({ - type: 'inline', - content: s.getLines(x, x + 1, s.blkIndent, !1).trim(), - level: s.level + 1, - lines: [x, x], - children: [] - }), - s.tokens.push({ type: 'dt_close', level: --s.level }); - ; - - ) { - if ( - (s.tokens.push({ type: 'dd_open', lines: (C = [B, 0]), level: s.level++ }), - (Y = s.tight), - (V = s.ddIndent), - ($ = s.blkIndent), - (z = s.tShift[w]), - (U = s.parentType), - (s.blkIndent = s.ddIndent = s.tShift[w] + 2), - (s.tShift[w] = _ - s.bMarks[w]), - (s.tight = !0), - (s.parentType = 'deflist'), - s.parser.tokenize(s, w, i, !0), - (s.tight && !Z) || (ee = !1), - (Z = s.line - w > 1 && s.isEmpty(s.line - 1)), - (s.tShift[w] = z), - (s.tight = Y), - (s.parentType = U), - (s.blkIndent = $), - (s.ddIndent = V), - s.tokens.push({ type: 'dd_close', level: --s.level }), - (C[1] = B = s.line), - B >= i) - ) - break e; - if (s.tShift[B] < s.blkIndent) break e; - if ((_ = skipMarker(s, B)) < 0) break; - w = B; - } - if (B >= i) break; - if (((x = B), s.isEmpty(x))) break; - if (s.tShift[x] < s.blkIndent) break; - if ((w = x + 1) >= i) break; - if ((s.isEmpty(w) && w++, w >= i)) break; - if (s.tShift[w] < s.blkIndent) break; - if ((_ = skipMarker(s, w)) < 0) break; - } - return ( - s.tokens.push({ type: 'dl_close', level: --s.level }), - (j[1] = B), - (s.line = B), - ee && - (function markTightParagraphs$1(s, o) { - var i, - u, - _ = s.level + 2; - for (i = o + 2, u = s.tokens.length - 2; i < u; i++) - s.tokens[i].level === _ && - 'paragraph_open' === s.tokens[i].type && - ((s.tokens[i + 2].tight = !0), (s.tokens[i].tight = !0), (i += 2)); - })(s, L), - !0 - ); - }, - ['paragraph'] - ], - [ - 'paragraph', - function paragraph(s, o) { - var i, - u, - _, - w, - x, - C, - j = o + 1; - if (j < (i = s.lineMax) && !s.isEmpty(j)) - for (C = s.parser.ruler.getRules('paragraph'); j < i && !s.isEmpty(j); j++) - if (!(s.tShift[j] - s.blkIndent > 3)) { - for (_ = !1, w = 0, x = C.length; w < x; w++) - if (C[w](s, j, i, !0)) { - _ = !0; - break; - } - if (_) break; - } - return ( - (u = s.getLines(o, j, s.blkIndent, !1).trim()), - (s.line = j), - u.length && - (s.tokens.push({ - type: 'paragraph_open', - tight: !1, - lines: [o, s.line], - level: s.level - }), - s.tokens.push({ - type: 'inline', - content: u, - level: s.level + 1, - lines: [o, s.line], - children: [] - }), - s.tokens.push({ type: 'paragraph_close', tight: !1, level: s.level })), - !0 - ); - } - ] - ]; - function ParserBlock() { - this.ruler = new Ruler(); - for (var s = 0; s < wC.length; s++) - this.ruler.push(wC[s][0], wC[s][1], { alt: (wC[s][2] || []).slice() }); - } - ParserBlock.prototype.tokenize = function (s, o, i) { - for ( - var u, _ = this.ruler.getRules(''), w = _.length, x = o, C = !1; - x < i && - ((s.line = x = s.skipEmptyLines(x)), !(x >= i)) && - !(s.tShift[x] < s.blkIndent); - - ) { - for (u = 0; u < w && !_[u](s, x, i, !1); u++); - if ( - ((s.tight = !C), s.isEmpty(s.line - 1) && (C = !0), (x = s.line) < i && s.isEmpty(x)) - ) { - if (((C = !0), ++x < i && 'list' === s.parentType && s.isEmpty(x))) break; - s.line = x; - } - } - }; - var SC = /[\n\t]/g, - xC = /\r[\n\u0085]|[\u2424\u2028\u0085]/g, - kC = /\u00a0/g; - function isTerminatorChar(s) { - switch (s) { - case 10: - case 92: - case 96: - case 42: - case 95: - case 94: - case 91: - case 93: - case 33: - case 38: - case 60: - case 62: - case 123: - case 125: - case 36: - case 37: - case 64: - case 126: - case 43: - case 61: - case 58: - return !0; - default: - return !1; - } - } - ParserBlock.prototype.parse = function (s, o, i, u) { - var _, - w = 0, - x = 0; - if (!s) return []; - (s = (s = s.replace(kC, ' ')).replace(xC, '\n')).indexOf('\t') >= 0 && - (s = s.replace(SC, function (o, i) { - var u; - return 10 === s.charCodeAt(i) - ? ((w = i + 1), (x = 0), o) - : ((u = ' '.slice((i - w - x) % 4)), (x = i - w + 1), u); - })), - (_ = new StateBlock(s, this, o, i, u)), - this.tokenize(_, _.line, _.lineMax); - }; - for (var CC = [], OC = 0; OC < 256; OC++) CC.push(0); - function isAlphaNum(s) { - return (s >= 48 && s <= 57) || (s >= 65 && s <= 90) || (s >= 97 && s <= 122); - } - function scanDelims(s, o) { - var i, - u, - _, - w = o, - x = !0, - C = !0, - j = s.posMax, - L = s.src.charCodeAt(o); - for (i = o > 0 ? s.src.charCodeAt(o - 1) : -1; w < j && s.src.charCodeAt(w) === L; ) w++; - return ( - w >= j && (x = !1), - (_ = w - o) >= 4 - ? (x = C = !1) - : ((32 !== (u = w < j ? s.src.charCodeAt(w) : -1) && 10 !== u) || (x = !1), - (32 !== i && 10 !== i) || (C = !1), - 95 === L && (isAlphaNum(i) && (x = !1), isAlphaNum(u) && (C = !1))), - { can_open: x, can_close: C, delims: _ } - ); - } - '\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-'.split('').forEach(function (s) { - CC[s.charCodeAt(0)] = 1; - }); - var AC = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; - var jC = /\\([ \\!"#$%&'()*+,.\/:;<=>?@[\]^_`{|}~-])/g; - var IC = [ - 'coap', - 'doi', - 'javascript', - 'aaa', - 'aaas', - 'about', - 'acap', - 'cap', - 'cid', - 'crid', - 'data', - 'dav', - 'dict', - 'dns', - 'file', - 'ftp', - 'geo', - 'go', - 'gopher', - 'h323', - 'http', - 'https', - 'iax', - 'icap', - 'im', - 'imap', - 'info', - 'ipp', - 'iris', - 'iris.beep', - 'iris.xpc', - 'iris.xpcs', - 'iris.lwz', - 'ldap', - 'mailto', - 'mid', - 'msrp', - 'msrps', - 'mtqp', - 'mupdate', - 'news', - 'nfs', - 'ni', - 'nih', - 'nntp', - 'opaquelocktoken', - 'pop', - 'pres', - 'rtsp', - 'service', - 'session', - 'shttp', - 'sieve', - 'sip', - 'sips', - 'sms', - 'snmp', - 'soap.beep', - 'soap.beeps', - 'tag', - 'tel', - 'telnet', - 'tftp', - 'thismessage', - 'tn3270', - 'tip', - 'tv', - 'urn', - 'vemmi', - 'ws', - 'wss', - 'xcon', - 'xcon-userid', - 'xmlrpc.beep', - 'xmlrpc.beeps', - 'xmpp', - 'z39.50r', - 'z39.50s', - 'adiumxtra', - 'afp', - 'afs', - 'aim', - 'apt', - 'attachment', - 'aw', - 'beshare', - 'bitcoin', - 'bolo', - 'callto', - 'chrome', - 'chrome-extension', - 'com-eventbrite-attendee', - 'content', - 'cvs', - 'dlna-playsingle', - 'dlna-playcontainer', - 'dtn', - 'dvb', - 'ed2k', - 'facetime', - 'feed', - 'finger', - 'fish', - 'gg', - 'git', - 'gizmoproject', - 'gtalk', - 'hcp', - 'icon', - 'ipn', - 'irc', - 'irc6', - 'ircs', - 'itms', - 'jar', - 'jms', - 'keyparc', - 'lastfm', - 'ldaps', - 'magnet', - 'maps', - 'market', - 'message', - 'mms', - 'ms-help', - 'msnim', - 'mumble', - 'mvn', - 'notes', - 'oid', - 'palm', - 'paparazzi', - 'platform', - 'proxy', - 'psyc', - 'query', - 'res', - 'resource', - 'rmi', - 'rsync', - 'rtmp', - 'secondlife', - 'sftp', - 'sgn', - 'skype', - 'smb', - 'soldat', - 'spotify', - 'ssh', - 'steam', - 'svn', - 'teamspeak', - 'things', - 'udp', - 'unreal', - 'ut2004', - 'ventrilo', - 'view-source', - 'webcal', - 'wtai', - 'wyciwyg', - 'xfire', - 'xri', - 'ymsgr' - ], - PC = - /^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/, - MC = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/; - function replace$1(s, o) { - return ( - (s = s.source), - (o = o || ''), - function self(i, u) { - return i ? ((u = u.source || u), (s = s.replace(i, u)), self) : new RegExp(s, o); - } - ); - } - var TC = replace$1(/(?:unquoted|single_quoted|double_quoted)/)( - 'unquoted', - /[^"'=<>`\x00-\x20]+/ - )('single_quoted', /'[^']*'/)('double_quoted', /"[^"]*"/)(), - NC = replace$1(/(?:\s+attr_name(?:\s*=\s*attr_value)?)/)( - 'attr_name', - /[a-zA-Z_:][a-zA-Z0-9:._-]*/ - )('attr_value', TC)(), - RC = replace$1(/<[A-Za-z][A-Za-z0-9]*attribute*\s*\/?>/)('attribute', NC)(), - DC = replace$1(/^(?:open_tag|close_tag|comment|processing|declaration|cdata)/)( - 'open_tag', - RC - )('close_tag', /<\/[A-Za-z][A-Za-z0-9]*\s*>/)( - 'comment', - /|/ - )('processing', /<[?].*?[?]>/)('declaration', /]*>/)( - 'cdata', - // - )(); - var LC = /^&#((?:x[a-f0-9]{1,8}|[0-9]{1,8}));/i, - BC = /^&([a-z][a-z0-9]{1,31});/i; - var FC = [ - [ - 'text', - function index_browser_text(s, o) { - for (var i = s.pos; i < s.posMax && !isTerminatorChar(s.src.charCodeAt(i)); ) i++; - return i !== s.pos && (o || (s.pending += s.src.slice(s.pos, i)), (s.pos = i), !0); - } - ], - [ - 'newline', - function newline(s, o) { - var i, - u, - _ = s.pos; - if (10 !== s.src.charCodeAt(_)) return !1; - if (((i = s.pending.length - 1), (u = s.posMax), !o)) - if (i >= 0 && 32 === s.pending.charCodeAt(i)) - if (i >= 1 && 32 === s.pending.charCodeAt(i - 1)) { - for (var w = i - 2; w >= 0; w--) - if (32 !== s.pending.charCodeAt(w)) { - s.pending = s.pending.substring(0, w + 1); - break; - } - s.push({ type: 'hardbreak', level: s.level }); - } else - (s.pending = s.pending.slice(0, -1)), - s.push({ type: 'softbreak', level: s.level }); - else s.push({ type: 'softbreak', level: s.level }); - for (_++; _ < u && 32 === s.src.charCodeAt(_); ) _++; - return (s.pos = _), !0; - } - ], - [ - 'escape', - function index_browser_escape(s, o) { - var i, - u = s.pos, - _ = s.posMax; - if (92 !== s.src.charCodeAt(u)) return !1; - if (++u < _) { - if ((i = s.src.charCodeAt(u)) < 256 && 0 !== CC[i]) - return o || (s.pending += s.src[u]), (s.pos += 2), !0; - if (10 === i) { - for ( - o || s.push({ type: 'hardbreak', level: s.level }), u++; - u < _ && 32 === s.src.charCodeAt(u); - - ) - u++; - return (s.pos = u), !0; - } - } - return o || (s.pending += '\\'), s.pos++, !0; - } - ], - [ - 'backticks', - function backticks(s, o) { - var i, - u, - _, - w, - x, - C = s.pos; - if (96 !== s.src.charCodeAt(C)) return !1; - for (i = C, C++, u = s.posMax; C < u && 96 === s.src.charCodeAt(C); ) C++; - for (_ = s.src.slice(i, C), w = x = C; -1 !== (w = s.src.indexOf('`', x)); ) { - for (x = w + 1; x < u && 96 === s.src.charCodeAt(x); ) x++; - if (x - w === _.length) - return ( - o || - s.push({ - type: 'code', - content: s.src - .slice(C, w) - .replace(/[ \n]+/g, ' ') - .trim(), - block: !1, - level: s.level - }), - (s.pos = x), - !0 - ); - } - return o || (s.pending += _), (s.pos += _.length), !0; - } - ], - [ - 'del', - function del(s, o) { - var i, - u, - _, - w, - x, - C = s.posMax, - j = s.pos; - if (126 !== s.src.charCodeAt(j)) return !1; - if (o) return !1; - if (j + 4 >= C) return !1; - if (126 !== s.src.charCodeAt(j + 1)) return !1; - if (s.level >= s.options.maxNesting) return !1; - if ( - ((w = j > 0 ? s.src.charCodeAt(j - 1) : -1), - (x = s.src.charCodeAt(j + 2)), - 126 === w) - ) - return !1; - if (126 === x) return !1; - if (32 === x || 10 === x) return !1; - for (u = j + 2; u < C && 126 === s.src.charCodeAt(u); ) u++; - if (u > j + 3) return (s.pos += u - j), o || (s.pending += s.src.slice(j, u)), !0; - for (s.pos = j + 2, _ = 1; s.pos + 1 < C; ) { - if ( - 126 === s.src.charCodeAt(s.pos) && - 126 === s.src.charCodeAt(s.pos + 1) && - ((w = s.src.charCodeAt(s.pos - 1)), - 126 !== (x = s.pos + 2 < C ? s.src.charCodeAt(s.pos + 2) : -1) && - 126 !== w && - (32 !== w && 10 !== w ? _-- : 32 !== x && 10 !== x && _++, _ <= 0)) - ) { - i = !0; - break; - } - s.parser.skipToken(s); - } - return i - ? ((s.posMax = s.pos), - (s.pos = j + 2), - o || - (s.push({ type: 'del_open', level: s.level++ }), - s.parser.tokenize(s), - s.push({ type: 'del_close', level: --s.level })), - (s.pos = s.posMax + 2), - (s.posMax = C), - !0) - : ((s.pos = j), !1); - } - ], - [ - 'ins', - function ins(s, o) { - var i, - u, - _, - w, - x, - C = s.posMax, - j = s.pos; - if (43 !== s.src.charCodeAt(j)) return !1; - if (o) return !1; - if (j + 4 >= C) return !1; - if (43 !== s.src.charCodeAt(j + 1)) return !1; - if (s.level >= s.options.maxNesting) return !1; - if ( - ((w = j > 0 ? s.src.charCodeAt(j - 1) : -1), - (x = s.src.charCodeAt(j + 2)), - 43 === w) - ) - return !1; - if (43 === x) return !1; - if (32 === x || 10 === x) return !1; - for (u = j + 2; u < C && 43 === s.src.charCodeAt(u); ) u++; - if (u !== j + 2) return (s.pos += u - j), o || (s.pending += s.src.slice(j, u)), !0; - for (s.pos = j + 2, _ = 1; s.pos + 1 < C; ) { - if ( - 43 === s.src.charCodeAt(s.pos) && - 43 === s.src.charCodeAt(s.pos + 1) && - ((w = s.src.charCodeAt(s.pos - 1)), - 43 !== (x = s.pos + 2 < C ? s.src.charCodeAt(s.pos + 2) : -1) && - 43 !== w && - (32 !== w && 10 !== w ? _-- : 32 !== x && 10 !== x && _++, _ <= 0)) - ) { - i = !0; - break; - } - s.parser.skipToken(s); - } - return i - ? ((s.posMax = s.pos), - (s.pos = j + 2), - o || - (s.push({ type: 'ins_open', level: s.level++ }), - s.parser.tokenize(s), - s.push({ type: 'ins_close', level: --s.level })), - (s.pos = s.posMax + 2), - (s.posMax = C), - !0) - : ((s.pos = j), !1); - } - ], - [ - 'mark', - function mark(s, o) { - var i, - u, - _, - w, - x, - C = s.posMax, - j = s.pos; - if (61 !== s.src.charCodeAt(j)) return !1; - if (o) return !1; - if (j + 4 >= C) return !1; - if (61 !== s.src.charCodeAt(j + 1)) return !1; - if (s.level >= s.options.maxNesting) return !1; - if ( - ((w = j > 0 ? s.src.charCodeAt(j - 1) : -1), - (x = s.src.charCodeAt(j + 2)), - 61 === w) - ) - return !1; - if (61 === x) return !1; - if (32 === x || 10 === x) return !1; - for (u = j + 2; u < C && 61 === s.src.charCodeAt(u); ) u++; - if (u !== j + 2) return (s.pos += u - j), o || (s.pending += s.src.slice(j, u)), !0; - for (s.pos = j + 2, _ = 1; s.pos + 1 < C; ) { - if ( - 61 === s.src.charCodeAt(s.pos) && - 61 === s.src.charCodeAt(s.pos + 1) && - ((w = s.src.charCodeAt(s.pos - 1)), - 61 !== (x = s.pos + 2 < C ? s.src.charCodeAt(s.pos + 2) : -1) && - 61 !== w && - (32 !== w && 10 !== w ? _-- : 32 !== x && 10 !== x && _++, _ <= 0)) - ) { - i = !0; - break; - } - s.parser.skipToken(s); - } - return i - ? ((s.posMax = s.pos), - (s.pos = j + 2), - o || - (s.push({ type: 'mark_open', level: s.level++ }), - s.parser.tokenize(s), - s.push({ type: 'mark_close', level: --s.level })), - (s.pos = s.posMax + 2), - (s.posMax = C), - !0) - : ((s.pos = j), !1); - } - ], - [ - 'emphasis', - function emphasis(s, o) { - var i, - u, - _, - w, - x, - C, - j, - L = s.posMax, - B = s.pos, - $ = s.src.charCodeAt(B); - if (95 !== $ && 42 !== $) return !1; - if (o) return !1; - if (((i = (j = scanDelims(s, B)).delims), !j.can_open)) - return (s.pos += i), o || (s.pending += s.src.slice(B, s.pos)), !0; - if (s.level >= s.options.maxNesting) return !1; - for (s.pos = B + i, C = [i]; s.pos < L; ) - if (s.src.charCodeAt(s.pos) !== $) s.parser.skipToken(s); - else { - if (((u = (j = scanDelims(s, s.pos)).delims), j.can_close)) { - for (w = C.pop(), x = u; w !== x; ) { - if (x < w) { - C.push(w - x); - break; - } - if (((x -= w), 0 === C.length)) break; - (s.pos += w), (w = C.pop()); - } - if (0 === C.length) { - (i = w), (_ = !0); - break; - } - s.pos += u; - continue; - } - j.can_open && C.push(u), (s.pos += u); - } - return _ - ? ((s.posMax = s.pos), - (s.pos = B + i), - o || - ((2 !== i && 3 !== i) || s.push({ type: 'strong_open', level: s.level++ }), - (1 !== i && 3 !== i) || s.push({ type: 'em_open', level: s.level++ }), - s.parser.tokenize(s), - (1 !== i && 3 !== i) || s.push({ type: 'em_close', level: --s.level }), - (2 !== i && 3 !== i) || s.push({ type: 'strong_close', level: --s.level })), - (s.pos = s.posMax + i), - (s.posMax = L), - !0) - : ((s.pos = B), !1); - } - ], - [ - 'sub', - function sub(s, o) { - var i, - u, - _ = s.posMax, - w = s.pos; - if (126 !== s.src.charCodeAt(w)) return !1; - if (o) return !1; - if (w + 2 >= _) return !1; - if (s.level >= s.options.maxNesting) return !1; - for (s.pos = w + 1; s.pos < _; ) { - if (126 === s.src.charCodeAt(s.pos)) { - i = !0; - break; - } - s.parser.skipToken(s); - } - return i && w + 1 !== s.pos - ? (u = s.src.slice(w + 1, s.pos)).match(/(^|[^\\])(\\\\)*\s/) - ? ((s.pos = w), !1) - : ((s.posMax = s.pos), - (s.pos = w + 1), - o || s.push({ type: 'sub', level: s.level, content: u.replace(AC, '$1') }), - (s.pos = s.posMax + 1), - (s.posMax = _), - !0) - : ((s.pos = w), !1); - } - ], - [ - 'sup', - function sup(s, o) { - var i, - u, - _ = s.posMax, - w = s.pos; - if (94 !== s.src.charCodeAt(w)) return !1; - if (o) return !1; - if (w + 2 >= _) return !1; - if (s.level >= s.options.maxNesting) return !1; - for (s.pos = w + 1; s.pos < _; ) { - if (94 === s.src.charCodeAt(s.pos)) { - i = !0; - break; - } - s.parser.skipToken(s); - } - return i && w + 1 !== s.pos - ? (u = s.src.slice(w + 1, s.pos)).match(/(^|[^\\])(\\\\)*\s/) - ? ((s.pos = w), !1) - : ((s.posMax = s.pos), - (s.pos = w + 1), - o || s.push({ type: 'sup', level: s.level, content: u.replace(jC, '$1') }), - (s.pos = s.posMax + 1), - (s.posMax = _), - !0) - : ((s.pos = w), !1); - } - ], - [ - 'links', - function links(s, o) { - var i, - u, - _, - w, - x, - C, - j, - L, - B = !1, - $ = s.pos, - V = s.posMax, - U = s.pos, - z = s.src.charCodeAt(U); - if ((33 === z && ((B = !0), (z = s.src.charCodeAt(++U))), 91 !== z)) return !1; - if (s.level >= s.options.maxNesting) return !1; - if (((i = U + 1), (u = parseLinkLabel(s, U)) < 0)) return !1; - if ((C = u + 1) < V && 40 === s.src.charCodeAt(C)) { - for (C++; C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); C++); - if (C >= V) return !1; - for ( - U = C, - parseLinkDestination(s, C) ? ((w = s.linkContent), (C = s.pos)) : (w = ''), - U = C; - C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); - C++ - ); - if (C < V && U !== C && parseLinkTitle(s, C)) - for ( - x = s.linkContent, C = s.pos; - C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); - C++ - ); - else x = ''; - if (C >= V || 41 !== s.src.charCodeAt(C)) return (s.pos = $), !1; - C++; - } else { - if (s.linkLevel > 0) return !1; - for (; C < V && (32 === (L = s.src.charCodeAt(C)) || 10 === L); C++); - if ( - (C < V && - 91 === s.src.charCodeAt(C) && - ((U = C + 1), - (C = parseLinkLabel(s, C)) >= 0 ? (_ = s.src.slice(U, C++)) : (C = U - 1)), - _ || (void 0 === _ && (C = u + 1), (_ = s.src.slice(i, u))), - !(j = s.env.references[normalizeReference(_)])) - ) - return (s.pos = $), !1; - (w = j.href), (x = j.title); - } - return ( - o || - ((s.pos = i), - (s.posMax = u), - B - ? s.push({ - type: 'image', - src: w, - title: x, - alt: s.src.substr(i, u - i), - level: s.level - }) - : (s.push({ type: 'link_open', href: w, title: x, level: s.level++ }), - s.linkLevel++, - s.parser.tokenize(s), - s.linkLevel--, - s.push({ type: 'link_close', level: --s.level }))), - (s.pos = C), - (s.posMax = V), - !0 - ); - } - ], - [ - 'footnote_inline', - function footnote_inline(s, o) { - var i, - u, - _, - w, - x = s.posMax, - C = s.pos; - return ( - !(C + 2 >= x) && - 94 === s.src.charCodeAt(C) && - 91 === s.src.charCodeAt(C + 1) && - !(s.level >= s.options.maxNesting) && - ((i = C + 2), - !((u = parseLinkLabel(s, C + 1)) < 0) && - (o || - (s.env.footnotes || (s.env.footnotes = {}), - s.env.footnotes.list || (s.env.footnotes.list = []), - (_ = s.env.footnotes.list.length), - (s.pos = i), - (s.posMax = u), - s.push({ type: 'footnote_ref', id: _, level: s.level }), - s.linkLevel++, - (w = s.tokens.length), - s.parser.tokenize(s), - (s.env.footnotes.list[_] = { tokens: s.tokens.splice(w) }), - s.linkLevel--), - (s.pos = u + 1), - (s.posMax = x), - !0)) - ); - } - ], - [ - 'footnote_ref', - function footnote_ref(s, o) { - var i, - u, - _, - w, - x = s.posMax, - C = s.pos; - if (C + 3 > x) return !1; - if (!s.env.footnotes || !s.env.footnotes.refs) return !1; - if (91 !== s.src.charCodeAt(C)) return !1; - if (94 !== s.src.charCodeAt(C + 1)) return !1; - if (s.level >= s.options.maxNesting) return !1; - for (u = C + 2; u < x; u++) { - if (32 === s.src.charCodeAt(u)) return !1; - if (10 === s.src.charCodeAt(u)) return !1; - if (93 === s.src.charCodeAt(u)) break; - } - return ( - u !== C + 2 && - !(u >= x) && - (u++, - (i = s.src.slice(C + 2, u - 1)), - void 0 !== s.env.footnotes.refs[':' + i] && - (o || - (s.env.footnotes.list || (s.env.footnotes.list = []), - s.env.footnotes.refs[':' + i] < 0 - ? ((_ = s.env.footnotes.list.length), - (s.env.footnotes.list[_] = { label: i, count: 0 }), - (s.env.footnotes.refs[':' + i] = _)) - : (_ = s.env.footnotes.refs[':' + i]), - (w = s.env.footnotes.list[_].count), - s.env.footnotes.list[_].count++, - s.push({ type: 'footnote_ref', id: _, subId: w, level: s.level })), - (s.pos = u), - (s.posMax = x), - !0)) - ); - } - ], - [ - 'autolink', - function autolink(s, o) { - var i, - u, - _, - w, - x, - C = s.pos; - return ( - 60 === s.src.charCodeAt(C) && - !((i = s.src.slice(C)).indexOf('>') < 0) && - ((u = i.match(MC)) - ? !(IC.indexOf(u[1].toLowerCase()) < 0) && - ((x = normalizeLink((w = u[0].slice(1, -1)))), - !!s.parser.validateLink(w) && - (o || - (s.push({ type: 'link_open', href: x, level: s.level }), - s.push({ type: 'text', content: w, level: s.level + 1 }), - s.push({ type: 'link_close', level: s.level })), - (s.pos += u[0].length), - !0)) - : !!(_ = i.match(PC)) && - ((x = normalizeLink('mailto:' + (w = _[0].slice(1, -1)))), - !!s.parser.validateLink(x) && - (o || - (s.push({ type: 'link_open', href: x, level: s.level }), - s.push({ type: 'text', content: w, level: s.level + 1 }), - s.push({ type: 'link_close', level: s.level })), - (s.pos += _[0].length), - !0))) - ); - } - ], - [ - 'htmltag', - function htmltag(s, o) { - var i, - u, - _, - w = s.pos; - return ( - !!s.options.html && - ((_ = s.posMax), - !(60 !== s.src.charCodeAt(w) || w + 2 >= _) && - !( - 33 !== (i = s.src.charCodeAt(w + 1)) && - 63 !== i && - 47 !== i && - !(function isLetter$2(s) { - var o = 32 | s; - return o >= 97 && o <= 122; - })(i) - ) && - !!(u = s.src.slice(w).match(DC)) && - (o || - s.push({ - type: 'htmltag', - content: s.src.slice(w, w + u[0].length), - level: s.level - }), - (s.pos += u[0].length), - !0)) - ); - } - ], - [ - 'entity', - function entity(s, o) { - var i, - u, - _ = s.pos, - w = s.posMax; - if (38 !== s.src.charCodeAt(_)) return !1; - if (_ + 1 < w) - if (35 === s.src.charCodeAt(_ + 1)) { - if ((u = s.src.slice(_).match(LC))) - return ( - o || - ((i = - 'x' === u[1][0].toLowerCase() - ? parseInt(u[1].slice(1), 16) - : parseInt(u[1], 10)), - (s.pending += isValidEntityCode(i) - ? fromCodePoint(i) - : fromCodePoint(65533))), - (s.pos += u[0].length), - !0 - ); - } else if ((u = s.src.slice(_).match(BC))) { - var x = decodeEntity(u[1]); - if (u[1] !== x) return o || (s.pending += x), (s.pos += u[0].length), !0; - } - return o || (s.pending += '&'), s.pos++, !0; - } - ] - ]; - function ParserInline() { - this.ruler = new Ruler(); - for (var s = 0; s < FC.length; s++) this.ruler.push(FC[s][0], FC[s][1]); - this.validateLink = validateLink; - } - function validateLink(s) { - var o = s.trim().toLowerCase(); - return ( - -1 === (o = replaceEntities(o)).indexOf(':') || - -1 === ['vbscript', 'javascript', 'file', 'data'].indexOf(o.split(':')[0]) - ); - } - (ParserInline.prototype.skipToken = function (s) { - var o, - i, - u = this.ruler.getRules(''), - _ = u.length, - w = s.pos; - if ((i = s.cacheGet(w)) > 0) s.pos = i; - else { - for (o = 0; o < _; o++) if (u[o](s, !0)) return void s.cacheSet(w, s.pos); - s.pos++, s.cacheSet(w, s.pos); - } - }), - (ParserInline.prototype.tokenize = function (s) { - for (var o, i, u = this.ruler.getRules(''), _ = u.length, w = s.posMax; s.pos < w; ) { - for (i = 0; i < _ && !(o = u[i](s, !1)); i++); - if (o) { - if (s.pos >= w) break; - } else s.pending += s.src[s.pos++]; - } - s.pending && s.pushPending(); - }), - (ParserInline.prototype.parse = function (s, o, i, u) { - var _ = new StateInline(s, this, o, i, u); - this.tokenize(_); - }); - var qC = { - default: { - options: { - html: !1, - xhtmlOut: !1, - breaks: !1, - langPrefix: 'language-', - linkTarget: '', - typographer: !1, - quotes: '“”‘’', - highlight: null, - maxNesting: 20 - }, - components: { - core: { - rules: [ - 'block', - 'inline', - 'references', - 'replacements', - 'smartquotes', - 'references', - 'abbr2', - 'footnote_tail' - ] - }, - block: { - rules: [ - 'blockquote', - 'code', - 'fences', - 'footnote', - 'heading', - 'hr', - 'htmlblock', - 'lheading', - 'list', - 'paragraph', - 'table' - ] - }, - inline: { - rules: [ - 'autolink', - 'backticks', - 'del', - 'emphasis', - 'entity', - 'escape', - 'footnote_ref', - 'htmltag', - 'links', - 'newline', - 'text' - ] - } - } - }, - full: { - options: { - html: !1, - xhtmlOut: !1, - breaks: !1, - langPrefix: 'language-', - linkTarget: '', - typographer: !1, - quotes: '“”‘’', - highlight: null, - maxNesting: 20 - }, - components: { core: {}, block: {}, inline: {} } - }, - commonmark: { - options: { - html: !0, - xhtmlOut: !0, - breaks: !1, - langPrefix: 'language-', - linkTarget: '', - typographer: !1, - quotes: '“”‘’', - highlight: null, - maxNesting: 20 - }, - components: { - core: { rules: ['block', 'inline', 'references', 'abbr2'] }, - block: { - rules: [ - 'blockquote', - 'code', - 'fences', - 'heading', - 'hr', - 'htmlblock', - 'lheading', - 'list', - 'paragraph' - ] - }, - inline: { - rules: [ - 'autolink', - 'backticks', - 'emphasis', - 'entity', - 'escape', - 'htmltag', - 'links', - 'newline', - 'text' - ] - } - } - } - }; - function StateCore(s, o, i) { - (this.src = o), - (this.env = i), - (this.options = s.options), - (this.tokens = []), - (this.inlineMode = !1), - (this.inline = s.inline), - (this.block = s.block), - (this.renderer = s.renderer), - (this.typographer = s.typographer); - } - function Remarkable(s, o) { - 'string' != typeof s && ((o = s), (s = 'default')), - o && - null != o.linkify && - console.warn( - "linkify option is removed. Use linkify plugin instead:\n\nimport Remarkable from 'remarkable';\nimport linkify from 'remarkable/linkify';\nnew Remarkable().use(linkify)\n" - ), - (this.inline = new ParserInline()), - (this.block = new ParserBlock()), - (this.core = new Core()), - (this.renderer = new Renderer()), - (this.ruler = new Ruler()), - (this.options = {}), - this.configure(qC[s]), - this.set(o || {}); - } - (Remarkable.prototype.set = function (s) { - index_browser_assign(this.options, s); - }), - (Remarkable.prototype.configure = function (s) { - var o = this; - if (!s) throw new Error('Wrong `remarkable` preset, check name/content'); - s.options && o.set(s.options), - s.components && - Object.keys(s.components).forEach(function (i) { - s.components[i].rules && o[i].ruler.enable(s.components[i].rules, !0); - }); - }), - (Remarkable.prototype.use = function (s, o) { - return s(this, o), this; - }), - (Remarkable.prototype.parse = function (s, o) { - var i = new StateCore(this, s, o); - return this.core.process(i), i.tokens; - }), - (Remarkable.prototype.render = function (s, o) { - return (o = o || {}), this.renderer.render(this.parse(s, o), this.options, o); - }), - (Remarkable.prototype.parseInline = function (s, o) { - var i = new StateCore(this, s, o); - return (i.inlineMode = !0), this.core.process(i), i.tokens; - }), - (Remarkable.prototype.renderInline = function (s, o) { - return (o = o || {}), this.renderer.render(this.parseInline(s, o), this.options, o); - }); - function indexOf(s, o) { - if (Array.prototype.indexOf) return s.indexOf(o); - for (var i = 0, u = s.length; i < u; i++) if (s[i] === o) return i; - return -1; - } - function utils_remove(s, o) { - for (var i = s.length - 1; i >= 0; i--) !0 === o(s[i]) && s.splice(i, 1); - } - function throwUnhandledCaseError(s) { - throw new Error("Unhandled case for value: '".concat(s, "'")); - } - var $C = (function () { - function HtmlTag(s) { - void 0 === s && (s = {}), - (this.tagName = ''), - (this.attrs = {}), - (this.innerHTML = ''), - (this.whitespaceRegex = /\s+/), - (this.tagName = s.tagName || ''), - (this.attrs = s.attrs || {}), - (this.innerHTML = s.innerHtml || s.innerHTML || ''); - } - return ( - (HtmlTag.prototype.setTagName = function (s) { - return (this.tagName = s), this; - }), - (HtmlTag.prototype.getTagName = function () { - return this.tagName || ''; - }), - (HtmlTag.prototype.setAttr = function (s, o) { - return (this.getAttrs()[s] = o), this; - }), - (HtmlTag.prototype.getAttr = function (s) { - return this.getAttrs()[s]; - }), - (HtmlTag.prototype.setAttrs = function (s) { - return Object.assign(this.getAttrs(), s), this; - }), - (HtmlTag.prototype.getAttrs = function () { - return this.attrs || (this.attrs = {}); - }), - (HtmlTag.prototype.setClass = function (s) { - return this.setAttr('class', s); - }), - (HtmlTag.prototype.addClass = function (s) { - for ( - var o, - i = this.getClass(), - u = this.whitespaceRegex, - _ = i ? i.split(u) : [], - w = s.split(u); - (o = w.shift()); - - ) - -1 === indexOf(_, o) && _.push(o); - return (this.getAttrs().class = _.join(' ')), this; - }), - (HtmlTag.prototype.removeClass = function (s) { - for ( - var o, - i = this.getClass(), - u = this.whitespaceRegex, - _ = i ? i.split(u) : [], - w = s.split(u); - _.length && (o = w.shift()); - - ) { - var x = indexOf(_, o); - -1 !== x && _.splice(x, 1); - } - return (this.getAttrs().class = _.join(' ')), this; - }), - (HtmlTag.prototype.getClass = function () { - return this.getAttrs().class || ''; - }), - (HtmlTag.prototype.hasClass = function (s) { - return -1 !== (' ' + this.getClass() + ' ').indexOf(' ' + s + ' '); - }), - (HtmlTag.prototype.setInnerHTML = function (s) { - return (this.innerHTML = s), this; - }), - (HtmlTag.prototype.setInnerHtml = function (s) { - return this.setInnerHTML(s); - }), - (HtmlTag.prototype.getInnerHTML = function () { - return this.innerHTML || ''; - }), - (HtmlTag.prototype.getInnerHtml = function () { - return this.getInnerHTML(); - }), - (HtmlTag.prototype.toAnchorString = function () { - var s = this.getTagName(), - o = this.buildAttrsStr(); - return ['<', s, (o = o ? ' ' + o : ''), '>', this.getInnerHtml(), ''].join( - '' - ); - }), - (HtmlTag.prototype.buildAttrsStr = function () { - if (!this.attrs) return ''; - var s = this.getAttrs(), - o = []; - for (var i in s) s.hasOwnProperty(i) && o.push(i + '="' + s[i] + '"'); - return o.join(' '); - }), - HtmlTag - ); - })(); - var VC = (function () { - function AnchorTagBuilder(s) { - void 0 === s && (s = {}), - (this.newWindow = !1), - (this.truncate = {}), - (this.className = ''), - (this.newWindow = s.newWindow || !1), - (this.truncate = s.truncate || {}), - (this.className = s.className || ''); - } - return ( - (AnchorTagBuilder.prototype.build = function (s) { - return new $C({ - tagName: 'a', - attrs: this.createAttrs(s), - innerHtml: this.processAnchorText(s.getAnchorText()) - }); - }), - (AnchorTagBuilder.prototype.createAttrs = function (s) { - var o = { href: s.getAnchorHref() }, - i = this.createCssClass(s); - return ( - i && (o.class = i), - this.newWindow && ((o.target = '_blank'), (o.rel = 'noopener noreferrer')), - this.truncate && - this.truncate.length && - this.truncate.length < s.getAnchorText().length && - (o.title = s.getAnchorHref()), - o - ); - }), - (AnchorTagBuilder.prototype.createCssClass = function (s) { - var o = this.className; - if (o) { - for (var i = [o], u = s.getCssClassSuffixes(), _ = 0, w = u.length; _ < w; _++) - i.push(o + '-' + u[_]); - return i.join(' '); - } - return ''; - }), - (AnchorTagBuilder.prototype.processAnchorText = function (s) { - return (s = this.doTruncate(s)); - }), - (AnchorTagBuilder.prototype.doTruncate = function (s) { - var o = this.truncate; - if (!o || !o.length) return s; - var i = o.length, - u = o.location; - return 'smart' === u - ? (function truncateSmart(s, o, i) { - var u, _; - null == i - ? ((i = '…'), (_ = 3), (u = 8)) - : ((_ = i.length), (u = i.length)); - var buildUrl = function (s) { - var o = ''; - return ( - s.scheme && s.host && (o += s.scheme + '://'), - s.host && (o += s.host), - s.path && (o += '/' + s.path), - s.query && (o += '?' + s.query), - s.fragment && (o += '#' + s.fragment), - o - ); - }, - buildSegment = function (s, o) { - var u = o / 2, - _ = Math.ceil(u), - w = -1 * Math.floor(u), - x = ''; - return w < 0 && (x = s.substr(w)), s.substr(0, _) + i + x; - }; - if (s.length <= o) return s; - var w = o - _, - x = (function (s) { - var o = {}, - i = s, - u = i.match(/^([a-z]+):\/\//i); - return ( - u && ((o.scheme = u[1]), (i = i.substr(u[0].length))), - (u = i.match(/^(.*?)(?=(\?|#|\/|$))/i)) && - ((o.host = u[1]), (i = i.substr(u[0].length))), - (u = i.match(/^\/(.*?)(?=(\?|#|$))/i)) && - ((o.path = u[1]), (i = i.substr(u[0].length))), - (u = i.match(/^\?(.*?)(?=(#|$))/i)) && - ((o.query = u[1]), (i = i.substr(u[0].length))), - (u = i.match(/^#(.*?)$/i)) && (o.fragment = u[1]), - o - ); - })(s); - if (x.query) { - var C = x.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i); - C && ((x.query = x.query.substr(0, C[1].length)), (s = buildUrl(x))); - } - if (s.length <= o) return s; - if ( - (x.host && ((x.host = x.host.replace(/^www\./, '')), (s = buildUrl(x))), - s.length <= o) - ) - return s; - var j = ''; - if ((x.host && (j += x.host), j.length >= w)) - return x.host.length == o - ? (x.host.substr(0, o - _) + i).substr(0, w + u) - : buildSegment(j, w).substr(0, w + u); - var L = ''; - if ((x.path && (L += '/' + x.path), x.query && (L += '?' + x.query), L)) { - if ((j + L).length >= w) - return (j + L).length == o - ? (j + L).substr(0, o) - : (j + buildSegment(L, w - j.length)).substr(0, w + u); - j += L; - } - if (x.fragment) { - var B = '#' + x.fragment; - if ((j + B).length >= w) - return (j + B).length == o - ? (j + B).substr(0, o) - : (j + buildSegment(B, w - j.length)).substr(0, w + u); - j += B; - } - if (x.scheme && x.host) { - var $ = x.scheme + '://'; - if ((j + $).length < w) return ($ + j).substr(0, o); - } - if (j.length <= o) return j; - var V = ''; - return ( - w > 0 && (V = j.substr(-1 * Math.floor(w / 2))), - (j.substr(0, Math.ceil(w / 2)) + i + V).substr(0, w + u) - ); - })(s, i) - : 'middle' === u - ? (function truncateMiddle(s, o, i) { - if (s.length <= o) return s; - var u, _; - null == i - ? ((i = '…'), (u = 8), (_ = 3)) - : ((u = i.length), (_ = i.length)); - var w = o - _, - x = ''; - return ( - w > 0 && (x = s.substr(-1 * Math.floor(w / 2))), - (s.substr(0, Math.ceil(w / 2)) + i + x).substr(0, w + u) - ); - })(s, i) - : (function truncateEnd(s, o, i) { - return (function ellipsis(s, o, i) { - var u; - return ( - s.length > o && - (null == i ? ((i = '…'), (u = 3)) : (u = i.length), - (s = s.substring(0, o - u) + i)), - s - ); - })(s, o, i); - })(s, i); - }), - AnchorTagBuilder - ); - })(), - UC = (function () { - function Match(s) { - (this.__jsduckDummyDocProp = null), - (this.matchedText = ''), - (this.offset = 0), - (this.tagBuilder = s.tagBuilder), - (this.matchedText = s.matchedText), - (this.offset = s.offset); - } - return ( - (Match.prototype.getMatchedText = function () { - return this.matchedText; - }), - (Match.prototype.setOffset = function (s) { - this.offset = s; - }), - (Match.prototype.getOffset = function () { - return this.offset; - }), - (Match.prototype.getCssClassSuffixes = function () { - return [this.getType()]; - }), - (Match.prototype.buildTag = function () { - return this.tagBuilder.build(this); - }), - Match - ); - })(), - extendStatics = function (s, o) { - return ( - (extendStatics = - Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && - function (s, o) { - s.__proto__ = o; - }) || - function (s, o) { - for (var i in o) Object.prototype.hasOwnProperty.call(o, i) && (s[i] = o[i]); - }), - extendStatics(s, o) - ); - }; - function tslib_es6_extends(s, o) { - if ('function' != typeof o && null !== o) - throw new TypeError( - 'Class extends value ' + String(o) + ' is not a constructor or null' - ); - function __() { - this.constructor = s; - } - extendStatics(s, o), - (s.prototype = - null === o ? Object.create(o) : ((__.prototype = o.prototype), new __())); - } - var __assign = function () { - return ( - (__assign = - Object.assign || - function __assign(s) { - for (var o, i = 1, u = arguments.length; i < u; i++) - for (var _ in (o = arguments[i])) - Object.prototype.hasOwnProperty.call(o, _) && (s[_] = o[_]); - return s; - }), - __assign.apply(this, arguments) - ); - }; - Object.create; - Object.create; - 'function' == typeof SuppressedError && SuppressedError; - var zC, - WC = (function (s) { - function EmailMatch(o) { - var i = s.call(this, o) || this; - return (i.email = ''), (i.email = o.email), i; - } - return ( - tslib_es6_extends(EmailMatch, s), - (EmailMatch.prototype.getType = function () { - return 'email'; - }), - (EmailMatch.prototype.getEmail = function () { - return this.email; - }), - (EmailMatch.prototype.getAnchorHref = function () { - return 'mailto:' + this.email; - }), - (EmailMatch.prototype.getAnchorText = function () { - return this.email; - }), - EmailMatch - ); - })(UC), - KC = (function (s) { - function HashtagMatch(o) { - var i = s.call(this, o) || this; - return ( - (i.serviceName = ''), - (i.hashtag = ''), - (i.serviceName = o.serviceName), - (i.hashtag = o.hashtag), - i - ); - } - return ( - tslib_es6_extends(HashtagMatch, s), - (HashtagMatch.prototype.getType = function () { - return 'hashtag'; - }), - (HashtagMatch.prototype.getServiceName = function () { - return this.serviceName; - }), - (HashtagMatch.prototype.getHashtag = function () { - return this.hashtag; - }), - (HashtagMatch.prototype.getAnchorHref = function () { - var s = this.serviceName, - o = this.hashtag; - switch (s) { - case 'twitter': - return 'https://twitter.com/hashtag/' + o; - case 'facebook': - return 'https://www.facebook.com/hashtag/' + o; - case 'instagram': - return 'https://instagram.com/explore/tags/' + o; - case 'tiktok': - return 'https://www.tiktok.com/tag/' + o; - default: - throw new Error('Unknown service name to point hashtag to: ' + s); - } - }), - (HashtagMatch.prototype.getAnchorText = function () { - return '#' + this.hashtag; - }), - HashtagMatch - ); - })(UC), - HC = (function (s) { - function MentionMatch(o) { - var i = s.call(this, o) || this; - return ( - (i.serviceName = 'twitter'), - (i.mention = ''), - (i.mention = o.mention), - (i.serviceName = o.serviceName), - i - ); - } - return ( - tslib_es6_extends(MentionMatch, s), - (MentionMatch.prototype.getType = function () { - return 'mention'; - }), - (MentionMatch.prototype.getMention = function () { - return this.mention; - }), - (MentionMatch.prototype.getServiceName = function () { - return this.serviceName; - }), - (MentionMatch.prototype.getAnchorHref = function () { - switch (this.serviceName) { - case 'twitter': - return 'https://twitter.com/' + this.mention; - case 'instagram': - return 'https://instagram.com/' + this.mention; - case 'soundcloud': - return 'https://soundcloud.com/' + this.mention; - case 'tiktok': - return 'https://www.tiktok.com/@' + this.mention; - default: - throw new Error( - 'Unknown service name to point mention to: ' + this.serviceName - ); - } - }), - (MentionMatch.prototype.getAnchorText = function () { - return '@' + this.mention; - }), - (MentionMatch.prototype.getCssClassSuffixes = function () { - var o = s.prototype.getCssClassSuffixes.call(this), - i = this.getServiceName(); - return i && o.push(i), o; - }), - MentionMatch - ); - })(UC), - JC = (function (s) { - function PhoneMatch(o) { - var i = s.call(this, o) || this; - return ( - (i.number = ''), - (i.plusSign = !1), - (i.number = o.number), - (i.plusSign = o.plusSign), - i - ); - } - return ( - tslib_es6_extends(PhoneMatch, s), - (PhoneMatch.prototype.getType = function () { - return 'phone'; - }), - (PhoneMatch.prototype.getPhoneNumber = function () { - return this.number; - }), - (PhoneMatch.prototype.getNumber = function () { - return this.getPhoneNumber(); - }), - (PhoneMatch.prototype.getAnchorHref = function () { - return 'tel:' + (this.plusSign ? '+' : '') + this.number; - }), - (PhoneMatch.prototype.getAnchorText = function () { - return this.matchedText; - }), - PhoneMatch - ); - })(UC), - GC = (function (s) { - function UrlMatch(o) { - var i = s.call(this, o) || this; - return ( - (i.url = ''), - (i.urlMatchType = 'scheme'), - (i.protocolUrlMatch = !1), - (i.protocolRelativeMatch = !1), - (i.stripPrefix = { scheme: !0, www: !0 }), - (i.stripTrailingSlash = !0), - (i.decodePercentEncoding = !0), - (i.schemePrefixRegex = /^(https?:\/\/)?/i), - (i.wwwPrefixRegex = /^(https?:\/\/)?(www\.)?/i), - (i.protocolRelativeRegex = /^\/\//), - (i.protocolPrepended = !1), - (i.urlMatchType = o.urlMatchType), - (i.url = o.url), - (i.protocolUrlMatch = o.protocolUrlMatch), - (i.protocolRelativeMatch = o.protocolRelativeMatch), - (i.stripPrefix = o.stripPrefix), - (i.stripTrailingSlash = o.stripTrailingSlash), - (i.decodePercentEncoding = o.decodePercentEncoding), - i - ); - } - return ( - tslib_es6_extends(UrlMatch, s), - (UrlMatch.prototype.getType = function () { - return 'url'; - }), - (UrlMatch.prototype.getUrlMatchType = function () { - return this.urlMatchType; - }), - (UrlMatch.prototype.getUrl = function () { - var s = this.url; - return ( - this.protocolRelativeMatch || - this.protocolUrlMatch || - this.protocolPrepended || - ((s = this.url = 'http://' + s), (this.protocolPrepended = !0)), - s - ); - }), - (UrlMatch.prototype.getAnchorHref = function () { - return this.getUrl().replace(/&/g, '&'); - }), - (UrlMatch.prototype.getAnchorText = function () { - var s = this.getMatchedText(); - return ( - this.protocolRelativeMatch && (s = this.stripProtocolRelativePrefix(s)), - this.stripPrefix.scheme && (s = this.stripSchemePrefix(s)), - this.stripPrefix.www && (s = this.stripWwwPrefix(s)), - this.stripTrailingSlash && (s = this.removeTrailingSlash(s)), - this.decodePercentEncoding && (s = this.removePercentEncoding(s)), - s - ); - }), - (UrlMatch.prototype.stripSchemePrefix = function (s) { - return s.replace(this.schemePrefixRegex, ''); - }), - (UrlMatch.prototype.stripWwwPrefix = function (s) { - return s.replace(this.wwwPrefixRegex, '$1'); - }), - (UrlMatch.prototype.stripProtocolRelativePrefix = function (s) { - return s.replace(this.protocolRelativeRegex, ''); - }), - (UrlMatch.prototype.removeTrailingSlash = function (s) { - return '/' === s.charAt(s.length - 1) && (s = s.slice(0, -1)), s; - }), - (UrlMatch.prototype.removePercentEncoding = function (s) { - var o = s - .replace(/%22/gi, '"') - .replace(/%26/gi, '&') - .replace(/%27/gi, ''') - .replace(/%3C/gi, '<') - .replace(/%3E/gi, '>'); - try { - return decodeURIComponent(o); - } catch (s) { - return o; - } - }), - UrlMatch - ); - })(UC), - YC = function YC(s) { - (this.__jsduckDummyDocProp = null), (this.tagBuilder = s.tagBuilder); - }, - XC = /[A-Za-z]/, - ZC = /[\d]/, - QC = /[\D]/, - eO = /\s/, - tO = /['"]/, - rO = /[\x00-\x1F\x7F]/, - nO = - /A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC/ - .source, - sO = - nO + - /\u2700-\u27bf\udde6-\uddff\ud800-\udbff\udc00-\udfff\ufe0e\ufe0f\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0\ud83c\udffb-\udfff\u200d\u3299\u3297\u303d\u3030\u24c2\ud83c\udd70-\udd71\udd7e-\udd7f\udd8e\udd91-\udd9a\udde6-\uddff\ude01-\ude02\ude1a\ude2f\ude32-\ude3a\ude50-\ude51\u203c\u2049\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u00a9\u00ae\u2122\u2139\udc04\u2600-\u26FF\u2b05\u2b06\u2b07\u2b1b\u2b1c\u2b50\u2b55\u231a\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\udccf\u2935\u2934\u2190-\u21ff/ - .source + - /\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D4-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C00-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D01-\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u192B\u1930-\u193B\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1AB0-\u1ABE\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF5\u1DFB-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C5\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9E5\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F/ - .source, - oO = - /0-9\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F\u0DE6-\u0DEF\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29\u1040-\u1049\u1090-\u1099\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\uA620-\uA629\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uA9F0-\uA9F9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19/ - .source, - iO = sO + oO, - aO = sO + oO, - lO = new RegExp('['.concat(aO, ']')), - cO = '(?:[' + oO + ']{1,3}\\.){3}[' + oO + ']{1,3}', - uO = '[' + aO + '](?:[' + aO + '\\-_]{0,61}[' + aO + '])?', - getDomainLabelStr = function (s) { - return '(?=(' + uO + '))\\' + s; - }, - getDomainNameStr = function (s) { - return ( - '(?:' + - getDomainLabelStr(s) + - '(?:\\.' + - getDomainLabelStr(s + 1) + - '){0,126}|' + - cO + - ')' - ); - }, - pO = (new RegExp('[' + aO + '.\\-]*[' + aO + '\\-]'), lO), - hO = - /(?:xn--vermgensberatung-pwb|xn--vermgensberater-ctb|xn--clchc0ea0b2g2a9gcd|xn--w4r85el8fhu5dnra|northwesternmutual|travelersinsurance|vermögensberatung|xn--5su34j936bgsg|xn--bck1b9a5dre4c|xn--mgbah1a3hjkrd|xn--mgbai9azgqp6j|xn--mgberp4a5d4ar|xn--xkc2dl3a5ee0h|vermögensberater|xn--fzys8d69uvgm|xn--mgba7c0bbn0a|xn--mgbcpq6gpa1a|xn--xkc2al3hye2a|americanexpress|kerryproperties|sandvikcoromant|xn--i1b6b1a6a2e|xn--kcrx77d1x4a|xn--lgbbat1ad8j|xn--mgba3a4f16a|xn--mgbaakc7dvf|xn--mgbc0a9azcg|xn--nqv7fs00ema|americanfamily|bananarepublic|cancerresearch|cookingchannel|kerrylogistics|weatherchannel|xn--54b7fta0cc|xn--6qq986b3xl|xn--80aqecdr1a|xn--b4w605ferd|xn--fiq228c5hs|xn--h2breg3eve|xn--jlq480n2rg|xn--jlq61u9w7b|xn--mgba3a3ejt|xn--mgbaam7a8h|xn--mgbayh7gpa|xn--mgbbh1a71e|xn--mgbca7dzdo|xn--mgbi4ecexp|xn--mgbx4cd0ab|xn--rvc1e0am3e|international|lifeinsurance|travelchannel|wolterskluwer|xn--cckwcxetd|xn--eckvdtc9d|xn--fpcrj9c3d|xn--fzc2c9e2c|xn--h2brj9c8c|xn--tiq49xqyj|xn--yfro4i67o|xn--ygbi2ammx|construction|lplfinancial|scholarships|versicherung|xn--3e0b707e|xn--45br5cyl|xn--4dbrk0ce|xn--80adxhks|xn--80asehdb|xn--8y0a063a|xn--gckr3f0f|xn--mgb9awbf|xn--mgbab2bd|xn--mgbgu82a|xn--mgbpl2fh|xn--mgbt3dhd|xn--mk1bu44c|xn--ngbc5azd|xn--ngbe9e0a|xn--ogbpf8fl|xn--qcka1pmc|accountants|barclaycard|blackfriday|blockbuster|bridgestone|calvinklein|contractors|creditunion|engineering|enterprises|foodnetwork|investments|kerryhotels|lamborghini|motorcycles|olayangroup|photography|playstation|productions|progressive|redumbrella|williamhill|xn--11b4c3d|xn--1ck2e1b|xn--1qqw23a|xn--2scrj9c|xn--3bst00m|xn--3ds443g|xn--3hcrj9c|xn--42c2d9a|xn--45brj9c|xn--55qw42g|xn--6frz82g|xn--80ao21a|xn--9krt00a|xn--cck2b3b|xn--czr694b|xn--d1acj3b|xn--efvy88h|xn--fct429k|xn--fjq720a|xn--flw351e|xn--g2xx48c|xn--gecrj9c|xn--gk3at1e|xn--h2brj9c|xn--hxt814e|xn--imr513n|xn--j6w193g|xn--jvr189m|xn--kprw13d|xn--kpry57d|xn--mgbbh1a|xn--mgbtx2b|xn--mix891f|xn--nyqy26a|xn--otu796d|xn--pgbs0dh|xn--q9jyb4c|xn--rhqv96g|xn--rovu88b|xn--s9brj9c|xn--ses554g|xn--t60b56a|xn--vuq861b|xn--w4rs40l|xn--xhq521b|xn--zfr164b|சிங்கப்பூர்|accountant|apartments|associates|basketball|bnpparibas|boehringer|capitalone|consulting|creditcard|cuisinella|eurovision|extraspace|foundation|healthcare|immobilien|industries|management|mitsubishi|nextdirect|properties|protection|prudential|realestate|republican|restaurant|schaeffler|tatamotors|technology|university|vlaanderen|volkswagen|xn--30rr7y|xn--3pxu8k|xn--45q11c|xn--4gbrim|xn--55qx5d|xn--5tzm5g|xn--80aswg|xn--90a3ac|xn--9dbq2a|xn--9et52u|xn--c2br7g|xn--cg4bki|xn--czrs0t|xn--czru2d|xn--fiq64b|xn--fiqs8s|xn--fiqz9s|xn--io0a7i|xn--kput3i|xn--mxtq1m|xn--o3cw4h|xn--pssy2u|xn--q7ce6a|xn--unup4y|xn--wgbh1c|xn--wgbl6a|xn--y9a3aq|accenture|alfaromeo|allfinanz|amsterdam|analytics|aquarelle|barcelona|bloomberg|christmas|community|directory|education|equipment|fairwinds|financial|firestone|fresenius|frontdoor|furniture|goldpoint|hisamitsu|homedepot|homegoods|homesense|institute|insurance|kuokgroup|lancaster|landrover|lifestyle|marketing|marshalls|melbourne|microsoft|panasonic|passagens|pramerica|richardli|shangrila|solutions|statebank|statefarm|stockholm|travelers|vacations|xn--90ais|xn--c1avg|xn--d1alf|xn--e1a4c|xn--fhbei|xn--j1aef|xn--j1amh|xn--l1acc|xn--ngbrx|xn--nqv7f|xn--p1acf|xn--qxa6a|xn--tckwe|xn--vhquv|yodobashi|موريتانيا|abudhabi|airforce|allstate|attorney|barclays|barefoot|bargains|baseball|boutique|bradesco|broadway|brussels|builders|business|capetown|catering|catholic|cipriani|cityeats|cleaning|clinique|clothing|commbank|computer|delivery|deloitte|democrat|diamonds|discount|discover|download|engineer|ericsson|etisalat|exchange|feedback|fidelity|firmdale|football|frontier|goodyear|grainger|graphics|guardian|hdfcbank|helsinki|holdings|hospital|infiniti|ipiranga|istanbul|jpmorgan|lighting|lundbeck|marriott|maserati|mckinsey|memorial|merckmsd|mortgage|observer|partners|pharmacy|pictures|plumbing|property|redstone|reliance|saarland|samsclub|security|services|shopping|showtime|softbank|software|stcgroup|supplies|training|vanguard|ventures|verisign|woodside|xn--90ae|xn--node|xn--p1ai|xn--qxam|yokohama|السعودية|abogado|academy|agakhan|alibaba|android|athleta|auction|audible|auspost|avianca|banamex|bauhaus|bentley|bestbuy|booking|brother|bugatti|capital|caravan|careers|channel|charity|chintai|citadel|clubmed|college|cologne|comcast|company|compare|contact|cooking|corsica|country|coupons|courses|cricket|cruises|dentist|digital|domains|exposed|express|farmers|fashion|ferrari|ferrero|finance|fishing|fitness|flights|florist|flowers|forsale|frogans|fujitsu|gallery|genting|godaddy|grocery|guitars|hamburg|hangout|hitachi|holiday|hosting|hoteles|hotmail|hyundai|ismaili|jewelry|juniper|kitchen|komatsu|lacaixa|lanxess|lasalle|latrobe|leclerc|limited|lincoln|markets|monster|netbank|netflix|network|neustar|okinawa|oldnavy|organic|origins|philips|pioneer|politie|realtor|recipes|rentals|reviews|rexroth|samsung|sandvik|schmidt|schwarz|science|shiksha|singles|staples|storage|support|surgery|systems|temasek|theater|theatre|tickets|tiffany|toshiba|trading|walmart|wanggou|watches|weather|website|wedding|whoswho|windows|winners|xfinity|yamaxun|youtube|zuerich|католик|اتصالات|البحرين|الجزائر|العليان|پاکستان|كاثوليك|இந்தியா|abarth|abbott|abbvie|africa|agency|airbus|airtel|alipay|alsace|alstom|amazon|anquan|aramco|author|bayern|beauty|berlin|bharti|bostik|boston|broker|camera|career|casino|center|chanel|chrome|church|circle|claims|clinic|coffee|comsec|condos|coupon|credit|cruise|dating|datsun|dealer|degree|dental|design|direct|doctor|dunlop|dupont|durban|emerck|energy|estate|events|expert|family|flickr|futbol|gallup|garden|george|giving|global|google|gratis|health|hermes|hiphop|hockey|hotels|hughes|imamat|insure|intuit|jaguar|joburg|juegos|kaufen|kinder|kindle|kosher|lancia|latino|lawyer|lefrak|living|locker|london|luxury|madrid|maison|makeup|market|mattel|mobile|monash|mormon|moscow|museum|mutual|nagoya|natura|nissan|nissay|norton|nowruz|office|olayan|online|oracle|orange|otsuka|pfizer|photos|physio|pictet|quebec|racing|realty|reisen|repair|report|review|rocher|rogers|ryukyu|safety|sakura|sanofi|school|schule|search|secure|select|shouji|soccer|social|stream|studio|supply|suzuki|swatch|sydney|taipei|taobao|target|tattoo|tennis|tienda|tjmaxx|tkmaxx|toyota|travel|unicom|viajes|viking|villas|virgin|vision|voting|voyage|vuelos|walter|webcam|xihuan|yachts|yandex|zappos|москва|онлайн|ابوظبي|ارامكو|الاردن|المغرب|امارات|فلسطين|مليسيا|भारतम्|இலங்கை|ファッション|actor|adult|aetna|amfam|amica|apple|archi|audio|autos|azure|baidu|beats|bible|bingo|black|boats|bosch|build|canon|cards|chase|cheap|cisco|citic|click|cloud|coach|codes|crown|cymru|dabur|dance|deals|delta|drive|dubai|earth|edeka|email|epson|faith|fedex|final|forex|forum|gallo|games|gifts|gives|glass|globo|gmail|green|gripe|group|gucci|guide|homes|honda|horse|house|hyatt|ikano|irish|jetzt|koeln|kyoto|lamer|lease|legal|lexus|lilly|linde|lipsy|loans|locus|lotte|lotto|macys|mango|media|miami|money|movie|music|nexus|nikon|ninja|nokia|nowtv|omega|osaka|paris|parts|party|phone|photo|pizza|place|poker|praxi|press|prime|promo|quest|radio|rehab|reise|ricoh|rocks|rodeo|rugby|salon|sener|seven|sharp|shell|shoes|skype|sling|smart|smile|solar|space|sport|stada|store|study|style|sucks|swiss|tatar|tires|tirol|tmall|today|tokyo|tools|toray|total|tours|trade|trust|tunes|tushu|ubank|vegas|video|vodka|volvo|wales|watch|weber|weibo|works|world|xerox|yahoo|ישראל|ایران|بازار|بھارت|سودان|سورية|همراه|भारोत|संगठन|বাংলা|భారత్|ഭാരതം|嘉里大酒店|aarp|able|adac|aero|akdn|ally|amex|arab|army|arpa|arte|asda|asia|audi|auto|baby|band|bank|bbva|beer|best|bike|bing|blog|blue|bofa|bond|book|buzz|cafe|call|camp|care|cars|casa|case|cash|cbre|cern|chat|citi|city|club|cool|coop|cyou|data|date|dclk|deal|dell|desi|diet|dish|docs|dvag|erni|fage|fail|fans|farm|fast|fiat|fido|film|fire|fish|flir|food|ford|free|fund|game|gbiz|gent|ggee|gift|gmbh|gold|golf|goog|guge|guru|hair|haus|hdfc|help|here|hgtv|host|hsbc|icbc|ieee|imdb|immo|info|itau|java|jeep|jobs|jprs|kddi|kids|kiwi|kpmg|kred|land|lego|lgbt|lidl|life|like|limo|link|live|loan|loft|love|ltda|luxe|maif|meet|meme|menu|mini|mint|mobi|moda|moto|name|navy|news|next|nico|nike|ollo|open|page|pars|pccw|pics|ping|pink|play|plus|pohl|porn|post|prod|prof|qpon|read|reit|rent|rest|rich|room|rsvp|ruhr|safe|sale|sarl|save|saxo|scot|seat|seek|sexy|shaw|shia|shop|show|silk|sina|site|skin|sncf|sohu|song|sony|spot|star|surf|talk|taxi|team|tech|teva|tiaa|tips|town|toys|tube|vana|visa|viva|vivo|vote|voto|wang|weir|wien|wiki|wine|work|xbox|yoga|zara|zero|zone|дети|сайт|بارت|بيتك|ڀارت|تونس|شبكة|عراق|عمان|موقع|भारत|ভারত|ভাৰত|ਭਾਰਤ|ભારત|ଭାରତ|ಭಾರತ|ලංකා|アマゾン|グーグル|クラウド|ポイント|组织机构|電訊盈科|香格里拉|aaa|abb|abc|aco|ads|aeg|afl|aig|anz|aol|app|art|aws|axa|bar|bbc|bbt|bcg|bcn|bet|bid|bio|biz|bms|bmw|bom|boo|bot|box|buy|bzh|cab|cal|cam|car|cat|cba|cbn|cbs|ceo|cfa|cfd|com|cpa|crs|dad|day|dds|dev|dhl|diy|dnp|dog|dot|dtv|dvr|eat|eco|edu|esq|eus|fan|fit|fly|foo|fox|frl|ftr|fun|fyi|gal|gap|gay|gdn|gea|gle|gmo|gmx|goo|gop|got|gov|hbo|hiv|hkt|hot|how|ibm|ice|icu|ifm|inc|ing|ink|int|ist|itv|jcb|jio|jll|jmp|jnj|jot|joy|kfh|kia|kim|kpn|krd|lat|law|lds|llc|llp|lol|lpl|ltd|man|map|mba|med|men|mil|mit|mlb|mls|mma|moe|moi|mom|mov|msd|mtn|mtr|nab|nba|nec|net|new|nfl|ngo|nhk|now|nra|nrw|ntt|nyc|obi|one|ong|onl|ooo|org|ott|ovh|pay|pet|phd|pid|pin|pnc|pro|pru|pub|pwc|red|ren|ril|rio|rip|run|rwe|sap|sas|sbi|sbs|sca|scb|ses|sew|sex|sfr|ski|sky|soy|spa|srl|stc|tab|tax|tci|tdk|tel|thd|tjx|top|trv|tui|tvs|ubs|uno|uol|ups|vet|vig|vin|vip|wed|win|wme|wow|wtc|wtf|xin|xxx|xyz|you|yun|zip|бел|ком|қаз|мкд|мон|орг|рус|срб|укр|հայ|קום|عرب|قطر|كوم|مصر|कॉम|नेट|คอม|ไทย|ລາວ|ストア|セール|みんな|中文网|亚马逊|天主教|我爱你|新加坡|淡马锡|诺基亚|飞利浦|ac|ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw|ελ|ευ|бг|ею|рф|გე|닷넷|닷컴|삼성|한국|コム|世界|中信|中国|中國|企业|佛山|信息|健康|八卦|公司|公益|台湾|台灣|商城|商店|商标|嘉里|在线|大拿|娱乐|家電|广东|微博|慈善|手机|招聘|政务|政府|新闻|时尚|書籍|机构|游戏|澳門|点看|移动|网址|网店|网站|网络|联通|谷歌|购物|通販|集团|食品|餐厅|香港)/, - dO = new RegExp('['.concat(aO, "!#$%&'*+/=?^_`{|}~-]")), - fO = new RegExp('^'.concat(hO.source, '$')), - mO = (function (s) { - function EmailMatcher() { - var o = (null !== s && s.apply(this, arguments)) || this; - return (o.localPartCharRegex = dO), (o.strictTldRegex = fO), o; - } - return ( - tslib_es6_extends(EmailMatcher, s), - (EmailMatcher.prototype.parseMatches = function (s) { - for ( - var o = this.tagBuilder, - i = this.localPartCharRegex, - u = this.strictTldRegex, - _ = [], - w = s.length, - x = new gO(), - C = { m: 'a', a: 'i', i: 'l', l: 't', t: 'o', o: ':' }, - j = 0, - L = 0, - B = x; - j < w; - - ) { - var $ = s.charAt(j); - switch (L) { - case 0: - stateNonEmailAddress($); - break; - case 1: - stateMailTo(s.charAt(j - 1), $); - break; - case 2: - stateLocalPart($); - break; - case 3: - stateLocalPartDot($); - break; - case 4: - stateAtSign($); - break; - case 5: - stateDomainChar($); - break; - case 6: - stateDomainHyphen($); - break; - case 7: - stateDomainDot($); - break; - default: - throwUnhandledCaseError(L); - } - j++; - } - return captureMatchIfValidAndReset(), _; - function stateNonEmailAddress(s) { - 'm' === s ? beginEmailMatch(1) : i.test(s) && beginEmailMatch(); - } - function stateMailTo(s, o) { - ':' === s - ? i.test(o) - ? ((L = 2), (B = new gO(__assign(__assign({}, B), { hasMailtoPrefix: !0 })))) - : resetToNonEmailMatchState() - : C[s] === o || - (i.test(o) - ? (L = 2) - : '.' === o - ? (L = 3) - : '@' === o - ? (L = 4) - : resetToNonEmailMatchState()); - } - function stateLocalPart(s) { - '.' === s - ? (L = 3) - : '@' === s - ? (L = 4) - : i.test(s) || resetToNonEmailMatchState(); - } - function stateLocalPartDot(s) { - '.' === s || '@' === s - ? resetToNonEmailMatchState() - : i.test(s) - ? (L = 2) - : resetToNonEmailMatchState(); - } - function stateAtSign(s) { - pO.test(s) ? (L = 5) : resetToNonEmailMatchState(); - } - function stateDomainChar(s) { - '.' === s - ? (L = 7) - : '-' === s - ? (L = 6) - : pO.test(s) || captureMatchIfValidAndReset(); - } - function stateDomainHyphen(s) { - '-' === s || '.' === s - ? captureMatchIfValidAndReset() - : pO.test(s) - ? (L = 5) - : captureMatchIfValidAndReset(); - } - function stateDomainDot(s) { - '.' === s || '-' === s - ? captureMatchIfValidAndReset() - : pO.test(s) - ? ((L = 5), (B = new gO(__assign(__assign({}, B), { hasDomainDot: !0 })))) - : captureMatchIfValidAndReset(); - } - function beginEmailMatch(s) { - void 0 === s && (s = 2), (L = s), (B = new gO({ idx: j })); - } - function resetToNonEmailMatchState() { - (L = 0), (B = x); - } - function captureMatchIfValidAndReset() { - if (B.hasDomainDot) { - var i = s.slice(B.idx, j); - /[-.]$/.test(i) && (i = i.slice(0, -1)); - var w = B.hasMailtoPrefix ? i.slice(7) : i; - (function doesEmailHaveValidTld(s) { - var o = s.split('.').pop() || '', - i = o.toLowerCase(); - return u.test(i); - })(w) && - _.push(new WC({ tagBuilder: o, matchedText: i, offset: B.idx, email: w })); - } - resetToNonEmailMatchState(); - } - }), - EmailMatcher - ); - })(YC), - gO = function gO(s) { - void 0 === s && (s = {}), - (this.idx = void 0 !== s.idx ? s.idx : -1), - (this.hasMailtoPrefix = !!s.hasMailtoPrefix), - (this.hasDomainDot = !!s.hasDomainDot); - }, - yO = (function () { - function UrlMatchValidator() {} - return ( - (UrlMatchValidator.isValid = function (s, o) { - return !( - (o && !this.isValidUriScheme(o)) || - this.urlMatchDoesNotHaveProtocolOrDot(s, o) || - (this.urlMatchDoesNotHaveAtLeastOneWordChar(s, o) && !this.isValidIpAddress(s)) || - this.containsMultipleDots(s) - ); - }), - (UrlMatchValidator.isValidIpAddress = function (s) { - var o = new RegExp(this.hasFullProtocolRegex.source + this.ipRegex.source); - return null !== s.match(o); - }), - (UrlMatchValidator.containsMultipleDots = function (s) { - var o = s; - return ( - this.hasFullProtocolRegex.test(s) && (o = s.split('://')[1]), - o.split('/')[0].indexOf('..') > -1 - ); - }), - (UrlMatchValidator.isValidUriScheme = function (s) { - var o = s.match(this.uriSchemeRegex), - i = o && o[0].toLowerCase(); - return 'javascript:' !== i && 'vbscript:' !== i; - }), - (UrlMatchValidator.urlMatchDoesNotHaveProtocolOrDot = function (s, o) { - return !(!s || (o && this.hasFullProtocolRegex.test(o)) || -1 !== s.indexOf('.')); - }), - (UrlMatchValidator.urlMatchDoesNotHaveAtLeastOneWordChar = function (s, o) { - return ( - !(!s || !o) && - !this.hasFullProtocolRegex.test(o) && - !this.hasWordCharAfterProtocolRegex.test(s) - ); - }), - (UrlMatchValidator.hasFullProtocolRegex = /^[A-Za-z][-.+A-Za-z0-9]*:\/\//), - (UrlMatchValidator.uriSchemeRegex = /^[A-Za-z][-.+A-Za-z0-9]*:/), - (UrlMatchValidator.hasWordCharAfterProtocolRegex = new RegExp( - ':[^\\s]*?[' + nO + ']' - )), - (UrlMatchValidator.ipRegex = - /[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?(:[0-9]*)?\/?$/), - UrlMatchValidator - ); - })(), - vO = - ((zC = new RegExp( - '[/?#](?:[' + - aO + - "\\-+&@#/%=~_()|'$*\\[\\]{}?!:,.;^✓]*[" + - aO + - "\\-+&@#/%=~_()|'$*\\[\\]{}✓])?" - )), - new RegExp( - [ - '(?:', - '(', - /(?:[A-Za-z][-.+A-Za-z0-9]{0,63}:(?![A-Za-z][-.+A-Za-z0-9]{0,63}:\/\/)(?!\d+\/?)(?:\/\/)?)/ - .source, - getDomainNameStr(2), - ')', - '|', - '(', - '(//)?', - /(?:www\.)/.source, - getDomainNameStr(6), - ')', - '|', - '(', - '(//)?', - getDomainNameStr(10) + '\\.', - hO.source, - '(?![-' + iO + '])', - ')', - ')', - '(?::[0-9]+)?', - '(?:' + zC.source + ')?' - ].join(''), - 'gi' - )), - bO = new RegExp('[' + aO + ']'), - _O = (function (s) { - function UrlMatcher(o) { - var i = s.call(this, o) || this; - return ( - (i.stripPrefix = { scheme: !0, www: !0 }), - (i.stripTrailingSlash = !0), - (i.decodePercentEncoding = !0), - (i.matcherRegex = vO), - (i.wordCharRegExp = bO), - (i.stripPrefix = o.stripPrefix), - (i.stripTrailingSlash = o.stripTrailingSlash), - (i.decodePercentEncoding = o.decodePercentEncoding), - i - ); - } - return ( - tslib_es6_extends(UrlMatcher, s), - (UrlMatcher.prototype.parseMatches = function (s) { - for ( - var o, - i = this.matcherRegex, - u = this.stripPrefix, - _ = this.stripTrailingSlash, - w = this.decodePercentEncoding, - x = this.tagBuilder, - C = [], - _loop_1 = function () { - var i = o[0], - L = o[1], - B = o[4], - $ = o[5], - V = o[9], - U = o.index, - z = $ || V, - Y = s.charAt(U - 1); - if (!yO.isValid(i, L)) return 'continue'; - if (U > 0 && '@' === Y) return 'continue'; - if (U > 0 && z && j.wordCharRegExp.test(Y)) return 'continue'; - if ( - (/\?$/.test(i) && (i = i.substr(0, i.length - 1)), - j.matchHasUnbalancedClosingParen(i)) - ) - i = i.substr(0, i.length - 1); - else { - var Z = j.matchHasInvalidCharAfterTld(i, L); - Z > -1 && (i = i.substr(0, Z)); - } - var ee = ['http://', 'https://'].find(function (s) { - return !!L && -1 !== L.indexOf(s); - }); - if (ee) { - var ie = i.indexOf(ee); - (i = i.substr(ie)), (L = L.substr(ie)), (U += ie); - } - var ae = L ? 'scheme' : B ? 'www' : 'tld', - le = !!L; - C.push( - new GC({ - tagBuilder: x, - matchedText: i, - offset: U, - urlMatchType: ae, - url: i, - protocolUrlMatch: le, - protocolRelativeMatch: !!z, - stripPrefix: u, - stripTrailingSlash: _, - decodePercentEncoding: w - }) - ); - }, - j = this; - null !== (o = i.exec(s)); - - ) - _loop_1(); - return C; - }), - (UrlMatcher.prototype.matchHasUnbalancedClosingParen = function (s) { - var o, - i = s.charAt(s.length - 1); - if (')' === i) o = '('; - else if (']' === i) o = '['; - else { - if ('}' !== i) return !1; - o = '{'; - } - for (var u = 0, _ = 0, w = s.length - 1; _ < w; _++) { - var x = s.charAt(_); - x === o ? u++ : x === i && (u = Math.max(u - 1, 0)); - } - return 0 === u; - }), - (UrlMatcher.prototype.matchHasInvalidCharAfterTld = function (s, o) { - if (!s) return -1; - var i = 0; - o && ((i = s.indexOf(':')), (s = s.slice(i))); - var u = new RegExp('^((.?//)?[-.' + aO + ']*[-' + aO + ']\\.[-' + aO + ']+)').exec( - s - ); - return null === u - ? -1 - : ((i += u[1].length), - (s = s.slice(u[1].length)), - /^[^-.A-Za-z0-9:\/?#]/.test(s) ? i : -1); - }), - UrlMatcher - ); - })(YC), - EO = new RegExp('[_'.concat(aO, ']')), - wO = (function (s) { - function HashtagMatcher(o) { - var i = s.call(this, o) || this; - return (i.serviceName = 'twitter'), (i.serviceName = o.serviceName), i; - } - return ( - tslib_es6_extends(HashtagMatcher, s), - (HashtagMatcher.prototype.parseMatches = function (s) { - for ( - var o = this.tagBuilder, - i = this.serviceName, - u = [], - _ = s.length, - w = 0, - x = -1, - C = 0; - w < _; - - ) { - var j = s.charAt(w); - switch (C) { - case 0: - stateNone(j); - break; - case 1: - stateNonHashtagWordChar(j); - break; - case 2: - stateHashtagHashChar(j); - break; - case 3: - stateHashtagTextChar(j); - break; - default: - throwUnhandledCaseError(C); - } - w++; - } - return captureMatchIfValid(), u; - function stateNone(s) { - '#' === s ? ((C = 2), (x = w)) : lO.test(s) && (C = 1); - } - function stateNonHashtagWordChar(s) { - lO.test(s) || (C = 0); - } - function stateHashtagHashChar(s) { - C = EO.test(s) ? 3 : lO.test(s) ? 1 : 0; - } - function stateHashtagTextChar(s) { - EO.test(s) || (captureMatchIfValid(), (x = -1), (C = lO.test(s) ? 1 : 0)); - } - function captureMatchIfValid() { - if (x > -1 && w - x <= 140) { - var _ = s.slice(x, w), - C = new KC({ - tagBuilder: o, - matchedText: _, - offset: x, - serviceName: i, - hashtag: _.slice(1) - }); - u.push(C); - } - } - }), - HashtagMatcher - ); - })(YC), - SO = ['twitter', 'facebook', 'instagram', 'tiktok'], - xO = new RegExp( - '' - .concat( - /(?:(?:(?:(\+)?\d{1,3}[-\040.]?)?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]?\d{4})|(?:(\+)(?:9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)[-\040.]?(?:\d[-\040.]?){6,12}\d+))([,;]+[0-9]+#?)*/ - .source, - '|' - ) - .concat( - /(0([1-9]{1}-?[1-9]\d{3}|[1-9]{2}-?\d{3}|[1-9]{2}\d{1}-?\d{2}|[1-9]{2}\d{2}-?\d{1})-?\d{4}|0[789]0-?\d{4}-?\d{4}|050-?\d{4}-?\d{4})/ - .source - ), - 'g' - ), - kO = (function (s) { - function PhoneMatcher() { - var o = (null !== s && s.apply(this, arguments)) || this; - return (o.matcherRegex = xO), o; - } - return ( - tslib_es6_extends(PhoneMatcher, s), - (PhoneMatcher.prototype.parseMatches = function (s) { - for ( - var o, i = this.matcherRegex, u = this.tagBuilder, _ = []; - null !== (o = i.exec(s)); - - ) { - var w = o[0], - x = w.replace(/[^0-9,;#]/g, ''), - C = !(!o[1] && !o[2]), - j = 0 == o.index ? '' : s.substr(o.index - 1, 1), - L = s.substr(o.index + w.length, 1), - B = !j.match(/\d/) && !L.match(/\d/); - this.testMatch(o[3]) && - this.testMatch(w) && - B && - _.push( - new JC({ - tagBuilder: u, - matchedText: w, - offset: o.index, - number: x, - plusSign: C - }) - ); - } - return _; - }), - (PhoneMatcher.prototype.testMatch = function (s) { - return QC.test(s); - }), - PhoneMatcher - ); - })(YC), - CO = new RegExp('@[_'.concat(aO, ']{1,50}(?![_').concat(aO, '])'), 'g'), - OO = new RegExp('@[_.'.concat(aO, ']{1,30}(?![_').concat(aO, '])'), 'g'), - AO = new RegExp('@[-_.'.concat(aO, ']{1,50}(?![-_').concat(aO, '])'), 'g'), - jO = new RegExp( - '@[_.'.concat(aO, ']{1,23}[_').concat(aO, '](?![_').concat(aO, '])'), - 'g' - ), - IO = new RegExp('[^' + aO + ']'), - PO = (function (s) { - function MentionMatcher(o) { - var i = s.call(this, o) || this; - return ( - (i.serviceName = 'twitter'), - (i.matcherRegexes = { twitter: CO, instagram: OO, soundcloud: AO, tiktok: jO }), - (i.nonWordCharRegex = IO), - (i.serviceName = o.serviceName), - i - ); - } - return ( - tslib_es6_extends(MentionMatcher, s), - (MentionMatcher.prototype.parseMatches = function (s) { - var o, - i = this.serviceName, - u = this.matcherRegexes[this.serviceName], - _ = this.nonWordCharRegex, - w = this.tagBuilder, - x = []; - if (!u) return x; - for (; null !== (o = u.exec(s)); ) { - var C = o.index, - j = s.charAt(C - 1); - if (0 === C || _.test(j)) { - var L = o[0].replace(/\.+$/g, ''), - B = L.slice(1); - x.push( - new HC({ - tagBuilder: w, - matchedText: L, - offset: C, - serviceName: i, - mention: B - }) - ); - } - } - return x; - }), - MentionMatcher - ); - })(YC); - function parseHtml(s, o) { - for ( - var i = o.onOpenTag, - u = o.onCloseTag, - _ = o.onText, - w = o.onComment, - x = o.onDoctype, - C = new MO(), - j = 0, - L = s.length, - B = 0, - $ = 0, - V = C; - j < L; - - ) { - var U = s.charAt(j); - switch (B) { - case 0: - stateData(U); - break; - case 1: - stateTagOpen(U); - break; - case 2: - stateEndTagOpen(U); - break; - case 3: - stateTagName(U); - break; - case 4: - stateBeforeAttributeName(U); - break; - case 5: - stateAttributeName(U); - break; - case 6: - stateAfterAttributeName(U); - break; - case 7: - stateBeforeAttributeValue(U); - break; - case 8: - stateAttributeValueDoubleQuoted(U); - break; - case 9: - stateAttributeValueSingleQuoted(U); - break; - case 10: - stateAttributeValueUnquoted(U); - break; - case 11: - stateAfterAttributeValueQuoted(U); - break; - case 12: - stateSelfClosingStartTag(U); - break; - case 13: - stateMarkupDeclarationOpen(U); - break; - case 14: - stateCommentStart(U); - break; - case 15: - stateCommentStartDash(U); - break; - case 16: - stateComment(U); - break; - case 17: - stateCommentEndDash(U); - break; - case 18: - stateCommentEnd(U); - break; - case 19: - stateCommentEndBang(U); - break; - case 20: - stateDoctype(U); - break; - default: - throwUnhandledCaseError(B); - } - j++; - } - function stateData(s) { - '<' === s && startNewTag(); - } - function stateTagOpen(s) { - '!' === s - ? (B = 13) - : '/' === s - ? ((B = 2), (V = new MO(__assign(__assign({}, V), { isClosing: !0 })))) - : '<' === s - ? startNewTag() - : XC.test(s) - ? ((B = 3), (V = new MO(__assign(__assign({}, V), { isOpening: !0 })))) - : ((B = 0), (V = C)); - } - function stateTagName(s) { - eO.test(s) - ? ((V = new MO(__assign(__assign({}, V), { name: captureTagName() }))), (B = 4)) - : '<' === s - ? startNewTag() - : '/' === s - ? ((V = new MO(__assign(__assign({}, V), { name: captureTagName() }))), (B = 12)) - : '>' === s - ? ((V = new MO(__assign(__assign({}, V), { name: captureTagName() }))), - emitTagAndPreviousTextNode()) - : XC.test(s) || ZC.test(s) || ':' === s || resetToDataState(); - } - function stateEndTagOpen(s) { - '>' === s ? resetToDataState() : XC.test(s) ? (B = 3) : resetToDataState(); - } - function stateBeforeAttributeName(s) { - eO.test(s) || - ('/' === s - ? (B = 12) - : '>' === s - ? emitTagAndPreviousTextNode() - : '<' === s - ? startNewTag() - : '=' === s || tO.test(s) || rO.test(s) - ? resetToDataState() - : (B = 5)); - } - function stateAttributeName(s) { - eO.test(s) - ? (B = 6) - : '/' === s - ? (B = 12) - : '=' === s - ? (B = 7) - : '>' === s - ? emitTagAndPreviousTextNode() - : '<' === s - ? startNewTag() - : tO.test(s) && resetToDataState(); - } - function stateAfterAttributeName(s) { - eO.test(s) || - ('/' === s - ? (B = 12) - : '=' === s - ? (B = 7) - : '>' === s - ? emitTagAndPreviousTextNode() - : '<' === s - ? startNewTag() - : tO.test(s) - ? resetToDataState() - : (B = 5)); - } - function stateBeforeAttributeValue(s) { - eO.test(s) || - ('"' === s - ? (B = 8) - : "'" === s - ? (B = 9) - : /[>=`]/.test(s) - ? resetToDataState() - : '<' === s - ? startNewTag() - : (B = 10)); - } - function stateAttributeValueDoubleQuoted(s) { - '"' === s && (B = 11); - } - function stateAttributeValueSingleQuoted(s) { - "'" === s && (B = 11); - } - function stateAttributeValueUnquoted(s) { - eO.test(s) - ? (B = 4) - : '>' === s - ? emitTagAndPreviousTextNode() - : '<' === s && startNewTag(); - } - function stateAfterAttributeValueQuoted(s) { - eO.test(s) - ? (B = 4) - : '/' === s - ? (B = 12) - : '>' === s - ? emitTagAndPreviousTextNode() - : '<' === s - ? startNewTag() - : ((B = 4), - (function reconsumeCurrentCharacter() { - j--; - })()); - } - function stateSelfClosingStartTag(s) { - '>' === s - ? ((V = new MO(__assign(__assign({}, V), { isClosing: !0 }))), - emitTagAndPreviousTextNode()) - : (B = 4); - } - function stateMarkupDeclarationOpen(o) { - '--' === s.substr(j, 2) - ? ((j += 2), (V = new MO(__assign(__assign({}, V), { type: 'comment' }))), (B = 14)) - : 'DOCTYPE' === s.substr(j, 7).toUpperCase() - ? ((j += 7), (V = new MO(__assign(__assign({}, V), { type: 'doctype' }))), (B = 20)) - : resetToDataState(); - } - function stateCommentStart(s) { - '-' === s ? (B = 15) : '>' === s ? resetToDataState() : (B = 16); - } - function stateCommentStartDash(s) { - '-' === s ? (B = 18) : '>' === s ? resetToDataState() : (B = 16); - } - function stateComment(s) { - '-' === s && (B = 17); - } - function stateCommentEndDash(s) { - B = '-' === s ? 18 : 16; - } - function stateCommentEnd(s) { - '>' === s ? emitTagAndPreviousTextNode() : '!' === s ? (B = 19) : '-' === s || (B = 16); - } - function stateCommentEndBang(s) { - '-' === s ? (B = 17) : '>' === s ? emitTagAndPreviousTextNode() : (B = 16); - } - function stateDoctype(s) { - '>' === s ? emitTagAndPreviousTextNode() : '<' === s && startNewTag(); - } - function resetToDataState() { - (B = 0), (V = C); - } - function startNewTag() { - (B = 1), (V = new MO({ idx: j })); - } - function emitTagAndPreviousTextNode() { - var o = s.slice($, V.idx); - o && _(o, $), - 'comment' === V.type - ? w(V.idx) - : 'doctype' === V.type - ? x(V.idx) - : (V.isOpening && i(V.name, V.idx), V.isClosing && u(V.name, V.idx)), - resetToDataState(), - ($ = j + 1); - } - function captureTagName() { - var o = V.idx + (V.isClosing ? 2 : 1); - return s.slice(o, j).toLowerCase(); - } - $ < j && - (function emitText() { - var o = s.slice($, j); - _(o, $), ($ = j + 1); - })(); - } - var MO = function MO(s) { - void 0 === s && (s = {}), - (this.idx = void 0 !== s.idx ? s.idx : -1), - (this.type = s.type || 'tag'), - (this.name = s.name || ''), - (this.isOpening = !!s.isOpening), - (this.isClosing = !!s.isClosing); - }, - TO = (function () { - function Autolinker(s) { - void 0 === s && (s = {}), - (this.version = Autolinker.version), - (this.urls = {}), - (this.email = !0), - (this.phone = !0), - (this.hashtag = !1), - (this.mention = !1), - (this.newWindow = !0), - (this.stripPrefix = { scheme: !0, www: !0 }), - (this.stripTrailingSlash = !0), - (this.decodePercentEncoding = !0), - (this.truncate = { length: 0, location: 'end' }), - (this.className = ''), - (this.replaceFn = null), - (this.context = void 0), - (this.sanitizeHtml = !1), - (this.matchers = null), - (this.tagBuilder = null), - (this.urls = this.normalizeUrlsCfg(s.urls)), - (this.email = 'boolean' == typeof s.email ? s.email : this.email), - (this.phone = 'boolean' == typeof s.phone ? s.phone : this.phone), - (this.hashtag = s.hashtag || this.hashtag), - (this.mention = s.mention || this.mention), - (this.newWindow = 'boolean' == typeof s.newWindow ? s.newWindow : this.newWindow), - (this.stripPrefix = this.normalizeStripPrefixCfg(s.stripPrefix)), - (this.stripTrailingSlash = - 'boolean' == typeof s.stripTrailingSlash - ? s.stripTrailingSlash - : this.stripTrailingSlash), - (this.decodePercentEncoding = - 'boolean' == typeof s.decodePercentEncoding - ? s.decodePercentEncoding - : this.decodePercentEncoding), - (this.sanitizeHtml = s.sanitizeHtml || !1); - var o = this.mention; - if (!1 !== o && -1 === ['twitter', 'instagram', 'soundcloud', 'tiktok'].indexOf(o)) - throw new Error("invalid `mention` cfg '".concat(o, "' - see docs")); - var i = this.hashtag; - if (!1 !== i && -1 === SO.indexOf(i)) - throw new Error("invalid `hashtag` cfg '".concat(i, "' - see docs")); - (this.truncate = this.normalizeTruncateCfg(s.truncate)), - (this.className = s.className || this.className), - (this.replaceFn = s.replaceFn || this.replaceFn), - (this.context = s.context || this); - } - return ( - (Autolinker.link = function (s, o) { - return new Autolinker(o).link(s); - }), - (Autolinker.parse = function (s, o) { - return new Autolinker(o).parse(s); - }), - (Autolinker.prototype.normalizeUrlsCfg = function (s) { - return ( - null == s && (s = !0), - 'boolean' == typeof s - ? { schemeMatches: s, wwwMatches: s, tldMatches: s } - : { - schemeMatches: 'boolean' != typeof s.schemeMatches || s.schemeMatches, - wwwMatches: 'boolean' != typeof s.wwwMatches || s.wwwMatches, - tldMatches: 'boolean' != typeof s.tldMatches || s.tldMatches - } - ); - }), - (Autolinker.prototype.normalizeStripPrefixCfg = function (s) { - return ( - null == s && (s = !0), - 'boolean' == typeof s - ? { scheme: s, www: s } - : { - scheme: 'boolean' != typeof s.scheme || s.scheme, - www: 'boolean' != typeof s.www || s.www - } - ); - }), - (Autolinker.prototype.normalizeTruncateCfg = function (s) { - return 'number' == typeof s - ? { length: s, location: 'end' } - : (function defaults(s, o) { - for (var i in o) o.hasOwnProperty(i) && void 0 === s[i] && (s[i] = o[i]); - return s; - })(s || {}, { length: Number.POSITIVE_INFINITY, location: 'end' }); - }), - (Autolinker.prototype.parse = function (s) { - var o = this, - i = ['a', 'style', 'script'], - u = 0, - _ = []; - return ( - parseHtml(s, { - onOpenTag: function (s) { - i.indexOf(s) >= 0 && u++; - }, - onText: function (s, i) { - if (0 === u) { - var w = (function splitAndCapture(s, o) { - if (!o.global) - throw new Error("`splitRegex` must have the 'g' flag set"); - for (var i, u = [], _ = 0; (i = o.exec(s)); ) - u.push(s.substring(_, i.index)), - u.push(i[0]), - (_ = i.index + i[0].length); - return u.push(s.substring(_)), u; - })(s, /( | |<|<|>|>|"|"|')/gi), - x = i; - w.forEach(function (s, i) { - if (i % 2 == 0) { - var u = o.parseText(s, x); - _.push.apply(_, u); - } - x += s.length; - }); - } - }, - onCloseTag: function (s) { - i.indexOf(s) >= 0 && (u = Math.max(u - 1, 0)); - }, - onComment: function (s) {}, - onDoctype: function (s) {} - }), - (_ = this.compactMatches(_)), - (_ = this.removeUnwantedMatches(_)) - ); - }), - (Autolinker.prototype.compactMatches = function (s) { - s.sort(function (s, o) { - return s.getOffset() - o.getOffset(); - }); - for (var o = 0; o < s.length - 1; ) { - var i = s[o], - u = i.getOffset(), - _ = i.getMatchedText().length, - w = u + _; - if (o + 1 < s.length) { - if (s[o + 1].getOffset() === u) { - var x = s[o + 1].getMatchedText().length > _ ? o : o + 1; - s.splice(x, 1); - continue; - } - if (s[o + 1].getOffset() < w) { - s.splice(o + 1, 1); - continue; - } - } - o++; - } - return s; - }), - (Autolinker.prototype.removeUnwantedMatches = function (s) { - return ( - this.hashtag || - utils_remove(s, function (s) { - return 'hashtag' === s.getType(); - }), - this.email || - utils_remove(s, function (s) { - return 'email' === s.getType(); - }), - this.phone || - utils_remove(s, function (s) { - return 'phone' === s.getType(); - }), - this.mention || - utils_remove(s, function (s) { - return 'mention' === s.getType(); - }), - this.urls.schemeMatches || - utils_remove(s, function (s) { - return 'url' === s.getType() && 'scheme' === s.getUrlMatchType(); - }), - this.urls.wwwMatches || - utils_remove(s, function (s) { - return 'url' === s.getType() && 'www' === s.getUrlMatchType(); - }), - this.urls.tldMatches || - utils_remove(s, function (s) { - return 'url' === s.getType() && 'tld' === s.getUrlMatchType(); - }), - s - ); - }), - (Autolinker.prototype.parseText = function (s, o) { - void 0 === o && (o = 0), (o = o || 0); - for (var i = this.getMatchers(), u = [], _ = 0, w = i.length; _ < w; _++) { - for (var x = i[_].parseMatches(s), C = 0, j = x.length; C < j; C++) - x[C].setOffset(o + x[C].getOffset()); - u.push.apply(u, x); - } - return u; - }), - (Autolinker.prototype.link = function (s) { - if (!s) return ''; - this.sanitizeHtml && (s = s.replace(//g, '>')); - for (var o = this.parse(s), i = [], u = 0, _ = 0, w = o.length; _ < w; _++) { - var x = o[_]; - i.push(s.substring(u, x.getOffset())), - i.push(this.createMatchReturnVal(x)), - (u = x.getOffset() + x.getMatchedText().length); - } - return i.push(s.substring(u)), i.join(''); - }), - (Autolinker.prototype.createMatchReturnVal = function (s) { - var o; - return ( - this.replaceFn && (o = this.replaceFn.call(this.context, s)), - 'string' == typeof o - ? o - : !1 === o - ? s.getMatchedText() - : o instanceof $C - ? o.toAnchorString() - : s.buildTag().toAnchorString() - ); - }), - (Autolinker.prototype.getMatchers = function () { - if (this.matchers) return this.matchers; - var s = this.getTagBuilder(), - o = [ - new wO({ tagBuilder: s, serviceName: this.hashtag }), - new mO({ tagBuilder: s }), - new kO({ tagBuilder: s }), - new PO({ tagBuilder: s, serviceName: this.mention }), - new _O({ - tagBuilder: s, - stripPrefix: this.stripPrefix, - stripTrailingSlash: this.stripTrailingSlash, - decodePercentEncoding: this.decodePercentEncoding - }) - ]; - return (this.matchers = o); - }), - (Autolinker.prototype.getTagBuilder = function () { - var s = this.tagBuilder; - return ( - s || - (s = this.tagBuilder = - new VC({ - newWindow: this.newWindow, - truncate: this.truncate, - className: this.className - })), - s - ); - }), - (Autolinker.version = '3.16.2'), - (Autolinker.AnchorTagBuilder = VC), - (Autolinker.HtmlTag = $C), - (Autolinker.matcher = { - Email: mO, - Hashtag: wO, - Matcher: YC, - Mention: PO, - Phone: kO, - Url: _O - }), - (Autolinker.match = { - Email: WC, - Hashtag: KC, - Match: UC, - Mention: HC, - Phone: JC, - Url: GC - }), - Autolinker - ); - })(); - const NO = TO; - var RO = /www|@|\:\/\//; - function isLinkOpen(s) { - return /^\s]/i.test(s); - } - function isLinkClose(s) { - return /^<\/a\s*>/i.test(s); - } - function createLinkifier() { - var s = [], - o = new NO({ - stripPrefix: !1, - url: !0, - email: !0, - replaceFn: function (o) { - switch (o.getType()) { - case 'url': - s.push({ text: o.matchedText, url: o.getUrl() }); - break; - case 'email': - s.push({ - text: o.matchedText, - url: 'mailto:' + o.getEmail().replace(/^mailto:/i, '') - }); - } - return !1; - } - }); - return { links: s, autolinker: o }; - } - function parseTokens(s) { - var o, - i, - u, - _, - w, - x, - C, - j, - L, - B, - $, - V, - U, - z = s.tokens, - Y = null; - for (i = 0, u = z.length; i < u; i++) - if ('inline' === z[i].type) - for ($ = 0, o = (_ = z[i].children).length - 1; o >= 0; o--) - if ('link_close' !== (w = _[o]).type) { - if ( - ('htmltag' === w.type && - (isLinkOpen(w.content) && $ > 0 && $--, isLinkClose(w.content) && $++), - !($ > 0) && 'text' === w.type && RO.test(w.content)) - ) { - if ( - (Y || ((V = (Y = createLinkifier()).links), (U = Y.autolinker)), - (x = w.content), - (V.length = 0), - U.link(x), - !V.length) - ) - continue; - for (C = [], B = w.level, j = 0; j < V.length; j++) - s.inline.validateLink(V[j].url) && - ((L = x.indexOf(V[j].text)) && - C.push({ type: 'text', content: x.slice(0, L), level: B }), - C.push({ type: 'link_open', href: V[j].url, title: '', level: B++ }), - C.push({ type: 'text', content: V[j].text, level: B }), - C.push({ type: 'link_close', level: --B }), - (x = x.slice(L + V[j].text.length))); - x.length && C.push({ type: 'text', content: x, level: B }), - (z[i].children = _ = [].concat(_.slice(0, o), C, _.slice(o + 1))); - } - } else for (o--; _[o].level !== w.level && 'link_open' !== _[o].type; ) o--; - } - function linkify(s) { - s.core.ruler.push('linkify', parseTokens); - } - var DO = __webpack_require__(42838), - LO = __webpack_require__.n(DO); - LO().addHook && - LO().addHook('beforeSanitizeElements', function (s) { - return s.href && s.setAttribute('rel', 'noopener noreferrer'), s; - }); - const BO = function Markdown({ - source: s, - className: o = '', - getConfigs: i = () => ({ useUnsafeMarkdown: !1 }) - }) { - if ('string' != typeof s) return null; - const u = new Remarkable({ - html: !0, - typographer: !0, - breaks: !0, - linkTarget: '_blank' - }).use(linkify); - u.core.ruler.disable(['replacements', 'smartquotes']); - const { useUnsafeMarkdown: _ } = i(), - w = u.render(s), - x = sanitizer(w, { useUnsafeMarkdown: _ }); - return s && w && x - ? Pe.createElement('div', { - className: Hn()(o, 'markdown'), - dangerouslySetInnerHTML: { __html: x } - }) - : null; - }; - function sanitizer(s, { useUnsafeMarkdown: o = !1 } = {}) { - const i = o, - u = o ? [] : ['style', 'class']; - return ( - o && - !sanitizer.hasWarnedAboutDeprecation && - (console.warn( - 'useUnsafeMarkdown display configuration parameter is deprecated since >3.26.0 and will be removed in v4.0.0.' - ), - (sanitizer.hasWarnedAboutDeprecation = !0)), - LO().sanitize(s, { - ADD_ATTR: ['target'], - FORBID_TAGS: ['style', 'form'], - ALLOW_DATA_ATTR: i, - FORBID_ATTR: u - }) - ); - } - sanitizer.hasWarnedAboutDeprecation = !1; - class BaseLayout extends Pe.Component { - render() { - const { errSelectors: s, specSelectors: o, getComponent: i } = this.props, - u = i('SvgAssets'), - _ = i('InfoContainer', !0), - w = i('VersionPragmaFilter'), - x = i('operations', !0), - C = i('Models', !0), - j = i('Webhooks', !0), - L = i('Row'), - B = i('Col'), - $ = i('errors', !0), - V = i('ServersContainer', !0), - U = i('SchemesContainer', !0), - z = i('AuthorizeBtnContainer', !0), - Y = i('FilterContainer', !0), - Z = o.isSwagger2(), - ee = o.isOAS3(), - ie = o.isOAS31(), - ae = !o.specStr(), - le = o.loadingStatus(); - let ce = null; - if ( - ('loading' === le && - (ce = Pe.createElement( - 'div', - { className: 'info' }, - Pe.createElement( - 'div', - { className: 'loading-container' }, - Pe.createElement('div', { className: 'loading' }) - ) - )), - 'failed' === le && - (ce = Pe.createElement( - 'div', - { className: 'info' }, - Pe.createElement( - 'div', - { className: 'loading-container' }, - Pe.createElement( - 'h4', - { className: 'title' }, - 'Failed to load API definition.' - ), - Pe.createElement($, null) - ) - )), - 'failedConfig' === le) - ) { - const o = s.lastError(), - i = o ? o.get('message') : ''; - ce = Pe.createElement( - 'div', - { className: 'info failed-config' }, - Pe.createElement( - 'div', - { className: 'loading-container' }, - Pe.createElement( - 'h4', - { className: 'title' }, - 'Failed to load remote configuration.' - ), - Pe.createElement('p', null, i) - ) - ); - } - if ( - (!ce && ae && (ce = Pe.createElement('h4', null, 'No API definition provided.')), ce) - ) - return Pe.createElement( - 'div', - { className: 'swagger-ui' }, - Pe.createElement('div', { className: 'loading-container' }, ce) - ); - const pe = o.servers(), - de = o.schemes(), - fe = pe && pe.size, - ye = de && de.size, - be = !!o.securityDefinitions(); - return Pe.createElement( - 'div', - { className: 'swagger-ui' }, - Pe.createElement(u, null), - Pe.createElement( - w, - { isSwagger2: Z, isOAS3: ee, alsoShow: Pe.createElement($, null) }, - Pe.createElement($, null), - Pe.createElement( - L, - { className: 'information-container' }, - Pe.createElement(B, { mobile: 12 }, Pe.createElement(_, null)) - ), - fe || ye || be - ? Pe.createElement( - 'div', - { className: 'scheme-container' }, - Pe.createElement( - B, - { className: 'schemes wrapper', mobile: 12 }, - fe || ye - ? Pe.createElement( - 'div', - { className: 'schemes-server-container' }, - fe ? Pe.createElement(V, null) : null, - ye ? Pe.createElement(U, null) : null - ) - : null, - be ? Pe.createElement(z, null) : null - ) - ) - : null, - Pe.createElement(Y, null), - Pe.createElement( - L, - null, - Pe.createElement(B, { mobile: 12, desktop: 12 }, Pe.createElement(x, null)) - ), - ie && - Pe.createElement( - L, - { className: 'webhooks-container' }, - Pe.createElement(B, { mobile: 12, desktop: 12 }, Pe.createElement(j, null)) - ), - Pe.createElement( - L, - null, - Pe.createElement(B, { mobile: 12, desktop: 12 }, Pe.createElement(C, null)) - ) - ) - ); - } - } - const core_components = () => ({ - components: { - App: fk, - authorizationPopup: AuthorizationPopup, - authorizeBtn: AuthorizeBtn, - AuthorizeBtnContainer, - authorizeOperationBtn: AuthorizeOperationBtn, - auths: Auths, - AuthItem: auth_item_Auths, - authError: AuthError, - oauth2: Oauth2, - apiKeyAuth: ApiKeyAuth, - basicAuth: BasicAuth, - clear: Clear, - liveResponse: LiveResponse, - InitializedInput, - info: qk, - InfoContainer, - InfoUrl, - InfoBasePath, - Contact: Vk, - License: zk, - JumpToPath, - CopyToClipboardBtn, - onlineValidatorBadge: OnlineValidatorBadge, - operations: Operations, - operation: operation_Operation, - OperationSummary, - OperationSummaryMethod, - OperationSummaryPath, - responses: responses_Responses, - response: response_Response, - ResponseExtension: response_extension, - responseBody: ResponseBody, - parameters: Parameters, - parameterRow: ParameterRow, - execute: Execute, - headers: headers_Headers, - errors: Errors, - contentType: ContentType, - overview: Overview, - footer: Footer, - FilterContainer, - ParamBody, - curl: Curl, - Property: property, - TryItOutButton, - Markdown: BO, - BaseLayout, - VersionPragmaFilter, - VersionStamp: version_stamp, - OperationExt: operation_extensions, - OperationExtRow: operation_extension_row, - ParameterExt: parameter_extension, - ParameterIncludeEmpty, - OperationTag, - OperationContainer, - OpenAPIVersion: openapi_version, - DeepLink: deep_link, - SvgAssets: svg_assets, - Example: example_Example, - ExamplesSelect, - ExamplesSelectValueRetainer - } - }), - form_components = () => ({ components: { ...ye } }), - base = () => [ - configsPlugin, - util, - logs, - view, - view_legacy, - plugins_spec, - err, - icons, - plugins_layout, - json_schema_5, - json_schema_5_samples, - core_components, - form_components, - swagger_client, - auth, - downloadUrlPlugin, - deep_linking, - filter, - on_complete, - plugins_request_snippets, - syntax_highlighting, - versions, - safe_render() - ], - FO = (0, qe.Map)(); - function onlyOAS3(s) { - return (o, i) => - (...u) => { - if (i.getSystem().specSelectors.isOAS3()) { - const o = s(...u); - return 'function' == typeof o ? o(i) : o; - } - return o(...u); - }; - } - const qO = onlyOAS3(Ss()(null)), - $O = onlyOAS3((s, o) => (s) => s.getSystem().specSelectors.findSchema(o)), - VO = onlyOAS3(() => (s) => { - const o = s.getSystem().specSelectors.specJson().getIn(['components', 'schemas']); - return qe.Map.isMap(o) ? o : FO; - }), - UO = onlyOAS3(() => (s) => s.getSystem().specSelectors.specJson().hasIn(['servers', 0])), - zO = onlyOAS3(Ut(Ms, (s) => s.getIn(['components', 'securitySchemes']) || null)), - wrap_selectors_validOperationMethods = - (s, o) => - (i, ...u) => - o.specSelectors.isOAS3() ? o.oas3Selectors.validOperationMethods() : s(...u), - WO = qO, - KO = qO, - HO = qO, - JO = qO, - GO = qO; - const YO = (function wrap_selectors_onlyOAS3(s) { - return (o, i) => - (...u) => { - if (i.getSystem().specSelectors.isOAS3()) { - let o = i - .getState() - .getIn(['spec', 'resolvedSubtrees', 'components', 'securitySchemes']); - return s(i, o, ...u); - } - return o(...u); - }; - })( - Ut( - (s) => s, - ({ specSelectors: s }) => s.securityDefinitions(), - (s, o) => { - let i = (0, qe.List)(); - return o - ? (o.entrySeq().forEach(([s, o]) => { - const u = o.get('type'); - if ( - ('oauth2' === u && - o - .get('flows') - .entrySeq() - .forEach(([u, _]) => { - let w = (0, qe.fromJS)({ - flow: u, - authorizationUrl: _.get('authorizationUrl'), - tokenUrl: _.get('tokenUrl'), - scopes: _.get('scopes'), - type: o.get('type'), - description: o.get('description') - }); - i = i.push(new qe.Map({ [s]: w.filter((s) => void 0 !== s) })); - }), - ('http' !== u && 'apiKey' !== u) || (i = i.push(new qe.Map({ [s]: o }))), - 'openIdConnect' === u && o.get('openIdConnectData')) - ) { - let u = o.get('openIdConnectData'); - ( - u.get('grant_types_supported') || ['authorization_code', 'implicit'] - ).forEach((_) => { - let w = - u.get('scopes_supported') && - u.get('scopes_supported').reduce((s, o) => s.set(o, ''), new qe.Map()), - x = (0, qe.fromJS)({ - flow: _, - authorizationUrl: u.get('authorization_endpoint'), - tokenUrl: u.get('token_endpoint'), - scopes: w, - type: 'oauth2', - openIdConnectUrl: o.get('openIdConnectUrl') - }); - i = i.push(new qe.Map({ [s]: x.filter((s) => void 0 !== s) })); - }); - } - }), - i) - : i; - } - ) - ); - function OAS3ComponentWrapFactory(s) { - return (o, i) => (u) => - 'function' == typeof i.specSelectors?.isOAS3 - ? i.specSelectors.isOAS3() - ? Pe.createElement(s, Rn()({}, u, i, { Ori: o })) - : Pe.createElement(o, u) - : (console.warn("OAS3 wrapper: couldn't get spec"), null); - } - const XO = (0, qe.Map)(), - selectors_isSwagger2 = () => (s) => - (function isSwagger2(s) { - const o = s.get('swagger'); - return 'string' == typeof o && '2.0' === o; - })(s.getSystem().specSelectors.specJson()), - selectors_isOAS30 = () => (s) => - (function isOAS30(s) { - const o = s.get('openapi'); - return 'string' == typeof o && /^3\.0\.([0123])(?:-rc[012])?$/.test(o); - })(s.getSystem().specSelectors.specJson()), - selectors_isOAS3 = () => (s) => s.getSystem().specSelectors.isOAS30(); - function selectors_onlyOAS3(s) { - return (o, ...i) => - (u) => { - if (u.specSelectors.isOAS3()) { - const _ = s(o, ...i); - return 'function' == typeof _ ? _(u) : _; - } - return null; - }; - } - const ZO = selectors_onlyOAS3(() => (s) => s.specSelectors.specJson().get('servers', XO)), - findSchema = (s, o) => { - const i = s.getIn(['resolvedSubtrees', 'components', 'schemas', o], null), - u = s.getIn(['json', 'components', 'schemas', o], null); - return i || u || null; - }, - QO = selectors_onlyOAS3((s, { callbacks: o, specPath: i }) => (s) => { - const u = s.specSelectors.validOperationMethods(); - return qe.Map.isMap(o) - ? o - .reduce( - (s, o, _) => { - if (!qe.Map.isMap(o)) return s; - const w = o.reduce( - (s, o, w) => { - if (!qe.Map.isMap(o)) return s; - const x = o - .entrySeq() - .filter(([s]) => u.includes(s)) - .map(([s, o]) => ({ - operation: (0, qe.Map)({ operation: o }), - method: s, - path: w, - callbackName: _, - specPath: i.concat([_, w, s]) - })); - return s.concat(x); - }, - (0, qe.List)() - ); - return s.concat(w); - }, - (0, qe.List)() - ) - .groupBy((s) => s.callbackName) - .map((s) => s.toArray()) - .toObject() - : {}; - }), - callbacks = ({ callbacks: s, specPath: o, specSelectors: i, getComponent: u }) => { - const _ = i.callbacksOperations({ callbacks: s, specPath: o }), - w = Object.keys(_), - x = u('OperationContainer', !0); - return 0 === w.length - ? Pe.createElement('span', null, 'No callbacks') - : Pe.createElement( - 'div', - null, - w.map((s) => - Pe.createElement( - 'div', - { key: `${s}` }, - Pe.createElement('h2', null, s), - _[s].map((o) => - Pe.createElement(x, { - key: `${s}-${o.path}-${o.method}`, - op: o.operation, - tag: 'callbacks', - method: o.method, - path: o.path, - specPath: o.specPath, - allowTryItOut: !1 - }) - ) - ) - ) - ); - }, - getDefaultRequestBodyValue = (s, o, i, u) => { - const _ = s.getIn(['content', o]) ?? (0, qe.OrderedMap)(), - w = _.get('schema', (0, qe.OrderedMap)()).toJS(), - x = void 0 !== _.get('examples'), - C = _.get('example'), - j = x ? _.getIn(['examples', i, 'value']) : C; - return stringify(u.getSampleSchema(w, o, { includeWriteOnly: !0 }, j)); - }, - components_request_body = ({ - userHasEditedBody: s, - requestBody: o, - requestBodyValue: i, - requestBodyInclusionSetting: u, - requestBodyErrors: _, - getComponent: w, - getConfigs: x, - specSelectors: C, - fn: j, - contentType: L, - isExecute: B, - specPath: $, - onChange: V, - onChangeIncludeEmpty: U, - activeExamplesKey: z, - updateActiveExamplesKey: Y, - setRetainRequestBodyValueFlag: Z - }) => { - const handleFile = (s) => { - V(s.target.files[0]); - }, - setIsIncludedOptions = (s) => { - let o = { key: s, shouldDispatchInit: !1, defaultValue: !0 }; - return 'no value' === u.get(s, 'no value') && (o.shouldDispatchInit = !0), o; - }, - ee = w('Markdown', !0), - ie = w('modelExample'), - ae = w('RequestBodyEditor'), - le = w('HighlightCode', !0), - ce = w('ExamplesSelectValueRetainer'), - pe = w('Example'), - de = w('ParameterIncludeEmpty'), - { showCommonExtensions: fe } = x(), - ye = o?.get('description') ?? null, - be = o?.get('content') ?? new qe.OrderedMap(); - L = L || be.keySeq().first() || ''; - const _e = be.get(L) ?? (0, qe.OrderedMap)(), - we = _e.get('schema', (0, qe.OrderedMap)()), - Se = _e.get('examples', null), - xe = Se?.map((s, i) => { - const u = s?.get('value', null); - return u && (s = s.set('value', getDefaultRequestBodyValue(o, L, i, j), u)), s; - }); - if (((_ = qe.List.isList(_) ? _ : (0, qe.List)()), !_e.size)) return null; - const Te = 'object' === _e.getIn(['schema', 'type']), - Re = 'binary' === _e.getIn(['schema', 'format']), - $e = 'base64' === _e.getIn(['schema', 'format']); - if ( - 'application/octet-stream' === L || - 0 === L.indexOf('image/') || - 0 === L.indexOf('audio/') || - 0 === L.indexOf('video/') || - Re || - $e - ) { - const s = w('Input'); - return B - ? Pe.createElement(s, { type: 'file', onChange: handleFile }) - : Pe.createElement( - 'i', - null, - 'Example values are not available for ', - Pe.createElement('code', null, L), - ' media types.' - ); - } - if ( - Te && - ('application/x-www-form-urlencoded' === L || 0 === L.indexOf('multipart/')) && - we.get('properties', (0, qe.OrderedMap)()).size > 0 - ) { - const s = w('JsonSchemaForm'), - o = w('ParameterExt'), - x = we.get('properties', (0, qe.OrderedMap)()); - return ( - (i = qe.Map.isMap(i) ? i : (0, qe.OrderedMap)()), - Pe.createElement( - 'div', - { className: 'table-container' }, - ye && Pe.createElement(ee, { source: ye }), - Pe.createElement( - 'table', - null, - Pe.createElement( - 'tbody', - null, - qe.Map.isMap(x) && - x.entrySeq().map(([x, C]) => { - if (C.get('readOnly')) return; - const L = C.get('oneOf')?.get(0)?.toJS(), - $ = C.get('anyOf')?.get(0)?.toJS(); - C = (0, qe.fromJS)(j.mergeJsonSchema(C.toJS(), L ?? $ ?? {})); - let z = fe ? getCommonExtensions(C) : null; - const Y = we.get('required', (0, qe.List)()).includes(x), - Z = C.get('type'), - ie = C.get('format'), - ae = C.get('description'), - le = i.getIn([x, 'value']), - ce = i.getIn([x, 'errors']) || _, - pe = u.get(x) || !1; - let ye = j.getSampleSchema(C, !1, { includeWriteOnly: !0 }); - !1 === ye && (ye = 'false'), - 0 === ye && (ye = '0'), - 'string' != typeof ye && 'object' === Z && (ye = stringify(ye)), - 'string' == typeof ye && 'array' === Z && (ye = JSON.parse(ye)); - const be = 'string' === Z && ('binary' === ie || 'base64' === ie); - return Pe.createElement( - 'tr', - { key: x, className: 'parameters', 'data-property-name': x }, - Pe.createElement( - 'td', - { className: 'parameters-col_name' }, - Pe.createElement( - 'div', - { className: Y ? 'parameter__name required' : 'parameter__name' }, - x, - Y ? Pe.createElement('span', null, ' *') : null - ), - Pe.createElement( - 'div', - { className: 'parameter__type' }, - Z, - ie && - Pe.createElement( - 'span', - { className: 'prop-format' }, - '($', - ie, - ')' - ), - fe && z.size - ? z - .entrySeq() - .map(([s, i]) => - Pe.createElement(o, { key: `${s}-${i}`, xKey: s, xVal: i }) - ) - : null - ), - Pe.createElement( - 'div', - { className: 'parameter__deprecated' }, - C.get('deprecated') ? 'deprecated' : null - ) - ), - Pe.createElement( - 'td', - { className: 'parameters-col_description' }, - Pe.createElement(ee, { source: ae }), - B - ? Pe.createElement( - 'div', - null, - Pe.createElement(s, { - fn: j, - dispatchInitialValue: !be, - schema: C, - description: x, - getComponent: w, - value: void 0 === le ? ye : le, - required: Y, - errors: ce, - onChange: (s) => { - V(s, [x]); - } - }), - Y - ? null - : Pe.createElement(de, { - onChange: (s) => U(x, s), - isIncluded: pe, - isIncludedOptions: setIsIncludedOptions(x), - isDisabled: Array.isArray(le) - ? 0 !== le.length - : !isEmptyValue(le) - }) - ) - : null - ) - ); - }) - ) - ) - ) - ); - } - const ze = getDefaultRequestBodyValue(o, L, z, j); - let We = null; - return ( - getKnownSyntaxHighlighterLanguage(ze) && (We = 'json'), - Pe.createElement( - 'div', - null, - ye && Pe.createElement(ee, { source: ye }), - xe - ? Pe.createElement(ce, { - userHasEditedBody: s, - examples: xe, - currentKey: z, - currentUserInputValue: i, - onSelect: (s) => { - Y(s); - }, - updateValue: V, - defaultToFirstExample: !0, - getComponent: w, - setRetainRequestBodyValueFlag: Z - }) - : null, - B - ? Pe.createElement( - 'div', - null, - Pe.createElement(ae, { - value: i, - errors: _, - defaultValue: ze, - onChange: V, - getComponent: w - }) - ) - : Pe.createElement(ie, { - getComponent: w, - getConfigs: x, - specSelectors: C, - expandDepth: 1, - isExecute: B, - schema: _e.get('schema'), - specPath: $.push('content', L), - example: Pe.createElement( - le, - { className: 'body-param__example', language: We }, - stringify(i) || ze - ), - includeWriteOnly: !0 - }), - xe - ? Pe.createElement(pe, { example: xe.get(z), getComponent: w, getConfigs: x }) - : null - ) - ); - }; - class operation_link_OperationLink extends Pe.Component { - render() { - const { link: s, name: o, getComponent: i } = this.props, - u = i('Markdown', !0); - let _ = s.get('operationId') || s.get('operationRef'), - w = s.get('parameters') && s.get('parameters').toJS(), - x = s.get('description'); - return Pe.createElement( - 'div', - { className: 'operation-link' }, - Pe.createElement( - 'div', - { className: 'description' }, - Pe.createElement('b', null, Pe.createElement('code', null, o)), - x ? Pe.createElement(u, { source: x }) : null - ), - Pe.createElement( - 'pre', - null, - 'Operation `', - _, - '`', - Pe.createElement('br', null), - Pe.createElement('br', null), - 'Parameters ', - (function padString(s, o) { - if ('string' != typeof o) return ''; - return o - .split('\n') - .map((o, i) => (i > 0 ? Array(s + 1).join(' ') + o : o)) - .join('\n'); - })(0, JSON.stringify(w, null, 2)) || '{}', - Pe.createElement('br', null) - ) - ); - } - } - const eA = operation_link_OperationLink, - components_servers = ({ - servers: s, - currentServer: o, - setSelectedServer: i, - setServerVariableValue: u, - getServerVariable: _, - getEffectiveServerValue: w - }) => { - const x = - (s.find((s) => s.get('url') === o) || (0, qe.OrderedMap)()).get('variables') || - (0, qe.OrderedMap)(), - C = 0 !== x.size; - (0, Pe.useEffect)(() => { - o || i(s.first()?.get('url')); - }, []), - (0, Pe.useEffect)(() => { - const _ = s.find((s) => s.get('url') === o); - if (!_) return void i(s.first().get('url')); - (_.get('variables') || (0, qe.OrderedMap)()).map((s, i) => { - u({ server: o, key: i, val: s.get('default') || '' }); - }); - }, [o, s]); - const j = (0, Pe.useCallback)( - (s) => { - i(s.target.value); - }, - [i] - ), - L = (0, Pe.useCallback)( - (s) => { - const i = s.target.getAttribute('data-variable'), - _ = s.target.value; - u({ server: o, key: i, val: _ }); - }, - [u, o] - ); - return Pe.createElement( - 'div', - { className: 'servers' }, - Pe.createElement( - 'label', - { htmlFor: 'servers' }, - Pe.createElement( - 'select', - { onChange: j, value: o, id: 'servers' }, - s - .valueSeq() - .map((s) => - Pe.createElement( - 'option', - { value: s.get('url'), key: s.get('url') }, - s.get('url'), - s.get('description') && ` - ${s.get('description')}` - ) - ) - .toArray() - ) - ), - C && - Pe.createElement( - 'div', - null, - Pe.createElement( - 'div', - { className: 'computed-url' }, - 'Computed URL:', - Pe.createElement('code', null, w(o)) - ), - Pe.createElement('h4', null, 'Server variables'), - Pe.createElement( - 'table', - null, - Pe.createElement( - 'tbody', - null, - x.entrySeq().map(([s, i]) => - Pe.createElement( - 'tr', - { key: s }, - Pe.createElement('td', null, s), - Pe.createElement( - 'td', - null, - i.get('enum') - ? Pe.createElement( - 'select', - { 'data-variable': s, onChange: L }, - i - .get('enum') - .map((i) => - Pe.createElement( - 'option', - { selected: i === _(o, s), key: i, value: i }, - i - ) - ) - ) - : Pe.createElement('input', { - type: 'text', - value: _(o, s) || '', - onChange: L, - 'data-variable': s - }) - ) - ) - ) - ) - ) - ) - ); - }; - class ServersContainer extends Pe.Component { - render() { - const { - specSelectors: s, - oas3Selectors: o, - oas3Actions: i, - getComponent: u - } = this.props, - _ = s.servers(), - w = u('Servers'); - return _ && _.size - ? Pe.createElement( - 'div', - null, - Pe.createElement('span', { className: 'servers-title' }, 'Servers'), - Pe.createElement(w, { - servers: _, - currentServer: o.selectedServer(), - setSelectedServer: i.setSelectedServer, - setServerVariableValue: i.setServerVariableValue, - getServerVariable: o.serverVariableValue, - getEffectiveServerValue: o.serverEffectiveValue - }) - ) - : null; - } - } - const tA = Function.prototype; - class RequestBodyEditor extends Pe.PureComponent { - static defaultProps = { onChange: tA, userHasEditedBody: !1 }; - constructor(s, o) { - super(s, o), - (this.state = { value: stringify(s.value) || s.defaultValue }), - s.onChange(s.value); - } - applyDefaultValue = (s) => { - const { onChange: o, defaultValue: i } = s || this.props; - return this.setState({ value: i }), o(i); - }; - onChange = (s) => { - this.props.onChange(stringify(s)); - }; - onDomChange = (s) => { - const o = s.target.value; - this.setState({ value: o }, () => this.onChange(o)); - }; - UNSAFE_componentWillReceiveProps(s) { - this.props.value !== s.value && - s.value !== this.state.value && - this.setState({ value: stringify(s.value) }), - !s.value && s.defaultValue && this.state.value && this.applyDefaultValue(s); - } - render() { - let { getComponent: s, errors: o } = this.props, - { value: i } = this.state, - u = o.size > 0; - const _ = s('TextArea'); - return Pe.createElement( - 'div', - { className: 'body-param' }, - Pe.createElement(_, { - className: Hn()('body-param__text', { invalid: u }), - title: o.size ? o.join(', ') : '', - value: i, - onChange: this.onDomChange - }) - ); - } - } - class HttpAuth extends Pe.Component { - constructor(s, o) { - super(s, o); - let { name: i, schema: u } = this.props, - _ = this.getValue(); - this.state = { name: i, schema: u, value: _ }; - } - getValue() { - let { name: s, authorized: o } = this.props; - return o && o.getIn([s, 'value']); - } - onChange = (s) => { - let { onChange: o } = this.props, - { value: i, name: u } = s.target, - _ = Object.assign({}, this.state.value); - u ? (_[u] = i) : (_ = i), this.setState({ value: _ }, () => o(this.state)); - }; - render() { - let { schema: s, getComponent: o, errSelectors: i, name: u } = this.props; - const _ = o('Input'), - w = o('Row'), - x = o('Col'), - C = o('authError'), - j = o('Markdown', !0), - L = o('JumpToPath', !0), - B = (s.get('scheme') || '').toLowerCase(); - let $ = this.getValue(), - V = i.allErrors().filter((s) => s.get('authId') === u); - if ('basic' === B) { - let o = $ ? $.get('username') : null; - return Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - Pe.createElement('code', null, u || s.get('name')), - '  (http, Basic)', - Pe.createElement(L, { path: ['securityDefinitions', u] }) - ), - o && Pe.createElement('h6', null, 'Authorized'), - Pe.createElement(w, null, Pe.createElement(j, { source: s.get('description') })), - Pe.createElement( - w, - null, - Pe.createElement('label', { htmlFor: 'auth-basic-username' }, 'Username:'), - o - ? Pe.createElement('code', null, ' ', o, ' ') - : Pe.createElement( - x, - null, - Pe.createElement(_, { - id: 'auth-basic-username', - type: 'text', - required: 'required', - name: 'username', - 'aria-label': 'auth-basic-username', - onChange: this.onChange, - autoFocus: !0 - }) - ) - ), - Pe.createElement( - w, - null, - Pe.createElement('label', { htmlFor: 'auth-basic-password' }, 'Password:'), - o - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - x, - null, - Pe.createElement(_, { - id: 'auth-basic-password', - autoComplete: 'new-password', - name: 'password', - type: 'password', - 'aria-label': 'auth-basic-password', - onChange: this.onChange - }) - ) - ), - V.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) - ); - } - return 'bearer' === B - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - Pe.createElement('code', null, u || s.get('name')), - '  (http, Bearer)', - Pe.createElement(L, { path: ['securityDefinitions', u] }) - ), - $ && Pe.createElement('h6', null, 'Authorized'), - Pe.createElement(w, null, Pe.createElement(j, { source: s.get('description') })), - Pe.createElement( - w, - null, - Pe.createElement('label', { htmlFor: 'auth-bearer-value' }, 'Value:'), - $ - ? Pe.createElement('code', null, ' ****** ') - : Pe.createElement( - x, - null, - Pe.createElement(_, { - id: 'auth-bearer-value', - type: 'text', - 'aria-label': 'auth-bearer-value', - onChange: this.onChange, - autoFocus: !0 - }) - ) - ), - V.valueSeq().map((s, o) => Pe.createElement(C, { error: s, key: o })) - ) - : Pe.createElement( - 'div', - null, - Pe.createElement( - 'em', - null, - Pe.createElement('b', null, u), - ' HTTP authentication: unsupported scheme ', - `'${B}'` - ) - ); - } - } - class operation_servers_OperationServers extends Pe.Component { - setSelectedServer = (s) => { - const { path: o, method: i } = this.props; - return this.forceUpdate(), this.props.setSelectedServer(s, `${o}:${i}`); - }; - setServerVariableValue = (s) => { - const { path: o, method: i } = this.props; - return ( - this.forceUpdate(), - this.props.setServerVariableValue({ ...s, namespace: `${o}:${i}` }) - ); - }; - getSelectedServer = () => { - const { path: s, method: o } = this.props; - return this.props.getSelectedServer(`${s}:${o}`); - }; - getServerVariable = (s, o) => { - const { path: i, method: u } = this.props; - return this.props.getServerVariable({ namespace: `${i}:${u}`, server: s }, o); - }; - getEffectiveServerValue = (s) => { - const { path: o, method: i } = this.props; - return this.props.getEffectiveServerValue({ server: s, namespace: `${o}:${i}` }); - }; - render() { - const { operationServers: s, pathServers: o, getComponent: i } = this.props; - if (!s && !o) return null; - const u = i('Servers'), - _ = s || o, - w = s ? 'operation' : 'path'; - return Pe.createElement( - 'div', - { className: 'opblock-section operation-servers' }, - Pe.createElement( - 'div', - { className: 'opblock-section-header' }, - Pe.createElement( - 'div', - { className: 'tab-header' }, - Pe.createElement('h4', { className: 'opblock-title' }, 'Servers') - ) - ), - Pe.createElement( - 'div', - { className: 'opblock-description-wrapper' }, - Pe.createElement( - 'h4', - { className: 'message' }, - 'These ', - w, - '-level options override the global server options.' - ), - Pe.createElement(u, { - servers: _, - currentServer: this.getSelectedServer(), - setSelectedServer: this.setSelectedServer, - setServerVariableValue: this.setServerVariableValue, - getServerVariable: this.getServerVariable, - getEffectiveServerValue: this.getEffectiveServerValue - }) - ) - ); - } - } - const rA = { - Callbacks: callbacks, - HttpAuth, - RequestBody: components_request_body, - Servers: components_servers, - ServersContainer, - RequestBodyEditor, - OperationServers: operation_servers_OperationServers, - operationLink: eA - }, - nA = new Remarkable('commonmark'); - nA.block.ruler.enable(['table']), nA.set({ linkTarget: '_blank' }); - const sA = OAS3ComponentWrapFactory( - ({ - source: s, - className: o = '', - getConfigs: i = () => ({ useUnsafeMarkdown: !1 }) - }) => { - if ('string' != typeof s) return null; - if (s) { - const { useUnsafeMarkdown: u } = i(), - _ = sanitizer(nA.render(s), { useUnsafeMarkdown: u }); - let w; - return ( - 'string' == typeof _ && (w = _.trim()), - Pe.createElement('div', { - dangerouslySetInnerHTML: { __html: w }, - className: Hn()(o, 'renderedMarkdown') - }) - ); - } - return null; - } - ), - oA = OAS3ComponentWrapFactory(({ Ori: s, ...o }) => { - const { - schema: i, - getComponent: u, - errSelectors: _, - authorized: w, - onAuthChange: x, - name: C - } = o, - j = u('HttpAuth'); - return 'http' === i.get('type') - ? Pe.createElement(j, { - key: C, - schema: i, - name: C, - errSelectors: _, - authorized: w, - getComponent: u, - onChange: x - }) - : Pe.createElement(s, o); - }), - iA = OAS3ComponentWrapFactory(OnlineValidatorBadge); - class ModelComponent extends Pe.Component { - render() { - let { getConfigs: s, schema: o, Ori: i } = this.props, - u = ['model-box'], - _ = null; - return ( - !0 === o.get('deprecated') && - (u.push('deprecated'), - (_ = Pe.createElement( - 'span', - { className: 'model-deprecated-warning' }, - 'Deprecated:' - ))), - Pe.createElement( - 'div', - { className: u.join(' ') }, - _, - Pe.createElement( - i, - Rn()({}, this.props, { - getConfigs: s, - depth: 1, - expandDepth: this.props.expandDepth || 0 - }) - ) - ) - ); - } - } - const aA = OAS3ComponentWrapFactory(ModelComponent), - lA = OAS3ComponentWrapFactory(({ Ori: s, ...o }) => { - const { schema: i, getComponent: u, errors: _, onChange: w } = o, - x = i && i.get ? i.get('format') : null, - C = i && i.get ? i.get('type') : null, - j = u('Input'); - return C && 'string' === C && x && ('binary' === x || 'base64' === x) - ? Pe.createElement(j, { - type: 'file', - className: _.length ? 'invalid' : '', - title: _.length ? _ : '', - onChange: (s) => { - w(s.target.files[0]); - }, - disabled: s.isDisabled - }) - : Pe.createElement(s, o); - }), - cA = { - Markdown: sA, - AuthItem: oA, - OpenAPIVersion: (function OAS30ComponentWrapFactory(s) { - return (o, i) => (u) => - 'function' == typeof i.specSelectors?.isOAS30 - ? i.specSelectors.isOAS30() - ? Pe.createElement(s, Rn()({}, u, i, { Ori: o })) - : Pe.createElement(o, u) - : (console.warn("OAS30 wrapper: couldn't get spec"), null); - })((s) => { - const { Ori: o } = s; - return Pe.createElement(o, { oasVersion: '3.0' }); - }), - JsonSchema_string: lA, - model: aA, - onlineValidatorBadge: iA - }, - uA = 'oas3_set_servers', - pA = 'oas3_set_request_body_value', - hA = 'oas3_set_request_body_retain_flag', - dA = 'oas3_set_request_body_inclusion', - fA = 'oas3_set_active_examples_member', - mA = 'oas3_set_request_content_type', - gA = 'oas3_set_response_content_type', - yA = 'oas3_set_server_variable_value', - vA = 'oas3_set_request_body_validate_error', - bA = 'oas3_clear_request_body_validate_error', - _A = 'oas3_clear_request_body_value'; - function setSelectedServer(s, o) { - return { type: uA, payload: { selectedServerUrl: s, namespace: o } }; - } - function setRequestBodyValue({ value: s, pathMethod: o }) { - return { type: pA, payload: { value: s, pathMethod: o } }; - } - const setRetainRequestBodyValueFlag = ({ value: s, pathMethod: o }) => ({ - type: hA, - payload: { value: s, pathMethod: o } - }); - function setRequestBodyInclusion({ value: s, pathMethod: o, name: i }) { - return { type: dA, payload: { value: s, pathMethod: o, name: i } }; - } - function setActiveExamplesMember({ - name: s, - pathMethod: o, - contextType: i, - contextName: u - }) { - return { type: fA, payload: { name: s, pathMethod: o, contextType: i, contextName: u } }; - } - function setRequestContentType({ value: s, pathMethod: o }) { - return { type: mA, payload: { value: s, pathMethod: o } }; - } - function setResponseContentType({ value: s, path: o, method: i }) { - return { type: gA, payload: { value: s, path: o, method: i } }; - } - function setServerVariableValue({ server: s, namespace: o, key: i, val: u }) { - return { type: yA, payload: { server: s, namespace: o, key: i, val: u } }; - } - const setRequestBodyValidateError = ({ path: s, method: o, validationErrors: i }) => ({ - type: vA, - payload: { path: s, method: o, validationErrors: i } - }), - clearRequestBodyValidateError = ({ path: s, method: o }) => ({ - type: bA, - payload: { path: s, method: o } - }), - initRequestBodyValidateError = ({ pathMethod: s }) => ({ - type: bA, - payload: { path: s[0], method: s[1] } - }), - clearRequestBodyValue = ({ pathMethod: s }) => ({ type: _A, payload: { pathMethod: s } }); - var EA = __webpack_require__(60680), - wA = __webpack_require__.n(EA); - const oas3_selectors_onlyOAS3 = - (s) => - (o, ...i) => - (u) => { - if (u.getSystem().specSelectors.isOAS3()) { - const _ = s(o, ...i); - return 'function' == typeof _ ? _(u) : _; - } - return null; - }; - const SA = oas3_selectors_onlyOAS3((s, o) => { - const i = o ? [o, 'selectedServer'] : ['selectedServer']; - return s.getIn(i) || ''; - }), - xA = oas3_selectors_onlyOAS3( - (s, o, i) => s.getIn(['requestData', o, i, 'bodyValue']) || null - ), - kA = oas3_selectors_onlyOAS3( - (s, o, i) => s.getIn(['requestData', o, i, 'retainBodyValue']) || !1 - ), - selectDefaultRequestBodyValue = (s, o, i) => (s) => { - const { oas3Selectors: u, specSelectors: _, fn: w } = s.getSystem(); - if (_.isOAS3()) { - const s = u.requestContentType(o, i); - if (s) - return getDefaultRequestBodyValue( - _.specResolvedSubtree(['paths', o, i, 'requestBody']), - s, - u.activeExamplesMember(o, i, 'requestBody', 'requestBody'), - w - ); - } - return null; - }, - CA = oas3_selectors_onlyOAS3((s, o, i) => (s) => { - const { oas3Selectors: u, specSelectors: _, fn: w } = s; - let x = !1; - const C = u.requestContentType(o, i); - let j = u.requestBodyValue(o, i); - const L = _.specResolvedSubtree(['paths', o, i, 'requestBody']); - if (!L) return !1; - if ( - (qe.Map.isMap(j) && - (j = stringify( - j.mapEntries((s) => (qe.Map.isMap(s[1]) ? [s[0], s[1].get('value')] : s)).toJS() - )), - qe.List.isList(j) && (j = stringify(j)), - C) - ) { - const s = getDefaultRequestBodyValue( - L, - C, - u.activeExamplesMember(o, i, 'requestBody', 'requestBody'), - w - ); - x = !!j && j !== s; - } - return x; - }), - OA = oas3_selectors_onlyOAS3( - (s, o, i) => s.getIn(['requestData', o, i, 'bodyInclusion']) || (0, qe.Map)() - ), - AA = oas3_selectors_onlyOAS3( - (s, o, i) => s.getIn(['requestData', o, i, 'errors']) || null - ), - jA = oas3_selectors_onlyOAS3( - (s, o, i, u, _) => s.getIn(['examples', o, i, u, _, 'activeExample']) || null - ), - IA = oas3_selectors_onlyOAS3( - (s, o, i) => s.getIn(['requestData', o, i, 'requestContentType']) || null - ), - PA = oas3_selectors_onlyOAS3( - (s, o, i) => s.getIn(['requestData', o, i, 'responseContentType']) || null - ), - MA = oas3_selectors_onlyOAS3((s, o, i) => { - let u; - if ('string' != typeof o) { - const { server: s, namespace: _ } = o; - u = _ ? [_, 'serverVariableValues', s, i] : ['serverVariableValues', s, i]; - } else { - u = ['serverVariableValues', o, i]; - } - return s.getIn(u) || null; - }), - TA = oas3_selectors_onlyOAS3((s, o) => { - let i; - if ('string' != typeof o) { - const { server: s, namespace: u } = o; - i = u ? [u, 'serverVariableValues', s] : ['serverVariableValues', s]; - } else { - i = ['serverVariableValues', o]; - } - return s.getIn(i) || (0, qe.OrderedMap)(); - }), - NA = oas3_selectors_onlyOAS3((s, o) => { - var i, u; - if ('string' != typeof o) { - const { server: _, namespace: w } = o; - (u = _), - (i = w - ? s.getIn([w, 'serverVariableValues', u]) - : s.getIn(['serverVariableValues', u])); - } else (u = o), (i = s.getIn(['serverVariableValues', u])); - i = i || (0, qe.OrderedMap)(); - let _ = u; - return ( - i.map((s, o) => { - _ = _.replace(new RegExp(`{${wA()(o)}}`, 'g'), s); - }), - _ - ); - }), - RA = (function validateRequestBodyIsRequired(s) { - return (...o) => - (i) => { - const u = i.getSystem().specSelectors.specJson(); - let _ = [...o][1] || []; - return !u.getIn(['paths', ..._, 'requestBody', 'required']) || s(...o); - }; - })((s, o) => - ((s, o) => ((o = o || []), !!s.getIn(['requestData', ...o, 'bodyValue'])))(s, o) - ), - validateShallowRequired = ( - s, - { - oas3RequiredRequestBodyContentType: o, - oas3RequestContentType: i, - oas3RequestBodyValue: u - } - ) => { - let _ = []; - if (!qe.Map.isMap(u)) return _; - let w = []; - return ( - Object.keys(o.requestContentType).forEach((s) => { - if (s === i) { - o.requestContentType[s].forEach((s) => { - w.indexOf(s) < 0 && w.push(s); - }); - } - }), - w.forEach((s) => { - u.getIn([s, 'value']) || _.push(s); - }), - _ - ); - }, - DA = Ss()(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']), - LA = { - [uA]: (s, { payload: { selectedServerUrl: o, namespace: i } }) => { - const u = i ? [i, 'selectedServer'] : ['selectedServer']; - return s.setIn(u, o); - }, - [pA]: (s, { payload: { value: o, pathMethod: i } }) => { - let [u, _] = i; - if (!qe.Map.isMap(o)) return s.setIn(['requestData', u, _, 'bodyValue'], o); - let w, - x = s.getIn(['requestData', u, _, 'bodyValue']) || (0, qe.Map)(); - qe.Map.isMap(x) || (x = (0, qe.Map)()); - const [...C] = o.keys(); - return ( - C.forEach((s) => { - let i = o.getIn([s]); - (x.has(s) && qe.Map.isMap(i)) || (w = x.setIn([s, 'value'], i)); - }), - s.setIn(['requestData', u, _, 'bodyValue'], w) - ); - }, - [hA]: (s, { payload: { value: o, pathMethod: i } }) => { - let [u, _] = i; - return s.setIn(['requestData', u, _, 'retainBodyValue'], o); - }, - [dA]: (s, { payload: { value: o, pathMethod: i, name: u } }) => { - let [_, w] = i; - return s.setIn(['requestData', _, w, 'bodyInclusion', u], o); - }, - [fA]: (s, { payload: { name: o, pathMethod: i, contextType: u, contextName: _ } }) => { - let [w, x] = i; - return s.setIn(['examples', w, x, u, _, 'activeExample'], o); - }, - [mA]: (s, { payload: { value: o, pathMethod: i } }) => { - let [u, _] = i; - return s.setIn(['requestData', u, _, 'requestContentType'], o); - }, - [gA]: (s, { payload: { value: o, path: i, method: u } }) => - s.setIn(['requestData', i, u, 'responseContentType'], o), - [yA]: (s, { payload: { server: o, namespace: i, key: u, val: _ } }) => { - const w = i ? [i, 'serverVariableValues', o, u] : ['serverVariableValues', o, u]; - return s.setIn(w, _); - }, - [vA]: (s, { payload: { path: o, method: i, validationErrors: u } }) => { - let _ = []; - if ((_.push('Required field is not provided'), u.missingBodyValue)) - return s.setIn(['requestData', o, i, 'errors'], (0, qe.fromJS)(_)); - if (u.missingRequiredKeys && u.missingRequiredKeys.length > 0) { - const { missingRequiredKeys: w } = u; - return s.updateIn(['requestData', o, i, 'bodyValue'], (0, qe.fromJS)({}), (s) => - w.reduce((s, o) => s.setIn([o, 'errors'], (0, qe.fromJS)(_)), s) - ); - } - return console.warn('unexpected result: SET_REQUEST_BODY_VALIDATE_ERROR'), s; - }, - [bA]: (s, { payload: { path: o, method: i } }) => { - const u = s.getIn(['requestData', o, i, 'bodyValue']); - if (!qe.Map.isMap(u)) - return s.setIn(['requestData', o, i, 'errors'], (0, qe.fromJS)([])); - const [..._] = u.keys(); - return _ - ? s.updateIn(['requestData', o, i, 'bodyValue'], (0, qe.fromJS)({}), (s) => - _.reduce((s, o) => s.setIn([o, 'errors'], (0, qe.fromJS)([])), s) - ) - : s; - }, - [_A]: (s, { payload: { pathMethod: o } }) => { - let [i, u] = o; - const _ = s.getIn(['requestData', i, u, 'bodyValue']); - return _ - ? qe.Map.isMap(_) - ? s.setIn(['requestData', i, u, 'bodyValue'], (0, qe.Map)()) - : s.setIn(['requestData', i, u, 'bodyValue'], '') - : s; - } - }; - function oas3() { - return { - components: rA, - wrapComponents: cA, - statePlugins: { - spec: { wrapSelectors: be, selectors: we }, - auth: { wrapSelectors: _e }, - oas3: { actions: { ...Se }, reducers: LA, selectors: { ...xe } } - } - }; - } - const webhooks = ({ specSelectors: s, getComponent: o }) => { - const i = s.selectWebhooksOperations(), - u = Object.keys(i), - _ = o('OperationContainer', !0); - return 0 === u.length - ? null - : Pe.createElement( - 'div', - { className: 'webhooks' }, - Pe.createElement('h2', null, 'Webhooks'), - u.map((s) => - Pe.createElement( - 'div', - { key: `${s}-webhook` }, - i[s].map((o) => - Pe.createElement(_, { - key: `${s}-${o.method}-webhook`, - op: o.operation, - tag: 'webhooks', - method: o.method, - path: s, - specPath: (0, qe.List)(o.specPath), - allowTryItOut: !1 - }) - ) - ) - ) - ); - }, - oas31_components_license = ({ getComponent: s, specSelectors: o }) => { - const i = o.selectLicenseNameField(), - u = o.selectLicenseUrl(), - _ = s('Link'); - return Pe.createElement( - 'div', - { className: 'info__license' }, - u - ? Pe.createElement( - 'div', - { className: 'info__license__url' }, - Pe.createElement(_, { target: '_blank', href: sanitizeUrl(u) }, i) - ) - : Pe.createElement('span', null, i) - ); - }, - oas31_components_contact = ({ getComponent: s, specSelectors: o }) => { - const i = o.selectContactNameField(), - u = o.selectContactUrl(), - _ = o.selectContactEmailField(), - w = s('Link'); - return Pe.createElement( - 'div', - { className: 'info__contact' }, - u && - Pe.createElement( - 'div', - null, - Pe.createElement(w, { href: sanitizeUrl(u), target: '_blank' }, i, ' - Website') - ), - _ && - Pe.createElement( - w, - { href: sanitizeUrl(`mailto:${_}`) }, - u ? `Send email to ${i}` : `Contact ${i}` - ) - ); - }, - oas31_components_info = ({ getComponent: s, specSelectors: o }) => { - const i = o.version(), - u = o.url(), - _ = o.basePath(), - w = o.host(), - x = o.selectInfoSummaryField(), - C = o.selectInfoDescriptionField(), - j = o.selectInfoTitleField(), - L = o.selectInfoTermsOfServiceUrl(), - B = o.selectExternalDocsUrl(), - $ = o.selectExternalDocsDescriptionField(), - V = o.contact(), - U = o.license(), - z = s('Markdown', !0), - Y = s('Link'), - Z = s('VersionStamp'), - ee = s('OpenAPIVersion'), - ie = s('InfoUrl'), - ae = s('InfoBasePath'), - le = s('License', !0), - ce = s('Contact', !0), - pe = s('JsonSchemaDialect', !0); - return Pe.createElement( - 'div', - { className: 'info' }, - Pe.createElement( - 'hgroup', - { className: 'main' }, - Pe.createElement( - 'h2', - { className: 'title' }, - j, - Pe.createElement( - 'span', - null, - i && Pe.createElement(Z, { version: i }), - Pe.createElement(ee, { oasVersion: '3.1' }) - ) - ), - (w || _) && Pe.createElement(ae, { host: w, basePath: _ }), - u && Pe.createElement(ie, { getComponent: s, url: u }) - ), - x && Pe.createElement('p', { className: 'info__summary' }, x), - Pe.createElement( - 'div', - { className: 'info__description description' }, - Pe.createElement(z, { source: C }) - ), - L && - Pe.createElement( - 'div', - { className: 'info__tos' }, - Pe.createElement( - Y, - { target: '_blank', href: sanitizeUrl(L) }, - 'Terms of service' - ) - ), - V.size > 0 && Pe.createElement(ce, null), - U.size > 0 && Pe.createElement(le, null), - B && - Pe.createElement( - Y, - { className: 'info__extdocs', target: '_blank', href: sanitizeUrl(B) }, - $ || B - ), - Pe.createElement(pe, null) - ); - }, - json_schema_dialect = ({ getComponent: s, specSelectors: o }) => { - const i = o.selectJsonSchemaDialectField(), - u = o.selectJsonSchemaDialectDefault(), - _ = s('Link'); - return Pe.createElement( - Pe.Fragment, - null, - i && - i === u && - Pe.createElement( - 'p', - { className: 'info__jsonschemadialect' }, - 'JSON Schema dialect:', - ' ', - Pe.createElement(_, { target: '_blank', href: sanitizeUrl(i) }, i) - ), - i && - i !== u && - Pe.createElement( - 'div', - { className: 'error-wrapper' }, - Pe.createElement( - 'div', - { className: 'no-margin' }, - Pe.createElement( - 'div', - { className: 'errors' }, - Pe.createElement( - 'div', - { className: 'errors-wrapper' }, - Pe.createElement('h4', { className: 'center' }, 'Warning'), - Pe.createElement( - 'p', - { className: 'message' }, - Pe.createElement('strong', null, 'OpenAPI.jsonSchemaDialect'), - ' field contains a value different from the default value of', - ' ', - Pe.createElement(_, { target: '_blank', href: u }, u), - '. Values different from the default one are currently not supported. Please either omit the field or provide it with the default value.' - ) - ) - ) - ) - ) - ); - }, - version_pragma_filter = ({ - bypass: s, - isSwagger2: o, - isOAS3: i, - isOAS31: u, - alsoShow: _, - children: w - }) => - s - ? Pe.createElement('div', null, w) - : o && (i || u) - ? Pe.createElement( - 'div', - { className: 'version-pragma' }, - _, - Pe.createElement( - 'div', - { className: 'version-pragma__message version-pragma__message--ambiguous' }, - Pe.createElement( - 'div', - null, - Pe.createElement('h3', null, 'Unable to render this definition'), - Pe.createElement( - 'p', - null, - Pe.createElement('code', null, 'swagger'), - ' and ', - Pe.createElement('code', null, 'openapi'), - ' fields cannot be present in the same Swagger or OpenAPI definition. Please remove one of the fields.' - ), - Pe.createElement( - 'p', - null, - 'Supported version fields are ', - Pe.createElement('code', null, 'swagger: "2.0"'), - ' and those that match ', - Pe.createElement('code', null, 'openapi: 3.x.y'), - ' (for example,', - ' ', - Pe.createElement('code', null, 'openapi: 3.1.0'), - ').' - ) - ) - ) - ) - : o || i || u - ? Pe.createElement('div', null, w) - : Pe.createElement( - 'div', - { className: 'version-pragma' }, - _, - Pe.createElement( - 'div', - { className: 'version-pragma__message version-pragma__message--missing' }, - Pe.createElement( - 'div', - null, - Pe.createElement('h3', null, 'Unable to render this definition'), - Pe.createElement( - 'p', - null, - 'The provided definition does not specify a valid version field.' - ), - Pe.createElement( - 'p', - null, - 'Please indicate a valid Swagger or OpenAPI version field. Supported version fields are ', - Pe.createElement('code', null, 'swagger: "2.0"'), - ' and those that match ', - Pe.createElement('code', null, 'openapi: 3.x.y'), - ' (for example,', - ' ', - Pe.createElement('code', null, 'openapi: 3.1.0'), - ').' - ) - ) - ) - ), - getModelName = (s) => - 'string' == typeof s && s.includes('#/components/schemas/') - ? ((s) => { - const o = s.replace(/~1/g, '/').replace(/~0/g, '~'); - try { - return decodeURIComponent(o); - } catch { - return o; - } - })(s.replace(/^.*#\/components\/schemas\//, '')) - : null, - BA = (0, Pe.forwardRef)(({ schema: s, getComponent: o, onToggle: i = () => {} }, u) => { - const _ = o('JSONSchema202012'), - w = getModelName(s.get('$$ref')), - x = (0, Pe.useCallback)( - (s, o) => { - i(w, o); - }, - [w, i] - ); - return Pe.createElement(_, { name: w, schema: s.toJS(), ref: u, onExpand: x }); - }), - FA = BA, - models = ({ - specActions: s, - specSelectors: o, - layoutSelectors: i, - layoutActions: u, - getComponent: _, - getConfigs: w, - fn: x - }) => { - const C = o.selectSchemas(), - j = Object.keys(C).length > 0, - L = ['components', 'schemas'], - { docExpansion: B, defaultModelsExpandDepth: $ } = w(), - V = $ > 0 && 'none' !== B, - U = i.isShown(L, V), - z = _('Collapse'), - Y = _('JSONSchema202012'), - Z = _('ArrowUpIcon'), - ee = _('ArrowDownIcon'), - { getTitle: ie } = x.jsonSchema202012.useFn(); - (0, Pe.useEffect)(() => { - const i = U && $ > 1, - u = null != o.specResolvedSubtree(L); - i && !u && s.requestResolvedSubtree(L); - }, [U, $]); - const ae = (0, Pe.useCallback)(() => { - u.show(L, !U); - }, [U]), - le = (0, Pe.useCallback)((s) => { - null !== s && u.readyToScroll(L, s); - }, []), - handleJSONSchema202012Ref = (s) => (o) => { - null !== o && u.readyToScroll([...L, s], o); - }, - handleJSONSchema202012Expand = (i) => (u, _) => { - if (_) { - const u = [...L, i]; - null != o.specResolvedSubtree(u) || s.requestResolvedSubtree([...L, i]); - } - }; - return !j || $ < 0 - ? null - : Pe.createElement( - 'section', - { className: Hn()('models', { 'is-open': U }), ref: le }, - Pe.createElement( - 'h4', - null, - Pe.createElement( - 'button', - { 'aria-expanded': U, className: 'models-control', onClick: ae }, - Pe.createElement('span', null, 'Schemas'), - U ? Pe.createElement(Z, null) : Pe.createElement(ee, null) - ) - ), - Pe.createElement( - z, - { isOpened: U }, - Object.entries(C).map(([s, o]) => { - const i = ie(o, { lookup: 'basic' }) || s; - return Pe.createElement(Y, { - key: s, - ref: handleJSONSchema202012Ref(s), - schema: o, - name: i, - onExpand: handleJSONSchema202012Expand(s) - }); - }) - ) - ); - }, - mutual_tls_auth = ({ schema: s, getComponent: o }) => { - const i = o('JumpToPath', !0); - return Pe.createElement( - 'div', - null, - Pe.createElement( - 'h4', - null, - s.get('name'), - ' (mutualTLS)', - ' ', - Pe.createElement(i, { path: ['securityDefinitions', s.get('name')] }) - ), - Pe.createElement( - 'p', - null, - 'Mutual TLS is required by this API/Operation. Certificates are managed via your Operating System and/or your browser.' - ), - Pe.createElement('p', null, s.get('description')) - ); - }; - class auths_Auths extends Pe.Component { - constructor(s, o) { - super(s, o), (this.state = {}); - } - onAuthChange = (s) => { - let { name: o } = s; - this.setState({ [o]: s }); - }; - submitAuth = (s) => { - s.preventDefault(); - let { authActions: o } = this.props; - o.authorizeWithPersistOption(this.state); - }; - logoutClick = (s) => { - s.preventDefault(); - let { authActions: o, definitions: i } = this.props, - u = i.map((s, o) => o).toArray(); - this.setState(u.reduce((s, o) => ((s[o] = ''), s), {})), o.logoutWithPersistOption(u); - }; - close = (s) => { - s.preventDefault(); - let { authActions: o } = this.props; - o.showDefinitions(!1); - }; - render() { - let { definitions: s, getComponent: o, authSelectors: i, errSelectors: u } = this.props; - const _ = o('AuthItem'), - w = o('oauth2', !0), - x = o('Button'), - C = i.authorized(), - j = s.filter((s, o) => !!C.get(o)), - L = s.filter((s) => 'oauth2' !== s.get('type') && 'mutualTLS' !== s.get('type')), - B = s.filter((s) => 'oauth2' === s.get('type')), - $ = s.filter((s) => 'mutualTLS' === s.get('type')); - return Pe.createElement( - 'div', - { className: 'auth-container' }, - L.size > 0 && - Pe.createElement( - 'form', - { onSubmit: this.submitAuth }, - L.map((s, i) => - Pe.createElement(_, { - key: i, - schema: s, - name: i, - getComponent: o, - onAuthChange: this.onAuthChange, - authorized: C, - errSelectors: u - }) - ).toArray(), - Pe.createElement( - 'div', - { className: 'auth-btn-wrapper' }, - L.size === j.size - ? Pe.createElement( - x, - { - className: 'btn modal-btn auth', - onClick: this.logoutClick, - 'aria-label': 'Remove authorization' - }, - 'Logout' - ) - : Pe.createElement( - x, - { - type: 'submit', - className: 'btn modal-btn auth authorize', - 'aria-label': 'Apply credentials' - }, - 'Authorize' - ), - Pe.createElement( - x, - { className: 'btn modal-btn auth btn-done', onClick: this.close }, - 'Close' - ) - ) - ), - B.size > 0 - ? Pe.createElement( - 'div', - null, - Pe.createElement( - 'div', - { className: 'scope-def' }, - Pe.createElement( - 'p', - null, - 'Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.' - ), - Pe.createElement( - 'p', - null, - 'API requires the following scopes. Select which ones you want to grant to Swagger UI.' - ) - ), - s - .filter((s) => 'oauth2' === s.get('type')) - .map((s, o) => - Pe.createElement( - 'div', - { key: o }, - Pe.createElement(w, { authorized: C, schema: s, name: o }) - ) - ) - .toArray() - ) - : null, - $.size > 0 && - Pe.createElement( - 'div', - null, - $.map((s, i) => - Pe.createElement(_, { - key: i, - schema: s, - name: i, - getComponent: o, - onAuthChange: this.onAuthChange, - authorized: C, - errSelectors: u - }) - ).toArray() - ) - ); - } - } - const qA = auths_Auths, - isOAS31 = (s) => { - const o = s.get('openapi'); - return 'string' == typeof o && /^3\.1\.(?:[1-9]\d*|0)$/.test(o); - }, - fn_createOnlyOAS31Selector = - (s) => - (o, ...i) => - (u) => { - if (u.getSystem().specSelectors.isOAS31()) { - const _ = s(o, ...i); - return 'function' == typeof _ ? _(u) : _; - } - return null; - }, - createOnlyOAS31SelectorWrapper = - (s) => - (o, i) => - (u, ..._) => { - if (i.getSystem().specSelectors.isOAS31()) { - const w = s(u, ..._); - return 'function' == typeof w ? w(o, i) : w; - } - return o(..._); - }, - fn_createSystemSelector = - (s) => - (o, ...i) => - (u) => { - const _ = s(o, u, ...i); - return 'function' == typeof _ ? _(u) : _; - }, - createOnlyOAS31ComponentWrapper = (s) => (o, i) => (u) => - i.specSelectors.isOAS31() - ? Pe.createElement(s, Rn()({}, u, { originalComponent: o, getSystem: i.getSystem })) - : Pe.createElement(o, u), - $A = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { - const o = s().getComponent('OAS31License', !0); - return Pe.createElement(o, null); - }), - VA = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { - const o = s().getComponent('OAS31Contact', !0); - return Pe.createElement(o, null); - }), - UA = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { - const o = s().getComponent('OAS31Info', !0); - return Pe.createElement(o, null); - }), - zA = createOnlyOAS31ComponentWrapper(({ getSystem: s, ...o }) => { - const i = s(), - { getComponent: u, fn: _, getConfigs: w } = i, - x = w(), - C = u('OAS31Model'), - j = u('JSONSchema202012'), - L = u('JSONSchema202012Keyword$schema'), - B = u('JSONSchema202012Keyword$vocabulary'), - $ = u('JSONSchema202012Keyword$id'), - V = u('JSONSchema202012Keyword$anchor'), - U = u('JSONSchema202012Keyword$dynamicAnchor'), - z = u('JSONSchema202012Keyword$ref'), - Y = u('JSONSchema202012Keyword$dynamicRef'), - Z = u('JSONSchema202012Keyword$defs'), - ee = u('JSONSchema202012Keyword$comment'), - ie = u('JSONSchema202012KeywordAllOf'), - ae = u('JSONSchema202012KeywordAnyOf'), - le = u('JSONSchema202012KeywordOneOf'), - ce = u('JSONSchema202012KeywordNot'), - pe = u('JSONSchema202012KeywordIf'), - de = u('JSONSchema202012KeywordThen'), - fe = u('JSONSchema202012KeywordElse'), - ye = u('JSONSchema202012KeywordDependentSchemas'), - be = u('JSONSchema202012KeywordPrefixItems'), - _e = u('JSONSchema202012KeywordItems'), - we = u('JSONSchema202012KeywordContains'), - Se = u('JSONSchema202012KeywordProperties'), - xe = u('JSONSchema202012KeywordPatternProperties'), - Te = u('JSONSchema202012KeywordAdditionalProperties'), - Re = u('JSONSchema202012KeywordPropertyNames'), - qe = u('JSONSchema202012KeywordUnevaluatedItems'), - $e = u('JSONSchema202012KeywordUnevaluatedProperties'), - ze = u('JSONSchema202012KeywordType'), - We = u('JSONSchema202012KeywordEnum'), - He = u('JSONSchema202012KeywordConst'), - Ye = u('JSONSchema202012KeywordConstraint'), - Xe = u('JSONSchema202012KeywordDependentRequired'), - Qe = u('JSONSchema202012KeywordContentSchema'), - et = u('JSONSchema202012KeywordTitle'), - tt = u('JSONSchema202012KeywordDescription'), - rt = u('JSONSchema202012KeywordDefault'), - nt = u('JSONSchema202012KeywordDeprecated'), - st = u('JSONSchema202012KeywordReadOnly'), - ot = u('JSONSchema202012KeywordWriteOnly'), - it = u('JSONSchema202012Accordion'), - at = u('JSONSchema202012ExpandDeepButton'), - lt = u('JSONSchema202012ChevronRightIcon'), - ct = u('withJSONSchema202012Context')(C, { - config: { - default$schema: 'https://spec.openapis.org/oas/3.1/dialect/base', - defaultExpandedLevels: x.defaultModelExpandDepth, - includeReadOnly: Boolean(o.includeReadOnly), - includeWriteOnly: Boolean(o.includeWriteOnly) - }, - components: { - JSONSchema: j, - Keyword$schema: L, - Keyword$vocabulary: B, - Keyword$id: $, - Keyword$anchor: V, - Keyword$dynamicAnchor: U, - Keyword$ref: z, - Keyword$dynamicRef: Y, - Keyword$defs: Z, - Keyword$comment: ee, - KeywordAllOf: ie, - KeywordAnyOf: ae, - KeywordOneOf: le, - KeywordNot: ce, - KeywordIf: pe, - KeywordThen: de, - KeywordElse: fe, - KeywordDependentSchemas: ye, - KeywordPrefixItems: be, - KeywordItems: _e, - KeywordContains: we, - KeywordProperties: Se, - KeywordPatternProperties: xe, - KeywordAdditionalProperties: Te, - KeywordPropertyNames: Re, - KeywordUnevaluatedItems: qe, - KeywordUnevaluatedProperties: $e, - KeywordType: ze, - KeywordEnum: We, - KeywordConst: He, - KeywordConstraint: Ye, - KeywordDependentRequired: Xe, - KeywordContentSchema: Qe, - KeywordTitle: et, - KeywordDescription: tt, - KeywordDefault: rt, - KeywordDeprecated: nt, - KeywordReadOnly: st, - KeywordWriteOnly: ot, - Accordion: it, - ExpandDeepButton: at, - ChevronRightIcon: lt - }, - fn: { - upperFirst: _.upperFirst, - isExpandable: _.jsonSchema202012.isExpandable, - getProperties: _.jsonSchema202012.getProperties - } - }); - return Pe.createElement(ct, o); - }), - WA = zA, - KA = createOnlyOAS31ComponentWrapper(({ getSystem: s }) => { - const { getComponent: o, fn: i, getConfigs: u } = s(), - _ = u(); - if (KA.ModelsWithJSONSchemaContext) - return Pe.createElement(KA.ModelsWithJSONSchemaContext, null); - const w = o('OAS31Models', !0), - x = o('JSONSchema202012'), - C = o('JSONSchema202012Keyword$schema'), - j = o('JSONSchema202012Keyword$vocabulary'), - L = o('JSONSchema202012Keyword$id'), - B = o('JSONSchema202012Keyword$anchor'), - $ = o('JSONSchema202012Keyword$dynamicAnchor'), - V = o('JSONSchema202012Keyword$ref'), - U = o('JSONSchema202012Keyword$dynamicRef'), - z = o('JSONSchema202012Keyword$defs'), - Y = o('JSONSchema202012Keyword$comment'), - Z = o('JSONSchema202012KeywordAllOf'), - ee = o('JSONSchema202012KeywordAnyOf'), - ie = o('JSONSchema202012KeywordOneOf'), - ae = o('JSONSchema202012KeywordNot'), - le = o('JSONSchema202012KeywordIf'), - ce = o('JSONSchema202012KeywordThen'), - pe = o('JSONSchema202012KeywordElse'), - de = o('JSONSchema202012KeywordDependentSchemas'), - fe = o('JSONSchema202012KeywordPrefixItems'), - ye = o('JSONSchema202012KeywordItems'), - be = o('JSONSchema202012KeywordContains'), - _e = o('JSONSchema202012KeywordProperties'), - we = o('JSONSchema202012KeywordPatternProperties'), - Se = o('JSONSchema202012KeywordAdditionalProperties'), - xe = o('JSONSchema202012KeywordPropertyNames'), - Te = o('JSONSchema202012KeywordUnevaluatedItems'), - Re = o('JSONSchema202012KeywordUnevaluatedProperties'), - qe = o('JSONSchema202012KeywordType'), - $e = o('JSONSchema202012KeywordEnum'), - ze = o('JSONSchema202012KeywordConst'), - We = o('JSONSchema202012KeywordConstraint'), - He = o('JSONSchema202012KeywordDependentRequired'), - Ye = o('JSONSchema202012KeywordContentSchema'), - Xe = o('JSONSchema202012KeywordTitle'), - Qe = o('JSONSchema202012KeywordDescription'), - et = o('JSONSchema202012KeywordDefault'), - tt = o('JSONSchema202012KeywordDeprecated'), - rt = o('JSONSchema202012KeywordReadOnly'), - nt = o('JSONSchema202012KeywordWriteOnly'), - st = o('JSONSchema202012Accordion'), - ot = o('JSONSchema202012ExpandDeepButton'), - it = o('JSONSchema202012ChevronRightIcon'), - at = o('withJSONSchema202012Context'); - return ( - (KA.ModelsWithJSONSchemaContext = at(w, { - config: { - default$schema: 'https://spec.openapis.org/oas/3.1/dialect/base', - defaultExpandedLevels: _.defaultModelsExpandDepth - 1, - includeReadOnly: !0, - includeWriteOnly: !0 - }, - components: { - JSONSchema: x, - Keyword$schema: C, - Keyword$vocabulary: j, - Keyword$id: L, - Keyword$anchor: B, - Keyword$dynamicAnchor: $, - Keyword$ref: V, - Keyword$dynamicRef: U, - Keyword$defs: z, - Keyword$comment: Y, - KeywordAllOf: Z, - KeywordAnyOf: ee, - KeywordOneOf: ie, - KeywordNot: ae, - KeywordIf: le, - KeywordThen: ce, - KeywordElse: pe, - KeywordDependentSchemas: de, - KeywordPrefixItems: fe, - KeywordItems: ye, - KeywordContains: be, - KeywordProperties: _e, - KeywordPatternProperties: we, - KeywordAdditionalProperties: Se, - KeywordPropertyNames: xe, - KeywordUnevaluatedItems: Te, - KeywordUnevaluatedProperties: Re, - KeywordType: qe, - KeywordEnum: $e, - KeywordConst: ze, - KeywordConstraint: We, - KeywordDependentRequired: He, - KeywordContentSchema: Ye, - KeywordTitle: Xe, - KeywordDescription: Qe, - KeywordDefault: et, - KeywordDeprecated: tt, - KeywordReadOnly: rt, - KeywordWriteOnly: nt, - Accordion: st, - ExpandDeepButton: ot, - ChevronRightIcon: it - }, - fn: { - upperFirst: i.upperFirst, - isExpandable: i.jsonSchema202012.isExpandable, - getProperties: i.jsonSchema202012.getProperties - } - })), - Pe.createElement(KA.ModelsWithJSONSchemaContext, null) - ); - }); - KA.ModelsWithJSONSchemaContext = null; - const HA = KA, - wrap_components_version_pragma_filter = (s, o) => (s) => { - const i = o.specSelectors.isOAS31(), - u = o.getComponent('OAS31VersionPragmaFilter'); - return Pe.createElement(u, Rn()({ isOAS31: i }, s)); - }, - JA = createOnlyOAS31ComponentWrapper(({ originalComponent: s, ...o }) => { - const { getComponent: i, schema: u } = o, - _ = i('MutualTLSAuth', !0); - return 'mutualTLS' === u.get('type') - ? Pe.createElement(_, { schema: u }) - : Pe.createElement(s, o); - }), - GA = JA, - YA = createOnlyOAS31ComponentWrapper(({ getSystem: s, ...o }) => { - const i = s().getComponent('OAS31Auths', !0); - return Pe.createElement(i, o); - }), - XA = (0, qe.Map)(), - ZA = Ut((s, o) => o.specSelectors.specJson(), isOAS31), - selectors_webhooks = () => (s) => { - const o = s.specSelectors.specJson().get('webhooks'); - return qe.Map.isMap(o) ? o : XA; - }, - QA = Ut( - [ - (s, o) => o.specSelectors.webhooks(), - (s, o) => o.specSelectors.validOperationMethods(), - (s, o) => o.specSelectors.specResolvedSubtree(['webhooks']) - ], - (s, o) => - s - .reduce( - (s, i, u) => { - if (!qe.Map.isMap(i)) return s; - const _ = i - .entrySeq() - .filter(([s]) => o.includes(s)) - .map(([s, o]) => ({ - operation: (0, qe.Map)({ operation: o }), - method: s, - path: u, - specPath: ['webhooks', u, s] - })); - return s.concat(_); - }, - (0, qe.List)() - ) - .groupBy((s) => s.path) - .map((s) => s.toArray()) - .toObject() - ), - selectors_license = () => (s) => { - const o = s.specSelectors.info().get('license'); - return qe.Map.isMap(o) ? o : XA; - }, - selectLicenseNameField = () => (s) => s.specSelectors.license().get('name', 'License'), - selectLicenseUrlField = () => (s) => s.specSelectors.license().get('url'), - ej = Ut( - [ - (s, o) => o.specSelectors.url(), - (s, o) => o.oas3Selectors.selectedServer(), - (s, o) => o.specSelectors.selectLicenseUrlField() - ], - (s, o, i) => { - if (i) return safeBuildUrl(i, s, { selectedServer: o }); - } - ), - selectLicenseIdentifierField = () => (s) => s.specSelectors.license().get('identifier'), - selectors_contact = () => (s) => { - const o = s.specSelectors.info().get('contact'); - return qe.Map.isMap(o) ? o : XA; - }, - selectContactNameField = () => (s) => - s.specSelectors.contact().get('name', 'the developer'), - selectContactEmailField = () => (s) => s.specSelectors.contact().get('email'), - selectContactUrlField = () => (s) => s.specSelectors.contact().get('url'), - fj = Ut( - [ - (s, o) => o.specSelectors.url(), - (s, o) => o.oas3Selectors.selectedServer(), - (s, o) => o.specSelectors.selectContactUrlField() - ], - (s, o, i) => { - if (i) return safeBuildUrl(i, s, { selectedServer: o }); - } - ), - selectInfoTitleField = () => (s) => s.specSelectors.info().get('title'), - selectInfoSummaryField = () => (s) => s.specSelectors.info().get('summary'), - selectInfoDescriptionField = () => (s) => s.specSelectors.info().get('description'), - selectInfoTermsOfServiceField = () => (s) => s.specSelectors.info().get('termsOfService'), - mj = Ut( - [ - (s, o) => o.specSelectors.url(), - (s, o) => o.oas3Selectors.selectedServer(), - (s, o) => o.specSelectors.selectInfoTermsOfServiceField() - ], - (s, o, i) => { - if (i) return safeBuildUrl(i, s, { selectedServer: o }); - } - ), - selectExternalDocsDescriptionField = () => (s) => - s.specSelectors.externalDocs().get('description'), - selectExternalDocsUrlField = () => (s) => s.specSelectors.externalDocs().get('url'), - _j = Ut( - [ - (s, o) => o.specSelectors.url(), - (s, o) => o.oas3Selectors.selectedServer(), - (s, o) => o.specSelectors.selectExternalDocsUrlField() - ], - (s, o, i) => { - if (i) return safeBuildUrl(i, s, { selectedServer: o }); - } - ), - selectJsonSchemaDialectField = () => (s) => - s.specSelectors.specJson().get('jsonSchemaDialect'), - selectJsonSchemaDialectDefault = () => 'https://spec.openapis.org/oas/3.1/dialect/base', - Cj = Ut( - (s, o) => o.specSelectors.definitions(), - (s, o) => o.specSelectors.specResolvedSubtree(['components', 'schemas']), - (s, o) => - qe.Map.isMap(s) - ? qe.Map.isMap(o) - ? Object.entries(s.toJS()).reduce((s, [i, u]) => { - const _ = o.get(i); - return (s[i] = _?.toJS() || u), s; - }, {}) - : s.toJS() - : {} - ), - wrap_selectors_isOAS3 = - (s, o) => - (i, ...u) => - o.specSelectors.isOAS31() || s(...u), - Aj = createOnlyOAS31SelectorWrapper(() => (s, o) => o.oas31Selectors.selectLicenseUrl()), - Nj = createOnlyOAS31SelectorWrapper(() => (s, o) => { - const i = o.specSelectors.securityDefinitions(); - let u = s(); - return i - ? (i.entrySeq().forEach(([s, o]) => { - 'mutualTLS' === o.get('type') && (u = u.push(new qe.Map({ [s]: o }))); - }), - u) - : u; - }), - Bj = Ut( - [ - (s, o) => o.specSelectors.url(), - (s, o) => o.oas3Selectors.selectedServer(), - (s, o) => o.specSelectors.selectLicenseUrlField(), - (s, o) => o.specSelectors.selectLicenseIdentifierField() - ], - (s, o, i, u) => - i - ? safeBuildUrl(i, s, { selectedServer: o }) - : u - ? `https://spdx.org/licenses/${u}.html` - : void 0 - ), - keywords_Example = ({ schema: s, getSystem: o }) => { - const { fn: i } = o(), - { hasKeyword: u, stringify: _ } = i.jsonSchema202012.useFn(); - return u(s, 'example') - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--example' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'Example' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' - }, - _(s.example) - ) - ) - : null; - }, - keywords_Xml = ({ schema: s, getSystem: o }) => { - const i = s?.xml || {}, - { fn: u, getComponent: _ } = o(), - { useIsExpandedDeeply: w, useComponent: x } = u.jsonSchema202012, - C = w(), - j = !!(i.name || i.namespace || i.prefix), - [L, B] = (0, Pe.useState)(C), - [$, V] = (0, Pe.useState)(!1), - U = x('Accordion'), - z = x('ExpandDeepButton'), - Y = _('JSONSchema202012DeepExpansionContext')(), - Z = (0, Pe.useCallback)(() => { - B((s) => !s); - }, []), - ee = (0, Pe.useCallback)((s, o) => { - B(o), V(o); - }, []); - return 0 === Object.keys(i).length - ? null - : Pe.createElement( - Y.Provider, - { value: $ }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--xml' }, - j - ? Pe.createElement( - Pe.Fragment, - null, - Pe.createElement( - U, - { expanded: L, onChange: Z }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'XML' - ) - ), - Pe.createElement(z, { expanded: L, onClick: ee }) - ) - : Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'XML' - ), - !0 === i.attribute && - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' - }, - 'attribute' - ), - !0 === i.wrapped && - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' - }, - 'wrapped' - ), - Pe.createElement( - 'strong', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'object' - ), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !L - }) - }, - L && - Pe.createElement( - Pe.Fragment, - null, - i.name && - Pe.createElement( - 'li', - { className: 'json-schema-2020-12-property' }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword' - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'name' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - i.name - ) - ) - ), - i.namespace && - Pe.createElement( - 'li', - { className: 'json-schema-2020-12-property' }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'namespace' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - i.namespace - ) - ) - ), - i.prefix && - Pe.createElement( - 'li', - { className: 'json-schema-2020-12-property' }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'prefix' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - i.prefix - ) - ) - ) - ) - ) - ) - ); - }, - Discriminator_DiscriminatorMapping = ({ discriminator: s }) => { - const o = s?.mapping || {}; - return 0 === Object.keys(o).length - ? null - : Object.entries(o).map(([s, o]) => - Pe.createElement( - 'div', - { key: `${s}-${o}`, className: 'json-schema-2020-12-keyword' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - s - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - o - ) - ) - ); - }, - keywords_Discriminator_Discriminator = ({ schema: s, getSystem: o }) => { - const i = s?.discriminator || {}, - { fn: u, getComponent: _ } = o(), - { useIsExpandedDeeply: w, useComponent: x } = u.jsonSchema202012, - C = w(), - j = !!i.mapping, - [L, B] = (0, Pe.useState)(C), - [$, V] = (0, Pe.useState)(!1), - U = x('Accordion'), - z = x('ExpandDeepButton'), - Y = _('JSONSchema202012DeepExpansionContext')(), - Z = (0, Pe.useCallback)(() => { - B((s) => !s); - }, []), - ee = (0, Pe.useCallback)((s, o) => { - B(o), V(o); - }, []); - return 0 === Object.keys(i).length - ? null - : Pe.createElement( - Y.Provider, - { value: $ }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--discriminator' - }, - j - ? Pe.createElement( - Pe.Fragment, - null, - Pe.createElement( - U, - { expanded: L, onChange: Z }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'Discriminator' - ) - ), - Pe.createElement(z, { expanded: L, onClick: ee }) - ) - : Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'Discriminator' - ), - i.propertyName && - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' - }, - i.propertyName - ), - Pe.createElement( - 'strong', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'object' - ), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !L - }) - }, - L && - Pe.createElement( - 'li', - { className: 'json-schema-2020-12-property' }, - Pe.createElement(Discriminator_DiscriminatorMapping, { discriminator: i }) - ) - ) - ) - ); - }, - keywords_ExternalDocs = ({ schema: s, getSystem: o }) => { - const i = s?.externalDocs || {}, - { fn: u, getComponent: _ } = o(), - { useIsExpandedDeeply: w, useComponent: x } = u.jsonSchema202012, - C = w(), - j = !(!i.description && !i.url), - [L, B] = (0, Pe.useState)(C), - [$, V] = (0, Pe.useState)(!1), - U = x('Accordion'), - z = x('ExpandDeepButton'), - Y = _('JSONSchema202012KeywordDescription'), - Z = _('Link'), - ee = _('JSONSchema202012DeepExpansionContext')(), - ie = (0, Pe.useCallback)(() => { - B((s) => !s); - }, []), - ae = (0, Pe.useCallback)((s, o) => { - B(o), V(o); - }, []); - return 0 === Object.keys(i).length - ? null - : Pe.createElement( - ee.Provider, - { value: $ }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--externalDocs' - }, - j - ? Pe.createElement( - Pe.Fragment, - null, - Pe.createElement( - U, - { expanded: L, onChange: ie }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'External documentation' - ) - ), - Pe.createElement(z, { expanded: L, onClick: ae }) - ) - : Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'External documentation' - ), - Pe.createElement( - 'strong', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'object' - ), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !L - }) - }, - L && - Pe.createElement( - Pe.Fragment, - null, - i.description && - Pe.createElement( - 'li', - { className: 'json-schema-2020-12-property' }, - Pe.createElement(Y, { schema: i, getSystem: o }) - ), - i.url && - Pe.createElement( - 'li', - { className: 'json-schema-2020-12-property' }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword' - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - 'url' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - Pe.createElement( - Z, - { target: '_blank', href: sanitizeUrl(i.url) }, - i.url - ) - ) - ) - ) - ) - ) - ) - ); - }, - keywords_Description = ({ schema: s, getSystem: o }) => { - if (!s?.description) return null; - const { getComponent: i } = o(), - u = i('Markdown'); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--description' }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary' - }, - Pe.createElement(u, { source: s.description }) - ) - ); - }, - $j = createOnlyOAS31ComponentWrapper(keywords_Description), - zj = createOnlyOAS31ComponentWrapper( - ({ schema: s, getSystem: o, originalComponent: i }) => { - const { getComponent: u } = o(), - _ = u('JSONSchema202012KeywordDiscriminator'), - w = u('JSONSchema202012KeywordXml'), - x = u('JSONSchema202012KeywordExample'), - C = u('JSONSchema202012KeywordExternalDocs'); - return Pe.createElement( - Pe.Fragment, - null, - Pe.createElement(i, { schema: s }), - Pe.createElement(_, { schema: s, getSystem: o }), - Pe.createElement(w, { schema: s, getSystem: o }), - Pe.createElement(C, { schema: s, getSystem: o }), - Pe.createElement(x, { schema: s, getSystem: o }) - ); - } - ), - Kj = zj, - keywords_Properties = ({ schema: s, getSystem: o }) => { - const { fn: i } = o(), - { useComponent: u } = i.jsonSchema202012, - { getDependentRequired: _, getProperties: w } = i.jsonSchema202012.useFn(), - x = i.jsonSchema202012.useConfig(), - C = Array.isArray(s?.required) ? s.required : [], - j = u('JSONSchema'), - L = w(s, x); - return 0 === Object.keys(L).length - ? null - : Pe.createElement( - 'div', - { - className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--properties' - }, - Pe.createElement( - 'ul', - null, - Object.entries(L).map(([o, i]) => { - const u = C.includes(o), - w = _(o, s); - return Pe.createElement( - 'li', - { - key: o, - className: Hn()('json-schema-2020-12-property', { - 'json-schema-2020-12-property--required': u - }) - }, - Pe.createElement(j, { name: o, schema: i, dependentRequired: w }) - ); - }) - ) - ); - }, - Jj = createOnlyOAS31ComponentWrapper(keywords_Properties), - getProperties = (s, { includeReadOnly: o, includeWriteOnly: i }) => { - if (!s?.properties) return {}; - const u = Object.entries(s.properties).filter( - ([, s]) => (!(!0 === s?.readOnly) || o) && (!(!0 === s?.writeOnly) || i) - ); - return Object.fromEntries(u); - }; - const Gj = function oas31_after_load_afterLoad({ fn: s, getSystem: o }) { - if (s.jsonSchema202012) { - const i = ((s, o) => { - const { fn: i } = o(); - if ('function' != typeof s) return null; - const { hasKeyword: u } = i.jsonSchema202012; - return (o) => - s(o) || u(o, 'example') || o?.xml || o?.discriminator || o?.externalDocs; - })(s.jsonSchema202012.isExpandable, o); - Object.assign(this.fn.jsonSchema202012, { isExpandable: i, getProperties }); - } - if ('function' == typeof s.sampleFromSchema && s.jsonSchema202012) { - const i = ((s, o) => { - const { fn: i, specSelectors: u } = o; - return Object.fromEntries( - Object.entries(s).map(([s, o]) => { - const _ = i[s]; - return [ - s, - (...s) => (u.isOAS31() ? o(...s) : 'function' == typeof _ ? _(...s) : void 0) - ]; - }) - ); - })( - { - sampleFromSchema: s.jsonSchema202012.sampleFromSchema, - sampleFromSchemaGeneric: s.jsonSchema202012.sampleFromSchemaGeneric, - createXMLExample: s.jsonSchema202012.createXMLExample, - memoizedSampleFromSchema: s.jsonSchema202012.memoizedSampleFromSchema, - memoizedCreateXMLExample: s.jsonSchema202012.memoizedCreateXMLExample, - getJsonSampleSchema: s.jsonSchema202012.getJsonSampleSchema, - getYamlSampleSchema: s.jsonSchema202012.getYamlSampleSchema, - getXmlSampleSchema: s.jsonSchema202012.getXmlSampleSchema, - getSampleSchema: s.jsonSchema202012.getSampleSchema, - mergeJsonSchema: s.jsonSchema202012.mergeJsonSchema - }, - o() - ); - Object.assign(this.fn, i); - } - }, - oas31 = ({ fn: s }) => { - const o = s.createSystemSelector || fn_createSystemSelector, - i = s.createOnlyOAS31Selector || fn_createOnlyOAS31Selector; - return { - afterLoad: Gj, - fn: { - isOAS31, - createSystemSelector: fn_createSystemSelector, - createOnlyOAS31Selector: fn_createOnlyOAS31Selector - }, - components: { - Webhooks: webhooks, - JsonSchemaDialect: json_schema_dialect, - MutualTLSAuth: mutual_tls_auth, - OAS31Info: oas31_components_info, - OAS31License: oas31_components_license, - OAS31Contact: oas31_components_contact, - OAS31VersionPragmaFilter: version_pragma_filter, - OAS31Model: FA, - OAS31Models: models, - OAS31Auths: qA, - JSONSchema202012KeywordExample: keywords_Example, - JSONSchema202012KeywordXml: keywords_Xml, - JSONSchema202012KeywordDiscriminator: keywords_Discriminator_Discriminator, - JSONSchema202012KeywordExternalDocs: keywords_ExternalDocs - }, - wrapComponents: { - InfoContainer: UA, - License: $A, - Contact: VA, - VersionPragmaFilter: wrap_components_version_pragma_filter, - Model: WA, - Models: HA, - AuthItem: GA, - auths: YA, - JSONSchema202012KeywordDescription: $j, - JSONSchema202012KeywordDefault: Kj, - JSONSchema202012KeywordProperties: Jj - }, - statePlugins: { - auth: { wrapSelectors: { definitionsToAuthorize: Nj } }, - spec: { - selectors: { - isOAS31: o(ZA), - license: selectors_license, - selectLicenseNameField, - selectLicenseUrlField, - selectLicenseIdentifierField: i(selectLicenseIdentifierField), - selectLicenseUrl: o(ej), - contact: selectors_contact, - selectContactNameField, - selectContactEmailField, - selectContactUrlField, - selectContactUrl: o(fj), - selectInfoTitleField, - selectInfoSummaryField: i(selectInfoSummaryField), - selectInfoDescriptionField, - selectInfoTermsOfServiceField, - selectInfoTermsOfServiceUrl: o(mj), - selectExternalDocsDescriptionField, - selectExternalDocsUrlField, - selectExternalDocsUrl: o(_j), - webhooks: i(selectors_webhooks), - selectWebhooksOperations: i(o(QA)), - selectJsonSchemaDialectField, - selectJsonSchemaDialectDefault, - selectSchemas: o(Cj) - }, - wrapSelectors: { isOAS3: wrap_selectors_isOAS3, selectLicenseUrl: Aj } - }, - oas31: { selectors: { selectLicenseUrl: i(o(Bj)) } } - } - }; - }, - Xj = ts().object, - eI = ts().bool, - tI = (ts().oneOfType([Xj, eI]), (0, Pe.createContext)(null)); - tI.displayName = 'JSONSchemaContext'; - const rI = (0, Pe.createContext)(0); - rI.displayName = 'JSONSchemaLevelContext'; - const nI = (0, Pe.createContext)(!1); - nI.displayName = 'JSONSchemaDeepExpansionContext'; - const sI = (0, Pe.createContext)(new Set()), - useConfig = () => { - const { config: s } = (0, Pe.useContext)(tI); - return s; - }, - useComponent = (s) => { - const { components: o } = (0, Pe.useContext)(tI); - return o[s] || null; - }, - useFn = (s = void 0) => { - const { fn: o } = (0, Pe.useContext)(tI); - return void 0 !== s ? o[s] : o; - }, - useLevel = () => { - const s = (0, Pe.useContext)(rI); - return [s, s + 1]; - }, - useIsExpanded = () => { - const [s] = useLevel(), - { defaultExpandedLevels: o } = useConfig(); - return o - s > 0; - }, - useIsExpandedDeeply = () => (0, Pe.useContext)(nI), - useRenderedSchemas = (s = void 0) => { - if (void 0 === s) return (0, Pe.useContext)(sI); - const o = (0, Pe.useContext)(sI); - return new Set([...o, s]); - }, - oI = (0, Pe.forwardRef)( - ({ schema: s, name: o = '', dependentRequired: i = [], onExpand: u = () => {} }, _) => { - const w = useFn(), - x = useIsExpanded(), - C = useIsExpandedDeeply(), - [j, L] = (0, Pe.useState)(x || C), - [B, $] = (0, Pe.useState)(C), - [V, U] = useLevel(), - z = (() => { - const [s] = useLevel(); - return s > 0; - })(), - Y = w.isExpandable(s) || i.length > 0, - Z = ((s) => useRenderedSchemas().has(s))(s), - ee = useRenderedSchemas(s), - ie = w.stringifyConstraints(s), - ae = useComponent('Accordion'), - le = useComponent('Keyword$schema'), - ce = useComponent('Keyword$vocabulary'), - pe = useComponent('Keyword$id'), - de = useComponent('Keyword$anchor'), - fe = useComponent('Keyword$dynamicAnchor'), - ye = useComponent('Keyword$ref'), - be = useComponent('Keyword$dynamicRef'), - _e = useComponent('Keyword$defs'), - we = useComponent('Keyword$comment'), - Se = useComponent('KeywordAllOf'), - xe = useComponent('KeywordAnyOf'), - Te = useComponent('KeywordOneOf'), - Re = useComponent('KeywordNot'), - qe = useComponent('KeywordIf'), - $e = useComponent('KeywordThen'), - ze = useComponent('KeywordElse'), - We = useComponent('KeywordDependentSchemas'), - He = useComponent('KeywordPrefixItems'), - Ye = useComponent('KeywordItems'), - Xe = useComponent('KeywordContains'), - Qe = useComponent('KeywordProperties'), - et = useComponent('KeywordPatternProperties'), - tt = useComponent('KeywordAdditionalProperties'), - rt = useComponent('KeywordPropertyNames'), - nt = useComponent('KeywordUnevaluatedItems'), - st = useComponent('KeywordUnevaluatedProperties'), - ot = useComponent('KeywordType'), - it = useComponent('KeywordEnum'), - at = useComponent('KeywordConst'), - lt = useComponent('KeywordConstraint'), - ct = useComponent('KeywordDependentRequired'), - ut = useComponent('KeywordContentSchema'), - pt = useComponent('KeywordTitle'), - ht = useComponent('KeywordDescription'), - dt = useComponent('KeywordDefault'), - mt = useComponent('KeywordDeprecated'), - gt = useComponent('KeywordReadOnly'), - yt = useComponent('KeywordWriteOnly'), - vt = useComponent('ExpandDeepButton'); - (0, Pe.useEffect)(() => { - $(C); - }, [C]), - (0, Pe.useEffect)(() => { - $(B); - }, [B]); - const bt = (0, Pe.useCallback)( - (s, o) => { - L(o), !o && $(!1), u(s, o, !1); - }, - [u] - ), - _t = (0, Pe.useCallback)( - (s, o) => { - L(o), $(o), u(s, o, !0); - }, - [u] - ); - return Pe.createElement( - rI.Provider, - { value: U }, - Pe.createElement( - nI.Provider, - { value: B }, - Pe.createElement( - sI.Provider, - { value: ee }, - Pe.createElement( - 'article', - { - ref: _, - 'data-json-schema-level': V, - className: Hn()('json-schema-2020-12', { - 'json-schema-2020-12--embedded': z, - 'json-schema-2020-12--circular': Z - }) - }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-head' }, - Y && !Z - ? Pe.createElement( - Pe.Fragment, - null, - Pe.createElement( - ae, - { expanded: j, onChange: bt }, - Pe.createElement(pt, { title: o, schema: s }) - ), - Pe.createElement(vt, { expanded: j, onClick: _t }) - ) - : Pe.createElement(pt, { title: o, schema: s }), - Pe.createElement(mt, { schema: s }), - Pe.createElement(gt, { schema: s }), - Pe.createElement(yt, { schema: s }), - Pe.createElement(ot, { schema: s, isCircular: Z }), - ie.length > 0 && - ie.map((s) => - Pe.createElement(lt, { key: `${s.scope}-${s.value}`, constraint: s }) - ) - ), - Pe.createElement( - 'div', - { - className: Hn()('json-schema-2020-12-body', { - 'json-schema-2020-12-body--collapsed': !j - }) - }, - j && - Pe.createElement( - Pe.Fragment, - null, - Pe.createElement(ht, { schema: s }), - !Z && - Y && - Pe.createElement( - Pe.Fragment, - null, - Pe.createElement(Qe, { schema: s }), - Pe.createElement(et, { schema: s }), - Pe.createElement(tt, { schema: s }), - Pe.createElement(st, { schema: s }), - Pe.createElement(rt, { schema: s }), - Pe.createElement(Se, { schema: s }), - Pe.createElement(xe, { schema: s }), - Pe.createElement(Te, { schema: s }), - Pe.createElement(Re, { schema: s }), - Pe.createElement(qe, { schema: s }), - Pe.createElement($e, { schema: s }), - Pe.createElement(ze, { schema: s }), - Pe.createElement(We, { schema: s }), - Pe.createElement(He, { schema: s }), - Pe.createElement(Ye, { schema: s }), - Pe.createElement(nt, { schema: s }), - Pe.createElement(Xe, { schema: s }), - Pe.createElement(ut, { schema: s }) - ), - Pe.createElement(it, { schema: s }), - Pe.createElement(at, { schema: s }), - Pe.createElement(ct, { schema: s, dependentRequired: i }), - Pe.createElement(dt, { schema: s }), - Pe.createElement(le, { schema: s }), - Pe.createElement(ce, { schema: s }), - Pe.createElement(pe, { schema: s }), - Pe.createElement(de, { schema: s }), - Pe.createElement(fe, { schema: s }), - Pe.createElement(ye, { schema: s }), - !Z && Y && Pe.createElement(_e, { schema: s }), - Pe.createElement(be, { schema: s }), - Pe.createElement(we, { schema: s }) - ) - ) - ) - ) - ) - ); - } - ), - iI = oI, - keywords_$schema = ({ schema: s }) => - s?.$schema - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$schema' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$schema' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$schema - ) - ) - : null, - $vocabulary_$vocabulary = ({ schema: s }) => { - const o = useIsExpanded(), - i = useIsExpandedDeeply(), - [u, _] = (0, Pe.useState)(o || i), - w = useComponent('Accordion'), - x = (0, Pe.useCallback)(() => { - _((s) => !s); - }, []); - return s?.$vocabulary - ? 'object' != typeof s.$vocabulary - ? null - : Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$vocabulary' - }, - Pe.createElement( - w, - { expanded: u, onChange: x }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$vocabulary' - ) - ), - Pe.createElement( - 'strong', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'object' - ), - Pe.createElement( - 'ul', - null, - u && - Object.entries(s.$vocabulary).map(([s, o]) => - Pe.createElement( - 'li', - { - key: s, - className: Hn()('json-schema-2020-12-$vocabulary-uri', { - 'json-schema-2020-12-$vocabulary-uri--disabled': !o - }) - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s - ) - ) - ) - ) - ) - : null; - }, - keywords_$id = ({ schema: s }) => - s?.$id - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$id' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$id' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$id - ) - ) - : null, - keywords_$anchor = ({ schema: s }) => - s?.$anchor - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$anchor' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$anchor' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$anchor - ) - ) - : null, - keywords_$dynamicAnchor = ({ schema: s }) => - s?.$dynamicAnchor - ? Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicAnchor' - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$dynamicAnchor' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$dynamicAnchor - ) - ) - : null, - keywords_$ref = ({ schema: s }) => - s?.$ref - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$ref' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$ref' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$ref - ) - ) - : null, - keywords_$dynamicRef = ({ schema: s }) => - s?.$dynamicRef - ? Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$dynamicRef' - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$dynamicRef' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$dynamicRef - ) - ) - : null, - keywords_$defs = ({ schema: s }) => { - const o = s?.$defs || {}, - i = useIsExpanded(), - u = useIsExpandedDeeply(), - [_, w] = (0, Pe.useState)(i || u), - [x, C] = (0, Pe.useState)(!1), - j = useComponent('Accordion'), - L = useComponent('ExpandDeepButton'), - B = useComponent('JSONSchema'), - $ = (0, Pe.useCallback)(() => { - w((s) => !s); - }, []), - V = (0, Pe.useCallback)((s, o) => { - w(o), C(o); - }, []); - return 0 === Object.keys(o).length - ? null - : Pe.createElement( - nI.Provider, - { value: x }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$defs' }, - Pe.createElement( - j, - { expanded: _, onChange: $ }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$defs' - ) - ), - Pe.createElement(L, { expanded: _, onClick: V }), - Pe.createElement( - 'strong', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'object' - ), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !_ - }) - }, - _ && - Pe.createElement( - Pe.Fragment, - null, - Object.entries(o).map(([s, o]) => - Pe.createElement( - 'li', - { key: s, className: 'json-schema-2020-12-property' }, - Pe.createElement(B, { name: s, schema: o }) - ) - ) - ) - ) - ) - ); - }, - keywords_$comment = ({ schema: s }) => - s?.$comment - ? Pe.createElement( - 'div', - { - className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--$comment' - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--secondary' - }, - '$comment' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--secondary' - }, - s.$comment - ) - ) - : null, - keywords_AllOf = ({ schema: s }) => { - const o = s?.allOf || [], - i = useFn(), - u = useIsExpanded(), - _ = useIsExpandedDeeply(), - [w, x] = (0, Pe.useState)(u || _), - [C, j] = (0, Pe.useState)(!1), - L = useComponent('Accordion'), - B = useComponent('ExpandDeepButton'), - $ = useComponent('JSONSchema'), - V = useComponent('KeywordType'), - U = (0, Pe.useCallback)(() => { - x((s) => !s); - }, []), - z = (0, Pe.useCallback)((s, o) => { - x(o), j(o); - }, []); - return Array.isArray(o) && 0 !== o.length - ? Pe.createElement( - nI.Provider, - { value: C }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--allOf' }, - Pe.createElement( - L, - { expanded: w, onChange: U }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'All of' - ) - ), - Pe.createElement(B, { expanded: w, onClick: z }), - Pe.createElement(V, { schema: { allOf: o } }), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !w - }) - }, - w && - Pe.createElement( - Pe.Fragment, - null, - o.map((s, o) => - Pe.createElement( - 'li', - { key: `#${o}`, className: 'json-schema-2020-12-property' }, - Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) - ) - ) - ) - ) - ) - ) - : null; - }, - keywords_AnyOf = ({ schema: s }) => { - const o = s?.anyOf || [], - i = useFn(), - u = useIsExpanded(), - _ = useIsExpandedDeeply(), - [w, x] = (0, Pe.useState)(u || _), - [C, j] = (0, Pe.useState)(!1), - L = useComponent('Accordion'), - B = useComponent('ExpandDeepButton'), - $ = useComponent('JSONSchema'), - V = useComponent('KeywordType'), - U = (0, Pe.useCallback)(() => { - x((s) => !s); - }, []), - z = (0, Pe.useCallback)((s, o) => { - x(o), j(o); - }, []); - return Array.isArray(o) && 0 !== o.length - ? Pe.createElement( - nI.Provider, - { value: C }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--anyOf' }, - Pe.createElement( - L, - { expanded: w, onChange: U }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Any of' - ) - ), - Pe.createElement(B, { expanded: w, onClick: z }), - Pe.createElement(V, { schema: { anyOf: o } }), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !w - }) - }, - w && - Pe.createElement( - Pe.Fragment, - null, - o.map((s, o) => - Pe.createElement( - 'li', - { key: `#${o}`, className: 'json-schema-2020-12-property' }, - Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) - ) - ) - ) - ) - ) - ) - : null; - }, - keywords_OneOf = ({ schema: s }) => { - const o = s?.oneOf || [], - i = useFn(), - u = useIsExpanded(), - _ = useIsExpandedDeeply(), - [w, x] = (0, Pe.useState)(u || _), - [C, j] = (0, Pe.useState)(!1), - L = useComponent('Accordion'), - B = useComponent('ExpandDeepButton'), - $ = useComponent('JSONSchema'), - V = useComponent('KeywordType'), - U = (0, Pe.useCallback)(() => { - x((s) => !s); - }, []), - z = (0, Pe.useCallback)((s, o) => { - x(o), j(o); - }, []); - return Array.isArray(o) && 0 !== o.length - ? Pe.createElement( - nI.Provider, - { value: C }, - Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--oneOf' }, - Pe.createElement( - L, - { expanded: w, onChange: U }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'One of' - ) - ), - Pe.createElement(B, { expanded: w, onClick: z }), - Pe.createElement(V, { schema: { oneOf: o } }), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !w - }) - }, - w && - Pe.createElement( - Pe.Fragment, - null, - o.map((s, o) => - Pe.createElement( - 'li', - { key: `#${o}`, className: 'json-schema-2020-12-property' }, - Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) - ) - ) - ) - ) - ) - ) - : null; - }, - keywords_Not = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'not')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Not' - ); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--not' }, - Pe.createElement(i, { name: u, schema: s.not }) - ); - }, - keywords_If = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'if')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'If' - ); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--if' }, - Pe.createElement(i, { name: u, schema: s.if }) - ); - }, - keywords_Then = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'then')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Then' - ); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--then' }, - Pe.createElement(i, { name: u, schema: s.then }) - ); - }, - keywords_Else = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'else')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Else' - ); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--if' }, - Pe.createElement(i, { name: u, schema: s.else }) - ); - }, - keywords_DependentSchemas = ({ schema: s }) => { - const o = s?.dependentSchemas || [], - i = useIsExpanded(), - u = useIsExpandedDeeply(), - [_, w] = (0, Pe.useState)(i || u), - [x, C] = (0, Pe.useState)(!1), - j = useComponent('Accordion'), - L = useComponent('ExpandDeepButton'), - B = useComponent('JSONSchema'), - $ = (0, Pe.useCallback)(() => { - w((s) => !s); - }, []), - V = (0, Pe.useCallback)((s, o) => { - w(o), C(o); - }, []); - return 'object' != typeof o || 0 === Object.keys(o).length - ? null - : Pe.createElement( - nI.Provider, - { value: x }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentSchemas' - }, - Pe.createElement( - j, - { expanded: _, onChange: $ }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Dependent schemas' - ) - ), - Pe.createElement(L, { expanded: _, onClick: V }), - Pe.createElement( - 'strong', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'object' - ), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !_ - }) - }, - _ && - Pe.createElement( - Pe.Fragment, - null, - Object.entries(o).map(([s, o]) => - Pe.createElement( - 'li', - { key: s, className: 'json-schema-2020-12-property' }, - Pe.createElement(B, { name: s, schema: o }) - ) - ) - ) - ) - ) - ); - }, - keywords_PrefixItems = ({ schema: s }) => { - const o = s?.prefixItems || [], - i = useFn(), - u = useIsExpanded(), - _ = useIsExpandedDeeply(), - [w, x] = (0, Pe.useState)(u || _), - [C, j] = (0, Pe.useState)(!1), - L = useComponent('Accordion'), - B = useComponent('ExpandDeepButton'), - $ = useComponent('JSONSchema'), - V = useComponent('KeywordType'), - U = (0, Pe.useCallback)(() => { - x((s) => !s); - }, []), - z = (0, Pe.useCallback)((s, o) => { - x(o), j(o); - }, []); - return Array.isArray(o) && 0 !== o.length - ? Pe.createElement( - nI.Provider, - { value: C }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--prefixItems' - }, - Pe.createElement( - L, - { expanded: w, onChange: U }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Prefix items' - ) - ), - Pe.createElement(B, { expanded: w, onClick: z }), - Pe.createElement(V, { schema: { prefixItems: o } }), - Pe.createElement( - 'ul', - { - className: Hn()('json-schema-2020-12-keyword__children', { - 'json-schema-2020-12-keyword__children--collapsed': !w - }) - }, - w && - Pe.createElement( - Pe.Fragment, - null, - o.map((s, o) => - Pe.createElement( - 'li', - { key: `#${o}`, className: 'json-schema-2020-12-property' }, - Pe.createElement($, { name: `#${o} ${i.getTitle(s)}`, schema: s }) - ) - ) - ) - ) - ) - ) - : null; - }, - keywords_Items = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'items')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Items' - ); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--items' }, - Pe.createElement(i, { name: u, schema: s.items }) - ); - }, - keywords_Contains = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'contains')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Contains' - ); - return Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--contains' }, - Pe.createElement(i, { name: u, schema: s.contains }) - ); - }, - keywords_Properties_Properties = ({ schema: s }) => { - const o = useFn(), - i = s?.properties || {}, - u = Array.isArray(s?.required) ? s.required : [], - _ = useComponent('JSONSchema'); - return 0 === Object.keys(i).length - ? null - : Pe.createElement( - 'div', - { - className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--properties' - }, - Pe.createElement( - 'ul', - null, - Object.entries(i).map(([i, w]) => { - const x = u.includes(i), - C = o.getDependentRequired(i, s); - return Pe.createElement( - 'li', - { - key: i, - className: Hn()('json-schema-2020-12-property', { - 'json-schema-2020-12-property--required': x - }) - }, - Pe.createElement(_, { name: i, schema: w, dependentRequired: C }) - ); - }) - ) - ); - }, - PatternProperties_PatternProperties = ({ schema: s }) => { - const o = s?.patternProperties || {}, - i = useComponent('JSONSchema'); - return 0 === Object.keys(o).length - ? null - : Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--patternProperties' - }, - Pe.createElement( - 'ul', - null, - Object.entries(o).map(([s, o]) => - Pe.createElement( - 'li', - { key: s, className: 'json-schema-2020-12-property' }, - Pe.createElement(i, { name: s, schema: o }) - ) - ) - ) - ); - }, - keywords_AdditionalProperties = ({ schema: s }) => { - const o = useFn(), - { additionalProperties: i } = s, - u = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'additionalProperties')) return null; - const _ = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Additional properties' - ); - return Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--additionalProperties' - }, - !0 === i - ? Pe.createElement( - Pe.Fragment, - null, - _, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'allowed' - ) - ) - : !1 === i - ? Pe.createElement( - Pe.Fragment, - null, - _, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - 'forbidden' - ) - ) - : Pe.createElement(u, { name: _, schema: i }) - ); - }, - keywords_PropertyNames = ({ schema: s }) => { - const o = useFn(), - { propertyNames: i } = s, - u = useComponent('JSONSchema'), - _ = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Property names' - ); - return o.hasKeyword(s, 'propertyNames') - ? Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--propertyNames' - }, - Pe.createElement(u, { name: _, schema: i }) - ) - : null; - }, - keywords_UnevaluatedItems = ({ schema: s }) => { - const o = useFn(), - { unevaluatedItems: i } = s, - u = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'unevaluatedItems')) return null; - const _ = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Unevaluated items' - ); - return Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedItems' - }, - Pe.createElement(u, { name: _, schema: i }) - ); - }, - keywords_UnevaluatedProperties = ({ schema: s }) => { - const o = useFn(), - { unevaluatedProperties: i } = s, - u = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'unevaluatedProperties')) return null; - const _ = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Unevaluated properties' - ); - return Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--unevaluatedProperties' - }, - Pe.createElement(u, { name: _, schema: i }) - ); - }, - keywords_Type = ({ schema: s, isCircular: o = !1 }) => { - const i = useFn().getType(s), - u = o ? ' [circular]' : ''; - return Pe.createElement( - 'strong', - { - className: 'json-schema-2020-12__attribute json-schema-2020-12__attribute--primary' - }, - `${i}${u}` - ); - }, - Enum_Enum = ({ schema: s }) => { - const o = useFn(); - return Array.isArray(s?.enum) - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--enum' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Allowed values' - ), - Pe.createElement( - 'ul', - null, - s.enum.map((s) => { - const i = o.stringify(s); - return Pe.createElement( - 'li', - { key: i }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' - }, - i - ) - ); - }) - ) - ) - : null; - }, - keywords_Const = ({ schema: s }) => { - const o = useFn(); - return o.hasKeyword(s, 'const') - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--const' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Const' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' - }, - o.stringify(s.const) - ) - ) - : null; - }, - Constraint = ({ constraint: s }) => - Pe.createElement( - 'span', - { - className: `json-schema-2020-12__constraint json-schema-2020-12__constraint--${s.scope}` - }, - s.value - ), - aI = Pe.memo(Constraint), - DependentRequired_DependentRequired = ({ dependentRequired: s }) => - 0 === s.length - ? null - : Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--dependentRequired' - }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Required when defined' - ), - Pe.createElement( - 'ul', - null, - s.map((s) => - Pe.createElement( - 'li', - { key: s }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--warning' - }, - s - ) - ) - ) - ) - ), - keywords_ContentSchema = ({ schema: s }) => { - const o = useFn(), - i = useComponent('JSONSchema'); - if (!o.hasKeyword(s, 'contentSchema')) return null; - const u = Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Content schema' - ); - return Pe.createElement( - 'div', - { - className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--contentSchema' - }, - Pe.createElement(i, { name: u, schema: s.contentSchema }) - ); - }, - Title_Title = ({ title: s = '', schema: o }) => { - const i = useFn(), - u = s || i.getTitle(o); - return u - ? Pe.createElement('div', { className: 'json-schema-2020-12__title' }, u) - : null; - }, - keywords_Description_Description = ({ schema: s }) => - s?.description - ? Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-keyword json-schema-2020-12-keyword--description' - }, - Pe.createElement( - 'div', - { - className: - 'json-schema-2020-12-core-keyword__value json-schema-2020-12-core-keyword__value--secondary' - }, - s.description - ) - ) - : null, - keywords_Default = ({ schema: s }) => { - const o = useFn(); - return o.hasKeyword(s, 'default') - ? Pe.createElement( - 'div', - { className: 'json-schema-2020-12-keyword json-schema-2020-12-keyword--default' }, - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__name json-schema-2020-12-keyword__name--primary' - }, - 'Default' - ), - Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12-keyword__value json-schema-2020-12-keyword__value--const' - }, - o.stringify(s.default) - ) - ) - : null; - }, - keywords_Deprecated = ({ schema: s }) => - !0 !== s?.deprecated - ? null - : Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--warning' - }, - 'deprecated' - ), - keywords_ReadOnly = ({ schema: s }) => - !0 !== s?.readOnly - ? null - : Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' - }, - 'read-only' - ), - keywords_WriteOnly = ({ schema: s }) => - !0 !== s?.writeOnly - ? null - : Pe.createElement( - 'span', - { - className: - 'json-schema-2020-12__attribute json-schema-2020-12__attribute--muted' - }, - 'write-only' - ), - Accordion_Accordion = ({ expanded: s = !1, children: o, onChange: i }) => { - const u = useComponent('ChevronRightIcon'), - _ = (0, Pe.useCallback)( - (o) => { - i(o, !s); - }, - [s, i] - ); - return Pe.createElement( - 'button', - { type: 'button', className: 'json-schema-2020-12-accordion', onClick: _ }, - Pe.createElement('div', { className: 'json-schema-2020-12-accordion__children' }, o), - Pe.createElement( - 'span', - { - className: Hn()('json-schema-2020-12-accordion__icon', { - 'json-schema-2020-12-accordion__icon--expanded': s, - 'json-schema-2020-12-accordion__icon--collapsed': !s - }) - }, - Pe.createElement(u, null) - ) - ); - }, - ExpandDeepButton_ExpandDeepButton = ({ expanded: s, onClick: o }) => { - const i = (0, Pe.useCallback)( - (i) => { - o(i, !s); - }, - [s, o] - ); - return Pe.createElement( - 'button', - { type: 'button', className: 'json-schema-2020-12-expand-deep-button', onClick: i }, - s ? 'Collapse all' : 'Expand all' - ); - }, - icons_ChevronRight = () => - Pe.createElement( - 'svg', - { - xmlns: 'http://www.w3.org/2000/svg', - width: '24', - height: '24', - viewBox: '0 0 24 24' - }, - Pe.createElement('path', { d: 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' }) - ), - fn_upperFirst = (s) => - 'string' == typeof s ? `${s.charAt(0).toUpperCase()}${s.slice(1)}` : s, - getTitle = (s, { lookup: o = 'extended' } = {}) => { - const i = useFn(); - if (null != s?.title) return i.upperFirst(String(s.title)); - if ('extended' === o) { - if (null != s?.$anchor) return i.upperFirst(String(s.$anchor)); - if (null != s?.$id) return String(s.$id); - } - return ''; - }, - getType = (s, o = new WeakSet()) => { - const i = useFn(); - if (null == s) return 'any'; - if (i.isBooleanJSONSchema(s)) return s ? 'any' : 'never'; - if ('object' != typeof s) return 'any'; - if (o.has(s)) return 'any'; - o.add(s); - const { type: u, prefixItems: _, items: w } = s, - getArrayType = () => { - if (Array.isArray(_)) { - const s = _.map((s) => getType(s, o)), - i = w ? getType(w, o) : 'any'; - return `array<[${s.join(', ')}], ${i}>`; - } - if (w) { - return `array<${getType(w, o)}>`; - } - return 'array'; - }; - if (s.not && 'any' === getType(s.not)) return 'never'; - const handleCombiningKeywords = (i, u) => { - if (Array.isArray(s[i])) { - return `(${s[i].map((s) => getType(s, o)).join(u)})`; - } - return null; - }, - x = [ - Array.isArray(u) - ? u.map((s) => ('array' === s ? getArrayType() : s)).join(' | ') - : 'array' === u - ? getArrayType() - : [ - 'null', - 'boolean', - 'object', - 'array', - 'number', - 'integer', - 'string' - ].includes(u) - ? u - : (() => { - if ( - Object.hasOwn(s, 'prefixItems') || - Object.hasOwn(s, 'items') || - Object.hasOwn(s, 'contains') - ) - return getArrayType(); - if ( - Object.hasOwn(s, 'properties') || - Object.hasOwn(s, 'additionalProperties') || - Object.hasOwn(s, 'patternProperties') - ) - return 'object'; - if (['int32', 'int64'].includes(s.format)) return 'integer'; - if (['float', 'double'].includes(s.format)) return 'number'; - if ( - Object.hasOwn(s, 'minimum') || - Object.hasOwn(s, 'maximum') || - Object.hasOwn(s, 'exclusiveMinimum') || - Object.hasOwn(s, 'exclusiveMaximum') || - Object.hasOwn(s, 'multipleOf') - ) - return 'number | integer'; - if ( - Object.hasOwn(s, 'pattern') || - Object.hasOwn(s, 'format') || - Object.hasOwn(s, 'minLength') || - Object.hasOwn(s, 'maxLength') - ) - return 'string'; - if (void 0 !== s.const) { - if (null === s.const) return 'null'; - if ('boolean' == typeof s.const) return 'boolean'; - if ('number' == typeof s.const) - return Number.isInteger(s.const) ? 'integer' : 'number'; - if ('string' == typeof s.const) return 'string'; - if (Array.isArray(s.const)) return 'array'; - if ('object' == typeof s.const) return 'object'; - } - return null; - })(), - handleCombiningKeywords('oneOf', ' | '), - handleCombiningKeywords('anyOf', ' | '), - handleCombiningKeywords('allOf', ' & ') - ] - .filter(Boolean) - .join(' | '); - return o.delete(s), x || 'any'; - }, - isBooleanJSONSchema = (s) => 'boolean' == typeof s, - hasKeyword = (s, o) => null !== s && 'object' == typeof s && Object.hasOwn(s, o), - isExpandable = (s) => { - const o = useFn(); - return ( - s?.$schema || - s?.$vocabulary || - s?.$id || - s?.$anchor || - s?.$dynamicAnchor || - s?.$ref || - s?.$dynamicRef || - s?.$defs || - s?.$comment || - s?.allOf || - s?.anyOf || - s?.oneOf || - o.hasKeyword(s, 'not') || - o.hasKeyword(s, 'if') || - o.hasKeyword(s, 'then') || - o.hasKeyword(s, 'else') || - s?.dependentSchemas || - s?.prefixItems || - o.hasKeyword(s, 'items') || - o.hasKeyword(s, 'contains') || - s?.properties || - s?.patternProperties || - o.hasKeyword(s, 'additionalProperties') || - o.hasKeyword(s, 'propertyNames') || - o.hasKeyword(s, 'unevaluatedItems') || - o.hasKeyword(s, 'unevaluatedProperties') || - s?.description || - s?.enum || - o.hasKeyword(s, 'const') || - o.hasKeyword(s, 'contentSchema') || - o.hasKeyword(s, 'default') - ); - }, - fn_stringify = (s) => - null === s || ['number', 'bigint', 'boolean'].includes(typeof s) - ? String(s) - : Array.isArray(s) - ? `[${s.map(fn_stringify).join(', ')}]` - : JSON.stringify(s), - stringifyConstraintRange = (s, o, i) => { - const u = 'number' == typeof o, - _ = 'number' == typeof i; - return u && _ - ? o === i - ? `${o} ${s}` - : `[${o}, ${i}] ${s}` - : u - ? `>= ${o} ${s}` - : _ - ? `<= ${i} ${s}` - : null; - }, - stringifyConstraints = (s) => { - const o = [], - i = ((s) => { - if ('number' != typeof s?.multipleOf) return null; - if (s.multipleOf <= 0) return null; - if (1 === s.multipleOf) return null; - const { multipleOf: o } = s; - if (Number.isInteger(o)) return `multiple of ${o}`; - const i = 10 ** o.toString().split('.')[1].length; - return `multiple of ${o * i}/${i}`; - })(s); - null !== i && o.push({ scope: 'number', value: i }); - const u = ((s) => { - const o = s?.minimum, - i = s?.maximum, - u = s?.exclusiveMinimum, - _ = s?.exclusiveMaximum, - w = 'number' == typeof o, - x = 'number' == typeof i, - C = 'number' == typeof u, - j = 'number' == typeof _, - L = C && (!w || o < u), - B = j && (!x || i > _); - if ((w || C) && (x || j)) - return `${L ? '(' : '['}${L ? u : o}, ${B ? _ : i}${B ? ')' : ']'}`; - if (w || C) return `${L ? '>' : '≥'} ${L ? u : o}`; - if (x || j) return `${B ? '<' : '≤'} ${B ? _ : i}`; - return null; - })(s); - null !== u && o.push({ scope: 'number', value: u }), - s?.format && o.push({ scope: 'string', value: s.format }); - const _ = stringifyConstraintRange('characters', s?.minLength, s?.maxLength); - null !== _ && o.push({ scope: 'string', value: _ }), - s?.pattern && o.push({ scope: 'string', value: `matches ${s?.pattern}` }), - s?.contentMediaType && - o.push({ scope: 'string', value: `media type: ${s.contentMediaType}` }), - s?.contentEncoding && - o.push({ scope: 'string', value: `encoding: ${s.contentEncoding}` }); - const w = stringifyConstraintRange( - s?.hasUniqueItems ? 'unique items' : 'items', - s?.minItems, - s?.maxItems - ); - null !== w && o.push({ scope: 'array', value: w }); - const x = stringifyConstraintRange('contained items', s?.minContains, s?.maxContains); - null !== x && o.push({ scope: 'array', value: x }); - const C = stringifyConstraintRange('properties', s?.minProperties, s?.maxProperties); - return null !== C && o.push({ scope: 'object', value: C }), o; - }, - getDependentRequired = (s, o) => - o?.dependentRequired - ? Array.from( - Object.entries(o.dependentRequired).reduce( - (o, [i, u]) => (Array.isArray(u) && u.includes(s) ? (o.add(i), o) : o), - new Set() - ) - ) - : [], - withJSONSchemaContext = (s, o = {}) => { - const i = { - components: { - JSONSchema: iI, - Keyword$schema: keywords_$schema, - Keyword$vocabulary: $vocabulary_$vocabulary, - Keyword$id: keywords_$id, - Keyword$anchor: keywords_$anchor, - Keyword$dynamicAnchor: keywords_$dynamicAnchor, - Keyword$ref: keywords_$ref, - Keyword$dynamicRef: keywords_$dynamicRef, - Keyword$defs: keywords_$defs, - Keyword$comment: keywords_$comment, - KeywordAllOf: keywords_AllOf, - KeywordAnyOf: keywords_AnyOf, - KeywordOneOf: keywords_OneOf, - KeywordNot: keywords_Not, - KeywordIf: keywords_If, - KeywordThen: keywords_Then, - KeywordElse: keywords_Else, - KeywordDependentSchemas: keywords_DependentSchemas, - KeywordPrefixItems: keywords_PrefixItems, - KeywordItems: keywords_Items, - KeywordContains: keywords_Contains, - KeywordProperties: keywords_Properties_Properties, - KeywordPatternProperties: PatternProperties_PatternProperties, - KeywordAdditionalProperties: keywords_AdditionalProperties, - KeywordPropertyNames: keywords_PropertyNames, - KeywordUnevaluatedItems: keywords_UnevaluatedItems, - KeywordUnevaluatedProperties: keywords_UnevaluatedProperties, - KeywordType: keywords_Type, - KeywordEnum: Enum_Enum, - KeywordConst: keywords_Const, - KeywordConstraint: aI, - KeywordDependentRequired: DependentRequired_DependentRequired, - KeywordContentSchema: keywords_ContentSchema, - KeywordTitle: Title_Title, - KeywordDescription: keywords_Description_Description, - KeywordDefault: keywords_Default, - KeywordDeprecated: keywords_Deprecated, - KeywordReadOnly: keywords_ReadOnly, - KeywordWriteOnly: keywords_WriteOnly, - Accordion: Accordion_Accordion, - ExpandDeepButton: ExpandDeepButton_ExpandDeepButton, - ChevronRightIcon: icons_ChevronRight, - ...o.components - }, - config: { - default$schema: 'https://json-schema.org/draft/2020-12/schema', - defaultExpandedLevels: 0, - ...o.config - }, - fn: { - upperFirst: fn_upperFirst, - getTitle, - getType, - isBooleanJSONSchema, - hasKeyword, - isExpandable, - stringify: fn_stringify, - stringifyConstraints, - getDependentRequired, - ...o.fn - } - }, - HOC = (o) => Pe.createElement(tI.Provider, { value: i }, Pe.createElement(s, o)); - return ( - (HOC.contexts = { JSONSchemaContext: tI }), (HOC.displayName = s.displayName), HOC - ); - }, - json_schema_2020_12 = () => ({ - components: { - JSONSchema202012: iI, - JSONSchema202012Keyword$schema: keywords_$schema, - JSONSchema202012Keyword$vocabulary: $vocabulary_$vocabulary, - JSONSchema202012Keyword$id: keywords_$id, - JSONSchema202012Keyword$anchor: keywords_$anchor, - JSONSchema202012Keyword$dynamicAnchor: keywords_$dynamicAnchor, - JSONSchema202012Keyword$ref: keywords_$ref, - JSONSchema202012Keyword$dynamicRef: keywords_$dynamicRef, - JSONSchema202012Keyword$defs: keywords_$defs, - JSONSchema202012Keyword$comment: keywords_$comment, - JSONSchema202012KeywordAllOf: keywords_AllOf, - JSONSchema202012KeywordAnyOf: keywords_AnyOf, - JSONSchema202012KeywordOneOf: keywords_OneOf, - JSONSchema202012KeywordNot: keywords_Not, - JSONSchema202012KeywordIf: keywords_If, - JSONSchema202012KeywordThen: keywords_Then, - JSONSchema202012KeywordElse: keywords_Else, - JSONSchema202012KeywordDependentSchemas: keywords_DependentSchemas, - JSONSchema202012KeywordPrefixItems: keywords_PrefixItems, - JSONSchema202012KeywordItems: keywords_Items, - JSONSchema202012KeywordContains: keywords_Contains, - JSONSchema202012KeywordProperties: keywords_Properties_Properties, - JSONSchema202012KeywordPatternProperties: PatternProperties_PatternProperties, - JSONSchema202012KeywordAdditionalProperties: keywords_AdditionalProperties, - JSONSchema202012KeywordPropertyNames: keywords_PropertyNames, - JSONSchema202012KeywordUnevaluatedItems: keywords_UnevaluatedItems, - JSONSchema202012KeywordUnevaluatedProperties: keywords_UnevaluatedProperties, - JSONSchema202012KeywordType: keywords_Type, - JSONSchema202012KeywordEnum: Enum_Enum, - JSONSchema202012KeywordConst: keywords_Const, - JSONSchema202012KeywordConstraint: aI, - JSONSchema202012KeywordDependentRequired: DependentRequired_DependentRequired, - JSONSchema202012KeywordContentSchema: keywords_ContentSchema, - JSONSchema202012KeywordTitle: Title_Title, - JSONSchema202012KeywordDescription: keywords_Description_Description, - JSONSchema202012KeywordDefault: keywords_Default, - JSONSchema202012KeywordDeprecated: keywords_Deprecated, - JSONSchema202012KeywordReadOnly: keywords_ReadOnly, - JSONSchema202012KeywordWriteOnly: keywords_WriteOnly, - JSONSchema202012Accordion: Accordion_Accordion, - JSONSchema202012ExpandDeepButton: ExpandDeepButton_ExpandDeepButton, - JSONSchema202012ChevronRightIcon: icons_ChevronRight, - withJSONSchema202012Context: withJSONSchemaContext, - JSONSchema202012DeepExpansionContext: () => nI - }, - fn: { - upperFirst: fn_upperFirst, - jsonSchema202012: { - isExpandable, - hasKeyword, - useFn, - useConfig, - useComponent, - useIsExpandedDeeply - } - } - }); - var lI = __webpack_require__(11331), - cI = __webpack_require__.n(lI); - const array = (s, { sample: o }) => - ((s, o = {}) => { - const { minItems: i, maxItems: u, uniqueItems: _ } = o, - { contains: w, minContains: x, maxContains: C } = o; - let j = [...s]; - if (null != w && 'object' == typeof w) { - if (Number.isInteger(x) && x > 1) { - const s = j.at(0); - for (let o = 1; o < x; o += 1) j.unshift(s); - } - Number.isInteger(C); - } - if ( - (Number.isInteger(u) && u > 0 && (j = s.slice(0, u)), Number.isInteger(i) && i > 0) - ) - for (let s = 0; j.length < i; s += 1) j.push(j[s % j.length]); - return !0 === _ && (j = Array.from(new Set(j))), j; - })(o, s), - object = () => { - throw new Error('Not implemented'); - }, - bytes = (s) => St()(s), - random_pick = (s) => s.at(0), - predicates_isBooleanJSONSchema = (s) => 'boolean' == typeof s, - isJSONSchemaObject = (s) => cI()(s), - isJSONSchema = (s) => predicates_isBooleanJSONSchema(s) || isJSONSchemaObject(s); - const uI = class Registry { - data = {}; - register(s, o) { - this.data[s] = o; - } - unregister(s) { - void 0 === s ? (this.data = {}) : delete this.data[s]; - } - get(s) { - return this.data[s]; - } - }, - int32 = () => (2 ** 30) >>> 0, - int64 = () => 2 ** 53 - 1, - generators_float = () => 0.1, - generators_double = () => 0.1, - email = () => 'user@example.com', - idn_email = () => '실례@example.com', - hostname = () => 'example.com', - idn_hostname = () => '실례.com', - ipv4 = () => '198.51.100.42', - ipv6 = () => '2001:0db8:5b96:0000:0000:426f:8e17:642a', - uri = () => 'https://example.com/', - uri_reference = () => 'path/index.html', - iri = () => 'https://실례.com/', - iri_reference = () => 'path/실례.html', - uuid = () => '3fa85f64-5717-4562-b3fc-2c963f66afa6', - uri_template = () => 'https://example.com/dictionary/{term:1}/{term}', - json_pointer = () => '/a/b/c', - relative_json_pointer = () => '1/0', - date_time = () => new Date().toISOString(), - date = () => new Date().toISOString().substring(0, 10), - time = () => new Date().toISOString().substring(11), - duration = () => 'P3D', - generators_password = () => '********', - regex = () => '^[a-z]+$'; - const pI = new (class FormatRegistry extends uI { - #t = { - int32, - int64, - float: generators_float, - double: generators_double, - email, - 'idn-email': idn_email, - hostname, - 'idn-hostname': idn_hostname, - ipv4, - ipv6, - uri, - 'uri-reference': uri_reference, - iri, - 'iri-reference': iri_reference, - uuid, - 'uri-template': uri_template, - 'json-pointer': json_pointer, - 'relative-json-pointer': relative_json_pointer, - 'date-time': date_time, - date, - time, - duration, - password: generators_password, - regex - }; - data = { ...this.#t }; - get defaults() { - return { ...this.#t }; - } - })(), - formatAPI = (s, o) => - 'function' == typeof o ? pI.register(s, o) : null === o ? pI.unregister(s) : pI.get(s); - formatAPI.getDefaults = () => pI.defaults; - const hI = formatAPI; - var dI = __webpack_require__(48287).Buffer; - const _7bit = (s) => dI.from(s).toString('ascii'); - var fI = __webpack_require__(48287).Buffer; - const _8bit = (s) => fI.from(s).toString('utf8'); - var mI = __webpack_require__(48287).Buffer; - const encoders_binary = (s) => mI.from(s).toString('binary'), - quoted_printable = (s) => { - let o = ''; - for (let i = 0; i < s.length; i++) { - const u = s.charCodeAt(i); - if (61 === u) o += '=3D'; - else if ((u >= 33 && u <= 60) || (u >= 62 && u <= 126) || 9 === u || 32 === u) - o += s.charAt(i); - else if (13 === u || 10 === u) o += '\r\n'; - else if (u > 126) { - const u = unescape(encodeURIComponent(s.charAt(i))); - for (let s = 0; s < u.length; s++) - o += '=' + ('0' + u.charCodeAt(s).toString(16)).slice(-2).toUpperCase(); - } else o += '=' + ('0' + u.toString(16)).slice(-2).toUpperCase(); - } - return o; - }; - var gI = __webpack_require__(48287).Buffer; - const base16 = (s) => gI.from(s).toString('hex'); - var yI = __webpack_require__(48287).Buffer; - const base32 = (s) => { - const o = yI.from(s).toString('utf8'), - i = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; - let u = 0, - _ = '', - w = 0, - x = 0; - for (let s = 0; s < o.length; s++) - for (w = (w << 8) | o.charCodeAt(s), x += 8; x >= 5; ) - (_ += i.charAt((w >>> (x - 5)) & 31)), (x -= 5); - x > 0 && ((_ += i.charAt((w << (5 - x)) & 31)), (u = (8 - ((8 * o.length) % 5)) % 5)); - for (let s = 0; s < u; s++) _ += '='; - return _; - }; - var vI = __webpack_require__(48287).Buffer; - const base64 = (s) => vI.from(s).toString('base64'); - var bI = __webpack_require__(48287).Buffer; - const base64url = (s) => bI.from(s).toString('base64url'); - const _I = new (class EncoderRegistry extends uI { - #t = { - '7bit': _7bit, - '8bit': _8bit, - binary: encoders_binary, - 'quoted-printable': quoted_printable, - base16, - base32, - base64, - base64url - }; - data = { ...this.#t }; - get defaults() { - return { ...this.#t }; - } - })(), - encoderAPI = (s, o) => - 'function' == typeof o ? _I.register(s, o) : null === o ? _I.unregister(s) : _I.get(s); - encoderAPI.getDefaults = () => _I.defaults; - const EI = encoderAPI, - wI = { - 'text/plain': () => 'string', - 'text/css': () => '.selector { border: 1px solid red }', - 'text/csv': () => 'value1,value2,value3', - 'text/html': () => '

    content

    ', - 'text/calendar': () => 'BEGIN:VCALENDAR', - 'text/javascript': () => "console.dir('Hello world!');", - 'text/xml': () => 'John Doe', - 'text/*': () => 'string' - }, - SI = { 'image/*': () => bytes(25).toString('binary') }, - xI = { 'audio/*': () => bytes(25).toString('binary') }, - kI = { 'video/*': () => bytes(25).toString('binary') }, - CI = { - 'application/json': () => '{"key":"value"}', - 'application/ld+json': () => '{"name": "John Doe"}', - 'application/x-httpd-php': () => "Hello World!

    '; ?>", - 'application/rtf': () => String.raw`{\rtf1\adeflang1025\ansi\ansicpg1252\uc1`, - 'application/x-sh': () => 'echo "Hello World!"', - 'application/xhtml+xml': () => '

    content

    ', - 'application/*': () => bytes(25).toString('binary') - }; - const OI = new (class MediaTypeRegistry extends uI { - #t = { ...wI, ...SI, ...xI, ...kI, ...CI }; - data = { ...this.#t }; - get defaults() { - return { ...this.#t }; - } - })(), - mediaTypeAPI = (s, o) => { - if ('function' == typeof o) return OI.register(s, o); - if (null === o) return OI.unregister(s); - const i = s.split(';').at(0), - u = `${i.split('/').at(0)}/*`; - return OI.get(s) || OI.get(i) || OI.get(u); - }; - mediaTypeAPI.getDefaults = () => OI.defaults; - const AI = mediaTypeAPI, - applyStringConstraints = (s, o = {}) => { - const { maxLength: i, minLength: u } = o; - let _ = s; - if ( - (Number.isInteger(i) && i > 0 && (_ = _.slice(0, i)), Number.isInteger(u) && u > 0) - ) { - let s = 0; - for (; _.length < u; ) _ += _[s++ % _.length]; - } - return _; - }, - types_string = (s, { sample: o } = {}) => { - const { contentEncoding: i, contentMediaType: u, contentSchema: _ } = s, - { pattern: w, format: x } = s, - C = EI(i) || Mx(); - let j; - return ( - (j = - 'string' == typeof w - ? applyStringConstraints( - ((s) => { - try { - return new (us())(s).gen(); - } catch { - return 'string'; - } - })(w), - s - ) - : 'string' == typeof x - ? ((s) => { - const { format: o } = s, - i = hI(o); - return 'function' == typeof i ? i(s) : 'string'; - })(s) - : isJSONSchema(_) && 'string' == typeof u && void 0 !== o - ? Array.isArray(o) || 'object' == typeof o - ? JSON.stringify(o) - : applyStringConstraints(String(o), s) - : 'string' == typeof u - ? ((s) => { - const { contentMediaType: o } = s, - i = AI(o); - return 'function' == typeof i ? i(s) : 'string'; - })(s) - : applyStringConstraints('string', s)), - C(j) - ); - }, - applyNumberConstraints = (s, o = {}) => { - const { minimum: i, maximum: u, exclusiveMinimum: _, exclusiveMaximum: w } = o, - { multipleOf: x } = o, - C = Number.isInteger(s) ? 1 : Number.EPSILON; - let j = 'number' == typeof i ? i : null, - L = 'number' == typeof u ? u : null, - B = s; - if ( - ('number' == typeof _ && (j = null !== j ? Math.max(j, _ + C) : _ + C), - 'number' == typeof w && (L = null !== L ? Math.min(L, w - C) : w - C), - (B = (j > L && s) || j || L || B), - 'number' == typeof x && x > 0) - ) { - const s = B % x; - B = 0 === s ? B : B + x - s; - } - return B; - }, - types_number = (s) => { - const { format: o } = s; - let i; - return ( - (i = - 'string' == typeof o - ? ((s) => { - const { format: o } = s, - i = hI(o); - return 'function' == typeof i ? i(s) : 0; - })(s) - : 0), - applyNumberConstraints(i, s) - ); - }, - types_integer = (s) => { - const { format: o } = s; - let i; - return ( - (i = - 'string' == typeof o - ? ((s) => { - const { format: o } = s, - i = hI(o); - if ('function' == typeof i) return i(s); - switch (o) { - case 'int32': - return int32(); - case 'int64': - return int64(); - } - return 0; - })(s) - : 0), - applyNumberConstraints(i, s) - ); - }, - types_boolean = (s) => 'boolean' != typeof s.default || s.default, - jI = new Proxy( - { - array, - object, - string: types_string, - number: types_number, - integer: types_integer, - boolean: types_boolean, - null: () => null - }, - { - get: (s, o) => - 'string' == typeof o && Object.hasOwn(s, o) ? s[o] : () => `Unknown Type: ${o}` - } - ), - II = ['array', 'object', 'number', 'integer', 'string', 'boolean', 'null'], - hasExample = (s) => { - if (!isJSONSchemaObject(s)) return !1; - const { examples: o, example: i, default: u } = s; - return !!(Array.isArray(o) && o.length >= 1) || void 0 !== u || void 0 !== i; - }, - extractExample = (s) => { - if (!isJSONSchemaObject(s)) return null; - const { examples: o, example: i, default: u } = s; - return Array.isArray(o) && o.length >= 1 - ? o.at(0) - : void 0 !== u - ? u - : void 0 !== i - ? i - : void 0; - }, - PI = { - array: [ - 'items', - 'prefixItems', - 'contains', - 'maxContains', - 'minContains', - 'maxItems', - 'minItems', - 'uniqueItems', - 'unevaluatedItems' - ], - object: [ - 'properties', - 'additionalProperties', - 'patternProperties', - 'propertyNames', - 'minProperties', - 'maxProperties', - 'required', - 'dependentSchemas', - 'dependentRequired', - 'unevaluatedProperties' - ], - string: [ - 'pattern', - 'format', - 'minLength', - 'maxLength', - 'contentEncoding', - 'contentMediaType', - 'contentSchema' - ], - integer: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'] - }; - PI.number = PI.integer; - const MI = 'string', - inferTypeFromValue = (s) => - void 0 === s - ? null - : null === s - ? 'null' - : Array.isArray(s) - ? 'array' - : Number.isInteger(s) - ? 'integer' - : typeof s, - foldType = (s) => { - if (Array.isArray(s) && s.length >= 1) { - if (s.includes('array')) return 'array'; - if (s.includes('object')) return 'object'; - { - const o = random_pick(s); - if (II.includes(o)) return o; - } - } - return II.includes(s) ? s : null; - }, - inferType = (s, o = new WeakSet()) => { - if (!isJSONSchemaObject(s)) return MI; - if (o.has(s)) return MI; - o.add(s); - let { type: i, const: u } = s; - if (((i = foldType(i)), 'string' != typeof i)) { - const o = Object.keys(PI); - e: for (let u = 0; u < o.length; u += 1) { - const _ = o[u], - w = PI[_]; - for (let o = 0; o < w.length; o += 1) { - const u = w[o]; - if (Object.hasOwn(s, u)) { - i = _; - break e; - } - } - } - } - if ('string' != typeof i && void 0 !== u) { - const s = inferTypeFromValue(u); - i = 'string' == typeof s ? s : i; - } - if ('string' != typeof i) { - const combineTypes = (i) => { - if (Array.isArray(s[i])) { - const u = s[i].map((s) => inferType(s, o)); - return foldType(u); - } - return null; - }, - u = combineTypes('allOf'), - _ = combineTypes('anyOf'), - w = combineTypes('oneOf'), - x = s.not ? inferType(s.not, o) : null; - (u || _ || w || x) && (i = foldType([u, _, w, x].filter(Boolean))); - } - if ('string' != typeof i && hasExample(s)) { - const o = extractExample(s), - u = inferTypeFromValue(o); - i = 'string' == typeof u ? u : i; - } - return o.delete(s), i || MI; - }, - type_getType = (s) => inferType(s), - typeCast = (s) => - predicates_isBooleanJSONSchema(s) - ? ((s) => (!1 === s ? { not: {} } : {}))(s) - : isJSONSchemaObject(s) - ? s - : {}, - merge_merge = (s, o, i = {}) => { - if (predicates_isBooleanJSONSchema(s) && !0 === s) return !0; - if (predicates_isBooleanJSONSchema(s) && !1 === s) return !1; - if (predicates_isBooleanJSONSchema(o) && !0 === o) return !0; - if (predicates_isBooleanJSONSchema(o) && !1 === o) return !1; - if (!isJSONSchema(s)) return o; - if (!isJSONSchema(o)) return s; - const u = { ...o, ...s }; - if (o.type && s.type && Array.isArray(o.type) && 'string' == typeof o.type) { - const i = normalizeArray(o.type).concat(s.type); - u.type = Array.from(new Set(i)); - } - if ( - (Array.isArray(o.required) && - Array.isArray(s.required) && - (u.required = [...new Set([...s.required, ...o.required])]), - o.properties && s.properties) - ) { - const _ = new Set([...Object.keys(o.properties), ...Object.keys(s.properties)]); - u.properties = {}; - for (const w of _) { - const _ = o.properties[w] || {}, - x = s.properties[w] || {}; - (_.readOnly && !i.includeReadOnly) || (_.writeOnly && !i.includeWriteOnly) - ? (u.required = (u.required || []).filter((s) => s !== w)) - : (u.properties[w] = merge_merge(x, _, i)); - } - } - return ( - isJSONSchema(o.items) && - isJSONSchema(s.items) && - (u.items = merge_merge(s.items, o.items, i)), - isJSONSchema(o.contains) && - isJSONSchema(s.contains) && - (u.contains = merge_merge(s.contains, o.contains, i)), - isJSONSchema(o.contentSchema) && - isJSONSchema(s.contentSchema) && - (u.contentSchema = merge_merge(s.contentSchema, o.contentSchema, i)), - u - ); - }, - TI = merge_merge, - main_sampleFromSchemaGeneric = (s, o = {}, i = void 0, u = !1) => { - if (null == s && void 0 === i) return; - 'function' == typeof s?.toJS && (s = s.toJS()), (s = typeCast(s)); - let _ = void 0 !== i || hasExample(s); - const w = !_ && Array.isArray(s.oneOf) && s.oneOf.length > 0, - x = !_ && Array.isArray(s.anyOf) && s.anyOf.length > 0; - if (!_ && (w || x)) { - const i = typeCast(random_pick(w ? s.oneOf : s.anyOf)); - !(s = TI(s, i, o)).xml && i.xml && (s.xml = i.xml), - hasExample(s) && hasExample(i) && (_ = !0); - } - const C = {}; - let { xml: j, properties: L, additionalProperties: B, items: $, contains: V } = s || {}, - U = type_getType(s), - { includeReadOnly: z, includeWriteOnly: Y } = o; - j = j || {}; - let Z, - { name: ee, prefix: ie, namespace: ae } = j, - le = {}; - if ( - (Object.hasOwn(s, 'type') || (s.type = U), - u && ((ee = ee || 'notagname'), (Z = (ie ? `${ie}:` : '') + ee), ae)) - ) { - C[ie ? `xmlns:${ie}` : 'xmlns'] = ae; - } - u && (le[Z] = []); - const ce = objectify(L); - let pe, - de = 0; - const hasExceededMaxProperties = () => - Number.isInteger(s.maxProperties) && s.maxProperties > 0 && de >= s.maxProperties, - canAddProperty = (o) => - !(Number.isInteger(s.maxProperties) && s.maxProperties > 0) || - (!hasExceededMaxProperties() && - (!((o) => - !Array.isArray(s.required) || - 0 === s.required.length || - !s.required.includes(o))(o) || - s.maxProperties - - de - - (() => { - if (!Array.isArray(s.required) || 0 === s.required.length) return 0; - let o = 0; - return ( - u - ? s.required.forEach((s) => (o += void 0 === le[s] ? 0 : 1)) - : s.required.forEach((s) => { - o += void 0 === le[Z]?.find((o) => void 0 !== o[s]) ? 0 : 1; - }), - s.required.length - o - ); - })() > - 0)); - if ( - ((pe = u - ? (i, _ = void 0) => { - if (s && ce[i]) { - if (((ce[i].xml = ce[i].xml || {}), ce[i].xml.attribute)) { - const s = Array.isArray(ce[i].enum) ? random_pick(ce[i].enum) : void 0; - if (hasExample(ce[i])) C[ce[i].xml.name || i] = extractExample(ce[i]); - else if (void 0 !== s) C[ce[i].xml.name || i] = s; - else { - const s = typeCast(ce[i]), - o = type_getType(s), - u = ce[i].xml.name || i; - C[u] = jI[o](s); - } - return; - } - ce[i].xml.name = ce[i].xml.name || i; - } else ce[i] || !1 === B || (ce[i] = { xml: { name: i } }); - let w = main_sampleFromSchemaGeneric(ce[i], o, _, u); - canAddProperty(i) && - (de++, Array.isArray(w) ? (le[Z] = le[Z].concat(w)) : le[Z].push(w)); - } - : (i, _) => { - if (canAddProperty(i)) { - if ( - cI()(s.discriminator?.mapping) && - s.discriminator.propertyName === i && - 'string' == typeof s.$$ref - ) { - for (const o in s.discriminator.mapping) - if (-1 !== s.$$ref.search(s.discriminator.mapping[o])) { - le[i] = o; - break; - } - } else le[i] = main_sampleFromSchemaGeneric(ce[i], o, _, u); - de++; - } - }), - _) - ) { - let _; - if (((_ = void 0 !== i ? i : extractExample(s)), !u)) { - if ('number' == typeof _ && 'string' === U) return `${_}`; - if ('string' != typeof _ || 'string' === U) return _; - try { - return JSON.parse(_); - } catch { - return _; - } - } - if ('array' === U) { - if (!Array.isArray(_)) { - if ('string' == typeof _) return _; - _ = [_]; - } - let i = []; - return ( - isJSONSchemaObject($) && - (($.xml = $.xml || j || {}), - ($.xml.name = $.xml.name || j.name), - (i = _.map((s) => main_sampleFromSchemaGeneric($, o, s, u)))), - isJSONSchemaObject(V) && - ((V.xml = V.xml || j || {}), - (V.xml.name = V.xml.name || j.name), - (i = [main_sampleFromSchemaGeneric(V, o, void 0, u), ...i])), - (i = jI.array(s, { sample: i })), - j.wrapped ? ((le[Z] = i), hs()(C) || le[Z].push({ _attr: C })) : (le = i), - le - ); - } - if ('object' === U) { - if ('string' == typeof _) return _; - for (const s in _) - Object.hasOwn(_, s) && - ((ce[s]?.readOnly && !z) || - (ce[s]?.writeOnly && !Y) || - (ce[s]?.xml?.attribute ? (C[ce[s].xml.name || s] = _[s]) : pe(s, _[s]))); - return hs()(C) || le[Z].push({ _attr: C }), le; - } - return (le[Z] = hs()(C) ? _ : [{ _attr: C }, _]), le; - } - if ('array' === U) { - let i = []; - if (isJSONSchemaObject(V)) - if ( - (u && ((V.xml = V.xml || s.xml || {}), (V.xml.name = V.xml.name || j.name)), - Array.isArray(V.anyOf)) - ) { - const { anyOf: s, ..._ } = $; - i.push( - ...V.anyOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) - ); - } else if (Array.isArray(V.oneOf)) { - const { oneOf: s, ..._ } = $; - i.push( - ...V.oneOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) - ); - } else { - if (!(!u || (u && j.wrapped))) - return main_sampleFromSchemaGeneric(V, o, void 0, u); - i.push(main_sampleFromSchemaGeneric(V, o, void 0, u)); - } - if (isJSONSchemaObject($)) - if ( - (u && (($.xml = $.xml || s.xml || {}), ($.xml.name = $.xml.name || j.name)), - Array.isArray($.anyOf)) - ) { - const { anyOf: s, ..._ } = $; - i.push( - ...$.anyOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) - ); - } else if (Array.isArray($.oneOf)) { - const { oneOf: s, ..._ } = $; - i.push( - ...$.oneOf.map((s) => main_sampleFromSchemaGeneric(TI(s, _, o), o, void 0, u)) - ); - } else { - if (!(!u || (u && j.wrapped))) - return main_sampleFromSchemaGeneric($, o, void 0, u); - i.push(main_sampleFromSchemaGeneric($, o, void 0, u)); - } - return ( - (i = jI.array(s, { sample: i })), - u && j.wrapped ? ((le[Z] = i), hs()(C) || le[Z].push({ _attr: C }), le) : i - ); - } - if ('object' === U) { - for (let s in ce) - Object.hasOwn(ce, s) && - (ce[s]?.deprecated || - (ce[s]?.readOnly && !z) || - (ce[s]?.writeOnly && !Y) || - pe(s)); - if ((u && C && le[Z].push({ _attr: C }), hasExceededMaxProperties())) return le; - if (predicates_isBooleanJSONSchema(B) && B) - u - ? le[Z].push({ additionalProp: 'Anything can be here' }) - : (le.additionalProp1 = {}), - de++; - else if (isJSONSchemaObject(B)) { - const i = B, - _ = main_sampleFromSchemaGeneric(i, o, void 0, u); - if (u && 'string' == typeof i?.xml?.name && 'notagname' !== i?.xml?.name) - le[Z].push(_); - else { - const o = - Number.isInteger(s.minProperties) && s.minProperties > 0 && de < s.minProperties - ? s.minProperties - de - : 3; - for (let s = 1; s <= o; s++) { - if (hasExceededMaxProperties()) return le; - if (u) { - const o = {}; - (o['additionalProp' + s] = _.notagname), le[Z].push(o); - } else le['additionalProp' + s] = _; - de++; - } - } - } - return le; - } - let fe; - if (void 0 !== s.const) fe = s.const; - else if (s && Array.isArray(s.enum)) fe = random_pick(normalizeArray(s.enum)); - else { - const i = isJSONSchemaObject(s.contentSchema) - ? main_sampleFromSchemaGeneric(s.contentSchema, o, void 0, u) - : void 0; - fe = jI[U](s, { sample: i }); - } - return u ? ((le[Z] = hs()(C) ? fe : [{ _attr: C }, fe]), le) : fe; - }, - main_createXMLExample = (s, o, i) => { - const u = main_sampleFromSchemaGeneric(s, o, i, !0); - if (u) return 'string' == typeof u ? u : ls()(u, { declaration: !0, indent: '\t' }); - }, - main_sampleFromSchema = (s, o, i) => main_sampleFromSchemaGeneric(s, o, i, !1), - main_resolver = (s, o, i) => [s, JSON.stringify(o), JSON.stringify(i)], - NI = utils_memoizeN(main_createXMLExample, main_resolver), - RI = utils_memoizeN(main_sampleFromSchema, main_resolver); - const DI = new (class OptionRegistry extends uI { - #t = {}; - data = { ...this.#t }; - get defaults() { - return { ...this.#t }; - } - })(), - api_optionAPI = (s, o) => (void 0 !== o && DI.register(s, o), DI.get(s)), - LI = [{ when: /json/, shouldStringifyTypes: ['string'] }], - BI = ['object'], - fn_get_json_sample_schema = (s) => (o, i, u, _) => { - const { fn: w } = s(), - x = w.jsonSchema202012.memoizedSampleFromSchema(o, i, _), - C = typeof x, - j = LI.reduce((s, o) => (o.when.test(u) ? [...s, ...o.shouldStringifyTypes] : s), BI); - return mt()(j, (s) => s === C) ? JSON.stringify(x, null, 2) : x; - }, - fn_get_yaml_sample_schema = (s) => (o, i, u, _) => { - const { fn: w } = s(), - x = w.jsonSchema202012.getJsonSampleSchema(o, i, u, _); - let C; - try { - (C = mn.dump(mn.load(x), { lineWidth: -1 }, { schema: nn })), - '\n' === C[C.length - 1] && (C = C.slice(0, C.length - 1)); - } catch (s) { - return console.error(s), 'error: could not generate yaml example'; - } - return C.replace(/\t/g, ' '); - }, - fn_get_xml_sample_schema = (s) => (o, i, u) => { - const { fn: _ } = s(); - if ((o && !o.xml && (o.xml = {}), o && !o.xml.name)) { - if (!o.$$ref && (o.type || o.items || o.properties || o.additionalProperties)) - return '\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e'; - if (o.$$ref) { - let s = o.$$ref.match(/\S*\/(\S+)$/); - o.xml.name = s[1]; - } - } - return _.jsonSchema202012.memoizedCreateXMLExample(o, i, u); - }, - fn_get_sample_schema = - (s) => - (o, i = '', u = {}, _ = void 0) => { - const { fn: w } = s(); - return ( - 'function' == typeof o?.toJS && (o = o.toJS()), - 'function' == typeof _?.toJS && (_ = _.toJS()), - /xml/.test(i) - ? w.jsonSchema202012.getXmlSampleSchema(o, u, _) - : /(yaml|yml)/.test(i) - ? w.jsonSchema202012.getYamlSampleSchema(o, u, i, _) - : w.jsonSchema202012.getJsonSampleSchema(o, u, i, _) - ); - }, - json_schema_2020_12_samples = ({ getSystem: s }) => { - const o = fn_get_json_sample_schema(s), - i = fn_get_yaml_sample_schema(s), - u = fn_get_xml_sample_schema(s), - _ = fn_get_sample_schema(s); - return { - fn: { - jsonSchema202012: { - sampleFromSchema: main_sampleFromSchema, - sampleFromSchemaGeneric: main_sampleFromSchemaGeneric, - sampleOptionAPI: api_optionAPI, - sampleEncoderAPI: EI, - sampleFormatAPI: hI, - sampleMediaTypeAPI: AI, - createXMLExample: main_createXMLExample, - memoizedSampleFromSchema: RI, - memoizedCreateXMLExample: NI, - getJsonSampleSchema: o, - getYamlSampleSchema: i, - getXmlSampleSchema: u, - getSampleSchema: _, - mergeJsonSchema: TI - } - } - }; - }; - function PresetApis() { - return [base, oas3, json_schema_2020_12, json_schema_2020_12_samples, oas31]; - } - const inline_plugin = (s) => () => ({ fn: s.fn, components: s.components }), - factorization_system = (s) => { - const o = We()( - { - layout: { layout: s.layout, filter: s.filter }, - spec: { spec: '', url: s.url }, - requestSnippets: s.requestSnippets - }, - s.initialState - ); - if (s.initialState) - for (const [i, u] of Object.entries(s.initialState)) void 0 === u && delete o[i]; - return { system: { configs: s.configs }, plugins: s.presets, state: o }; - }, - sources_query = () => (s) => { - const o = s.queryConfigEnabled - ? (() => { - const s = new URLSearchParams(at.location.search); - return Object.fromEntries(s); - })() - : {}; - return Object.entries(o).reduce( - (s, [o, i]) => ( - 'config' === o - ? (s.configUrl = i) - : 'urls.primaryName' === o - ? (s[o] = i) - : (s = ao()(s, o, i)), - s - ), - {} - ); - }, - sources_url = - ({ url: s, system: o }) => - async (i) => { - if (!s) return {}; - if ('function' != typeof o.configsActions?.getConfigByUrl) return {}; - const u = (() => { - const s = {}; - return ( - (s.promise = new Promise((o, i) => { - (s.resolve = o), (s.reject = i); - })), - s - ); - })(); - return ( - o.configsActions.getConfigByUrl( - { - url: s, - loadRemoteConfig: !0, - requestInterceptor: i.requestInterceptor, - responseInterceptor: i.responseInterceptor - }, - (s) => { - u.resolve(s); - } - ), - u.promise - ); - }, - runtime = () => () => { - const s = {}; - return ( - globalThis.location && - (s.oauth2RedirectUrl = `${globalThis.location.protocol}//${globalThis.location.host}${globalThis.location.pathname.substring(0, globalThis.location.pathname.lastIndexOf('/'))}/oauth2-redirect.html`), - s - ); - }, - FI = Object.freeze({ - dom_id: null, - domNode: null, - spec: {}, - url: '', - urls: null, - configUrl: null, - layout: 'BaseLayout', - docExpansion: 'list', - maxDisplayedTags: -1, - filter: !1, - validatorUrl: 'https://validator.swagger.io/validator', - oauth2RedirectUrl: void 0, - persistAuthorization: !1, - configs: {}, - displayOperationId: !1, - displayRequestDuration: !1, - deepLinking: !1, - tryItOutEnabled: !1, - requestInterceptor: (s) => ((s.curlOptions = []), s), - responseInterceptor: (s) => s, - showMutatedRequest: !0, - defaultModelRendering: 'example', - defaultModelExpandDepth: 1, - defaultModelsExpandDepth: 1, - showExtensions: !1, - showCommonExtensions: !1, - withCredentials: !1, - requestSnippetsEnabled: !1, - requestSnippets: { - generators: { - curl_bash: { title: 'cURL (bash)', syntax: 'bash' }, - curl_powershell: { title: 'cURL (PowerShell)', syntax: 'powershell' }, - curl_cmd: { title: 'cURL (CMD)', syntax: 'bash' } - }, - defaultExpanded: !0, - languages: null - }, - supportedSubmitMethods: [ - 'get', - 'put', - 'post', - 'delete', - 'options', - 'head', - 'patch', - 'trace' - ], - queryConfigEnabled: !1, - presets: [PresetApis], - plugins: [], - initialState: {}, - fn: {}, - components: {}, - syntaxHighlight: { activated: !0, theme: 'agate' }, - operationsSorter: null, - tagsSorter: null, - onComplete: null, - modelPropertyMacro: null, - parameterMacro: null - }); - var qI = __webpack_require__(61448), - $I = __webpack_require__.n(qI), - VI = __webpack_require__(77731), - UI = __webpack_require__.n(VI); - const type_casters_array = (s, o = []) => (Array.isArray(s) ? s : o), - type_casters_boolean = (s, o = !1) => - !0 === s || - 'true' === s || - 1 === s || - '1' === s || - (!1 !== s && 'false' !== s && 0 !== s && '0' !== s && o), - dom_node = (s) => (null === s || 'null' === s ? null : s), - type_casters_filter = (s) => { - const o = String(s); - return type_casters_boolean(s, o); - }, - type_casters_function = (s, o) => ('function' == typeof s ? s : o), - nullable_array = (s) => (Array.isArray(s) ? s : null), - nullable_function = (s) => ('function' == typeof s ? s : null), - nullable_string = (s) => (null === s || 'null' === s ? null : String(s)), - type_casters_number = (s, o = -1) => { - const i = parseInt(s, 10); - return Number.isNaN(i) ? o : i; - }, - type_casters_object = (s, o = {}) => (cI()(s) ? s : o), - sorter = (s) => ('function' == typeof s || 'string' == typeof s ? s : null), - type_casters_string = (s) => String(s), - syntax_highlight = (s, o) => - cI()(s) ? s : !1 === s || 'false' === s || 0 === s || '0' === s ? { activated: !1 } : o, - undefined_string = (s) => (void 0 === s || 'undefined' === s ? void 0 : String(s)), - zI = { - components: { typeCaster: type_casters_object }, - configs: { typeCaster: type_casters_object }, - configUrl: { typeCaster: nullable_string }, - deepLinking: { typeCaster: type_casters_boolean, defaultValue: FI.deepLinking }, - defaultModelExpandDepth: { - typeCaster: type_casters_number, - defaultValue: FI.defaultModelExpandDepth - }, - defaultModelRendering: { typeCaster: type_casters_string }, - defaultModelsExpandDepth: { - typeCaster: type_casters_number, - defaultValue: FI.defaultModelsExpandDepth - }, - displayOperationId: { - typeCaster: type_casters_boolean, - defaultValue: FI.displayOperationId - }, - displayRequestDuration: { - typeCaster: type_casters_boolean, - defaultValue: FI.displayRequestDuration - }, - docExpansion: { typeCaster: type_casters_string }, - dom_id: { typeCaster: nullable_string }, - domNode: { typeCaster: dom_node }, - filter: { typeCaster: type_casters_filter }, - fn: { typeCaster: type_casters_object }, - initialState: { typeCaster: type_casters_object }, - layout: { typeCaster: type_casters_string }, - maxDisplayedTags: { - typeCaster: type_casters_number, - defaultValue: FI.maxDisplayedTags - }, - modelPropertyMacro: { typeCaster: nullable_function }, - oauth2RedirectUrl: { typeCaster: undefined_string }, - onComplete: { typeCaster: nullable_function }, - operationsSorter: { typeCaster: sorter }, - paramaterMacro: { typeCaster: nullable_function }, - persistAuthorization: { - typeCaster: type_casters_boolean, - defaultValue: FI.persistAuthorization - }, - plugins: { typeCaster: type_casters_array, defaultValue: FI.plugins }, - presets: { typeCaster: type_casters_array, defaultValue: FI.presets }, - requestInterceptor: { - typeCaster: type_casters_function, - defaultValue: FI.requestInterceptor - }, - requestSnippets: { typeCaster: type_casters_object, defaultValue: FI.requestSnippets }, - requestSnippetsEnabled: { - typeCaster: type_casters_boolean, - defaultValue: FI.requestSnippetsEnabled - }, - responseInterceptor: { - typeCaster: type_casters_function, - defaultValue: FI.responseInterceptor - }, - showCommonExtensions: { - typeCaster: type_casters_boolean, - defaultValue: FI.showCommonExtensions - }, - showExtensions: { typeCaster: type_casters_boolean, defaultValue: FI.showExtensions }, - showMutatedRequest: { - typeCaster: type_casters_boolean, - defaultValue: FI.showMutatedRequest - }, - spec: { typeCaster: type_casters_object, defaultValue: FI.spec }, - supportedSubmitMethods: { - typeCaster: type_casters_array, - defaultValue: FI.supportedSubmitMethods - }, - syntaxHighlight: { typeCaster: syntax_highlight, defaultValue: FI.syntaxHighlight }, - 'syntaxHighlight.activated': { - typeCaster: type_casters_boolean, - defaultValue: FI.syntaxHighlight.activated - }, - 'syntaxHighlight.theme': { typeCaster: type_casters_string }, - tagsSorter: { typeCaster: sorter }, - tryItOutEnabled: { typeCaster: type_casters_boolean, defaultValue: FI.tryItOutEnabled }, - url: { typeCaster: type_casters_string }, - urls: { typeCaster: nullable_array }, - 'urls.primaryName': { typeCaster: type_casters_string }, - validatorUrl: { typeCaster: nullable_string }, - withCredentials: { typeCaster: type_casters_boolean, defaultValue: FI.withCredentials } - }, - type_cast = (s) => - Object.entries(zI).reduce( - (s, [o, { typeCaster: i, defaultValue: u }]) => { - if ($I()(s, o)) { - const _ = i(jn()(s, o), u); - s = UI()(o, _, s); - } - return s; - }, - { ...s } - ), - config_merge = (s, ...o) => { - let i = Symbol.for('domNode'), - u = Symbol.for('primaryName'); - const _ = []; - for (const s of o) { - const o = { ...s }; - Object.hasOwn(o, 'domNode') && ((i = o.domNode), delete o.domNode), - Object.hasOwn(o, 'urls.primaryName') - ? ((u = o['urls.primaryName']), delete o['urls.primaryName']) - : Array.isArray(o.urls) && - Object.hasOwn(o.urls, 'primaryName') && - ((u = o.urls.primaryName), delete o.urls.primaryName), - _.push(o); - } - const w = We()(s, ..._); - return ( - i !== Symbol.for('domNode') && (w.domNode = i), - u !== Symbol.for('primaryName') && Array.isArray(w.urls) && (w.urls.primaryName = u), - type_cast(w) - ); - }; - function SwaggerUI(s) { - const o = sources_query()(s), - i = runtime()(), - u = SwaggerUI.config.merge({}, SwaggerUI.config.defaults, i, s, o), - _ = factorization_system(u), - w = inline_plugin(u), - x = new Store(_); - x.register([u.plugins, w]); - const C = x.getSystem(), - persistConfigs = (s) => { - x.setConfigs(s), C.configsActions.loaded(); - }, - updateSpec = (s) => { - !o.url && 'object' == typeof s.spec && Object.keys(s.spec).length > 0 - ? (C.specActions.updateUrl(''), - C.specActions.updateLoadingStatus('success'), - C.specActions.updateSpec(JSON.stringify(s.spec))) - : 'function' == typeof C.specActions.download && - s.url && - !s.urls && - (C.specActions.updateUrl(s.url), C.specActions.download(s.url)); - }, - render = (s) => { - if (s.domNode) C.render(s.domNode, 'App'); - else if (s.dom_id) { - const o = document.querySelector(s.dom_id); - C.render(o, 'App'); - } else - null === s.dom_id || - null === s.domNode || - console.error('Skipped rendering: no `dom_id` or `domNode` was specified'); - }; - return u.configUrl - ? ((async () => { - const { configUrl: s } = u, - i = await sources_url({ url: s, system: C })(u), - _ = SwaggerUI.config.merge({}, u, i, o); - persistConfigs(_), null !== i && updateSpec(_), render(_); - })(), - C) - : (persistConfigs(u), updateSpec(u), render(u), C); - } - (SwaggerUI.System = Store), - (SwaggerUI.config = { - defaults: FI, - merge: config_merge, - typeCast: type_cast, - typeCastMappings: zI - }), - (SwaggerUI.presets = { base, apis: PresetApis }), - (SwaggerUI.plugins = { - Auth: auth, - Configs: configsPlugin, - DeepLining: deep_linking, - Err: err, - Filter: filter, - Icons: icons, - JSONSchema5: json_schema_5, - JSONSchema5Samples: json_schema_5_samples, - JSONSchema202012: json_schema_2020_12, - JSONSchema202012Samples: json_schema_2020_12_samples, - Layout: plugins_layout, - Logs: logs, - OpenAPI30: oas3, - OpenAPI31: oas3, - OnComplete: on_complete, - RequestSnippets: plugins_request_snippets, - Spec: plugins_spec, - SwaggerClient: swagger_client, - Util: util, - View: view, - ViewLegacy: view_legacy, - DownloadUrl: downloadUrlPlugin, - SyntaxHighlighting: syntax_highlighting, - Versions: versions, - SafeRender: safe_render - }); - const WI = SwaggerUI; - })(), - (_ = _.default) - ); - })() -); +!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={67526(s,o){"use strict";o.byteLength=function byteLength(s){var o=getLens(s),i=o[0],a=o[1];return 3*(i+a)/4-a},o.toByteArray=function toByteArray(s){var o,i,_=getLens(s),w=_[0],x=_[1],C=new u(function _byteLength(s,o,i){return 3*(o+i)/4-i}(0,w,x)),j=0,L=x>0?w-4:w;for(i=0;i>16&255,C[j++]=o>>8&255,C[j++]=255&o;2===x&&(o=a[s.charCodeAt(i)]<<2|a[s.charCodeAt(i+1)]>>4,C[j++]=255&o);1===x&&(o=a[s.charCodeAt(i)]<<10|a[s.charCodeAt(i+1)]<<4|a[s.charCodeAt(i+2)]>>2,C[j++]=o>>8&255,C[j++]=255&o);return C},o.fromByteArray=function fromByteArray(s){for(var o,a=s.length,u=a%3,_=[],w=16383,x=0,C=a-u;xC?C:x+w));1===u?(o=s[a-1],_.push(i[o>>2]+i[o<<4&63]+"==")):2===u&&(o=(s[a-2]<<8)+s[a-1],_.push(i[o>>10]+i[o>>4&63]+i[o<<2&63]+"="));return _.join("")};for(var i=[],a=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",w=0;w<64;++w)i[w]=_[w],a[_.charCodeAt(w)]=w;function getLens(s){var o=s.length;if(o%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var i=s.indexOf("=");return-1===i&&(i=o),[i,i===o?0:4-i%4]}function encodeChunk(s,o,a){for(var u,_,w=[],x=o;x>18&63]+i[_>>12&63]+i[_>>6&63]+i[63&_]);return w.join("")}a["-".charCodeAt(0)]=62,a["_".charCodeAt(0)]=63},48287(s,o,i){"use strict";const a=i(67526),u=i(251),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;o.Buffer=Buffer,o.SlowBuffer=function SlowBuffer(s){+s!=s&&(s=0);return Buffer.alloc(+s)},o.INSPECT_MAX_BYTES=50;const w=2147483647;function createBuffer(s){if(s>w)throw new RangeError('The value "'+s+'" is invalid for option "size"');const o=new Uint8Array(s);return Object.setPrototypeOf(o,Buffer.prototype),o}function Buffer(s,o,i){if("number"==typeof s){if("string"==typeof o)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(s)}return from(s,o,i)}function from(s,o,i){if("string"==typeof s)return function fromString(s,o){"string"==typeof o&&""!==o||(o="utf8");if(!Buffer.isEncoding(o))throw new TypeError("Unknown encoding: "+o);const i=0|byteLength(s,o);let a=createBuffer(i);const u=a.write(s,o);u!==i&&(a=a.slice(0,u));return a}(s,o);if(ArrayBuffer.isView(s))return function fromArrayView(s){if(isInstance(s,Uint8Array)){const o=new Uint8Array(s);return fromArrayBuffer(o.buffer,o.byteOffset,o.byteLength)}return fromArrayLike(s)}(s);if(null==s)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s);if(isInstance(s,ArrayBuffer)||s&&isInstance(s.buffer,ArrayBuffer))return fromArrayBuffer(s,o,i);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(s,SharedArrayBuffer)||s&&isInstance(s.buffer,SharedArrayBuffer)))return fromArrayBuffer(s,o,i);if("number"==typeof s)throw new TypeError('The "value" argument must not be of type number. Received type number');const a=s.valueOf&&s.valueOf();if(null!=a&&a!==s)return Buffer.from(a,o,i);const u=function fromObject(s){if(Buffer.isBuffer(s)){const o=0|checked(s.length),i=createBuffer(o);return 0===i.length||s.copy(i,0,0,o),i}if(void 0!==s.length)return"number"!=typeof s.length||numberIsNaN(s.length)?createBuffer(0):fromArrayLike(s);if("Buffer"===s.type&&Array.isArray(s.data))return fromArrayLike(s.data)}(s);if(u)return u;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof s[Symbol.toPrimitive])return Buffer.from(s[Symbol.toPrimitive]("string"),o,i);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s)}function assertSize(s){if("number"!=typeof s)throw new TypeError('"size" argument must be of type number');if(s<0)throw new RangeError('The value "'+s+'" is invalid for option "size"')}function allocUnsafe(s){return assertSize(s),createBuffer(s<0?0:0|checked(s))}function fromArrayLike(s){const o=s.length<0?0:0|checked(s.length),i=createBuffer(o);for(let a=0;a=w)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+w.toString(16)+" bytes");return 0|s}function byteLength(s,o){if(Buffer.isBuffer(s))return s.length;if(ArrayBuffer.isView(s)||isInstance(s,ArrayBuffer))return s.byteLength;if("string"!=typeof s)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof s);const i=s.length,a=arguments.length>2&&!0===arguments[2];if(!a&&0===i)return 0;let u=!1;for(;;)switch(o){case"ascii":case"latin1":case"binary":return i;case"utf8":case"utf-8":return utf8ToBytes(s).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*i;case"hex":return i>>>1;case"base64":return base64ToBytes(s).length;default:if(u)return a?-1:utf8ToBytes(s).length;o=(""+o).toLowerCase(),u=!0}}function slowToString(s,o,i){let a=!1;if((void 0===o||o<0)&&(o=0),o>this.length)return"";if((void 0===i||i>this.length)&&(i=this.length),i<=0)return"";if((i>>>=0)<=(o>>>=0))return"";for(s||(s="utf8");;)switch(s){case"hex":return hexSlice(this,o,i);case"utf8":case"utf-8":return utf8Slice(this,o,i);case"ascii":return asciiSlice(this,o,i);case"latin1":case"binary":return latin1Slice(this,o,i);case"base64":return base64Slice(this,o,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,o,i);default:if(a)throw new TypeError("Unknown encoding: "+s);s=(s+"").toLowerCase(),a=!0}}function swap(s,o,i){const a=s[o];s[o]=s[i],s[i]=a}function bidirectionalIndexOf(s,o,i,a,u){if(0===s.length)return-1;if("string"==typeof i?(a=i,i=0):i>2147483647?i=2147483647:i<-2147483648&&(i=-2147483648),numberIsNaN(i=+i)&&(i=u?0:s.length-1),i<0&&(i=s.length+i),i>=s.length){if(u)return-1;i=s.length-1}else if(i<0){if(!u)return-1;i=0}if("string"==typeof o&&(o=Buffer.from(o,a)),Buffer.isBuffer(o))return 0===o.length?-1:arrayIndexOf(s,o,i,a,u);if("number"==typeof o)return o&=255,"function"==typeof Uint8Array.prototype.indexOf?u?Uint8Array.prototype.indexOf.call(s,o,i):Uint8Array.prototype.lastIndexOf.call(s,o,i):arrayIndexOf(s,[o],i,a,u);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(s,o,i,a,u){let _,w=1,x=s.length,C=o.length;if(void 0!==a&&("ucs2"===(a=String(a).toLowerCase())||"ucs-2"===a||"utf16le"===a||"utf-16le"===a)){if(s.length<2||o.length<2)return-1;w=2,x/=2,C/=2,i/=2}function read(s,o){return 1===w?s[o]:s.readUInt16BE(o*w)}if(u){let a=-1;for(_=i;_x&&(i=x-C),_=i;_>=0;_--){let i=!0;for(let a=0;au&&(a=u):a=u;const _=o.length;let w;for(a>_/2&&(a=_/2),w=0;w>8,u=i%256,_.push(u),_.push(a);return _}(o,s.length-i),s,i,a)}function base64Slice(s,o,i){return 0===o&&i===s.length?a.fromByteArray(s):a.fromByteArray(s.slice(o,i))}function utf8Slice(s,o,i){i=Math.min(s.length,i);const a=[];let u=o;for(;u239?4:o>223?3:o>191?2:1;if(u+w<=i){let i,a,x,C;switch(w){case 1:o<128&&(_=o);break;case 2:i=s[u+1],128==(192&i)&&(C=(31&o)<<6|63&i,C>127&&(_=C));break;case 3:i=s[u+1],a=s[u+2],128==(192&i)&&128==(192&a)&&(C=(15&o)<<12|(63&i)<<6|63&a,C>2047&&(C<55296||C>57343)&&(_=C));break;case 4:i=s[u+1],a=s[u+2],x=s[u+3],128==(192&i)&&128==(192&a)&&128==(192&x)&&(C=(15&o)<<18|(63&i)<<12|(63&a)<<6|63&x,C>65535&&C<1114112&&(_=C))}}null===_?(_=65533,w=1):_>65535&&(_-=65536,a.push(_>>>10&1023|55296),_=56320|1023&_),a.push(_),u+=w}return function decodeCodePointsArray(s){const o=s.length;if(o<=x)return String.fromCharCode.apply(String,s);let i="",a=0;for(;aa.length?(Buffer.isBuffer(o)||(o=Buffer.from(o)),o.copy(a,u)):Uint8Array.prototype.set.call(a,o,u);else{if(!Buffer.isBuffer(o))throw new TypeError('"list" argument must be an Array of Buffers');o.copy(a,u)}u+=o.length}return a},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const s=this.length;if(s%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let o=0;oi&&(s+=" ... "),""},_&&(Buffer.prototype[_]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(s,o,i,a,u){if(isInstance(s,Uint8Array)&&(s=Buffer.from(s,s.offset,s.byteLength)),!Buffer.isBuffer(s))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof s);if(void 0===o&&(o=0),void 0===i&&(i=s?s.length:0),void 0===a&&(a=0),void 0===u&&(u=this.length),o<0||i>s.length||a<0||u>this.length)throw new RangeError("out of range index");if(a>=u&&o>=i)return 0;if(a>=u)return-1;if(o>=i)return 1;if(this===s)return 0;let _=(u>>>=0)-(a>>>=0),w=(i>>>=0)-(o>>>=0);const x=Math.min(_,w),C=this.slice(a,u),j=s.slice(o,i);for(let s=0;s>>=0,isFinite(i)?(i>>>=0,void 0===a&&(a="utf8")):(a=i,i=void 0)}const u=this.length-o;if((void 0===i||i>u)&&(i=u),s.length>0&&(i<0||o<0)||o>this.length)throw new RangeError("Attempt to write outside buffer bounds");a||(a="utf8");let _=!1;for(;;)switch(a){case"hex":return hexWrite(this,s,o,i);case"utf8":case"utf-8":return utf8Write(this,s,o,i);case"ascii":case"latin1":case"binary":return asciiWrite(this,s,o,i);case"base64":return base64Write(this,s,o,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,s,o,i);default:if(_)throw new TypeError("Unknown encoding: "+a);a=(""+a).toLowerCase(),_=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const x=4096;function asciiSlice(s,o,i){let a="";i=Math.min(s.length,i);for(let u=o;ua)&&(i=a);let u="";for(let a=o;ai)throw new RangeError("Trying to access beyond buffer length")}function checkInt(s,o,i,a,u,_){if(!Buffer.isBuffer(s))throw new TypeError('"buffer" argument must be a Buffer instance');if(o>u||o<_)throw new RangeError('"value" argument is out of bounds');if(i+a>s.length)throw new RangeError("Index out of range")}function wrtBigUInt64LE(s,o,i,a,u){checkIntBI(o,a,u,s,i,7);let _=Number(o&BigInt(4294967295));s[i++]=_,_>>=8,s[i++]=_,_>>=8,s[i++]=_,_>>=8,s[i++]=_;let w=Number(o>>BigInt(32)&BigInt(4294967295));return s[i++]=w,w>>=8,s[i++]=w,w>>=8,s[i++]=w,w>>=8,s[i++]=w,i}function wrtBigUInt64BE(s,o,i,a,u){checkIntBI(o,a,u,s,i,7);let _=Number(o&BigInt(4294967295));s[i+7]=_,_>>=8,s[i+6]=_,_>>=8,s[i+5]=_,_>>=8,s[i+4]=_;let w=Number(o>>BigInt(32)&BigInt(4294967295));return s[i+3]=w,w>>=8,s[i+2]=w,w>>=8,s[i+1]=w,w>>=8,s[i]=w,i+8}function checkIEEE754(s,o,i,a,u,_){if(i+a>s.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("Index out of range")}function writeFloat(s,o,i,a,_){return o=+o,i>>>=0,_||checkIEEE754(s,0,i,4),u.write(s,o,i,a,23,4),i+4}function writeDouble(s,o,i,a,_){return o=+o,i>>>=0,_||checkIEEE754(s,0,i,8),u.write(s,o,i,a,52,8),i+8}Buffer.prototype.slice=function slice(s,o){const i=this.length;(s=~~s)<0?(s+=i)<0&&(s=0):s>i&&(s=i),(o=void 0===o?i:~~o)<0?(o+=i)<0&&(o=0):o>i&&(o=i),o>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s],u=1,_=0;for(;++_>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s+--o],u=1;for(;o>0&&(u*=256);)a+=this[s+--o]*u;return a},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(s,o){return s>>>=0,o||checkOffset(s,1,this.length),this[s]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(s,o){return s>>>=0,o||checkOffset(s,2,this.length),this[s]|this[s+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(s,o){return s>>>=0,o||checkOffset(s,2,this.length),this[s]<<8|this[s+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),(this[s]|this[s+1]<<8|this[s+2]<<16)+16777216*this[s+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),16777216*this[s]+(this[s+1]<<16|this[s+2]<<8|this[s+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(s){validateNumber(s>>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=o+256*this[++s]+65536*this[++s]+this[++s]*2**24,u=this[++s]+256*this[++s]+65536*this[++s]+i*2**24;return BigInt(a)+(BigInt(u)<>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=o*2**24+65536*this[++s]+256*this[++s]+this[++s],u=this[++s]*2**24+65536*this[++s]+256*this[++s]+i;return(BigInt(a)<>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=this[s],u=1,_=0;for(;++_=u&&(a-=Math.pow(2,8*o)),a},Buffer.prototype.readIntBE=function readIntBE(s,o,i){s>>>=0,o>>>=0,i||checkOffset(s,o,this.length);let a=o,u=1,_=this[s+--a];for(;a>0&&(u*=256);)_+=this[s+--a]*u;return u*=128,_>=u&&(_-=Math.pow(2,8*o)),_},Buffer.prototype.readInt8=function readInt8(s,o){return s>>>=0,o||checkOffset(s,1,this.length),128&this[s]?-1*(255-this[s]+1):this[s]},Buffer.prototype.readInt16LE=function readInt16LE(s,o){s>>>=0,o||checkOffset(s,2,this.length);const i=this[s]|this[s+1]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt16BE=function readInt16BE(s,o){s>>>=0,o||checkOffset(s,2,this.length);const i=this[s+1]|this[s]<<8;return 32768&i?4294901760|i:i},Buffer.prototype.readInt32LE=function readInt32LE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),this[s]|this[s+1]<<8|this[s+2]<<16|this[s+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),this[s]<<24|this[s+1]<<16|this[s+2]<<8|this[s+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(s){validateNumber(s>>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=this[s+4]+256*this[s+5]+65536*this[s+6]+(i<<24);return(BigInt(a)<>>=0,"offset");const o=this[s],i=this[s+7];void 0!==o&&void 0!==i||boundsError(s,this.length-8);const a=(o<<24)+65536*this[++s]+256*this[++s]+this[++s];return(BigInt(a)<>>=0,o||checkOffset(s,4,this.length),u.read(this,s,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(s,o){return s>>>=0,o||checkOffset(s,4,this.length),u.read(this,s,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(s,o){return s>>>=0,o||checkOffset(s,8,this.length),u.read(this,s,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(s,o){return s>>>=0,o||checkOffset(s,8,this.length),u.read(this,s,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(s,o,i,a){if(s=+s,o>>>=0,i>>>=0,!a){checkInt(this,s,o,i,Math.pow(2,8*i)-1,0)}let u=1,_=0;for(this[o]=255&s;++_>>=0,i>>>=0,!a){checkInt(this,s,o,i,Math.pow(2,8*i)-1,0)}let u=i-1,_=1;for(this[o+u]=255&s;--u>=0&&(_*=256);)this[o+u]=s/_&255;return o+i},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,1,255,0),this[o]=255&s,o+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,65535,0),this[o]=255&s,this[o+1]=s>>>8,o+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,65535,0),this[o]=s>>>8,this[o+1]=255&s,o+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,4294967295,0),this[o+3]=s>>>24,this[o+2]=s>>>16,this[o+1]=s>>>8,this[o]=255&s,o+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,4294967295,0),this[o]=s>>>24,this[o+1]=s>>>16,this[o+2]=s>>>8,this[o+3]=255&s,o+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(s,o=0){return wrtBigUInt64LE(this,s,o,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(s,o=0){return wrtBigUInt64BE(this,s,o,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeIntLE=function writeIntLE(s,o,i,a){if(s=+s,o>>>=0,!a){const a=Math.pow(2,8*i-1);checkInt(this,s,o,i,a-1,-a)}let u=0,_=1,w=0;for(this[o]=255&s;++u>>=0,!a){const a=Math.pow(2,8*i-1);checkInt(this,s,o,i,a-1,-a)}let u=i-1,_=1,w=0;for(this[o+u]=255&s;--u>=0&&(_*=256);)s<0&&0===w&&0!==this[o+u+1]&&(w=1),this[o+u]=(s/_|0)-w&255;return o+i},Buffer.prototype.writeInt8=function writeInt8(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,1,127,-128),s<0&&(s=255+s+1),this[o]=255&s,o+1},Buffer.prototype.writeInt16LE=function writeInt16LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,32767,-32768),this[o]=255&s,this[o+1]=s>>>8,o+2},Buffer.prototype.writeInt16BE=function writeInt16BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,2,32767,-32768),this[o]=s>>>8,this[o+1]=255&s,o+2},Buffer.prototype.writeInt32LE=function writeInt32LE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,2147483647,-2147483648),this[o]=255&s,this[o+1]=s>>>8,this[o+2]=s>>>16,this[o+3]=s>>>24,o+4},Buffer.prototype.writeInt32BE=function writeInt32BE(s,o,i){return s=+s,o>>>=0,i||checkInt(this,s,o,4,2147483647,-2147483648),s<0&&(s=4294967295+s+1),this[o]=s>>>24,this[o+1]=s>>>16,this[o+2]=s>>>8,this[o+3]=255&s,o+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(s,o=0){return wrtBigUInt64LE(this,s,o,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(s,o=0){return wrtBigUInt64BE(this,s,o,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(s,o,i){return writeFloat(this,s,o,!0,i)},Buffer.prototype.writeFloatBE=function writeFloatBE(s,o,i){return writeFloat(this,s,o,!1,i)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(s,o,i){return writeDouble(this,s,o,!0,i)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(s,o,i){return writeDouble(this,s,o,!1,i)},Buffer.prototype.copy=function copy(s,o,i,a){if(!Buffer.isBuffer(s))throw new TypeError("argument should be a Buffer");if(i||(i=0),a||0===a||(a=this.length),o>=s.length&&(o=s.length),o||(o=0),a>0&&a=this.length)throw new RangeError("Index out of range");if(a<0)throw new RangeError("sourceEnd out of bounds");a>this.length&&(a=this.length),s.length-o>>=0,i=void 0===i?this.length:i>>>0,s||(s=0),"number"==typeof s)for(u=o;u=a+4;i-=3)o=`_${s.slice(i-3,i)}${o}`;return`${s.slice(0,i)}${o}`}function checkIntBI(s,o,i,a,u,_){if(s>i||s3?0===o||o===BigInt(0)?`>= 0${a} and < 2${a} ** ${8*(_+1)}${a}`:`>= -(2${a} ** ${8*(_+1)-1}${a}) and < 2 ** ${8*(_+1)-1}${a}`:`>= ${o}${a} and <= ${i}${a}`,new C.ERR_OUT_OF_RANGE("value",u,s)}!function checkBounds(s,o,i){validateNumber(o,"offset"),void 0!==s[o]&&void 0!==s[o+i]||boundsError(o,s.length-(i+1))}(a,u,_)}function validateNumber(s,o){if("number"!=typeof s)throw new C.ERR_INVALID_ARG_TYPE(o,"number",s)}function boundsError(s,o,i){if(Math.floor(s)!==s)throw validateNumber(s,i),new C.ERR_OUT_OF_RANGE(i||"offset","an integer",s);if(o<0)throw new C.ERR_BUFFER_OUT_OF_BOUNDS;throw new C.ERR_OUT_OF_RANGE(i||"offset",`>= ${i?1:0} and <= ${o}`,s)}E("ERR_BUFFER_OUT_OF_BOUNDS",(function(s){return s?`${s} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),E("ERR_INVALID_ARG_TYPE",(function(s,o){return`The "${s}" argument must be of type number. Received type ${typeof o}`}),TypeError),E("ERR_OUT_OF_RANGE",(function(s,o,i){let a=`The value of "${s}" is out of range.`,u=i;return Number.isInteger(i)&&Math.abs(i)>2**32?u=addNumericalSeparator(String(i)):"bigint"==typeof i&&(u=String(i),(i>BigInt(2)**BigInt(32)||i<-(BigInt(2)**BigInt(32)))&&(u=addNumericalSeparator(u)),u+="n"),a+=` It must be ${o}. Received ${u}`,a}),RangeError);const j=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(s,o){let i;o=o||1/0;const a=s.length;let u=null;const _=[];for(let w=0;w55295&&i<57344){if(!u){if(i>56319){(o-=3)>-1&&_.push(239,191,189);continue}if(w+1===a){(o-=3)>-1&&_.push(239,191,189);continue}u=i;continue}if(i<56320){(o-=3)>-1&&_.push(239,191,189),u=i;continue}i=65536+(u-55296<<10|i-56320)}else u&&(o-=3)>-1&&_.push(239,191,189);if(u=null,i<128){if((o-=1)<0)break;_.push(i)}else if(i<2048){if((o-=2)<0)break;_.push(i>>6|192,63&i|128)}else if(i<65536){if((o-=3)<0)break;_.push(i>>12|224,i>>6&63|128,63&i|128)}else{if(!(i<1114112))throw new Error("Invalid code point");if((o-=4)<0)break;_.push(i>>18|240,i>>12&63|128,i>>6&63|128,63&i|128)}}return _}function base64ToBytes(s){return a.toByteArray(function base64clean(s){if((s=(s=s.split("=")[0]).trim().replace(j,"")).length<2)return"";for(;s.length%4!=0;)s+="=";return s}(s))}function blitBuffer(s,o,i,a){let u;for(u=0;u=o.length||u>=s.length);++u)o[u+i]=s[u];return u}function isInstance(s,o){return s instanceof o||null!=s&&null!=s.constructor&&null!=s.constructor.name&&s.constructor.name===o.name}function numberIsNaN(s){return s!=s}const L=function(){const s="0123456789abcdef",o=new Array(256);for(let i=0;i<16;++i){const a=16*i;for(let u=0;u<16;++u)o[a+u]=s[i]+s[u]}return o}();function defineBigIntMethod(s){return"undefined"==typeof BigInt?BufferBigIntNotDefined:s}function BufferBigIntNotDefined(){throw new Error("BigInt not supported")}},13144(s,o,i){"use strict";var a=i(66743),u=i(11002),_=i(10076),w=i(47119);s.exports=w||a.call(_,u)},12205(s,o,i){"use strict";var a=i(66743),u=i(11002),_=i(13144);s.exports=function applyBind(){return _(a,u,arguments)}},11002(s){"use strict";s.exports=Function.prototype.apply},10076(s){"use strict";s.exports=Function.prototype.call},73126(s,o,i){"use strict";var a=i(66743),u=i(69675),_=i(10076),w=i(13144);s.exports=function callBindBasic(s){if(s.length<1||"function"!=typeof s[0])throw new u("a function is required");return w(a,_,s)}},47119(s){"use strict";s.exports="undefined"!=typeof Reflect&&Reflect&&Reflect.apply},10487(s,o,i){"use strict";var a=i(96897),u=i(30655),_=i(73126),w=i(12205);s.exports=function callBind(s){var o=_(arguments),i=s.length-(arguments.length-1);return a(o,1+(i>0?i:0),!0)},u?u(s.exports,"apply",{value:w}):s.exports.apply=w},36556(s,o,i){"use strict";var a=i(70453),u=i(73126),_=u([a("%String.prototype.indexOf%")]);s.exports=function callBoundIntrinsic(s,o){var i=a(s,!!o);return"function"==typeof i&&_(s,".prototype.")>-1?u([i]):i}},17965(s,o,i){"use strict";var a=i(16426),u={"text/plain":"Text","text/html":"Url",default:"Text"};s.exports=function copy(s,o){var i,_,w,x,C,j,L=!1;o||(o={}),i=o.debug||!1;try{if(w=a(),x=document.createRange(),C=document.getSelection(),(j=document.createElement("span")).textContent=s,j.ariaHidden="true",j.style.all="unset",j.style.position="fixed",j.style.top=0,j.style.clip="rect(0, 0, 0, 0)",j.style.whiteSpace="pre",j.style.webkitUserSelect="text",j.style.MozUserSelect="text",j.style.msUserSelect="text",j.style.userSelect="text",j.addEventListener("copy",(function(a){if(a.stopPropagation(),o.format)if(a.preventDefault(),void 0===a.clipboardData){i&&console.warn("unable to use e.clipboardData"),i&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var _=u[o.format]||u.default;window.clipboardData.setData(_,s)}else a.clipboardData.clearData(),a.clipboardData.setData(o.format,s);o.onCopy&&(a.preventDefault(),o.onCopy(a.clipboardData))})),document.body.appendChild(j),x.selectNodeContents(j),C.addRange(x),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");L=!0}catch(a){i&&console.error("unable to copy using execCommand: ",a),i&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(o.format||"text",s),o.onCopy&&o.onCopy(window.clipboardData),L=!0}catch(a){i&&console.error("unable to copy using clipboardData: ",a),i&&console.error("falling back to prompt"),_=function format(s){var o=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return s.replace(/#{\s*key\s*}/g,o)}("message"in o?o.message:"Copy to clipboard: #{key}, Enter"),window.prompt(_,s)}}finally{C&&("function"==typeof C.removeRange?C.removeRange(x):C.removeAllRanges()),j&&document.body.removeChild(j),w()}return L}},2205(s,o,i){var a;a=void 0!==i.g?i.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var o,i=String(s),a=i.length,u=-1,_="",w=i.charCodeAt(0);++u=1&&o<=31||127==o||0==u&&o>=48&&o<=57||1==u&&o>=48&&o<=57&&45==w?"\\"+o.toString(16)+" ":0==u&&1==a&&45==o||!(o>=128||45==o||95==o||o>=48&&o<=57||o>=65&&o<=90||o>=97&&o<=122)?"\\"+i.charAt(u):i.charAt(u):_+="�";return _};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(a)},81919(s,o,i){"use strict";var a=i(48287).Buffer;function isSpecificValue(s){return s instanceof a||s instanceof Date||s instanceof RegExp}function cloneSpecificValue(s){if(s instanceof a){var o=a.alloc?a.alloc(s.length):new a(s.length);return s.copy(o),o}if(s instanceof Date)return new Date(s.getTime());if(s instanceof RegExp)return new RegExp(s);throw new Error("Unexpected situation")}function deepCloneArray(s){var o=[];return s.forEach((function(s,i){"object"==typeof s&&null!==s?Array.isArray(s)?o[i]=deepCloneArray(s):isSpecificValue(s)?o[i]=cloneSpecificValue(s):o[i]=u({},s):o[i]=s})),o}function safeGetProperty(s,o){return"__proto__"===o?void 0:s[o]}var u=s.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var s,o,i=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(a){"object"!=typeof a||null===a||Array.isArray(a)||Object.keys(a).forEach((function(_){return o=safeGetProperty(i,_),(s=safeGetProperty(a,_))===i?void 0:"object"!=typeof s||null===s?void(i[_]=s):Array.isArray(s)?void(i[_]=deepCloneArray(s)):isSpecificValue(s)?void(i[_]=cloneSpecificValue(s)):"object"!=typeof o||null===o||Array.isArray(o)?void(i[_]=u({},s)):void(i[_]=u(o,s))}))})),i}},14744(s){"use strict";var o=function isMergeableObject(s){return function isNonNullObject(s){return!!s&&"object"==typeof s}(s)&&!function isSpecial(s){var o=Object.prototype.toString.call(s);return"[object RegExp]"===o||"[object Date]"===o||function isReactElement(s){return s.$$typeof===i}(s)}(s)};var i="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(s,o){return!1!==o.clone&&o.isMergeableObject(s)?deepmerge(function emptyTarget(s){return Array.isArray(s)?[]:{}}(s),s,o):s}function defaultArrayMerge(s,o,i){return s.concat(o).map((function(s){return cloneUnlessOtherwiseSpecified(s,i)}))}function getKeys(s){return Object.keys(s).concat(function getEnumerableOwnPropertySymbols(s){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s).filter((function(o){return Object.propertyIsEnumerable.call(s,o)})):[]}(s))}function propertyIsOnObject(s,o){try{return o in s}catch(s){return!1}}function mergeObject(s,o,i){var a={};return i.isMergeableObject(s)&&getKeys(s).forEach((function(o){a[o]=cloneUnlessOtherwiseSpecified(s[o],i)})),getKeys(o).forEach((function(u){(function propertyIsUnsafe(s,o){return propertyIsOnObject(s,o)&&!(Object.hasOwnProperty.call(s,o)&&Object.propertyIsEnumerable.call(s,o))})(s,u)||(propertyIsOnObject(s,u)&&i.isMergeableObject(o[u])?a[u]=function getMergeFunction(s,o){if(!o.customMerge)return deepmerge;var i=o.customMerge(s);return"function"==typeof i?i:deepmerge}(u,i)(s[u],o[u],i):a[u]=cloneUnlessOtherwiseSpecified(o[u],i))})),a}function deepmerge(s,i,a){(a=a||{}).arrayMerge=a.arrayMerge||defaultArrayMerge,a.isMergeableObject=a.isMergeableObject||o,a.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var u=Array.isArray(i);return u===Array.isArray(s)?u?a.arrayMerge(s,i,a):mergeObject(s,i,a):cloneUnlessOtherwiseSpecified(i,a)}deepmerge.all=function deepmergeAll(s,o){if(!Array.isArray(s))throw new Error("first argument should be an array");return s.reduce((function(s,i){return deepmerge(s,i,o)}),{})};var a=deepmerge;s.exports=a},30041(s,o,i){"use strict";var a=i(30655),u=i(58068),_=i(69675),w=i(75795);s.exports=function defineDataProperty(s,o,i){if(!s||"object"!=typeof s&&"function"!=typeof s)throw new _("`obj` must be an object or a function`");if("string"!=typeof o&&"symbol"!=typeof o)throw new _("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new _("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new _("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new _("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new _("`loose`, if provided, must be a boolean");var x=arguments.length>3?arguments[3]:null,C=arguments.length>4?arguments[4]:null,j=arguments.length>5?arguments[5]:null,L=arguments.length>6&&arguments[6],B=!!w&&w(s,o);if(a)a(s,o,{configurable:null===j&&B?B.configurable:!j,enumerable:null===x&&B?B.enumerable:!x,value:i,writable:null===C&&B?B.writable:!C});else{if(!L&&(x||C||j))throw new u("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");s[o]=i}}},78004(s){"use strict";class SubRange{constructor(s,o){this.low=s,this.high=o,this.length=1+o-s}overlaps(s){return!(this.highs.high)}touches(s){return!(this.high+1s.high)}add(s){return new SubRange(Math.min(this.low,s.low),Math.max(this.high,s.high))}subtract(s){return s.low<=this.low&&s.high>=this.high?[]:s.low>this.low&&s.highs+o.length),0)}add(s,o){var _add=s=>{for(var o=0;o{for(var o=0;o{for(var o=0;o{for(var i=o.low;i<=o.high;)s.push(i),i++;return s}),[])}subranges(){return this.ranges.map((s=>({low:s.low,high:s.high,length:1+s.high-s.low})))}}s.exports=DRange},7176(s,o,i){"use strict";var a,u=i(73126),_=i(75795);try{a=[].__proto__===Array.prototype}catch(s){if(!s||"object"!=typeof s||!("code"in s)||"ERR_PROTO_ACCESS"!==s.code)throw s}var w=!!a&&_&&_(Object.prototype,"__proto__"),x=Object,C=x.getPrototypeOf;s.exports=w&&"function"==typeof w.get?u([w.get]):"function"==typeof C&&function getDunder(s){return C(null==s?s:x(s))}},30655(s){"use strict";var o=Object.defineProperty||!1;if(o)try{o({},"a",{value:1})}catch(s){o=!1}s.exports=o},41237(s){"use strict";s.exports=EvalError},69383(s){"use strict";s.exports=Error},79290(s){"use strict";s.exports=RangeError},79538(s){"use strict";s.exports=ReferenceError},58068(s){"use strict";s.exports=SyntaxError},69675(s){"use strict";s.exports=TypeError},35345(s){"use strict";s.exports=URIError},79612(s){"use strict";s.exports=Object},37007(s){"use strict";var o,i="object"==typeof Reflect?Reflect:null,a=i&&"function"==typeof i.apply?i.apply:function ReflectApply(s,o,i){return Function.prototype.apply.call(s,o,i)};o=i&&"function"==typeof i.ownKeys?i.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s))}:function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s)};var u=Number.isNaN||function NumberIsNaN(s){return s!=s};function EventEmitter(){EventEmitter.init.call(this)}s.exports=EventEmitter,s.exports.once=function once(s,o){return new Promise((function(i,a){function errorListener(i){s.removeListener(o,resolver),a(i)}function resolver(){"function"==typeof s.removeListener&&s.removeListener("error",errorListener),i([].slice.call(arguments))}eventTargetAgnosticAddListener(s,o,resolver,{once:!0}),"error"!==o&&function addErrorHandlerIfEventEmitter(s,o,i){"function"==typeof s.on&&eventTargetAgnosticAddListener(s,"error",o,i)}(s,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var _=10;function checkListener(s){if("function"!=typeof s)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof s)}function _getMaxListeners(s){return void 0===s._maxListeners?EventEmitter.defaultMaxListeners:s._maxListeners}function _addListener(s,o,i,a){var u,_,w;if(checkListener(i),void 0===(_=s._events)?(_=s._events=Object.create(null),s._eventsCount=0):(void 0!==_.newListener&&(s.emit("newListener",o,i.listener?i.listener:i),_=s._events),w=_[o]),void 0===w)w=_[o]=i,++s._eventsCount;else if("function"==typeof w?w=_[o]=a?[i,w]:[w,i]:a?w.unshift(i):w.push(i),(u=_getMaxListeners(s))>0&&w.length>u&&!w.warned){w.warned=!0;var x=new Error("Possible EventEmitter memory leak detected. "+w.length+" "+String(o)+" listeners added. Use emitter.setMaxListeners() to increase limit");x.name="MaxListenersExceededWarning",x.emitter=s,x.type=o,x.count=w.length,function ProcessEmitWarning(s){console&&console.warn&&console.warn(s)}(x)}return s}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(s,o,i){var a={fired:!1,wrapFn:void 0,target:s,type:o,listener:i},u=onceWrapper.bind(a);return u.listener=i,a.wrapFn=u,u}function _listeners(s,o,i){var a=s._events;if(void 0===a)return[];var u=a[o];return void 0===u?[]:"function"==typeof u?i?[u.listener||u]:[u]:i?function unwrapListeners(s){for(var o=new Array(s.length),i=0;i0&&(w=o[0]),w instanceof Error)throw w;var x=new Error("Unhandled error."+(w?" ("+w.message+")":""));throw x.context=w,x}var C=_[s];if(void 0===C)return!1;if("function"==typeof C)a(C,this,o);else{var j=C.length,L=arrayClone(C,j);for(i=0;i=0;_--)if(i[_]===o||i[_].listener===o){w=i[_].listener,u=_;break}if(u<0)return this;0===u?i.shift():function spliceOne(s,o){for(;o+1=0;a--)this.removeListener(s,o[a]);return this},EventEmitter.prototype.listeners=function listeners(s){return _listeners(this,s,!0)},EventEmitter.prototype.rawListeners=function rawListeners(s){return _listeners(this,s,!1)},EventEmitter.listenerCount=function(s,o){return"function"==typeof s.listenerCount?s.listenerCount(o):listenerCount.call(s,o)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?o(this._events):[]}},85587(s,o,i){"use strict";var a=i(26311),u=create(Error);function create(s){return FormattedError.displayName=s.displayName||s.name,FormattedError;function FormattedError(o){return o&&(o=a.apply(null,arguments)),new s(o)}}s.exports=u,u.eval=create(EvalError),u.range=create(RangeError),u.reference=create(ReferenceError),u.syntax=create(SyntaxError),u.type=create(TypeError),u.uri=create(URIError),u.create=create},82682(s,o,i){"use strict";var a=i(69600),u=Object.prototype.toString,_=Object.prototype.hasOwnProperty;s.exports=function forEach(s,o,i){if(!a(o))throw new TypeError("iterator must be a function");var w;arguments.length>=3&&(w=i),function isArray(s){return"[object Array]"===u.call(s)}(s)?function forEachArray(s,o,i){for(var a=0,u=s.length;a0?parseInt(i):null};x1&&"boolean"!=typeof o)throw new L('"allowMissing" argument must be a boolean');if(null===Xe(/^%?[^%]*%?$/,s))throw new j("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var i=function stringToPath(s){var o=Ye(s,0,1),i=Ye(s,-1);if("%"===o&&"%"!==i)throw new j("invalid intrinsic syntax, expected closing `%`");if("%"===i&&"%"!==o)throw new j("invalid intrinsic syntax, expected opening `%`");var a=[];return He(s,Qe,(function(s,o,i,u){a[a.length]=i?He(u,et,"$1"):o||s})),a}(s),a=i.length>0?i[0]:"",u=tt("%"+a+"%",o),_=u.name,w=u.value,x=!1,C=u.alias;C&&(a=C[0],We(i,ze([0,1],C)));for(var B=1,$=!0;B=i.length){var Y=ae(w,U);w=($=!!Y)&&"get"in Y&&!("originalValue"in Y.get)?Y.get:w[U]}else $=qe(w,U),w=w[U];$&&!x&&(xe[_]=w)}}return w}},71064(s,o,i){"use strict";var a=i(79612);s.exports=a.getPrototypeOf||null},48648(s){"use strict";s.exports="undefined"!=typeof Reflect&&Reflect.getPrototypeOf||null},93628(s,o,i){"use strict";var a=i(48648),u=i(71064),_=i(7176);s.exports=a?function getProto(s){return a(s)}:u?function getProto(s){if(!s||"object"!=typeof s&&"function"!=typeof s)throw new TypeError("getProto: not an object");return u(s)}:_?function getProto(s){return _(s)}:null},6549(s){"use strict";s.exports=Object.getOwnPropertyDescriptor},75795(s,o,i){"use strict";var a=i(6549);if(a)try{a([],"length")}catch(s){a=null}s.exports=a},30592(s,o,i){"use strict";var a=i(30655),u=function hasPropertyDescriptors(){return!!a};u.hasArrayLengthDefineBug=function hasArrayLengthDefineBug(){if(!a)return null;try{return 1!==a([],"length",{value:1}).length}catch(s){return!0}},s.exports=u},64039(s,o,i){"use strict";var a="undefined"!=typeof Symbol&&Symbol,u=i(41333);s.exports=function hasNativeSymbols(){return"function"==typeof a&&("function"==typeof Symbol&&("symbol"==typeof a("foo")&&("symbol"==typeof Symbol("bar")&&u())))}},41333(s){"use strict";s.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var s={},o=Symbol("test"),i=Object(o);if("string"==typeof o)return!1;if("[object Symbol]"!==Object.prototype.toString.call(o))return!1;if("[object Symbol]"!==Object.prototype.toString.call(i))return!1;for(var a in s[o]=42,s)return!1;if("function"==typeof Object.keys&&0!==Object.keys(s).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(s).length)return!1;var u=Object.getOwnPropertySymbols(s);if(1!==u.length||u[0]!==o)return!1;if(!Object.prototype.propertyIsEnumerable.call(s,o))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var _=Object.getOwnPropertyDescriptor(s,o);if(42!==_.value||!0!==_.enumerable)return!1}return!0}},49092(s,o,i){"use strict";var a=i(41333);s.exports=function hasToStringTagShams(){return a()&&!!Symbol.toStringTag}},9957(s,o,i){"use strict";var a=Function.prototype.call,u=Object.prototype.hasOwnProperty,_=i(66743);s.exports=_.call(a,u)},45981(s){function deepFreeze(s){return s instanceof Map?s.clear=s.delete=s.set=function(){throw new Error("map is read-only")}:s instanceof Set&&(s.add=s.clear=s.delete=function(){throw new Error("set is read-only")}),Object.freeze(s),Object.getOwnPropertyNames(s).forEach((function(o){var i=s[o];"object"!=typeof i||Object.isFrozen(i)||deepFreeze(i)})),s}var o=deepFreeze,i=deepFreeze;o.default=i;class Response{constructor(s){void 0===s.data&&(s.data={}),this.data=s.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(s){return s.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(s,...o){const i=Object.create(null);for(const o in s)i[o]=s[o];return o.forEach((function(s){for(const o in s)i[o]=s[o]})),i}const emitsWrappingTags=s=>!!s.kind;class HTMLRenderer{constructor(s,o){this.buffer="",this.classPrefix=o.classPrefix,s.walk(this)}addText(s){this.buffer+=escapeHTML(s)}openNode(s){if(!emitsWrappingTags(s))return;let o=s.kind;s.sublanguage||(o=`${this.classPrefix}${o}`),this.span(o)}closeNode(s){emitsWrappingTags(s)&&(this.buffer+="
    ")}value(){return this.buffer}span(s){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(s){this.top.children.push(s)}openNode(s){const o={kind:s,children:[]};this.add(o),this.stack.push(o)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(s){return this.constructor._walk(s,this.rootNode)}static _walk(s,o){return"string"==typeof o?s.addText(o):o.children&&(s.openNode(o),o.children.forEach((o=>this._walk(s,o))),s.closeNode(o)),s}static _collapse(s){"string"!=typeof s&&s.children&&(s.children.every((s=>"string"==typeof s))?s.children=[s.children.join("")]:s.children.forEach((s=>{TokenTree._collapse(s)})))}}class TokenTreeEmitter extends TokenTree{constructor(s){super(),this.options=s}addKeyword(s,o){""!==s&&(this.openNode(o),this.addText(s),this.closeNode())}addText(s){""!==s&&this.add(s)}addSublanguage(s,o){const i=s.root;i.kind=o,i.sublanguage=!0,this.add(i)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(s){return s?"string"==typeof s?s:s.source:null}const a=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const u="[a-zA-Z]\\w*",_="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",x="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",C="\\b(0b[01]+)",j={begin:"\\\\[\\s\\S]",relevance:0},L={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[j]},B={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[j]},$={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(s,o,i={}){const a=inherit({className:"comment",begin:s,end:o,contains:[]},i);return a.contains.push($),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},U=COMMENT("//","$"),V=COMMENT("/\\*","\\*/"),z=COMMENT("#","$"),Y={className:"number",begin:w,relevance:0},Z={className:"number",begin:x,relevance:0},ee={className:"number",begin:C,relevance:0},ie={className:"number",begin:w+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},ae={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[j,{begin:/\[/,end:/\]/,relevance:0,contains:[j]}]}]},ce={className:"title",begin:u,relevance:0},le={className:"title",begin:_,relevance:0},pe={begin:"\\.\\s*"+_,relevance:0};var de=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:u,UNDERSCORE_IDENT_RE:_,NUMBER_RE:w,C_NUMBER_RE:x,BINARY_NUMBER_RE:C,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(s={})=>{const o=/^#![ ]*\//;return s.binary&&(s.begin=function concat(...s){return s.map((s=>source(s))).join("")}(o,/.*\b/,s.binary,/\b.*/)),inherit({className:"meta",begin:o,end:/$/,relevance:0,"on:begin":(s,o)=>{0!==s.index&&o.ignoreMatch()}},s)},BACKSLASH_ESCAPE:j,APOS_STRING_MODE:L,QUOTE_STRING_MODE:B,PHRASAL_WORDS_MODE:$,COMMENT,C_LINE_COMMENT_MODE:U,C_BLOCK_COMMENT_MODE:V,HASH_COMMENT_MODE:z,NUMBER_MODE:Y,C_NUMBER_MODE:Z,BINARY_NUMBER_MODE:ee,CSS_NUMBER_MODE:ie,REGEXP_MODE:ae,TITLE_MODE:ce,UNDERSCORE_TITLE_MODE:le,METHOD_GUARD:pe,END_SAME_AS_BEGIN:function(s){return Object.assign(s,{"on:begin":(s,o)=>{o.data._beginMatch=s[1]},"on:end":(s,o)=>{o.data._beginMatch!==s[1]&&o.ignoreMatch()}})}});function skipIfhasPrecedingDot(s,o){"."===s.input[s.index-1]&&o.ignoreMatch()}function beginKeywords(s,o){o&&s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",s.__beforeBegin=skipIfhasPrecedingDot,s.keywords=s.keywords||s.beginKeywords,delete s.beginKeywords,void 0===s.relevance&&(s.relevance=0))}function compileIllegal(s,o){Array.isArray(s.illegal)&&(s.illegal=function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}(...s.illegal))}function compileMatch(s,o){if(s.match){if(s.begin||s.end)throw new Error("begin & end are not supported with match");s.begin=s.match,delete s.match}}function compileRelevance(s,o){void 0===s.relevance&&(s.relevance=1)}const fe=["of","and","for","in","not","or","if","then","parent","list","value"];function compileKeywords(s,o,i="keyword"){const a={};return"string"==typeof s?compileList(i,s.split(" ")):Array.isArray(s)?compileList(i,s):Object.keys(s).forEach((function(i){Object.assign(a,compileKeywords(s[i],o,i))})),a;function compileList(s,i){o&&(i=i.map((s=>s.toLowerCase()))),i.forEach((function(o){const i=o.split("|");a[i[0]]=[s,scoreForKeyword(i[0],i[1])]}))}}function scoreForKeyword(s,o){return o?Number(o):function commonKeyword(s){return fe.includes(s.toLowerCase())}(s)?0:1}function compileLanguage(s,{plugins:o}){function langRe(o,i){return new RegExp(source(o),"m"+(s.case_insensitive?"i":"")+(i?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(s,o){o.position=this.position++,this.matchIndexes[this.matchAt]=o,this.regexes.push([o,s]),this.matchAt+=function countMatchGroups(s){return new RegExp(s.toString()+"|").exec("").length-1}(s)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const s=this.regexes.map((s=>s[1]));this.matcherRe=langRe(function join(s,o="|"){let i=0;return s.map((s=>{i+=1;const o=i;let u=source(s),_="";for(;u.length>0;){const s=a.exec(u);if(!s){_+=u;break}_+=u.substring(0,s.index),u=u.substring(s.index+s[0].length),"\\"===s[0][0]&&s[1]?_+="\\"+String(Number(s[1])+o):(_+=s[0],"("===s[0]&&i++)}return _})).map((s=>`(${s})`)).join(o)}(s),!0),this.lastIndex=0}exec(s){this.matcherRe.lastIndex=this.lastIndex;const o=this.matcherRe.exec(s);if(!o)return null;const i=o.findIndex(((s,o)=>o>0&&void 0!==s)),a=this.matchIndexes[i];return o.splice(0,i),Object.assign(o,a)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(s){if(this.multiRegexes[s])return this.multiRegexes[s];const o=new MultiRegex;return this.rules.slice(s).forEach((([s,i])=>o.addRule(s,i))),o.compile(),this.multiRegexes[s]=o,o}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(s,o){this.rules.push([s,o]),"begin"===o.type&&this.count++}exec(s){const o=this.getMatcher(this.regexIndex);o.lastIndex=this.lastIndex;let i=o.exec(s);if(this.resumingScanAtSamePosition())if(i&&i.index===this.lastIndex);else{const o=this.getMatcher(0);o.lastIndex=this.lastIndex+1,i=o.exec(s)}return i&&(this.regexIndex+=i.position+1,this.regexIndex===this.count&&this.considerAll()),i}}if(s.compilerExtensions||(s.compilerExtensions=[]),s.contains&&s.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return s.classNameAliases=inherit(s.classNameAliases||{}),function compileMode(o,i){const a=o;if(o.isCompiled)return a;[compileMatch].forEach((s=>s(o,i))),s.compilerExtensions.forEach((s=>s(o,i))),o.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((s=>s(o,i))),o.isCompiled=!0;let u=null;if("object"==typeof o.keywords&&(u=o.keywords.$pattern,delete o.keywords.$pattern),o.keywords&&(o.keywords=compileKeywords(o.keywords,s.case_insensitive)),o.lexemes&&u)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return u=u||o.lexemes||/\w+/,a.keywordPatternRe=langRe(u,!0),i&&(o.begin||(o.begin=/\B|\b/),a.beginRe=langRe(o.begin),o.endSameAsBegin&&(o.end=o.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),o.end&&(a.endRe=langRe(o.end)),a.terminatorEnd=source(o.end)||"",o.endsWithParent&&i.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+i.terminatorEnd)),o.illegal&&(a.illegalRe=langRe(o.illegal)),o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((function(s){return function expandOrCloneMode(s){s.variants&&!s.cachedVariants&&(s.cachedVariants=s.variants.map((function(o){return inherit(s,{variants:null},o)})));if(s.cachedVariants)return s.cachedVariants;if(dependencyOnParent(s))return inherit(s,{starts:s.starts?inherit(s.starts):null});if(Object.isFrozen(s))return inherit(s);return s}("self"===s?o:s)}))),o.contains.forEach((function(s){compileMode(s,a)})),o.starts&&compileMode(o.starts,i),a.matcher=function buildModeRegex(s){const o=new ResumableMultiRegex;return s.contains.forEach((s=>o.addRule(s.begin,{rule:s,type:"begin"}))),s.terminatorEnd&&o.addRule(s.terminatorEnd,{type:"end"}),s.illegal&&o.addRule(s.illegal,{type:"illegal"}),o}(a),a}(s)}function dependencyOnParent(s){return!!s&&(s.endsWithParent||dependencyOnParent(s.starts))}function BuildVuePlugin(s){const o={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!s.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let o={};return this.autoDetect?(o=s.highlightAuto(this.code),this.detectedLanguage=o.language):(o=s.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),o.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(s){return Boolean(s||""===s)}(this.autodetect)},ignoreIllegals:()=>!0},render(s){return s("pre",{},[s("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:o,VuePlugin:{install(s){s.component("highlightjs",o)}}}}const ye={"after:highlightElement":({el:s,result:o,text:i})=>{const a=nodeStream(s);if(!a.length)return;const u=document.createElement("div");u.innerHTML=o.value,o.value=function mergeStreams(s,o,i){let a=0,u="";const _=[];function selectStream(){return s.length&&o.length?s[0].offset!==o[0].offset?s[0].offset"}function close(s){u+=""}function render(s){("start"===s.event?open:close)(s.node)}for(;s.length||o.length;){let o=selectStream();if(u+=escapeHTML(i.substring(a,o[0].offset)),a=o[0].offset,o===s){_.reverse().forEach(close);do{render(o.splice(0,1)[0]),o=selectStream()}while(o===s&&o.length&&o[0].offset===a);_.reverse().forEach(open)}else"start"===o[0].event?_.push(o[0].node):_.pop(),render(o.splice(0,1)[0])}return u+escapeHTML(i.substr(a))}(a,nodeStream(u),i)}};function tag(s){return s.nodeName.toLowerCase()}function nodeStream(s){const o=[];return function _nodeStream(s,i){for(let a=s.firstChild;a;a=a.nextSibling)3===a.nodeType?i+=a.nodeValue.length:1===a.nodeType&&(o.push({event:"start",offset:i,node:a}),i=_nodeStream(a,i),tag(a).match(/br|hr|img|input/)||o.push({event:"stop",offset:i,node:a}));return i}(s,0),o}const be={},error=s=>{console.error(s)},warn=(s,...o)=>{console.log(`WARN: ${s}`,...o)},deprecated=(s,o)=>{be[`${s}/${o}`]||(console.log(`Deprecated as of ${s}. ${o}`),be[`${s}/${o}`]=!0)},Se=escapeHTML,_e=inherit,we=Symbol("nomatch");var xe=function(s){const i=Object.create(null),a=Object.create(null),u=[];let _=!0;const w=/(^(<[^>]+>|\t|)+|\n)/gm,x="Could not find the language '{}', did you forget to load/include a language module?",C={disableAutodetect:!0,name:"Plain text",contains:[]};let j={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(s){return j.noHighlightRe.test(s)}function highlight(s,o,i,a){let u="",_="";"object"==typeof o?(u=s,i=o.ignoreIllegals,_=o.language,a=void 0):(deprecated("10.7.0","highlight(lang, code, ...args) has been deprecated."),deprecated("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),_=s,u=o);const w={code:u,language:_};fire("before:highlight",w);const x=w.result?w.result:_highlight(w.language,w.code,i,a);return x.code=w.code,fire("after:highlight",x),x}function _highlight(s,o,a,w){function keywordData(s,o){const i=L.case_insensitive?o[0].toLowerCase():o[0];return Object.prototype.hasOwnProperty.call(s.keywords,i)&&s.keywords[i]}function processBuffer(){null!=U.subLanguage?function processSubLanguage(){if(""===Y)return;let s=null;if("string"==typeof U.subLanguage){if(!i[U.subLanguage])return void z.addText(Y);s=_highlight(U.subLanguage,Y,!0,V[U.subLanguage]),V[U.subLanguage]=s.top}else s=highlightAuto(Y,U.subLanguage.length?U.subLanguage:null);U.relevance>0&&(Z+=s.relevance),z.addSublanguage(s.emitter,s.language)}():function processKeywords(){if(!U.keywords)return void z.addText(Y);let s=0;U.keywordPatternRe.lastIndex=0;let o=U.keywordPatternRe.exec(Y),i="";for(;o;){i+=Y.substring(s,o.index);const a=keywordData(U,o);if(a){const[s,u]=a;if(z.addText(i),i="",Z+=u,s.startsWith("_"))i+=o[0];else{const i=L.classNameAliases[s]||s;z.addKeyword(o[0],i)}}else i+=o[0];s=U.keywordPatternRe.lastIndex,o=U.keywordPatternRe.exec(Y)}i+=Y.substr(s),z.addText(i)}(),Y=""}function startNewMode(s){return s.className&&z.openNode(L.classNameAliases[s.className]||s.className),U=Object.create(s,{parent:{value:U}}),U}function endOfMode(s,o,i){let a=function startsWith(s,o){const i=s&&s.exec(o);return i&&0===i.index}(s.endRe,i);if(a){if(s["on:end"]){const i=new Response(s);s["on:end"](o,i),i.isMatchIgnored&&(a=!1)}if(a){for(;s.endsParent&&s.parent;)s=s.parent;return s}}if(s.endsWithParent)return endOfMode(s.parent,o,i)}function doIgnore(s){return 0===U.matcher.regexIndex?(Y+=s[0],1):(ae=!0,0)}function doBeginMatch(s){const o=s[0],i=s.rule,a=new Response(i),u=[i.__beforeBegin,i["on:begin"]];for(const i of u)if(i&&(i(s,a),a.isMatchIgnored))return doIgnore(o);return i&&i.endSameAsBegin&&(i.endRe=function escape(s){return new RegExp(s.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(o)),i.skip?Y+=o:(i.excludeBegin&&(Y+=o),processBuffer(),i.returnBegin||i.excludeBegin||(Y=o)),startNewMode(i),i.returnBegin?0:o.length}function doEndMatch(s){const i=s[0],a=o.substr(s.index),u=endOfMode(U,s,a);if(!u)return we;const _=U;_.skip?Y+=i:(_.returnEnd||_.excludeEnd||(Y+=i),processBuffer(),_.excludeEnd&&(Y=i));do{U.className&&z.closeNode(),U.skip||U.subLanguage||(Z+=U.relevance),U=U.parent}while(U!==u.parent);return u.starts&&(u.endSameAsBegin&&(u.starts.endRe=u.endRe),startNewMode(u.starts)),_.returnEnd?0:i.length}let C={};function processLexeme(i,u){const w=u&&u[0];if(Y+=i,null==w)return processBuffer(),0;if("begin"===C.type&&"end"===u.type&&C.index===u.index&&""===w){if(Y+=o.slice(u.index,u.index+1),!_){const o=new Error("0 width match regex");throw o.languageName=s,o.badRule=C.rule,o}return 1}if(C=u,"begin"===u.type)return doBeginMatch(u);if("illegal"===u.type&&!a){const s=new Error('Illegal lexeme "'+w+'" for mode "'+(U.className||"")+'"');throw s.mode=U,s}if("end"===u.type){const s=doEndMatch(u);if(s!==we)return s}if("illegal"===u.type&&""===w)return 1;if(ie>1e5&&ie>3*u.index){throw new Error("potential infinite loop, way more iterations than matches")}return Y+=w,w.length}const L=getLanguage(s);if(!L)throw error(x.replace("{}",s)),new Error('Unknown language: "'+s+'"');const B=compileLanguage(L,{plugins:u});let $="",U=w||B;const V={},z=new j.__emitter(j);!function processContinuations(){const s=[];for(let o=U;o!==L;o=o.parent)o.className&&s.unshift(o.className);s.forEach((s=>z.openNode(s)))}();let Y="",Z=0,ee=0,ie=0,ae=!1;try{for(U.matcher.considerAll();;){ie++,ae?ae=!1:U.matcher.considerAll(),U.matcher.lastIndex=ee;const s=U.matcher.exec(o);if(!s)break;const i=processLexeme(o.substring(ee,s.index),s);ee=s.index+i}return processLexeme(o.substr(ee)),z.closeAllNodes(),z.finalize(),$=z.toHTML(),{relevance:Math.floor(Z),value:$,language:s,illegal:!1,emitter:z,top:U}}catch(i){if(i.message&&i.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:i.message,context:o.slice(ee-100,ee+100),mode:i.mode},sofar:$,relevance:0,value:Se(o),emitter:z};if(_)return{illegal:!1,relevance:0,value:Se(o),emitter:z,language:s,top:U,errorRaised:i};throw i}}function highlightAuto(s,o){o=o||j.languages||Object.keys(i);const a=function justTextHighlightResult(s){const o={relevance:0,emitter:new j.__emitter(j),value:Se(s),illegal:!1,top:C};return o.emitter.addText(s),o}(s),u=o.filter(getLanguage).filter(autoDetection).map((o=>_highlight(o,s,!1)));u.unshift(a);const _=u.sort(((s,o)=>{if(s.relevance!==o.relevance)return o.relevance-s.relevance;if(s.language&&o.language){if(getLanguage(s.language).supersetOf===o.language)return 1;if(getLanguage(o.language).supersetOf===s.language)return-1}return 0})),[w,x]=_,L=w;return L.second_best=x,L}const L={"before:highlightElement":({el:s})=>{j.useBR&&(s.innerHTML=s.innerHTML.replace(/\n/g,"").replace(//g,"\n"))},"after:highlightElement":({result:s})=>{j.useBR&&(s.value=s.value.replace(/\n/g,"
    "))}},B=/^(<[^>]+>|\t)+/gm,$={"after:highlightElement":({result:s})=>{j.tabReplace&&(s.value=s.value.replace(B,(s=>s.replace(/\t/g,j.tabReplace))))}};function highlightElement(s){let o=null;const i=function blockLanguage(s){let o=s.className+" ";o+=s.parentNode?s.parentNode.className:"";const i=j.languageDetectRe.exec(o);if(i){const o=getLanguage(i[1]);return o||(warn(x.replace("{}",i[1])),warn("Falling back to no-highlight mode for this block.",s)),o?i[1]:"no-highlight"}return o.split(/\s+/).find((s=>shouldNotHighlight(s)||getLanguage(s)))}(s);if(shouldNotHighlight(i))return;fire("before:highlightElement",{el:s,language:i}),o=s;const u=o.textContent,_=i?highlight(u,{language:i,ignoreIllegals:!0}):highlightAuto(u);fire("after:highlightElement",{el:s,result:_,text:u}),s.innerHTML=_.value,function updateClassName(s,o,i){const u=o?a[o]:i;s.classList.add("hljs"),u&&s.classList.add(u)}(s,i,_.language),s.result={language:_.language,re:_.relevance,relavance:_.relevance},_.second_best&&(s.second_best={language:_.second_best.language,re:_.second_best.relevance,relavance:_.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(highlightElement)};let U=!1;function highlightAll(){if("loading"===document.readyState)return void(U=!0);document.querySelectorAll("pre code").forEach(highlightElement)}function getLanguage(s){return s=(s||"").toLowerCase(),i[s]||i[a[s]]}function registerAliases(s,{languageName:o}){"string"==typeof s&&(s=[s]),s.forEach((s=>{a[s.toLowerCase()]=o}))}function autoDetection(s){const o=getLanguage(s);return o&&!o.disableAutodetect}function fire(s,o){const i=s;u.forEach((function(s){s[i]&&s[i](o)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function boot(){U&&highlightAll()}),!1),Object.assign(s,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(s){return deprecated("10.2.0","fixMarkup will be removed entirely in v11.0"),deprecated("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function fixMarkup(s){return j.tabReplace||j.useBR?s.replace(w,(s=>"\n"===s?j.useBR?"
    ":s:j.tabReplace?s.replace(/\t/g,j.tabReplace):s)):s}(s)},highlightElement,highlightBlock:function deprecateHighlightBlock(s){return deprecated("10.7.0","highlightBlock will be removed entirely in v12.0"),deprecated("10.7.0","Please use highlightElement now."),highlightElement(s)},configure:function configure(s){s.useBR&&(deprecated("10.3.0","'useBR' will be removed entirely in v11.0"),deprecated("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),j=_e(j,s)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),U=!0},registerLanguage:function registerLanguage(o,a){let u=null;try{u=a(s)}catch(s){if(error("Language definition for '{}' could not be registered.".replace("{}",o)),!_)throw s;error(s),u=C}u.name||(u.name=o),i[o]=u,u.rawDefinition=a.bind(null,s),u.aliases&®isterAliases(u.aliases,{languageName:o})},unregisterLanguage:function unregisterLanguage(s){delete i[s];for(const o of Object.keys(a))a[o]===s&&delete a[o]},listLanguages:function listLanguages(){return Object.keys(i)},getLanguage,registerAliases,requireLanguage:function requireLanguage(s){deprecated("10.4.0","requireLanguage will be removed entirely in v11."),deprecated("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const o=getLanguage(s);if(o)return o;throw new Error("The '{}' language is required, but not loaded.".replace("{}",s))},autoDetection,inherit:_e,addPlugin:function addPlugin(s){!function upgradePluginAPI(s){s["before:highlightBlock"]&&!s["before:highlightElement"]&&(s["before:highlightElement"]=o=>{s["before:highlightBlock"](Object.assign({block:o.el},o))}),s["after:highlightBlock"]&&!s["after:highlightElement"]&&(s["after:highlightElement"]=o=>{s["after:highlightBlock"](Object.assign({block:o.el},o))})}(s),u.push(s)},vuePlugin:BuildVuePlugin(s).VuePlugin}),s.debugMode=function(){_=!1},s.safeMode=function(){_=!0},s.versionString="10.7.3";for(const s in de)"object"==typeof de[s]&&o(de[s]);return Object.assign(s,de),s.addPlugin(L),s.addPlugin(ye),s.addPlugin($),s}({});s.exports=xe},35344(s){function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function bash(s){const o={},i={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[o]}]};Object.assign(o,{className:"variable",variants:[{begin:concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},i]});const a={className:"subst",begin:/\$\(/,end:/\)/,contains:[s.BACKSLASH_ESCAPE]},u={begin:/<<-?\s*(?=\w+)/,starts:{contains:[s.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},_={className:"string",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE,o,a]};a.contains.push(_);const w={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},s.NUMBER_MODE,o]},x=s.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),C={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[s.inherit(s.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[x,s.SHEBANG(),C,w,s.HASH_COMMENT_MODE,u,_,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},o]}}},73402(s){function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function http(s){const o="HTTP/(2|1\\.[01])",i={className:"attribute",begin:concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},a=[i,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+o+" \\d{3})",end:/$/,contains:[{className:"meta",begin:o},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:a}},{begin:"(?=^[A-Z]+ (.*?) "+o+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:o},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:a}},s.inherit(i,{relevance:0})]}}},95089(s){const o="[A-Za-z$_][0-9A-Za-z$_]*",i=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],u=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function javascript(s){const _=o,w="<>",x="",C={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(s,o)=>{const i=s[0].length+s.index,a=s.input[i];"<"!==a?">"===a&&(((s,{after:o})=>{const i="",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:s.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:j,contains:ce}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:w,end:x},{begin:C.begin,"on:begin":C.isTrulyOpeningTag,end:C.end}],subLanguage:"xml",contains:[{begin:C.begin,end:C.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:j,contains:["self",s.inherit(s.TITLE_MODE,{begin:_}),le],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:s.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[le,s.inherit(s.TITLE_MODE,{begin:_})]},{variants:[{begin:"\\."+_},{begin:"\\$"+_}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},s.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[s.inherit(s.TITLE_MODE,{begin:_}),"self",le]},{begin:"(get|set)\\s+(?="+_+"\\()",end:/\{/,keywords:"get set",contains:[s.inherit(s.TITLE_MODE,{begin:_}),{begin:/\(\)/},le]},{begin:/\$[(.]/}]}}},65772(s){s.exports=function json(s){const o={literal:"true false null"},i=[s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE],a=[s.QUOTE_STRING_MODE,s.C_NUMBER_MODE],u={end:",",endsWithParent:!0,excludeEnd:!0,contains:a,keywords:o},_={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE],illegal:"\\n"},s.inherit(u,{begin:/:/})].concat(i),illegal:"\\S"},w={begin:"\\[",end:"\\]",contains:[s.inherit(u)],illegal:"\\S"};return a.push(_,w),i.forEach((function(s){a.push(s)})),{name:"JSON",contains:a,keywords:o,illegal:"\\S"}}},26571(s){s.exports=function powershell(s){const o={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},i={begin:"`[\\s\\S]",relevance:0},a={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},u={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[i,a,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},_={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},w=s.inherit(s.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),x={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},C={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[s.TITLE_MODE]},j={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[a]}]},L={begin:/using\s/,end:/$/,returnBegin:!0,contains:[u,_,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},B={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},$={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(o.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},s.inherit(s.TITLE_MODE,{endsParent:!0})]},U=[$,w,i,s.NUMBER_MODE,u,_,x,a,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],V={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",U,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return $.contains.unshift(V),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:o,contains:U.concat(C,j,L,B,V)}}},17285(s){function source(s){return s?"string"==typeof s?s:s.source:null}function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>source(s))).join("")}function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}s.exports=function xml(s){const o=concat(/[A-Z_]/,function optional(s){return concat("(",s,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),i={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},a={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},u=s.inherit(a,{begin:/\(/,end:/\)/}),_=s.inherit(s.APOS_STRING_MODE,{className:"meta-string"}),w=s.inherit(s.QUOTE_STRING_MODE,{className:"meta-string"}),x={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[a,w,_,u,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[a,u,w,_]}]}]},s.COMMENT(//,{relevance:10}),{begin://,relevance:10},i,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[x],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[x],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:o,relevance:0,starts:x}]},{className:"tag",begin:concat(/<\//,lookahead(concat(o,/>/))),contains:[{className:"name",begin:o,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},17533(s){s.exports=function yaml(s){var o="true false yes no null",i="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[s.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},u=s.inherit(a,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),_={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},w={end:",",endsWithParent:!0,excludeEnd:!0,keywords:o,relevance:0},x={begin:/\{/,end:/\}/,contains:[w],illegal:"\\n",relevance:0},C={begin:"\\[",end:"\\]",contains:[w],illegal:"\\n",relevance:0},j=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+i},{className:"type",begin:"!<"+i+">"},{className:"type",begin:"!"+i},{className:"type",begin:"!!"+i},{className:"meta",begin:"&"+s.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+s.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},s.HASH_COMMENT_MODE,{beginKeywords:o,keywords:{literal:o}},_,{className:"number",begin:s.C_NUMBER_RE+"\\b",relevance:0},x,C,a],L=[...j];return L.pop(),L.push(u),w.contains=L,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:j}}},251(s,o){o.read=function(s,o,i,a,u){var _,w,x=8*u-a-1,C=(1<>1,L=-7,B=i?u-1:0,$=i?-1:1,U=s[o+B];for(B+=$,_=U&(1<<-L)-1,U>>=-L,L+=x;L>0;_=256*_+s[o+B],B+=$,L-=8);for(w=_&(1<<-L)-1,_>>=-L,L+=a;L>0;w=256*w+s[o+B],B+=$,L-=8);if(0===_)_=1-j;else{if(_===C)return w?NaN:1/0*(U?-1:1);w+=Math.pow(2,a),_-=j}return(U?-1:1)*w*Math.pow(2,_-a)},o.write=function(s,o,i,a,u,_){var w,x,C,j=8*_-u-1,L=(1<>1,$=23===u?Math.pow(2,-24)-Math.pow(2,-77):0,U=a?0:_-1,V=a?1:-1,z=o<0||0===o&&1/o<0?1:0;for(o=Math.abs(o),isNaN(o)||o===1/0?(x=isNaN(o)?1:0,w=L):(w=Math.floor(Math.log(o)/Math.LN2),o*(C=Math.pow(2,-w))<1&&(w--,C*=2),(o+=w+B>=1?$/C:$*Math.pow(2,1-B))*C>=2&&(w++,C/=2),w+B>=L?(x=0,w=L):w+B>=1?(x=(o*C-1)*Math.pow(2,u),w+=B):(x=o*Math.pow(2,B-1)*Math.pow(2,u),w=0));u>=8;s[i+U]=255&x,U+=V,x/=256,u-=8);for(w=w<0;s[i+U]=255&w,U+=V,w/=256,j-=8);s[i+U-V]|=128*z}},9404(s){s.exports=function(){"use strict";var s=Array.prototype.slice;function createClass(s,o){o&&(s.prototype=Object.create(o.prototype)),s.prototype.constructor=s}function Iterable(s){return isIterable(s)?s:Seq(s)}function KeyedIterable(s){return isKeyed(s)?s:KeyedSeq(s)}function IndexedIterable(s){return isIndexed(s)?s:IndexedSeq(s)}function SetIterable(s){return isIterable(s)&&!isAssociative(s)?s:SetSeq(s)}function isIterable(s){return!(!s||!s[o])}function isKeyed(s){return!(!s||!s[i])}function isIndexed(s){return!(!s||!s[a])}function isAssociative(s){return isKeyed(s)||isIndexed(s)}function isOrdered(s){return!(!s||!s[u])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var o="@@__IMMUTABLE_ITERABLE__@@",i="@@__IMMUTABLE_KEYED__@@",a="@@__IMMUTABLE_INDEXED__@@",u="@@__IMMUTABLE_ORDERED__@@",_="delete",w=5,x=1<>>0;if(""+i!==o||4294967295===i)return NaN;o=i}return o<0?ensureSize(s)+o:o}function returnTrue(){return!0}function wholeSlice(s,o,i){return(0===s||void 0!==i&&s<=-i)&&(void 0===o||void 0!==i&&o>=i)}function resolveBegin(s,o){return resolveIndex(s,o,0)}function resolveEnd(s,o){return resolveIndex(s,o,o)}function resolveIndex(s,o,i){return void 0===s?i:s<0?Math.max(0,o+s):void 0===o?s:Math.min(o,s)}var $=0,U=1,V=2,z="function"==typeof Symbol&&Symbol.iterator,Y="@@iterator",Z=z||Y;function Iterator(s){this.next=s}function iteratorValue(s,o,i,a){var u=0===s?o:1===s?i:[o,i];return a?a.value=u:a={value:u,done:!1},a}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(s){return!!getIteratorFn(s)}function isIterator(s){return s&&"function"==typeof s.next}function getIterator(s){var o=getIteratorFn(s);return o&&o.call(s)}function getIteratorFn(s){var o=s&&(z&&s[z]||s[Y]);if("function"==typeof o)return o}function isArrayLike(s){return s&&"number"==typeof s.length}function Seq(s){return null==s?emptySequence():isIterable(s)?s.toSeq():seqFromValue(s)}function KeyedSeq(s){return null==s?emptySequence().toKeyedSeq():isIterable(s)?isKeyed(s)?s.toSeq():s.fromEntrySeq():keyedSeqFromValue(s)}function IndexedSeq(s){return null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s.toIndexedSeq():indexedSeqFromValue(s)}function SetSeq(s){return(null==s?emptySequence():isIterable(s)?isKeyed(s)?s.entrySeq():s:indexedSeqFromValue(s)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=$,Iterator.VALUES=U,Iterator.ENTRIES=V,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[Z]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!0)},Seq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(s,o){return seqIterate(this,s,o,!1)},IndexedSeq.prototype.__iterator=function(s,o){return seqIterator(this,s,o,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var ee,ie,ae,ce="@@__IMMUTABLE_SEQ__@@";function ArraySeq(s){this._array=s,this.size=s.length}function ObjectSeq(s){var o=Object.keys(s);this._object=s,this._keys=o,this.size=o.length}function IterableSeq(s){this._iterable=s,this.size=s.length||s.size}function IteratorSeq(s){this._iterator=s,this._iteratorCache=[]}function isSeq(s){return!(!s||!s[ce])}function emptySequence(){return ee||(ee=new ArraySeq([]))}function keyedSeqFromValue(s){var o=Array.isArray(s)?new ArraySeq(s).fromEntrySeq():isIterator(s)?new IteratorSeq(s).fromEntrySeq():hasIterator(s)?new IterableSeq(s).fromEntrySeq():"object"==typeof s?new ObjectSeq(s):void 0;if(!o)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+s);return o}function indexedSeqFromValue(s){var o=maybeIndexedSeqFromValue(s);if(!o)throw new TypeError("Expected Array or iterable object of values: "+s);return o}function seqFromValue(s){var o=maybeIndexedSeqFromValue(s)||"object"==typeof s&&new ObjectSeq(s);if(!o)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+s);return o}function maybeIndexedSeqFromValue(s){return isArrayLike(s)?new ArraySeq(s):isIterator(s)?new IteratorSeq(s):hasIterator(s)?new IterableSeq(s):void 0}function seqIterate(s,o,i,a){var u=s._cache;if(u){for(var _=u.length-1,w=0;w<=_;w++){var x=u[i?_-w:w];if(!1===o(x[1],a?x[0]:w,s))return w+1}return w}return s.__iterateUncached(o,i)}function seqIterator(s,o,i,a){var u=s._cache;if(u){var _=u.length-1,w=0;return new Iterator((function(){var s=u[i?_-w:w];return w++>_?iteratorDone():iteratorValue(o,a?s[0]:w-1,s[1])}))}return s.__iteratorUncached(o,i)}function fromJS(s,o){return o?fromJSWith(o,s,"",{"":s}):fromJSDefault(s)}function fromJSWith(s,o,i,a){return Array.isArray(o)?s.call(a,i,IndexedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):isPlainObj(o)?s.call(a,i,KeyedSeq(o).map((function(i,a){return fromJSWith(s,i,a,o)}))):o}function fromJSDefault(s){return Array.isArray(s)?IndexedSeq(s).map(fromJSDefault).toList():isPlainObj(s)?KeyedSeq(s).map(fromJSDefault).toMap():s}function isPlainObj(s){return s&&(s.constructor===Object||void 0===s.constructor)}function is(s,o){if(s===o||s!=s&&o!=o)return!0;if(!s||!o)return!1;if("function"==typeof s.valueOf&&"function"==typeof o.valueOf){if((s=s.valueOf())===(o=o.valueOf())||s!=s&&o!=o)return!0;if(!s||!o)return!1}return!("function"!=typeof s.equals||"function"!=typeof o.equals||!s.equals(o))}function deepEqual(s,o){if(s===o)return!0;if(!isIterable(o)||void 0!==s.size&&void 0!==o.size&&s.size!==o.size||void 0!==s.__hash&&void 0!==o.__hash&&s.__hash!==o.__hash||isKeyed(s)!==isKeyed(o)||isIndexed(s)!==isIndexed(o)||isOrdered(s)!==isOrdered(o))return!1;if(0===s.size&&0===o.size)return!0;var i=!isAssociative(s);if(isOrdered(s)){var a=s.entries();return o.every((function(s,o){var u=a.next().value;return u&&is(u[1],s)&&(i||is(u[0],o))}))&&a.next().done}var u=!1;if(void 0===s.size)if(void 0===o.size)"function"==typeof s.cacheResult&&s.cacheResult();else{u=!0;var _=s;s=o,o=_}var w=!0,x=o.__iterate((function(o,a){if(i?!s.has(o):u?!is(o,s.get(a,j)):!is(s.get(a,j),o))return w=!1,!1}));return w&&s.size===x}function Repeat(s,o){if(!(this instanceof Repeat))return new Repeat(s,o);if(this._value=s,this.size=void 0===o?1/0:Math.max(0,o),0===this.size){if(ie)return ie;ie=this}}function invariant(s,o){if(!s)throw new Error(o)}function Range(s,o,i){if(!(this instanceof Range))return new Range(s,o,i);if(invariant(0!==i,"Cannot step a Range by 0"),s=s||0,void 0===o&&(o=1/0),i=void 0===i?1:Math.abs(i),oa?iteratorDone():iteratorValue(s,u,i[o?a-u++:u++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(s,o){return void 0===o||this.has(s)?this._object[s]:o},ObjectSeq.prototype.has=function(s){return this._object.hasOwnProperty(s)},ObjectSeq.prototype.__iterate=function(s,o){for(var i=this._object,a=this._keys,u=a.length-1,_=0;_<=u;_++){var w=a[o?u-_:_];if(!1===s(i[w],w,this))return _+1}return _},ObjectSeq.prototype.__iterator=function(s,o){var i=this._object,a=this._keys,u=a.length-1,_=0;return new Iterator((function(){var w=a[o?u-_:_];return _++>u?iteratorDone():iteratorValue(s,w,i[w])}))},ObjectSeq.prototype[u]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);var i=getIterator(this._iterable),a=0;if(isIterator(i))for(var u;!(u=i.next()).done&&!1!==s(u.value,a++,this););return a},IterableSeq.prototype.__iteratorUncached=function(s,o){if(o)return this.cacheResult().__iterator(s,o);var i=getIterator(this._iterable);if(!isIterator(i))return new Iterator(iteratorDone);var a=0;return new Iterator((function(){var o=i.next();return o.done?o:iteratorValue(s,a++,o.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(s,o){if(o)return this.cacheResult().__iterate(s,o);for(var i,a=this._iterator,u=this._iteratorCache,_=0;_=a.length){var o=i.next();if(o.done)return o;a[u]=o.value}return iteratorValue(s,u,a[u++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(s,o){return this.has(s)?this._value:o},Repeat.prototype.includes=function(s){return is(this._value,s)},Repeat.prototype.slice=function(s,o){var i=this.size;return wholeSlice(s,o,i)?this:new Repeat(this._value,resolveEnd(o,i)-resolveBegin(s,i))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(s){return is(this._value,s)?0:-1},Repeat.prototype.lastIndexOf=function(s){return is(this._value,s)?this.size:-1},Repeat.prototype.__iterate=function(s,o){for(var i=0;i=0&&o=0&&ii?iteratorDone():iteratorValue(s,_++,w)}))},Range.prototype.equals=function(s){return s instanceof Range?this._start===s._start&&this._end===s._end&&this._step===s._step:deepEqual(this,s)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var le="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(s,o){var i=65535&(s|=0),a=65535&(o|=0);return i*a+((s>>>16)*a+i*(o>>>16)<<16>>>0)|0};function smi(s){return s>>>1&1073741824|3221225471&s}function hash(s){if(!1===s||null==s)return 0;if("function"==typeof s.valueOf&&(!1===(s=s.valueOf())||null==s))return 0;if(!0===s)return 1;var o=typeof s;if("number"===o){if(s!=s||s===1/0)return 0;var i=0|s;for(i!==s&&(i^=4294967295*s);s>4294967295;)i^=s/=4294967295;return smi(i)}if("string"===o)return s.length>_e?cachedHashString(s):hashString(s);if("function"==typeof s.hashCode)return s.hashCode();if("object"===o)return hashJSObj(s);if("function"==typeof s.toString)return hashString(s.toString());throw new Error("Value type "+o+" cannot be hashed.")}function cachedHashString(s){var o=Pe[s];return void 0===o&&(o=hashString(s),xe===we&&(xe=0,Pe={}),xe++,Pe[s]=o),o}function hashString(s){for(var o=0,i=0;i0)switch(s.nodeType){case 1:return s.uniqueID;case 9:return s.documentElement&&s.documentElement.uniqueID}}var fe,ye="function"==typeof WeakMap;ye&&(fe=new WeakMap);var be=0,Se="__immutablehash__";"function"==typeof Symbol&&(Se=Symbol(Se));var _e=16,we=255,xe=0,Pe={};function assertNotInfinite(s){invariant(s!==1/0,"Cannot perform this action with an infinite size.")}function Map(s){return null==s?emptyMap():isMap(s)&&!isOrdered(s)?s:emptyMap().withMutations((function(o){var i=KeyedIterable(s);assertNotInfinite(i.size),i.forEach((function(s,i){return o.set(i,s)}))}))}function isMap(s){return!(!s||!s[Re])}createClass(Map,KeyedCollection),Map.of=function(){var o=s.call(arguments,0);return emptyMap().withMutations((function(s){for(var i=0;i=o.length)throw new Error("Missing value for key: "+o[i]);s.set(o[i],o[i+1])}}))},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(s,o){return this._root?this._root.get(0,void 0,s,o):o},Map.prototype.set=function(s,o){return updateMap(this,s,o)},Map.prototype.setIn=function(s,o){return this.updateIn(s,j,(function(){return o}))},Map.prototype.remove=function(s){return updateMap(this,s,j)},Map.prototype.deleteIn=function(s){return this.updateIn(s,(function(){return j}))},Map.prototype.update=function(s,o,i){return 1===arguments.length?s(this):this.updateIn([s],o,i)},Map.prototype.updateIn=function(s,o,i){i||(i=o,o=void 0);var a=updateInDeepMap(this,forceIterator(s),o,i);return a===j?void 0:a},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(o){return mergeIntoMapWith(this,o,s.call(arguments,1))},Map.prototype.mergeIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return"function"==typeof s.merge?s.merge.apply(s,i):i[i.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(o){var i=s.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(o),i)},Map.prototype.mergeDeepIn=function(o){var i=s.call(arguments,1);return this.updateIn(o,emptyMap(),(function(s){return"function"==typeof s.mergeDeep?s.mergeDeep.apply(s,i):i[i.length-1]}))},Map.prototype.sort=function(s){return OrderedMap(sortFactory(this,s))},Map.prototype.sortBy=function(s,o){return OrderedMap(sortFactory(this,o,s))},Map.prototype.withMutations=function(s){var o=this.asMutable();return s(o),o.wasAltered()?o.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(s,o){return new MapIterator(this,s,o)},Map.prototype.__iterate=function(s,o){var i=this,a=0;return this._root&&this._root.iterate((function(o){return a++,s(o[1],o[0],i)}),o),a},Map.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeMap(this.size,this._root,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Map.isMap=isMap;var Te,Re="@@__IMMUTABLE_MAP__@@",$e=Map.prototype;function ArrayMapNode(s,o){this.ownerID=s,this.entries=o}function BitmapIndexedNode(s,o,i){this.ownerID=s,this.bitmap=o,this.nodes=i}function HashArrayMapNode(s,o,i){this.ownerID=s,this.count=o,this.nodes=i}function HashCollisionNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entries=i}function ValueNode(s,o,i){this.ownerID=s,this.keyHash=o,this.entry=i}function MapIterator(s,o,i){this._type=o,this._reverse=i,this._stack=s._root&&mapIteratorFrame(s._root)}function mapIteratorValue(s,o){return iteratorValue(s,o[0],o[1])}function mapIteratorFrame(s,o){return{node:s,index:0,__prev:o}}function makeMap(s,o,i,a){var u=Object.create($e);return u.size=s,u._root=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyMap(){return Te||(Te=makeMap(0))}function updateMap(s,o,i){var a,u;if(s._root){var _=MakeRef(L),w=MakeRef(B);if(a=updateNode(s._root,s.__ownerID,0,void 0,o,i,_,w),!w.value)return s;u=s.size+(_.value?i===j?-1:1:0)}else{if(i===j)return s;u=1,a=new ArrayMapNode(s.__ownerID,[[o,i]])}return s.__ownerID?(s.size=u,s._root=a,s.__hash=void 0,s.__altered=!0,s):a?makeMap(u,a):emptyMap()}function updateNode(s,o,i,a,u,_,w,x){return s?s.update(o,i,a,u,_,w,x):_===j?s:(SetRef(x),SetRef(w),new ValueNode(o,a,[u,_]))}function isLeafNode(s){return s.constructor===ValueNode||s.constructor===HashCollisionNode}function mergeIntoNode(s,o,i,a,u){if(s.keyHash===a)return new HashCollisionNode(o,a,[s.entry,u]);var _,x=(0===i?s.keyHash:s.keyHash>>>i)&C,j=(0===i?a:a>>>i)&C;return new BitmapIndexedNode(o,1<>>=1)w[C]=1&i?o[_++]:void 0;return w[a]=u,new HashArrayMapNode(s,_+1,w)}function mergeIntoMapWith(s,o,i){for(var a=[],u=0;u>1&1431655765))+(s>>2&858993459))+(s>>4)&252645135,s+=s>>8,127&(s+=s>>16)}function setIn(s,o,i,a){var u=a?s:arrCopy(s);return u[o]=i,u}function spliceIn(s,o,i,a){var u=s.length+1;if(a&&o+1===u)return s[o]=i,s;for(var _=new Array(u),w=0,x=0;x=qe)return createNodes(s,C,a,u);var U=s&&s===this.ownerID,V=U?C:arrCopy(C);return $?x?L===B-1?V.pop():V[L]=V.pop():V[L]=[a,u]:V.push([a,u]),U?(this.entries=V,this):new ArrayMapNode(s,V)}},BitmapIndexedNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=1<<((0===s?o:o>>>s)&C),_=this.bitmap;return _&u?this.nodes[popCount(_&u-1)].get(s+w,o,i,a):a},BitmapIndexedNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=1<=ze)return expandNodes(s,z,$,L,Z);if(U&&!Z&&2===z.length&&isLeafNode(z[1^V]))return z[1^V];if(U&&Z&&1===z.length&&isLeafNode(Z))return Z;var ee=s&&s===this.ownerID,ie=U?Z?$:$^B:$|B,ae=U?Z?setIn(z,V,Z,ee):spliceOut(z,V,ee):spliceIn(z,V,Z,ee);return ee?(this.bitmap=ie,this.nodes=ae,this):new BitmapIndexedNode(s,ie,ae)},HashArrayMapNode.prototype.get=function(s,o,i,a){void 0===o&&(o=hash(i));var u=(0===s?o:o>>>s)&C,_=this.nodes[u];return _?_.get(s+w,o,i,a):a},HashArrayMapNode.prototype.update=function(s,o,i,a,u,_,x){void 0===i&&(i=hash(a));var L=(0===o?i:i>>>o)&C,B=u===j,$=this.nodes,U=$[L];if(B&&!U)return this;var V=updateNode(U,s,o+w,i,a,u,_,x);if(V===U)return this;var z=this.count;if(U){if(!V&&--z0&&a=0&&s>>o&C;if(a>=this.array.length)return new VNode([],s);var u,_=0===a;if(o>0){var x=this.array[a];if((u=x&&x.removeBefore(s,o-w,i))===x&&_)return this}if(_&&!u)return this;var j=editableVNode(this,s);if(!_)for(var L=0;L>>o&C;if(u>=this.array.length)return this;if(o>0){var _=this.array[u];if((a=_&&_.removeAfter(s,o-w,i))===_&&u===this.array.length-1)return this}var x=editableVNode(this,s);return x.array.splice(u+1),a&&(x.array[u]=a),x};var Xe,Qe,et={};function iterateList(s,o){var i=s._origin,a=s._capacity,u=getTailOffset(a),_=s._tail;return iterateNodeOrLeaf(s._root,s._level,0);function iterateNodeOrLeaf(s,o,i){return 0===o?iterateLeaf(s,i):iterateNode(s,o,i)}function iterateLeaf(s,w){var C=w===u?_&&_.array:s&&s.array,j=w>i?0:i-w,L=a-w;return L>x&&(L=x),function(){if(j===L)return et;var s=o?--L:j++;return C&&C[s]}}function iterateNode(s,u,_){var C,j=s&&s.array,L=_>i?0:i-_>>u,B=1+(a-_>>u);return B>x&&(B=x),function(){for(;;){if(C){var s=C();if(s!==et)return s;C=null}if(L===B)return et;var i=o?--B:L++;C=iterateNodeOrLeaf(j&&j[i],u-w,_+(i<=s.size||o<0)return s.withMutations((function(s){o<0?setListBounds(s,o).set(0,i):setListBounds(s,0,o+1).set(o,i)}));o+=s._origin;var a=s._tail,u=s._root,_=MakeRef(B);return o>=getTailOffset(s._capacity)?a=updateVNode(a,s.__ownerID,0,o,i,_):u=updateVNode(u,s.__ownerID,s._level,o,i,_),_.value?s.__ownerID?(s._root=u,s._tail=a,s.__hash=void 0,s.__altered=!0,s):makeList(s._origin,s._capacity,s._level,u,a):s}function updateVNode(s,o,i,a,u,_){var x,j=a>>>i&C,L=s&&j0){var B=s&&s.array[j],$=updateVNode(B,o,i-w,a,u,_);return $===B?s:((x=editableVNode(s,o)).array[j]=$,x)}return L&&s.array[j]===u?s:(SetRef(_),x=editableVNode(s,o),void 0===u&&j===x.array.length-1?x.array.pop():x.array[j]=u,x)}function editableVNode(s,o){return o&&s&&o===s.ownerID?s:new VNode(s?s.array.slice():[],o)}function listNodeFor(s,o){if(o>=getTailOffset(s._capacity))return s._tail;if(o<1<0;)i=i.array[o>>>a&C],a-=w;return i}}function setListBounds(s,o,i){void 0!==o&&(o|=0),void 0!==i&&(i|=0);var a=s.__ownerID||new OwnerID,u=s._origin,_=s._capacity,x=u+o,j=void 0===i?_:i<0?_+i:u+i;if(x===u&&j===_)return s;if(x>=j)return s.clear();for(var L=s._level,B=s._root,$=0;x+$<0;)B=new VNode(B&&B.array.length?[void 0,B]:[],a),$+=1<<(L+=w);$&&(x+=$,u+=$,j+=$,_+=$);for(var U=getTailOffset(_),V=getTailOffset(j);V>=1<U?new VNode([],a):z;if(z&&V>U&&x<_&&z.array.length){for(var Z=B=editableVNode(B,a),ee=L;ee>w;ee-=w){var ie=U>>>ee&C;Z=Z.array[ie]=editableVNode(Z.array[ie],a)}Z.array[U>>>w&C]=z}if(j<_&&(Y=Y&&Y.removeAfter(a,0,j)),x>=V)x-=V,j-=V,L=w,B=null,Y=Y&&Y.removeBefore(a,0,x);else if(x>u||V>>L&C;if(ae!==V>>>L&C)break;ae&&($+=(1<u&&(B=B.removeBefore(a,L,x-$)),B&&Vu&&(u=x.size),isIterable(w)||(x=x.map((function(s){return fromJS(s)}))),a.push(x)}return u>s.size&&(s=s.setSize(u)),mergeIntoCollectionWith(s,o,a)}function getTailOffset(s){return s>>w<=x&&w.size>=2*_.size?(a=(u=w.filter((function(s,o){return void 0!==s&&C!==o}))).toKeyedSeq().map((function(s){return s[0]})).flip().toMap(),s.__ownerID&&(a.__ownerID=u.__ownerID=s.__ownerID)):(a=_.remove(o),u=C===w.size-1?w.pop():w.set(C,void 0))}else if(L){if(i===w.get(C)[1])return s;a=_,u=w.set(C,[o,i])}else a=_.set(o,w.size),u=w.set(w.size,[o,i]);return s.__ownerID?(s.size=a.size,s._map=a,s._list=u,s.__hash=void 0,s):makeOrderedMap(a,u)}function ToKeyedSequence(s,o){this._iter=s,this._useKeys=o,this.size=s.size}function ToIndexedSequence(s){this._iter=s,this.size=s.size}function ToSetSequence(s){this._iter=s,this.size=s.size}function FromEntriesSequence(s){this._iter=s,this.size=s.size}function flipFactory(s){var o=makeSequence(s);return o._iter=s,o.size=s.size,o.flip=function(){return s},o.reverse=function(){var o=s.reverse.apply(this);return o.flip=function(){return s.reverse()},o},o.has=function(o){return s.includes(o)},o.includes=function(o){return s.has(o)},o.cacheResult=cacheResultThrough,o.__iterateUncached=function(o,i){var a=this;return s.__iterate((function(s,i){return!1!==o(i,s,a)}),i)},o.__iteratorUncached=function(o,i){if(o===V){var a=s.__iterator(o,i);return new Iterator((function(){var s=a.next();if(!s.done){var o=s.value[0];s.value[0]=s.value[1],s.value[1]=o}return s}))}return s.__iterator(o===U?$:U,i)},o}function mapFactory(s,o,i){var a=makeSequence(s);return a.size=s.size,a.has=function(o){return s.has(o)},a.get=function(a,u){var _=s.get(a,j);return _===j?u:o.call(i,_,a,s)},a.__iterateUncached=function(a,u){var _=this;return s.__iterate((function(s,u,w){return!1!==a(o.call(i,s,u,w),u,_)}),u)},a.__iteratorUncached=function(a,u){var _=s.__iterator(V,u);return new Iterator((function(){var u=_.next();if(u.done)return u;var w=u.value,x=w[0];return iteratorValue(a,x,o.call(i,w[1],x,s),u)}))},a}function reverseFactory(s,o){var i=makeSequence(s);return i._iter=s,i.size=s.size,i.reverse=function(){return s},s.flip&&(i.flip=function(){var o=flipFactory(s);return o.reverse=function(){return s.flip()},o}),i.get=function(i,a){return s.get(o?i:-1-i,a)},i.has=function(i){return s.has(o?i:-1-i)},i.includes=function(o){return s.includes(o)},i.cacheResult=cacheResultThrough,i.__iterate=function(o,i){var a=this;return s.__iterate((function(s,i){return o(s,i,a)}),!i)},i.__iterator=function(o,i){return s.__iterator(o,!i)},i}function filterFactory(s,o,i,a){var u=makeSequence(s);return a&&(u.has=function(a){var u=s.get(a,j);return u!==j&&!!o.call(i,u,a,s)},u.get=function(a,u){var _=s.get(a,j);return _!==j&&o.call(i,_,a,s)?_:u}),u.__iterateUncached=function(u,_){var w=this,x=0;return s.__iterate((function(s,_,C){if(o.call(i,s,_,C))return x++,u(s,a?_:x-1,w)}),_),x},u.__iteratorUncached=function(u,_){var w=s.__iterator(V,_),x=0;return new Iterator((function(){for(;;){var _=w.next();if(_.done)return _;var C=_.value,j=C[0],L=C[1];if(o.call(i,L,j,s))return iteratorValue(u,a?j:x++,L,_)}}))},u}function countByFactory(s,o,i){var a=Map().asMutable();return s.__iterate((function(u,_){a.update(o.call(i,u,_,s),0,(function(s){return s+1}))})),a.asImmutable()}function groupByFactory(s,o,i){var a=isKeyed(s),u=(isOrdered(s)?OrderedMap():Map()).asMutable();s.__iterate((function(_,w){u.update(o.call(i,_,w,s),(function(s){return(s=s||[]).push(a?[w,_]:_),s}))}));var _=iterableClass(s);return u.map((function(o){return reify(s,_(o))}))}function sliceFactory(s,o,i,a){var u=s.size;if(void 0!==o&&(o|=0),void 0!==i&&(i===1/0?i=u:i|=0),wholeSlice(o,i,u))return s;var _=resolveBegin(o,u),w=resolveEnd(i,u);if(_!=_||w!=w)return sliceFactory(s.toSeq().cacheResult(),o,i,a);var x,C=w-_;C==C&&(x=C<0?0:C);var j=makeSequence(s);return j.size=0===x?x:s.size&&x||void 0,!a&&isSeq(s)&&x>=0&&(j.get=function(o,i){return(o=wrapIndex(this,o))>=0&&ox)return iteratorDone();var s=u.next();return a||o===U?s:iteratorValue(o,C-1,o===$?void 0:s.value[1],s)}))},j}function takeWhileFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterate(a,u);var w=0;return s.__iterate((function(s,u,x){return o.call(i,s,u,x)&&++w&&a(s,u,_)})),w},a.__iteratorUncached=function(a,u){var _=this;if(u)return this.cacheResult().__iterator(a,u);var w=s.__iterator(V,u),x=!0;return new Iterator((function(){if(!x)return iteratorDone();var s=w.next();if(s.done)return s;var u=s.value,C=u[0],j=u[1];return o.call(i,j,C,_)?a===V?s:iteratorValue(a,C,j,s):(x=!1,iteratorDone())}))},a}function skipWhileFactory(s,o,i,a){var u=makeSequence(s);return u.__iterateUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterate(u,_);var x=!0,C=0;return s.__iterate((function(s,_,j){if(!x||!(x=o.call(i,s,_,j)))return C++,u(s,a?_:C-1,w)})),C},u.__iteratorUncached=function(u,_){var w=this;if(_)return this.cacheResult().__iterator(u,_);var x=s.__iterator(V,_),C=!0,j=0;return new Iterator((function(){var s,_,L;do{if((s=x.next()).done)return a||u===U?s:iteratorValue(u,j++,u===$?void 0:s.value[1],s);var B=s.value;_=B[0],L=B[1],C&&(C=o.call(i,L,_,w))}while(C);return u===V?s:iteratorValue(u,_,L,s)}))},u}function concatFactory(s,o){var i=isKeyed(s),a=[s].concat(o).map((function(s){return isIterable(s)?i&&(s=KeyedIterable(s)):s=i?keyedSeqFromValue(s):indexedSeqFromValue(Array.isArray(s)?s:[s]),s})).filter((function(s){return 0!==s.size}));if(0===a.length)return s;if(1===a.length){var u=a[0];if(u===s||i&&isKeyed(u)||isIndexed(s)&&isIndexed(u))return u}var _=new ArraySeq(a);return i?_=_.toKeyedSeq():isIndexed(s)||(_=_.toSetSeq()),(_=_.flatten(!0)).size=a.reduce((function(s,o){if(void 0!==s){var i=o.size;if(void 0!==i)return s+i}}),0),_}function flattenFactory(s,o,i){var a=makeSequence(s);return a.__iterateUncached=function(a,u){var _=0,w=!1;function flatDeep(s,x){var C=this;s.__iterate((function(s,u){return(!o||x0}function zipWithFactory(s,o,i){var a=makeSequence(s);return a.size=new ArraySeq(i).map((function(s){return s.size})).min(),a.__iterate=function(s,o){for(var i,a=this.__iterator(U,o),u=0;!(i=a.next()).done&&!1!==s(i.value,u++,this););return u},a.__iteratorUncached=function(s,a){var u=i.map((function(s){return s=Iterable(s),getIterator(a?s.reverse():s)})),_=0,w=!1;return new Iterator((function(){var i;return w||(i=u.map((function(s){return s.next()})),w=i.some((function(s){return s.done}))),w?iteratorDone():iteratorValue(s,_++,o.apply(null,i.map((function(s){return s.value}))))}))},a}function reify(s,o){return isSeq(s)?o:s.constructor(o)}function validateEntry(s){if(s!==Object(s))throw new TypeError("Expected [K, V] tuple: "+s)}function resolveSize(s){return assertNotInfinite(s.size),ensureSize(s)}function iterableClass(s){return isKeyed(s)?KeyedIterable:isIndexed(s)?IndexedIterable:SetIterable}function makeSequence(s){return Object.create((isKeyed(s)?KeyedSeq:isIndexed(s)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(s,o){return s>o?1:s=0;i--)o={value:arguments[i],next:o};return this.__ownerID?(this.size=s,this._head=o,this.__hash=void 0,this.__altered=!0,this):makeStack(s,o)},Stack.prototype.pushAll=function(s){if(0===(s=IndexedIterable(s)).size)return this;assertNotInfinite(s.size);var o=this.size,i=this._head;return s.reverse().forEach((function(s){o++,i={value:s,next:i}})),this.__ownerID?(this.size=o,this._head=i,this.__hash=void 0,this.__altered=!0,this):makeStack(o,i)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(s){return this.pushAll(s)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(s,o){if(wholeSlice(s,o,this.size))return this;var i=resolveBegin(s,this.size);if(resolveEnd(o,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,s,o);for(var a=this.size-i,u=this._head;i--;)u=u.next;return this.__ownerID?(this.size=a,this._head=u,this.__hash=void 0,this.__altered=!0,this):makeStack(a,u)},Stack.prototype.__ensureOwner=function(s){return s===this.__ownerID?this:s?makeStack(this.size,this._head,s,this.__hash):(this.__ownerID=s,this.__altered=!1,this)},Stack.prototype.__iterate=function(s,o){if(o)return this.reverse().__iterate(s);for(var i=0,a=this._head;a&&!1!==s(a.value,i++,this);)a=a.next;return i},Stack.prototype.__iterator=function(s,o){if(o)return this.reverse().__iterator(s);var i=0,a=this._head;return new Iterator((function(){if(a){var o=a.value;return a=a.next,iteratorValue(s,i++,o)}return iteratorDone()}))},Stack.isStack=isStack;var at,ct="@@__IMMUTABLE_STACK__@@",lt=Stack.prototype;function makeStack(s,o,i,a){var u=Object.create(lt);return u.size=s,u._head=o,u.__ownerID=i,u.__hash=a,u.__altered=!1,u}function emptyStack(){return at||(at=makeStack(0))}function mixin(s,o){var keyCopier=function(i){s.prototype[i]=o[i]};return Object.keys(o).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(o).forEach(keyCopier),s}function isProtoKey(s){return"string"==typeof s&&("__proto__"===s||"constructor"===s)}lt[ct]=!0,lt.withMutations=$e.withMutations,lt.asMutable=$e.asMutable,lt.asImmutable=$e.asImmutable,lt.wasAltered=$e.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var s=new Array(this.size||0);return this.valueSeq().__iterate((function(o,i){s[i]=o})),s},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(s){return s&&"function"==typeof s.toJS?s.toJS():s})).__toJS()},toJSON:function(){return this.toSeq().map((function(s){return s&&"function"==typeof s.toJSON?s.toJSON():s})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var s={};return this.__iterate((function(o,i){isProtoKey(i)||(s[i]=o)})),s},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(s,o){return 0===this.size?s+o:s+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+o},concat:function(){return reify(this,concatFactory(this,s.call(arguments,0)))},includes:function(s){return this.some((function(o){return is(o,s)}))},entries:function(){return this.__iterator(V)},every:function(s,o){assertNotInfinite(this.size);var i=!0;return this.__iterate((function(a,u,_){if(!s.call(o,a,u,_))return i=!1,!1})),i},filter:function(s,o){return reify(this,filterFactory(this,s,o,!0))},find:function(s,o,i){var a=this.findEntry(s,o);return a?a[1]:i},forEach:function(s,o){return assertNotInfinite(this.size),this.__iterate(o?s.bind(o):s)},join:function(s){assertNotInfinite(this.size),s=void 0!==s?""+s:",";var o="",i=!0;return this.__iterate((function(a){i?i=!1:o+=s,o+=null!=a?a.toString():""})),o},keys:function(){return this.__iterator($)},map:function(s,o){return reify(this,mapFactory(this,s,o))},reduce:function(s,o,i){var a,u;return assertNotInfinite(this.size),arguments.length<2?u=!0:a=o,this.__iterate((function(o,_,w){u?(u=!1,a=o):a=s.call(i,a,o,_,w)})),a},reduceRight:function(s,o,i){var a=this.toKeyedSeq().reverse();return a.reduce.apply(a,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!0))},some:function(s,o){return!this.every(not(s),o)},sort:function(s){return reify(this,sortFactory(this,s))},values:function(){return this.__iterator(U)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(s,o){return ensureSize(s?this.toSeq().filter(s,o):this)},countBy:function(s,o){return countByFactory(this,s,o)},equals:function(s){return deepEqual(this,s)},entrySeq:function(){var s=this;if(s._cache)return new ArraySeq(s._cache);var o=s.toSeq().map(entryMapper).toIndexedSeq();return o.fromEntrySeq=function(){return s.toSeq()},o},filterNot:function(s,o){return this.filter(not(s),o)},findEntry:function(s,o,i){var a=i;return this.__iterate((function(i,u,_){if(s.call(o,i,u,_))return a=[u,i],!1})),a},findKey:function(s,o){var i=this.findEntry(s,o);return i&&i[0]},findLast:function(s,o,i){return this.toKeyedSeq().reverse().find(s,o,i)},findLastEntry:function(s,o,i){return this.toKeyedSeq().reverse().findEntry(s,o,i)},findLastKey:function(s,o){return this.toKeyedSeq().reverse().findKey(s,o)},first:function(){return this.find(returnTrue)},flatMap:function(s,o){return reify(this,flatMapFactory(this,s,o))},flatten:function(s){return reify(this,flattenFactory(this,s,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(s,o){return this.find((function(o,i){return is(i,s)}),void 0,o)},getIn:function(s,o){for(var i,a=this,u=forceIterator(s);!(i=u.next()).done;){var _=i.value;if((a=a&&a.get?a.get(_,j):j)===j)return o}return a},groupBy:function(s,o){return groupByFactory(this,s,o)},has:function(s){return this.get(s,j)!==j},hasIn:function(s){return this.getIn(s,j)!==j},isSubset:function(s){return s="function"==typeof s.includes?s:Iterable(s),this.every((function(o){return s.includes(o)}))},isSuperset:function(s){return(s="function"==typeof s.isSubset?s:Iterable(s)).isSubset(this)},keyOf:function(s){return this.findKey((function(o){return is(o,s)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(s){return this.toKeyedSeq().reverse().keyOf(s)},max:function(s){return maxFactory(this,s)},maxBy:function(s,o){return maxFactory(this,o,s)},min:function(s){return maxFactory(this,s?neg(s):defaultNegComparator)},minBy:function(s,o){return maxFactory(this,o?neg(o):defaultNegComparator,s)},rest:function(){return this.slice(1)},skip:function(s){return this.slice(Math.max(0,s))},skipLast:function(s){return reify(this,this.toSeq().reverse().skip(s).reverse())},skipWhile:function(s,o){return reify(this,skipWhileFactory(this,s,o,!0))},skipUntil:function(s,o){return this.skipWhile(not(s),o)},sortBy:function(s,o){return reify(this,sortFactory(this,o,s))},take:function(s){return this.slice(0,Math.max(0,s))},takeLast:function(s){return reify(this,this.toSeq().reverse().take(s).reverse())},takeWhile:function(s,o){return reify(this,takeWhileFactory(this,s,o))},takeUntil:function(s,o){return this.takeWhile(not(s),o)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var ut=Iterable.prototype;ut[o]=!0,ut[Z]=ut.values,ut.__toJS=ut.toArray,ut.__toStringMapper=quoteString,ut.inspect=ut.toSource=function(){return this.toString()},ut.chain=ut.flatMap,ut.contains=ut.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(s,o){var i=this,a=0;return reify(this,this.toSeq().map((function(u,_){return s.call(o,[_,u],a++,i)})).fromEntrySeq())},mapKeys:function(s,o){var i=this;return reify(this,this.toSeq().flip().map((function(a,u){return s.call(o,a,u,i)})).flip())}});var pt=KeyedIterable.prototype;function keyMapper(s,o){return o}function entryMapper(s,o){return[o,s]}function not(s){return function(){return!s.apply(this,arguments)}}function neg(s){return function(){return-s.apply(this,arguments)}}function quoteString(s){return"string"==typeof s?JSON.stringify(s):String(s)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(s,o){return so?-1:0}function hashIterable(s){if(s.size===1/0)return 0;var o=isOrdered(s),i=isKeyed(s),a=o?1:0;return murmurHashOfSize(s.__iterate(i?o?function(s,o){a=31*a+hashMerge(hash(s),hash(o))|0}:function(s,o){a=a+hashMerge(hash(s),hash(o))|0}:o?function(s){a=31*a+hash(s)|0}:function(s){a=a+hash(s)|0}),a)}function murmurHashOfSize(s,o){return o=le(o,3432918353),o=le(o<<15|o>>>-15,461845907),o=le(o<<13|o>>>-13,5),o=le((o=o+3864292196^s)^o>>>16,2246822507),o=smi((o=le(o^o>>>13,3266489909))^o>>>16)}function hashMerge(s,o){return s^o+2654435769+(s<<6)+(s>>2)}return pt[i]=!0,pt[Z]=ut.entries,pt.__toJS=ut.toObject,pt.__toStringMapper=function(s,o){return JSON.stringify(o)+": "+quoteString(s)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(s,o){return reify(this,filterFactory(this,s,o,!1))},findIndex:function(s,o){var i=this.findEntry(s,o);return i?i[0]:-1},indexOf:function(s){var o=this.keyOf(s);return void 0===o?-1:o},lastIndexOf:function(s){var o=this.lastKeyOf(s);return void 0===o?-1:o},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(s,o){return reify(this,sliceFactory(this,s,o,!1))},splice:function(s,o){var i=arguments.length;if(o=Math.max(0|o,0),0===i||2===i&&!o)return this;s=resolveBegin(s,s<0?this.count():this.size);var a=this.slice(0,s);return reify(this,1===i?a:a.concat(arrCopy(arguments,2),this.slice(s+o)))},findLastIndex:function(s,o){var i=this.findLastEntry(s,o);return i?i[0]:-1},first:function(){return this.get(0)},flatten:function(s){return reify(this,flattenFactory(this,s,!1))},get:function(s,o){return(s=wrapIndex(this,s))<0||this.size===1/0||void 0!==this.size&&s>this.size?o:this.find((function(o,i){return i===s}),void 0,o)},has:function(s){return(s=wrapIndex(this,s))>=0&&(void 0!==this.size?this.size===1/0||s=o||i<0||B&&s-j>=_}function timerExpired(){var s=now();if(shouldInvoke(s))return trailingEdge(s);x=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-C);return B?U(i,_-(s-j)):i}(s))}function trailingEdge(s){return x=void 0,V&&a?invokeFunc(s):(a=u=void 0,w)}function debounced(){var s=now(),i=shouldInvoke(s);if(a=arguments,u=this,C=s,i){if(void 0===x)return function leadingEdge(s){return j=s,x=setTimeout(timerExpired,o),L?invokeFunc(s):w}(C);if(B)return x=setTimeout(timerExpired,o),invokeFunc(C)}return void 0===x&&(x=setTimeout(timerExpired,o)),w}return o=toNumber(o)||0,isObject(i)&&(L=!!i.leading,_=(B="maxWait"in i)?$(toNumber(i.maxWait)||0,o):_,V="trailing"in i?!!i.trailing:V),debounced.cancel=function cancel(){void 0!==x&&clearTimeout(x),j=0,a=C=u=x=void 0},debounced.flush=function flush(){return void 0===x?w:trailingEdge(now())},debounced}},55580(s,o,i){var a=i(56110)(i(9325),"DataView");s.exports=a},21549(s,o,i){var a=i(22032),u=i(63862),_=i(66721),w=i(12749),x=i(35749);function Hash(s){var o=-1,i=null==s?0:s.length;for(this.clear();++o-1}},70695(s,o,i){var a=i(78096),u=i(72428),_=i(56449),w=i(3656),x=i(30361),C=i(37167),j=Object.prototype.hasOwnProperty;s.exports=function arrayLikeKeys(s,o){var i=_(s),L=!i&&u(s),B=!i&&!L&&w(s),$=!i&&!L&&!B&&C(s),U=i||L||B||$,V=U?a(s.length,String):[],z=V.length;for(var Y in s)!o&&!j.call(s,Y)||U&&("length"==Y||B&&("offset"==Y||"parent"==Y)||$&&("buffer"==Y||"byteLength"==Y||"byteOffset"==Y)||x(Y,z))||V.push(Y);return V}},34932(s){s.exports=function arrayMap(s,o){for(var i=-1,a=null==s?0:s.length,u=Array(a);++i0&&i(j)?o>1?baseFlatten(j,o-1,i,_,w):a(w,j):_||(w[w.length]=j)}return w}},86649(s,o,i){var a=i(83221)();s.exports=a},30641(s,o,i){var a=i(86649),u=i(95950);s.exports=function baseForOwn(s,o){return s&&a(s,o,u)}},47422(s,o,i){var a=i(31769),u=i(77797);s.exports=function baseGet(s,o){for(var i=0,_=(o=a(o,s)).length;null!=s&&i<_;)s=s[u(o[i++])];return i&&i==_?s:void 0}},82199(s,o,i){var a=i(14528),u=i(56449);s.exports=function baseGetAllKeys(s,o,i){var _=o(s);return u(s)?_:a(_,i(s))}},72552(s,o,i){var a=i(51873),u=i(659),_=i(59350),w=a?a.toStringTag:void 0;s.exports=function baseGetTag(s){return null==s?void 0===s?"[object Undefined]":"[object Null]":w&&w in Object(s)?u(s):_(s)}},20426(s){var o=Object.prototype.hasOwnProperty;s.exports=function baseHas(s,i){return null!=s&&o.call(s,i)}},28077(s){s.exports=function baseHasIn(s,o){return null!=s&&o in Object(s)}},96131(s,o,i){var a=i(2523),u=i(85463),_=i(76959);s.exports=function baseIndexOf(s,o,i){return o==o?_(s,o,i):a(s,u,i)}},27534(s,o,i){var a=i(72552),u=i(40346);s.exports=function baseIsArguments(s){return u(s)&&"[object Arguments]"==a(s)}},60270(s,o,i){var a=i(87068),u=i(40346);s.exports=function baseIsEqual(s,o,i,_,w){return s===o||(null==s||null==o||!u(s)&&!u(o)?s!=s&&o!=o:a(s,o,i,_,baseIsEqual,w))}},87068(s,o,i){var a=i(37217),u=i(25911),_=i(21986),w=i(50689),x=i(5861),C=i(56449),j=i(3656),L=i(37167),B="[object Arguments]",$="[object Array]",U="[object Object]",V=Object.prototype.hasOwnProperty;s.exports=function baseIsEqualDeep(s,o,i,z,Y,Z){var ee=C(s),ie=C(o),ae=ee?$:x(s),ce=ie?$:x(o),le=(ae=ae==B?U:ae)==U,pe=(ce=ce==B?U:ce)==U,de=ae==ce;if(de&&j(s)){if(!j(o))return!1;ee=!0,le=!1}if(de&&!le)return Z||(Z=new a),ee||L(s)?u(s,o,i,z,Y,Z):_(s,o,ae,i,z,Y,Z);if(!(1&i)){var fe=le&&V.call(s,"__wrapped__"),ye=pe&&V.call(o,"__wrapped__");if(fe||ye){var be=fe?s.value():s,Se=ye?o.value():o;return Z||(Z=new a),Y(be,Se,i,z,Z)}}return!!de&&(Z||(Z=new a),w(s,o,i,z,Y,Z))}},29172(s,o,i){var a=i(5861),u=i(40346);s.exports=function baseIsMap(s){return u(s)&&"[object Map]"==a(s)}},41799(s,o,i){var a=i(37217),u=i(60270);s.exports=function baseIsMatch(s,o,i,_){var w=i.length,x=w,C=!_;if(null==s)return!x;for(s=Object(s);w--;){var j=i[w];if(C&&j[2]?j[1]!==s[j[0]]:!(j[0]in s))return!1}for(;++wu?0:u+o),(i=i>u?u:i)<0&&(i+=u),u=o>i?0:i-o>>>0,o>>>=0;for(var _=Array(u);++a=u?s:a(s,o,i)}},49653(s,o,i){var a=i(37828);s.exports=function cloneArrayBuffer(s){var o=new s.constructor(s.byteLength);return new a(o).set(new a(s)),o}},93290(s,o,i){s=i.nmd(s);var a=i(9325),u=o&&!o.nodeType&&o,_=u&&s&&!s.nodeType&&s,w=_&&_.exports===u?a.Buffer:void 0,x=w?w.allocUnsafe:void 0;s.exports=function cloneBuffer(s,o){if(o)return s.slice();var i=s.length,a=x?x(i):new s.constructor(i);return s.copy(a),a}},76169(s,o,i){var a=i(49653);s.exports=function cloneDataView(s,o){var i=o?a(s.buffer):s.buffer;return new s.constructor(i,s.byteOffset,s.byteLength)}},73201(s){var o=/\w*$/;s.exports=function cloneRegExp(s){var i=new s.constructor(s.source,o.exec(s));return i.lastIndex=s.lastIndex,i}},93736(s,o,i){var a=i(51873),u=a?a.prototype:void 0,_=u?u.valueOf:void 0;s.exports=function cloneSymbol(s){return _?Object(_.call(s)):{}}},71961(s,o,i){var a=i(49653);s.exports=function cloneTypedArray(s,o){var i=o?a(s.buffer):s.buffer;return new s.constructor(i,s.byteOffset,s.length)}},91596(s){var o=Math.max;s.exports=function composeArgs(s,i,a,u){for(var _=-1,w=s.length,x=a.length,C=-1,j=i.length,L=o(w-x,0),B=Array(j+L),$=!u;++C1?i[_-1]:void 0,x=_>2?i[2]:void 0;for(w=s.length>3&&"function"==typeof w?(_--,w):void 0,x&&u(i[0],i[1],x)&&(w=_<3?void 0:w,_=1),o=Object(o);++a<_;){var C=i[a];C&&s(o,C,a,w)}return o}))}},38329(s,o,i){var a=i(64894);s.exports=function createBaseEach(s,o){return function(i,u){if(null==i)return i;if(!a(i))return s(i,u);for(var _=i.length,w=o?_:-1,x=Object(i);(o?w--:++w<_)&&!1!==u(x[w],w,x););return i}}},83221(s){s.exports=function createBaseFor(s){return function(o,i,a){for(var u=-1,_=Object(o),w=a(o),x=w.length;x--;){var C=w[s?x:++u];if(!1===i(_[C],C,_))break}return o}}},11842(s,o,i){var a=i(82819),u=i(9325);s.exports=function createBind(s,o,i){var _=1&o,w=a(s);return function wrapper(){return(this&&this!==u&&this instanceof wrapper?w:s).apply(_?i:this,arguments)}}},12507(s,o,i){var a=i(28754),u=i(49698),_=i(63912),w=i(13222);s.exports=function createCaseFirst(s){return function(o){o=w(o);var i=u(o)?_(o):void 0,x=i?i[0]:o.charAt(0),C=i?a(i,1).join(""):o.slice(1);return x[s]()+C}}},45539(s,o,i){var a=i(40882),u=i(50828),_=i(66645),w=RegExp("['’]","g");s.exports=function createCompounder(s){return function(o){return a(_(u(o).replace(w,"")),s,"")}}},82819(s,o,i){var a=i(39344),u=i(23805);s.exports=function createCtor(s){return function(){var o=arguments;switch(o.length){case 0:return new s;case 1:return new s(o[0]);case 2:return new s(o[0],o[1]);case 3:return new s(o[0],o[1],o[2]);case 4:return new s(o[0],o[1],o[2],o[3]);case 5:return new s(o[0],o[1],o[2],o[3],o[4]);case 6:return new s(o[0],o[1],o[2],o[3],o[4],o[5]);case 7:return new s(o[0],o[1],o[2],o[3],o[4],o[5],o[6])}var i=a(s.prototype),_=s.apply(i,o);return u(_)?_:i}}},77078(s,o,i){var a=i(91033),u=i(82819),_=i(37471),w=i(18073),x=i(11287),C=i(36306),j=i(9325);s.exports=function createCurry(s,o,i){var L=u(s);return function wrapper(){for(var u=arguments.length,B=Array(u),$=u,U=x(wrapper);$--;)B[$]=arguments[$];var V=u<3&&B[0]!==U&&B[u-1]!==U?[]:C(B,U);return(u-=V.length)-1?x[C?o[j]:j]:void 0}}},37471(s,o,i){var a=i(91596),u=i(53320),_=i(58523),w=i(82819),x=i(18073),C=i(11287),j=i(68294),L=i(36306),B=i(9325);s.exports=function createHybrid(s,o,i,$,U,V,z,Y,Z,ee){var ie=128&o,ae=1&o,ce=2&o,le=24&o,pe=512&o,de=ce?void 0:w(s);return function wrapper(){for(var fe=arguments.length,ye=Array(fe),be=fe;be--;)ye[be]=arguments[be];if(le)var Se=C(wrapper),_e=_(ye,Se);if($&&(ye=a(ye,$,U,le)),V&&(ye=u(ye,V,z,le)),fe-=_e,le&&fe1&&ye.reverse(),ie&&ZL))return!1;var $=C.get(s),U=C.get(o);if($&&U)return $==o&&U==s;var V=-1,z=!0,Y=2&i?new a:void 0;for(C.set(s,o),C.set(o,s);++V1?"& ":"")+i[u],i=i.join(a>2?", ":" "),s.replace(o,"{\n/* [wrapped with "+i+"] */\n")}},45891(s,o,i){var a=i(51873),u=i(72428),_=i(56449),w=a?a.isConcatSpreadable:void 0;s.exports=function isFlattenable(s){return _(s)||u(s)||!!(w&&s&&s[w])}},30361(s){var o=/^(?:0|[1-9]\d*)$/;s.exports=function isIndex(s,i){var a=typeof s;return!!(i=null==i?9007199254740991:i)&&("number"==a||"symbol"!=a&&o.test(s))&&s>-1&&s%1==0&&s-1}},31175(s,o,i){var a=i(26025);s.exports=function listCacheSet(s,o){var i=this.__data__,u=a(i,s);return u<0?(++this.size,i.push([s,o])):i[u][1]=o,this}},63040(s,o,i){var a=i(21549),u=i(80079),_=i(68223);s.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new a,map:new(_||u),string:new a}}},17670(s,o,i){var a=i(12651);s.exports=function mapCacheDelete(s){var o=a(this,s).delete(s);return this.size-=o?1:0,o}},90289(s,o,i){var a=i(12651);s.exports=function mapCacheGet(s){return a(this,s).get(s)}},4509(s,o,i){var a=i(12651);s.exports=function mapCacheHas(s){return a(this,s).has(s)}},72949(s,o,i){var a=i(12651);s.exports=function mapCacheSet(s,o){var i=a(this,s),u=i.size;return i.set(s,o),this.size+=i.size==u?0:1,this}},20317(s){s.exports=function mapToArray(s){var o=-1,i=Array(s.size);return s.forEach((function(s,a){i[++o]=[a,s]})),i}},67197(s){s.exports=function matchesStrictComparable(s,o){return function(i){return null!=i&&(i[s]===o&&(void 0!==o||s in Object(i)))}}},62224(s,o,i){var a=i(50104);s.exports=function memoizeCapped(s){var o=a(s,(function(s){return 500===i.size&&i.clear(),s})),i=o.cache;return o}},3209(s,o,i){var a=i(91596),u=i(53320),_=i(36306),w="__lodash_placeholder__",x=128,C=Math.min;s.exports=function mergeData(s,o){var i=s[1],j=o[1],L=i|j,B=L<131,$=j==x&&8==i||j==x&&256==i&&s[7].length<=o[8]||384==j&&o[7].length<=o[8]&&8==i;if(!B&&!$)return s;1&j&&(s[2]=o[2],L|=1&i?0:4);var U=o[3];if(U){var V=s[3];s[3]=V?a(V,U,o[4]):U,s[4]=V?_(s[3],w):o[4]}return(U=o[5])&&(V=s[5],s[5]=V?u(V,U,o[6]):U,s[6]=V?_(s[5],w):o[6]),(U=o[7])&&(s[7]=U),j&x&&(s[8]=null==s[8]?o[8]:C(s[8],o[8])),null==s[9]&&(s[9]=o[9]),s[0]=o[0],s[1]=L,s}},48152(s,o,i){var a=i(28303),u=a&&new a;s.exports=u},81042(s,o,i){var a=i(56110)(Object,"create");s.exports=a},3650(s,o,i){var a=i(74335)(Object.keys,Object);s.exports=a},90181(s){s.exports=function nativeKeysIn(s){var o=[];if(null!=s)for(var i in Object(s))o.push(i);return o}},86009(s,o,i){s=i.nmd(s);var a=i(34840),u=o&&!o.nodeType&&o,_=u&&s&&!s.nodeType&&s,w=_&&_.exports===u&&a.process,x=function(){try{var s=_&&_.require&&_.require("util").types;return s||w&&w.binding&&w.binding("util")}catch(s){}}();s.exports=x},59350(s){var o=Object.prototype.toString;s.exports=function objectToString(s){return o.call(s)}},74335(s){s.exports=function overArg(s,o){return function(i){return s(o(i))}}},56757(s,o,i){var a=i(91033),u=Math.max;s.exports=function overRest(s,o,i){return o=u(void 0===o?s.length-1:o,0),function(){for(var _=arguments,w=-1,x=u(_.length-o,0),C=Array(x);++w0){if(++i>=800)return arguments[0]}else i=0;return s.apply(void 0,arguments)}}},51420(s,o,i){var a=i(80079);s.exports=function stackClear(){this.__data__=new a,this.size=0}},90938(s){s.exports=function stackDelete(s){var o=this.__data__,i=o.delete(s);return this.size=o.size,i}},63605(s){s.exports=function stackGet(s){return this.__data__.get(s)}},29817(s){s.exports=function stackHas(s){return this.__data__.has(s)}},80945(s,o,i){var a=i(80079),u=i(68223),_=i(53661);s.exports=function stackSet(s,o){var i=this.__data__;if(i instanceof a){var w=i.__data__;if(!u||w.length<199)return w.push([s,o]),this.size=++i.size,this;i=this.__data__=new _(w)}return i.set(s,o),this.size=i.size,this}},76959(s){s.exports=function strictIndexOf(s,o,i){for(var a=i-1,u=s.length;++a=o||i<0||Y&&s-V>=L}function timerExpired(){var s=u();if(shouldInvoke(s))return trailingEdge(s);$=setTimeout(timerExpired,function remainingWait(s){var i=o-(s-U);return Y?x(i,L-(s-V)):i}(s))}function trailingEdge(s){return $=void 0,Z&&C?invokeFunc(s):(C=j=void 0,B)}function debounced(){var s=u(),i=shouldInvoke(s);if(C=arguments,j=this,U=s,i){if(void 0===$)return function leadingEdge(s){return V=s,$=setTimeout(timerExpired,o),z?invokeFunc(s):B}(U);if(Y)return clearTimeout($),$=setTimeout(timerExpired,o),invokeFunc(U)}return void 0===$&&($=setTimeout(timerExpired,o)),B}return o=_(o)||0,a(i)&&(z=!!i.leading,L=(Y="maxWait"in i)?w(_(i.maxWait)||0,o):L,Z="trailing"in i?!!i.trailing:Z),debounced.cancel=function cancel(){void 0!==$&&clearTimeout($),V=0,C=U=j=$=void 0},debounced.flush=function flush(){return void 0===$?B:trailingEdge(u())},debounced}},50828(s,o,i){var a=i(24647),u=i(13222),_=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,w=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");s.exports=function deburr(s){return(s=u(s))&&s.replace(_,a).replace(w,"")}},75288(s){s.exports=function eq(s,o){return s===o||s!=s&&o!=o}},60680(s,o,i){var a=i(13222),u=/[\\^$.*+?()[\]{}|]/g,_=RegExp(u.source);s.exports=function escapeRegExp(s){return(s=a(s))&&_.test(s)?s.replace(u,"\\$&"):s}},7309(s,o,i){var a=i(62006)(i(24713));s.exports=a},24713(s,o,i){var a=i(2523),u=i(15389),_=i(61489),w=Math.max;s.exports=function findIndex(s,o,i){var x=null==s?0:s.length;if(!x)return-1;var C=null==i?0:_(i);return C<0&&(C=w(x+C,0)),a(s,u(o,3),C)}},35970(s,o,i){var a=i(83120);s.exports=function flatten(s){return(null==s?0:s.length)?a(s,1):[]}},73424(s,o,i){var a=i(16962),u=i(2874),_=Array.prototype.push;function baseAry(s,o){return 2==o?function(o,i){return s(o,i)}:function(o){return s(o)}}function cloneArray(s){for(var o=s?s.length:0,i=Array(o);o--;)i[o]=s[o];return i}function wrapImmutable(s,o){return function(){var i=arguments.length;if(i){for(var a=Array(i);i--;)a[i]=arguments[i];var u=a[0]=o.apply(void 0,a);return s.apply(void 0,a),u}}}s.exports=function baseConvert(s,o,i,w){var x="function"==typeof o,C=o===Object(o);if(C&&(w=i,i=o,o=void 0),null==i)throw new TypeError;w||(w={});var j=!("cap"in w)||w.cap,L=!("curry"in w)||w.curry,B=!("fixed"in w)||w.fixed,$=!("immutable"in w)||w.immutable,U=!("rearg"in w)||w.rearg,V=x?i:u,z="curry"in w&&w.curry,Y="fixed"in w&&w.fixed,Z="rearg"in w&&w.rearg,ee=x?i.runInContext():void 0,ie=x?i:{ary:s.ary,assign:s.assign,clone:s.clone,curry:s.curry,forEach:s.forEach,isArray:s.isArray,isError:s.isError,isFunction:s.isFunction,isWeakMap:s.isWeakMap,iteratee:s.iteratee,keys:s.keys,rearg:s.rearg,toInteger:s.toInteger,toPath:s.toPath},ae=ie.ary,ce=ie.assign,le=ie.clone,pe=ie.curry,de=ie.forEach,fe=ie.isArray,ye=ie.isError,be=ie.isFunction,Se=ie.isWeakMap,_e=ie.keys,we=ie.rearg,xe=ie.toInteger,Pe=ie.toPath,Te=_e(a.aryMethod),Re={castArray:function(s){return function(){var o=arguments[0];return fe(o)?s(cloneArray(o)):s.apply(void 0,arguments)}},iteratee:function(s){return function(){var o=arguments[1],i=s(arguments[0],o),a=i.length;return j&&"number"==typeof o?(o=o>2?o-2:1,a&&a<=o?i:baseAry(i,o)):i}},mixin:function(s){return function(o){var i=this;if(!be(i))return s(i,Object(o));var a=[];return de(_e(o),(function(s){be(o[s])&&a.push([s,i.prototype[s]])})),s(i,Object(o)),de(a,(function(s){var o=s[1];be(o)?i.prototype[s[0]]=o:delete i.prototype[s[0]]})),i}},nthArg:function(s){return function(o){var i=o<0?1:xe(o)+1;return pe(s(o),i)}},rearg:function(s){return function(o,i){var a=i?i.length:0;return pe(s(o,i),a)}},runInContext:function(o){return function(i){return baseConvert(s,o(i),w)}}};function castCap(s,o){if(j){var i=a.iterateeRearg[s];if(i)return function iterateeRearg(s,o){return overArg(s,(function(s){var i=o.length;return function baseArity(s,o){return 2==o?function(o,i){return s.apply(void 0,arguments)}:function(o){return s.apply(void 0,arguments)}}(we(baseAry(s,i),o),i)}))}(o,i);var u=!x&&a.iterateeAry[s];if(u)return function iterateeAry(s,o){return overArg(s,(function(s){return"function"==typeof s?baseAry(s,o):s}))}(o,u)}return o}function castFixed(s,o,i){if(B&&(Y||!a.skipFixed[s])){var u=a.methodSpread[s],w=u&&u.start;return void 0===w?ae(o,i):function flatSpread(s,o){return function(){for(var i=arguments.length,a=i-1,u=Array(i);i--;)u[i]=arguments[i];var w=u[o],x=u.slice(0,o);return w&&_.apply(x,w),o!=a&&_.apply(x,u.slice(o+1)),s.apply(this,x)}}(o,w)}return o}function castRearg(s,o,i){return U&&i>1&&(Z||!a.skipRearg[s])?we(o,a.methodRearg[s]||a.aryRearg[i]):o}function cloneByPath(s,o){for(var i=-1,a=(o=Pe(o)).length,u=a-1,_=le(Object(s)),w=_;null!=w&&++i1?pe(o,i):o}(0,u=castCap(_,u),s),!1}})),!u})),u||(u=w),u==o&&(u=z?pe(u,1):function(){return o.apply(this,arguments)}),u.convert=createConverter(_,o),u.placeholder=o.placeholder=i,u}if(!C)return wrap(o,i,V);var $e=i,qe=[];return de(Te,(function(s){de(a.aryMethod[s],(function(s){var o=$e[a.remap[s]||s];o&&qe.push([s,wrap(s,o,$e)])}))})),de(_e($e),(function(s){var o=$e[s];if("function"==typeof o){for(var i=qe.length;i--;)if(qe[i][0]==s)return;o.convert=createConverter(s,o),qe.push([s,o])}})),de(qe,(function(s){$e[s[0]]=s[1]})),$e.convert=function convertLib(s){return $e.runInContext.convert(s)(void 0)},$e.placeholder=$e,de(_e($e),(function(s){de(a.realToAlias[s]||[],(function(o){$e[o]=$e[s]}))})),$e}},16962(s,o){o.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},o.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},o.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},o.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},o.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},o.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},o.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},o.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},o.realToAlias=function(){var s=Object.prototype.hasOwnProperty,i=o.aliasToReal,a={};for(var u in i){var _=i[u];s.call(a,_)?a[_].push(u):a[_]=[u]}return a}(),o.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},o.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},o.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},47934(s,o,i){s.exports={ary:i(64626),assign:i(74733),clone:i(32629),curry:i(49747),forEach:i(83729),isArray:i(56449),isError:i(23546),isFunction:i(1882),isWeakMap:i(47886),iteratee:i(33855),keys:i(88984),rearg:i(84195),toInteger:i(61489),toPath:i(42072)}},56367(s,o,i){s.exports=i(77731)},79920(s,o,i){var a=i(73424),u=i(47934);s.exports=function convert(s,o,i){return a(u,s,o,i)}},2874(s){s.exports={}},77731(s,o,i){var a=i(79920)("set",i(63560));a.placeholder=i(2874),s.exports=a},58156(s,o,i){var a=i(47422);s.exports=function get(s,o,i){var u=null==s?void 0:a(s,o);return void 0===u?i:u}},61448(s,o,i){var a=i(20426),u=i(49326);s.exports=function has(s,o){return null!=s&&u(s,o,a)}},80631(s,o,i){var a=i(28077),u=i(49326);s.exports=function hasIn(s,o){return null!=s&&u(s,o,a)}},83488(s){s.exports=function identity(s){return s}},72428(s,o,i){var a=i(27534),u=i(40346),_=Object.prototype,w=_.hasOwnProperty,x=_.propertyIsEnumerable,C=a(function(){return arguments}())?a:function(s){return u(s)&&w.call(s,"callee")&&!x.call(s,"callee")};s.exports=C},56449(s){var o=Array.isArray;s.exports=o},64894(s,o,i){var a=i(1882),u=i(30294);s.exports=function isArrayLike(s){return null!=s&&u(s.length)&&!a(s)}},83693(s,o,i){var a=i(64894),u=i(40346);s.exports=function isArrayLikeObject(s){return u(s)&&a(s)}},53812(s,o,i){var a=i(72552),u=i(40346);s.exports=function isBoolean(s){return!0===s||!1===s||u(s)&&"[object Boolean]"==a(s)}},3656(s,o,i){s=i.nmd(s);var a=i(9325),u=i(89935),_=o&&!o.nodeType&&o,w=_&&s&&!s.nodeType&&s,x=w&&w.exports===_?a.Buffer:void 0,C=(x?x.isBuffer:void 0)||u;s.exports=C},62193(s,o,i){var a=i(88984),u=i(5861),_=i(72428),w=i(56449),x=i(64894),C=i(3656),j=i(55527),L=i(37167),B=Object.prototype.hasOwnProperty;s.exports=function isEmpty(s){if(null==s)return!0;if(x(s)&&(w(s)||"string"==typeof s||"function"==typeof s.splice||C(s)||L(s)||_(s)))return!s.length;var o=u(s);if("[object Map]"==o||"[object Set]"==o)return!s.size;if(j(s))return!a(s).length;for(var i in s)if(B.call(s,i))return!1;return!0}},2404(s,o,i){var a=i(60270);s.exports=function isEqual(s,o){return a(s,o)}},23546(s,o,i){var a=i(72552),u=i(40346),_=i(11331);s.exports=function isError(s){if(!u(s))return!1;var o=a(s);return"[object Error]"==o||"[object DOMException]"==o||"string"==typeof s.message&&"string"==typeof s.name&&!_(s)}},1882(s,o,i){var a=i(72552),u=i(23805);s.exports=function isFunction(s){if(!u(s))return!1;var o=a(s);return"[object Function]"==o||"[object GeneratorFunction]"==o||"[object AsyncFunction]"==o||"[object Proxy]"==o}},30294(s){s.exports=function isLength(s){return"number"==typeof s&&s>-1&&s%1==0&&s<=9007199254740991}},87730(s,o,i){var a=i(29172),u=i(27301),_=i(86009),w=_&&_.isMap,x=w?u(w):a;s.exports=x},5187(s){s.exports=function isNull(s){return null===s}},98023(s,o,i){var a=i(72552),u=i(40346);s.exports=function isNumber(s){return"number"==typeof s||u(s)&&"[object Number]"==a(s)}},23805(s){s.exports=function isObject(s){var o=typeof s;return null!=s&&("object"==o||"function"==o)}},40346(s){s.exports=function isObjectLike(s){return null!=s&&"object"==typeof s}},11331(s,o,i){var a=i(72552),u=i(28879),_=i(40346),w=Function.prototype,x=Object.prototype,C=w.toString,j=x.hasOwnProperty,L=C.call(Object);s.exports=function isPlainObject(s){if(!_(s)||"[object Object]"!=a(s))return!1;var o=u(s);if(null===o)return!0;var i=j.call(o,"constructor")&&o.constructor;return"function"==typeof i&&i instanceof i&&C.call(i)==L}},38440(s,o,i){var a=i(16038),u=i(27301),_=i(86009),w=_&&_.isSet,x=w?u(w):a;s.exports=x},85015(s,o,i){var a=i(72552),u=i(56449),_=i(40346);s.exports=function isString(s){return"string"==typeof s||!u(s)&&_(s)&&"[object String]"==a(s)}},44394(s,o,i){var a=i(72552),u=i(40346);s.exports=function isSymbol(s){return"symbol"==typeof s||u(s)&&"[object Symbol]"==a(s)}},37167(s,o,i){var a=i(4901),u=i(27301),_=i(86009),w=_&&_.isTypedArray,x=w?u(w):a;s.exports=x},47886(s,o,i){var a=i(5861),u=i(40346);s.exports=function isWeakMap(s){return u(s)&&"[object WeakMap]"==a(s)}},33855(s,o,i){var a=i(9999),u=i(15389);s.exports=function iteratee(s){return u("function"==typeof s?s:a(s,1))}},95950(s,o,i){var a=i(70695),u=i(88984),_=i(64894);s.exports=function keys(s){return _(s)?a(s):u(s)}},37241(s,o,i){var a=i(70695),u=i(72903),_=i(64894);s.exports=function keysIn(s){return _(s)?a(s,!0):u(s)}},68090(s){s.exports=function last(s){var o=null==s?0:s.length;return o?s[o-1]:void 0}},50104(s,o,i){var a=i(53661);function memoize(s,o){if("function"!=typeof s||null!=o&&"function"!=typeof o)throw new TypeError("Expected a function");var memoized=function(){var i=arguments,a=o?o.apply(this,i):i[0],u=memoized.cache;if(u.has(a))return u.get(a);var _=s.apply(this,i);return memoized.cache=u.set(a,_)||u,_};return memoized.cache=new(memoize.Cache||a),memoized}memoize.Cache=a,s.exports=memoize},55364(s,o,i){var a=i(85250),u=i(20999)((function(s,o,i){a(s,o,i)}));s.exports=u},6048(s){s.exports=function negate(s){if("function"!=typeof s)throw new TypeError("Expected a function");return function(){var o=arguments;switch(o.length){case 0:return!s.call(this);case 1:return!s.call(this,o[0]);case 2:return!s.call(this,o[0],o[1]);case 3:return!s.call(this,o[0],o[1],o[2])}return!s.apply(this,o)}}},63950(s){s.exports=function noop(){}},10124(s,o,i){var a=i(9325);s.exports=function(){return a.Date.now()}},90179(s,o,i){var a=i(34932),u=i(9999),_=i(19931),w=i(31769),x=i(21791),C=i(53138),j=i(38816),L=i(83349),B=j((function(s,o){var i={};if(null==s)return i;var j=!1;o=a(o,(function(o){return o=w(o,s),j||(j=o.length>1),o})),x(s,L(s),i),j&&(i=u(i,7,C));for(var B=o.length;B--;)_(i,o[B]);return i}));s.exports=B},50583(s,o,i){var a=i(47237),u=i(17255),_=i(28586),w=i(77797);s.exports=function property(s){return _(s)?a(w(s)):u(s)}},84195(s,o,i){var a=i(66977),u=i(38816),_=u((function(s,o){return a(s,256,void 0,void 0,void 0,o)}));s.exports=_},40860(s,o,i){var a=i(40882),u=i(80909),_=i(15389),w=i(85558),x=i(56449);s.exports=function reduce(s,o,i){var C=x(s)?a:w,j=arguments.length<3;return C(s,_(o,4),i,j,u)}},63560(s,o,i){var a=i(73170);s.exports=function set(s,o,i){return null==s?s:a(s,o,i)}},42426(s,o,i){var a=i(14248),u=i(15389),_=i(90916),w=i(56449),x=i(36800);s.exports=function some(s,o,i){var C=w(s)?a:_;return i&&x(s,o,i)&&(o=void 0),C(s,u(o,3))}},63345(s){s.exports=function stubArray(){return[]}},89935(s){s.exports=function stubFalse(){return!1}},17400(s,o,i){var a=i(99374),u=1/0;s.exports=function toFinite(s){return s?(s=a(s))===u||s===-1/0?17976931348623157e292*(s<0?-1:1):s==s?s:0:0===s?s:0}},61489(s,o,i){var a=i(17400);s.exports=function toInteger(s){var o=a(s),i=o%1;return o==o?i?o-i:o:0}},80218(s,o,i){var a=i(13222);s.exports=function toLower(s){return a(s).toLowerCase()}},99374(s,o,i){var a=i(54128),u=i(23805),_=i(44394),w=/^[-+]0x[0-9a-f]+$/i,x=/^0b[01]+$/i,C=/^0o[0-7]+$/i,j=parseInt;s.exports=function toNumber(s){if("number"==typeof s)return s;if(_(s))return NaN;if(u(s)){var o="function"==typeof s.valueOf?s.valueOf():s;s=u(o)?o+"":o}if("string"!=typeof s)return 0===s?s:+s;s=a(s);var i=x.test(s);return i||C.test(s)?j(s.slice(2),i?2:8):w.test(s)?NaN:+s}},42072(s,o,i){var a=i(34932),u=i(23007),_=i(56449),w=i(44394),x=i(61802),C=i(77797),j=i(13222);s.exports=function toPath(s){return _(s)?a(s,C):w(s)?[s]:u(x(j(s)))}},69884(s,o,i){var a=i(21791),u=i(37241);s.exports=function toPlainObject(s){return a(s,u(s))}},13222(s,o,i){var a=i(77556);s.exports=function toString(s){return null==s?"":a(s)}},55808(s,o,i){var a=i(12507)("toUpperCase");s.exports=a},66645(s,o,i){var a=i(1733),u=i(45434),_=i(13222),w=i(22225);s.exports=function words(s,o,i){return s=_(s),void 0===(o=i?void 0:o)?u(s)?w(s):a(s):s.match(o)||[]}},53758(s,o,i){var a=i(30980),u=i(56017),_=i(94033),w=i(56449),x=i(40346),C=i(80257),j=Object.prototype.hasOwnProperty;function lodash(s){if(x(s)&&!w(s)&&!(s instanceof a)){if(s instanceof u)return s;if(j.call(s,"__wrapped__"))return C(s)}return new u(s)}lodash.prototype=_.prototype,lodash.prototype.constructor=lodash,s.exports=lodash},47248(s,o,i){var a=i(16547),u=i(51234);s.exports=function zipObject(s,o){return u(s||[],o||[],a)}},43768(s,o,i){"use strict";var a=i(45981),u=i(85587);o.highlight=highlight,o.highlightAuto=function highlightAuto(s,o){var i,w,x,C,j=o||{},L=j.subset||a.listLanguages(),B=j.prefix,$=L.length,U=-1;null==B&&(B=_);if("string"!=typeof s)throw u("Expected `string` for value, got `%s`",s);w={relevance:0,language:null,value:[]},i={relevance:0,language:null,value:[]};for(;++U<$;)C=L[U],a.getLanguage(C)&&((x=highlight(C,s,o)).language=C,x.relevance>w.relevance&&(w=x),x.relevance>i.relevance&&(w=i,i=x));w.language&&(i.secondBest=w);return i},o.registerLanguage=function registerLanguage(s,o){a.registerLanguage(s,o)},o.listLanguages=function listLanguages(){return a.listLanguages()},o.registerAlias=function registerAlias(s,o){var i,u=s;o&&((u={})[s]=o);for(i in u)a.registerAliases(u[i],{languageName:i})},Emitter.prototype.addText=function text(s){var o,i,a=this.stack;if(""===s)return;o=a[a.length-1],(i=o.children[o.children.length-1])&&"text"===i.type?i.value+=s:o.children.push({type:"text",value:s})},Emitter.prototype.addKeyword=function addKeyword(s,o){this.openNode(o),this.addText(s),this.closeNode()},Emitter.prototype.addSublanguage=function addSublanguage(s,o){var i=this.stack,a=i[i.length-1],u=s.rootNode.children,_=o?{type:"element",tagName:"span",properties:{className:[o]},children:u}:u;a.children=a.children.concat(_)},Emitter.prototype.openNode=function open(s){var o=this.stack,i=this.options.classPrefix+s,a=o[o.length-1],u={type:"element",tagName:"span",properties:{className:[i]},children:[]};a.children.push(u),o.push(u)},Emitter.prototype.closeNode=function close(){this.stack.pop()},Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=function toHtmlNoop(){return""};var _="hljs-";function highlight(s,o,i){var w,x=a.configure({}),C=(i||{}).prefix;if("string"!=typeof s)throw u("Expected `string` for name, got `%s`",s);if(!a.getLanguage(s))throw u("Unknown language: `%s` is not registered",s);if("string"!=typeof o)throw u("Expected `string` for value, got `%s`",o);if(null==C&&(C=_),a.configure({__emitter:Emitter,classPrefix:C}),w=a.highlight(o,{language:s,ignoreIllegals:!0}),a.configure(x||{}),w.errorRaised)throw w.errorRaised;return{relevance:w.relevance,language:w.language,value:w.emitter.rootNode.children}}function Emitter(s){this.options=s,this.rootNode={children:[]},this.stack=[this.rootNode]}function noop(){}},71514(s){"use strict";s.exports=Math.abs},58968(s){"use strict";s.exports=Math.floor},94459(s){"use strict";s.exports=Number.isNaN||function isNaN(s){return s!=s}},6188(s){"use strict";s.exports=Math.max},68002(s){"use strict";s.exports=Math.min},75880(s){"use strict";s.exports=Math.pow},70414(s){"use strict";s.exports=Math.round},73093(s,o,i){"use strict";var a=i(94459);s.exports=function sign(s){return a(s)||0===s?s:s<0?-1:1}},92340(s,o,i){const a=i(6048);function coerceElementMatchingCallback(s){return"string"==typeof s?o=>o.element===s:s.constructor&&s.extend?o=>o instanceof s:s}class ArraySlice{constructor(s){this.elements=s||[]}toValue(){return this.elements.map((s=>s.toValue()))}map(s,o){return this.elements.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return s=coerceElementMatchingCallback(s),new ArraySlice(this.elements.filter(s,o))}reject(s,o){return s=coerceElementMatchingCallback(s),new ArraySlice(this.elements.filter(a(s),o))}find(s,o){return s=coerceElementMatchingCallback(s),this.elements.find(s,o)}forEach(s,o){this.elements.forEach(s,o)}reduce(s,o){return this.elements.reduce(s,o)}includes(s){return this.elements.some((o=>o.equals(s)))}shift(){return this.elements.shift()}unshift(s){this.elements.unshift(this.refract(s))}push(s){return this.elements.push(this.refract(s)),this}add(s){this.push(s)}get(s){return this.elements[s]}getValue(s){const o=this.elements[s];if(o)return o.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}"undefined"!=typeof Symbol&&(ArraySlice.prototype[Symbol.iterator]=function symbol(){return this.elements[Symbol.iterator]()}),s.exports=ArraySlice},55973(s){class KeyValuePair{constructor(s,o){this.key=s,this.value=o}clone(){const s=new KeyValuePair;return this.key&&(s.key=this.key.clone()),this.value&&(s.value=this.value.clone()),s}}s.exports=KeyValuePair},3110(s,o,i){const a=i(5187),u=i(85015),_=i(98023),w=i(53812),x=i(23805),C=i(85105),j=i(86804);class Namespace{constructor(s){this.elementMap={},this.elementDetection=[],this.Element=j.Element,this.KeyValuePair=j.KeyValuePair,s&&s.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(s){return s.namespace&&s.namespace({base:this}),s.load&&s.load({base:this}),this}useDefault(){return this.register("null",j.NullElement).register("string",j.StringElement).register("number",j.NumberElement).register("boolean",j.BooleanElement).register("array",j.ArrayElement).register("object",j.ObjectElement).register("member",j.MemberElement).register("ref",j.RefElement).register("link",j.LinkElement),this.detect(a,j.NullElement,!1).detect(u,j.StringElement,!1).detect(_,j.NumberElement,!1).detect(w,j.BooleanElement,!1).detect(Array.isArray,j.ArrayElement,!1).detect(x,j.ObjectElement,!1),this}register(s,o){return this._elements=void 0,this.elementMap[s]=o,this}unregister(s){return this._elements=void 0,delete this.elementMap[s],this}detect(s,o,i){return void 0===i||i?this.elementDetection.unshift([s,o]):this.elementDetection.push([s,o]),this}toElement(s){if(s instanceof this.Element)return s;let o;for(let i=0;i{const o=s[0].toUpperCase()+s.substr(1);this._elements[o]=this.elementMap[s]}))),this._elements}get serialiser(){return new C(this)}}C.prototype.Namespace=Namespace,s.exports=Namespace},10866(s,o,i){const a=i(6048),u=i(92340);class ObjectSlice extends u{map(s,o){return this.elements.map((i=>s.bind(o)(i.value,i.key,i)))}filter(s,o){return new ObjectSlice(this.elements.filter((i=>s.bind(o)(i.value,i.key,i))))}reject(s,o){return this.filter(a(s.bind(o)))}forEach(s,o){return this.elements.forEach(((i,a)=>{s.bind(o)(i.value,i.key,i,a)}))}keys(){return this.map(((s,o)=>o.toValue()))}values(){return this.map((s=>s.toValue()))}}s.exports=ObjectSlice},86804(s,o,i){const a=i(10316),u=i(41067),_=i(71167),w=i(40239),x=i(12242),C=i(6233),j=i(87726),L=i(61045),B=i(86303),$=i(14540),U=i(92340),V=i(10866),z=i(55973);function refract(s){if(s instanceof a)return s;if("string"==typeof s)return new _(s);if("number"==typeof s)return new w(s);if("boolean"==typeof s)return new x(s);if(null===s)return new u;if(Array.isArray(s))return new C(s.map(refract));if("object"==typeof s){return new L(s)}return s}a.prototype.ObjectElement=L,a.prototype.RefElement=$,a.prototype.MemberElement=j,a.prototype.refract=refract,U.prototype.refract=refract,s.exports={Element:a,NullElement:u,StringElement:_,NumberElement:w,BooleanElement:x,ArrayElement:C,MemberElement:j,ObjectElement:L,LinkElement:B,RefElement:$,refract,ArraySlice:U,ObjectSlice:V,KeyValuePair:z}},86303(s,o,i){const a=i(10316);s.exports=class LinkElement extends a{constructor(s,o,i){super(s||[],o,i),this.element="link"}get relation(){return this.attributes.get("relation")}set relation(s){this.attributes.set("relation",s)}get href(){return this.attributes.get("href")}set href(s){this.attributes.set("href",s)}}},14540(s,o,i){const a=i(10316);s.exports=class RefElement extends a{constructor(s,o,i){super(s||[],o,i),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(s){this.attributes.set("path",s)}}},34035(s,o,i){const a=i(3110),u=i(86804);o.g$=a,o.KeyValuePair=i(55973),o.G6=u.ArraySlice,o.ot=u.ObjectSlice,o.Hg=u.Element,o.Om=u.StringElement,o.kT=u.NumberElement,o.bd=u.BooleanElement,o.Os=u.NullElement,o.wE=u.ArrayElement,o.Sh=u.ObjectElement,o.Pr=u.MemberElement,o.sI=u.RefElement,o.Ft=u.LinkElement,o.e=u.refract,i(85105),i(75147)},6233(s,o,i){const a=i(6048),u=i(10316),_=i(92340);class ArrayElement extends u{constructor(s,o,i){super(s||[],o,i),this.element="array"}primitive(){return"array"}get(s){return this.content[s]}getValue(s){const o=this.get(s);if(o)return o.toValue()}getIndex(s){return this.content[s]}set(s,o){return this.content[s]=this.refract(o),this}remove(s){const o=this.content.splice(s,1);return o.length?o[0]:null}map(s,o){return this.content.map(s,o)}flatMap(s,o){return this.map(s,o).reduce(((s,o)=>s.concat(o)),[])}compactMap(s,o){const i=[];return this.forEach((a=>{const u=s.bind(o)(a);u&&i.push(u)})),i}filter(s,o){return new _(this.content.filter(s,o))}reject(s,o){return this.filter(a(s),o)}reduce(s,o){let i,a;void 0!==o?(i=0,a=this.refract(o)):(i=1,a="object"===this.primitive()?this.first.value:this.first);for(let o=i;o{s.bind(o)(i,this.refract(a))}))}shift(){return this.content.shift()}unshift(s){this.content.unshift(this.refract(s))}push(s){return this.content.push(this.refract(s)),this}add(s){this.push(s)}findElements(s,o){const i=o||{},a=!!i.recursive,u=void 0===i.results?[]:i.results;return this.forEach(((o,i,_)=>{a&&void 0!==o.findElements&&o.findElements(s,{results:u,recursive:a}),s(o,i,_)&&u.push(o)})),u}find(s){return new _(this.findElements(s,{recursive:!0}))}findByElement(s){return this.find((o=>o.element===s))}findByClass(s){return this.find((o=>o.classes.includes(s)))}getById(s){return this.find((o=>o.id.toValue()===s)).first}includes(s){return this.content.some((o=>o.equals(s)))}contains(s){return this.includes(s)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(s){return new this.constructor(this.content.concat(s.content))}"fantasy-land/concat"(s){return this.concat(s)}"fantasy-land/map"(s){return new this.constructor(this.map(s))}"fantasy-land/chain"(s){return this.map((o=>s(o)),this).reduce(((s,o)=>s.concat(o)),this.empty())}"fantasy-land/filter"(s){return new this.constructor(this.content.filter(s))}"fantasy-land/reduce"(s,o){return this.content.reduce(s,o)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),s.exports=ArrayElement},12242(s,o,i){const a=i(10316);s.exports=class BooleanElement extends a{constructor(s,o,i){super(s,o,i),this.element="boolean"}primitive(){return"boolean"}}},10316(s,o,i){const a=i(2404),u=i(55973),_=i(92340);class Element{constructor(s,o,i){o&&(this.meta=o),i&&(this.attributes=i),this.content=s}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((s=>{s.parent=this,s.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const s=new this.constructor;return s.element=this.element,this.meta.length&&(s._meta=this.meta.clone()),this.attributes.length&&(s._attributes=this.attributes.clone()),this.content?this.content.clone?s.content=this.content.clone():Array.isArray(this.content)?s.content=this.content.map((s=>s.clone())):s.content=this.content:s.content=this.content,s}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof u?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((s=>s.toValue()),this):this.content}toRef(s){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const o=new this.RefElement(this.id.toValue());return s&&(o.path=s),o}findRecursive(...s){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const o=s.pop();let i=new _;const append=(s,o)=>(s.push(o),s),checkElement=(s,i)=>{i.element===o&&s.push(i);const a=i.findRecursive(o);return a&&a.reduce(append,s),i.content instanceof u&&(i.content.key&&checkElement(s,i.content.key),i.content.value&&checkElement(s,i.content.value)),s};return this.content&&(this.content.element&&checkElement(i,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,i)),s.isEmpty||(i=i.filter((o=>{let i=o.parents.map((s=>s.element));for(const o in s){const a=s[o],u=i.indexOf(a);if(-1===u)return!1;i=i.splice(0,u)}return!0}))),i}set(s){return this.content=s,this}equals(s){return a(this.toValue(),s)}getMetaProperty(s,o){if(!this.meta.hasKey(s)){if(this.isFrozen){const s=this.refract(o);return s.freeze(),s}this.meta.set(s,o)}return this.meta.get(s)}setMetaProperty(s,o){this.meta.set(s,o)}get element(){return this._storedElement||"element"}set element(s){this._storedElement=s}get content(){return this._content}set content(s){if(s instanceof Element)this._content=s;else if(s instanceof _)this.content=s.elements;else if("string"==typeof s||"number"==typeof s||"boolean"==typeof s||"null"===s||null==s)this._content=s;else if(s instanceof u)this._content=s;else if(Array.isArray(s))this._content=s.map(this.refract);else{if("object"!=typeof s)throw new Error("Cannot set content to given value");this._content=Object.keys(s).map((o=>new this.MemberElement(o,s[o])))}}get meta(){if(!this._meta){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._meta=new this.ObjectElement}return this._meta}set meta(s){s instanceof this.ObjectElement?this._meta=s:this.meta.set(s||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const s=new this.ObjectElement;return s.freeze(),s}this._attributes=new this.ObjectElement}return this._attributes}set attributes(s){s instanceof this.ObjectElement?this._attributes=s:this.attributes.set(s||{})}get id(){return this.getMetaProperty("id","")}set id(s){this.setMetaProperty("id",s)}get classes(){return this.getMetaProperty("classes",[])}set classes(s){this.setMetaProperty("classes",s)}get title(){return this.getMetaProperty("title","")}set title(s){this.setMetaProperty("title",s)}get description(){return this.getMetaProperty("description","")}set description(s){this.setMetaProperty("description",s)}get links(){return this.getMetaProperty("links",[])}set links(s){this.setMetaProperty("links",s)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:s}=this;const o=new _;for(;s;)o.push(s),s=s.parent;return o}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof u){const s=new _([this.content.key]);return this.content.value&&s.push(this.content.value),s}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const s=new _;return this.children.forEach((o=>{s.push(o),o.recursiveChildren.forEach((o=>{s.push(o)}))})),s}}s.exports=Element},87726(s,o,i){const a=i(55973),u=i(10316);s.exports=class MemberElement extends u{constructor(s,o,i,u){super(new a,i,u),this.element="member",this.key=s,this.value=o}get key(){return this.content.key}set key(s){this.content.key=this.refract(s)}get value(){return this.content.value}set value(s){this.content.value=this.refract(s)}}},41067(s,o,i){const a=i(10316);s.exports=class NullElement extends a{constructor(s,o,i){super(s||null,o,i),this.element="null"}primitive(){return"null"}set(){return new Error("Cannot set the value of null")}}},40239(s,o,i){const a=i(10316);s.exports=class NumberElement extends a{constructor(s,o,i){super(s,o,i),this.element="number"}primitive(){return"number"}}},61045(s,o,i){const a=i(6048),u=i(23805),_=i(6233),w=i(87726),x=i(10866);s.exports=class ObjectElement extends _{constructor(s,o,i){super(s||[],o,i),this.element="object"}primitive(){return"object"}toValue(){return this.content.reduce(((s,o)=>(s[o.key.toValue()]=o.value?o.value.toValue():void 0,s)),{})}get(s){const o=this.getMember(s);if(o)return o.value}getMember(s){if(void 0!==s)return this.content.find((o=>o.key.toValue()===s))}remove(s){let o=null;return this.content=this.content.filter((i=>i.key.toValue()!==s||(o=i,!1))),o}getKey(s){const o=this.getMember(s);if(o)return o.key}set(s,o){if(u(s))return Object.keys(s).forEach((o=>{this.set(o,s[o])})),this;const i=s,a=this.getMember(i);return a?a.value=o:this.content.push(new w(i,o)),this}keys(){return this.content.map((s=>s.key.toValue()))}values(){return this.content.map((s=>s.value.toValue()))}hasKey(s){return this.content.some((o=>o.key.equals(s)))}items(){return this.content.map((s=>[s.key.toValue(),s.value.toValue()]))}map(s,o){return this.content.map((i=>s.bind(o)(i.value,i.key,i)))}compactMap(s,o){const i=[];return this.forEach(((a,u,_)=>{const w=s.bind(o)(a,u,_);w&&i.push(w)})),i}filter(s,o){return new x(this.content).filter(s,o)}reject(s,o){return this.filter(a(s),o)}forEach(s,o){return this.content.forEach((i=>s.bind(o)(i.value,i.key,i)))}}},71167(s,o,i){const a=i(10316);s.exports=class StringElement extends a{constructor(s,o,i){super(s,o,i),this.element="string"}primitive(){return"string"}get length(){return this.content.length}}},75147(s,o,i){const a=i(85105);s.exports=class JSON06Serialiser extends a{serialise(s){if(!(s instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${s}\` is not an Element instance`);let o;s._attributes&&s.attributes.get("variable")&&(o=s.attributes.get("variable"));const i={element:s.element};s._meta&&s._meta.length>0&&(i.meta=this.serialiseObject(s.meta));const a="enum"===s.element||-1!==s.attributes.keys().indexOf("enumerations");if(a){const o=this.enumSerialiseAttributes(s);o&&(i.attributes=o)}else if(s._attributes&&s._attributes.length>0){let{attributes:a}=s;a.get("metadata")&&(a=a.clone(),a.set("meta",a.get("metadata")),a.remove("metadata")),"member"===s.element&&o&&(a=a.clone(),a.remove("variable")),a.length>0&&(i.attributes=this.serialiseObject(a))}if(a)i.content=this.enumSerialiseContent(s,i);else if(this[`${s.element}SerialiseContent`])i.content=this[`${s.element}SerialiseContent`](s,i);else if(void 0!==s.content){let a;o&&s.content.key?(a=s.content.clone(),a.key.attributes.set("variable",o),a=this.serialiseContent(a)):a=this.serialiseContent(s.content),this.shouldSerialiseContent(s,a)&&(i.content=a)}else this.shouldSerialiseContent(s,s.content)&&s instanceof this.namespace.elements.Array&&(i.content=[]);return i}shouldSerialiseContent(s,o){return"parseResult"===s.element||"httpRequest"===s.element||"httpResponse"===s.element||"category"===s.element||"link"===s.element||void 0!==o&&(!Array.isArray(o)||0!==o.length)}refSerialiseContent(s,o){return delete o.attributes,{href:s.toValue(),path:s.path.toValue()}}sourceMapSerialiseContent(s){return s.toValue()}dataStructureSerialiseContent(s){return[this.serialiseContent(s.content)]}enumSerialiseAttributes(s){const o=s.attributes.clone(),i=o.remove("enumerations")||new this.namespace.elements.Array([]),a=o.get("default");let u=o.get("samples")||new this.namespace.elements.Array([]);if(a&&a.content&&(a.content.attributes&&a.content.attributes.remove("typeAttributes"),o.set("default",new this.namespace.elements.Array([a.content]))),u.forEach((s=>{s.content&&s.content.element&&s.content.attributes.remove("typeAttributes")})),s.content&&0!==i.length&&u.unshift(s.content),u=u.map((s=>s instanceof this.namespace.elements.Array?[s]:new this.namespace.elements.Array([s.content]))),u.length&&o.set("samples",u),o.length>0)return this.serialiseObject(o)}enumSerialiseContent(s){if(s._attributes){const o=s.attributes.get("enumerations");if(o&&o.length>0)return o.content.map((s=>{const o=s.clone();return o.attributes.remove("typeAttributes"),this.serialise(o)}))}if(s.content){const o=s.content.clone();return o.attributes.remove("typeAttributes"),[this.serialise(o)]}return[]}deserialise(s){if("string"==typeof s)return new this.namespace.elements.String(s);if("number"==typeof s)return new this.namespace.elements.Number(s);if("boolean"==typeof s)return new this.namespace.elements.Boolean(s);if(null===s)return new this.namespace.elements.Null;if(Array.isArray(s))return new this.namespace.elements.Array(s.map(this.deserialise,this));const o=this.namespace.getElementClass(s.element),i=new o;i.element!==s.element&&(i.element=s.element),s.meta&&this.deserialiseObject(s.meta,i.meta),s.attributes&&this.deserialiseObject(s.attributes,i.attributes);const a=this.deserialiseContent(s.content);if(void 0===a&&null!==i.content||(i.content=a),"enum"===i.element){i.content&&i.attributes.set("enumerations",i.content);let s=i.attributes.get("samples");if(i.attributes.remove("samples"),s){const a=s;s=new this.namespace.elements.Array,a.forEach((a=>{a.forEach((a=>{const u=new o(a);u.element=i.element,s.push(u)}))}));const u=s.shift();i.content=u?u.content:void 0,i.attributes.set("samples",s)}else i.content=void 0;let a=i.attributes.get("default");if(a&&a.length>0){a=a.get(0);const s=new o(a);s.element=i.element,i.attributes.set("default",s)}}else if("dataStructure"===i.element&&Array.isArray(i.content))[i.content]=i.content;else if("category"===i.element){const s=i.attributes.get("meta");s&&(i.attributes.set("metadata",s),i.attributes.remove("meta"))}else"member"===i.element&&i.key&&i.key._attributes&&i.key._attributes.getValue("variable")&&(i.attributes.set("variable",i.key.attributes.get("variable")),i.key.attributes.remove("variable"));return i}serialiseContent(s){if(s instanceof this.namespace.elements.Element)return this.serialise(s);if(s instanceof this.namespace.KeyValuePair){const o={key:this.serialise(s.key)};return s.value&&(o.value=this.serialise(s.value)),o}return s&&s.map?s.map(this.serialise,this):s}deserialiseContent(s){if(s){if(s.element)return this.deserialise(s);if(s.key){const o=new this.namespace.KeyValuePair(this.deserialise(s.key));return s.value&&(o.value=this.deserialise(s.value)),o}if(s.map)return s.map(this.deserialise,this)}return s}shouldRefract(s){return!!(s._attributes&&s.attributes.keys().length||s._meta&&s.meta.keys().length)||"enum"!==s.element&&(s.element!==s.primitive()||"member"===s.element)}convertKeyToRefract(s,o){return this.shouldRefract(o)?this.serialise(o):"enum"===o.element?this.serialiseEnum(o):"array"===o.element?o.map((o=>this.shouldRefract(o)||"default"===s?this.serialise(o):"array"===o.element||"object"===o.element||"enum"===o.element?o.children.map((s=>this.serialise(s))):o.toValue())):"object"===o.element?(o.content||[]).map(this.serialise,this):o.toValue()}serialiseEnum(s){return s.children.map((s=>this.serialise(s)))}serialiseObject(s){const o={};return s.forEach(((s,i)=>{if(s){const a=i.toValue();o[a]=this.convertKeyToRefract(a,s)}})),o}deserialiseObject(s,o){Object.keys(s).forEach((i=>{o.set(i,this.deserialise(s[i]))}))}}},85105(s){s.exports=class JSONSerialiser{constructor(s){this.namespace=s||new this.Namespace}serialise(s){if(!(s instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${s}\` is not an Element instance`);const o={element:s.element};s._meta&&s._meta.length>0&&(o.meta=this.serialiseObject(s.meta)),s._attributes&&s._attributes.length>0&&(o.attributes=this.serialiseObject(s.attributes));const i=this.serialiseContent(s.content);return void 0!==i&&(o.content=i),o}deserialise(s){if(!s.element)throw new Error("Given value is not an object containing an element name");const o=new(this.namespace.getElementClass(s.element));o.element!==s.element&&(o.element=s.element),s.meta&&this.deserialiseObject(s.meta,o.meta),s.attributes&&this.deserialiseObject(s.attributes,o.attributes);const i=this.deserialiseContent(s.content);return void 0===i&&null!==o.content||(o.content=i),o}serialiseContent(s){if(s instanceof this.namespace.elements.Element)return this.serialise(s);if(s instanceof this.namespace.KeyValuePair){const o={key:this.serialise(s.key)};return s.value&&(o.value=this.serialise(s.value)),o}if(s&&s.map){if(0===s.length)return;return s.map(this.serialise,this)}return s}deserialiseContent(s){if(s){if(s.element)return this.deserialise(s);if(s.key){const o=new this.namespace.KeyValuePair(this.deserialise(s.key));return s.value&&(o.value=this.deserialise(s.value)),o}if(s.map)return s.map(this.deserialise,this)}return s}serialiseObject(s){const o={};if(s.forEach(((s,i)=>{s&&(o[i.toValue()]=this.serialise(s))})),0!==Object.keys(o).length)return o}deserialiseObject(s,o){Object.keys(s).forEach((i=>{o.set(i,this.deserialise(s[i]))}))}}},76578(s){"use strict";s.exports=["Float16Array","Float32Array","Float64Array","Int8Array","Int16Array","Int32Array","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array"]},65606(s){var o,i,a=s.exports={};function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}function runTimeout(s){if(o===setTimeout)return setTimeout(s,0);if((o===defaultSetTimout||!o)&&setTimeout)return o=setTimeout,setTimeout(s,0);try{return o(s,0)}catch(i){try{return o.call(null,s,0)}catch(i){return o.call(this,s,0)}}}!function(){try{o="function"==typeof setTimeout?setTimeout:defaultSetTimout}catch(s){o=defaultSetTimout}try{i="function"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(s){i=defaultClearTimeout}}();var u,_=[],w=!1,x=-1;function cleanUpNextTick(){w&&u&&(w=!1,u.length?_=u.concat(_):x=-1,_.length&&drainQueue())}function drainQueue(){if(!w){var s=runTimeout(cleanUpNextTick);w=!0;for(var o=_.length;o;){for(u=_,_=[];++x1)for(var i=1;i_)throw new RangeError("requested too many random bytes");var i=w.allocUnsafe(s);if(s>0)if(s>u)for(var C=0;C=0||(u[i]=s[i]);return u}(s,o);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||Object.prototype.propertyIsEnumerable.call(s,i)&&(u[i]=s[i])}return u}function _defineProperties(s,o){for(var i=0;i=0||(u[i]=s[i]);return u}(s,o);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(s);for(a=0;a<_.length;a++)i=_[a],o.indexOf(i)>=0||Object.prototype.propertyIsEnumerable.call(s,i)&&(u[i]=s[i])}return u}function ownKeys(s,o){var i=Object.keys(s);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(s);o&&(a=a.filter((function(o){return Object.getOwnPropertyDescriptor(s,o).enumerable}))),i.push.apply(i,a)}return i}function _objectSpread(s){for(var o=1;o=a?i.notify(s):o.length>u.length&&i.notify(_objectSpread(_objectSpread({},s),{},{target:_objectSpread(_objectSpread({},s.target),{},{value:""})}))}))})),_defineProperty(_assertThisInitialized(i),"onKeyDown",(function(s){"Enter"===s.key&&i.forceNotify(s);var o=i.props.onKeyDown;o&&(s.persist(),o(s))})),_defineProperty(_assertThisInitialized(i),"onBlur",(function(s){i.forceNotify(s);var o=i.props.onBlur;o&&(s.persist(),o(s))})),_defineProperty(_assertThisInitialized(i),"createNotifier",(function(s){if(s<0)i.notify=function(){return null};else if(0===s)i.notify=i.doNotify;else{var o=(0,u.default)((function(s){i.isDebouncing=!1,i.doNotify(s)}),s);i.notify=function(s){i.isDebouncing=!0,o(s)},i.flush=function(){return o.flush()},i.cancel=function(){i.isDebouncing=!1,o.cancel()}}})),_defineProperty(_assertThisInitialized(i),"doNotify",(function(){i.props.onChange.apply(void 0,arguments)})),_defineProperty(_assertThisInitialized(i),"forceNotify",(function(s){var o=i.props.debounceTimeout;if(i.isDebouncing||!(o>0)){i.cancel&&i.cancel();var a=i.state.value,u=i.props.minLength;a.length>=u?i.doNotify(s):i.doNotify(_objectSpread(_objectSpread({},s),{},{target:_objectSpread(_objectSpread({},s.target),{},{value:a})}))}})),i.isDebouncing=!1,i.state={value:void 0===s.value||null===s.value?"":s.value};var a=i.props.debounceTimeout;return i.createNotifier(a),i}return function _createClass(s,o,i){return o&&_defineProperties(s.prototype,o),i&&_defineProperties(s,i),Object.defineProperty(s,"prototype",{writable:!1}),s}(DebounceInput,[{key:"componentDidUpdate",value:function componentDidUpdate(s){if(!this.isDebouncing){var o=this.props,i=o.value,a=o.debounceTimeout,u=s.debounceTimeout,_=s.value,w=this.state.value;void 0!==i&&_!==i&&w!==i&&this.setState({value:i}),a!==u&&this.createNotifier(a)}}},{key:"componentWillUnmount",value:function componentWillUnmount(){this.flush&&this.flush()}},{key:"render",value:function render(){var s,o,i=this.props,u=i.element,w=(i.onChange,i.value,i.minLength,i.debounceTimeout,i.forceNotifyByEnter),x=i.forceNotifyOnBlur,C=i.onKeyDown,j=i.onBlur,L=i.inputRef,B=_objectWithoutProperties(i,_),$=this.state.value;s=w?{onKeyDown:this.onKeyDown}:C?{onKeyDown:C}:{},o=x?{onBlur:this.onBlur}:j?{onBlur:j}:{};var U=L?{ref:L}:{};return a.default.createElement(u,_objectSpread(_objectSpread(_objectSpread(_objectSpread({},B),{},{onChange:this.onChange,value:$},s),o),U))}}]),DebounceInput}(a.default.PureComponent);o.DebounceInput=w,_defineProperty(w,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},24677(s,o,i){"use strict";var a=i(81214).DebounceInput;a.DebounceInput=a,s.exports=a},22551(s,o,i){"use strict";var a=i(96540),u=i(69982);function p(s){for(var o="https://reactjs.org/docs/error-decoder.html?invariant="+s,i=1;i