diff --git a/.agents/skills/hydrogram-audit-bot-api/SKILL.md b/.agents/skills/hydrogram-audit-bot-api/SKILL.md new file mode 100644 index 000000000..3d1eb3431 --- /dev/null +++ b/.agents/skills/hydrogram-audit-bot-api/SKILL.md @@ -0,0 +1,70 @@ +--- +name: hydrogram-audit-bot-api +description: Compare Hydrogram with the Telegram Bot API and investigate missing methods, objects, or fields when auditing Bot API feature coverage. +--- + +# Audit Bot API coverage + +Produce an evidence-based comparison of Bot API features and Hydrogram's public API. +Follow `AGENTS.md` and `CONTRIBUTING.md`; run commands from the repository root. +This workflow analyzes coverage. Implement features or change the comparison tool only +when the user asks for those changes. + +## Collect the comparison + +- Establish the requested scope: a full audit, a Bot API release, or particular features. + Use the current official documentation when no version is specified. For a historical + release, use matching documentation; the script fetches the live page only. +- Inspect the current `// LAYER` in `compiler/api/source/main_api.tl` and the comparison + logic and `ignored_items` in `dev_tools/compare_to_bot_api.py`. +- Ensure generated bindings exist before importing Hydrogram. If they are missing or + stale, run `uv run make api`; use the dependency setup in `CONTRIBUTING.md` if needed. + An audit does not require updating the schema to a newer layer. +- Run `uv run make compare-bot-api`. It fetches the live Bot API documentation and prints + a Markdown comparison of method names, object names, and constructor parameters. + Check that the output contains plausible methods and objects. Fetch or parsing errors + and empty results do not establish that a feature is unsupported. + +Use [the official Bot API documentation](https://core.telegram.org/bots/api) to verify +feature semantics and the release/date being compared. If live access fails, explain the +limitation and use explicitly identified local or supplied evidence where available; +do not present an incomplete run as a current full audit. + +## Investigate findings + +The report is a list of candidates, not a compatibility test. For findings within the +requested scope, trace the relevant public methods in `hydrogram/methods/`, object +constructors and parsers in `hydrogram/types/`, and local API documentation in +`docs/source/`. Check raw schema definitions when MTProto support is relevant. + +Classify each investigated finding as: + +- **Confirmed gap:** the Bot API behavior has no equivalent public implementation. +- **Equivalent support:** Hydrogram exposes the behavior through a different name, + argument, object shape, or method. Cite the actual implementation. +- **Intentional difference:** the Bot API mechanism does not apply to the MTProto client, + such as HTTP webhooks or Bot API polling. Verify existing exclusions before proposing one. +- **Layer dependency:** the local MTProto schema lacks a definition needed for the feature. + Identify the missing definition and supporting evidence; do not infer a required layer + solely from a Bot API version number. +- **Unverified:** available evidence does not establish support or absence. + +Check both directions: matching names and arguments do not prove equivalent behavior, +permissions, defaults, parsing, or bot-account availability. Raw bindings alone do not +prove high-level support. Aliases such as `message_id` / `id` and `from` / `from_user` +can explain apparent gaps; inspect the code before applying an alias or exclusion. + +The script's percentages measure name/parameter matching, not functional coverage. +Ignored items still contribute to its current totals, so avoid presenting its numbers as +validated support percentages. Do not modify exclusions merely to improve the score. + +## Report the audit + +State the Bot API version/date checked, local MTProto layer, command outcome, and scope +actually investigated. Present a table with the Bot API feature, classification, +Hydrogram equivalent or missing behavior, and evidence. Link official documentation and +relevant local source files. Separate uninvestigated script findings from confirmed gaps. + +For confirmed gaps, describe a concrete next step and any schema prerequisite. If useful, +prioritize by user impact and implementation dependencies. Do not automatically upgrade +the layer, implement features, or create GitHub issues as part of the audit. diff --git a/.agents/skills/hydrogram-audit-bot-api/agents/openai.yaml b/.agents/skills/hydrogram-audit-bot-api/agents/openai.yaml new file mode 100644 index 000000000..4a158ceb7 --- /dev/null +++ b/.agents/skills/hydrogram-audit-bot-api/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Hydrogram Bot API Audit" + short_description: "Compare Bot API features and investigate coverage" diff --git a/.agents/skills/hydrogram-update-api/SKILL.md b/.agents/skills/hydrogram-update-api/SKILL.md new file mode 100644 index 000000000..fd208db84 --- /dev/null +++ b/.agents/skills/hydrogram-update-api/SKILL.md @@ -0,0 +1,118 @@ +--- +name: hydrogram-update-api +description: Update Hydrogram's Telegram MTProto API schema and regenerate bindings when upgrading the API layer or applying upstream schema changes. +--- + +# Update the MTProto API + +Deliver a reviewed schema update with regenerated bindings, compatible existing callers, +and validation results. Run commands from the repository root and follow `AGENTS.md` and +`CONTRIBUTING.md`. Use the existing tools under `dev_tools/`; no new helper is needed. + +## Establish the target + +- Inspect `git status --short` and the current `// LAYER` in + `compiler/api/source/main_api.tl`. Preserve existing local edits; if that schema is + already modified, inspect its diff before allowing an updater to overwrite it. +- Honor a requested layer or upstream revision. With no specified target, use the latest + schema from Telegram Desktop's `dev` branch, as configured in + `dev_tools/check_api_schema_updates.py`. +- For a pinned target, obtain the schema from the matching upstream revision and retain + its provenance. The updater always fetches `dev`; it cannot select an older layer. + Preserve the local header and manually handled basic types when adapting pinned input. + +## Update and review the schema + +For the latest upstream schema, run: + +```bash +uv run make check-api-schema +``` + +The script writes `compiler/api/source/main_api.tl`; it is not a read-only check. +Exit codes are **0** for an applied update, **2** for no change, and **1** for a fetch +or processing error. Treat 2 as a successful no-op. Do not force an update to work around +an error. If unchanged, report that result without inventing a release fragment or edits; +continue with documentation refresh only if the user requested it independently. + +Review the schema diff even when the layer number is unchanged. Identify added, removed, +and changed constructors and methods, including IDs, argument types, flags, and return +types. Verify that the target layer is present and the schema sections and manually +handled basic types remain valid. Change other schemas under `compiler/api/source/` only +when the upstream change requires it. + +Use [Corefork API documentation](https://corefork.telegram.org/api) and +[MTProto documentation](https://corefork.telegram.org/mtproto) for protocol semantics; +check that examples and fields apply to the target layer. + +## Refresh raw API descriptions when needed + +Evaluate a documentation refresh when the schema adds or renames types, constructors, +methods, or parameters, or when the user explicitly requests updated API documentation. +For parameter removals or other signature changes, check whether existing descriptions +became stale. Skip the scrape when the existing descriptions still fit the target schema; +a layer-number or constructor-ID change alone does not require fetching every page. + +`uv run make generate-docs-json` reads the updated local `main_api.tl`, fetches Corefork +pages for its objects, and replaces `compiler/api/docs.json`. The compiler uses this JSON +for raw binding docstrings. `uv run make docs` instead generates Sphinx reference pages +and builds HTML; it does not refresh the JSON. The scraper fetches current pages, so +verify descriptions against the target schema, especially for a pinned older layer. + +When refreshing, use this order: update schema, collect and validate the JSON, regenerate +bindings, then build the documentation. + +1. Before running the generator, save an exact copy of the existing JSON outside the + checkout in a temporary location. Preserve any uncommitted edits in that copy; do not + rely on `git restore` or the version in HEAD. If no JSON exists, record that fact. +2. Run `uv run make generate-docs-json` from the repository root and capture its output. + It fetches all objects, not only changed ones. HTTP errors, missing page content, + missing descriptions, and missing parameter sections can leave incomplete results + **even with exit code zero and a `Done!` message**. +3. Parse the candidate JSON and check its `type`, `constructor`, and `method` mappings. + Compare keys with those expected from the updated schema using the generator's + `parse_tl_file` naming rules. Compare entries, nonempty descriptions, and documented + parameter names with the saved JSON and target signatures. Inspect affected objects + and unexpected losses; do not require parameter tables for parameterless objects. + Distinguish legitimate removals or renames from failed requests or parsing changes. +4. Accept only reviewed documentation changes. If the fetch fails, truncates the JSON, + or drops valid content, restore the exact saved copy. If partial results are useful, + retain only individually verified updates merged into that copy, preserving unaffected + documentation and local edits. With no previous file, keep an incomplete candidate + outside the checkout rather than adopting it as a successful refresh. Report any + remaining missing documentation; do not invent descriptions or retry indefinitely. + +## Regenerate and adapt existing callers + +```bash +uv run make api +``` + +If dependencies are missing, use the setup commands in `CONTRIBUTING.md`. +Fix generation problems in compiler inputs or templates. Generated bindings and RPC +exceptions are ignored by Git; do not patch or force-add them. + +Search `hydrogram/` for uses of changed raw constructors, methods, and fields. Adapt +existing serialization, parsing, and invocation paths where the new schema breaks them. +Keep unrelated new high-level features outside this update unless requested. Add focused +regression tests for behavior changed in handwritten code. + +Regenerate bindings again after any later compiler input or documentation JSON changes. + +## Validate and report + +- Confirm `hydrogram.raw.all.layer` matches the target and that Hydrogram imports. + Exercise relevant generated signatures or serialization when the schema changes require it. +- Run focused tests for adapted callers, then `uv run pytest tests`. +- Run the repository Ruff checks if Python sources changed. Build with `uv run make docs` + when handwritten documentation or API documentation generation changed. +- For an actual update, add `news/.misc.rst` describing the layer/schema + change, following the repository's fragment naming convention. Update user-facing docs + if public behavior changed. +- Review the final diff and `git diff --check`. Report the old and new layer, upstream + source/revision when known, significant schema changes, caller adaptations, and checks + passed or blocked. State whether the documentation JSON was refreshed, skipped, or + restored, and list unresolved scrape failures or missing descriptions. Distinguish schema + support from new high-level API coverage. + +Commit, push, or open a pull request only when requested by the user. diff --git a/.agents/skills/hydrogram-update-api/agents/openai.yaml b/.agents/skills/hydrogram-update-api/agents/openai.yaml new file mode 100644 index 000000000..e3f49fac7 --- /dev/null +++ b/.agents/skills/hydrogram-update-api/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Hydrogram API Update" + short_description: "Update MTProto schema and validate API bindings" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4532e0ef9..d07057e1f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,30 +1,33 @@ name: Bug report -description: Report issues affecting the framework or the documentation +description: Report a reproducible problem with Hydrogram or its documentation body: + - type: markdown + attributes: + value: | + For usage questions, use [GitHub Discussions](https://github.com/orgs/hydrogram/discussions). + Remove API hashes, bot tokens, session strings, phone numbers, and other private data from examples and logs. Do not upload session databases. + - type: checkboxes + id: checklist attributes: - label: Checklist - description: Invalid, incomplete or inadequate issue reports may not be taken into consideration + label: Before submitting options: - - label: I am sure the error is coming from Hydrogram's code and not elsewhere - required: true - - label: I have searched in the issue tracker for similar bug reports, including closed ones - required: true - - label: I ran `pip3 install -U https://github.com/hydrogram/hydrogram/archive/dev.zip` and reproduced the issue using the latest development version + - label: I searched open and closed issues for an existing report of this problem. required: true - type: textarea + id: description attributes: - label: Description - description: Provide a clear and concise description of the issue - placeholder: Description... + label: Problem description + description: Describe the problem and what you were doing when it occurred. For documentation issues, include the affected page or section. validations: required: true - type: textarea + id: steps attributes: label: Steps to reproduce - description: Explain precisely how to reproduce the issue + description: List the steps, configuration, and inputs needed to reproduce the problem. placeholder: | 1. 2. @@ -33,20 +36,54 @@ body: required: true - type: textarea + id: expected + attributes: + label: Expected behavior + description: Explain what you expected to happen. + validations: + required: true + + - type: textarea + id: actual attributes: - label: Code example - description: Provide a [minimal, complete, consistently reproducible](https://stackoverflow.com/help/minimal-reproducible-example) and properly formatted example involving normal usages (if applicable) + label: Actual behavior + description: Explain what happened instead. + validations: + required: true + + - type: textarea + id: code + attributes: + label: Minimal code example + description: Include a small, self-contained example when applicable. Use placeholders for credentials. placeholder: | from hydrogram import Client ... render: python - type: textarea + id: environment attributes: - label: Logs - description: Provide the complete traceback (if applicable) + label: Environment + description: Include exact versions and the installation source (PyPI or a Git commit). For documentation-only issues, enter "Not applicable". placeholder: | - Traceback (most recent call last): - File "main.py", line 1, in - ... - render: shell + Hydrogram version or commit: + Python version and implementation (CPython/PyPy): + Operating system: + TgCrypto/uvloop versions, if installed: + Installation source: + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Traceback and logs + description: Paste the full relevant traceback and logs, with credentials and personal data removed. + render: text + + - type: textarea + id: context + attributes: + label: Additional context + description: Include related issues, workarounds, or the last working version. If you tested a newer release or development commit, include its version and the result. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3913b987a..5b75825b9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Ask Hydrogram related questions - url: https://stackoverflow.com/questions/tagged/hydrogram - about: This place is only for reporting issues about Hydrogram. You can ask questions at StackOverflow. - - name: Join the Telegram channel + - name: Questions and general feedback + url: https://github.com/orgs/hydrogram/discussions + about: Ask usage questions, get help, or discuss ideas with the community. + - name: Hydrogram news url: https://t.me/HydrogramNews - about: Join the official channel and stay tuned for news, updates and announcements. + about: Follow the official channel for news, updates, and announcements. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 0d89f0e8d..788e533f1 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,20 +1,45 @@ name: Feature request -description: Suggest ideas, new features or enhancements +description: Suggest a feature or improvement for Hydrogram labels: [enhancement] body: + - type: markdown + attributes: + value: | + For help using an existing feature, use [GitHub Discussions](https://github.com/orgs/hydrogram/discussions). + Keep credentials, session data, and personal information out of examples. + - type: checkboxes + id: checklist attributes: - label: Checklist + label: Before submitting options: - - label: I believe the idea is awesome and would benefit the framework - required: true - - label: I have searched in the issue tracker for similar requests, including closed ones + - label: I searched open and closed issues for similar requests. required: true - type: textarea + id: use_case + attributes: + label: Problem or use case + description: Explain what you are trying to accomplish, the current limitation, and who would benefit. + validations: + required: true + + - type: textarea + id: proposal attributes: - label: Description - description: Provide a detailed description of the request - placeholder: Description... + label: Proposed behavior + description: Describe how the feature should work. Include a proposed API or usage example when helpful, and explain how existing behavior would be affected. validations: required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives and workarounds + description: Describe existing methods, raw API calls, or other approaches you considered and why they do not meet your needs. + + - type: textarea + id: references + attributes: + label: References and additional context + description: Link relevant Telegram API documentation, related issues, or examples. If the request depends on a specific API layer, include it here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8325ac673..f20a6c9e7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,37 +1,52 @@ -# Pull Request Template - ## Description -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. - -Closes #(issue) + ## Type of change -Please delete options that are not relevant. + -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update +- [ ] Bug fix +- [ ] New feature or enhancement +- [ ] Documentation +- [ ] Refactoring, maintenance, or tooling +- [ ] Breaking change (describe the impact and migration steps below) -## How Has This Been Tested? +## Compatibility and migration -Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + -- [ ] Test A -- [ ] Test B +## Validation -### Test Configuration + -- Operating System: -- Python Version: +- Operating system: +- Python version: +- Commands and results: ## Checklist -- [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code -- [ ] I have made corresponding changes to the documentation -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] My changes generate no new warnings -- [ ] New and existing unit tests pass locally with my changes + + +- [ ] I reviewed my changes and followed the repository guidelines. +- [ ] The code remains compatible with Python 3.11+. +- [ ] I added or updated tests for changed behavior, where applicable. +- [ ] Tests and style checks pass, or limitations are explained above. +- [ ] I updated affected public API docstrings and documentation, where applicable. +- [ ] I added a release fragment at `news/..rst` (`feature`, `bugfix`, `doc`, `removal`, or `misc`). +- [ ] For compiler changes, I updated sources/templates and regenerated the API rather than editing generated files. +- [ ] For new public methods/types, I registered method mixins and exported types through the relevant `__init__.py` files. diff --git a/.github/workflows/check_api_updates.yml b/.github/workflows/check_api_updates.yml new file mode 100644 index 000000000..dfdfab20c --- /dev/null +++ b/.github/workflows/check_api_updates.yml @@ -0,0 +1,92 @@ +name: Check MTProto API Schema Updates + +on: + schedule: + - cron: '0 */12 * * *' + workflow_dispatch: + push: + paths: + - 'compiler/**' + - 'dev_tools/check_api_schema_updates.py' + - 'tests/test_*schema*.py' + - 'tests/test_layer_compatibility.py' + - '.github/workflows/check_api_updates.yml' + pull_request: + paths: + - 'compiler/**' + - 'dev_tools/check_api_schema_updates.py' + - 'tests/test_*schema*.py' + - 'tests/test_layer_compatibility.py' + - '.github/workflows/check_api_updates.yml' + +permissions: + contents: read + +concurrency: + group: mtproto-schema-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-schema-updates: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + version: '0.13.6' + python-version: '3.11' + enable-cache: true + - name: Install dependencies + run: uv sync --locked --all-extras --dev + - name: Validate checked-in generation and compatibility offline + run: uv run --locked python dev_tools/check_api_schema_updates.py --validate + - name: Detect upstream drift + id: drift + run: | + status=0 + uv run --locked python dev_tools/check_api_schema_updates.py \ + --report schema-update.md --json-report schema-update.json || status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + case "$status" in + 0|2) ;; + *) exit "$status" ;; + esac + - name: Validate the pinned candidate before producing a patch + if: steps.drift.outputs.status == '2' + run: | + uv run --locked python - <<'PY' + import json + import subprocess + from pathlib import Path + + source = json.loads(Path('schema-update.json').read_text())['provenance'] + subprocess.run([ + 'uv', 'run', '--locked', 'python', 'dev_tools/check_api_schema_updates.py', + '--apply', '--revision', source['desktop']['revision'], + '--tdlib-revision', source['tdlib']['revision'], + '--report', 'schema-update.md', '--json-report', 'schema-update.json', + ], check=True) + PY + git diff --binary -- compiler/api/source > schema-update.patch + - name: Publish adoption report and validation results + if: always() + run: | + if [ -f schema-update.md ]; then + cat schema-update.md >> "$GITHUB_STEP_SUMMARY" + fi + - name: Upload report and validated candidate patch + if: always() + uses: actions/upload-artifact@v4 + with: + name: mtproto-schema-report + path: | + schema-update.md + schema-update.json + schema-update.patch + if-no-files-found: ignore + - name: Signal protocol drift for maintainer review + if: steps.drift.outputs.status == '2' + run: | + echo '::error::MTProto schema drift detected. Review the report, compatibility tests, RPC errors, and candidate patch in the workflow artifacts.' + exit 1 diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index 3e968fdd4..4ad0d17fa 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -6,13 +6,20 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.13.6" + enable-cache: true + cache-dependency-glob: "uv.lock" + - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install ruff - - name: Analysing the code with pylint + uv sync --locked --all-extras --dev + + - name: Analysing the code with Ruff run: | - ruff format --check . + uv run --locked ruff format --check . + uv run --locked ruff check --output-format=github . diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 96a04c665..670e6b33d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,26 +10,66 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: + version: "0.13.6" + enable-cache: true + cache-dependency-glob: "uv.lock" python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install tox + uv sync --locked --all-extras --dev - name: Generate API run: | - make venv - make api + uv run --locked make api - name: Run tests run: | - tox + uv run --locked pytest + + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.13.6" + enable-cache: true + python-version: "3.14" + + - name: Install a fresh checkout with pip + run: | + uv venv --seed "$RUNNER_TEMP/hydrogram-source" + "$RUNNER_TEMP/hydrogram-source/bin/python" -m pip install . + "$RUNNER_TEMP/hydrogram-source/bin/python" -I -c "from hydrogram import Client, raw, errors; print(Client.APP_VERSION)" + + - name: Install editable development dependencies + run: | + make clean-api + uv sync --locked --dev + uv run --locked python -c "from hydrogram import Client; print(Client.APP_VERSION)" + + - name: Build and validate distributions + run: | + uv run --locked make build + uv run --locked twine check --strict dist/* + + - name: Import the wheel outside the checkout + working-directory: ${{ runner.temp }} + run: | + uv run --isolated --no-project --with "$GITHUB_WORKSPACE"/dist/*.whl python -c "from hydrogram import Client, raw, errors; print(Client.APP_VERSION)" + + - name: Import the source distribution outside the checkout + working-directory: ${{ runner.temp }} + run: | + uv run --isolated --no-project --with "$GITHUB_WORKSPACE"/dist/*.tar.gz python -c "from hydrogram import Client, raw, errors; print(Client.APP_VERSION)" diff --git a/.gitignore b/.gitignore index 0c5d72972..8a3d959c1 100644 --- a/.gitignore +++ b/.gitignore @@ -98,13 +98,6 @@ target/ # Jupyter Notebook .ipynb_checkpoints -# pyenv -.python-version - -# Rye -requirements.lock -requirements-dev.lock - # celery beat schedule file celerybeat-schedule diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7317d72f4..d739bc7c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,15 @@ repos: + - repo: local + hooks: + - id: news-fragment-filenames + name: NEWS fragment + language: fail + entry: NEWS fragment files must be named *.(removal|feature|bugfix|doc|misc).rst + exclude: ^news/(.gitignore|.template.rst.jinja2|.*\.(removal|feature|bugfix|doc|misc).rst) + files: ^news/ + - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: check-merge-conflict - id: detect-private-key @@ -11,8 +20,8 @@ repos: - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.10 + rev: v0.17.3 hooks: + - id: ruff-check + args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - - id: ruff - args: [--fix, --ignore=E501, --exit-non-zero-on-fix] diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..6324d401a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8397011c9..0aa38a3e0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.12" + python: "3.13" jobs: post_install: - pip install .[docs] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..709dbd226 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +Hydrogram is an async Telegram MTProto client library supporting Python 3.11+. + +- `hydrogram/`: client, methods, types, filters, handlers, sessions, storage, transports, and crypto. +- `compiler/`: API schemas, error definitions, templates, and generators. +- `tests/`: pytest suites for filters, parsers, and file identifiers. +- `docs/source/`: Sphinx sources; `_static/` contains assets. +- `news/`: Towncrier fragments; `dev_tools/`: maintenance scripts. + +See `CONTRIBUTING.md` for the full workflow. + +## References + +- [Hydrogram documentation](https://docs.hydrogram.org): public API behavior, usage examples, and library conventions. Prefer the local sources in `docs/source/` when working against this checkout. +- [Corefork Telegram API documentation](https://corefork.telegram.org/api) and [MTProto documentation](https://corefork.telegram.org/mtproto): API methods, constructors, and protocol details, including authorization, transport, and update handling. +- Schemas in `compiler/`: reference for the API layer implemented by this checkout. Check that external documentation matches the supported layer before applying changes; newer methods, constructors, or fields may require a schema update and regeneration. + +## Build, Test, and Development Commands + +Run from the repository root: + +- `uv sync --all-extras --dev`: install development tools and optional dependencies. +- `uv run make api`: generate raw bindings and exceptions before testing fresh checkouts. +- `uv run pytest tests`: run tests. +- `uv run ruff format .`: format; `uv run ruff check .`: lint as CI does. +- `uv run pre-commit install`: enable hooks; `uv run pre-commit run --all-files`: check everything. +- `uv build`: build distributions with Hatchling; the wheel hook generates the API automatically. +- `uv run make docs`: generate references and build HTML; `uv run make live-docs`: preview. + +## Coding Style & Naming Conventions + +Use four spaces (two for YAML), UTF-8, LF, and final newlines. Ruff targets 99-character lines. Use `snake_case` functions/modules, `PascalCase` classes, and `UPPER_SNAKE_CASE` constants. + +Type public APIs; use `from __future__ import annotations` for postponed annotations, including `str | None`. Keep runtime expressions compatible with 3.11; move type-only imports into `TYPE_CHECKING` blocks. Follow neighboring Sphinx docstrings, including parameters, returns, and examples. + +Preserve existing copyright notices. New Python files need the LGPL header with Hydrogram's copyright; see `compiler/docs/compiler.py`. + +## Testing Guidelines + +Use pytest, pytest-asyncio (`@pytest.mark.asyncio`), and pytest-cov. Name files `test_*.py` and functions `test_*`. Add regression/behavioral tests; run focused tests, then the suite. Coverage: `uv run pytest tests --cov=hydrogram`; no minimum is configured. CI covers Python 3.11–3.14 on Linux/macOS. + +## Commit & Pull Request Guidelines + +Use Conventional Commits, e.g. `fix(session): prevent connection cleanup race`. Follow `.github/PULL_REQUEST_TEMPLATE.md`: explain changes, link issues, document validation and OS/Python versions. Update documentation and add `news/..rst` (`feature`, `bugfix`, `doc`, `removal`, `misc`). + +## Architecture & Implementation Boundaries + +- Edit compiler inputs/templates and regenerate API output. +- Register method classes in `hydrogram/methods//__init__.py` mixins to expose them on `Client`. +- Derive public Telegram objects from `Object`; export types through subpackage and `hydrogram/types/__init__.py`. Preserve client binding for bound methods. +- Implement storage backends against `hydrogram/storage/base.py:BaseStorage`. +- Keep blocking work off the event loop; preserve executor paths for synchronous callbacks/crypto. +- Never commit credentials or session files. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96a97e3be..57cc46441 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,176 +1,175 @@ # Contributing to Hydrogram -Welcome, and thank you for your interest in contributing to Hydrogram! +Contributions are welcome: bug reports, features, tests, documentation, and code reviews. Read the [Code of Conduct](https://github.com/hydrogram/.github/blob/main/CODE_OF_CONDUCT.md) and [repository guidelines](AGENTS.md) before contributing. -Hydrogram is an open source project that depends on the generous help of contributors who volunteer their time and skills. We appreciate your involvement and support. +## Questions, Feedback, and Issues -You can contribute in many ways, not only by writing code. This document aims to give you a high-level overview of how you can get involved. +Use [GitHub Discussions](https://github.com/orgs/hydrogram/discussions) for questions and general feedback. Search existing issues before reporting a bug or requesting a feature, then use the appropriate issue template. -## Asking Questions +For bug reports, include: -If you have a question, please use the [discussions](https://github.com/orgs/hydrogram/discussions) instead of opening an issue. Simply start a discussion with the Q&A topic and our community will gladly answer. +- A minimal reproducible example and the steps to run it. +- Expected behavior, actual behavior, and relevant tracebacks. +- Hydrogram and Python versions, operating system, and relevant optional dependencies. -## Providing Feedback +For feature requests, explain the use case, proposed behavior, and any relevant Telegram API references. Remove API hashes, bot tokens, session strings, and other credentials from examples and logs. Never attach session databases. -Feedback from the community is very important to us at Hydrogram. If you have any suggestions, ideas, or concerns, please share them with us. You can use the [discussions](https://github.com/orgs/hydrogram/discussions) to start a conversation. +## Set Up the Development Environment -You might be wondering about the difference between asking questions, providing feedback, and making feature requests. Asking questions is about seeking help or clarification. Providing feedback involves sharing your thoughts and opinions on the existing features and the project as a whole. Feature requests, on the other hand, are about suggesting new features or improvements. See the next section for more details on feature requests. +Install [uv](https://docs.astral.sh/uv/) and Git. Hydrogram supports Python 3.11 and later; `.python-version` selects Python 3.14 for local development. The Makefile commands below also require `make`. -## Creating Issues +Fork the repository, then run these commands, replacing `YOUR_USERNAME` with your GitHub username: -Have you identified a reproducible problem in Hydrogram? Do you have a feature request? Here's how you can report your issue as efficiently as possible. - -### Look For an Existing Issue - -Before creating a new issue, check the open issues to see if someone else has already reported the same problem or requested the same feature. - -If you find an existing issue that is relevant to yours, you can comment on it and express your support or disagreement with a reaction: - -- upvote 👍 -- downvote 👎 - -If none of the open issues match your case, then create a new issue following the guidelines below.. - -### How to Write a Bug Report +```bash +git clone https://github.com/YOUR_USERNAME/hydrogram.git +cd hydrogram +git remote add upstream https://github.com/hydrogram/hydrogram.git +git switch -c fix/describe-your-change +uv sync --locked --all-extras --dev +uv run make api +uv run pre-commit install +``` -A bug report should contain the following information: +Run subsequent commands from the repository root. `uv run` uses the project's virtual environment without requiring activation. -- A clear and descriptive title that summarizes the problem. -- A brief description of what you were doing when the bug occurred, and what you expected to happen. -- A detailed description of the actual behavior and how it differs from the expected behavior. -- Steps to reproduce the bug, preferably with screenshots or a video recording. -- The version of Hydrogram and the operating system you are using. -- Any relevant error messages or logs. +`--locked` checks that `uv.lock` matches the project without updating it. The standard +`dependency-groups.dev` group installs testing and development tools and is included by +default; `--dev` makes that selection explicit. Use `uv add --dev` to add development +dependencies and commit both `pyproject.toml` and `uv.lock`. -### How to Write a Feature Request +`--all-extras` also installs Sphinx documentation dependencies and the optional +`fast` dependencies, including TgCrypto and, on supported platforms, uvloop. The +performance extras are optional for using Hydrogram. For a smaller development +environment, use `uv sync --locked --dev`; add `--extra docs` when working on +documentation. -A feature request should contain the following information: +To refresh all locked dependencies, run `uv lock --upgrade`, then synchronize and +validate the environment. Keep dependency lower bounds compatible with Python 3.11; +the universal lockfile selects newer releases on Python versions that support them. +Keep the Ruff pre-commit revision, `ruff.toml`'s `required-version`, and the exact +version declared in `pyproject.toml` aligned. Ruff's linter and formatter use preview +mode to retain the existing rules and style, so review their changes when upgrading. -- A clear and descriptive title that summarizes the feature. -- A brief explanation of why you need this feature and how it would benefit the project and the community. -- A detailed description of how the feature should work and what it should look like, preferably with mockups or examples. -- Optionally any relevant links or references to similar features in other projects or applications. +## Find and Change the Right Code -## Creating a Pull Request +- `hydrogram/`: the client library, including `methods/`, `types/`, filters, handlers, sessions, storage, and transports. +- `compiler/`: API schemas, RPC error definitions, templates, and generators. +- `tests/`: unit tests, including filter, parser, and file identifier tests. +- `docs/source/`: Sphinx sources and static assets. +- `news/`: release note fragments; `dev_tools/` contains maintenance scripts. -To contribute code to Hydrogram, simply open a pull request by following the guide below. +Generated raw API modules and RPC exceptions are ignored by Git. Change sources or templates under `compiler/api/` or `compiler/errors/`, then run `uv run make api`. Do not patch generated output directly. -You can read more about pull requests in the [GitHub docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). +For schema updates, follow [Updating the MTProto Schema](docs/source/topics/schema-updates.rst). +The update checker is read-only by default; `make update-api-schema` validates a +candidate in a temporary checkout before applying it. `make validate-api-schema` +checks pinned provenance, deterministic generation, and the complete offline suite. +Raw layer support and high-level Client feature coverage are distinct. -### Setting Up Development Environment +When adding a client method, follow the existing method-class pattern and import and register the class in `hydrogram/methods//__init__.py`. Export new public types through the relevant package `__init__.py` files. Keep asynchronous operations nonblocking and preserve Python 3.11 compatibility. -1. Fork the Hydrogram repository to your GitHub account. -2. Clone your forked repository of Hydrogram to your computer: +## Coding Style -```bash -git clone https://github.com//hydrogram -cd hydrogram -``` +Use four-space indentation, UTF-8, LF endings, and final newlines; YAML uses two spaces. Follow existing naming conventions: `snake_case` for modules and functions, `PascalCase` for classes, and `UPPER_SNAKE_CASE` for constants. Preserve license headers and document public APIs with type hints and docstrings matching neighboring code. -4. Add a track to the original repository: +Ruff handles formatting and linting, with a target line length of 99 characters. Format changes, then run the same style checks as CI: ```bash -git remote add upstream https://github.com/hydrogram/hydrogram +uv run ruff format . +uv run ruff format --check . +uv run ruff check . +uv run pre-commit run --all-files ``` -5. Install dependencies: +The shared `ruff.toml` ignores `E501` because the formatter wraps lines on a +best-effort basis, and excludes generated API modules and exceptions even when +passed directly. The Python target is inferred from `requires-python` in +`pyproject.toml`; keep it aligned with the library's minimum supported version. -Hydrogram uses and recommends [Rye](https://rye-up.com/) for managing virtual environmens and dependencies. +Pre-commit also checks whitespace, merge conflicts, private keys, TOML/YAML, and +release fragment filenames. Its Ruff hooks apply safe lint fixes before formatting; +review the resulting diff and rerun checks after fixes. Apply unsafe fixes only +deliberately with `uv run ruff check --fix --unsafe-fixes`, then review and test any +behavior changes. -```bash -rye sync --all-features -``` +## Testing -> We use `--all-features` to install all the optional dependencies, which are required to run the tests and build the documentation. +Add regression tests for bug fixes and behavioral tests for new features. Use `test_*.py` filenames and `test_*` functions, placing tests alongside related suites. For asynchronous tests, use pytest-asyncio with explicit `@pytest.mark.asyncio` markers. -6. Install pre-commit hooks: - -[Pre-commit](https://pre-commit.com/) is a tool that runs various checks before you make a commit. It helps you avoid committing any errors or warnings that might break your code or violate the coding standards. +Generate the API before testing a fresh checkout or after compiler changes: ```bash -pre-commit install +uv run make api +uv run pytest tests ``` -### Format the code (code-style) - -Hydrogram uses [Ruff](https://docs.astral.sh/ruff/) for linting and formatting the code to maintain it consistent and clean. You should [install](https://docs.astral.sh/ruff/installation/) and run Ruff on your code before committing: +Run a focused test or collect coverage with: ```bash -ruff check . +uv run pytest tests/filters/test_command.py::test_single +uv run pytest tests --cov=hydrogram --cov-report=term-missing ``` -However, you can also rely on [pre-commit](https://pre-commit.com/) for it: +No minimum coverage percentage is configured. CI runs tests on Python 3.11–3.14 on Linux and macOS. Prefer isolated tests with local fixtures or test doubles, following the existing filter tests. -```bash -pre-commit run --run-all-files -``` +## Documentation and Package Builds -### Run tests - -All changes should be tested: +Update documentation when public behavior changes. Edit handwritten Sphinx sources under `docs/source/` and API docstrings in the library. Regenerate API reference pages before building or previewing: ```bash -pytest tests +uv run make api +uv run make docs +uv run make live-docs ``` -Remember to write tests for your new features or modify the existing tests to cover your code changes. Testing is essential to ensure the quality and reliability of your code. - -### Docs +`make docs` generates reference pages and builds HTML in `docs/build/html/`; it does not start a server. `make live-docs` starts the live preview and watches `hydrogram/`. Generated reference pages are ignored by Git; edit their source docstrings or compiler templates instead. -We use Sphinx to generate documentation in the `docs` directory. You can edit the sources and preview the changes using a live-preview server with: +To build source and wheel distributions in `dist/`: ```bash -sphinx-autobuild docs/source/ docs/build/ --watch hydrogram/ +uv run --locked make build ``` -### Commit Messages +Hatchling's custom wheel hook generates raw API bindings and exceptions for both +regular and editable installations. `make build` runs `uv build --no-sources`; +`uv build`, `pip install .`, and Git/archive installations also work directly. +Source distributions include the compiler inputs and build hook, and regenerate +the API when a wheel is built from them. Generated modules are explicitly included +as wheel artifacts even though Git ignores them. Both distributions use core +metadata 2.4 for compatibility with the validation tools pinned in `uv.lock`. -We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), which provide a standardized and structured format for commit messages. This helps ensure clear and consistent communication about the changes made in each commit. The commit messages should have the following structure: +The package version is read from `__version__` in `hydrogram/__init__.py`; update +that value for a release, then update `uv.lock`. Hatchling is an isolated build +dependency and does not need to be installed as a development or runtime dependency. +The uv cache tracks the version source, build hook, compiler inputs, and NOTICE; +rerun `uv run make api` after compiler changes if regenerating manually. -``` -[optional scope]: +## Commits and Release Notes -[optional body] - -[optional footer(s)] -``` - -where: - -- ``: This is a keyword that indicates the kind of change that the commit introduces. It can be one of the following values: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, or `revert`. -- `[optional scope]`: This is an optional identifier that specifies the scope of the change. It can be a module, a component, a file, or any other logical unit of the project. -- ``: This is a short and concise summary of the change in the present tense and imperative mood. It should not end with a period. -- `[optional body]`: This is an optional section that provides more details and context about the change. It should be separated from the summary by a blank line and wrapped at 72 characters. -- `[optional footer(s)]`: This is an optional section that provides additional information such as references to issues, breaking changes, or acknowledgments. It should be separated from the body by a blank line and follow the format `: `. - -For example: +Use Conventional Commits: +```text +[optional scope]: ``` -feat(storage): support multiple database using orm -This commit adds support for multiple databases using the tortoise-orm. It also updates the configuration file to allow the user to specify the database type and connection string. +Types include `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, and `revert`. Use a short imperative summary without a trailing period. Separate optional body and footer paragraphs with blank lines, and wrap body text at 72 characters. For example: -BREAKING CHANGE: The database configuration has changed. The user must update the configuration file to specify the database type and connection string. +```text +fix(session): prevent race condition during connection cleanup Closes #123 ``` -### Describing Changes +Add a concise, user-facing release note at `news/..rst`. Categories are `feature`, `bugfix`, `doc`, `removal`, and `misc`. For example, `news/123.bugfix.rst` could contain: -Write a concise summary of your changes in one or more sentences, so that bot developers can see what's new or updated in the library. Create a file named `..rst` in the `news` directory and include your description there. - -`` is the issue or pull request number. After the release, a link to this issue will be added to the [changelog](https://docs.hydrogram.org/en/latest/releases.html) page. - -`` is a marker for the type of change, which can be one of: +```rst +Prevent a race condition when closing a connection. +``` -- `feature` - when you add a new feature -- `bugfix` - when you fix a bug -- `doc` - when you improve the documentation -- `removal` - when you remove something from the library -- `misc` - when you change something in the core or the project configuration +Preview release notes with `uv run make towncrier-draft`. Leave `NEWS.rst` assembly to the release process. -If you are not sure which category to use, you can ask the core contributors for help. +## Open a Pull Request -## Thank You +Follow `.github/PULL_REQUEST_TEMPLATE.md`. Include the motivation, behavior changes, linked issues, and any new dependencies. Describe the tests you ran, how to reproduce them, and your OS and Python version. Identify breaking changes and update affected documentation. -Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute. +Before submitting, review the diff, run the tests and style checks above, and ensure your release fragment describes the final change. Keep the pull request focused so reviewers can assess it independently. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index d9c50a913..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -## Include -include README.md COPYING COPYING.lesser NOTICE -recursive-include compiler *.py *.tl *.tsv *.txt -recursive-include tests *.py - -## Exclude -prune hydrogram/errors/exceptions -prune hydrogram/raw/functions -prune hydrogram/raw/types -prune hydrogram/raw/base -exclude hydrogram/raw/all.py diff --git a/Makefile b/Makefile index e6e8443c9..d2ea846b9 100644 --- a/Makefile +++ b/Makefile @@ -1,58 +1,103 @@ -VENV := .venv -PYTHON := $(VENV)/bin/python -HOST = $(shell ifconfig | grep "inet " | tail -1 | cut -d\ -f2) -TAG = v$(shell grep -E '__version__ = ".*"' hydrogram/__init__.py | cut -d\" -f2) +UV = uv +PYTHON = python +SPHINX_BUILD = sphinx-build +SPHINX_AUTOBUILD = sphinx-autobuild +TOWNCRIER = towncrier -RM := rm -rf +HYDROGRAM_DIR = hydrogram +DOCS_DIR = docs +DOCS_SOURCE = $(DOCS_DIR)/source +DOCS_BUILD = $(DOCS_DIR)/build +API_DIRS = $(HYDROGRAM_DIR)/errors/exceptions $(HYDROGRAM_DIR)/raw/all.py $(HYDROGRAM_DIR)/raw/base $(HYDROGRAM_DIR)/raw/functions $(HYDROGRAM_DIR)/raw/types +DOCS_API_DIRS = $(DOCS_SOURCE)/api/bound-methods $(DOCS_SOURCE)/api/methods $(DOCS_SOURCE)/api/types $(DOCS_SOURCE)/telegram -.PHONY: venv clean-build clean-api clean api build clean-docs docs towncrier-build towncrier-draft +.PHONY: all build clean clean-api clean-docs api api-raw api-errors docs docs-compile docs-serve live-docs towncrier towncrier-draft dev-tools check-api-schema update-api-schema validate-api-schema generate-docs-json compare-bot-api cherry-pick-pyro help -venv: - $(RM) $(VENV) - python3 -m venv $(VENV) - $(PYTHON) -m pip install -U pip wheel setuptools - $(PYTHON) -m pip install -U -e . - @echo "Created venv with $$($(PYTHON) --version)" +all: api docs -clean-build: - $(RM) *.egg-info build dist +# Hatchling generates the API automatically when building the wheel. +build: + @$(UV) build --no-sources -clean-docs: - $(RM) docs/build - $(RM) docs/source/api/bound-methods docs/source/api/methods docs/source/api/types docs/source/telegram +clean: clean-api clean-docs + @echo "All directories cleaned successfully" clean-api: - $(RM) hydrogram/errors/exceptions hydrogram/raw/all.py hydrogram/raw/base hydrogram/raw/functions hydrogram/raw/types + @echo "Cleaning generated API files..." + @rm -rf $(API_DIRS) -clean: - make clean-build - make clean-api +clean-docs: + @echo "Cleaning generated documentation..." + @rm -rf $(DOCS_BUILD) $(DOCS_API_DIRS) -api: - cd compiler/api && ../../$(PYTHON) compiler.py - cd compiler/errors && ../../$(PYTHON) compiler.py +api: api-raw api-errors + @echo "API compilation finished" -docs: - make clean-docs - cd compiler/docs && ../../$(PYTHON) compiler.py - $(VENV)/bin/sphinx-build \ - -b html "docs/source" "docs/build/html" -j auto +api-raw: + @echo "Compiling raw API..." + @$(PYTHON) -c "from compiler.api.compiler import start; start()" -build: - make clean - $(PYTHON) setup.py sdist - $(PYTHON) setup.py bdist_wheel +api-errors: + @echo "Compiling API errors..." + @$(PYTHON) -c "from compiler.errors.compiler import start; start()" -tag: - git tag $(TAG) - git push origin $(TAG) +docs: docs-compile docs-serve -dtag: - git tag -d $(TAG) - git push origin -d $(TAG) +docs-compile: + @echo "Compiling documentation..." + @$(PYTHON) -c "from compiler.docs.compiler import start; start()" -towncrier-build: - towncrier build --yes +docs-serve: + @echo "Building and serving documentation..." + @$(SPHINX_BUILD) -b html $(DOCS_SOURCE) $(DOCS_BUILD)/html -j auto + +live-docs: + @echo "Starting documentation server with live reload..." + @$(SPHINX_AUTOBUILD) $(DOCS_SOURCE) $(DOCS_BUILD)/html -j auto --watch $(HYDROGRAM_DIR) + +towncrier: + @echo "Generating release notes..." + @$(TOWNCRIER) build --yes towncrier-draft: - towncrier build --draft + @echo "Generating draft release notes..." + @$(TOWNCRIER) build --draft + +check-api-schema: + @echo "Checking Telegram API schema for updates..." + @$(PYTHON) dev_tools/check_api_schema_updates.py + +update-api-schema: + @$(PYTHON) dev_tools/check_api_schema_updates.py --apply + +validate-api-schema: + @$(PYTHON) dev_tools/check_api_schema_updates.py --validate + +generate-docs-json: + @echo "Generating API documentation JSON..." + @$(PYTHON) dev_tools/generate_docs_json.py + +compare-bot-api: + @echo "Comparing implementation against Bot API..." + @$(PYTHON) dev_tools/compare_to_bot_api.py + +cherry-pick-pyro: + @echo "Usage: make cherry-pick-pyro TYPE= ID=" + @[ "$(TYPE)" ] && [ "$(ID)" ] && $(PYTHON) dev_tools/cherry_pick_pyro.py $(TYPE) $(ID) || echo "Please provide TYPE and ID parameters" + +help: + @echo "Available targets:" + @echo " all : Compile API and documentation" + @echo " build : Generate API and build source and wheel distributions" + @echo " clean : Remove all generated files" + @echo " api : Compile all API components" + @echo " docs : Compile and serve documentation" + @echo " live-docs : Start documentation server with live reload" + @echo " towncrier : Generate release notes" + @echo " towncrier-draft: Generate draft release notes" + @echo " check-api-schema: Check Telegram API schema for updates" + @echo " update-api-schema: Validate and apply the latest verified schema" + @echo " validate-api-schema: Validate local schema and deterministic generation offline" + @echo " generate-docs-json: Generate API documentation JSON" + @echo " compare-bot-api: Compare implementation against Bot API" + @echo " cherry-pick-pyro: Cherry-pick code from Pyrogram (usage: make cherry-pick-pyro TYPE= ID=)" diff --git a/NEWS.rst b/NEWS.rst index 85fcecb76..563f26ad8 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -72,7 +72,7 @@ Misc - Added the `from __future__ import annotations` statement to the codebase in order to simplify the usage of the typing module. This statement allows for the use of forward references in type hints, which can improve code readability and maintainability. `#24 `_ - Various fixes, improvements and micro-optimizations. - + 0.1.4 (2023-12-04) diff --git a/README.md b/README.md index 95e0850c2..ac4d53e4e 100644 --- a/README.md +++ b/README.md @@ -29,37 +29,50 @@ # Hydrogram [![We use Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![PyPI package version](https://img.shields.io/pypi/v/hydrogram.svg)](https://pypi.python.org/pypi/hydrogram) -[![PyPI license](https://img.shields.io/pypi/l/hydrogram.svg)](https://pypi.python.org/pypi/hydrogram) -[![PyPI python versions](https://img.shields.io/pypi/pyversions/hydrogram.svg)](https://pypi.python.org/pypi/hydrogram) -[![PyPI download month](https://img.shields.io/pypi/dm/hydrogram.svg)](https://pypi.python.org/pypi/hydrogram/) -[![GitHub Actions status](https://github.com/hydrogram/hydrogram/actions/workflows/python.yml/badge.svg)](https://github.com/hydrogram/hydrogram/actions) +[![PyPI package version](https://img.shields.io/pypi/v/hydrogram.svg)](https://pypi.org/project/hydrogram/) +[![PyPI license](https://img.shields.io/pypi/l/hydrogram.svg)](COPYING.lesser) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/hydrogram.svg)](https://pypi.org/project/hydrogram/) +[![Monthly PyPI downloads](https://img.shields.io/pypi/dm/hydrogram.svg)](https://pypi.org/project/hydrogram/) +[![Tests](https://github.com/hydrogram/hydrogram/actions/workflows/python.yml/badge.svg)](https://github.com/hydrogram/hydrogram/actions/workflows/python.yml) -## Description - -Hydrogram is a Python library for interacting with the Telegram MTProto API. It provides a simple and intuitive interface for developers to leverage the power of Telegram's API in their Python applications. +Hydrogram is an asynchronous Python library for the Telegram MTProto API. +Build bots and user clients with high-level methods, message handlers, and filters, +or access the raw API when you need more control. ## Installation -To install Hydrogram, you need Python 3 installed on your system. If you don't have Python installed, you can download it from the official website. +Requires **Python 3.11 or later**. Install or upgrade with pip: + +```bash +python -m pip install --upgrade hydrogram +``` -To install Hydrogram, use pip: +To include optional performance dependencies (TgCrypto and, on supported platforms, uvloop): ```bash -pip install hydrogram -U +python -m pip install --upgrade "hydrogram[fast]" ``` -## Usage +See the [speedups guide](https://docs.hydrogram.org/en/latest/topics/speedups.html) +for details, including how to enable uvloop. -Here is a basic example of how to use Hydrogram: +## Quick start + +Get your `api_id` and `api_hash` from [my.telegram.org/apps](https://my.telegram.org/apps). +Replace the placeholders below with your credentials and save the example as `hello.py`. +It replies to private messages from other users: ```python from hydrogram import Client, filters -app = Client("my_account") +app = Client( + "my_account", + api_id=12345, + api_hash="0123456789abcdef0123456789abcdef", +) -@app.on_message(filters.private) +@app.on_message(filters.private & ~filters.me) async def hello(client, message): await message.reply("Hello from Hydrogram!") @@ -67,32 +80,39 @@ async def hello(client, message): app.run() ``` +Run `python hello.py` and follow the sign-in prompts. Once connected, send a private +message to the account from another account to see the reply. +Hydrogram saves the login in `my_account.session` for subsequent runs; keep this file private. + +To sign in as a bot, also pass `bot_token` to `Client`. +See [authorization](https://docs.hydrogram.org/en/latest/start/auth.html) +and the [examples](https://docs.hydrogram.org/en/latest/start/examples/index.html) for more. + ## Features -- **Easy to use:** Hydrogram provides a simple and intuitive interface for developers to leverage the power of Telegram's API in their Python applications, while still allowing advanced usages. -- **Elegant:** Low-level details are abstracted and re-presented in a more convenient way, making the Telegram API more accessible. -- **Fast:** Hydrogram is boosted by [TgCrypto](https://github.com/pyrogram/tgcrypto), a high-performance cryptography library written in C, which makes it faster than other Python Telegram libraries. -- **Type-hinted:** Types and methods are all type-hinted, enabling excellent editor support and making it easier to write and maintain code. -- **Async:** Hydrogram is fully asynchronous, which means it can handle multiple requests at the same time, making it faster and more efficient. -- **Powerful:** Hydrogram provides full access to Telegram's API to execute any official client action and more, giving developers the flexibility to build powerful applications. +- **Async API:** Use `async` and `await` to handle updates and make concurrent requests. +- **High-level interface:** Work with messages, chats, and users through convenient methods and bound objects. +- **Handlers and filters:** Route updates to callbacks with composable filters. +- **Raw API access:** Call MTProto methods from the API layer supported by Hydrogram. +- **Type hints:** Get editor completion and support for static analysis. +- **Optional speedups:** Use TgCrypto for faster cryptography and uvloop as an alternative event loop. ## Resources - The [documentation](https://docs.hydrogram.org) is the technical reference for Hydrogram. It includes detailed usage guides, API reference, and more. - The [homepage](https://hydrogram.org) is the official website for Hydrogram. It includes a quickstart guide, a list of features, and more. - Our [Telegram channel](https://t.me/HydrogramNews) is where we post news and updates about Hydrogram. +- Ask questions in [GitHub Discussions](https://github.com/orgs/hydrogram/discussions), or report bugs through [GitHub Issues](https://github.com/hydrogram/hydrogram/issues). ## Contributing Hydrogram is an open source project and we welcome contributions from the community. We appreciate all types of contributions, including bug reports, feature requests, documentation improvements, and code contributions. -To get started, please review our [Contribution Guidelines](https://github.com/hydrogram/hydrogram/blob/main/CONTRIBUTING.md) for more information. +See the [Contributing Guide](CONTRIBUTING.md) for development setup, tests, and pull request guidelines. All contributors are expected to adhere to the [Code of Conduct](https://github.com/hydrogram/.github/blob/main/CODE_OF_CONDUCT.md). Please read it before contributing. -We appreciate your help in making Hydrogram better! - -## Support +## Support development Hydrogram is an open source project. Your support helps us maintain and improve the library. You can support the development of Hydrogram through the following platforms: @@ -105,4 +125,6 @@ Hydrogram is an open source project. Your support helps us maintain and improve ## License -You may copy, distribute and modify the software provided that modifications are described and licensed for free under [LGPL-3](https://www.gnu.org/licenses/lgpl-3.0.html). Derivatives works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3, but applications that use the library don't have to be. +Hydrogram is licensed under the **GNU Lesser General Public License v3.0 or later** +(`LGPL-3.0-or-later`). See [COPYING.lesser](COPYING.lesser) and [COPYING](COPYING) +for the full license terms, and [NOTICE](NOTICE) for copyright information. diff --git a/compiler/api/compiler.py b/compiler/api/compiler.py index c68291bb9..668cf81a0 100644 --- a/compiler/api/compiler.py +++ b/compiler/api/compiler.py @@ -124,7 +124,7 @@ def get_type_hint(type: str) -> str: if type in {"Object", "!X"}: return "TLObject" - if re.match("^vector", type, re.IGNORECASE): + if re.match(r"^vector", type, re.IGNORECASE): is_core = True sub_type = type.split("<")[1][:-1] @@ -135,7 +135,7 @@ def get_type_hint(type: str) -> str: ns, name = type.split(".") if "." in type else ("", type) type = '"raw.base.' + ".".join([ns, name]).strip(".") + '"' - return f'{type}{" = None" if is_flag else ""}' + return f"{type}{' = None' if is_flag else ''}" def sort_args(args: list[tuple[str, str]]): @@ -198,10 +198,45 @@ def get_references(t: str, kind: str): return ("\n ".join(items), len(items)) if items else (None, 0) +def validate_schema(schema: list[str]) -> None: + """Reject unsupported declarations instead of silently omitting wire fields.""" + seen_names = set() + seen_ids = set() + section = None + argument_re = re.compile(r"\w+:(?:flags\d?\.\d+\?)?(?:[\w.]+(?:<[\w.]+>)?|#|!X)") + for original in schema: + line = original.split("//", 1)[0].strip() + if not line: + continue + if line in {"---types---", "---functions---"}: + section = line + continue + match = COMBINATOR_RE.fullmatch(line) + if not match or section is None: + raise ValueError(f"Unsupported TL declaration: {line}") + name, constructor_id, _ = match.groups() + if name in seen_names or int(constructor_id, 16) in seen_ids: + raise ValueError(f"Duplicate TL declaration: {name}") + seen_names.add(name) + seen_ids.add(int(constructor_id, 16)) + tokens = line.split() + for token in tokens[1 : tokens.index("=")]: + if token != "{X:Type}" and not argument_re.fullmatch(token): + raise ValueError(f"Unsupported TL argument in {name}: {token}") + + def start(format: bool = False): - shutil.rmtree(DESTINATION_PATH / "types", ignore_errors=True) - shutil.rmtree(DESTINATION_PATH / "functions", ignore_errors=True) - shutil.rmtree(DESTINATION_PATH / "base", ignore_errors=True) + # Compilations can run repeatedly in a build process. Never retain references + # (including constructors removed by a later schema) from an earlier run. + for mapping in ( + types_to_constructors, + types_to_functions, + constructors_to_functions, + namespaces_to_types, + namespaces_to_constructors, + namespaces_to_functions, + ): + mapping.clear() with ( open(API_HOME_PATH / "source/auth_key.tl") as f1, @@ -210,6 +245,11 @@ def start(format: bool = False): ): schema = (f1.read() + f2.read() + f3.read()).splitlines() + validate_schema(schema) + shutil.rmtree(DESTINATION_PATH / "types", ignore_errors=True) + shutil.rmtree(DESTINATION_PATH / "functions", ignore_errors=True) + shutil.rmtree(DESTINATION_PATH / "base", ignore_errors=True) + with ( open(API_HOME_PATH / "template/type.txt") as f1, open(API_HOME_PATH / "template/combinator.txt") as f2, @@ -376,7 +416,7 @@ def start(format: bool = False): arg_docs = arg_docs["params"].get(arg_name, "N/A") if arg_docs else "N/A" docstring_args.append( - f'{arg_name} ({get_docstring_arg_type(arg_type)}{", *optional*" if is_optional else ""}):\n {arg_docs}\n' + f"{arg_name} ({get_docstring_arg_type(arg_type)}{', *optional*' if is_optional else ''}):\n {arg_docs}\n" ) if c.section == "types": @@ -420,7 +460,7 @@ def start(format: bool = False): if arg_name != f"flags{flag.group(1)}": continue - if flag.group(3) == "true" or flag.group(3).startswith("Vector"): + if flag.group(3) == "true": write_flags.append( f"{arg_name} |= (1 << {flag.group(2)}) if self.{i[0]} else 0" ) @@ -455,13 +495,14 @@ def start(format: bool = False): read_types += f"{arg_name} = {flag_type.title()}.read(b) if flags{number} & (1 << {index}) else None" elif "vector" in flag_type.lower(): sub_type = arg_type.split("<")[1][:-1] + bare = flag_type.startswith("vector<") write_types += "\n " write_types += f"if self.{arg_name} is not None:\n " - write_types += f'b.write(Vector(self.{arg_name}{f", {sub_type.title()}" if sub_type in CORE_TYPES else ""}))\n ' + write_types += f"b.write(Vector(self.{arg_name}{f', {sub_type.title()}' if sub_type in CORE_TYPES else ''}){'[4:]' if bare else ''})\n " read_types += "\n " - read_types += f'{arg_name} = TLObject.read(b{f", {sub_type.title()}" if sub_type in CORE_TYPES else ""}) if flags{number} & (1 << {index}) else []\n ' + read_types += f"{arg_name} = {'Vector' if bare else 'TLObject'}.read(b, {sub_type.title() if sub_type in CORE_TYPES else 'TLObject'}) if flags{number} & (1 << {index}) else None\n " else: write_types += "\n " write_types += f"if self.{arg_name} is not None:\n " @@ -478,11 +519,12 @@ def start(format: bool = False): read_types += f"{arg_name} = {arg_type.title()}.read(b)\n " elif "vector" in arg_type.lower(): sub_type = arg_type.split("<")[1][:-1] + bare = arg_type.startswith("vector<") - write_types += f'b.write(Vector(self.{arg_name}{f", {sub_type.title()}" if sub_type in CORE_TYPES else ""}))\n ' + write_types += f"b.write(Vector(self.{arg_name}{f', {sub_type.title()}' if sub_type in CORE_TYPES else ''}){'[4:]' if bare else ''})\n " read_types += "\n " - read_types += f'{arg_name} = TLObject.read(b{f", {sub_type.title()}" if sub_type in CORE_TYPES else ""})\n ' + read_types += f"{arg_name} = {'Vector' if bare else 'TLObject'}.read(b, {sub_type.title() if sub_type in CORE_TYPES else 'TLObject'})\n " else: write_types += f"b.write(self.{arg_name}.write())\n " diff --git a/compiler/api/docs.json b/compiler/api/docs.json index bae465027..5d5da25e5 100644 --- a/compiler/api/docs.json +++ b/compiler/api/docs.json @@ -6,12 +6,6 @@ "days": "This account will self-destruct in the specified number of days" } }, - "AppWebViewResultUrl": { - "desc": "Contains the link that must be used to open a direct link Mini App.", - "params": { - "url": "The URL to open" - } - }, "AttachMenuBot": { "desc": "Represents a bot mini app that can be launched from the attachment/side menu \u00bb", "params": { @@ -48,7 +42,7 @@ "desc": "Represents a list of bot mini apps that can be launched from the attachment menu \u00bb", "params": { "bots": "List of bot mini apps that can be launched from the attachment menu \u00bb", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "users": "Info about related users/bots" } }, @@ -83,6 +77,14 @@ "desc": "The bot attachment menu entry is available in the chat with the bot that offers it", "params": {} }, + "AuctionBidLevel": { + "desc": "{schema}", + "params": { + "amount": "", + "date": "", + "pos": "" + } + }, "Authorization": { "desc": "Logged-in session", "params": { @@ -141,15 +143,15 @@ } }, "AvailableEffect": { - "desc": "{schema}", + "desc": "Represents a message effect \u00bb.", "params": { - "effect_animation_id": "", - "effect_sticker_id": "", - "emoticon": "", + "effect_animation_id": "If set, contains the actual animated effect (TGS format \u00bb). If not set, the animated effect must be set equal to the premium animated sticker effect associated to the animated sticker specified in effect_sticker_id (always different from the preview animation, fetched thanks to the videoSize of type f as specified here \u00bb).", + "effect_sticker_id": "Contains the preview animation (TGS format \u00bb), used for the effect selection menu.", + "emoticon": "Emoji corresponding to the effect, to be used as icon for the effect if static_icon_id is not set.", "flags": "Flags, see TL conditional fields", - "id": "", - "premium_required": "", - "static_icon_id": "" + "id": "Unique effect ID.", + "premium_required": "Whether a Premium subscription is required to use this effect.", + "static_icon_id": "ID of the document containing the static icon (WEBP) of the effect." } }, "AvailableReaction": { @@ -197,12 +199,12 @@ "params": {} }, "Birthday": { - "desc": "{schema}", + "desc": "Birthday information for a user.", "params": { - "day": "", + "day": "Birth day", "flags": "Flags, see TL conditional fields", - "month": "", - "year": "" + "month": "Birth month", + "year": "(Optional) birth year." } }, "Boost": { @@ -211,11 +213,12 @@ "date": "When was the boost applied", "expires": "When does the boost expire", "flags": "Flags, see TL conditional fields", - "gift": "Whether this boost was applied because the channel directly gifted a subscription to the user.", - "giveaway": "Whether this boost was applied because the user was chosen in a giveaway started by the channel.", + "gift": "Whether this boost was applied because the channel/supergroup directly gifted a subscription to the user.", + "giveaway": "Whether this boost was applied because the user was chosen in a giveaway started by the channel/supergroup.", "giveaway_msg_id": "The message ID of the giveaway", "id": "Unique ID for this set of boosts.", "multiplier": "If set, this boost counts as multiplier boosts, otherwise it counts as a single boost.", + "stars": "Number of Telegram Stars distributed among the winners of the giveaway.", "unclaimed": "If set, the user hasn't yet invoked payments.applyGiftCode to claim a subscription gifted directly or in a giveaway by the channel.", "used_gift_slug": "The created Telegram Premium gift code, only set if either gift or giveaway are set AND it is either a gift code for the currently logged in user or if it was already claimed.", "user_id": "ID of the user that applied the boost." @@ -239,16 +242,27 @@ "desc": "Bot app info hasn't changed.", "params": {} }, + "BotAppSettings": { + "desc": "Mini app \u00bb settings", + "params": { + "background_color": "Default light mode background color", + "background_dark_color": "Default dark mode background color", + "flags": "Flags, see TL conditional fields", + "header_color": "Default light mode header color", + "header_dark_color": "Default dark mode header color", + "placeholder_path": "SVG placeholder logo, compressed using the same format used for vector thumbnails \u00bb." + } + }, "BotBusinessConnection": { - "desc": "{schema}", + "desc": "Contains info about a bot business connection.", "params": { - "can_reply": "", - "connection_id": "", - "date": "", - "dc_id": "", - "disabled": "", + "connection_id": "Business connection ID, used to identify messages coming from the connection and to reply to them as specified here \u00bb.", + "date": "When was the connection created.", + "dc_id": "ID of the datacenter where to send queries wrapped in a invokeWithBusinessConnection as specified here \u00bb.", + "disabled": "Whether this business connection is currently disabled", "flags": "Flags, see TL conditional fields", - "user_id": "" + "rights": "Business bot rights.", + "user_id": "ID of the user that the bot is connected to via this connection." } }, "BotCommand": { @@ -296,13 +310,17 @@ "BotInfo": { "desc": "Info about bots (available bot commands, etc)", "params": { + "app_settings": "Mini app \u00bb settings", "commands": "Bot commands that can be used in the chat", "description": "Description of the bot", "description_document": "Description animation in MPEG4 format", "description_photo": "Description photo", "flags": "Flags, see TL conditional fields", + "has_preview_medias": "If set, the bot has some preview medias for the configured Main Mini App, see here \u00bb for more info on Main Mini App preview medias.", "menu_button": "Indicates the action to execute when pressing the in-UI menu button for bots", - "user_id": "ID of the bot" + "privacy_policy_url": "The HTTP link to the privacy policy of the bot. If not set, then the /privacy command must be used, if supported by the bot (i.e. if it's present in the commands vector). If it isn't supported, then https://telegram.org/privacy-tpa must be opened, instead.", + "user_id": "ID of the bot", + "verifier_settings": "This bot can verify peers: this field contains more info about the verification the bot can assign to peers." } }, "BotInlineMediaResult": { @@ -353,7 +371,7 @@ "BotInlineMessageMediaInvoice": { "desc": "Send an invoice", "params": { - "currency": "Three-letter ISO 4217 currency code", + "currency": "Three-letter ISO 4217 currency code, or XTR for Telegram Stars.", "description": "Product description, 1-255 characters", "flags": "Flags, see TL conditional fields", "photo": "Product photo", @@ -432,143 +450,151 @@ "desc": "Placeholder bot menu button never returned to users: see the docs for more info.", "params": {} }, - "BroadcastRevenueBalances": { - "desc": "{schema}", - "params": { - "available_balance": "", - "current_balance": "", - "overall_revenue": "" - } - }, - "BroadcastRevenueTransactionProceeds": { - "desc": "{schema}", + "BotPreviewMedia": { + "desc": "Represents a Main Mini App preview media, see here \u00bb for more info.", "params": { - "amount": "", - "from_date": "", - "to_date": "" + "date": "When was this media last updated.", + "media": "The actual photo/video." } }, - "BroadcastRevenueTransactionRefund": { - "desc": "{schema}", + "BotVerification": { + "desc": "Describes a bot verification icon \u00bb.", "params": { - "amount": "", - "date": "", - "provider": "" + "bot_id": "ID of the bot that verified this peer", + "description": "Verification description", + "icon": "Verification icon" } }, - "BroadcastRevenueTransactionWithdrawal": { - "desc": "{schema}", + "BotVerifierSettings": { + "desc": "Info about the current verifier bot \u00bb.", "params": { - "amount": "", - "date": "", - "failed": "", + "can_modify_custom_description": "Indicates whether the bot is allowed to set a custom description field for individual verified peers, different from the custom_description provided here.", + "company": "The name of the organization that provides the verification", + "custom_description": "An optional default description for the verification", "flags": "Flags, see TL conditional fields", - "pending": "", - "provider": "", - "transaction_date": "", - "transaction_url": "" + "icon": "Verification icon" } }, "BusinessAwayMessage": { - "desc": "{schema}", + "desc": "Describes a Telegram Business away message, automatically sent to users writing to us when we're offline, during closing hours, while we're on vacation, or in some other custom time period when we cannot immediately answer to the user.", "params": { "flags": "Flags, see TL conditional fields", - "offline_only": "", - "recipients": "", - "schedule": "", - "shortcut_id": "" + "offline_only": "If set, the messages will not be sent if the account was online in the last 10 minutes.", + "recipients": "Allowed recipients for the away messages.", + "schedule": "Specifies when should the away messages be sent.", + "shortcut_id": "ID of a quick reply shorcut, containing the away messages to send, see here \u00bb for more info." } }, "BusinessAwayMessageScheduleAlways": { - "desc": "{schema}", + "desc": "Always send Telegram Business away messages to users writing to us in private.", "params": {} }, "BusinessAwayMessageScheduleCustom": { - "desc": "{schema}", + "desc": "Send Telegram Business away messages to users writing to us in private in the specified time span.", "params": { - "end_date": "", - "start_date": "" + "end_date": "End date (UNIX timestamp).", + "start_date": "Start date (UNIX timestamp)." } }, "BusinessAwayMessageScheduleOutsideWorkHours": { - "desc": "{schema}", + "desc": "Send Telegram Business away messages to users writing to us in private outside of the configured Telegram Business working hours.", "params": {} }, "BusinessBotRecipients": { - "desc": "{schema}", + "desc": "Specifies the private chats that a connected business bot \u00bb may receive messages and interact with.", "params": { - "contacts": "", - "exclude_selected": "", - "exclude_users": "", - "existing_chats": "", + "contacts": "Selects all private chats with contacts.", + "exclude_selected": "If set, then all private chats except the ones selected by existing_chats, new_chats, contacts, non_contacts and users are chosen. Note that if this flag is set, any values passed in exclude_users will be merged and moved into users by the server, thus exclude_users will always be empty.", + "exclude_users": "Identifiers of private chats that are always excluded.", + "existing_chats": "Selects all existing private chats.", "flags": "Flags, see TL conditional fields", - "new_chats": "", - "non_contacts": "", - "users": "" + "new_chats": "Selects all new private chats.", + "non_contacts": "Selects all private chats with non-contacts.", + "users": "Explicitly selected private chats." + } + }, + "BusinessBotRights": { + "desc": "Business bot rights.", + "params": { + "change_gift_settings": "Whether the bot can change the privacy settings pertaining to gifts for the business account.", + "delete_received_messages": "Whether the bot can delete received private messages in managed chats.", + "delete_sent_messages": "Whether the bot can delete messages sent by the bot.", + "edit_bio": "Whether the bot can edit the bio of the business account.", + "edit_name": "Whether the bot can edit the first and last name of the business account.", + "edit_profile_photo": "Whether the bot can edit the profile photo of the business account.", + "edit_username": "Whether the bot can edit the username of the business account.", + "flags": "Flags, see TL conditional fields", + "manage_stories": "Whether the bot can post, edit and delete stories on behalf of the business account.", + "read_messages": "Whether the bot can mark incoming private messages as read.", + "reply": "Whether the bot can send and edit messages in private chats that had incoming messages in the last 24 hours.", + "sell_gifts": "Whether the bot can convert regular gifts owned by the business account to Telegram Stars.", + "transfer_and_upgrade_gifts": "Whether the bot can transfer and upgrade gifts owned by the business account.", + "transfer_stars": "Whether the bot can transfer Telegram Stars received by the business account to its own account, or use them to upgrade and transfer gifts.", + "view_gifts": "Whether the bot can view gifts and the amount of Telegram Stars owned by the business account." } }, "BusinessChatLink": { - "desc": "{schema}", + "desc": "Contains info about a business chat deep link \u00bb created by the current account.", "params": { "entities": "Message entities for styled text", "flags": "Flags, see TL conditional fields", - "link": "", - "message": "", - "title": "", - "views": "" + "link": "Business chat deep link.", + "message": "Message to pre-fill in the message input field.", + "title": "Human-readable name of the link, to simplify management in the UI (only visible to the creator of the link).", + "views": "Number of times the link was resolved (clicked/scanned/etc...)." } }, "BusinessGreetingMessage": { - "desc": "{schema}", + "desc": "Describes a Telegram Business greeting, automatically sent to new users writing to us in private for the first time, or after a certain inactivity period.", "params": { - "no_activity_days": "", - "recipients": "", - "shortcut_id": "" + "no_activity_days": "The number of days after which a private chat will be considered as inactive; currently, must be one of 7, 14, 21, or 28.", + "recipients": "Allowed recipients for the greeting messages.", + "shortcut_id": "ID of a quick reply shorcut, containing the greeting messages to send, see here \u00bb for more info." } }, "BusinessIntro": { - "desc": "{schema}", + "desc": "Telegram Business introduction \u00bb.", "params": { - "description": "", + "description": "Profile introduction (max intro_description_length_limit \u00bb UTF-8 characters).", "flags": "Flags, see TL conditional fields", - "sticker": "", - "title": "" + "sticker": "Optional introduction sticker.", + "title": "Title of the introduction message (max intro_title_length_limit \u00bb UTF-8 characters)." } }, "BusinessLocation": { - "desc": "{schema}", + "desc": "Represents the location of a Telegram Business \u00bb.", "params": { - "address": "", + "address": "Textual description of the address (mandatory).", "flags": "Flags, see TL conditional fields", - "geo_point": "" + "geo_point": "Geographical coordinates (optional)." } }, "BusinessRecipients": { - "desc": "{schema}", + "desc": "Specifies the chats that can receive Telegram Business away \u00bb and greeting \u00bb messages.", "params": { - "contacts": "", - "exclude_selected": "", - "existing_chats": "", + "contacts": "All private chats with contacts.", + "exclude_selected": "If set, inverts the selection.", + "existing_chats": "All existing private chats.", "flags": "Flags, see TL conditional fields", - "new_chats": "", - "non_contacts": "", - "users": "" + "new_chats": "All new private chats.", + "non_contacts": "All private chats with non-contacts.", + "users": "Only private chats with the specified users." } }, "BusinessWeeklyOpen": { - "desc": "{schema}", + "desc": "A time interval, indicating the opening hours of a business.", "params": { - "end_minute": "", - "start_minute": "" + "end_minute": "End minute in minutes of the week, 1 to 8*24*60 inclusively (8 and not 7 because this allows to specify intervals that, for example, start on Sunday 21:00 and end on Monday 04:00 (6*24*60+21*60 to 7*24*60+4*60) without passing an invalid end_minute < start_minute). See here \u00bb for more info.", + "start_minute": "Start minute in minutes of the week, 0 to 7*24*60 inclusively." } }, "BusinessWorkHours": { - "desc": "{schema}", + "desc": "Specifies a set of Telegram Business opening hours.", "params": { "flags": "Flags, see TL conditional fields", - "open_now": "", - "timezone_id": "", - "weekly_open": "" + "open_now": "Ignored if set while invoking account.updateBusinessWorkHours, only returned by the server in userFull.business_work_hours, indicating whether the business is currently open according to the current time and the values in weekly_open and timezone.", + "timezone_id": "An ID of one of the timezones returned by help.getTimezonesList. The timezone ID is contained timezone.id, a human-readable, localized name of the timezone is available in timezone.name and the timezone.utc_offset field contains the UTC offset in seconds, which may be displayed in hh:mm format by the client together with the human-readable name (i.e. $name UTC -01:00).", + "weekly_open": "A list of time intervals (max 28) represented by businessWeeklyOpen \u00bb, indicating the opening hours of their business." } }, "CdnConfig": { @@ -587,10 +613,13 @@ "Channel": { "desc": "Channel/supergroup info", "params": { - "access_hash": "Access hash", + "access_hash": "Access hash, see here \u00bb for more info", "admin_rights": "Admin rights of the user in this channel (see rights)", + "autotranslation": "If set, autotranslation was enabled for all users by the admin of the channel, as specified here \u00bb.", "banned_rights": "Banned rights of the user in this channel (see rights)", + "bot_verification_icon": "Describes a bot verification icon \u00bb.", "broadcast": "Is this a channel?", + "broadcast_messages_allowed": "If set, this channel has an associated monoforum \u00bb, and its ID is specified in the linked_monoforum_id flag.", "call_active": "Whether a group call or livestream is currently active", "call_not_empty": "Whether there's anyone in the group call or livestream", "color": "The channel's accent color.", @@ -598,35 +627,41 @@ "date": "Date when the user joined the supergroup/channel, or if the user isn't a member, its creation date", "default_banned_rights": "Default chat rights (see rights)", "emoji_status": "Emoji status", - "fake": "If set, this supergroup/channel was reported by many users as a fake or scam: be careful when interacting with it.", + "fake": "If set, this supergroup/channel was reported by many users as a fake or scam: be careful when interacting with it. Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", "flags": "Flags, see TL conditional fields", "flags2": "Flags, see TL conditional fields", - "forum": "Whether this supergroup is a forum", - "gigagroup": "Whether this supergroup is a gigagroup", + "forum": "Whether this supergroup is a forum. Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", + "forum_tabs": "If set, enables the tabbed forum UI \u00bb.", + "gigagroup": "Whether this supergroup is a gigagroupChanges to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", "has_geo": "Whether this chanel has a geoposition", - "has_link": "Whether this channel has a private join link", - "id": "ID of the channel", - "join_request": "Whether a user's join request will have to be approved by administrators, toggle using channels.toggleJoinToSend", - "join_to_send": "Whether a user needs to join the supergroup before they can send messages: can be false only for discussion groups \u00bb, toggle using channels.toggleJoinToSend", + "has_link": "Whether this channel has a linked discussion group \u00bb (or this supergroup is a channel's discussion group). The actual ID of the linked channel/supergroup is contained in channelFull.linked_chat_id. Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", + "id": "ID of the channel, see here \u00bb for more info and the available ID range.", + "join_request": "Whether a user's join request will have to be approved by administrators, toggle using channels.toggleJoinToSendChanges to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", + "join_to_send": "Whether a user needs to join the supergroup before they can send messages: can be false only for discussion groups \u00bb, toggle using channels.toggleJoinToSendChanges to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", "left": "Whether the current user has left or is not a member of this channel", - "level": "Boost level", - "megagroup": "Is this a supergroup?", + "level": "Boost level. Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", + "linked_monoforum_id": "For channels with associated monoforums, the monoforum ID. For Monoforums, the ID of the associated channel.", + "megagroup": "Is this a supergroup? Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", "min": "See min", + "monoforum": "If set, this is a monoforum \u00bb, and the ID of the associated channel is specified in the linked_monoforum_id.", "noforwards": "Whether this channel or group is protected, thus does not allow forwarding messages from it", "participants_count": "Participant count", "photo": "Profile photo", "profile_color": "The channel's profile color.", - "restricted": "Whether viewing/writing in this channel for a reason (see restriction_reason", - "restriction_reason": "Contains the reason why access to this channel must be restricted.", - "scam": "This channel/supergroup is probably a scam", + "restricted": "Whether viewing/writing in this channel for a reason (see restriction_reason)", + "restriction_reason": "Contains the reason why access to this channel must be restricted. Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", + "scam": "This channel/supergroup is probably a scam Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", + "send_paid_messages_stars": "If set, this supergroup or monoforum has enabled paid messages \u00bb, we might need to pay the specified amount of Stars to send messages to it, depending on the configured exceptions: check channelFull.send_paid_messages_stars to see if the currently logged in user actually has to pay or not, see here \u00bb for the full flow (only set for the monoforum, not the associated channel).", + "signature_profiles": "If set, messages sent by admins to this channel will link to the admin's profile (just like with groups).", "signatures": "Whether signatures are enabled (channels)", - "slowmode_enabled": "Whether slow mode is enabled for groups to prevent flood in chat", + "slowmode_enabled": "Whether slow mode is enabled for groups to prevent flood in chat. Changes to this flag should invalidate the local channelFull cache for this channel/supergroup ID, see here \u00bb for more info.", "stories_hidden": "Whether we have hidden all stories posted by this channel \u00bb.", "stories_hidden_min": "If set, indicates that the stories_hidden flag was not populated, and its value must cannot be relied on; use the previously cached value, or re-fetch the constructor using channels.getChannels to obtain the latest value of the stories_hidden flag.", "stories_max_id": "ID of the maximum read story.", "stories_unavailable": "No stories from the channel are visible.", + "subscription_until_date": "Expiration date of the Telegram Star subscription \u00bb the current user has bought to gain access to this channel.", "title": "Title", - "username": "Username", + "username": "Main active username.", "usernames": "Additional usernames", "verified": "Is this channel verified by telegram?" } @@ -662,10 +697,10 @@ } }, "ChannelAdminLogEventActionChangeEmojiStickerSet": { - "desc": "{schema}", + "desc": "The supergroup's custom emoji stickerset was changed.", "params": { - "new_stickerset": "", - "prev_stickerset": "" + "new_stickerset": "New value", + "prev_stickerset": "Old value" } }, "ChannelAdminLogEventActionChangeHistoryTTL": { @@ -809,6 +844,14 @@ "invite": "The invite link that was revoked" } }, + "ChannelAdminLogEventActionParticipantEditRank": { + "desc": "{schema}", + "params": { + "new_rank": "", + "prev_rank": "", + "user_id": "" + } + }, "ChannelAdminLogEventActionParticipantInvite": { "desc": "A user was invited to the group", "params": { @@ -844,6 +887,13 @@ "participant": "The participant that was muted" } }, + "ChannelAdminLogEventActionParticipantSubExtend": { + "desc": "A paid subscriber has extended their Telegram Star subscription \u00bb.", + "params": { + "new_participant": "The subscriber that extended the subscription.", + "prev_participant": "Same as new_participant." + } + }, "ChannelAdminLogEventActionParticipantToggleAdmin": { "desc": "The admin rights of a user were changed", "params": { @@ -902,6 +952,12 @@ "new_value": "Whether antispam functionality was enabled or disabled." } }, + "ChannelAdminLogEventActionToggleAutotranslation": { + "desc": "Channel autotranslation was toggled \u00bb.", + "params": { + "new_value": "New value of the toggle" + } + }, "ChannelAdminLogEventActionToggleForum": { "desc": "Forum functionality was enabled or disabled.", "params": { @@ -932,6 +988,12 @@ "new_value": "New value" } }, + "ChannelAdminLogEventActionToggleSignatureProfiles": { + "desc": "Channel signature profiles were enabled/disabled.", + "params": { + "new_value": "New value" + } + }, "ChannelAdminLogEventActionToggleSignatures": { "desc": "Channel signatures were enabled/disabled", "params": { @@ -958,6 +1020,7 @@ "delete": "Message deletion events", "demote": "Admin demotion events", "edit": "Message edit events", + "edit_rank": "", "flags": "Flags, see TL conditional fields", "forums": "Forum-related events", "group_call": "Group call events", @@ -971,6 +1034,7 @@ "promote": "Admin promotion events", "send": "A message was posted in a channel", "settings": "Settings change events (invites, hidden prehistory, signatures, default banned rights, forum toggle events)", + "sub_extend": "Telegram Star subscription extension events \u00bb", "unban": "Unban events", "unkick": "Unkick events" } @@ -983,6 +1047,7 @@ "flags": "Flags, see TL conditional fields", "id": "Channel ID", "megagroup": "Is this a supergroup", + "monoforum": "", "title": "Title", "until_date": "The ban is valid until the specified date" } @@ -997,20 +1062,22 @@ "available_reactions": "Allowed message reactions \u00bb", "banned_count": "Number of users banned from the channel", "blocked": "Whether any anonymous admin of this supergroup was blocked: if set, you won't receive messages from anonymous group admins in discussion replies via @replies", - "boosts_applied": "", - "boosts_unrestrict": "", + "boosts_applied": "The number of boosts the current user has applied to the current supergroup.", + "boosts_unrestrict": "The number of boosts this supergroup requires to bypass slowmode and other restrictions, see here \u00bb for more info.", "bot_info": "Info about bots in the channel/supergroup", + "bot_verification": "Bot verification icon", "call": "Livestream or group call information", "can_delete_channel": "Can we delete this channel?", "can_set_location": "Can we set the geolocation of this group (for geogroups)", "can_set_stickers": "Can we associate a stickerpack to the supergroup?", "can_set_username": "Can we set the channel's username?", "can_view_participants": "Can we view the participant list?", - "can_view_revenue": "", + "can_view_revenue": "If set, this user can view ad revenue statistics \u00bb for this channel.", + "can_view_stars_revenue": "If set, this user can view Telegram Star revenue statistics \u00bb for this channel.", "can_view_stats": "Can the user view channel/supergroup statistics", "chat_photo": "Channel picture", "default_send_as": "Default peer used for sending messages to this channel", - "emojiset": "", + "emojiset": "Custom emoji stickerset associated to the current supergroup, set using channels.setEmojiStickers after reaching the appropriate boost level, see here \u00bb for more info.", "exported_invite": "Invite link", "flags": "Flags, see TL conditional fields", "flags2": "Flags, see TL conditional fields", @@ -1020,25 +1087,32 @@ "hidden_prehistory": "Is the history before we joined hidden to us?", "id": "ID of the channel", "kicked_count": "Number of users kicked from the channel", - "linked_chat_id": "ID of the linked discussion chat for channels", + "linked_chat_id": "ID of the linked discussion chat for channels (and vice versa, the ID of the linked channel for discussion chats).", "location": "Location of the geogroup", + "main_tab": "The main tab for the channel's profile, see here \u00bb for more info.", "migrated_from_chat_id": "The chat ID from which this group was migrated", "migrated_from_max_id": "The message ID in the original chat at which this group was migrated", "notify_settings": "Notification settings", "online_count": "Number of users currently online", + "paid_media_allowed": "Whether the current user can send or forward paid media \u00bb to this channel.", + "paid_messages_available": "If set, admins may enable enable paid messages \u00bb in this supergroup.", + "paid_reactions_available": "If set, users may send paid Telegram Star reactions \u00bb to messages of this channel.", "participants_count": "Number of participants of the channel", "participants_hidden": "Whether the participant list is hidden.", "pending_suggestions": "A list of suggested actions for the supergroup admin, see here for more info \u00bb.", "pinned_msg_id": "Message ID of the last pinned message", "pts": "Latest PTS for this channel", - "reactions_limit": "", + "reactions_limit": "This flag may be used to impose a custom limit of unique reactions (i.e. a customizable version of appConfig.reactions_uniq_max).", "read_inbox_max_id": "Position up to which all incoming messages are read.", "read_outbox_max_id": "Position up to which all outgoing messages are read.", "recent_requesters": "IDs of users who requested to join recently", "requests_pending": "Pending join requests \u00bb", - "restricted_sponsored": "", + "restricted_sponsored": "Whether ads on this channel were disabled as specified here \u00bb (this flag is only visible to the owner of the channel).", + "send_paid_messages_stars": "If set and bigger than 0, this supergroup, monoforum or the monoforum associated to this channel has enabled paid messages \u00bb and we must pay the specified amount of Stars to send messages to it, see here \u00bb for the full flow. This flag will be set both for the monoforum and for channelFull of the associated channel). If set and equal to 0, the monoforum requires payment in general but we were exempted from paying.", "slowmode_next_send_date": "Indicates when the user will be allowed to send another message in the supergroup (unixtime)", "slowmode_seconds": "If specified, users in supergroups will only be able to send one message every slowmode_seconds seconds", + "stargifts_available": "If set, users may send Gifts \u00bb to this channel.", + "stargifts_count": "Admins with chatAdminRights.post_messages rights will see the total number of received gifts, everyone else will see the number of gifts added to the channel's profile.", "stats_dc": "If set, specifies the DC to use for fetching channel statistics", "stickerset": "Associated stickerset", "stories": "Channel stories", @@ -1078,6 +1152,9 @@ "desc": "Channel/supergroup participant", "params": { "date": "Date joined", + "flags": "Flags, see TL conditional fields", + "rank": "", + "subscription_until_date": "If set, contains the expiration date of the current Telegram Star subscription period \u00bb for the specified participant.", "user_id": "Participant user ID" } }, @@ -1103,7 +1180,8 @@ "flags": "Flags, see TL conditional fields", "kicked_by": "User was kicked by the specified admin", "left": "Whether the user has left the group", - "peer": "The banned peer" + "peer": "The banned peer", + "rank": "" } }, "ChannelParticipantCreator": { @@ -1127,6 +1205,8 @@ "date": "When did I join the channel/supergroup", "flags": "Flags, see TL conditional fields", "inviter_id": "User that invited me to the channel/supergroup", + "rank": "", + "subscription_until_date": "If set, contains the expiration date of the current Telegram Star subscription period \u00bb for the specified participant.", "user_id": "User ID", "via_request": "Whether I joined upon specific approval of an admin" } @@ -1176,7 +1256,7 @@ } }, "Chat": { - "desc": "Info about a group", + "desc": "Info about a group.", "params": { "admin_rights": "Admin rights of the user in the group", "call_active": "Whether a group call is currently active", @@ -1186,7 +1266,7 @@ "deactivated": "Whether the group was migrated", "default_banned_rights": "Default banned rights of all users in the group", "flags": "Flags, see TL conditional fields", - "id": "ID of the group", + "id": "ID of the group, see here \u00bb for more info and the available ID range.", "left": "Whether the current user has left the group", "migrated_to": "Means this chat was upgraded to a supergroup", "noforwards": "Whether this group is protected, thus does not allow forwarding messages from it", @@ -1210,6 +1290,8 @@ "flags": "Flags, see TL conditional fields", "invite_users": "If set, allows the admin to invite users in the channel/supergroup", "manage_call": "If set, allows the admin to change group call/livestream settings", + "manage_direct_messages": "If set, allows the admin to manage the direct messages monoforum \u00bb and decline suggested posts \u00bb.", + "manage_ranks": "", "manage_topics": "If set, allows the admin to create, delete or modify forum topics \u00bb.", "other": "Set this flag if none of the other flags are set, but you still want the user to be an admin: if this or any of the other flags are set, the admin can get the chat admin log, get chat statistics, get message statistics in channels, get channel members, see anonymous administrators in supergroups and ignore slow mode.", "pin_messages": "If set, allows the admin to pin messages in the channel/supergroup", @@ -1229,6 +1311,7 @@ "desc": "Represents the rights of a normal user in a supergroup/channel/chat. In this case, the flags are inverted: if set, a flag does not allow a user to do X.", "params": { "change_info": "If set, does not allow any user to change the description of a supergroup/chat", + "edit_rank": "", "embed_links": "If set, does not allow a user to embed links in the messages of a supergroup/chat", "flags": "Flags, see TL conditional fields", "invite_users": "If set, does not allow any user to invite users in a supergroup/chat", @@ -1283,7 +1366,7 @@ "notify_settings": "Notification settings", "participants": "Participant list", "pinned_msg_id": "Message ID of the last pinned message", - "reactions_limit": "", + "reactions_limit": "This flag may be used to impose a custom limit of unique reactions (i.e. a customizable version of appConfig.reactions_uniq_max).", "recent_requesters": "IDs of users who requested to join recently", "requests_pending": "Pending join requests \u00bb", "theme_emoticon": "Emoji representing a specific chat theme", @@ -1295,7 +1378,9 @@ "desc": "Chat invite info", "params": { "about": "Description of the group of channel", + "bot_verification": "Describes a bot verification icon \u00bb.", "broadcast": "Whether this is a channel", + "can_refulfill_subscription": "If set, indicates that the user has already paid for the associated Telegram Star subscriptions \u00bb and it hasn't expired yet, so they may re-join the channel using messages.importChatInvite without repeating the payment.", "channel": "Whether this is a channel/supergroup or a normal group", "color": "Profile color palette ID", "fake": "If set, this chat was reported by many users as a fake or scam: be careful when interacting with it.", @@ -1307,6 +1392,8 @@ "public": "Whether this is a public channel/supergroup", "request_needed": "Whether the join request \u00bb must be first approved by an administrator", "scam": "This chat is probably a scam", + "subscription_form_id": "For Telegram Star subscriptions \u00bb, the ID of the payment form for the subscription.", + "subscription_pricing": "For Telegram Star subscriptions \u00bb, contains the pricing of the subscription the user must activate to join the private channel.", "title": "Chat/supergroup/channel title", "verified": "Is this chat or channel verified by Telegram?" } @@ -1330,6 +1417,8 @@ "requested": "Number of users that have already used this link to join", "revoked": "Whether this chat invite was revoked", "start_date": "When was this chat invite last modified", + "subscription_expired": "For Telegram Star subscriptions \u00bb, contains the number of chat members which have already joined the chat using the link, but have already left due to expiration of their subscription.", + "subscription_pricing": "For Telegram Star subscriptions \u00bb, contains the pricing of the subscription the user must activate to join the private channel.", "title": "Custom description for the invite link, visible only to admins", "usage": "How many users joined using this link", "usage_limit": "Maximum number of users that can join using this link" @@ -1368,7 +1457,9 @@ "desc": "Group member.", "params": { "date": "Date added to the group", + "flags": "Flags, see TL conditional fields", "inviter_id": "ID of the user that added the member to the group", + "rank": "", "user_id": "Member user ID" } }, @@ -1376,13 +1467,17 @@ "desc": "Chat admin", "params": { "date": "Date when the user was added", + "flags": "Flags, see TL conditional fields", "inviter_id": "ID of the user that added the member to the group", + "rank": "", "user_id": "ID of a group member that is admin" } }, "ChatParticipantCreator": { "desc": "Represents the creator of the group", "params": { + "flags": "Flags, see TL conditional fields", + "rank": "", "user_id": "ID of the user that created the group" } }, @@ -1433,6 +1528,19 @@ "reactions": "Allowed set of reactions: the reactions_in_chat_max configuration field indicates the maximum number of reactions that can be specified in this field." } }, + "ChatTheme": { + "desc": "A chat theme", + "params": { + "emoticon": "The emoji identifying the chat theme." + } + }, + "ChatThemeUniqueGift": { + "desc": "A chat theme based on a collectible gift \u00bb.", + "params": { + "gift": "The owned collectible gift on which this theme is based, as a starGiftUnique constructor.", + "theme_settings": "Theme settings." + } + }, "CodeSettings": { "desc": "Settings used by telegram servers for sending the confirm code.", "params": { @@ -1445,7 +1553,7 @@ "flags": "Flags, see TL conditional fields", "logout_tokens": "Previously stored future auth tokens, see the documentation for more info \u00bb", "token": "Used only by official iOS apps for Firebase auth: device token for apple push.", - "unknown_number": "" + "unknown_number": "Set this flag if there is a SIM card in the current device, but it is not possible to check whether the specified phone number matches the SIM's phone number." } }, "Config": { @@ -1502,12 +1610,26 @@ } }, "ConnectedBot": { - "desc": "{schema}", + "desc": "Contains info about a connected business bot \u00bb.", + "params": { + "bot_id": "ID of the connected bot", + "flags": "Flags, see TL conditional fields", + "recipients": "Specifies the private chats that a connected business bot \u00bb may receive messages and interact with.", + "rights": "Business bot rights." + } + }, + "ConnectedBotStarRef": { + "desc": "Info about an active affiliate program we have with a Mini App", "params": { - "bot_id": "", - "can_reply": "", + "bot_id": "ID of the mini app that created the affiliate program", + "commission_permille": "The number of Telegram Stars received by the affiliate for each 1000 Telegram Stars received by bot_id", + "date": "When did we affiliate with bot_id", + "duration_months": "Number of months the program will be active; if not set, there is no expiration date.", "flags": "Flags, see TL conditional fields", - "recipients": "" + "participants": "The number of users that used the affiliate program", + "revenue": "The number of Telegram Stars that were earned by the affiliate program", + "revoked": "If set, this affiliation was revoked by the affiliate using payments.editConnectedStarRefBot, or by the affiliation program owner using bots.updateStarRefProgram", + "url": "Referral link to be shared" } }, "Contact": { @@ -1518,10 +1640,10 @@ } }, "ContactBirthday": { - "desc": "{schema}", + "desc": "Birthday information of a contact.", "params": { - "birthday": "", - "contact_id": "" + "birthday": "Birthday information.", + "contact_id": "User ID." } }, "ContactStatus": { @@ -1585,7 +1707,7 @@ "params": { "bots": "Whether to include all bots in this folder", "broadcasts": "Whether to include all channels in this folder", - "color": "", + "color": "A color ID for the folder tag associated to this folder, see here \u00bb for more info.", "contacts": "Whether to include all contacts in this folder", "emoticon": "Emoji to use as icon for the folder.", "exclude_archived": "Whether to exclude archived chats from this folder", @@ -1598,20 +1720,22 @@ "include_peers": "Include the following chats in this folder", "non_contacts": "Whether to include all non-contacts in this folder", "pinned_peers": "Pinned chats, folders can have unlimited pinned chats", - "title": "Folder name (max 12 UTF-8 chars)" + "title": "Folder name (max 12 UTF-8 chars)", + "title_noanimate": "If set, any animated emojis present in title should not be animated and should be instead frozen on the first frame." } }, "DialogFilterChatlist": { "desc": "A folder imported using a chat folder deep link \u00bb.", "params": { - "color": "", + "color": "A color ID for the folder tag associated to this folder, see here \u00bb for more info.", "emoticon": "Emoji to use as icon for the folder.", "flags": "Flags, see TL conditional fields", "has_my_invites": "Whether the current user has created some chat folder deep links \u00bb to share the folder as well.", "id": "ID of the folder", "include_peers": "Chats to include in the folder", "pinned_peers": "Pinned chats, folders can have unlimited pinned chats", - "title": "Name of the folder (max 12 UTF-8 chars)" + "title": "Name of the folder (max 12 UTF-8 chars)", + "title_noanimate": "If set, any animated emojis present in title should not be animated and should be instead frozen on the first frame." } }, "DialogFilterDefault": { @@ -1651,6 +1775,17 @@ "folder_id": "Peer folder ID, for more info click here" } }, + "DisallowedGiftsSettings": { + "desc": "Disallow the reception of specific gift types.", + "params": { + "disallow_limited_stargifts": "Disallow the reception of limited-supply gifts (those with the starGift.limited flag set).", + "disallow_premium_gifts": "Disallow the reception of gifted Telegram Premium subscriptions \u00bb.", + "disallow_stargifts_from_channels": "", + "disallow_unique_stargifts": "Disallow the reception of collectible gifts \u00bb.", + "disallow_unlimited_stargifts": "Disallow the reception of gifts with an unlimited supply (those with the starGift.limited flag not set).", + "flags": "Flags, see TL conditional fields" + } + }, "Document": { "desc": "Document", "params": { @@ -1725,10 +1860,12 @@ "duration": "Duration in seconds", "flags": "Flags, see TL conditional fields", "h": "Video height", - "nosound": "Whether the specified document is a video file with no audio tracks (a GIF animation (even as MPEG4), for example)", + "nosound": "Whether the specified document is a video file with no audio tracks", "preload_prefix_size": "Number of bytes to preload when preloading videos (particularly video stories).", "round_message": "Whether this is a round video", "supports_streaming": "Whether the video supports streaming", + "video_codec": "Codec used for the video, i.e. \"h264\", \"h265\", or \"av1\"", + "video_start_ts": "Floating point UNIX timestamp in seconds, indicating the frame of the video that should be used as static preview and thumbnail.", "w": "Video width" } }, @@ -1742,13 +1879,15 @@ "desc": "Represents a message draft.", "params": { "date": "Date of last update of the draft.", + "effect": "A message effect that should be played as specified here \u00bb.", "entities": "Message entities for styled text.", "flags": "Flags, see TL conditional fields", "invert_media": "If set, any eventual webpage preview will be shown on top of the message instead of at the bottom.", "media": "Media.", "message": "The draft", "no_webpage": "Whether no webpage preview will be generated", - "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story." + "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story.", + "suggested_post": "Used to suggest a post to a channel, see here \u00bb for more info on the full flow." } }, "DraftMessageEmpty": { @@ -1800,18 +1939,18 @@ } }, "EmojiGroupGreeting": { - "desc": "{schema}", + "desc": "Represents an emoji category, that should be moved to the top of the list when choosing a sticker for a business introduction", "params": { - "emoticons": "", - "icon_emoji_id": "", - "title": "" + "emoticons": "A list of UTF-8 emojis, matching the category.", + "icon_emoji_id": "A single custom emoji used as preview for the category.", + "title": "Category name, i.e. \"Animals\", \"Flags\", \"Faces\" and so on..." } }, "EmojiGroupPremium": { - "desc": "{schema}", + "desc": "An emoji category, used to select all Premium-only stickers (i.e. those with a Premium effect \u00bb)/Premium-only custom emojis (i.e. those where the documentAttributeCustomEmoji.free flag is not set)", "params": { - "icon_emoji_id": "", - "title": "" + "icon_emoji_id": "A single custom emoji used as preview for the category.", + "title": "Category name, i.e. \"Animals\", \"Flags\", \"Faces\" and so on..." } }, "EmojiKeyword": { @@ -1847,7 +1986,7 @@ "desc": "Represents a list of custom emojis.", "params": { "document_id": "Custom emoji IDs", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "EmojiListNotModified": { @@ -1857,20 +1996,31 @@ "EmojiStatus": { "desc": "An emoji status", "params": { - "document_id": "Custom emoji document ID" + "document_id": "Custom emoji document ID", + "flags": "Flags, see TL conditional fields", + "until": "If set, the emoji status will be active until the specified unixtime." + } + }, + "EmojiStatusCollectible": { + "desc": "An owned collectible gift \u00bb as emoji status.", + "params": { + "center_color": "Color of the center of the profile backdrop in RGB24 format, from the gift's starGiftAttributeBackdrop.", + "collectible_id": "ID of the collectible (from starGiftUnique.id).", + "document_id": "ID of the custom emoji representing the status.", + "edge_color": "Color of the edges of the profile backdrop in RGB24 format, from the gift's starGiftAttributeBackdrop.", + "flags": "Flags, see TL conditional fields", + "pattern_color": "Color of the pattern_document_id applied on the profile backdrop in RGB24 format, from the gift's starGiftAttributeBackdrop.", + "pattern_document_id": "The ID of a pattern to apply on the profile's backdrop, correlated to the starGiftAttributePattern from the gift in slug.", + "slug": "Unique identifier of the collectible that may be used to create a collectible gift link \u00bb for the current collectible, or to fetch further info about the collectible using payments.getUniqueStarGift.", + "text_color": "Color of text on the profile backdrop in RGB24 format, from the gift's starGiftAttributeBackdrop.", + "title": "Name of the collectible.", + "until": "If set, the emoji status will be active until the specified unixtime." } }, "EmojiStatusEmpty": { "desc": "No emoji status is set", "params": {} }, - "EmojiStatusUntil": { - "desc": "An emoji status valid until the specified date", - "params": { - "document_id": "Custom emoji document ID", - "until": "This status is valid until this date" - } - }, "EmojiURL": { "desc": "An HTTP URL which can be used to automatically log in into translation platform and suggest new emoji replacements. The URL will be valid for 30 seconds after generation", "params": { @@ -1989,13 +2139,13 @@ } }, "FactCheck": { - "desc": "{schema}", + "desc": "Represents a fact-check \u00bb created by an independent fact-checker.", "params": { - "country": "", + "country": "A two-letter ISO 3166-1 alpha-2 country code of the country for which the fact-check should be shown.", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here", - "need_check": "", - "text": "" + "hash": "Hash used for caching, for more info click here", + "need_check": "If set, the country/text fields will not be set, and the fact check must be fetched manually by the client (if it isn't already cached with the key specified in hash) using bundled messages.getFactCheck requests, when the message with the factcheck scrolls into view.", + "text": "The fact-check." } }, "FileHash": { @@ -2039,11 +2189,13 @@ "id": "Topic ID", "my": "Whether the topic was created by the current user", "notify_settings": "Notification settings", + "peer": "", "pinned": "Whether the topic is pinned", "read_inbox_max_id": "Position up to which all incoming messages are read.", "read_outbox_max_id": "Position up to which all outgoing messages are read.", "short": "Whether this constructor is a reduced version of the full topic information. If set, only the my, closed, id, date, title, icon_color, icon_emoji_id and from_id parameters will contain valid information. Reduced info is usually only returned in topic-related admin log events \u00bb and in the messages.channelMessages constructor: if needed, full information can be fetched using channels.getForumTopicsByID.", "title": "Topic title", + "title_missing": "", "top_message": "ID of the last message that was sent to this topic", "unread_count": "Number of unread messages", "unread_mentions_count": "Number of unread mentions", @@ -2056,6 +2208,13 @@ "id": "The ID of the deleted forum topic." } }, + "FoundStory": { + "desc": "A story found using global story search \u00bb.", + "params": { + "peer": "The peer that posted the story.", + "story": "The story." + } + }, "Game": { "desc": "Indicates an already sent game", "params": { @@ -2079,6 +2238,16 @@ "long": "Longitude" } }, + "GeoPointAddress": { + "desc": "Address optionally associated to a geoPoint.", + "params": { + "city": "City", + "country_iso2": "Two-letter ISO 3166-1 alpha-2 country code", + "flags": "Flags, see TL conditional fields", + "state": "State", + "street": "Street" + } + }, "GeoPointEmpty": { "desc": "Empty constructor.", "params": {} @@ -2087,11 +2256,14 @@ "desc": "Global privacy settings", "params": { "archive_and_mute_new_noncontact_peers": "Whether to archive and mute new chats from non-contacts", + "disallowed_gifts": "Disallows the reception of specific gift types.", + "display_gifts_button": "Enables or disables our userFull.display_gifts_button flag: if the userFull.display_gifts_button flag of both us and another user is set, a gift button should always be displayed in the text field in private chats with the other user: once clicked, the gift UI should be displayed, offering the user options to gift Telegram Premium \u00bb subscriptions or Telegram Gifts \u00bb.", "flags": "Flags, see TL conditional fields", - "hide_read_marks": "", + "hide_read_marks": "If this flag is set, the inputPrivacyKeyStatusTimestamp key will also apply to the ability to use messages.getOutboxReadDate on messages sent to us. Meaning, users that cannot see our exact last online date due to the current value of the inputPrivacyKeyStatusTimestamp key will receive a 403 USER_PRIVACY_RESTRICTED error when invoking messages.getOutboxReadDate to fetch the exact read date of a message they sent to us. The userFull.read_dates_private flag will be set for users that have this flag enabled.", "keep_archived_folders": "Whether unmuted chats that are always included or pinned in a folder, will be kept in the Archive chat list when they get a new message. Ignored if keep_archived_unmuted is set.", "keep_archived_unmuted": "Whether unmuted chats will be kept in the Archive chat list when they get a new message.", - "new_noncontact_peers_require_premium": "" + "new_noncontact_peers_require_premium": "See here for more info on this flag \u00bb.", + "noncontact_peers_paid_stars": "If configured, specifies the number of stars users must pay us to send us a message, see here \u00bb for more info on paid messages." } }, "GroupCall": { @@ -2099,18 +2271,26 @@ "params": { "access_hash": "Group call access hash", "can_change_join_muted": "Whether the current user can change the value of the join_muted flag using phone.toggleGroupCallSettings", + "can_change_messages_enabled": "", "can_start_video": "Whether you can start streaming video into the call", + "conference": "Whether this is an E2E conference call.", + "creator": "Whether we're created this group call.", + "default_send_as": "", "flags": "Flags, see TL conditional fields", "id": "Group call ID", + "invite_link": "Invitation link for the conference.", "join_date_asc": "Specifies the ordering to use when locally sorting by date and displaying in the UI group call participants.", "join_muted": "Whether the user should be muted upon joining the call", "listeners_hidden": "Whether the listeners list is hidden and cannot be fetched using phone.getGroupParticipants. The phone.groupParticipants.count and groupCall.participants_count counters will still include listeners.", + "messages_enabled": "", + "min": "", "participants_count": "Participant count", "record_start_date": "When was the recording started", "record_video_active": "Whether the group call is currently being recorded", "rtmp_stream": "Whether RTMP streams are allowed", "schedule_date": "When is the call scheduled to start", "schedule_start_subscribed": "Whether we subscribed to the scheduled call", + "send_paid_messages_stars": "", "stream_dc_id": "DC ID to be used for livestream chunks", "title": "Group call title", "unmuted_video_count": "Number of people currently streaming video into the call", @@ -2126,6 +2306,28 @@ "id": "Group call ID" } }, + "GroupCallDonor": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "my": "", + "peer_id": "", + "stars": "", + "top": "" + } + }, + "GroupCallMessage": { + "desc": "{schema}", + "params": { + "date": "", + "flags": "Flags, see TL conditional fields", + "from_admin": "", + "from_id": "", + "id": "", + "message": "", + "paid_message_stars": "" + } + }, "GroupCallParticipant": { "desc": "Info about a group call participant", "params": { @@ -2139,6 +2341,7 @@ "min": "If not set, the volume and muted_by_you fields can be safely used to overwrite locally cached information; otherwise, volume will contain valid information only if volume_by_admin is set both in the cache and in the received constructor.", "muted": "Whether the participant is muted", "muted_by_you": "Whether this participant was muted by the current user", + "paid_stars_total": "", "peer": "Peer information", "presentation": "Info about the screen sharing stream the participant is currently broadcasting", "raise_hand_rating": "Specifies the UI visualization order of peers with raised hands: peers with a higher rating should be showed first in the list.", @@ -2404,64 +2607,64 @@ } }, "InputBusinessAwayMessage": { - "desc": "{schema}", + "desc": "Describes a Telegram Business away message, automatically sent to users writing to us when we're offline, during closing hours, while we're on vacation, or in some other custom time period when we cannot immediately answer to the user.", "params": { "flags": "Flags, see TL conditional fields", - "offline_only": "", - "recipients": "", - "schedule": "", - "shortcut_id": "" + "offline_only": "If set, the messages will not be sent if the account was online in the last 10 minutes.", + "recipients": "Allowed recipients for the away messages.", + "schedule": "Specifies when should the away messages be sent.", + "shortcut_id": "ID of a quick reply shorcut, containing the away messages to send, see here \u00bb for more info." } }, "InputBusinessBotRecipients": { - "desc": "{schema}", + "desc": "Specifies the private chats that a connected business bot \u00bb may interact with.", "params": { - "contacts": "", - "exclude_selected": "", - "exclude_users": "", - "existing_chats": "", + "contacts": "Selects all private chats with contacts.", + "exclude_selected": "If set, then all private chats except the ones selected by existing_chats, new_chats, contacts, non_contacts and users are chosen. Note that if this flag is set, any values passed in exclude_users will be merged and moved into users by the server.", + "exclude_users": "Identifiers of private chats that are always excluded.", + "existing_chats": "Selects all existing private chats.", "flags": "Flags, see TL conditional fields", - "new_chats": "", - "non_contacts": "", - "users": "" + "new_chats": "Selects all new private chats.", + "non_contacts": "Selects all private chats with non-contacts.", + "users": "Explicitly selected private chats." } }, "InputBusinessChatLink": { - "desc": "{schema}", + "desc": "Contains info about a business chat deep link \u00bb to be created by the current account.", "params": { "entities": "Message entities for styled text", "flags": "Flags, see TL conditional fields", - "message": "", - "title": "" + "message": "Message to pre-fill in the message input field.", + "title": "Human-readable name of the link, to simplify management in the UI (only visible to the creator of the link)." } }, "InputBusinessGreetingMessage": { - "desc": "{schema}", + "desc": "Describes a Telegram Business greeting, automatically sent to new users writing to us in private for the first time, or after a certain inactivity period.", "params": { - "no_activity_days": "", - "recipients": "", - "shortcut_id": "" + "no_activity_days": "The number of days after which a private chat will be considered as inactive; currently, must be one of 7, 14, 21, or 28.", + "recipients": "Allowed recipients for the greeting messages.", + "shortcut_id": "ID of a quick reply shorcut, containing the greeting messages to send, see here \u00bb for more info." } }, "InputBusinessIntro": { - "desc": "{schema}", + "desc": "Telegram Business introduction \u00bb.", "params": { - "description": "", + "description": "Profile introduction", "flags": "Flags, see TL conditional fields", - "sticker": "", - "title": "" + "sticker": "Optional introduction sticker.", + "title": "Title of the introduction message" } }, "InputBusinessRecipients": { - "desc": "{schema}", + "desc": "Specifies the chats that can receive Telegram Business away \u00bb and greeting \u00bb messages.", "params": { - "contacts": "", - "exclude_selected": "", - "existing_chats": "", + "contacts": "All private chats with contacts.", + "exclude_selected": "If set, inverts the selection.", + "existing_chats": "All existing private chats.", "flags": "Flags, see TL conditional fields", - "new_chats": "", - "non_contacts": "", - "users": "" + "new_chats": "All new private chats.", + "non_contacts": "All private chats with non-contacts.", + "users": "Only private chats with the specified users." } }, "InputChannel": { @@ -2493,6 +2696,22 @@ "desc": "Empty constructor, remove group photo.", "params": {} }, + "InputChatTheme": { + "desc": "Set an emoji-based chat theme, returned by account.getChatThemes.", + "params": { + "emoticon": "The emoji." + } + }, + "InputChatThemeEmpty": { + "desc": "Remove any currently configured theme.", + "params": {} + }, + "InputChatThemeUniqueGift": { + "desc": "Set a theme based on an owned collectible gift \u00bb, returned by account.getUniqueGiftChatThemes.", + "params": { + "slug": "The slug from starGiftUnique.slug." + } + }, "InputChatUploadedPhoto": { "desc": "New photo to be set as group profile photo.", "params": { @@ -2529,15 +2748,15 @@ } }, "InputCollectiblePhone": { - "desc": "{schema}", + "desc": "Represents a phone number fragment collectible", "params": { - "phone": "" + "phone": "Phone number" } }, "InputCollectibleUsername": { - "desc": "{schema}", + "desc": "Represents a username fragment collectible", "params": { - "username": "" + "username": "Username" } }, "InputDialogPeer": { @@ -2573,6 +2792,14 @@ "thumb_size": "Thumbnail size to download the thumbnail" } }, + "InputEmojiStatusCollectible": { + "desc": "An owned collectible gift \u00bb as emoji status: can only be used in account.updateEmojiStatus, is never returned by the API.", + "params": { + "collectible_id": "ID of the collectible (from starGiftUnique.id).", + "flags": "Flags, see TL conditional fields", + "until": "If set, the emoji status will be active until the specified unixtime." + } + }, "InputEncryptedChat": { "desc": "Creates an encrypted chat.", "params": { @@ -2641,6 +2868,12 @@ "volume_id": "Server volume" } }, + "InputFileStoryDocument": { + "desc": "Used to edit the thumbnail/static preview of a story, see here \u00bb for more info on the full flow.", + "params": { + "id": "The old story video." + } + }, "InputFolderPeer": { "desc": "Peer in a folder", "params": { @@ -2682,6 +2915,18 @@ "id": "Group call ID" } }, + "InputGroupCallInviteMessage": { + "desc": "Join a group call through a messageActionConferenceCall invitation message.", + "params": { + "msg_id": "ID of the messageActionConferenceCall." + } + }, + "InputGroupCallSlug": { + "desc": "Join a conference call through an invitation link \u00bb.", + "params": { + "slug": "Slug from the conference link \u00bb." + } + }, "InputGroupCallStream": { "desc": "Chunk of a livestream", "params": { @@ -2693,43 +2938,132 @@ "video_quality": "Selected video quality (0 = lowest, 1 = medium, 2 = best)" } }, + "InputInvoiceBusinessBotTransferStars": { + "desc": "Transfer stars from the balance of a user account connected to a business bot, to the balance of the business bot, see here \u00bb for more info on the full flow.", + "params": { + "bot": "Always inputUserSelf.", + "stars": "The number of stars to transfer." + } + }, + "InputInvoiceChatInviteSubscription": { + "desc": "Used to pay for a Telegram Star subscription \u00bb.", + "params": { + "hash": "The invitation link of the Telegram Star subscription \u00bb" + } + }, "InputInvoiceMessage": { - "desc": "An invoice contained in a messageMediaInvoice message.", + "desc": "An invoice contained in a messageMediaInvoice message or paid media \u00bb.", "params": { "msg_id": "Message ID", - "peer": "Chat where the invoice was sent" + "peer": "Chat where the invoice/paid media was sent" + } + }, + "InputInvoicePremiumAuthCode": { + "desc": "Used to pay for login codes, in case of high cost of SMS verification codes for the user's country/provider, see here \u00bb for more info.", + "params": { + "purpose": "Must contain an inputStorePaymentAuthCode." } }, "InputInvoicePremiumGiftCode": { - "desc": "Used if the user wishes to start a channel giveaway or send some giftcodes to members of a channel, in exchange for boosts.", + "desc": "Used if the user wishes to start a channel/supergroup giveaway or send some giftcodes to members of a channel/supergroup, in exchange for boosts.", "params": { "option": "Should be populated with one of the giveaway options returned by payments.getPremiumGiftCodeOptions, see the giveaways \u00bb documentation for more info.", "purpose": "Should be populated with inputStorePaymentPremiumGiveaway for giveaways and inputStorePaymentPremiumGiftCode for gifts." } }, + "InputInvoicePremiumGiftStars": { + "desc": "Used to gift a Telegram Premium subscription to another user, paying with Telegram Stars.", + "params": { + "flags": "Flags, see TL conditional fields", + "message": "Message attached with the gift.", + "months": "Duration of the subscription in months, must be one of the options with currency == \"XTR\" returned by payments.getPremiumGiftCodeOptions.", + "user_id": "Who will receive the gifted subscription." + } + }, "InputInvoiceSlug": { "desc": "An invoice slug taken from an invoice deep link or from the premium_invoice_slug app config parameter \u00bb", "params": { "slug": "The invoice slug" } }, - "InputInvoiceStars": { + "InputInvoiceStarGift": { + "desc": "Used to buy a Telegram Star Gift, see here \u00bb for more info.", + "params": { + "flags": "Flags, see TL conditional fields", + "gift_id": "Identifier of the gift, from starGift.id", + "hide_name": "If set, your name will be hidden if the destination user decides to display the gift on their profile (they will still see that you sent the gift)", + "include_upgrade": "Also pay for an eventual upgrade of the gift to a collectible gift \u00bb.", + "message": "Optional message, attached with the gift. The maximum length for this field is specified in the stargifts_message_length_max client configuration value \u00bb.", + "peer": "Receiver of the gift." + } + }, + "InputInvoiceStarGiftAuctionBid": { "desc": "{schema}", "params": { - "option": "" + "bid_amount": "", + "flags": "Flags, see TL conditional fields", + "gift_id": "", + "hide_name": "", + "message": "", + "peer": "", + "update_bid": "" } }, - "InputKeyboardButtonRequestPeer": { + "InputInvoiceStarGiftDropOriginalDetails": { "desc": "{schema}", "params": { - "button_id": "", + "stargift": "" + } + }, + "InputInvoiceStarGiftPrepaidUpgrade": { + "desc": "Separately prepay for the upgrade of a gift \u00bb.", + "params": { + "hash": "The upgrade hash from messageActionStarGift.prepaid_upgrade_hash or savedStarGift.prepaid_upgrade_hash.", + "peer": "The peer that owns the gift." + } + }, + "InputInvoiceStarGiftResale": { + "desc": "Used to buy a collectible gift currently up on resale, see here for more info on the full flow.", + "params": { + "flags": "Flags, see TL conditional fields", + "slug": "Slug of the gift to buy.", + "to_id": "The receiver of the gift.", + "ton": "Buy the gift using TON." + } + }, + "InputInvoiceStarGiftTransfer": { + "desc": "Used to pay to transfer a collectible gift to another peer, see the gifts \u00bb documentation for more info.", + "params": { + "stargift": "The identifier of the received gift", + "to_id": "The destination peer" + } + }, + "InputInvoiceStarGiftUpgrade": { + "desc": "Used to pay to upgrade a Gift to a collectible gift, see the collectible gifts \u00bb documentation for more info on the full flow.", + "params": { + "flags": "Flags, see TL conditional fields", + "keep_original_details": "Set this flag to keep the original gift text, sender and receiver in the upgraded gift as a starGiftAttributeOriginalDetails attribute.", + "stargift": "The identifier of the received gift to upgrade." + } + }, + "InputInvoiceStars": { + "desc": "Used to top up the Telegram Stars balance of the current account or someone else's account, or to start a Telegram Star giveaway \u00bb.", + "params": { + "purpose": "An inputStorePaymentStarsGiveaway, inputStorePaymentStarsTopup or inputStorePaymentStarsGift." + } + }, + "InputKeyboardButtonRequestPeer": { + "desc": "Prompts the user to select and share one or more peers with the bot using messages.sendBotRequestedPeer.", + "params": { + "button_id": "Button ID, to be passed to messages.sendBotRequestedPeer.", "flags": "Flags, see TL conditional fields", - "max_quantity": "", - "name_requested": "", - "peer_type": "", - "photo_requested": "", - "text": "", - "username_requested": "" + "max_quantity": "Maximum number of peers that can be chosen.", + "name_requested": "Set this flag to request the peer's name.", + "peer_type": "Filtering criteria to use for the peer selection list shown to the user. The list should display all existing peers of the specified type, and should also offer an option for the user to create and immediately use one or more (up to max_quantity) peers of the specified type, if needed.", + "photo_requested": "Set this flag to request the peer's photo (if any).", + "style": "", + "text": "Button text", + "username_requested": "Set this flag to request the peer's @username (if any)." } }, "InputKeyboardButtonUrlAuth": { @@ -2739,6 +3073,7 @@ "flags": "Flags, see TL conditional fields", "fwd_text": "New text of the button in forwarded messages.", "request_write_access": "Set this flag to request the permission for your bot to send messages to the user.", + "style": "", "text": "Button text", "url": "An HTTP URL to be opened with user authorization data added to the query string when the button is pressed. If the user refuses to provide authorization data, the original URL without information about the user will be opened. The data added is the same as described in Receiving authorization data.NOTE: You must always check the hash of the received data to verify the authentication and the integrity of the data as described in Checking authorization." } @@ -2746,6 +3081,8 @@ "InputKeyboardButtonUserProfile": { "desc": "Button that links directly to a user profile", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text", "user_id": "User ID" } @@ -2788,7 +3125,9 @@ "id": "The document to be forwarded.", "query": "Text query or emoji that was used by the user to find this sticker or GIF: used to improve search result relevance.", "spoiler": "Whether this media should be hidden behind a spoiler warning", - "ttl_seconds": "Time to live of self-destructing document" + "ttl_seconds": "Time to live of self-destructing document", + "video_cover": "Custom video cover.", + "video_timestamp": "Start playing the video at the specified timestamp (seconds)." } }, "InputMediaDocumentExternal": { @@ -2797,7 +3136,9 @@ "flags": "Flags, see TL conditional fields", "spoiler": "Whether this media should be hidden behind a spoiler warning", "ttl_seconds": "Self-destruct time to live of document", - "url": "URL of the document" + "url": "URL of the document", + "video_cover": "Custom video cover.", + "video_timestamp": "Start playing the video at the specified timestamp (seconds)." } }, "InputMediaEmpty": { @@ -2831,7 +3172,7 @@ "desc": "Generated invoice of a bot payment", "params": { "description": "Product description, 1-255 characters", - "extended_media": "Extended media", + "extended_media": "Deprecated", "flags": "Flags, see TL conditional fields", "invoice": "The actual invoice", "payload": "Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes.", @@ -2842,6 +3183,15 @@ "title": "Product name, 1-32 characters" } }, + "InputMediaPaidMedia": { + "desc": "Paid media, see here \u00bb for more info.", + "params": { + "extended_media": "Photos or videos.", + "flags": "Flags, see TL conditional fields", + "payload": "Bots only, specifies a custom payload that will then be passed in updateBotPurchasedPaidMedia when a payment is made (this field will not be visible to the user)", + "stars_amount": "The price of the media in Telegram Stars." + } + }, "InputMediaPhoto": { "desc": "Forwarded photo", "params": { @@ -2863,11 +3213,21 @@ "InputMediaPoll": { "desc": "A poll", "params": { - "correct_answers": "Correct answer IDs (for quiz polls)", + "attached_media": "Optional media attachment to display alongside the poll", + "correct_answers": "0-based indices of the correct answers in the answers vector (for quiz polls)", "flags": "Flags, see TL conditional fields", "poll": "The poll to send", - "solution": "Explanation of quiz solution", - "solution_entities": "Message entities for styled text" + "solution": "Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds.", + "solution_entities": "Styled text message entities for the solution explanation.", + "solution_media": "Optional media attachment shown alongside the quiz solution explanation" + } + }, + "InputMediaStakeDice": { + "desc": "{schema}", + "params": { + "client_seed": "", + "game_hash": "", + "ton_amount": "" } }, "InputMediaStory": { @@ -2877,6 +3237,12 @@ "peer": "Peer where the story was posted" } }, + "InputMediaTodo": { + "desc": "Creates a todo list \u00bb.", + "params": { + "todo": "The todo list." + } + }, "InputMediaUploadedDocument": { "desc": "New document", "params": { @@ -2885,11 +3251,13 @@ "flags": "Flags, see TL conditional fields", "force_file": "Force the media file to be uploaded as document", "mime_type": "MIME type of document", - "nosound_video": "Whether the specified document is a video file with no audio tracks (a GIF animation (even as MPEG4), for example)", + "nosound_video": "Whether to send the file as a video even if it doesn't have an audio track (i.e. if set, the documentAttributeAnimated attribute will not be set even for videos without audio)", "spoiler": "Whether this media should be hidden behind a spoiler warning", "stickers": "Attached stickers", "thumb": "Thumbnail of the document, uploaded as for the file", - "ttl_seconds": "Time to live in seconds of self-destructing document" + "ttl_seconds": "Time to live in seconds of self-destructing document", + "video_cover": "Start playing the video at the specified timestamp (seconds).", + "video_timestamp": "Start playing the video at the specified timestamp (seconds)." } }, "InputMediaUploadedPhoto": { @@ -3050,6 +3418,36 @@ "desc": "Notifications generated by all users.", "params": {} }, + "InputPasskeyCredentialFirebasePNV": { + "desc": "{schema}", + "params": { + "pnv_token": "" + } + }, + "InputPasskeyCredentialPublicKey": { + "desc": "{schema}", + "params": { + "id": "", + "raw_id": "", + "response": "" + } + }, + "InputPasskeyResponseLogin": { + "desc": "{schema}", + "params": { + "authenticator_data": "", + "client_data": "", + "signature": "", + "user_handle": "" + } + }, + "InputPasskeyResponseRegister": { + "desc": "{schema}", + "params": { + "attestation_data": "", + "client_data": "" + } + }, "InputPaymentCredentials": { "desc": "Payment credentials", "params": { @@ -3098,6 +3496,12 @@ "chat_id": "Chat identifier" } }, + "InputPeerColorCollectible": { + "desc": "{schema}", + "params": { + "collectible_id": "" + } + }, "InputPeerEmpty": { "desc": "An empty constructor, no user or chat is defined.", "params": {} @@ -3155,7 +3559,9 @@ "params": { "client_id": "An arbitrary 64-bit integer: it should be set, for example, to an incremental number when using contacts.importContacts, in order to retry importing only the contacts that weren't imported successfully, according to the client_ids returned in contacts.importedContacts.retry_contacts.", "first_name": "Contact's first name", + "flags": "Flags, see TL conditional fields", "last_name": "Contact's last name", + "note": "", "phone": "Phone number" } }, @@ -3191,6 +3597,14 @@ "volume_id": "Volume ID" } }, + "InputPollAnswer": { + "desc": "An answer option to add to an open-answer poll \u00bb", + "params": { + "flags": "Flags, see TL conditional fields", + "media": "Optional media attachment to display alongside the answer", + "text": "The answer text" + } + }, "InputPrivacyKeyAbout": { "desc": "Whether people can see your bio", "params": {} @@ -3200,7 +3614,7 @@ "params": {} }, "InputPrivacyKeyBirthday": { - "desc": "{schema}", + "desc": "Whether the user can see our birthday.", "params": {} }, "InputPrivacyKeyChatInvite": { @@ -3211,6 +3625,10 @@ "desc": "Whether messages forwarded from you will be anonymous", "params": {} }, + "InputPrivacyKeyNoPaidMessages": { + "desc": "Who can send you messages without paying, if paid messages \u00bb are enabled.", + "params": {} + }, "InputPrivacyKeyPhoneCall": { "desc": "Whether you will accept phone calls", "params": {} @@ -3227,22 +3645,34 @@ "desc": "Whether people will be able to see your profile picture", "params": {} }, + "InputPrivacyKeySavedMusic": { + "desc": "{schema}", + "params": {} + }, + "InputPrivacyKeyStarGiftsAutoSave": { + "desc": "Whether received gifts will be automatically displayed on our profile", + "params": {} + }, "InputPrivacyKeyStatusTimestamp": { - "desc": "Whether people will be able to see your exact last online timestamp", + "desc": "Whether people will be able to see our exact last online timestamp.", "params": {} }, "InputPrivacyKeyVoiceMessages": { - "desc": "Whether people can send you voice messages", + "desc": "Whether people can send you voice messages or round videos (Premium users only).", "params": {} }, "InputPrivacyValueAllowAll": { "desc": "Allow all users", "params": {} }, + "InputPrivacyValueAllowBots": { + "desc": "Allow bots and mini apps", + "params": {} + }, "InputPrivacyValueAllowChatParticipants": { "desc": "Allow only participants of certain chats", "params": { - "chats": "Allowed chat IDs" + "chats": "Allowed chat IDs (either a chat or a supergroup ID, verbatim the way it is received in the constructor (i.e. unlike with bot API IDs, here group and supergroup IDs should be treated in the same way))." } }, "InputPrivacyValueAllowCloseFriends": { @@ -3254,7 +3684,7 @@ "params": {} }, "InputPrivacyValueAllowPremium": { - "desc": "{schema}", + "desc": "Allow only users with a Premium subscription \u00bb, currently only usable for inputPrivacyKeyChatInvite.", "params": {} }, "InputPrivacyValueAllowUsers": { @@ -3267,10 +3697,14 @@ "desc": "Disallow all", "params": {} }, + "InputPrivacyValueDisallowBots": { + "desc": "Disallow bots and mini apps", + "params": {} + }, "InputPrivacyValueDisallowChatParticipants": { "desc": "Disallow only participants of certain chats", "params": { - "chats": "Disallowed chat IDs" + "chats": "Disallowed chat IDs (either a chat or a supergroup ID, verbatim the way it is received in the constructor (i.e. unlike with bot API IDs, here group and supergroup IDs should be treated in the same way))." } }, "InputPrivacyValueDisallowContacts": { @@ -3284,33 +3718,41 @@ } }, "InputQuickReplyShortcut": { - "desc": "{schema}", + "desc": "Selects a quick reply shortcut by name.", "params": { - "shortcut": "" + "shortcut": "Shortcut name." } }, "InputQuickReplyShortcutId": { - "desc": "{schema}", + "desc": "Selects a quick reply shortcut by its numeric ID.", "params": { - "shortcut_id": "" + "shortcut_id": "Shortcut ID." } }, "InputReplyToMessage": { "desc": "Reply to a message.", "params": { "flags": "Flags, see TL conditional fields", + "monoforum_peer_id": "Must be set to the ID of the topic when replying to a message within a monoforum topic.", "quote_entities": "Message entities for styled text from the quote_text field.", "quote_offset": "Offset of the message quote_text within the original message (in UTF-16 code units).", "quote_text": "Used to quote-reply to only a certain section (specified here) of the original message. The maximum UTF-8 length for quotes is specified in the quote_length_max config key.", "reply_to_msg_id": "The message ID to reply to.", "reply_to_peer_id": "Used to reply to messages sent to another chat (specified here), can only be used for non-protected chats and messages.", + "todo_item_id": "Can be set to reply to the specified item of a todo list \u00bb.", "top_msg_id": "This field must contain the topic ID only when replying to messages in forum topics different from the \"General\" topic (i.e. reply_to_msg_id is set and reply_to_msg_id != topicID and topicID != 1). If the replied-to message is deleted before the method finishes execution, the value in this field will be used to send the message to the correct topic, instead of the \"General\" topic." } }, + "InputReplyToMonoForum": { + "desc": "Used to send messages to a monoforum topic.", + "params": { + "monoforum_peer_id": "The topic ID." + } + }, "InputReplyToStory": { "desc": "Reply to a story.", "params": { - "peer": "", + "peer": "Sender of the story", "story_id": "ID of the story to reply to." } }, @@ -3354,6 +3796,25 @@ "desc": "Report for violence", "params": {} }, + "InputSavedStarGiftChat": { + "desc": "A gift received by a channel we own.", + "params": { + "peer": "The channel.", + "saved_id": "ID of the gift, must be the saved_id of a messageActionStarGift/messageActionStarGiftUnique constructor." + } + }, + "InputSavedStarGiftSlug": { + "desc": "Points to a collectible gift obtained from a collectible gift link \u00bb.", + "params": { + "slug": "Slug from the link." + } + }, + "InputSavedStarGiftUser": { + "desc": "A gift received in a private chat with another user.", + "params": { + "msg_id": "ID of the messageService with the messageActionStarGift with the gift." + } + }, "InputSecureFile": { "desc": "Pre-uploaded passport file, for more info see the passport docs \u00bb", "params": { @@ -3402,6 +3863,26 @@ "random_id": "Unique client media ID required to prevent message resending" } }, + "InputStarGiftAuction": { + "desc": "{schema}", + "params": { + "gift_id": "" + } + }, + "InputStarGiftAuctionSlug": { + "desc": "{schema}", + "params": { + "slug": "" + } + }, + "InputStarsTransaction": { + "desc": "Used to fetch info about a Telegram Star transaction \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "id": "Transaction ID.", + "refund": "If set, fetches info about the refund transaction for this transaction." + } + }, "InputStickerSetAnimatedEmoji": { "desc": "Animated emojis stickerset", "params": {} @@ -3470,6 +3951,10 @@ "thumb_version": "Thumbnail version" } }, + "InputStickerSetTonGifts": { + "desc": "TON gifts stickerset.", + "params": {} + }, "InputStickeredMediaDocument": { "desc": "A document with stickers attached", "params": { @@ -3482,6 +3967,17 @@ "id": "The photo" } }, + "InputStorePaymentAuthCode": { + "desc": "Indicates payment for a login code.", + "params": { + "amount": "Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code", + "flags": "Flags, see TL conditional fields", + "phone_code_hash": "phone_code_hash returned by auth.sendCode.", + "phone_number": "Phone number.", + "restore": "Set this flag to restore a previously made purchase." + } + }, "InputStorePaymentGiftPremium": { "desc": "Info about a gifted Telegram Premium purchase", "params": { @@ -3491,12 +3987,13 @@ } }, "InputStorePaymentPremiumGiftCode": { - "desc": "Used to gift Telegram Premium subscriptions only to some specific subscribers of a channel or to some of our contacts, see here \u00bb for more info on giveaways and gifts.", + "desc": "Used to gift Telegram Premium subscriptions only to some specific subscribers of a channel/supergroup or to some of our contacts, see here \u00bb for more info on giveaways and gifts.", "params": { "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", - "boost_peer": "If set, the gifts will be sent on behalf of a channel we are an admin of, which will also assign some boosts to it. Otherwise, the gift will be sent directly from the currently logged in users, and we will gain some extra boost slots. See here \u00bb for more info on giveaways and gifts.", + "boost_peer": "If set, the gifts will be sent on behalf of a channel/supergroup we are an admin of, which will also assign some boosts to it. Otherwise, the gift will be sent directly from the currently logged in user, and we will gain some extra boost slots. See here \u00bb for more info on giveaways and gifts.", "currency": "Three-letter ISO 4217 currency code", "flags": "Flags, see TL conditional fields", + "message": "Message attached with the gift", "users": "The users that will receive the Telegram Premium subscriptions." } }, @@ -3505,7 +4002,7 @@ "params": { "additional_peers": "Additional channels that the user must join to participate to the giveaway can be specified here.", "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", - "boost_peer": "The channel starting the giveaway, that the user must join to participate, that will receive the giveaway boosts; see here \u00bb for more info on giveaways.", + "boost_peer": "The channel/supergroup starting the giveaway, that the user must join to participate, that will receive the giveaway boosts; see here \u00bb for more info on giveaways.", "countries_iso2": "The set of users that can participate to the giveaway can be restricted by passing here an explicit whitelist of up to giveaway_countries_max countries, specified as two-letter ISO 3166-1 alpha-2 country codes.", "currency": "Three-letter ISO 4217 currency code", "flags": "Flags, see TL conditional fields", @@ -3524,13 +4021,41 @@ "upgrade": "Pass true if this is an upgrade from a monthly subscription to a yearly subscription; only for App Store" } }, - "InputStorePaymentStars": { - "desc": "{schema}", + "InputStorePaymentStarsGift": { + "desc": "Used to gift Telegram Stars to a friend.", "params": { - "amount": "", - "currency": "", + "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code", + "stars": "Amount of stars to gift", + "user_id": "The user to which the stars should be gifted." + } + }, + "InputStorePaymentStarsGiveaway": { + "desc": "Used to pay for a star giveaway, see here \u00bb for more info.", + "params": { + "additional_peers": "Additional channels that the user must join to participate to the giveaway can be specified here.", + "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "boost_peer": "The channel/supergroup starting the giveaway, that the user must join to participate, that will receive the giveaway boosts; see here \u00bb for more info on giveaways.", + "countries_iso2": "The set of users that can participate to the giveaway can be restricted by passing here an explicit whitelist of up to giveaway_countries_max countries, specified as two-letter ISO 3166-1 alpha-2 country codes.", + "currency": "Three-letter ISO 4217 currency code", + "flags": "Flags, see TL conditional fields", + "only_new_subscribers": "If set, only new subscribers starting from the giveaway creation date will be able to participate to the giveaway.", + "prize_description": "Can contain a textual description of additional giveaway prizes.", + "random_id": "Random ID to avoid resending the giveaway", + "stars": "Total number of Telegram Stars being given away (each user will receive stars/users stars).", + "until_date": "The end date of the giveaway, must be at most giveaway_period_max seconds in the future; see here \u00bb for more info on giveaways.", + "users": "Number of winners.", + "winners_are_visible": "If set, giveaway winners are public and will be listed in a messageMediaGiveawayResults message that will be automatically sent to the channel once the giveaway ends." + } + }, + "InputStorePaymentStarsTopup": { + "desc": "Used to top up the Telegram Stars balance of the current account.", + "params": { + "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code", "flags": "Flags, see TL conditional fields", - "stars": "" + "spend_purpose_peer": "Should be populated with the peer where the topup process was initiated due to low funds (i.e. a bot for bot payments, a channel for paid media/reactions, etc); leave this flag unpopulated if the topup flow was not initated when attempting to spend more Stars than currently available on the account's balance.", + "stars": "Amount of stars to topup" } }, "InputTakeoutFileLocation": { @@ -3645,7 +4170,7 @@ "Invoice": { "desc": "Invoice", "params": { - "currency": "Three-letter ISO 4217 currency code", + "currency": "Three-letter ISO 4217 currency code, or XTR for Telegram Stars.", "email_requested": "Set this flag if you require the user's email address to complete the order", "email_to_provider": "Set this flag if user's email address should be sent to provider", "flags": "Flags, see TL conditional fields", @@ -3657,6 +4182,7 @@ "prices": "Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.)", "recurring": "Whether this is a recurring payment", "shipping_address_requested": "Set this flag if you require the user's shipping address to complete the order", + "subscription_period": "The number of seconds between consecutive Telegram Star debiting for bot subscription invoices", "suggested_tip_amounts": "A vector of suggested amounts of tips in the smallest units of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount.", "terms_url": "Terms of service URL", "test": "Test invoice" @@ -3706,12 +4232,16 @@ "KeyboardButton": { "desc": "Bot keyboard button", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text" } }, "KeyboardButtonBuy": { "desc": "Button to buy a product", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text" } }, @@ -3721,18 +4251,32 @@ "data": "Callback data", "flags": "Flags, see TL conditional fields", "requires_password": "Whether the user should verify his identity by entering his 2FA SRP parameters to the messages.getBotCallbackAnswer method. NOTE: telegram and the bot WILL NOT have access to the plaintext password, thanks to SRP. This button is mainly used by the official @botfather bot, for verifying the user's identity before transferring ownership of a bot to another user.", + "style": "", "text": "Button text" } }, + "KeyboardButtonCopy": { + "desc": "Clipboard button: when clicked, the attached text must be copied to the clipboard.", + "params": { + "copy_text": "The text that will be copied to the clipboard", + "flags": "Flags, see TL conditional fields", + "style": "", + "text": "Title of the button" + } + }, "KeyboardButtonGame": { "desc": "Button to start a game", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text" } }, "KeyboardButtonRequestGeoLocation": { "desc": "Button to request a user's geolocation", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text" } }, @@ -3740,14 +4284,18 @@ "desc": "Prompts the user to select and share one or more peers with the bot using messages.sendBotRequestedPeer", "params": { "button_id": "Button ID, to be passed to messages.sendBotRequestedPeer.", - "max_quantity": "Maximum number of peers that can be chosne.", + "flags": "Flags, see TL conditional fields", + "max_quantity": "Maximum number of peers that can be chosen.", "peer_type": "Filtering criteria to use for the peer selection list shown to the user. The list should display all existing peers of the specified type, and should also offer an option for the user to create and immediately use one or more (up to max_quantity) peers of the specified type, if needed.", + "style": "", "text": "Button text" } }, "KeyboardButtonRequestPhone": { "desc": "Button to request a user's phone number", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text" } }, @@ -3756,6 +4304,7 @@ "params": { "flags": "Flags, see TL conditional fields", "quiz": "If set, only quiz polls can be sent", + "style": "", "text": "Button text" } }, @@ -3768,10 +4317,22 @@ "KeyboardButtonSimpleWebView": { "desc": "Button to open a bot mini app using messages.requestSimpleWebView, without sending user information to the web app.", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text", "url": "Web app URL" } }, + "KeyboardButtonStyle": { + "desc": "{schema}", + "params": { + "bg_danger": "", + "bg_primary": "", + "bg_success": "", + "flags": "Flags, see TL conditional fields", + "icon": "" + } + }, "KeyboardButtonSwitchInline": { "desc": "Button to force a user to switch to inline mode: pressing the button will prompt the user to select one of their chats, open that chat and insert the bot's username and the specified inline query in the input field.", "params": { @@ -3779,12 +4340,15 @@ "peer_types": "Filter to use when selecting chats.", "query": "The inline query to use", "same_peer": "If set, pressing the button will insert the bot's username and the specified inline query in the current chat's input field.", + "style": "", "text": "Button label" } }, "KeyboardButtonUrl": { "desc": "URL button", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button label", "url": "URL" } @@ -3795,6 +4359,7 @@ "button_id": "ID of the button to pass to messages.requestUrlAuth", "flags": "Flags, see TL conditional fields", "fwd_text": "New text of the button in forwarded messages.", + "style": "", "text": "Button label", "url": "An HTTP URL to be opened with user authorization data added to the query string when the button is pressed. If the user refuses to provide authorization data, the original URL without information about the user will be opened. The data added is the same as described in Receiving authorization data.NOTE: Services must always check the hash of the received data to verify the authentication and the integrity of the data as described in Checking authorization." } @@ -3802,6 +4367,8 @@ "KeyboardButtonUserProfile": { "desc": "Button that links directly to a user profile", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text", "user_id": "User ID" } @@ -3809,6 +4376,8 @@ "KeyboardButtonWebView": { "desc": "Button to open a bot mini app using messages.requestWebView, sending over user information after user confirmation.", "params": { + "flags": "Flags, see TL conditional fields", + "style": "", "text": "Button text", "url": "Web app url" } @@ -3890,9 +4459,11 @@ } }, "MediaAreaCoordinates": { - "desc": "Coordinates and size of a clicable rectangular area on top of a story.", + "desc": "Coordinates and size of a clickable rectangular area on top of a story.", "params": { + "flags": "Flags, see TL conditional fields", "h": "The height of the rectangle, as a percentage of the media height (0-100).", + "radius": "The radius of the rectangle corner rounding, as a percentage of the media width.", "rotation": "Clockwise rotation angle of the rectangle, in degrees (0-360).", "w": "The width of the rectangle, as a percentage of the media width (0-100).", "x": "The abscissa of the rectangle's center, as a percentage of the media width (0-100).", @@ -3902,10 +4473,19 @@ "MediaAreaGeoPoint": { "desc": "Represents a geolocation tag attached to a story.", "params": { + "address": "Optional textual representation of the address.", "coordinates": "The size and position of the media area corresponding to the location sticker on top of the story media.", + "flags": "Flags, see TL conditional fields", "geo": "Coordinates of the geolocation tag." } }, + "MediaAreaStarGift": { + "desc": "Represents a collectible gift \u00bb.", + "params": { + "coordinates": "Coordinates of the media area.", + "slug": "slug from starGiftUnique.slug, that can be resolved as specified here \u00bb." + } + }, "MediaAreaSuggestedReaction": { "desc": "Represents a reaction bubble.", "params": { @@ -3916,6 +4496,13 @@ "reaction": "The reaction that should be sent when this area is clicked." } }, + "MediaAreaUrl": { + "desc": "Represents a URL media area.", + "params": { + "coordinates": "The size and location of the media area corresponding to the URL button on top of the story media.", + "url": "URL to open when clicked." + } + }, "MediaAreaVenue": { "desc": "Represents a location tag attached to a story, with additional venue information.", "params": { @@ -3928,20 +4515,30 @@ "venue_type": "Venue type in the provider's database" } }, + "MediaAreaWeather": { + "desc": "Represents a weather widget \u00bb.", + "params": { + "color": "ARGB background color.", + "coordinates": "The size and location of the media area corresponding to the widget on top of the story media.", + "emoji": "Weather emoji, should be rendered as an animated emoji.", + "temperature_c": "Temperature in degrees Celsius." + } + }, "Message": { "desc": "A message", "params": { "date": "Date of the message", "edit_date": "Last edit date of this message", "edit_hide": "Whether the message should be shown as not modified to the user, even if an edit date is present", - "effect": "", + "effect": "A message effect that should be played as specified here \u00bb.", "entities": "Message entities for styled text", - "factcheck": "", + "factcheck": "Represents a fact-check \u00bb.", "flags": "Flags, see TL conditional fields", - "flags2": "", + "flags2": "Flags, see TL conditional fields", "forwards": "Forward counter", - "from_boosts_applied": "", + "from_boosts_applied": "Supergroups only, contains the number of boosts this user has given the current supergroup, and should be shown in the UI in the header of the message. Only present for incoming messages from non-anonymous supergroup members that have boosted the supergroup. Note that this counter should be locally overridden for non-anonymous outgoing messages, according to the current value of channelFull.boosts_applied, to ensure the value is correct even for messages sent by the current user before a supergroup was boosted (or after a boost has expired or the number of boosts has changed); do not update this value for incoming messages from other users, even if their boosts have changed.", "from_id": "ID of the sender of the message", + "from_rank": "", "from_scheduled": "Whether this is a scheduled message", "fwd_from": "Info about forwarded messages", "grouped_id": "Multiple media messages sent using messages.sendMultiMedia with the same grouped ID indicate an album or media group", @@ -3953,30 +4550,38 @@ "mentioned": "Whether we were mentioned in this message", "message": "The message", "noforwards": "Whether this message is protected and thus cannot be forwarded; clients should also prevent users from saving attached media (i.e. videos should only be streamed, photos should be kept in RAM, et cetera).", - "offline": "", + "offline": "If set, the message was sent because of a scheduled action by the message sender, for example, as away, or a greeting service message.", "out": "Is this an outgoing message", + "paid_message_stars": "The amount of stars the sender has paid to send the message, see here \u00bb for more info.", + "paid_suggested_post_stars": "Set if this is a suggested channel post \u00bb that was paid using Telegram Stars.", + "paid_suggested_post_ton": "Set if this is a suggested channel post \u00bb that was paid using Toncoins.", "peer_id": "Peer ID, the chat where this message was sent", "pinned": "Whether this message is pinned", "post": "Whether this is a channel post", "post_author": "Name of the author of this message for channel posts (with signatures enabled)", - "quick_reply_shortcut_id": "", + "quick_reply_shortcut_id": "If set, this message is a quick reply shortcut message \u00bb (note that quick reply shortcut messages sent to a private chat will not have this field set).", "reactions": "Reactions to this message", "replies": "Info about post comments (for channels) or message replies (for groups)", "reply_markup": "Reply markup (bot/inline keyboards)", "reply_to": "Reply information", + "report_delivery_until_date": "Used for Telegram Gateway verification messages: if set and the current unixtime is bigger than the specified unixtime, invoke messages.reportMessagesDelivery passing the ID and the peer of this message as soon as it is received by the client (optionally batching requests for the same peer).", "restriction_reason": "Contains the reason why access to this message must be restricted.", - "saved_peer_id": "Messages fetched from a saved messages dialog \u00bb will have peer=inputPeerSelf and the saved_peer_id flag set to the ID of the saved dialog.", + "saved_peer_id": "Messages from a saved messages dialog \u00bb will have peer=inputPeerSelf and the saved_peer_id flag set to the ID of the saved dialog.Messages from a monoforum \u00bb will have peer=ID of the monoforum and the saved_peer_id flag set to the ID of a topic.", + "schedule_repeat_period": "", "silent": "Whether this is a silent message (no notification triggered)", + "suggested_post": "Used to suggest a post to a channel, see here \u00bb for more info on the full flow.", + "summary_from_language": "", "ttl_period": "Time To Live of the message, once message.date+message.ttl_period === time(), the message will be deleted on the server, and must be deleted locally as well.", "via_bot_id": "ID of the inline bot that generated the message", - "via_business_bot_id": "", + "via_business_bot_id": "Whether the message was sent by the business bot specified in via_bot_id on behalf of the user.", + "video_processing_pending": "The video contained in the message is currently being processed by the server (i.e. to generate alternative qualities, that will be contained in the final messageMediaDocument.alt_document), and will be sent once the video is processed, which will happen approximately at the specified date (i.e. messages with this flag set should be treated similarly to scheduled messages, but instead of the scheduled date, date contains the estimated conversion date). See here \u00bb for more info.", "views": "View count for channel posts" } }, "MessageActionBoostApply": { - "desc": "{schema}", + "desc": "Some boosts \u00bb were applied to the channel or supergroup.", "params": { - "boosts": "" + "boosts": "Number of applied boosts." } }, "MessageActionBotAllowed": { @@ -3989,6 +4594,12 @@ "from_request": "We have allowed the bot to send us messages using bots.allowSendMessage \u00bb." } }, + "MessageActionChangeCreator": { + "desc": "{schema}", + "params": { + "new_creator_id": "" + } + }, "MessageActionChannelCreate": { "desc": "The channel was created", "params": { @@ -4053,6 +4664,18 @@ "channel_id": "The supergroup it was migrated to" } }, + "MessageActionConferenceCall": { + "desc": "Represents a conference call (or an invitation to a conference call, if neither the missed nor active flags are set).", + "params": { + "active": "Whether the user is currently in the conference call.", + "call_id": "Call ID.", + "duration": "Call duration, for left calls only.", + "flags": "Flags, see TL conditional fields", + "missed": "Whether the conference call has ended and the user hasn't joined.", + "other_participants": "Identifiers of some other call participants.", + "video": "Whether this is a video conference call." + } + }, "MessageActionContactSignUp": { "desc": "A contact just signed up to telegram", "params": {} @@ -4086,15 +4709,16 @@ "desc": "Contains a Telegram Premium giftcode link.", "params": { "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", - "boost_peer": "Identifier of the channel that created the gift code either directly or through a giveaway: if we import this giftcode link, we will also automatically boost this channel.", + "boost_peer": "Identifier of the channel/supergroup that created the gift code either directly or through a giveaway: if we import this giftcode link, we will also automatically boost this channel/supergroup.", "crypto_amount": "If crypto_currency is set, contains the paid amount, in the smallest units of the cryptocurrency.", "crypto_currency": "If set, the gift was made using the specified cryptocurrency.", "currency": "Three-letter ISO 4217 currency code", + "days": "", "flags": "Flags, see TL conditional fields", - "months": "Duration in months of the gifted Telegram Premium subscription.", + "message": "Message attached with the gift", "slug": "Slug of the Telegram Premium giftcode link", "unclaimed": "If set, the link was not redeemed yet.", - "via_giveaway": "If set, this gift code was received from a giveaway \u00bb started by a channel we're subscribed to." + "via_giveaway": "If set, this gift code was received from a giveaway \u00bb started by a channel/supergroup we're subscribed to." } }, "MessageActionGiftPremium": { @@ -4104,17 +4728,46 @@ "crypto_amount": "If the gift was bought using a cryptocurrency, price of the gift in the smallest units of a cryptocurrency.", "crypto_currency": "If the gift was bought using a cryptocurrency, the cryptocurrency name.", "currency": "Three-letter ISO 4217 currency code", + "days": "", + "flags": "Flags, see TL conditional fields", + "message": "Message attached with the gift" + } + }, + "MessageActionGiftStars": { + "desc": "You gifted or were gifted some Telegram Stars.", + "params": { + "amount": "Price of the gift in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "crypto_amount": "If the gift was bought using a cryptocurrency, price of the gift in the smallest units of a cryptocurrency.", + "crypto_currency": "If the gift was bought using a cryptocurrency, the cryptocurrency name.", + "currency": "Three-letter ISO 4217 currency code", + "flags": "Flags, see TL conditional fields", + "stars": "Amount of gifted stars", + "transaction_id": "Identifier of the transaction, only visible to the receiver of the gift." + } + }, + "MessageActionGiftTon": { + "desc": "You were gifted some toncoins.", + "params": { + "amount": "FIAT currency equivalent (in the currency specified in currency) of the amount specified in crypto_amount.", + "crypto_amount": "Amount in the smallest unit of the cryptocurrency (for TONs, one billionth of a ton, AKA a nanoton).", + "crypto_currency": "Name of the cryptocurrency.", + "currency": "Name of a localized FIAT currency.", "flags": "Flags, see TL conditional fields", - "months": "Duration of the gifted Telegram Premium subscription" + "transaction_id": "Transaction ID." } }, "MessageActionGiveawayLaunch": { "desc": "A giveaway was started.", - "params": {} + "params": { + "flags": "Flags, see TL conditional fields", + "stars": "For Telegram Star giveaways, the total number of Telegram Stars being given away." + } }, "MessageActionGiveawayResults": { "desc": "A giveaway has ended.", "params": { + "flags": "Flags, see TL conditional fields", + "stars": "If set, this is a Telegram Star giveaway", "unclaimed_count": "Number of undistributed prizes", "winners_count": "Number of winners in the giveaway" } @@ -4145,14 +4798,63 @@ "users": "The invited users" } }, + "MessageActionNewCreatorPending": { + "desc": "{schema}", + "params": { + "new_creator_id": "" + } + }, + "MessageActionNoForwardsRequest": { + "desc": "{schema}", + "params": { + "expired": "", + "flags": "Flags, see TL conditional fields", + "new_value": "", + "prev_value": "" + } + }, + "MessageActionNoForwardsToggle": { + "desc": "{schema}", + "params": { + "new_value": "", + "prev_value": "" + } + }, + "MessageActionPaidMessagesPrice": { + "desc": "The price of paid messages \u00bb in this chat was changed.", + "params": { + "broadcast_messages_allowed": "Can only be set for channels, if set indicates that direct messages were enabled \u00bb, otherwise indicates that direct messages were disabled; the price of paid messages is related to the price of direct messages (aka those sent to the associated monoforum).", + "flags": "Flags, see TL conditional fields", + "stars": "The new price in Telegram Stars, can be 0 if messages are now free." + } + }, + "MessageActionPaidMessagesRefunded": { + "desc": "Sent from peer A to B, indicates that A refunded all stars B previously paid to send messages to A, see here \u00bb for more info on paid messages.", + "params": { + "count": "Number of paid messages affected by the refund.", + "stars": "Number of refunded stars." + } + }, + "MessageActionPaymentRefunded": { + "desc": "Describes a payment refund (service message received by both users and bots).", + "params": { + "charge": "Provider payment identifier", + "currency": "Currency, XTR for Telegram Stars.", + "flags": "Flags, see TL conditional fields", + "payload": "Bot specified invoice payload (only received by bots).", + "peer": "Identifier of the peer that returned the funds.", + "total_amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies)." + } + }, "MessageActionPaymentSent": { "desc": "A payment was sent", "params": { - "currency": "Three-letter ISO 4217 currency code", + "currency": "Three-letter ISO 4217 currency code, or XTR for Telegram Stars.", "flags": "Flags, see TL conditional fields", "invoice_slug": "An invoice slug taken from an invoice deep link or from the premium_invoice_slug app config parameter \u00bb", "recurring_init": "Whether this is the first payment of a recurring payment we just subscribed to", "recurring_used": "Whether this payment is part of a recurring payment", + "subscription_until_date": "Expiration date of the Telegram Star subscription \u00bb.", "total_amount": "Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies)." } }, @@ -4160,13 +4862,14 @@ "desc": "A user just sent a payment to me (a bot)", "params": { "charge": "Provider payment identifier", - "currency": "Three-letter ISO 4217 currency code", + "currency": "Three-letter ISO 4217 currency code, or XTR for Telegram Stars.", "flags": "Flags, see TL conditional fields", "info": "Order info provided by the user", "payload": "Bot specified invoice payload", "recurring_init": "Whether this is the first payment of a recurring payment we just subscribed to", "recurring_used": "Whether this payment is part of a recurring payment", "shipping_option_id": "Identifier of the shipping option chosen by the user", + "subscription_until_date": "Expiration date of the Telegram Star subscription \u00bb.", "total_amount": "Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies)." } }, @@ -4184,18 +4887,29 @@ "desc": "A message was pinned", "params": {} }, + "MessageActionPrizeStars": { + "desc": "You won some Telegram Stars in a Telegram Star giveaway \u00bb.", + "params": { + "boost_peer": "Identifier of the peer that was automatically boosted by the winners of the giveaway.", + "flags": "Flags, see TL conditional fields", + "giveaway_msg_id": "ID of the message containing the messageMediaGiveaway", + "stars": "The number of Telegram Stars you won", + "transaction_id": "ID of the telegram star transaction.", + "unclaimed": "If set, this indicates the reverse transaction that refunds the remaining stars to the creator of a giveaway if, when the giveaway ends, the number of members in the channel is smaller than the number of winners in the giveaway." + } + }, "MessageActionRequestedPeer": { - "desc": "Contains info about one or more peers that the user shared with the bot after clicking on a keyboardButtonRequestPeer button.", + "desc": "Contains info about one or more peers that the we (the user) shared with the bot after clicking on a keyboardButtonRequestPeer button (service message sent by the user).", "params": { "button_id": "button_id contained in the keyboardButtonRequestPeer", "peers": "The shared peers" } }, "MessageActionRequestedPeerSentMe": { - "desc": "{schema}", + "desc": "Contains info about one or more peers that the a user shared with the me (the bot) after clicking on a keyboardButtonRequestPeer button (service message received by the bot).", "params": { - "button_id": "", - "peers": "" + "button_id": "button_id contained in the keyboardButtonRequestPeer", + "peers": "Info about the shared peers." } }, "MessageActionScreenshotTaken": { @@ -4218,7 +4932,7 @@ "MessageActionSetChatTheme": { "desc": "The chat theme was changed", "params": { - "emoticon": "The emoji that identifies a chat theme" + "theme": "The new chat theme." } }, "MessageActionSetChatWallPaper": { @@ -4238,19 +4952,135 @@ "period": "New Time-To-Live of all messages sent in this chat; if 0, autodeletion was disabled." } }, + "MessageActionStarGift": { + "desc": "You received a gift, see here \u00bb for more info.", + "params": { + "auction_acquired": "", + "can_upgrade": "If set, this gift can be upgraded to a collectible gift; can only be set for the receiver of a gift.", + "convert_stars": "The receiver of this gift may convert it to this many Telegram Stars, instead of displaying it on their profile page.convert_stars will be equal to stars only if the gift was bought using recently bought Telegram Stars, otherwise it will be less than stars.", + "converted": "Whether this gift was converted to Telegram Stars and cannot be displayed on the profile anymore.", + "flags": "Flags, see TL conditional fields", + "from_id": "Sender of the gift (unset for anonymous gifts).", + "gift": "Info about the gift", + "gift_msg_id": "For separate upgrades, the identifier of the message with the gift whose upgrade was prepaid (only valid for the receiver of the service message).", + "gift_num": "", + "message": "Additional message from the sender of the gift", + "name_hidden": "If set, the name of the sender of the gift will be hidden if the destination user decides to display the gift on their profile", + "peer": "Receiver of the gift.", + "prepaid_upgrade": "The sender has already pre-paid for the upgrade of this gift to a collectible gift.", + "prepaid_upgrade_hash": "Hash to prepay for a gift upgrade separately \u00bb.", + "refunded": "This gift is not available anymore because a request to refund the payment related to this gift was made, and the money was returned.", + "saved": "Whether this gift was added to the destination user's profile (may be toggled using payments.saveStarGift and fetched using payments.getSavedStarGifts)", + "saved_id": "For channel gifts, ID to use in inputSavedStarGiftChat constructors.", + "to_id": "", + "upgrade_msg_id": "If set, this gift was upgraded to a collectible gift, and the corresponding messageActionStarGiftUnique is available at the specified message ID.", + "upgrade_separate": "This service message is the notification of a separate pre-payment for the upgrade of a gift we own.", + "upgrade_stars": "The number of Telegram Stars the user can pay to convert the gift into a collectible gift \u00bb.", + "upgraded": "This gift was upgraded to a collectible gift \u00bb." + } + }, + "MessageActionStarGiftPurchaseOffer": { + "desc": "{schema}", + "params": { + "accepted": "", + "declined": "", + "expires_at": "", + "flags": "Flags, see TL conditional fields", + "gift": "", + "price": "" + } + }, + "MessageActionStarGiftPurchaseOfferDeclined": { + "desc": "{schema}", + "params": { + "expired": "", + "flags": "Flags, see TL conditional fields", + "gift": "", + "price": "" + } + }, + "MessageActionStarGiftUnique": { + "desc": "A gift \u00bb was upgraded to a collectible gift \u00bb.", + "params": { + "assigned": "", + "can_craft_at": "", + "can_export_at": "If set, indicates that the current gift can't be exported to the TON blockchain \u00bb yet: the owner will be able to export it at the specified unixtime.", + "can_resell_at": "If set, indicates that the current gift can't be resold \u00bb yet: the owner will be able to put it up for sale at the specified unixtime.", + "can_transfer_at": "If set, indicates that the current gift can't be transferred \u00bb yet: the owner will be able to transfer it at the specified unixtime.", + "craft": "", + "drop_original_details_stars": "", + "flags": "Flags, see TL conditional fields", + "from_id": "Sender of the gift (unset for anonymous gifts).", + "from_offer": "", + "gift": "The collectible gift.", + "peer": "Receiver of the gift.", + "prepaid_upgrade": "The sender has pre-paid for the upgrade of this gift to a collectible gift.", + "refunded": "This gift was upgraded to a collectible gift \u00bb and then re-downgraded to a regular gift because a request to refund the payment related to the upgrade was made, and the money was returned.", + "resale_amount": "Resale price of the gift.", + "saved": "If set, this gift is visible on the user or channel's profile page; can only be set for the receiver of a gift.", + "saved_id": "For channel gifts, ID to use in inputSavedStarGiftChat constructors.", + "transfer_stars": "If set, indicates that the gift can be transferred \u00bb to another user by paying the specified amount of stars.", + "transferred": "If set, this collectible was transferred (either to the current user or by the current user to the other user in the private chat, depending on the out flag of the containing messageService).", + "upgrade": "If set, this collectible was upgraded \u00bb to a collectible gift from a previously received or sent (depending on the out flag of the containing messageService) non-collectible gift." + } + }, + "MessageActionSuggestBirthday": { + "desc": "{schema}", + "params": { + "birthday": "" + } + }, "MessageActionSuggestProfilePhoto": { "desc": "A new profile picture was suggested using photos.uploadContactProfilePhoto.", "params": { "photo": "The photo that the user suggested we set as profile picture." } }, + "MessageActionSuggestedPostApproval": { + "desc": "A suggested post \u00bb was approved or rejected.", + "params": { + "balance_too_low": "If set, the post was approved but the user's balance is too low to pay for the suggested post.", + "flags": "Flags, see TL conditional fields", + "price": "Price for the suggested post.", + "reject_comment": "If the suggested post was rejected, can optionally contain a rejection comment.", + "rejected": "Whether the suggested post was rejected.", + "schedule_date": "Scheduling date." + } + }, + "MessageActionSuggestedPostRefund": { + "desc": "A suggested post \u00bb was accepted and posted or scheduled, but either the channel deleted the posted/scheduled post before stars_suggested_post_age_min seconds have elapsed, or the user refunded the payment for the stars used to pay for the suggested post.", + "params": { + "flags": "Flags, see TL conditional fields", + "payer_initiated": "If set, the user refunded the payment for the stars used to pay for the suggested post." + } + }, + "MessageActionSuggestedPostSuccess": { + "desc": "A suggested post \u00bb was successfully posted, and payment for it was successfully received.", + "params": { + "price": "The price." + } + }, + "MessageActionTodoAppendTasks": { + "desc": "Items were appended to the todo list \u00bb.", + "params": { + "list": "Appended items." + } + }, + "MessageActionTodoCompletions": { + "desc": "Items were marked as completed or not completed in a todo list \u00bb.", + "params": { + "completed": "Items marked as completed.", + "incompleted": "Items marked as not completed." + } + }, "MessageActionTopicCreate": { "desc": "A forum topic was created.", "params": { "flags": "Flags, see TL conditional fields", "icon_color": "If no custom emoji icon is specified, specifies the color of the fallback topic icon (RGB), one of 0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, or 0xFB6F5F.", "icon_emoji_id": "ID of the custom emoji used as topic icon.", - "title": "Topic name." + "title": "Topic name.", + "title_missing": "" } }, "MessageActionTopicEdit": { @@ -4294,7 +5124,7 @@ "MessageEntityBlockquote": { "desc": "Message entity representing a block quote.", "params": { - "collapsed": "", + "collapsed": "Whether the quote is collapsed by default.", "flags": "Flags, see TL conditional fields", "length": "Length of message entity within message (in UTF-16 code units)", "offset": "Offset of message entity within message (in UTF-16 code units)" @@ -4343,6 +5173,21 @@ "offset": "Offset of message entity within message (in UTF-16 code units)" } }, + "MessageEntityFormattedDate": { + "desc": "{schema}", + "params": { + "date": "", + "day_of_week": "", + "flags": "Flags, see TL conditional fields", + "length": "", + "long_date": "", + "long_time": "", + "offset": "", + "relative": "", + "short_date": "", + "short_time": "" + } + }, "MessageEntityHashtag": { "desc": "#hashtag message entity", "params": { @@ -4431,18 +5276,18 @@ } }, "MessageExtendedMedia": { - "desc": "Extended media", + "desc": "Already purchased paid media, see here \u00bb for more info.", "params": { - "media": "Media" + "media": "The media we purchased." } }, "MessageExtendedMediaPreview": { - "desc": "Extended media preview", + "desc": "Paid media preview for not yet purchased paid media, see here \u00bb for more info.", "params": { "flags": "Flags, see TL conditional fields", "h": "Height", - "thumb": "Thumbnail", - "video_duration": "Video duration", + "thumb": "Extremely low resolution thumbnail.", + "video_duration": "Video duration for videos.", "w": "Width" } }, @@ -4479,13 +5324,15 @@ "desc": "Dice-based animated sticker", "params": { "emoticon": "The emoji, for now , and are supported", + "flags": "Flags, see TL conditional fields", + "game_outcome": "", "value": "Dice value" } }, "MessageMediaDocument": { "desc": "Document (video, audio, voice, sticker, any media type except photo)", "params": { - "alt_document": "Currently only used for story videos, may contain an alternative version of the story video, explicitly encoded using H.264 (in MPEG4 transport) at a lower resolution than document.", + "alt_documents": "Videos only, contains alternative qualities of the video.", "document": "Attached document", "flags": "Flags, see TL conditional fields", "nopremium": "Whether this is a normal sticker, if not set this is a premium sticker and a premium sticker animation must be played.", @@ -4493,6 +5340,8 @@ "spoiler": "Whether this media should be hidden behind a spoiler warning", "ttl_seconds": "Time to live of self-destructing document", "video": "Whether this is a video.", + "video_cover": "Custom video cover.", + "video_timestamp": "Start playing the video at the specified timestamp (seconds).", "voice": "Whether this is a voice message." } }, @@ -4532,6 +5381,7 @@ "only_new_subscribers": "If set, only new subscribers starting from the giveaway creation date will be able to participate to the giveaway.", "prize_description": "Can contain a textual description of additional giveaway prizes.", "quantity": "Number of Telegram Premium subscriptions given away.", + "stars": "For Telegram Star giveaways, the total number of Telegram Stars being given away.", "until_date": "The end date of the giveaway.", "winners_are_visible": "If set, giveaway winners are public and will be listed in a messageMediaGiveawayResults message that will be automatically sent to the channel once the giveaway ends." } @@ -4540,13 +5390,14 @@ "desc": "A giveaway with public winners has finished, this constructor contains info about the winners.", "params": { "additional_peers_count": "Number of other channels that participated in the giveaway.", - "channel_id": "ID of the channel that was automatically boosted by the winners of the giveaway for duration of the Premium subscription.", + "channel_id": "ID of the channel/supergroup that was automatically boosted by the winners of the giveaway for duration of the Premium subscription.", "flags": "Flags, see TL conditional fields", "launch_msg_id": "Identifier of the message with the giveaway in channel_id.", "months": "Duration in months of each Telegram Premium subscription in the giveaway.", "only_new_subscribers": "If set, only new subscribers starting from the giveaway creation date participated in the giveaway.", "prize_description": "Can contain a textual description of additional giveaway prizes.", "refunded": "If set, the giveaway was canceled and was fully refunded.", + "stars": "For Telegram Star giveaways, the total number of Telegram Stars being given away.", "unclaimed_count": "Number of not-yet-claimed prizes.", "until_date": "Point in time (Unix timestamp) when the winners were selected. May be bigger than winners selection date specified in initial parameters of the giveaway.", "winners": "Up to 100 user identifiers of the winners of the giveaway.", @@ -4556,9 +5407,9 @@ "MessageMediaInvoice": { "desc": "Invoice", "params": { - "currency": "Three-letter ISO 4217 currency code", + "currency": "Three-letter ISO 4217 currency code, or XTR for Telegram Stars.", "description": "Product description, 1-255 characters", - "extended_media": "Extended media", + "extended_media": "Deprecated", "flags": "Flags, see TL conditional fields", "photo": "URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for.", "receipt_msg_id": "Message ID of receipt: if set, clients should change the text of the first keyboardButtonBuy button always attached to the message to a localized version of the word Receipt", @@ -4569,6 +5420,13 @@ "total_amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies)." } }, + "MessageMediaPaidMedia": { + "desc": "Paid media, see here \u00bb for more info.", + "params": { + "extended_media": "Either the paid-for media, or super low resolution media previews if the media wasn't purchased yet, see here \u00bb for more info.", + "stars_amount": "The price of the media in Telegram Stars." + } + }, "MessageMediaPhoto": { "desc": "Attached photo.", "params": { @@ -4581,6 +5439,8 @@ "MessageMediaPoll": { "desc": "Poll", "params": { + "attached_media": "Optional media attachment displayed alongside the poll", + "flags": "Flags, see TL conditional fields", "poll": "The poll", "results": "The results of the poll" } @@ -4595,6 +5455,14 @@ "via_mention": "If set, indicates that this someone has mentioned us in this story (i.e. by tagging us in the description) or vice versa, we have mentioned the other peer (if the message is outgoing)." } }, + "MessageMediaToDo": { + "desc": "Represents a todo list \u00bb.", + "params": { + "completions": "Completed items.", + "flags": "Flags, see TL conditional fields", + "todo": "The todo list." + } + }, "MessageMediaUnsupported": { "desc": "Current version of the client does not support this media type.", "params": {} @@ -4610,6 +5478,14 @@ "venue_type": "Venue type in the provider's database" } }, + "MessageMediaVideoStream": { + "desc": "{schema}", + "params": { + "call": "", + "flags": "Flags, see TL conditional fields", + "rtmp_stream": "" + } + }, "MessageMediaWebPage": { "desc": "Preview of webpage", "params": { @@ -4669,13 +5545,25 @@ "can_see_list": "Whether messages.getMessageReactionsList can be used to see how each specific peer reacted to the message", "flags": "Flags, see TL conditional fields", "min": "Similar to min objects, used for message reaction \u00bb constructors that are the same for all users so they don't have the reactions sent by the current user (you can use messages.getMessagesReactions to get the full reaction info).", - "reactions_as_tags": "", + "reactions_as_tags": "If set or if there are no reactions, all present and future reactions should be treated as message tags, see here \u00bb for more info.", "recent_reactions": "List of recent peers and their reactions", - "results": "Reactions" + "results": "Reactions", + "top_reactors": "Paid Telegram Star reactions leaderboard \u00bb for this message." + } + }, + "MessageReactor": { + "desc": "Info about a user in the paid Star reactions leaderboard for a message.", + "params": { + "anonymous": "If set, the reactor is anonymous.", + "count": "The number of sent Telegram Stars.", + "flags": "Flags, see TL conditional fields", + "my": "If set, this reactor is the current user.", + "peer_id": "Identifier of the peer that reacted: may be unset for anonymous reactors different from the current user (i.e. if the current user sent an anonymous reaction anonymous will be set but this field will also be set).", + "top": "If set, the reactor is one of the most active reactors; may be unset if the reactor is the current user." } }, "MessageReplies": { - "desc": "Info about the comment section of a channel post, or a simple message thread", + "desc": "Info about the comment section of a channel post, a simple message thread, a forum topic, or a direct messages topic (all features ultimately based on message threads).", "params": { "channel_id": "For channel post comments, contains the ID of the associated discussion supergroup", "comments": "Whether this constructor contains information about the comment section of a channel post, or a simple message thread", @@ -4701,16 +5589,24 @@ "reply_to_msg_id": "ID of message to which this message is replying", "reply_to_peer_id": "For replies sent in channel discussion threads of which the current user is not a member, the discussion group ID", "reply_to_scheduled": "This is a reply to a scheduled message.", - "reply_to_top_id": "ID of the message that started this message thread" + "reply_to_top_id": "ID of the message that started this message thread", + "todo_item_id": "Can be set to reply to the specified item of a todo list \u00bb." } }, "MessageReplyStoryHeader": { "desc": "Represents a reply to a story", "params": { - "peer": "", + "peer": "Sender of the story.", "story_id": "Story ID" } }, + "MessageReportOption": { + "desc": "Report menu option", + "params": { + "option": "Option identifier: if the user selects this option, re-invoke messages.report, passing this option to option", + "text": "Option title" + } + }, "MessageService": { "desc": "Indicates a service message", "params": { @@ -4725,7 +5621,10 @@ "out": "Whether the message is outgoing", "peer_id": "Sender of service message", "post": "Whether it's a channel post", + "reactions": "Reactions \u00bb.", + "reactions_are_possible": "Whether you can react to this message \u00bb.", "reply_to": "Reply (thread) information", + "saved_peer_id": "Will only be set for service messages within a monoforum topic \u00bb: peer will be equal to the ID of the monoforum and the saved_peer_id flag will be set to the ID of a topic.", "silent": "Whether the message is silent", "ttl_period": "Time To Live of the message, once message.date+message.ttl_period === time(), the message will be deleted on the server, and must be deleted locally as well." } @@ -4740,12 +5639,27 @@ } }, "MissingInvitee": { - "desc": "{schema}", + "desc": "Info about why a specific user could not be invited \u00bb.", "params": { "flags": "Flags, see TL conditional fields", - "premium_required_for_pm": "", - "premium_would_allow_invite": "", - "user_id": "" + "premium_required_for_pm": "If set, we could not add the user because of their privacy settings, and additionally, the current account needs to purchase a Telegram Premium subscription to directly share an invite link with the user via a private message.", + "premium_would_allow_invite": "If set, we could not add the user only because the current account needs to purchase a Telegram Premium subscription to complete the operation.", + "user_id": "ID of the user. If neither of the flags below are set, we could not add the user because of their privacy settings, and we can create and directly share an invite link with them using a normal message, instead." + } + }, + "MonoForumDialog": { + "desc": "Represents a monoforum topic \u00bb.", + "params": { + "draft": "A pending message draft.", + "flags": "Flags, see TL conditional fields", + "nopaid_messages_exception": "If set, an admin has exempted this peer from payment to send messages using account.toggleNoPaidMessagesException.", + "peer": "The peer associated to the topic, AKA the topic ID.", + "read_inbox_max_id": "Position up to which all incoming messages are read.", + "read_outbox_max_id": "Position up to which all outgoing messages are read.", + "top_message": "The latest message ID", + "unread_count": "Number of unread messages.", + "unread_mark": "Whether this topic has a manually set (with messages.markDialogUnread) unread mark.", + "unread_reactions_count": "Number of unread reactions." } }, "MyBoost": { @@ -4814,9 +5728,9 @@ "params": {} }, "OutboxReadDate": { - "desc": "{schema}", + "desc": "Exact read date of a private message we sent to another user.", "params": { - "date": "" + "date": "UNIX timestamp with the read date." } }, "Page": { @@ -5110,17 +6024,42 @@ "cells": "Table cells" } }, - "PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow": { - "desc": "This key derivation algorithm defines that SRP 2FA login must be used", - "params": { - "g": "Base (see SRP 2FA login)", - "p": "2048-bit modulus (see SRP 2FA login)", - "salt1": "One of two salts used by the derivation function (see SRP 2FA login)", - "salt2": "One of two salts used by the derivation function (see SRP 2FA login)" - } + "PaidReactionPrivacyAnonymous": { + "desc": "Send paid reactions anonymously.", + "params": {} }, - "PasswordKdfAlgoUnknown": { - "desc": "Unknown KDF (most likely, the client is outdated and does not support the specified KDF algorithm)", + "PaidReactionPrivacyDefault": { + "desc": "Uses the default reaction privacy, set using messages.togglePaidReactionPrivacy.", + "params": {} + }, + "PaidReactionPrivacyPeer": { + "desc": "Send paid reactions as the specified peer, fetched using channels.getSendAs.", + "params": { + "peer": "The peer to send reactions as." + } + }, + "Passkey": { + "desc": "{schema}", + "params": { + "date": "", + "flags": "Flags, see TL conditional fields", + "id": "", + "last_usage_date": "", + "name": "", + "software_emoji_id": "" + } + }, + "PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow": { + "desc": "This key derivation algorithm defines that SRP 2FA login must be used", + "params": { + "g": "Base (see SRP 2FA login)", + "p": "2048-bit modulus (see SRP 2FA login)", + "salt1": "One of two salts used by the derivation function (see SRP 2FA login)", + "salt2": "One of two salts used by the derivation function (see SRP 2FA login)" + } + }, + "PasswordKdfAlgoUnknown": { + "desc": "Unknown KDF (most likely, the client is outdated and does not support the specified KDF algorithm)", "params": {} }, "PaymentCharge": { @@ -5181,6 +6120,19 @@ "flags": "Flags, see TL conditional fields" } }, + "PeerColorCollectible": { + "desc": "{schema}", + "params": { + "accent_color": "", + "background_emoji_id": "", + "collectible_id": "", + "colors": "", + "dark_accent_color": "", + "dark_colors": "", + "flags": "Flags, see TL conditional fields", + "gift_emoji_id": "" + } + }, "PeerLocated": { "desc": "Peer geolocated nearby", "params": { @@ -5218,14 +6170,19 @@ "add_contact": "Whether we can add the user as contact", "autoarchived": "Whether this peer was automatically archived according to privacy settings and can be unarchived", "block_contact": "Whether we can block the user", - "business_bot_can_reply": "", - "business_bot_id": "", - "business_bot_manage_url": "", - "business_bot_paused": "", + "business_bot_can_reply": "This flag is set if both business_bot_id and business_bot_manage_url are set and connected business bots \u00bb can reply to messages in this chat, as specified by the settings during initial configuration.", + "business_bot_id": "Contains the ID of the business bot \u00bb managing this chat, used to display info about the bot in the action bar.", + "business_bot_manage_url": "Contains a deep link \u00bb, used to open a management menu in the business bot. This flag is set if and only if business_bot_id is set.", + "business_bot_paused": "This flag is set if both business_bot_id and business_bot_manage_url are set and all connected business bots \u00bb were paused in this chat using account.toggleConnectedBotPaused \u00bb.", + "charge_paid_message_stars": "All users that must pay us \u00bb to send us private messages will have this flag set only for us, containing the amount of required stars, see here \u00bb for more info on paid messages.", "flags": "Flags, see TL conditional fields", "geo_distance": "Distance in meters between us and this peer", "invite_members": "If set, this is a recently created group chat to which new members can be invited", + "name_change_date": "When was the user's name last changed.", "need_contacts_exception": "Whether a special exception for contacts is needed", + "phone_country": "The country code of the user's phone number.", + "photo_change_date": "When was the user's photo last changed.", + "registration_month": "Used to display the user's registration year and month, the string is in MM.YYYY format, where MM is the registration month (1-12), and YYYY is the registration year.", "report_geo": "Whether we can report a geogroup as irrelevant for this location", "report_spam": "Whether we can still report the user for spam", "request_chat_broadcast": "This flag is set if request_chat_title and request_chat_date fields are set and the join request \u00bb is related to a channel (otherwise if only the request fields are set, the join request \u00bb is related to a chat).", @@ -5249,13 +6206,23 @@ "user_id": "User identifier" } }, + "PendingSuggestion": { + "desc": "Represents a custom pending suggestion \u00bb.", + "params": { + "description": "Body of the suggestion.", + "suggestion": "The suggestion ID, can be passed to help.dismissSuggestion.", + "title": "Title of the suggestion.", + "url": "URL to open when the user clicks on the suggestion." + } + }, "PhoneCall": { "desc": "Phone call", "params": { "access_hash": "Access hash", "admin_id": "User ID of the creator of the call", - "connections": "List of endpoints the user can connect to to exchange call data", - "custom_parameters": "", + "conference_supported": "If set, the other party supports upgrading of the call to a conference call.", + "connections": "List of endpoints the user can connect to exchange call data", + "custom_parameters": "Custom JSON-encoded call parameters to be passed to tgcalls.", "date": "Date of creation of the call", "flags": "Flags, see TL conditional fields", "g_a_or_b": "Parameter for key exchange", @@ -5294,6 +6261,12 @@ "desc": "The phone call was ended normally", "params": {} }, + "PhoneCallDiscardReasonMigrateConferenceCall": { + "desc": "This phone call was migrated to a conference call.", + "params": { + "slug": "Conference link \u00bb slug." + } + }, "PhoneCallDiscardReasonMissed": { "desc": "The phone call was missed", "params": {} @@ -5451,23 +6424,35 @@ "Poll": { "desc": "Poll", "params": { - "answers": "The possible answers, vote using messages.sendVote.", - "close_date": "Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future; can't be used together with close_period.", - "close_period": "Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with close_date.", + "answers": "The possible answers (2-poll_answers_max), vote using messages.sendVote.", + "close_date": "Point in time (Unix timestamp) when the poll will be automatically closed. Up to poll_close_period_max \u00bb seconds in the future; can't be used together with close_period.", + "close_period": "Amount of time in seconds the poll will be active after creation, up to poll_close_period_max \u00bb seconds. Can't be used together with close_date.", "closed": "Whether the poll is closed and doesn't accept any more answers", + "countries_iso2": "If set, only users from the specified ISO 3166-1 alpha-2 country codes may vote, see country-restricted polls \u00bb; a user may vote only if the phone_country_iso2 \u00bb configuration parameter is contained in this list.", + "creator": "Whether the current user created this poll", "flags": "Flags, see TL conditional fields", + "hash": "Hash for use with messages.getPollResults", + "hide_results_until_close": "Whether vote results are hidden from all participants until the poll is closed", "id": "ID of the poll", "multiple_choice": "Whether multiple options can be chosen as answer", + "open_answers": "Whether users can add new answer options after the poll is created, see open-answer polls \u00bb", "public_voters": "Whether cast votes are publicly visible to all users (non-anonymous poll)", - "question": "The question of the poll", - "quiz": "Whether this is a quiz (with wrong and correct answers, results shown in the return type)" + "question": "The question of the poll (only Premium users can use custom emoji entities here).", + "quiz": "Whether this is a quiz (with wrong and correct answers, results shown in the return type). When creating a poll, the correct answers are specified in inputMediaPoll.correct_answers.", + "revoting_disabled": "If set, users cannot change their vote after casting it", + "shuffle_answers": "Whether answer options are displayed in a randomized order to each user", + "subscribers_only": "Whether only subscribers can vote: a user may vote only if they are currently a member of the channel/supergroup and joined it at least 24 hours before the poll was posted, see subscriber-only polls \u00bb." } }, "PollAnswer": { "desc": "A possible answer of a poll", "params": { + "added_by": "The peer who added this answer; only set for answers dynamically added to an open-answer poll, see polls \u00bb", + "date": "When this answer was added; only set for answers dynamically added to an open-answer poll", + "flags": "Flags, see TL conditional fields", + "media": "Optional media attachment displayed alongside the answer", "option": "The param that has to be passed to messages.sendVote.", - "text": "Textual representation of the answer" + "text": "Textual representation of the answer (only Premium users can use custom emoji entities here)." } }, "PollAnswerVoters": { @@ -5477,18 +6462,22 @@ "correct": "For quizzes, whether the option we have chosen is correct", "flags": "Flags, see TL conditional fields", "option": "The param that has to be passed to messages.sendVote.", + "recent_voters": "Peers of the most recent voters for this option", "voters": "How many users voted for this option" } }, "PollResults": { "desc": "Results of poll", "params": { + "can_view_stats": "Whether the current user can view detailed poll statistics \u00bb", "flags": "Flags, see TL conditional fields", + "has_unread_votes": "Whether there are unread votes in this non-anonymous poll, see polls \u00bb", "min": "Similar to min objects, used for poll constructors that are the same for all users so they don't have the option chosen by the current user (you can use messages.getPollResults to get the full poll results).", "recent_voters": "IDs of the last users that recently voted in the poll", "results": "Poll results", "solution": "Explanation of quiz solution", "solution_entities": "Message entities for styled text in quiz solution", + "solution_media": "Optional media attachment shown alongside the quiz solution explanation", "total_voters": "Total number of people that voted in the poll" } }, @@ -5540,17 +6529,6 @@ "users": "Number of users which will be able to activate the gift codes." } }, - "PremiumGiftOption": { - "desc": "Telegram Premium gift option", - "params": { - "amount": "Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", - "bot_url": "An invoice deep link \u00bb to an invoice for in-app payment, using the official Premium bot; may be empty if direct payment isn't available.", - "currency": "Three-letter ISO 4217 currency code", - "flags": "Flags, see TL conditional fields", - "months": "Duration of gifted Telegram Premium subscription", - "store_product": "An identifier for the App Store/Play Store product associated with the Premium gift." - } - }, "PremiumSubscriptionOption": { "desc": "Describes a Telegram Premium subscription option", "params": { @@ -5574,6 +6552,16 @@ "quantity": "Number of given away Telegram Premium subscriptions." } }, + "PrepaidStarsGiveaway": { + "desc": "Contains info about a prepaid Telegram Star giveaway \u00bb.", + "params": { + "boosts": "Number of boosts the channel will gain by launching the giveaway.", + "date": "When was the giveaway paid for", + "id": "Prepaid giveaway ID.", + "quantity": "Number of giveaway winners", + "stars": "Number of given away Telegram Stars \u00bb" + } + }, "PrivacyKeyAbout": { "desc": "Whether people can see your bio", "params": {} @@ -5583,7 +6571,7 @@ "params": {} }, "PrivacyKeyBirthday": { - "desc": "{schema}", + "desc": "Whether the user can see our birthday.", "params": {} }, "PrivacyKeyChatInvite": { @@ -5594,6 +6582,10 @@ "desc": "Whether messages forwarded from the user will be anonymously forwarded", "params": {} }, + "PrivacyKeyNoPaidMessages": { + "desc": "Who can send you messages without paying, if paid messages \u00bb are enabled.", + "params": {} + }, "PrivacyKeyPhoneCall": { "desc": "Whether the user accepts phone calls", "params": {} @@ -5610,8 +6602,16 @@ "desc": "Whether the profile picture of the user is visible", "params": {} }, + "PrivacyKeySavedMusic": { + "desc": "{schema}", + "params": {} + }, + "PrivacyKeyStarGiftsAutoSave": { + "desc": "Whether received gifts will be automatically displayed on our profile", + "params": {} + }, "PrivacyKeyStatusTimestamp": { - "desc": "Whether we can see the last online timestamp of this user", + "desc": "Whether we can see the last online timestamp of this user.", "params": {} }, "PrivacyKeyVoiceMessages": { @@ -5622,10 +6622,14 @@ "desc": "Allow all users", "params": {} }, + "PrivacyValueAllowBots": { + "desc": "Allow bots and mini apps", + "params": {} + }, "PrivacyValueAllowChatParticipants": { "desc": "Allow all participants of certain chats", "params": { - "chats": "Allowed chats" + "chats": "Allowed chat IDs (either a chat or a supergroup ID, verbatim the way it is received in the constructor (i.e. unlike with bot API IDs, here group and supergroup IDs should be treated in the same way))." } }, "PrivacyValueAllowCloseFriends": { @@ -5637,7 +6641,7 @@ "params": {} }, "PrivacyValueAllowPremium": { - "desc": "{schema}", + "desc": "Allow only users with a Premium subscription \u00bb, currently only usable for inputPrivacyKeyChatInvite.", "params": {} }, "PrivacyValueAllowUsers": { @@ -5650,10 +6654,14 @@ "desc": "Disallow all users", "params": {} }, + "PrivacyValueDisallowBots": { + "desc": "Disallow bots and mini apps", + "params": {} + }, "PrivacyValueDisallowChatParticipants": { "desc": "Disallow only participants of certain chats", "params": { - "chats": "Disallowed chats" + "chats": "Disallowed chats IDs (either a chat or a supergroup ID, verbatim the way it is received in the constructor (i.e. unlike with bot API IDs, here group and supergroup IDs should be treated in the same way))." } }, "PrivacyValueDisallowContacts": { @@ -5666,6 +6674,38 @@ "users": "Disallowed users" } }, + "ProfileTabFiles": { + "desc": "Represents the shared files tab of a profile.", + "params": {} + }, + "ProfileTabGifs": { + "desc": "Represents the gifs tab of a profile page.", + "params": {} + }, + "ProfileTabGifts": { + "desc": "Represents the gifts tab of a profile page.", + "params": {} + }, + "ProfileTabLinks": { + "desc": "Represents the shared links tab of a profile page.", + "params": {} + }, + "ProfileTabMedia": { + "desc": "Represents the media tab of a profile page.", + "params": {} + }, + "ProfileTabMusic": { + "desc": "Represents the music tab of a profile page.", + "params": {} + }, + "ProfileTabPosts": { + "desc": "Represents the stories tab of a profile page.", + "params": {} + }, + "ProfileTabVoice": { + "desc": "Represents the voice messages tab of a profile page.", + "params": {} + }, "PublicForwardMessage": { "desc": "Contains info about a forward of a story as a message.", "params": { @@ -5680,12 +6720,12 @@ } }, "QuickReply": { - "desc": "{schema}", + "desc": "A quick reply shortcut.", "params": { - "count": "", - "shortcut": "", - "shortcut_id": "", - "top_message": "" + "count": "Total number of messages in the shortcut.", + "shortcut": "Shortcut name.", + "shortcut_id": "Unique shortcut ID.", + "top_message": "ID of the last message in the shortcut." } }, "ReactionCount": { @@ -5714,21 +6754,25 @@ "params": {} }, "ReactionNotificationsFromAll": { - "desc": "{schema}", + "desc": "Receive notifications about reactions made by any user.", "params": {} }, "ReactionNotificationsFromContacts": { - "desc": "{schema}", + "desc": "Receive notifications about reactions made only by our contacts.", + "params": {} + }, + "ReactionPaid": { + "desc": "Represents a paid Telegram Star reaction \u00bb.", "params": {} }, "ReactionsNotifySettings": { - "desc": "{schema}", + "desc": "Reaction notification settings, see here \u00bb for more info.", "params": { "flags": "Flags, see TL conditional fields", - "messages_notify_from": "", - "show_previews": "", - "sound": "", - "stories_notify_from": "" + "messages_notify_from": "Message reaction notification settings, if not set completely disables notifications/updates about message reactions.", + "show_previews": "If false, push notifications \u00bb about message/story reactions will only be of type REACT_HIDDEN/REACT_STORY_HIDDEN, without any information about the reacted-to story or the reaction itself.", + "sound": "Notification sound for reactions \u00bb", + "stories_notify_from": "Story reaction notification settings, if not set completely disables notifications/updates about reactions to stories." } }, "ReadParticipantDate": { @@ -5779,6 +6823,14 @@ "user_id": "User ID" } }, + "RecentStory": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "live": "", + "max_id": "" + } + }, "ReplyInlineMarkup": { "desc": "Bot or inline keyboard", "params": { @@ -5813,6 +6865,25 @@ "single_use": "Requests clients to hide the keyboard as soon as it's been used. The keyboard will still be available, but clients will automatically display the usual letter-keyboard in the chat \u2013 the user can press a special button in the input field to see the custom keyboard again." } }, + "ReportResultAddComment": { + "desc": "The user should enter an additional comment for the moderators, and then messages.report must be re-invoked, passing the comment to messages.report.message.", + "params": { + "flags": "Flags, see TL conditional fields", + "option": "The messages.report method must be re-invoked, passing this option to option", + "optional": "Whether this step can be skipped by the user, passing an empty message to messages.report, or if a non-empty message is mandatory." + } + }, + "ReportResultChooseOption": { + "desc": "The user must choose one of the following options, and then messages.report must be re-invoked, passing the option's option identifier to messages.report.option.", + "params": { + "options": "Available options, rendered as menu entries.", + "title": "Title of the option popup" + } + }, + "ReportResultReported": { + "desc": "The report was sent successfully, no further actions are required.", + "params": {} + }, "RequestPeerTypeBroadcast": { "desc": "Choose a channel", "params": { @@ -5844,40 +6915,54 @@ } }, "RequestedPeerChannel": { - "desc": "{schema}", + "desc": "Info about a channel/supergroup, shared by a user with the currently logged in bot using messages.sendBotRequestedPeer.", "params": { - "channel_id": "", + "channel_id": "Channel/supergroup ID.", "flags": "Flags, see TL conditional fields", - "photo": "", - "title": "", - "username": "" + "photo": "Channel/supergroup photo.", + "title": "Channel/supergroup title.", + "username": "Channel/supergroup username." } }, "RequestedPeerChat": { - "desc": "{schema}", + "desc": "Info about a chat, shared by a user with the currently logged in bot using messages.sendBotRequestedPeer.", "params": { - "chat_id": "", + "chat_id": "Chat ID.", "flags": "Flags, see TL conditional fields", - "photo": "", - "title": "" + "photo": "Chat photo.", + "title": "Chat title." } }, "RequestedPeerUser": { - "desc": "{schema}", + "desc": "Info about a user, shared by a user with the currently logged in bot using messages.sendBotRequestedPeer.", "params": { - "first_name": "", + "first_name": "First name.", "flags": "Flags, see TL conditional fields", - "last_name": "", - "photo": "", - "user_id": "", - "username": "" + "last_name": "Last name.", + "photo": "Profile photo.", + "user_id": "User ID.", + "username": "Username." + } + }, + "RequirementToContactEmpty": { + "desc": "This user can be freely contacted.", + "params": {} + }, + "RequirementToContactPaidMessages": { + "desc": "This user requires us to pay the specified amount of Telegram Stars to send them a message, see here \u00bb for the full flow.", + "params": { + "stars_amount": "The required amount of Telegram Stars." } }, + "RequirementToContactPremium": { + "desc": "This user requires us to buy a Premium subscription in order to contact them.", + "params": {} + }, "RestrictionReason": { "desc": "Restriction reason.", "params": { "platform": "Platform identifier (ios, android, wp, all, etc.), can be concatenated with a dash as separator (android-ios, ios-wp, etc)", - "reason": "Restriction reason (porno, terms, etc.)", + "reason": "Restriction reason (porno, terms, etc.). Ignore this restriction reason if it is contained in the ignore_restriction_reasons \u00bb client configuration parameter.", "text": "Error message to be shown to the user" } }, @@ -5900,12 +6985,52 @@ } }, "SavedReactionTag": { - "desc": "{schema}", + "desc": "Info about a saved message reaction tag \u00bb.", + "params": { + "count": "Number of messages tagged with this tag.", + "flags": "Flags, see TL conditional fields", + "reaction": "Reaction associated to the tag.", + "title": "Custom tag name assigned by the user (max 12 UTF-8 chars)." + } + }, + "SavedStarGift": { + "desc": "Represents a gift owned by a peer.", + "params": { + "can_craft_at": "", + "can_export_at": "If set, indicates that the current gift can't be exported to the TON blockchain \u00bb yet: the owner will be able to export it at the specified unixtime.", + "can_resell_at": "If set, indicates that the current gift can't be resold \u00bb yet: the owner will be able to put it up for sale at the specified unixtime.", + "can_transfer_at": "If set, indicates that the current gift can't be transferred \u00bb yet: the owner will be able to transfer it at the specified unixtime.", + "can_upgrade": "Only set for non-collectible gifts, if they can be upgraded to a collectible gift \u00bb.", + "collection_id": "IDs of the collections \u00bb that this gift is a part of.", + "convert_stars": "For non-collectible gifts, the receiver of this gift may convert it to this many Telegram Stars, instead of displaying it on their profile page.", + "date": "Reception date of the gift.", + "drop_original_details_stars": "", + "flags": "Flags, see TL conditional fields", + "from_id": "Sender of the gift (unset for anonymous gifts).", + "gift": "The collectible gift.", + "gift_num": "", + "message": "Message attached to the gift.", + "msg_id": "For gifts received by users, ID to use in inputSavedStarGiftUser constructors.", + "name_hidden": "If set, the gift sender in from_id and the message are set only for the receiver of the gift.", + "pinned_to_top": "Whether this gift is pinned on top of the user's profile page.", + "prepaid_upgrade_hash": "Hash to prepay for a gift upgrade separately \u00bb.", + "refunded": "This gift was upgraded to a collectible gift \u00bb and then re-downgraded to a regular gift because a request to refund the payment related to the upgrade was made, and the money was returned.", + "saved_id": "For gifts received by channels, ID to use in inputSavedStarGiftChat constructors.", + "transfer_stars": "If set, indicates that the gift can be transferred \u00bb to another user by paying the specified amount of stars.", + "unsaved": "If set, the gift is not pinned on the user's profile.", + "upgrade_separate": "If set, someone already separately pre-paid for the upgrade of this gift.", + "upgrade_stars": "Only for pre-paid non-collectible gifts, the number of Telegram Stars the sender has already paid to convert the gift into a collectible gift \u00bb (this is different from the meaning of the flag in messageActionStarGift, where it signals the upgrade price for not yet upgraded gifts)." + } + }, + "SearchPostsFlood": { + "desc": "Indicates if the specified global post search \u00bb requires payment.", "params": { - "count": "", "flags": "Flags, see TL conditional fields", - "reaction": "", - "title": "" + "query_is_free": "The specified query is free (and it will not use up free search slots).", + "remains": "Remaining number of free search slots.", + "stars_amount": "The number of Telegram Stars to pay for each non-free search.", + "total_daily": "Total number of daily free search slots.", + "wait_till": "If there are no more search slots, specifies the unixtime when more search slots will be available." } }, "SearchResultPosition": { @@ -6216,6 +7341,13 @@ "desc": "User is recording a video.", "params": {} }, + "SendMessageTextDraftAction": { + "desc": "{schema}", + "params": { + "random_id": "", + "text": "" + } + }, "SendMessageTypingAction": { "desc": "User is typing.", "params": {} @@ -6258,18 +7390,12 @@ "title": "Title" } }, - "SimpleWebViewResultUrl": { - "desc": "Contains the webview URL with appropriate theme parameters added", - "params": { - "url": "URL" - } - }, "SmsJob": { - "desc": "{schema}", + "desc": "Info about an SMS job.", "params": { - "job_id": "", - "phone_number": "", - "text": "" + "job_id": "Job ID", + "phone_number": "Destination phone number", + "text": "Text" } }, "SpeakingInGroupCallAction": { @@ -6280,171 +7406,588 @@ "desc": "A sponsored message.", "params": { "additional_info": "If set, contains additional information about the sponsored message to be shown along with the message.", - "button_text": "Text of the sponsored message button.", - "can_report": "", - "color": "", - "entities": "Message entities for styled text", + "button_text": "Label of the sponsored message button.", + "can_report": "Whether this message can be reported as specified here \u00bb.", + "color": "If set, the sponsored message should use the message accent color \u00bb specified in color.", + "entities": "Message entities for styled text in message.", "flags": "Flags, see TL conditional fields", + "max_display_duration": "For sponsored messages to show on channel videos \u00bb, autohide the ad after after the specified amount of seconds.", + "media": "If set, contains some media.", "message": "Sponsored message", - "photo": "", + "min_display_duration": "For sponsored messages to show on channel videos \u00bb, allow the user to hide the ad only after the specified amount of seconds.", + "photo": "If set, contains a custom profile photo bubble that should be displayed for the sponsored message, like for messages sent in groups.", "random_id": "Message ID", "recommended": "Whether the message needs to be labeled as \"recommended\" instead of \"sponsored\"", "sponsor_info": "If set, contains additional information about the sponsor to be shown along with the message.", - "title": "", - "url": "" + "title": "Contains the title of the sponsored message.", + "url": "Contains the URL to open when the user clicks on the sponsored message." } }, "SponsoredMessageReportOption": { + "desc": "A report option for a sponsored message \u00bb.", + "params": { + "option": "Option identifier to pass to messages.reportSponsoredMessage.", + "text": "Localized description of the option." + } + }, + "SponsoredPeer": { + "desc": "A sponsored peer.", + "params": { + "additional_info": "If set, contains additional information about the sponsored message to be shown along with the peer.", + "flags": "Flags, see TL conditional fields", + "peer": "The sponsored peer.", + "random_id": "ID of the sponsored peer, to be passed to messages.viewSponsoredMessage, messages.clickSponsoredMessage or messages.reportSponsoredMessage (the same methods used for sponsored messages »).", + "sponsor_info": "If set, contains additional information about the sponsor to be shown along with the peer." + } + }, + "StarGift": { + "desc": "Represents a star gift, see here \u00bb for more info.", + "params": { + "auction": "", + "auction_slug": "", + "auction_start_date": "", + "availability_remains": "For limited-supply gifts: the remaining number of gifts that may be bought.", + "availability_resale": "The total number of (upgraded to collectibles) gifts of this type currently on resale", + "availability_total": "For limited-supply gifts: the total number of gifts that was available in the initial supply.", + "background": "", + "birthday": "Whether this is a birthday-themed gift", + "convert_stars": "The receiver of this gift may convert it to this many Telegram Stars, instead of displaying it on their profile page.convert_stars will be equal to stars only if the gift was bought using recently bought Telegram Stars, otherwise it will be less than stars.", + "first_sale_date": "For sold out gifts only: when was the gift first bought.", + "flags": "Flags, see TL conditional fields", + "gifts_per_round": "", + "id": "Identifier of the gift", + "last_sale_date": "For sold out gifts only: when was the gift last bought.", + "limited": "Whether this is a limited-supply gift.", + "limited_per_user": "If set, the maximum number of gifts of this type that can be owned by a single user is limited and specified in per_user_total, and the remaining slots for the current user in per_user_remains.", + "locked_until_date": "If set, the specified gift possibly cannot be sent until the specified date, see here \u00bb for the full flow.", + "peer_color_available": "", + "per_user_remains": "Remaining number of gifts of this type that can be owned by the current user.", + "per_user_total": "Maximum number of gifts of this type that can be owned by any user.", + "released_by": "This gift was released by the specified peer.", + "require_premium": "This gift can only be bought by users with a Premium subscription.", + "resell_min_stars": "The minimum price in Stars for gifts of this type currently on resale.", + "sold_out": "Whether this gift sold out and cannot be bought anymore.", + "stars": "Price of the gift in Telegram Stars.", + "sticker": "Sticker that represents the gift.", + "title": "Title of the gift", + "upgrade_stars": "The number of Telegram Stars the user can pay to convert the gift into a collectible gift \u00bb.", + "upgrade_variants": "" + } + }, + "StarGiftActiveAuctionState": { "desc": "{schema}", "params": { - "option": "", - "text": "" + "gift": "", + "state": "", + "user_state": "" } }, - "StarsTopupOption": { - "desc": "{schema}", + "StarGiftAttributeBackdrop": { + "desc": "The backdrop of a collectible gift \u00bb.", "params": { - "amount": "", - "currency": "", - "extended": "", + "backdrop_id": "Unique ID of the backdrop", + "center_color": "Color of the center of the backdrop in RGB24 format.", + "edge_color": "Color of the edges of the backdrop in RGB24 format.", + "name": "Name of the backdrop", + "pattern_color": "Color of the starGiftAttributePattern applied on the backdrop in RGB24 format.", + "rarity": "", + "text_color": "Color of the text on the backdrop in RGB24 format." + } + }, + "StarGiftAttributeCounter": { + "desc": "Indicates the total number of gifts that have the specified attribute.", + "params": { + "attribute": "The attribute (just the ID, without the attribute itself).", + "count": "Total number of gifts with this attribute." + } + }, + "StarGiftAttributeIdBackdrop": { + "desc": "The ID of a backdrop of a collectible gift \u00bb.", + "params": { + "backdrop_id": "Unique ID of the backdrop." + } + }, + "StarGiftAttributeIdModel": { + "desc": "The ID of a model of a collectible gift \u00bb.", + "params": { + "document_id": "The sticker representing the upgraded gift" + } + }, + "StarGiftAttributeIdPattern": { + "desc": "The ID of a pattern of a collectible gift \u00bb.", + "params": { + "document_id": "The sticker representing the symbol" + } + }, + "StarGiftAttributeModel": { + "desc": "The model of a collectible gift \u00bb.", + "params": { + "crafted": "", + "document": "The sticker representing the upgraded gift", "flags": "Flags, see TL conditional fields", - "stars": "", - "store_product": "" + "name": "Name of the model", + "rarity": "" } }, - "StarsTransaction": { - "desc": "{schema}", + "StarGiftAttributeOriginalDetails": { + "desc": "Info about the sender, receiver and message attached to the original gift \u00bb, before it was upgraded to a collectible gift \u00bb.", "params": { - "date": "", - "description": "", + "date": "When was the gift sent.", "flags": "Flags, see TL conditional fields", - "id": "", - "peer": "", - "photo": "", - "refund": "", - "stars": "", - "title": "" + "message": "Original message attached to the gift, if present.", + "recipient_id": "Original receiver of the gift.", + "sender_id": "Original sender of the gift, absent if the gift was private." } }, - "StarsTransactionPeer": { + "StarGiftAttributePattern": { + "desc": "A sticker applied on the backdrop of a collectible gift \u00bb using a repeating pattern.", + "params": { + "document": "The symbol", + "name": "Name of the symbol", + "rarity": "" + } + }, + "StarGiftAttributeRarity": { "desc": "{schema}", "params": { - "peer": "" + "permille": "" } }, - "StarsTransactionPeerAppStore": { + "StarGiftAttributeRarityEpic": { "desc": "{schema}", "params": {} }, - "StarsTransactionPeerFragment": { + "StarGiftAttributeRarityLegendary": { "desc": "{schema}", "params": {} }, - "StarsTransactionPeerPlayMarket": { + "StarGiftAttributeRarityRare": { "desc": "{schema}", "params": {} }, - "StarsTransactionPeerPremiumBot": { + "StarGiftAttributeRarityUncommon": { "desc": "{schema}", "params": {} }, - "StarsTransactionPeerUnsupported": { + "StarGiftAuctionAcquiredGift": { "desc": "{schema}", - "params": {} + "params": { + "bid_amount": "", + "date": "", + "flags": "Flags, see TL conditional fields", + "gift_num": "", + "message": "", + "name_hidden": "", + "peer": "", + "pos": "", + "round": "" + } }, - "StatsAbsValueAndPrev": { - "desc": "Statistics value couple; initial and final value for period of time currently in consideration", + "StarGiftAuctionRound": { + "desc": "{schema}", "params": { - "current": "Current value", - "previous": "Previous value" + "duration": "", + "num": "" } }, - "StatsDateRangeDays": { - "desc": "Channel statistics date range", + "StarGiftAuctionRoundExtendable": { + "desc": "{schema}", "params": { - "max_date": "Final date", - "min_date": "Initial date" + "duration": "", + "extend_top": "", + "extend_window": "", + "num": "" } }, - "StatsGraph": { - "desc": "Channel statistics graph", + "StarGiftAuctionState": { + "desc": "{schema}", + "params": { + "bid_levels": "", + "current_round": "", + "end_date": "", + "gifts_left": "", + "last_gift_num": "", + "min_bid_amount": "", + "next_round_at": "", + "rounds": "", + "start_date": "", + "top_bidders": "", + "total_rounds": "", + "version": "" + } + }, + "StarGiftAuctionStateFinished": { + "desc": "{schema}", "params": { + "average_price": "", + "end_date": "", "flags": "Flags, see TL conditional fields", - "json": "Statistics data", - "zoom_token": "Zoom token" + "fragment_listed_count": "", + "fragment_listed_url": "", + "listed_count": "", + "start_date": "" } }, - "StatsGraphAsync": { - "desc": "This channel statistics graph must be generated asynchronously using stats.loadAsyncGraph to reduce server load", + "StarGiftAuctionStateNotModified": { + "desc": "{schema}", + "params": {} + }, + "StarGiftAuctionUserState": { + "desc": "{schema}", "params": { - "token": "Token to use for fetching the async graph" + "acquired_count": "", + "bid_amount": "", + "bid_date": "", + "bid_peer": "", + "flags": "Flags, see TL conditional fields", + "min_bid_amount": "", + "returned": "" } }, - "StatsGraphError": { - "desc": "An error occurred while generating the statistics graph", + "StarGiftBackground": { + "desc": "{schema}", "params": { - "error": "The error" + "center_color": "", + "edge_color": "", + "text_color": "" } }, - "StatsGroupTopAdmin": { - "desc": "Information about an active admin in a supergroup", + "StarGiftCollection": { + "desc": "Represents a star gift collection \u00bb.", + "params": { + "collection_id": "The ID of the collection.", + "flags": "Flags, see TL conditional fields", + "gifts_count": "Number of gifts in the collection.", + "hash": "Field to use instead of collection_id when generating the hash to pass to payments.getStarGiftCollections.", + "icon": "Optional icon for the collection, taken from the first gift in the collection.", + "title": "Title of the collection." + } + }, + "StarGiftUnique": { + "desc": "Represents a collectible star gift, see here \u00bb for more info.", + "params": { + "attributes": "Collectible attributes", + "availability_issued": "Total number of gifts of the same type that were upgraded to a collectible gift.", + "availability_total": "Total number of gifts of the same type that can be upgraded or were already upgraded to a collectible gift.", + "burned": "", + "craft_chance_permille": "", + "crafted": "", + "flags": "Flags, see TL conditional fields", + "gift_address": "For NFTs on the TON blockchain, contains the address of the NFT (append it to the ton_blockchain_explorer_url client configuration value \u00bb to obtain a link with information about the address).", + "gift_id": "Unique ID of the gift.", + "host_id": "", + "id": "Identifier of the collectible gift.", + "num": "Unique identifier of this collectible gift among all (already upgraded) collectible gifts of the same type.", + "offer_min_stars": "", + "owner_address": "For NFTs on the TON blockchain, contains the address of the owner (append it to the ton_blockchain_explorer_url client configuration value \u00bb to obtain a link with information about the address).", + "owner_id": "The owner of the gift.", + "owner_name": "The name of the owner if neither owner_id nor owner_address are set.", + "peer_color": "", + "released_by": "This gift was released by the specified peer.", + "require_premium": "This gift can only be bought by users with a Premium subscription.", + "resale_ton_only": "Whether the gift can be bought only using Toncoins.", + "resell_amount": "Resale price of the gift.", + "slug": "Slug that can be used to create a collectible gift deep link \u00bb, or elsewhere in the API where a collectible slug is accepted.", + "theme_available": "A chat theme associated to this gift is available, see here \u00bb for more info on how to use it.", + "theme_peer": "The current chat where the associated chat theme is installed, if any (gift-based themes can only be installed in one chat at a time).", + "title": "Collectible title.", + "value_amount": "Price of the gift.", + "value_currency": "Currency for the gift's price.", + "value_usd_amount": "" + } + }, + "StarGiftUpgradePrice": { + "desc": "{schema}", "params": { - "banned": "Number of banned users for statistics period in consideration", - "deleted": "Number of deleted messages for statistics period in consideration", - "kicked": "Number of kicked users for statistics period in consideration", - "user_id": "User ID" + "date": "", + "upgrade_stars": "" } }, - "StatsGroupTopInviter": { - "desc": "Information about an active supergroup inviter", + "StarRefProgram": { + "desc": "Indo about an affiliate program offered by a bot", "params": { - "invitations": "Number of invitations for statistics period in consideration", - "user_id": "User ID" + "bot_id": "ID of the bot that offers the program", + "commission_permille": "An affiliate gets a commission of starRefProgram.commission_permille\u2030 Telegram Stars for every mini app transaction made by users they refer", + "daily_revenue_per_user": "The amount of daily revenue per user in Telegram Stars of the bot that created the affiliate program. To obtain the approximated revenue per referred user, multiply this value by commission_permille and divide by 1000.", + "duration_months": "An affiliate gets a commission for every mini app transaction made by users they refer, for duration_months months after a referral link is imported, starting the bot for the first time", + "end_date": "Point in time (Unix timestamp) when the affiliate program will be closed (optional, if not set the affiliate program isn't scheduled to be closed)", + "flags": "Flags, see TL conditional fields" } }, - "StatsGroupTopPoster": { - "desc": "Information about an active user in a supergroup", + "StarsAmount": { + "desc": "Describes a real (i.e. possibly decimal) amount of Telegram Stars.", "params": { - "avg_chars": "Average number of characters per message", - "messages": "Number of messages for statistics period in consideration", - "user_id": "User ID" + "amount": "The integer amount of Telegram Stars.", + "nanos": "The decimal amount of Telegram Stars, expressed as nanostars (i.e. 1 nanostar is equal to 1/1'000'000'000th (one billionth) of a Telegram Star). This field may also be negative (the allowed range is -999999999 to 999999999)." } }, - "StatsPercentValue": { - "desc": "Channel statistics percentage.\nCompute the percentage simply by doing part * total / 100", + "StarsGiftOption": { + "desc": "Telegram Stars gift option.", "params": { - "part": "Partial value", - "total": "Total value" + "amount": "Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code", + "extended": "If set, the option must only be shown in the full list of topup options.", + "flags": "Flags, see TL conditional fields", + "stars": "Amount of Telegram stars.", + "store_product": "Identifier of the store product associated with the option, official apps only." } }, - "StatsURL": { - "desc": "URL with chat statistics", + "StarsGiveawayOption": { + "desc": "Contains info about a Telegram Star giveaway option.", "params": { - "url": "Chat statistics" + "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code", + "default": "If set, this option must be pre-selected by default in the option list.", + "extended": "If set, this option must only be shown in the full list of giveaway options (i.e. they must be added to the list only when the user clicks on the expand button).", + "flags": "Flags, see TL conditional fields", + "stars": "The number of Telegram Stars that will be distributed among winners", + "store_product": "Identifier of the store product associated with the option, official apps only.", + "winners": "Allowed options for the number of giveaway winners.", + "yearly_boosts": "Number of times the chat will be boosted for one year if the inputStorePaymentStarsGiveaway.boost_peer flag is populated" } }, - "StickerKeyword": { - "desc": "Keywords for a certain sticker", + "StarsGiveawayWinnersOption": { + "desc": "Allowed options for the number of giveaway winners.", "params": { - "document_id": "Sticker ID", - "keyword": "Keywords" + "default": "If set, this option must be pre-selected by default in the option list.", + "flags": "Flags, see TL conditional fields", + "per_user_stars": "The number of Telegram Stars each winner will receive.", + "users": "The number of users that will be randomly chosen as winners." } }, - "StickerPack": { - "desc": "A stickerpack is a group of stickers associated to the same emoji.\nIt is not a sticker pack the way it is usually intended, you may be looking for a StickerSet.", + "StarsRating": { + "desc": "Represents the profile's star rating, see here \u00bb for more info.", "params": { - "documents": "Stickers", - "emoticon": "Emoji" + "current_level_stars": "The numerical value of the rating required for the current level.", + "flags": "Flags, see TL conditional fields", + "level": "The current level, may be negative.", + "next_level_stars": "The numerical value of the rating required for the next level.", + "stars": "Numerical value of the current rating." } }, - "StickerSet": { - "desc": "Represents a stickerset (stickerpack)", + "StarsRevenueStatus": { + "desc": "Describes Telegram Star revenue balances \u00bb.", "params": { - "access_hash": "Access hash of stickerset", - "archived": "Whether this stickerset was archived (due to too many saved stickers in the current account)", - "channel_emoji_status": "If set, this custom emoji stickerset can be used in channel emoji statuses.", - "count": "Number of stickers in pack", - "creator": "", + "available_balance": "Amount of withdrawable Telegram Stars.", + "current_balance": "Amount of not-yet-withdrawn Telegram Stars.", + "flags": "Flags, see TL conditional fields", + "next_withdrawal_at": "Unixtime indicating when will withdrawal be available to the user. If not set, withdrawal can be started now.", + "overall_revenue": "Total amount of earned Telegram Stars.", + "withdrawal_enabled": "If set, the user may withdraw up to available_balance stars." + } + }, + "StarsSubscription": { + "desc": "Represents a Telegram Star subscription \u00bb.", + "params": { + "bot_canceled": "Set if this bot subscription was cancelled by the bot", + "can_refulfill": "Whether we left the associated private channel, but we can still rejoin it using payments.fulfillStarsSubscription because the current subscription period hasn't expired yet.", + "canceled": "Whether this subscription was cancelled.", + "chat_invite_hash": "Invitation link, used to renew the subscription after cancellation or expiration.", + "flags": "Flags, see TL conditional fields", + "id": "Subscription ID.", + "invoice_slug": "For bot subscriptions, the identifier of the subscription invoice", + "missing_balance": "Whether this subscription has expired because there are not enough stars on the user's balance to extend it.", + "peer": "Identifier of the associated private chat.", + "photo": "For bot subscriptions, the photo from the subscription invoice", + "pricing": "Pricing of the subscription in Telegram Stars.", + "title": "For bot subscriptions, the title of the subscription invoice", + "until_date": "Expiration date of the current subscription period." + } + }, + "StarsSubscriptionPricing": { + "desc": "Pricing of a Telegram Star subscription \u00bb.", + "params": { + "amount": "Price of the subscription in Telegram Stars.", + "period": "The user should pay amount stars every period seconds to gain and maintain access to the channel. Currently the only allowed subscription period is 30*24*60*60, i.e. the user will be debited amount stars every month." + } + }, + "StarsTonAmount": { + "desc": "Describes an amount of toncoin in nanotons (i.e. 1/1_000_000_000 of a toncoin).", + "params": { + "amount": "The amount in nanotons." + } + }, + "StarsTopupOption": { + "desc": "Telegram Stars topup option.", + "params": { + "amount": "Price of the product in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code", + "extended": "If set, the option must only be shown in the full list of topup options.", + "flags": "Flags, see TL conditional fields", + "stars": "Amount of Telegram stars.", + "store_product": "Identifier of the store product associated with the option, official apps only." + } + }, + "StarsTransaction": { + "desc": "Represents a Telegram Stars or TON transaction \u00bb.", + "params": { + "ads_proceeds_from_date": "Indicates that this is payment for ad revenue from the specified unixtime (always set together with ads_proceeds_to_date).", + "ads_proceeds_to_date": "Indicates that this is payment for ad revenue to the specified unixtime.", + "amount": "Amount of Telegram Stars or TON.", + "bot_payload": "Bot specified invoice payload (i.e. the payload passed to inputMediaInvoice when creating the invoice).", + "business_transfer": "This transaction transfers stars from the balance of a user account connected to a business bot, to the balance of the business bot, see here \u00bb for more info.", + "date": "Date of the transaction (unixtime).", + "description": "For transactions with bots, description of the bought product.", + "extended_media": "The purchased paid media \u00bb.", + "failed": "This transaction has failed.", + "flags": "Flags, see TL conditional fields", + "floodskip_number": "This transaction is payment for paid bot broadcasts. Paid broadcasts are only allowed if the allow_paid_floodskip parameter of messages.sendMessage and other message sending methods is set while trying to broadcast more than 30 messages per second to bot users. The integer value returned by this flag indicates the number of billed API calls.", + "gift": "This transaction was a gift from the user in peer.peer.", + "giveaway_post_id": "ID of the message containing the messageMediaGiveaway, for incoming star giveaway prizes.", + "id": "Transaction ID.", + "msg_id": "For paid media transactions \u00bb, message ID of the paid media posted to peer.peer (can point to a deleted message; either way, extended_media will always contain the bought media).", + "offer": "", + "paid_messages": "This transaction is related to the reception or transmission of a paid message \u00bb.", + "peer": "Source of the incoming transaction, or its recipient for outgoing transactions.", + "pending": "The transaction is currently pending.", + "phonegroup_message": "", + "photo": "For transactions with bots, photo of the bought product.", + "posts_search": "Represents payment for a paid global post search \u00bb.", + "premium_gift_months": "This transaction indicates the payment for a gifted Telegram Premium subscription \u00bb.", + "reaction": "This transaction is a paid reaction \u00bb.", + "refund": "Whether this transaction is a refund.", + "stargift": "This transaction indicates a purchase or a sale (conversion back to Stars) of a gift \u00bb.", + "stargift_auction_bid": "", + "stargift_drop_original_details": "", + "stargift_prepaid_upgrade": "Represents payment for a separate prepaid upgrade of a gift.", + "stargift_resale": "This transaction is related to the resale of a collectible gift \u00bb.", + "stargift_upgrade": "This transaction pays for the upgrade of a gift to a collectible gift \u00bb.", + "starref_amount": "For transactions made by referred users, the amount of Telegram Stars received by the affiliate, can be negative for refunds.", + "starref_commission_permille": "This transaction is the receival (or refund) of an affiliate commission (i.e. this is the transaction received by the peer that created the referral link, flag 17 is for transactions made by users that imported the referral link).", + "starref_peer": "For transactions made by referred users, the peer that received the affiliate commission.", + "subscription_period": "The number of seconds between consecutive Telegram Star debiting for Telegram Star subscriptions \u00bb.", + "title": "For transactions with bots, title of the bought product.", + "transaction_date": "If neither pending nor failed are set, the transaction was completed successfully, and this field will contain the point in time (Unix timestamp) when the withdrawal was completed successfully.", + "transaction_url": "If neither pending nor failed are set, the transaction was completed successfully, and this field will contain a URL where the withdrawal transaction can be viewed." + } + }, + "StarsTransactionPeer": { + "desc": "Describes a Telegram Star transaction with another peer.", + "params": { + "peer": "The peer." + } + }, + "StarsTransactionPeerAPI": { + "desc": "Describes a Telegram Star transaction used to pay for paid API usage, such as paid bot broadcasts.", + "params": {} + }, + "StarsTransactionPeerAds": { + "desc": "Describes a Telegram Star transaction used to pay for Telegram ads as specified here \u00bb.", + "params": {} + }, + "StarsTransactionPeerAppStore": { + "desc": "Describes a Telegram Star transaction with the App Store, used when purchasing Telegram Stars through the App Store.", + "params": {} + }, + "StarsTransactionPeerFragment": { + "desc": "Describes a Telegram Star transaction with Fragment, used when purchasing Telegram Stars through Fragment.", + "params": {} + }, + "StarsTransactionPeerPlayMarket": { + "desc": "Describes a Telegram Star transaction with the Play Store, used when purchasing Telegram Stars through the Play Store.", + "params": {} + }, + "StarsTransactionPeerPremiumBot": { + "desc": "Describes a Telegram Star transaction made using @PremiumBot (i.e. using the inputInvoiceStars flow described here \u00bb).", + "params": {} + }, + "StarsTransactionPeerUnsupported": { + "desc": "Describes a Telegram Star transaction that cannot be described using the current layer.", + "params": {} + }, + "StatsAbsValueAndPrev": { + "desc": "Statistics value couple; initial and final value for period of time currently in consideration", + "params": { + "current": "Current value", + "previous": "Previous value" + } + }, + "StatsDateRangeDays": { + "desc": "Channel statistics date range", + "params": { + "max_date": "Final date", + "min_date": "Initial date" + } + }, + "StatsGraph": { + "desc": "Channel statistics graph", + "params": { + "flags": "Flags, see TL conditional fields", + "json": "Statistics data", + "zoom_token": "Zoom token" + } + }, + "StatsGraphAsync": { + "desc": "This channel statistics graph must be generated asynchronously using stats.loadAsyncGraph to reduce server load", + "params": { + "token": "Token to use for fetching the async graph" + } + }, + "StatsGraphError": { + "desc": "An error occurred while generating the statistics graph", + "params": { + "error": "The error" + } + }, + "StatsGroupTopAdmin": { + "desc": "Information about an active admin in a supergroup", + "params": { + "banned": "Number of banned users for statistics period in consideration", + "deleted": "Number of deleted messages for statistics period in consideration", + "kicked": "Number of kicked users for statistics period in consideration", + "user_id": "User ID" + } + }, + "StatsGroupTopInviter": { + "desc": "Information about an active supergroup inviter", + "params": { + "invitations": "Number of invitations for statistics period in consideration", + "user_id": "User ID" + } + }, + "StatsGroupTopPoster": { + "desc": "Information about an active user in a supergroup", + "params": { + "avg_chars": "Average number of characters per message", + "messages": "Number of messages for statistics period in consideration", + "user_id": "User ID" + } + }, + "StatsPercentValue": { + "desc": "Channel statistics percentage.\nCompute the percentage simply by doing part * total / 100", + "params": { + "part": "Partial value", + "total": "Total value" + } + }, + "StatsURL": { + "desc": "URL with chat statistics", + "params": { + "url": "Chat statistics" + } + }, + "StickerKeyword": { + "desc": "Keywords for a certain sticker", + "params": { + "document_id": "Sticker ID", + "keyword": "Keywords" + } + }, + "StickerPack": { + "desc": "A stickerpack is a group of stickers associated to the same emoji.\nIt is not a sticker pack the way it is usually intended, you may be looking for a StickerSet.", + "params": { + "documents": "Stickers", + "emoticon": "Emoji" + } + }, + "StickerSet": { + "desc": "Represents a stickerset (stickerpack)", + "params": { + "access_hash": "Access hash of stickerset", + "archived": "Whether this stickerset was archived (due to too many saved stickers in the current account)", + "channel_emoji_status": "If set, this custom emoji stickerset can be used in channel/supergroup emoji statuses.", + "count": "Number of stickers in pack", + "creator": "Whether we created this stickerset", "emojis": "This is a custom emoji stickerset", "flags": "Flags, see TL conditional fields", "hash": "Hash", @@ -6498,6 +8041,16 @@ "flags": "Flags, see TL conditional fields" } }, + "StoryAlbum": { + "desc": "Represents a story album \u00bb.", + "params": { + "album_id": "ID of the album.", + "flags": "Flags, see TL conditional fields", + "icon_photo": "Photo from the first story of the album, if it's a photo.", + "icon_video": "Video from the first story of the album, if it's a video.", + "title": "Name of the album." + } + }, "StoryFwdHeader": { "desc": "Contains info about the original poster of a reposted story.", "params": { @@ -6511,6 +8064,7 @@ "StoryItem": { "desc": "Represents a story.", "params": { + "albums": "Albums this story is part of.", "caption": "Story caption.", "close_friends": "Whether this story can only be viewed by our close friends, see here \u00bb for more info", "contacts": "Whether this story can only be viewed by our contacts", @@ -6519,7 +8073,7 @@ "entities": "Message entities for styled text", "expire_date": "When does the story expire.", "flags": "Flags, see TL conditional fields", - "from_id": "", + "from_id": "Sender of the story.", "fwd_from": "For reposted stories \u00bb, contains info about the original story.", "id": "ID of the story.", "media": "Story media.", @@ -6548,7 +8102,8 @@ "date": "When was the story posted.", "expire_date": "When does the story expire.", "flags": "Flags, see TL conditional fields", - "id": "Story ID" + "id": "Story ID", + "live": "" } }, "StoryReaction": { @@ -6614,6 +8169,16 @@ "views_count": "View counter of the story" } }, + "SuggestedPost": { + "desc": "Contains info about a suggested post \u00bb.", + "params": { + "accepted": "Whether the suggested post was accepted.", + "flags": "Flags, see TL conditional fields", + "price": "Price of the suggested post.", + "rejected": "Whether the suggested post was rejected.", + "schedule_date": "Scheduling date." + } + }, "TextAnchor": { "desc": "Text linking to another section of the page", "params": { @@ -6752,11 +8317,36 @@ } }, "Timezone": { - "desc": "{schema}", + "desc": "Timezone information.", "params": { - "id": "", - "name": "", - "utc_offset": "" + "id": "Unique timezone ID.", + "name": "Human-readable and localized timezone name.", + "utc_offset": "UTC offset in seconds, which may be displayed in hh:mm format by the client together with the human-readable name (i.e. $name UTC -01:00)." + } + }, + "TodoCompletion": { + "desc": "A completed todo list \u00bb item.", + "params": { + "completed_by": "ID of the user that completed the item.", + "date": "When was the item completed.", + "id": "The ID of the completed item." + } + }, + "TodoItem": { + "desc": "An item of a todo list \u00bb.", + "params": { + "id": "ID of the item, a positive (non-zero) integer unique within the current list.", + "title": "Text of the item, maximum length equal to todo_item_length_max \u00bb." + } + }, + "TodoList": { + "desc": "Represents a todo list \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "list": "Items of the list.", + "others_can_append": "If set, users different from the creator of the list can append items to the list.", + "others_can_complete": "If set, users different from the creator of the list can complete items in the list.", + "title": "Title of the todo list, maximum length equal to todo_title_length_max \u00bb." } }, "TopPeer": { @@ -6766,6 +8356,10 @@ "rating": "Rating as computed in top peer rating \u00bb" } }, + "TopPeerCategoryBotsApp": { + "desc": "Most frequently used Main Mini Bot Apps.", + "params": {} + }, "TopPeerCategoryBotsInline": { "desc": "Most used inline bots", "params": {} @@ -6815,10 +8409,10 @@ "params": {} }, "UpdateBotBusinessConnect": { - "desc": "{schema}", + "desc": "Connecting or disconnecting a business bot or changing the connection settings will emit an updateBotBusinessConnect update to the bot, with the new settings and a connection_id that will be used by the bot to handle updates from and send messages as the user.", "params": { - "connection": "", - "qts": "" + "connection": "Business connection settings", + "qts": "New qts value, see updates \u00bb for more info." } }, "UpdateBotCallbackQuery": { @@ -6835,7 +8429,7 @@ } }, "UpdateBotChatBoost": { - "desc": "A channel boost has changed (bots only)", + "desc": "A channel/supergroup boost has changed (bots only)", "params": { "boost": "New boost information", "peer": "Channel", @@ -6862,22 +8456,22 @@ } }, "UpdateBotDeleteBusinessMessage": { - "desc": "{schema}", + "desc": "A message was deleted in a connected business chat \u00bb.", "params": { - "connection_id": "", - "messages": "", - "peer": "", - "qts": "" + "connection_id": "Business connection ID.", + "messages": "IDs of the messages that were deleted.", + "peer": "Peer where the messages were deleted.", + "qts": "New qts value, see updates \u00bb for more info." } }, "UpdateBotEditBusinessMessage": { - "desc": "{schema}", + "desc": "A message was edited in a connected business chat \u00bb.", "params": { - "connection_id": "", + "connection_id": "Business connection ID", "flags": "Flags, see TL conditional fields", - "message": "", - "qts": "", - "reply_to_message": "" + "message": "New message.", + "qts": "New qts value, see updates \u00bb for more info.", + "reply_to_message": "The message that message is replying to." } }, "UpdateBotInlineQuery": { @@ -6933,19 +8527,19 @@ } }, "UpdateBotNewBusinessMessage": { - "desc": "{schema}", + "desc": "A message was received via a connected business chat \u00bb.", "params": { - "connection_id": "", + "connection_id": "Connection ID.", "flags": "Flags, see TL conditional fields", - "message": "", - "qts": "", - "reply_to_message": "" + "message": "New message.", + "qts": "New qts value, see updates \u00bb for more info.", + "reply_to_message": "The message that message is replying to." } }, "UpdateBotPrecheckoutQuery": { "desc": "This object contains information about an incoming pre-checkout query.", "params": { - "currency": "Three-letter ISO 4217 currency code", + "currency": "Three-letter ISO 4217 currency code, or XTR for Telegram Stars.", "flags": "Flags, see TL conditional fields", "info": "Order info provided by the user", "payload": "Bot specified invoice payload", @@ -6955,6 +8549,14 @@ "user_id": "User who sent the query" } }, + "UpdateBotPurchasedPaidMedia": { + "desc": "Bots only: a user has purchased a paid media.", + "params": { + "payload": "Payload passed by the bot in inputMediaPaidMedia.payload", + "qts": "New qts value, see updates \u00bb for more info.", + "user_id": "The user that bought the media" + } + }, "UpdateBotShippingQuery": { "desc": "This object contains information about an incoming shipping query.", "params": { @@ -6987,15 +8589,21 @@ "timeout": "Query timeout" } }, - "UpdateBroadcastRevenueTransactions": { - "desc": "{schema}", + "UpdateBusinessBotCallbackQuery": { + "desc": "A callback button sent via a business connection was pressed, and the button data was sent to the bot that created the button.", "params": { - "balances": "", - "peer": "" + "chat_instance": "Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent. Useful for high scores in games.", + "connection_id": "Business connection ID", + "data": "Callback data", + "flags": "Flags, see TL conditional fields", + "message": "Message that contains the keyboard (also contains info about the chat where the message was sent).", + "query_id": "Query ID", + "reply_to_message": "The message that message is replying to.", + "user_id": "ID of the user that pressed the button" } }, "UpdateChannel": { - "desc": "A new channel or supergroup is available, or info about an existing channel has changed and must be refeteched.", + "desc": "Channel/supergroup (channel and/or channelFull) information was updated.", "params": { "channel_id": "Channel ID" } @@ -7038,29 +8646,13 @@ "via_chatlist": "Whether the participant joined using a chat folder deep link \u00bb." } }, - "UpdateChannelPinnedTopic": { - "desc": "A forum topic \u00bb was pinned or unpinned.", - "params": { - "channel_id": "The forum ID", - "flags": "Flags, see TL conditional fields", - "pinned": "Whether the topic was pinned or unpinned", - "topic_id": "The topic ID" - } - }, - "UpdateChannelPinnedTopics": { - "desc": "The pinned topics of a forum have changed.", - "params": { - "channel_id": "Forum ID.", - "flags": "Flags, see TL conditional fields", - "order": "Ordered list containing the IDs of all pinned topics." - } - }, "UpdateChannelReadMessagesContents": { - "desc": "The specified channel/supergroup messages were read", + "desc": "The specified channel/supergroup messages were read (emitted specifically for messages like voice messages or video, only once the media is watched and marked as read using channels.readMessageContents)", "params": { "channel_id": "Channel/supergroup ID", "flags": "Flags, see TL conditional fields", "messages": "IDs of messages that were read", + "saved_peer_id": "If set, the messages were read within the specified monoforum topic \u00bb.", "top_msg_id": "Forum topic ID." } }, @@ -7099,7 +8691,7 @@ } }, "UpdateChat": { - "desc": "A new chat is available", + "desc": "Chat (chat and/or chatFull) information was updated.", "params": { "chat_id": "Chat ID" } @@ -7153,6 +8745,15 @@ "version": "Used in basic groups to reorder updates and make sure that all of them was received." } }, + "UpdateChatParticipantRank": { + "desc": "{schema}", + "params": { + "chat_id": "", + "rank": "", + "user_id": "", + "version": "" + } + }, "UpdateChatParticipants": { "desc": "Composition of chat participants changed.", "params": { @@ -7168,7 +8769,7 @@ } }, "UpdateConfig": { - "desc": "The server-side configuration has changed; the client should re-fetch the config using help.getConfig", + "desc": "The server-side configuration has changed; the client should re-fetch the config using help.getConfig and help.getAppConfig.", "params": {} }, "UpdateContactsReset": { @@ -7190,6 +8791,13 @@ "pts_count": "Number of events that were generated" } }, + "UpdateDeleteGroupCallMessages": { + "desc": "{schema}", + "params": { + "call": "", + "messages": "" + } + }, "UpdateDeleteMessages": { "desc": "Messages were deleted.", "params": { @@ -7199,23 +8807,25 @@ } }, "UpdateDeleteQuickReply": { - "desc": "{schema}", + "desc": "A quick reply shortcut \u00bb was deleted. This will not emit updateDeleteQuickReplyMessages updates, even if all the messages in the shortcut are also deleted by this update.", "params": { - "shortcut_id": "" + "shortcut_id": "ID of the quick reply shortcut that was deleted." } }, "UpdateDeleteQuickReplyMessages": { - "desc": "{schema}", + "desc": "One or more messages in a quick reply shortcut \u00bb were deleted.", "params": { - "messages": "", - "shortcut_id": "" + "messages": "IDs of the deleted messages.", + "shortcut_id": "Quick reply shortcut ID." } }, "UpdateDeleteScheduledMessages": { - "desc": "Some scheduled messages were deleted from the schedule queue of a chat", + "desc": "Some scheduled messages were deleted (or sent) from the schedule queue of a chat", "params": { + "flags": "Flags, see TL conditional fields", "messages": "Deleted scheduled messages", - "peer": "Peer" + "peer": "Peer", + "sent_messages": "If set, this update indicates that some scheduled messages were sent (not simply deleted from the schedule queue). In this case, the messages field will contain the scheduled message IDs for the sent messages (initially returned in updateNewScheduledMessage), and sent_messages will contain the real message IDs for the sent messages." } }, "UpdateDialogFilter": { @@ -7250,6 +8860,7 @@ "params": { "flags": "Flags, see TL conditional fields", "peer": "The dialog", + "saved_peer_id": "If set, the mark is related to the specified monoforum topic ID \u00bb.", "unread": "Was the chat marked or unmarked as read" } }, @@ -7259,6 +8870,7 @@ "draft": "The draft", "flags": "Flags, see TL conditional fields", "peer": "The peer to which the draft is associated", + "saved_peer_id": "If set, the draft is related to the specified monoforum topic ID \u00bb.", "top_msg_id": "ID of the forum topic to which the draft is associated" } }, @@ -7278,6 +8890,12 @@ "pts_count": "PTS count" } }, + "UpdateEmojiGameInfo": { + "desc": "{schema}", + "params": { + "info": "" + } + }, "UpdateEncryptedChatTyping": { "desc": "Interlocutor is typing a message in an encrypted chat. Update period is 6 second. If upon this time there is no repeated update, it shall be considered that the interlocutor stopped typing.", "params": { @@ -7322,7 +8940,18 @@ "desc": "A new groupcall was started", "params": { "call": "Info about the group call or livestream", - "chat_id": "The channel/supergroup where this group call or livestream takes place" + "flags": "Flags, see TL conditional fields", + "live_story": "", + "peer": "" + } + }, + "UpdateGroupCallChainBlocks": { + "desc": "Contains updates to the blockchain of a conference call, see here \u00bb for more info.", + "params": { + "blocks": "Blocks.", + "call": "The conference call.", + "next_offset": "Offset of the next block.", + "sub_chain_id": "Subchain ID." } }, "UpdateGroupCallConnection": { @@ -7333,6 +8962,21 @@ "presentation": "Are these parameters related to the screen capture session currently in progress?" } }, + "UpdateGroupCallEncryptedMessage": { + "desc": "{schema}", + "params": { + "call": "", + "encrypted_message": "", + "from_id": "" + } + }, + "UpdateGroupCallMessage": { + "desc": "{schema}", + "params": { + "call": "", + "message": "" + } + }, "UpdateGroupCallParticipants": { "desc": "The participant list of a certain group call has changed", "params": { @@ -7370,11 +9014,11 @@ "params": {} }, "UpdateMessageExtendedMedia": { - "desc": "Extended media update", + "desc": "You bought a paid media \u00bb: this update contains the revealed media.", "params": { - "extended_media": "Extended media", - "msg_id": "Message ID", - "peer": "Peer" + "extended_media": "Revealed media, contains only messageExtendedMedia constructors.", + "msg_id": "ID of the message containing the paid media", + "peer": "Peer where the paid media was posted" } }, "UpdateMessageID": { @@ -7409,9 +9053,19 @@ "msg_id": "Message ID", "peer": "Peer", "reactions": "Reactions", + "saved_peer_id": "If set, the reactions are in the specified monoforum topic \u00bb.", "top_msg_id": "Forum topic ID" } }, + "UpdateMonoForumNoPaidException": { + "desc": "An admin has (un)exempted this monoforum topic \u00bb from payment to send messages using account.toggleNoPaidMessagesException.", + "params": { + "channel_id": "The monoforum ID.", + "exception": "If set, an admin has exempted this peer, otherwise the peer was unexempted.", + "flags": "Flags, see TL conditional fields", + "saved_peer_id": "The peer/topic ID." + } + }, "UpdateMoveStickerSetToTop": { "desc": "A stickerset was just moved to top, see here for more info \u00bb", "params": { @@ -7427,7 +9081,7 @@ "date": "Authorization date", "device": "Name of device, for example Android", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "location": "Location, for example USA, NY (IP=1.2.3.4)", "unconfirmed": "Whether the session is unconfirmed, see here \u00bb for more info." } @@ -7456,9 +9110,9 @@ } }, "UpdateNewQuickReply": { - "desc": "{schema}", + "desc": "A new quick reply shortcut \u00bb was created.", "params": { - "quick_reply": "" + "quick_reply": "Quick reply shortcut." } }, "UpdateNewScheduledMessage": { @@ -7474,11 +9128,11 @@ } }, "UpdateNewStoryReaction": { - "desc": "{schema}", + "desc": "Represents a new reaction to a story.", "params": { - "peer": "", - "reaction": "", - "story_id": "" + "peer": "The peer where the story was posted.", + "reaction": "The reaction.", + "story_id": "Story ID." } }, "UpdateNotifySettings": { @@ -7488,6 +9142,12 @@ "peer": "Notification source" } }, + "UpdatePaidReactionPrivacy": { + "desc": "Contains the current default paid reaction privacy, see here \u00bb for more info.", + "params": { + "private": "Paid reaction privacy settings." + } + }, "UpdatePeerBlocked": { "desc": "We blocked a peer, see here \u00bb for more info on blocklists.", "params": { @@ -7567,6 +9227,23 @@ "order": "New order of pinned dialogs" } }, + "UpdatePinnedForumTopic": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "peer": "", + "pinned": "", + "topic_id": "" + } + }, + "UpdatePinnedForumTopics": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "order": "", + "peer": "" + } + }, "UpdatePinnedMessages": { "desc": "Some messages were pinned in a chat", "params": { @@ -7597,15 +9274,15 @@ "params": {} }, "UpdateQuickReplies": { - "desc": "{schema}", + "desc": "Info about or the order of quick reply shortcuts \u00bb was changed.", "params": { - "quick_replies": "" + "quick_replies": "New quick reply shortcut order and information." } }, "UpdateQuickReplyMessage": { - "desc": "{schema}", + "desc": "A new message was added to a quick reply shortcut \u00bb.", "params": { - "message": "" + "message": "The message that was added (the message.quick_reply_shortcut_id field will contain the shortcut ID)." } }, "UpdateReadChannelDiscussionInbox": { @@ -7662,7 +9339,8 @@ "peer": "Peer", "pts": "Event count after generation", "pts_count": "Number of events that were generated", - "still_unread_count": "Number of messages that are still unread" + "still_unread_count": "Number of messages that are still unread", + "top_msg_id": "" } }, "UpdateReadHistoryOutbox": { @@ -7675,7 +9353,7 @@ } }, "UpdateReadMessagesContents": { - "desc": "Contents of messages in the common message box were read", + "desc": "Contents of messages in the common message box were read (emitted specifically for messages like voice messages or video, only once the media is watched and marked as read using messages.readMessageContents).", "params": { "date": "When was the last message in messages marked as read.", "flags": "Flags, see TL conditional fields", @@ -7684,6 +9362,22 @@ "pts_count": "Number of events that were generated" } }, + "UpdateReadMonoForumInbox": { + "desc": "Incoming messages in a monoforum topic were read", + "params": { + "channel_id": "ID of the monoforum.", + "read_max_id": "Position up to which all incoming messages are read.", + "saved_peer_id": "Topic ID." + } + }, + "UpdateReadMonoForumOutbox": { + "desc": "Outgoing messages in a monoforum were read.", + "params": { + "channel_id": "ID of the monoforum.", + "read_max_id": "Position up to which all outgoing messages are read.", + "saved_peer_id": "Topic ID." + } + }, "UpdateReadStories": { "desc": "Stories of a specific peer were marked as read.", "params": { @@ -7716,13 +9410,19 @@ "params": {} }, "UpdateSavedReactionTags": { - "desc": "{schema}", + "desc": "The list of reaction tag \u00bb names assigned by the user has changed and should be refetched using messages.getSavedReactionTags \u00bb.", "params": {} }, "UpdateSavedRingtones": { "desc": "The list of saved notification sounds has changed, use account.getSavedRingtones to fetch the new list.", "params": {} }, + "UpdateSentPhoneCode": { + "desc": "A paid login SMS code was successfully sent.", + "params": { + "sent_code": "Info about the sent code." + } + }, "UpdateSentStoryReaction": { "desc": "Indicates we reacted to a story \u00bb.", "params": { @@ -7809,15 +9509,40 @@ } }, "UpdateSmsJob": { + "desc": "A new SMS job was received", + "params": { + "job_id": "SMS job ID" + } + }, + "UpdateStarGiftAuctionState": { "desc": "{schema}", "params": { - "job_id": "" + "gift_id": "", + "state": "" } }, - "UpdateStarsBalance": { + "UpdateStarGiftAuctionUserState": { "desc": "{schema}", "params": { - "balance": "" + "gift_id": "", + "user_state": "" + } + }, + "UpdateStarGiftCraftFail": { + "desc": "{schema}", + "params": {} + }, + "UpdateStarsBalance": { + "desc": "The current account's Telegram Stars balance \u00bb has changed.", + "params": { + "balance": "New balance." + } + }, + "UpdateStarsRevenueStatus": { + "desc": "The Telegram Star balance of a channel/bot we own has changed \u00bb.", + "params": { + "peer": "Channel/bot", + "status": "New Telegram Star balance." } }, "UpdateStickerSets": { @@ -7875,7 +9600,7 @@ } }, "UpdateUser": { - "desc": "User information was updated, it must be refetched using users.getFullUser.", + "desc": "User (user and/or userFull) information was updated.", "params": { "user_id": "User ID" } @@ -7914,6 +9639,8 @@ "desc": "The user is preparing a message; typing, recording, uploading, etc. This update is valid for 6 seconds. If no further updates of this kind are received after 6 seconds, it should be considered that the user stopped doing whatever they were doing", "params": { "action": "Action type", + "flags": "Flags, see TL conditional fields", + "top_msg_id": "", "user_id": "User id" } }, @@ -7959,6 +9686,7 @@ "UrlAuthResultAccepted": { "desc": "Details about an accepted authorization request, for more info click here \u00bb", "params": { + "flags": "Flags, see TL conditional fields", "url": "The URL name of the website on which the user has logged in." } }, @@ -7970,56 +9698,70 @@ "desc": "Details about the authorization request, for more info click here \u00bb", "params": { "bot": "Username of a bot, which will be used for user authorization. If not specified, the current bot's username will be assumed. The url's domain must be the same as the domain linked with the bot. See Linking your domain to the bot for more details.", + "browser": "", "domain": "The domain name of the website on which the user will log in.", "flags": "Flags, see TL conditional fields", - "request_write_access": "Whether the bot would like to send messages to the user" + "ip": "", + "match_codes": "", + "match_codes_first": "", + "platform": "", + "region": "", + "request_phone_number": "", + "request_write_access": "Whether the bot would like to send messages to the user", + "user_id_hint": "" } }, "User": { - "desc": "Indicates info about a certain user", + "desc": "Indicates info about a certain user.", "params": { - "access_hash": "Access hash of the user", - "apply_min_photo": "If set, the profile picture for this user should be refetched", - "attach_menu_enabled": "Whether we installed the attachment menu web app offered by this bot", - "bot": "Is this user a bot?", + "access_hash": "Access hash of the user, see here \u00bb for more info. If this flag is set, when updating the local peer database, generate a virtual flag called min_access_hash, which is: - Set to true if min is set AND -- The phone flag is not set OR -- The phone flag is set and the associated phone number string is non-empty - Set to false otherwise. Then, apply both access_hash and min_access_hash to the local database if: - min_access_hash is false OR - min_access_hash is true AND -- There is no locally cached object for this user OR -- There is no access_hash in the local cache OR -- The cached object's min_access_hash is also true If the final merged object stored to the database has the min_access_hash field set to true, the related access_hash is only suitable to use in inputPeerPhotoFileLocation \u00bb, to directly download the profile pictures of users, everywhere else a inputPeer*FromMessage constructor will have to be generated as specified here \u00bb. Bots can also use min access hashes in some conditions, by passing 0 instead of the min access hash.", + "apply_min_photo": "If set and min is set, the value of photo can be used to update the local database, see the documentation of that flag for more info.", + "attach_menu_enabled": "Whether we installed the attachment menu web app offered by this bot. When updating the local peer database, do not apply changes to this field if the min flag is set.", + "bot": "Is this user a bot? Changes to this flag should invalidate the local userFull cache for this user ID, see here \u00bb for more info.", + "bot_active_users": "Monthly Active Users (MAU) of this bot (may be absent for small bots).", "bot_attach_menu": "Whether this bot offers an attachment menu web app", - "bot_business": "", - "bot_can_edit": "Whether we can edit the profile picture, name, about text and description of this bot because we own it.", + "bot_business": "Whether this bot can be connected to a user as specified here \u00bb.", + "bot_can_edit": "Whether we can edit the profile picture, name, about text and description of this bot because we own it. When updating the local peer database, do not apply changes to this field if the min flag is set. Changes to this flag (if min is not set) should invalidate the local userFull cache for this user ID.", "bot_chat_history": "Can the bot see all messages in groups?", - "bot_info_version": "Version of the bot_info field in userFull, incremented every time it changes", + "bot_forum_can_manage_topics": "", + "bot_forum_view": "", + "bot_has_main_app": "If set, this bot has configured a Main Mini App \u00bb.", + "bot_info_version": "Version of the bot_info field in userFull, incremented every time it changes. Changes to this flag should invalidate the local userFull cache for this user ID, see here \u00bb for more info.", "bot_inline_geo": "Whether the bot can request our geolocation in inline mode", "bot_inline_placeholder": "Inline placeholder for this inline bot", "bot_nochats": "Can the bot be added to groups?", - "close_friend": "Whether we marked this user as a close friend, see here \u00bb for more info", + "bot_verification_icon": "Describes a bot verification icon \u00bb.", + "close_friend": "Whether we marked this user as a close friend, see here \u00bb for more info. When updating the local peer database, do not apply changes to this field if the min flag is set.", "color": "The user's accent color.", - "contact": "Whether this user is a contact", - "contact_require_premium": "", - "deleted": "Whether the account of this user was deleted", + "contact": "Whether this user is a contact When updating the local peer database, do not apply changes to this field if the min flag is set.", + "contact_require_premium": "See here for more info on this flag \u00bb.", + "deleted": "Whether the account of this user was deleted. Changes to this flag should invalidate the local userFull cache for this user ID, see here \u00bb for more info.", "emoji_status": "Emoji status", "fake": "If set, this user was reported by many users as a fake or scam user: be careful when interacting with them.", - "first_name": "First name", + "first_name": "First name. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The min flag of the locally cached user entry is set.", "flags": "Flags, see TL conditional fields", "flags2": "Flags, see TL conditional fields", - "id": "ID of the user", + "id": "ID of the user, see here \u00bb for more info and the available ID range.", "lang_code": "Language code of the user", - "last_name": "Last name", + "last_name": "Last name. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The min flag of the locally cached user entry is set.", "min": "See min", - "mutual_contact": "Whether this user is a mutual contact", - "phone": "Phone number", - "photo": "Profile picture of user", - "premium": "Whether this user is a Telegram Premium user", + "mutual_contact": "Whether this user is a mutual contact. When updating the local peer database, do not apply changes to this field if the min flag is set.", + "phone": "Phone number. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The min flag of the locally cached user entry is set.", + "photo": "Profile picture of user. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The apply_min_photo flag is set OR -- The min flag of the locally cached user entry is set.", + "premium": "Whether this user is a Telegram Premium user Changes to this flag should invalidate the local userFull cache for this user ID, see here \u00bb for more info. Changes to this flag if the self flag is set should also trigger the following calls, to refresh the respective caches: - The help.getConfig cache - The messages.getTopReactions cache if the bot flag is not set", "profile_color": "The user's profile color.", "restricted": "Access to this user must be restricted for the reason specified in restriction_reason", "restriction_reason": "Contains the reason why access to this user must be restricted.", "scam": "This may be a scam user", "self": "Whether this user indicates the currently logged in user", - "status": "Online status of user", - "stories_hidden": "Whether we have hidden \u00bb all active stories of this user.", - "stories_max_id": "ID of the maximum read story.", + "send_paid_messages_stars": "If set, the user has enabled paid messages \u00bb, we might need to pay the specified amount of Stars to send them messages, depending on the configured exceptions: check userFull.send_paid_messages_stars or users.getRequirementsToContact to see if the currently logged in user actually has to pay or not, see here \u00bb for the full flow.", + "status": "Online status of user. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The min flag of the locally cached user entry is set OR -- The locally cached user entry is equal to userStatusEmpty.", + "stories_hidden": "Whether we have hidden \u00bb all active stories of this user. When updating the local peer database, do not apply changes to this field if the min flag is set.", + "stories_max_id": "ID of the maximum read story. When updating the local peer database, do not apply changes to this field if the min flag of the incoming constructor is set.", "stories_unavailable": "No stories from this user are visible.", "support": "Whether this is an official support user", - "username": "Username", - "usernames": "Additional usernames", + "username": "Main active username. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The min flag of the locally cached user entry is set. Changes to this flag should invalidate the local userFull cache for this user ID if the above conditions are respected and the bot_can_edit flag is also set.", + "usernames": "Additional usernames. When updating the local peer database, apply changes to this field only if: - The min flag is not set OR - The min flag is set AND -- The min flag of the locally cached user entry is set. Changes to this flag (if the above conditions are respected) should invalidate the local userFull cache for this user ID.", "verified": "Whether this user is verified" } }, @@ -8033,42 +9775,57 @@ "desc": "Extended user info", "params": { "about": "Bio of the user", - "birthday": "", + "birthday": "Contains info about the user's birthday \u00bb.", "blocked": "Whether you have blocked this user", "blocked_my_stories_from": "Whether we've blocked this user, preventing them from seeing our stories \u00bb.", "bot_broadcast_admin_rights": "A suggested set of administrator rights for the bot, to be shown when adding the bot as admin to a channel, see here for more info on how to handle them \u00bb.", + "bot_can_manage_emoji_status": "If set, this is a bot that can change our emoji status \u00bb", "bot_group_admin_rights": "A suggested set of administrator rights for the bot, to be shown when adding the bot as admin to a group, see here for more info on how to handle them \u00bb.", "bot_info": "For bots, info about the bot (bot commands, etc)", - "business_away_message": "", - "business_greeting_message": "", - "business_intro": "", - "business_location": "", - "business_work_hours": "", + "bot_verification": "Describes a bot verification icon \u00bb.", + "business_away_message": "Telegram Business away message \u00bb.", + "business_greeting_message": "Telegram Business greeting message \u00bb.", + "business_intro": "Specifies a custom Telegram Business profile introduction \u00bb.", + "business_location": "Telegram Business location \u00bb.", + "business_work_hours": "Telegram Business working hours \u00bb.", "can_pin_message": "Whether you can pin messages in the chat with this user, you can do this only for a chat with yourself", + "can_view_revenue": "If set, this user can view ad revenue statistics \u00bb for this bot.", "common_chats_count": "Chats in common with this user", - "contact_require_premium": "", + "contact_require_premium": "If set, we cannot write to this user: subscribe to Telegram Premium to get permission to write to this user. To set this flag for ourselves invoke account.setGlobalPrivacySettings, setting the settings.new_noncontact_peers_require_premium flag, see here \u00bb for more info.", + "disallowed_gifts": "Disallows the reception of specific gift types.", + "display_gifts_button": "If this flag is set for both us and another user (changed through globalPrivacySettings), a gift button should always be displayed in the text field in private chats with the other user: once clicked, the gift UI should be displayed, offering the user options to gift Telegram Premium \u00bb subscriptions or Telegram Gifts \u00bb.", "fallback_photo": "Fallback profile photo, displayed if no photo is present in profile_photo or personal_photo, due to privacy settings.", "flags": "Flags, see TL conditional fields", - "flags2": "", + "flags2": "Flags, see TL conditional fields", "folder_id": "Peer folder ID, for more info click here", "has_scheduled": "Whether scheduled messages are available", "id": "User ID", + "main_tab": "The main tab for the user's profile, see here \u00bb for more info.", + "noforwards_my_enabled": "", + "noforwards_peer_enabled": "", + "note": "", "notify_settings": "Notification settings", - "personal_channel_id": "", - "personal_channel_message": "", + "personal_channel_id": "ID of the associated personal channel \u00bb, that should be shown in the profile page.", + "personal_channel_message": "ID of the latest message of the associated personal channel \u00bb, that should be previewed in the profile page.", "personal_photo": "Personal profile photo, to be shown instead of profile_photo.", "phone_calls_available": "Whether this user can make VoIP calls", "phone_calls_private": "Whether this user's privacy settings allow you to call them", "pinned_msg_id": "Message ID of the last pinned message", - "premium_gifts": "Telegram Premium subscriptions gift options", "private_forward_name": "Anonymized text to be shown instead of the user's name on forwarded messages", "profile_photo": "Profile photo", - "read_dates_private": "", + "read_dates_private": "If set, we cannot fetch the exact read date of messages we send to this user using messages.getOutboxReadDate. The exact read date of messages might still be unavailable for other reasons, see here \u00bb for more info. To set this flag for ourselves invoke account.setGlobalPrivacySettings, setting the settings.hide_read_marks flag.", + "saved_music": "The first song on the music tab of the profile, see here \u00bb for more info on the music profile tab.", + "send_paid_messages_stars": "If set and bigger than 0, this user has enabled paid messages \u00bb and we must pay the specified amount of Stars to send messages to them, see here \u00bb for the full flow. If set and equal to 0, the user requires payment in general but we were exempted from paying for any of the reasons specified in the docs \u00bb.", "settings": "Peer settings", - "sponsored_enabled": "", + "sponsored_enabled": "Whether ads were re-enabled for the current account (only accessible to the currently logged-in user), see here \u00bb for more info.", + "stargifts_count": "Number of gifts the user has chosen to display on their profile", + "starref_program": "This bot has an active referral program \u00bb", + "stars_my_pending_rating": "Our pending star rating, only visible for ourselves.", + "stars_my_pending_rating_date": "When the pending star rating will be applied, only visible for ourselves.", + "stars_rating": "The user's star rating.", "stories": "Active stories \u00bb", "stories_pinned_available": "Whether this user has some pinned stories.", - "theme_emoticon": "Emoji associated with chat theme", + "theme": "The chat theme associated with this user \u00bb.", "translations_disabled": "Whether the real-time chat translation popup should be hidden.", "ttl_period": "Time To Live of all messages in this chat; once a message is this many seconds old, it must be deleted.", "video_calls_available": "Whether the user can receive video calls", @@ -8099,14 +9856,14 @@ "UserStatusLastMonth": { "desc": "Online status: last seen last month", "params": { - "by_me": "", + "by_me": "If set, the exact user status of this user is actually available to us, but to view it we must first purchase a Premium subscription, or allow this user to see our exact last online status. See here \u00bb for more info.", "flags": "Flags, see TL conditional fields" } }, "UserStatusLastWeek": { "desc": "Online status: last seen last week", "params": { - "by_me": "", + "by_me": "If set, the exact user status of this user is actually available to us, but to view it we must first purchase a Premium subscription, or allow this user to see our exact last online status. See here \u00bb for more info.", "flags": "Flags, see TL conditional fields" } }, @@ -8125,7 +9882,7 @@ "UserStatusRecently": { "desc": "Online status: last seen recently", "params": { - "by_me": "", + "by_me": "If set, the exact user status of this user is actually available to us, but to view it we must first purchase a Premium subscription, or allow this user to see our exact last online status. See here \u00bb for more info.", "flags": "Flags, see TL conditional fields" } }, @@ -8253,22 +10010,36 @@ "embed_width": "Width of the embedded preview", "flags": "Flags, see TL conditional fields", "has_large_media": "Whether the size of the media in the preview can be changed.", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "id": "Preview ID", "photo": "Image representing the content", "site_name": "Short name of the site (e.g., Google Docs, App Store)", "title": "Title of the content", - "type": "Type of the web page. Can be: article, photo, audio, video, document, profile, app, or something else", - "url": "URL of previewed webpage" + "type": "Type of the web page. One of the following: - app- article- document- gif- photo- profile- telegram_album- telegram_auction- telegram_background- telegram_bot- telegram_botapp- telegram_call- telegram_channel- telegram_channel_boost- telegram_channel_direct- telegram_channel_request- telegram_chat- telegram_chat_request- telegram_chatlist- telegram_collection- telegram_community- telegram_giftcode- telegram_group_boost- telegram_livestream- telegram_megagroup- telegram_megagroup_request- telegram_message- telegram_nft- telegram_stickerset- telegram_story- telegram_story_album- telegram_theme- telegram_user- telegram_videochat- telegram_voicechat- video", + "url": "URL of previewed webpage", + "video_cover_photo": "Represents a custom video cover." } }, - "WebPageAttributeStickerSet": { + "WebPageAttributeStarGiftAuction": { "desc": "{schema}", "params": { - "emojis": "", + "end_date": "", + "gift": "" + } + }, + "WebPageAttributeStarGiftCollection": { + "desc": "Contains info about a gift collection \u00bb for a webPage preview of a gift collection \u00bb (the webPage will have a type of telegram_collection).", + "params": { + "icons": "Gifts in the collection." + } + }, + "WebPageAttributeStickerSet": { + "desc": "Contains info about a stickerset \u00bb, for a webPage preview of a stickerset deep link \u00bb (the webPage will have a type of telegram_stickerset).", + "params": { + "emojis": "Whether this i s a custom emoji stickerset.", "flags": "Flags, see TL conditional fields", - "stickers": "", - "text_color": "" + "stickers": "A subset of the stickerset in the stickerset.", + "text_color": "Whether the color of this TGS custom emoji stickerset should be changed to the text color when used in messages, the accent color if used as emoji status, white on chat photos, or another appropriate color based on context." } }, "WebPageAttributeStory": { @@ -8288,6 +10059,12 @@ "settings": "Theme settings" } }, + "WebPageAttributeUniqueStarGift": { + "desc": "Contains info about collectible gift \u00bb for a webPage preview of a collectible gift \u00bb (the webPage will have a type of telegram_nft).", + "params": { + "gift": "The starGiftUnique constructor." + } + }, "WebPageEmpty": { "desc": "No preview is available for the webpage", "params": { @@ -8322,7 +10099,10 @@ "WebViewResultUrl": { "desc": "Contains the webview URL with appropriate theme and user info parameters added", "params": { - "query_id": "Webview session ID", + "flags": "Flags, see TL conditional fields", + "fullscreen": "If set, the app must be opened in fullscreen", + "fullsize": "If set, the app must be opened in fullsize mode instead of compact mode.", + "query_id": "Webview session ID (only returned by inline button mini apps, menu button mini apps, attachment menu mini apps).", "url": "Webview URL to open" } }, @@ -8364,18 +10144,33 @@ } }, "account.BusinessChatLinks": { - "desc": "{schema}", + "desc": "Contains info about business chat deep links \u00bb created by the current account.", "params": { - "chats": "", - "links": "", - "users": "" + "chats": "Mentioned chats", + "links": "Links", + "users": "Mentioned users" + } + }, + "account.ChatThemes": { + "desc": "Available chat themes", + "params": { + "chats": "Chats mentioned in the themes field.", + "flags": "Flags, see TL conditional fields", + "hash": "Hash to pass to the method that returned this constructor, to avoid refetching the result if it hasn't changed.", + "next_offset": "Next offset for pagination.", + "themes": "Themes.", + "users": "Users mentioned in the themes field." } }, + "account.ChatThemesNotModified": { + "desc": "The available chat themes were not modified", + "params": {} + }, "account.ConnectedBots": { - "desc": "{schema}", + "desc": "Info about currently connected business bots.", "params": { - "connected_bots": "", - "users": "" + "connected_bots": "Info about the connected bots", + "users": "Bot information" } }, "account.ContentSettings": { @@ -8402,7 +10197,7 @@ "account.EmojiStatuses": { "desc": "A list of emoji statuses", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "statuses": "Emoji statuses" } }, @@ -8410,6 +10205,24 @@ "desc": "The server-side list of emoji statuses hasn't changed", "params": {} }, + "account.PaidMessagesRevenue": { + "desc": "Total number of non-refunded Telegram Stars a user has spent on sending us messages either directly or through a channel, see here \u00bb for more info on paid messages.", + "params": { + "stars_amount": "Amount in Stars." + } + }, + "account.PasskeyRegistrationOptions": { + "desc": "{schema}", + "params": { + "options": "" + } + }, + "account.Passkeys": { + "desc": "{schema}", + "params": { + "passkeys": "" + } + }, "account.Password": { "desc": "Configuration for two-factor authorization", "params": { @@ -8473,16 +10286,26 @@ } }, "account.ResolvedBusinessChatLinks": { - "desc": "{schema}", + "desc": "Contains info about a single resolved business chat deep link \u00bb.", "params": { - "chats": "", + "chats": "Mentioned chats", "entities": "Message entities for styled text", "flags": "Flags, see TL conditional fields", - "message": "", - "peer": "", - "users": "" + "message": "Message to pre-fill in the message input field.", + "peer": "Destination peer", + "users": "Mentioned users" } }, + "account.SavedMusicIds": { + "desc": "List of IDs of songs (document.ids) currently pinned on our profile, see here \u00bb for more info.", + "params": { + "ids": "Full list of document.ids" + } + }, + "account.SavedMusicIdsNotModified": { + "desc": "The list of IDs of songs (document.ids) currently pinned on our profile hasn't changed.", + "params": {} + }, "account.SavedRingtone": { "desc": "The notification sound was already in MP3 format and was saved without any modification", "params": {} @@ -8496,7 +10319,7 @@ "account.SavedRingtones": { "desc": "A list of saved notification sounds", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "ringtones": "Saved notification sounds" } }, @@ -8520,7 +10343,7 @@ "account.Themes": { "desc": "Installed themes", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "themes": "Themes" } }, @@ -8538,7 +10361,7 @@ "account.WallPapers": { "desc": "Installed wallpapers", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "wallpapers": "Wallpapers" } }, @@ -8558,7 +10381,7 @@ "params": { "flags": "Flags, see TL conditional fields", "future_auth_token": "A future auth token", - "otherwise_relogin_days": "Iff setup_password_required is set and the user declines to set a 2-step verification password, they will be able to log into their account via SMS again only after this many days pass.", + "otherwise_relogin_days": "If and only if setup_password_required is set and the user declines to set a 2-step verification password, they will be able to log into their account via SMS again only after this many days pass.", "setup_password_required": "Suggests the user to set up a 2-step verification password to be able to log in again", "tmp_sessions": "Temporary passport sessions", "user": "Info on authorized user" @@ -8625,6 +10448,12 @@ "authorization": "Authorization info" } }, + "auth.PasskeyLoginOptions": { + "desc": "{schema}", + "params": { + "options": "" + } + }, "auth.PasswordRecovery": { "desc": "Recovery info of a 2FA password, only for accounts with a recovery email configured.", "params": { @@ -8641,6 +10470,17 @@ "type": "Phone code type" } }, + "auth.SentCodePaymentRequired": { + "desc": "Official apps may receive this constructor, indicating that due to the high cost of SMS verification codes for the user's country/provider, the user must purchase a Telegram Premium subscription in order to proceed with the login/signup, see here \u00bb for more info.", + "params": { + "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "currency": "Three-letter ISO 4217 currency code.", + "phone_code_hash": "Phone code hash, to be stored and later re-used with auth.signIn", + "store_product": "For official apps, tore identifier of the Telegram Premium subscription.", + "support_email_address": "An email address that can be contacted for more information about this request.", + "support_email_subject": "The mandatory subject for the email." + } + }, "auth.SentCodeSuccess": { "desc": "The user successfully authorized using future auth tokens", "params": { @@ -8677,7 +10517,8 @@ "flags": "Flags, see TL conditional fields", "length": "Length of the code that will be delivered.", "nonce": "On Android, the nonce to be used as described in the auth documentation \u00bb", - "play_integrity_nonce": "", + "play_integrity_nonce": "Play Integrity API nonce", + "play_integrity_project_id": "Google Play Integrity project ID", "push_timeout": "On iOS: if a push notification with the ios_push_secret isn't received within push_timeout seconds, the next_type authentication method must be used, with auth.resendCode.", "receipt": "On iOS, must be compared with the receipt extracted from the received push notification." } @@ -8717,16 +10558,16 @@ } }, "auth.SentCodeTypeSmsPhrase": { - "desc": "{schema}", + "desc": "The code was sent via SMS as a secret phrase starting with the word specified in beginning", "params": { - "beginning": "", + "beginning": "If set, the secret phrase (and the SMS) starts with this word.", "flags": "Flags, see TL conditional fields" } }, "auth.SentCodeTypeSmsWord": { - "desc": "{schema}", + "desc": "The code was sent via SMS as a secret word, starting with the letter specified in beginning", "params": { - "beginning": "", + "beginning": "If set, the secret word in the sent SMS (which may contain multiple words) starts with this letter.", "flags": "Flags, see TL conditional fields" } }, @@ -8738,6 +10579,21 @@ "name": "Bot name" } }, + "bots.PopularAppBots": { + "desc": "Popular Main Mini Apps, to be used in the apps tab of global search \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "next_offset": "Offset for pagination.", + "users": "The bots associated to each Main Mini App, see here \u00bb for more info." + } + }, + "bots.PreviewInfo": { + "desc": "Contains info about Main Mini App previews, see here \u00bb for more info.", + "params": { + "lang_codes": "All available language codes for which preview medias were uploaded (regardless of the language code passed to bots.getPreviewInfo).", + "media": "All preview medias for the language code passed to bots.getPreviewInfo." + } + }, "channels.AdminLogResults": { "desc": "Admin log events", "params": { @@ -8776,18 +10632,18 @@ } }, "channels.SponsoredMessageReportResultAdsHidden": { - "desc": "{schema}", + "desc": "Sponsored messages were hidden for the user in all chats.", "params": {} }, "channels.SponsoredMessageReportResultChooseOption": { - "desc": "{schema}", + "desc": "The user must choose a report option from the localized options available in options, and after selection, messages.reportSponsoredMessage must be invoked again, passing the option's option field to the option param of the method.", "params": { - "options": "", - "title": "" + "options": "Localized list of options.", + "title": "Title of the option selection popup." } }, "channels.SponsoredMessageReportResultReported": { - "desc": "{schema}", + "desc": "The sponsored message was reported successfully.", "params": {} }, "chatlists.ChatlistInvite": { @@ -8798,6 +10654,7 @@ "flags": "Flags, see TL conditional fields", "peers": "Supergroups and channels to join", "title": "Name of the link", + "title_noanimate": "If set, any animated emojis present in title should not be animated and should be instead frozen on the first frame.", "users": "Related user information" } }, @@ -8852,10 +10709,10 @@ } }, "contacts.ContactBirthdays": { - "desc": "{schema}", + "desc": "Birthday information of our contacts.", "params": { - "contacts": "", - "users": "" + "contacts": "Birthday info", + "users": "User information" } }, "contacts.Contacts": { @@ -8896,6 +10753,18 @@ "users": "Users" } }, + "contacts.SponsoredPeers": { + "desc": "Sponsored peers.", + "params": { + "chats": "Info about sponsored chats and channels", + "peers": "Sponsored peers.", + "users": "Info about sponsored users" + } + }, + "contacts.SponsoredPeersEmpty": { + "desc": "There are no sponsored peers for this query.", + "params": {} + }, "contacts.TopPeers": { "desc": "Top peers", "params": { @@ -8913,21 +10782,21 @@ "params": {} }, "fragment.CollectibleInfo": { - "desc": "{schema}", + "desc": "Info about a fragment collectible.", "params": { - "amount": "", - "crypto_amount": "", - "crypto_currency": "", - "currency": "", - "purchase_date": "", - "url": "" + "amount": "Total price in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies).", + "crypto_amount": "Price, in the smallest units of the cryptocurrency.", + "crypto_currency": "Cryptocurrency name.", + "currency": "Three-letter ISO 4217 currency code for amount", + "purchase_date": "Purchase date (unixtime)", + "url": "Fragment URL with more info about the collectible" } }, "help.AppConfig": { "desc": "Contains various client configuration parameters", "params": { "config": "Client configuration parameters", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "help.AppConfigNotModified": { @@ -8952,7 +10821,7 @@ "desc": "Name, ISO code, localized name and phone codes/patterns of all available countries", "params": { "countries": "Name, ISO code, localized name and phone codes/patterns of all available countries", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "help.CountriesListNotModified": { @@ -9006,7 +10875,7 @@ "desc": "Telegram passport configuration", "params": { "countries_langs": "Localization", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "help.PassportConfigNotModified": { @@ -9018,10 +10887,10 @@ "params": { "channel_min_level": "Channels can use this palette only after reaching at least the boost level specified in this field.", "color_id": "Palette ID.", - "colors": "Light mode palette. Will be empty for IDs 0 to 6 inclusive, in which case a palette containing a single color from the following colors should be used: red, orange, violet, green, cyan, blue, pink for indexes 0 to 6.", + "colors": "Light mode palette. Will be empty for IDs 0 to 6 inclusive, in which case a palette containing a single color from the following colors should be used: red, orange, violet, green, cyan, blue, pink for indexes 0 to 6 (i.e. the same colors used for randomized fallback message accent colors).", "dark_colors": "Dark mode palette. Optional, defaults to the palette in colors (or the autogenerated palette for IDs 0 to 6) if absent.", "flags": "Flags, see TL conditional fields", - "group_min_level": "", + "group_min_level": "Supergroups can use this palette only after reaching at least the boost level specified in this field.", "hidden": "Whether this palette should not be displayed as an option to the user when choosing a palette to apply to profile pages or message accents." } }, @@ -9043,7 +10912,7 @@ "desc": "Contains info about multiple color palettes \u00bb.", "params": { "colors": "Usable color palettes.", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "help.PeerColorsNotModified": { @@ -9062,15 +10931,18 @@ } }, "help.PromoData": { - "desc": "MTProxy/Public Service Announcement information", + "desc": "A set of useful suggestions and a PSA/MTProxy sponsored peer, see here \u00bb for more info.", "params": { "chats": "Chat info", - "expires": "Expiry of PSA/MTProxy info", + "custom_pending_suggestion": "Contains a list of custom pending suggestions \u00bb.", + "dismissed_suggestions": "Contains a list of inverted suggestions \u00bb.", + "expires": "Unixtime when to re-invoke help.getPromoData.", "flags": "Flags, see TL conditional fields", "peer": "MTProxy/PSA peer", - "proxy": "MTProxy-related channel", - "psa_message": "PSA message", - "psa_type": "PSA type", + "pending_suggestions": "Contains a list of pending suggestions \u00bb.", + "proxy": "Set when connecting using an MTProxy that has configured an associated peer (that will be passed in peer, i.e. the channel that sponsored the MTProxy) that should be pinned on top of the chat list.", + "psa_message": "For Public Service Announcement peers, contains the PSA itself.", + "psa_type": "For Public Service Announcement peers, indicates the type of the PSA.", "users": "User info" } }, @@ -9126,14 +10998,14 @@ } }, "help.TimezonesList": { - "desc": "{schema}", + "desc": "Timezone information that may be used elsewhere in the API, such as to set Telegram Business opening hours \u00bb.", "params": { - "hash": "Hash for pagination, for more info click here", - "timezones": "" + "hash": "Hash used for caching, for more info click here", + "timezones": "Timezones" } }, "help.TimezonesListNotModified": { - "desc": "{schema}", + "desc": "The timezone list has not changed.", "params": {} }, "help.UserInfo": { @@ -9176,7 +11048,7 @@ "messages.AllStickers": { "desc": "Info about all installed stickers", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "sets": "All stickersets" } }, @@ -9192,21 +11064,21 @@ } }, "messages.AvailableEffects": { - "desc": "{schema}", + "desc": "The full list of usable animated message effects \u00bb.", "params": { - "documents": "", - "effects": "", - "hash": "Hash for pagination, for more info click here" + "documents": "Documents specified in the effects constructors.", + "effects": "Message effects", + "hash": "Hash used for caching, for more info click here" } }, "messages.AvailableEffectsNotModified": { - "desc": "{schema}", + "desc": "The full list of usable animated message effects \u00bb hasn't changed.", "params": {} }, "messages.AvailableReactions": { "desc": "Animations and metadata associated with message reactions \u00bb", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "reactions": "Animations and metadata associated with message reactions \u00bb" } }, @@ -9236,6 +11108,13 @@ "url": "URL to open" } }, + "messages.BotPreparedInlineMessage": { + "desc": "Represents a prepared inline message saved by a bot, to be sent to the user via a web app \u00bb", + "params": { + "expire_date": "Expiration date of the message", + "id": "The ID of the saved message, to be passed to the id field of the web_app_send_prepared_message event \u00bb" + } + }, "messages.BotResults": { "desc": "Result of a query to an inline bot", "params": { @@ -9322,11 +11201,11 @@ } }, "messages.DialogFilters": { - "desc": "{schema}", + "desc": "Folder and folder tags information", "params": { - "filters": "", + "filters": "Folders.", "flags": "Flags, see TL conditional fields", - "tags_enabled": "" + "tags_enabled": "Whether folder tags are enabled." } }, "messages.Dialogs": { @@ -9367,11 +11246,34 @@ "users": "Users mentioned in constructor" } }, + "messages.EmojiGameDiceInfo": { + "desc": "{schema}", + "params": { + "current_streak": "", + "flags": "Flags, see TL conditional fields", + "game_hash": "", + "params": "", + "plays_left": "", + "prev_stake": "" + } + }, + "messages.EmojiGameOutcome": { + "desc": "{schema}", + "params": { + "seed": "", + "stake_ton_amount": "", + "ton_amount": "" + } + }, + "messages.EmojiGameUnavailable": { + "desc": "{schema}", + "params": {} + }, "messages.EmojiGroups": { "desc": "Represents a list of emoji categories.", "params": { "groups": "A list of emoji categories.", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "messages.EmojiGroupsNotModified": { @@ -9404,7 +11306,7 @@ "messages.FavedStickers": { "desc": "Favorited stickers", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "packs": "Emojis associated to stickers", "stickers": "Favorited stickers" } @@ -9418,7 +11320,7 @@ "params": { "count": "Total number of featured stickers", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "premium": "Whether this is a premium stickerset", "sets": "Featured stickersets", "unread": "IDs of new featured stickersets" @@ -9446,7 +11348,7 @@ "messages.FoundStickerSets": { "desc": "Found stickersets", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "sets": "Found stickersets" } }, @@ -9454,6 +11356,22 @@ "desc": "No further results were found", "params": {} }, + "messages.FoundStickers": { + "desc": "Found stickers", + "params": { + "flags": "Flags, see TL conditional fields", + "hash": "Hash used for caching, for more info click here", + "next_offset": "Offset for pagination", + "stickers": "Found stickers" + } + }, + "messages.FoundStickersNotModified": { + "desc": "No new stickers were found for the specified query", + "params": { + "flags": "Flags, see TL conditional fields", + "next_offset": "Offset for pagination" + } + }, "messages.HighScores": { "desc": "Highscores in a game", "params": { @@ -9485,10 +11403,10 @@ } }, "messages.InvitedUsers": { - "desc": "{schema}", + "desc": "Contains info about successfully or unsuccessfully invited \u00bb users.", "params": { - "missing_invitees": "", - "updates": "" + "missing_invitees": "A list of users that could not be invited, along with the reason why they couldn't be invited.", + "updates": "List of updates about successfully invited users (and eventually info about the created group)" } }, "messages.MessageEditData": { @@ -9522,6 +11440,7 @@ "params": { "chats": "List of chats mentioned in dialogs", "messages": "List of messages", + "topics": "", "users": "List of users mentioned in messages and chats" } }, @@ -9540,15 +11459,17 @@ "inexact": "If set, indicates that the results may be inexact", "messages": "List of messages", "next_rate": "Rate to use in the offset_rate parameter in the next call to messages.searchGlobal", - "offset_id_offset": "Indicates the absolute position of messages[0] within the total result set with count count. This is useful, for example, if the result was fetched using offset_id, and we need to display a progress/total counter (like photo 134 of 200, for all media in a chat, we could simply use photo ${offset_id_offset} of ${count}.", + "offset_id_offset": "Indicates the absolute position of messages[0] within the total result set with count count. This is useful, for example, if the result was fetched using offset_id, and we need to display a progress/total counter (like photo 134 of 200, for all media in a chat, we could simply use photo ${offset_id_offset} of ${count}).", + "search_flood": "For global post searches \u00bb, the remaining amount of free searches, here query_is_free is related to the current call only, not to the next paginated call, and all subsequent pagination calls will always be free.", + "topics": "", "users": "List of users mentioned in messages and chats" } }, "messages.MyStickers": { - "desc": "{schema}", + "desc": "The list of stickersets owned by the current account \u00bb.", "params": { - "count": "", - "sets": "" + "count": "Total number of owned stickersets.", + "sets": "Stickersets" } }, "messages.PeerDialogs": { @@ -9569,23 +11490,33 @@ "users": "Mentioned users" } }, + "messages.PreparedInlineMessage": { + "desc": "Represents a prepared inline message received via a bot's mini app, that can be sent to some chats \u00bb", + "params": { + "cache_time": "Caching validity of the results", + "peer_types": "Types of chats where this message can be sent", + "query_id": "The query_id to pass to messages.sendInlineBotResult", + "result": "The contents of the message, to be shown in a preview", + "users": "Users mentioned in the results" + } + }, "messages.QuickReplies": { - "desc": "{schema}", + "desc": "Info about quick reply shortcuts \u00bb.", "params": { - "chats": "", - "messages": "", - "quick_replies": "", - "users": "" + "chats": "Mentioned chats", + "messages": "Messages mentioned in quick_replies.", + "quick_replies": "Quick reply shortcuts.", + "users": "Mentioned users" } }, "messages.QuickRepliesNotModified": { - "desc": "{schema}", + "desc": "Info about quick reply shortcuts \u00bb hasn't changed.", "params": {} }, "messages.Reactions": { "desc": "List of message reactions", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, can also be locally regenerated using the algorithm specified here \u00bb.", "reactions": "Reactions" } }, @@ -9597,7 +11528,7 @@ "desc": "Recently used stickers", "params": { "dates": "When was each sticker last used", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "packs": "Emojis associated to stickers", "stickers": "Recent stickers" } @@ -9635,7 +11566,7 @@ "desc": "Saved gifs", "params": { "gifs": "List of saved gifs", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "messages.SavedGifsNotModified": { @@ -9643,14 +11574,14 @@ "params": {} }, "messages.SavedReactionTags": { - "desc": "{schema}", + "desc": "List of reaction tag \u00bb names assigned by the user.", "params": { - "hash": "Hash for pagination, for more info click here", - "tags": "" + "hash": "Hash used for caching, for more info click here. Can also be manually regenerated, if needed, using the custom algorithm specified here \u00bb.", + "tags": "Saved reaction tags." } }, "messages.SavedReactionTagsNotModified": { - "desc": "{schema}", + "desc": "The list of reaction tag \u00bb names assigned by the user hasn't changed.", "params": {} }, "messages.SearchCounter": { @@ -9700,10 +11631,12 @@ "messages.SponsoredMessages": { "desc": "A set of sponsored messages associated to a channel", "params": { + "between_delay": "For sponsored messages to show on channel videos \u00bb, the number of seconds to wait after the previous ad is hidden, before showing the next ad.", "chats": "Chats mentioned in the sponsored messages", "flags": "Flags, see TL conditional fields", "messages": "Sponsored messages", "posts_between": "If set, specifies the minimum number of messages between shown sponsored messages; otherwise, only one sponsored message must be shown after all ordinary messages.", + "start_delay": "For sponsored messages to show on channel videos \u00bb, the number of seconds to wait before showing the first ad.", "users": "Users mentioned in the sponsored messages" } }, @@ -9737,7 +11670,7 @@ "messages.Stickers": { "desc": "Found stickers", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "stickers": "Stickers" } }, @@ -9781,6 +11714,14 @@ "webpage": "The instant view webpage." } }, + "messages.WebPagePreview": { + "desc": "Represents a webpage preview.", + "params": { + "chats": "Chats mentioned in the gift field.", + "media": "The messageMediaWebPage or a messageMediaEmpty if there is no preview.", + "users": "Users mentioned within the media object." + } + }, "payments.BankCardData": { "desc": "Credit card info, provided by the card's bank(s)", "params": { @@ -9788,21 +11729,39 @@ "title": "Credit card title" } }, + "payments.CheckCanSendGiftResultFail": { + "desc": "The specified gift cannot be sent yet for the specified reason.", + "params": { + "reason": "The reason why it can't be sent yet." + } + }, + "payments.CheckCanSendGiftResultOk": { + "desc": "The specified gift can be sent.", + "params": {} + }, "payments.CheckedGiftCode": { "desc": "Contains info about a Telegram Premium giftcode link.", "params": { "chats": "Mentioned chats", "date": "Creation date of the gift code.", + "days": "", "flags": "Flags, see TL conditional fields", "from_id": "The peer that created the gift code.", "giveaway_msg_id": "Message ID of the giveaway in the channel specified in from_id.", - "months": "Duration in months of the gifted Telegram Premium subscription.", "to_id": "The destination user of the gift.", "used_date": "When was the giftcode imported, if it was imported.", "users": "Mentioned users", "via_giveaway": "Whether this giftcode was created by a giveaway." } }, + "payments.ConnectedStarRefBots": { + "desc": "Active affiliations", + "params": { + "connected_bots": "The affiliations", + "count": "Total number of active affiliations", + "users": "Peers mentioned in connected_bots" + } + }, "payments.ExportedInvoice": { "desc": "Exported invoice deep link", "params": { @@ -9829,6 +11788,7 @@ "flags": "Flags, see TL conditional fields", "gift_code_slug": "If we're one of the winners of this giveaway, contains the Premium gift code, see here \u00bb for more info on the full giveaway flow.", "refunded": "Whether the giveaway was canceled and was fully refunded.", + "stars_prize": "If we're one of the winners of this Telegram Star giveaway, the number Telegram Stars we won.", "start_date": "Start date of the giveaway", "winner": "Whether we're one of the winners of this giveaway.", "winners_count": "Number of winners in the giveaway" @@ -9856,17 +11816,24 @@ "users": "Users" } }, + "payments.PaymentFormStarGift": { + "desc": "Represents a payment form for a gift, see here \u00bb for more info.", + "params": { + "form_id": "Form ID.", + "invoice": "Invoice" + } + }, "payments.PaymentFormStars": { - "desc": "{schema}", + "desc": "Represents a payment form, for payments to be using Telegram Stars, see here \u00bb for more info.", "params": { - "bot_id": "", - "description": "", + "bot_id": "Bot ID.", + "description": "Description", "flags": "Flags, see TL conditional fields", - "form_id": "", - "invoice": "", - "photo": "", - "title": "", - "users": "" + "form_id": "Form ID.", + "invoice": "Invoice", + "photo": "Product photo", + "title": "Form title", + "users": "Info about users mentioned in the other fields." } }, "payments.PaymentReceipt": { @@ -9890,19 +11857,19 @@ } }, "payments.PaymentReceiptStars": { - "desc": "{schema}", + "desc": "Receipt for payment made using Telegram Stars.", "params": { - "bot_id": "", - "currency": "", - "date": "", - "description": "", + "bot_id": "Bot ID", + "currency": "Currency, always XTR.", + "date": "Date of generation", + "description": "Description", "flags": "Flags, see TL conditional fields", - "invoice": "", - "photo": "", - "title": "", - "total_amount": "", - "transaction_id": "", - "users": "" + "invoice": "Invoice", + "photo": "Product photo", + "title": "Title", + "total_amount": "Amount of Telegram Stars.", + "transaction_id": "Transaction ID", + "users": "Info about users mentioned in the other fields." } }, "payments.PaymentResult": { @@ -9917,6 +11884,20 @@ "url": "URL for additional payment credentials verification" } }, + "payments.ResaleStarGifts": { + "desc": "List of gifts currently on resale \u00bb.", + "params": { + "attributes": "Possible gift attributes, only set if payments.getResaleStarGifts.attributes_hash is set (on the first call, it must be equal to 0).", + "attributes_hash": "Hash of the attributes field, pass this to payments.getResaleStarGifts.attributes_hash to avoid returning any attributes (flag not set) if they haven't changed.", + "chats": "Chats mentioned in the attributes.", + "count": "Total number of results.", + "counters": "Indicates the total number of gifts that have a specific attribute, only set if payments.getResaleStarGifts.offset is empty (since this field is not related to the current result page but to all of them, it's only returned on the first page).", + "flags": "Flags, see TL conditional fields", + "gifts": "Collectible gifts on resale (may be less than count, in which case next_offset will be set).", + "next_offset": "Offset for pagination, pass this to payments.getResaleStarGifts.offset to fetch the next results.", + "users": "Users mentioned in the attributes." + } + }, "payments.SavedInfo": { "desc": "Saved server-side order information", "params": { @@ -9925,76 +11906,234 @@ "saved_info": "Saved server-side order information" } }, - "payments.StarsStatus": { + "payments.SavedStarGifts": { + "desc": "Represents a list of gifts.", + "params": { + "chat_notifications_enabled": "Ternary value: can be not set, set&true, set&false. Can only be set for channels we own: the value indicates whether we enabled gift notifications for this channel.", + "chats": "Channels mentioned in gifts", + "count": "Total number of results (can be less than the returned gifts, in which case next_offset will be set).", + "flags": "Flags, see TL conditional fields", + "gifts": "Gifts", + "next_offset": "Offset to pass to payments.getSavedStarGifts to fetch the next page of results.", + "users": "Users mentioned in gifts" + } + }, + "payments.StarGiftActiveAuctions": { "desc": "{schema}", "params": { - "balance": "", + "auctions": "", "chats": "", - "flags": "Flags, see TL conditional fields", - "history": "", - "next_offset": "", "users": "" } }, - "payments.ValidatedRequestedInfo": { - "desc": "Validated user-provided info", + "payments.StarGiftActiveAuctionsNotModified": { + "desc": "{schema}", + "params": {} + }, + "payments.StarGiftAuctionAcquiredGifts": { + "desc": "{schema}", "params": { - "flags": "Flags, see TL conditional fields", - "id": "ID", - "shipping_options": "Shipping options" + "chats": "", + "gifts": "", + "users": "" } }, - "phone.ExportedGroupCallInvite": { - "desc": "An invite to a group call or livestream", + "payments.StarGiftAuctionState": { + "desc": "{schema}", "params": { - "link": "Invite link" + "chats": "", + "gift": "", + "state": "", + "timeout": "", + "user_state": "", + "users": "" } }, - "phone.GroupCall": { - "desc": "Contains info about a group call, and partial info about its participants.", + "payments.StarGiftCollections": { + "desc": "Represents a list of star gift collections \u00bb.", "params": { - "call": "Info about the group call", - "chats": "Chats mentioned in the participants vector", - "participants": "A partial list of participants.", - "participants_next_offset": "Next offset to use when fetching the remaining participants using phone.getGroupParticipants", - "users": "Users mentioned in the participants vector" + "collections": "Star gift collections." } }, - "phone.GroupCallStreamChannels": { - "desc": "Info about RTMP streams in a group call or livestream", + "payments.StarGiftCollectionsNotModified": { + "desc": "The list of star gift collections \u00bb hasn't changed.", + "params": {} + }, + "payments.StarGiftUpgradeAttributes": { + "desc": "{schema}", "params": { - "channels": "RTMP streams" + "attributes": "" } }, - "phone.GroupCallStreamRtmpUrl": { - "desc": "RTMP URL and stream key to be used in streaming software", + "payments.StarGiftUpgradePreview": { + "desc": "A preview of the possible attributes (chosen randomly) a gift \u00bb can receive after upgrading it to a collectible gift \u00bb, see here \u00bb for more info.", "params": { - "key": "Stream key", - "url": "RTMP URL" + "next_prices": "", + "prices": "", + "sample_attributes": "Possible gift attributes" } }, - "phone.GroupParticipants": { - "desc": "Info about the participants of a group call or livestream", + "payments.StarGiftWithdrawalUrl": { + "desc": "A URL that can be used to import the exported NFT on Fragment.", "params": { - "chats": "Mentioned chats", - "count": "Number of participants", - "next_offset": "If not empty, the specified list of participants is partial, and more participants can be fetched specifying this parameter as offset in phone.getGroupParticipants.", - "participants": "List of participants", - "users": "Mentioned users", - "version": "Version info" + "url": "The URL to open." } }, - "phone.JoinAsPeers": { - "desc": "A list of peers that can be used to join a group call, presenting yourself as a specific user/channel.", + "payments.StarGifts": { + "desc": "Available gifts \u00bb.", "params": { - "chats": "Chats mentioned in the peers vector", - "peers": "Peers", - "users": "Users mentioned in the peers vector" + "chats": "Chats mentioned in the gifts field.", + "gifts": "List of available gifts.", + "hash": "Hash used for caching, for more info click here", + "users": "Users mentioned in the gifts field." } }, - "phone.PhoneCall": { - "desc": "A VoIP phone call", - "params": { + "payments.StarGiftsNotModified": { + "desc": "The list of available gifts \u00bb hasn't changed.", + "params": {} + }, + "payments.StarsRevenueAdsAccountUrl": { + "desc": "Contains a URL leading to a page where the user will be able to place ads for the channel/bot, paying using Telegram Stars.", + "params": { + "url": "URL to open." + } + }, + "payments.StarsRevenueStats": { + "desc": "Star revenue statistics, see here \u00bb for more info.", + "params": { + "flags": "Flags, see TL conditional fields", + "revenue_graph": "Star revenue graph (number of earned stars)", + "status": "Current balance, current withdrawable balance and overall earned Telegram Stars", + "top_hours_graph": "For ad revenue statistics, ad impressions graph", + "usd_rate": "Current conversion rate of Telegram Stars to USD" + } + }, + "payments.StarsRevenueWithdrawalUrl": { + "desc": "Contains the URL to use to withdraw Telegram Star revenue.", + "params": { + "url": "Contains the URL to use to withdraw Telegram Star revenue." + } + }, + "payments.StarsStatus": { + "desc": "Info about the current Telegram Star subscriptions, balance and transaction history \u00bb.", + "params": { + "balance": "Current Telegram Star balance.", + "chats": "Chats mentioned in history.", + "flags": "Flags, see TL conditional fields", + "history": "List of Telegram Star transactions (partial if next_offset is set).", + "next_offset": "Offset to use to fetch more transactions from the transaction history using payments.getStarsTransactions.", + "subscriptions": "Info about current Telegram Star subscriptions, only returned when invoking payments.getStarsTransactions and payments.getStarsSubscriptions.", + "subscriptions_missing_balance": "The number of Telegram Stars the user should buy to be able to extend expired subscriptions soon (i.e. the current balance is not enough to extend all expired subscriptions).", + "subscriptions_next_offset": "Offset for pagination of subscriptions: only usable and returned when invoking payments.getStarsSubscriptions.", + "users": "Users mentioned in history." + } + }, + "payments.SuggestedStarRefBots": { + "desc": "A list of suggested mini apps with available affiliate programs", + "params": { + "count": "Total number of results (for pagination)", + "flags": "Flags, see TL conditional fields", + "next_offset": "Next offset for pagination", + "suggested_bots": "Suggested affiliate programs (full or partial list to be fetched using pagination)", + "users": "Peers mentioned in suggested_bots" + } + }, + "payments.UniqueStarGift": { + "desc": "Represents a collectible gift \u00bb.", + "params": { + "chats": "Chats mentioned in the gift field.", + "gift": "The starGiftUnique constructor.", + "users": "Users mentioned in the gift field." + } + }, + "payments.UniqueStarGiftValueInfo": { + "desc": "Information about the value of a collectible gift \u00bb.", + "params": { + "average_price": "The current average sale price of collectible gifts of the same type, in the smallest unit of the currency specified in currency.", + "currency": "Three-letter ISO 4217 currency code (a localized fiat currency used to represent prices and price estimations in this constructor).", + "flags": "Flags, see TL conditional fields", + "floor_price": "The current minimum price of collectible gifts of the same type, in the smallest unit of the currency specified in currency.", + "fragment_listed_count": "Number of gifts of the same type currently being resold on fragment.", + "fragment_listed_url": "Fragment link to the listing of gifts of the same type currently being resold on fragment.", + "initial_sale_date": "Initial purchase date of the gift.", + "initial_sale_price": "Initial purchase price in the smallest unit of the currency specified in currency (automatically converted from initial_sale_stars).", + "initial_sale_stars": "Initial purchase price in Stars.", + "last_sale_date": "Last resale date of the gift.", + "last_sale_on_fragment": "If set, the last sale was completed on Fragment.", + "last_sale_price": "Last resale price, in the smallest unit of the currency specified in currency.", + "listed_count": "Number of gifts of the same type currently being resold on Telegram.", + "value": "Estimated value of the gift, in the smallest unit of the currency specified in currency.", + "value_is_average": "If set, the value is calculated from the average value of sold gifts of the same type. Otherwise, it is based on the sale price of the gift." + } + }, + "payments.ValidatedRequestedInfo": { + "desc": "Validated user-provided info", + "params": { + "flags": "Flags, see TL conditional fields", + "id": "ID", + "shipping_options": "Shipping options" + } + }, + "phone.ExportedGroupCallInvite": { + "desc": "An invite to a group call or livestream", + "params": { + "link": "Invite link" + } + }, + "phone.GroupCall": { + "desc": "Contains info about a group call, and partial info about its participants.", + "params": { + "call": "Info about the group call", + "chats": "Chats mentioned in the participants vector", + "participants": "A partial list of participants.", + "participants_next_offset": "Next offset to use when fetching the remaining participants using phone.getGroupParticipants", + "users": "Users mentioned in the participants vector" + } + }, + "phone.GroupCallStars": { + "desc": "{schema}", + "params": { + "chats": "", + "top_donors": "", + "total_stars": "", + "users": "" + } + }, + "phone.GroupCallStreamChannels": { + "desc": "Info about RTMP streams in a group call or livestream", + "params": { + "channels": "RTMP streams" + } + }, + "phone.GroupCallStreamRtmpUrl": { + "desc": "RTMP URL and stream key to be used in streaming software", + "params": { + "key": "Stream key", + "url": "RTMP URL" + } + }, + "phone.GroupParticipants": { + "desc": "Info about the participants of a group call or livestream", + "params": { + "chats": "Mentioned chats", + "count": "Number of participants", + "next_offset": "If not empty, the specified list of participants is partial, and more participants can be fetched specifying this parameter as offset in phone.getGroupParticipants.", + "participants": "List of participants", + "users": "Mentioned users", + "version": "Version info" + } + }, + "phone.JoinAsPeers": { + "desc": "A list of peers that can be used to join a group call, presenting yourself as a specific user/channel.", + "params": { + "chats": "Chats mentioned in the peers vector", + "peers": "Peers", + "users": "Users mentioned in the peers vector" + } + }, + "phone.PhoneCall": { + "desc": "A VoIP phone call", + "params": { "phone_call": "The VoIP phone call", "users": "VoIP phone call participants" } @@ -10038,13 +12177,13 @@ "boosts": "Total number of boosts acquired so far.", "current_level_boosts": "The number of boosts acquired so far in the current level.", "flags": "Flags, see TL conditional fields", - "gift_boosts": "The number of boosts acquired from created Telegram Premium gift codes and giveaways; only returned to channel admins.", - "level": "The current boost level of the channel.", - "my_boost": "Whether we're currently boosting this channel, my_boost_slots will also be set.", + "gift_boosts": "The number of boosts acquired from created Telegram Premium gift codes and giveaways; only returned to channel/supergroup admins.", + "level": "The current boost level of the channel/supergroup.", + "my_boost": "Whether we're currently boosting this channel/supergroup, my_boost_slots will also be set.", "my_boost_slots": "Indicates which of our boost slots we've assigned to this peer (populated if my_boost is set).", "next_level_boosts": "Total number of boosts needed to reach the next level; if absent, the next level isn't available.", - "premium_audience": "Only returned to channel admins: contains the approximated number of Premium users subscribed to the channel, related to the total number of subscribers.", - "prepaid_giveaways": "A list of prepaid giveaways available for the chat; only returned to channel admins." + "premium_audience": "Only returned to channel/supergroup admins: contains the approximated number of Premium users subscribed to the channel/supergroup, related to the total number of subscribers.", + "prepaid_giveaways": "A list of prepaid giveaways available for the chat; only returned to channel/supergroup admins." } }, "premium.MyBoosts": { @@ -10056,46 +12195,24 @@ } }, "smsjobs.EligibleToJoin": { - "desc": "{schema}", + "desc": "SMS jobs eligibility", "params": { - "monthly_sent_sms": "", - "terms_url": "" + "monthly_sent_sms": "Monthly sent SMSes", + "terms_url": "Terms of service URL" } }, "smsjobs.Status": { - "desc": "{schema}", + "desc": "Status", "params": { - "allow_international": "", + "allow_international": "Allow international numbers", "flags": "Flags, see TL conditional fields", - "last_gift_slug": "", - "recent_remains": "", - "recent_sent": "", - "recent_since": "", - "terms_url": "", - "total_sent": "", - "total_since": "" - } - }, - "stats.BroadcastRevenueStats": { - "desc": "{schema}", - "params": { - "balances": "", - "revenue_graph": "", - "top_hours_graph": "", - "usd_rate": "" - } - }, - "stats.BroadcastRevenueTransactions": { - "desc": "{schema}", - "params": { - "count": "", - "transactions": "" - } - }, - "stats.BroadcastRevenueWithdrawalUrl": { - "desc": "{schema}", - "params": { - "url": "" + "last_gift_slug": "Last gift deep link", + "recent_remains": "Remaining", + "recent_sent": "Recently sent", + "recent_since": "Since", + "terms_url": "Terms of service URL", + "total_sent": "Total sent", + "total_since": "Total since" } }, "stats.BroadcastStats": { @@ -10218,6 +12335,17 @@ "desc": "WEBP image. MIME type: image/webp.", "params": {} }, + "stories.Albums": { + "desc": "Story albums \u00bb.", + "params": { + "albums": "The albums.", + "hash": "Hash to pass to stories.getAlbums to avoid returning any results if they haven't changed." + } + }, + "stories.AlbumsNotModified": { + "desc": "The story album list \u00bb hasn't changed.", + "params": {} + }, "stories.AllStories": { "desc": "Full list of active (or active and hidden) stories.", "params": { @@ -10239,6 +12367,23 @@ "stealth_mode": "Current stealth mode information" } }, + "stories.CanSendStoryCount": { + "desc": "Contains the number of available active story slots (equal to the value of the story_expiring_limit_* client configuration parameter minus the number of currently active stories).", + "params": { + "count_remains": "Remaining active story slots." + } + }, + "stories.FoundStories": { + "desc": "Stories found using global story search \u00bb.", + "params": { + "chats": "Mentioned chats", + "count": "Total number of results found for the query.", + "flags": "Flags, see TL conditional fields", + "next_offset": "Offset used to fetch the next page, if not set this is the final page.", + "stories": "Matching stories.", + "users": "Mentioned users" + } + }, "stories.PeerStories": { "desc": "Active story list of a specific peer.", "params": { @@ -10253,7 +12398,7 @@ "chats": "Mentioned chats", "count": "Total number of stories that can be fetched", "flags": "Flags, see TL conditional fields", - "pinned_to_top": "", + "pinned_to_top": "IDs of pinned stories.", "stories": "Stories", "users": "Mentioned users" } @@ -10299,7 +12444,7 @@ "new_messages": "New messages", "other_updates": "Other updates", "pts": "The PTS from which to start getting updates the next time", - "timeout": "Clients are supposed to refetch the channel difference after timeout seconds have elapsed", + "timeout": "Clients are supposed to refetch the channel difference after timeout seconds have elapsed, if the user is currently viewing the chat, see here \u00bb for more info.", "users": "Users" } }, @@ -10309,7 +12454,7 @@ "final": "Whether there are more updates that must be fetched (always false)", "flags": "Flags, see TL conditional fields", "pts": "The latest PTS", - "timeout": "Clients are supposed to refetch the channel difference after timeout seconds have elapsed" + "timeout": "Clients are supposed to refetch the channel difference after timeout seconds have elapsed, if the user is currently viewing the chat, see here \u00bb for more info." } }, "updates.ChannelDifferenceTooLong": { @@ -10409,6 +12554,19 @@ "size": "File size" } }, + "users.SavedMusic": { + "desc": "List of songs currently pinned on a user's profile, see here \u00bb for more info.", + "params": { + "count": "Total number of songs (can be bigger than documents depending on the passed limit, and the default maximum limit in which case pagination is required).", + "documents": "Songs." + } + }, + "users.SavedMusicNotModified": { + "desc": "This subset of the songs currently pinned on a user's profile hasn't changed, see here \u00bb for more info.", + "params": { + "count": "Total number of songs on the user's profile." + } + }, "users.UserFull": { "desc": "Full user information", "params": { @@ -10416,6 +12574,19 @@ "full_user": "Full user information", "users": "Mentioned users" } + }, + "users.Users": { + "desc": "Describes a list of users (or bots).", + "params": { + "users": "Users" + } + }, + "users.UsersSlice": { + "desc": "Describes a partial list of users.", + "params": { + "count": "Total number of users (bigger than the users specified in users)", + "users": "Subset of users." + } } }, "method": { @@ -10426,9 +12597,9 @@ "app_version": "Application version", "device_model": "Device model", "flags": "Flags, see TL conditional fields", - "lang_code": "Code for the language used on the client, ISO 639-1 standard", - "lang_pack": "Language pack to use", - "params": "Additional initConnection parameters. For now, only the tz_offset field is supported, for specifying timezone offset in seconds.", + "lang_code": "Either an ISO 639-1 language code or a language pack name obtained from a language pack link.", + "lang_pack": "Platform identifier (i.e. android, tdesktop, etc).", + "params": "Additional initConnection parameters. For now, only the tz_offset field is supported, for specifying the timezone offset in seconds.", "proxy": "Info about an MTProto proxy", "query": "The query itself", "system_lang_code": "Code for the language used on the device's OS, ISO 639-1 standard", @@ -10450,26 +12621,26 @@ } }, "InvokeWithApnsSecret": { - "desc": "{schema}", + "desc": "Official clients only, invoke with Apple push verification.", "params": { - "nonce": "", - "query": "", - "secret": "" + "nonce": "Nonce.", + "query": "Query.", + "secret": "Secret." } }, "InvokeWithBusinessConnection": { - "desc": "{schema}", + "desc": "Invoke a method using a Telegram Business Bot connection, see here \u00bb for more info, including a list of the methods that can be wrapped in this constructor.", "params": { - "connection_id": "", - "query": "" + "connection_id": "Business connection ID.", + "query": "The actual query." } }, "InvokeWithGooglePlayIntegrity": { - "desc": "{schema}", + "desc": "Official clients only, invoke with Google Play Integrity token.", "params": { - "nonce": "", - "query": "", - "token": "" + "nonce": "Nonce.", + "query": "Query.", + "token": "Token." } }, "InvokeWithLayer": { @@ -10486,6 +12657,13 @@ "range": "Message range" } }, + "InvokeWithReCaptcha": { + "desc": "Official clients only: re-execute a method call that required reCAPTCHA verification via a RECAPTCHA_CHECK_%s__%s, where the first placeholder is the action, and the second one is the reCAPTCHA key ID.", + "params": { + "query": "The original method call.", + "token": "reCAPTCHA token received after verification." + } + }, "InvokeWithTakeout": { "desc": "Invoke a method within a takeout session, see here \u00bb for more info.", "params": { @@ -10555,9 +12733,9 @@ } }, "account.CreateBusinessChatLink": { - "desc": "{schema}", + "desc": "Create a business chat deep link \u00bb.", "params": { - "link": "" + "link": "Info about the link to create." } }, "account.CreateTheme": { @@ -10587,9 +12765,15 @@ "params": {} }, "account.DeleteBusinessChatLink": { + "desc": "Delete a business chat deep link \u00bb.", + "params": { + "slug": "Slug of the link, obtained as specified here \u00bb." + } + }, + "account.DeletePasskey": { "desc": "{schema}", "params": { - "slug": "" + "id": "" } }, "account.DeleteSecureValue": { @@ -10599,16 +12783,16 @@ } }, "account.DisablePeerConnectedBot": { - "desc": "{schema}", + "desc": "Permanently disconnect a specific chat from all business bots \u00bb (equivalent to specifying it in recipients.exclude_users during initial configuration with account.updateConnectedBot \u00bb); to reconnect of a chat disconnected using this method the user must reconnect the entire bot by invoking account.updateConnectedBot \u00bb.", "params": { - "peer": "" + "peer": "The chat to disconnect" } }, "account.EditBusinessChatLink": { - "desc": "{schema}", + "desc": "Edit a created business chat deep link \u00bb.", "params": { - "link": "", - "slug": "" + "link": "New link information.", + "slug": "Slug of the link, obtained as specified here \u00bb." } }, "account.FinishTakeoutSession": { @@ -10647,35 +12831,41 @@ "params": {} }, "account.GetBotBusinessConnection": { - "desc": "{schema}", + "desc": "Bots may invoke this method to re-fetch the updateBotBusinessConnect constructor associated with a specific business connection_id, see here \u00bb for more info on connected business bots.\nThis is needed for example for freshly logged in bots that are receiving some updateBotNewBusinessMessage, etc. updates because some users have already connected to the bot before it could login.\nIn this case, the bot is receiving messages from the business connection, but it hasn't cached the associated updateBotBusinessConnect with info about the connection (can it reply to messages? etc.) yet, and cannot receive the old ones because they were sent when the bot wasn't logged into the session yet.\nThis method can be used to fetch info about a not-yet-cached business connection, and should not be invoked if the info is already cached or to fetch changes, as eventual changes will automatically be sent as new updateBotBusinessConnect updates to the bot using the usual update delivery methods \u00bb.", "params": { - "connection_id": "" + "connection_id": "Business connection ID \u00bb." } }, "account.GetBusinessChatLinks": { - "desc": "{schema}", + "desc": "List all created business chat deep links \u00bb.", "params": {} }, "account.GetChannelDefaultEmojiStatuses": { "desc": "Get a list of default suggested channel emoji statuses.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.emojiStatuses.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetChannelRestrictedStatusEmojis": { "desc": "Returns fetch the full list of custom emoji IDs \u00bb that cannot be used in channel emoji statuses \u00bb.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the emojiList.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetChatThemes": { "desc": "Get all available chat themes \u00bb.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.themes.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." + } + }, + "account.GetCollectibleEmojiStatuses": { + "desc": "Obtain a list of emoji statuses \u00bb for owned collectible gifts.", + "params": { + "hash": "Hash for pagination" } }, "account.GetConnectedBots": { - "desc": "{schema}", + "desc": "List all currently connected business bots \u00bb", "params": {} }, "account.GetContactSignUpNotification": { @@ -10689,25 +12879,25 @@ "account.GetDefaultBackgroundEmojis": { "desc": "Get a set of suggested custom emoji stickers that can be used in an accent color pattern.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the emojiList.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetDefaultEmojiStatuses": { "desc": "Get a list of default suggested emoji statuses", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.emojiStatuses.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetDefaultGroupPhotoEmojis": { "desc": "Get a set of suggested custom emoji stickers that can be used as group picture", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the emojiList.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetDefaultProfilePhotoEmojis": { "desc": "Get a set of suggested custom emoji stickers that can be used as profile picture", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the emojiList.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetGlobalPrivacySettings": { @@ -10735,6 +12925,18 @@ "peer": "Notification source" } }, + "account.GetPaidMessagesRevenue": { + "desc": "Get the number of stars we have received from the specified user thanks to paid messages \u00bb; the received amount will be equal to the sent amount multiplied by stars_paid_message_commission_permille divided by 1000.", + "params": { + "flags": "Flags, see TL conditional fields", + "parent_peer": "If set, can contain the ID of a monoforum (channel direct messages) to obtain the number of stars the user has spent to send us direct messages via the channel.", + "user_id": "The user that paid to send us messages." + } + }, + "account.GetPasskeys": { + "desc": "{schema}", + "params": {} + }, "account.GetPassword": { "desc": "Obtain configuration for two-factor authorization with password", "params": {} @@ -10752,19 +12954,25 @@ } }, "account.GetReactionsNotifySettings": { - "desc": "{schema}", + "desc": "Get the current reaction notification settings \u00bb.", "params": {} }, "account.GetRecentEmojiStatuses": { "desc": "Get recently used emoji statuses", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.emojiStatuses.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." + } + }, + "account.GetSavedMusicIds": { + "desc": "Fetch the full list of only the IDs of songs currently added to the profile, see here \u00bb for more info.", + "params": { + "hash": "Hash generated \u00bb from the previously returned list of IDs." } }, "account.GetSavedRingtones": { "desc": "Fetch saved notification sounds", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.savedRingtones.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetSecureValue": { @@ -10784,7 +12992,7 @@ "desc": "Get installed themes", "params": { "format": "Theme format, a string that identifies the theming engines supported by the client", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.themes.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetTmpPassword": { @@ -10794,6 +13002,14 @@ "period": "Time during which the temporary password will be valid, in seconds; should be between 60 and 86400" } }, + "account.GetUniqueGiftChatThemes": { + "desc": "Obtain all chat themes \u00bb associated to owned collectible gifts \u00bb.", + "params": { + "hash": "Hash from a previously returned account.chatThemes constructor, to avoid returning any result if the theme list hasn't changed.", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination." + } + }, "account.GetWallPaper": { "desc": "Get info about a certain wallpaper", "params": { @@ -10803,13 +13019,17 @@ "account.GetWallPapers": { "desc": "Returns a list of available wallpapers.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the account.wallPapers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "account.GetWebAuthorizations": { "desc": "Get web login widget authorizations", "params": {} }, + "account.InitPasskeyRegistration": { + "desc": "{schema}", + "params": {} + }, "account.InitTakeoutSession": { "desc": "Initialize a takeout session, see here \u00bb for more info.", "params": { @@ -10858,6 +13078,12 @@ "token_type": "Device token type, see PUSH updates for the possible values." } }, + "account.RegisterPasskey": { + "desc": "{schema}", + "params": { + "credential": "" + } + }, "account.ReorderUsernames": { "desc": "Reorder usernames associated with the currently logged-in user.", "params": { @@ -10914,9 +13140,9 @@ "params": {} }, "account.ResolveBusinessChatLink": { - "desc": "{schema}", + "desc": "Resolve a business chat deep link \u00bb.", "params": { - "slug": "" + "slug": "Slug of the link, obtained as specified here \u00bb." } }, "account.SaveAutoDownloadSettings": { @@ -10939,6 +13165,15 @@ "users": "Whether the new settings should affect all private chats" } }, + "account.SaveMusic": { + "desc": "Adds or removes a song from the current user's profile see here \u00bb for more info on the music tab of the profile page.", + "params": { + "after_id": "If set, the song will be added after the passed song (must be already pinned on the profile).", + "flags": "Flags, see TL conditional fields", + "id": "The song to add or remove; can be an already added song when reordering songs with after_id. Adding an already added song will never re-add it, only move it to the top of the song list (or after the song passed in after_id).", + "unsave": "If set, removes the song." + } + }, "account.SaveRingtone": { "desc": "Save or remove saved notification sound.", "params": { @@ -11027,6 +13262,12 @@ "settings": "Global privacy settings" } }, + "account.SetMainProfileTab": { + "desc": "Changes the main profile tab of the current user, see here \u00bb for more info.", + "params": { + "tab": "The tab to set as main tab." + } + }, "account.SetPrivacy": { "desc": "Change privacy settings of current account", "params": { @@ -11035,22 +13276,32 @@ } }, "account.SetReactionsNotifySettings": { - "desc": "{schema}", + "desc": "Change the reaction notification settings \u00bb.", "params": { - "settings": "" + "settings": "New reaction notification settings." } }, "account.ToggleConnectedBotPaused": { - "desc": "{schema}", + "desc": "Pause or unpause a specific chat, temporarily disconnecting it from all business bots \u00bb.", "params": { - "paused": "", - "peer": "" + "paused": "Whether to pause or unpause the chat", + "peer": "The chat to pause" + } + }, + "account.ToggleNoPaidMessagesException": { + "desc": "Allow a user to send us messages without paying if paid messages \u00bb are enabled.", + "params": { + "flags": "Flags, see TL conditional fields", + "parent_peer": "If set, applies the setting within the monoforum aka direct messages \u00bb (pass the ID of the monoforum, not the ID of the associated channel).", + "refund_charged": "If set and require_payment is not set, refunds the amounts the user has already paid us to send us messages (directly or via a monoforum).", + "require_payment": "If set, requires the user to pay in order to send us messages. Can only be set by monoforums, not users, i.e. parent_peer must be set if this flag is set; users must instead use the inputPrivacyKeyNoPaidMessages privacy setting to remove a previously added exemption. If not set, allows the user to send us messages without paying (can be unset by both monoforums and users).", + "user_id": "The user to exempt or unexempt." } }, "account.ToggleSponsoredMessages": { - "desc": "{schema}", + "desc": "Disable or re-enable Telegram ads for the current Premium account.", "params": { - "enabled": "" + "enabled": "Enable or disable ads." } }, "account.ToggleUsername": { @@ -11069,65 +13320,64 @@ } }, "account.UpdateBirthday": { - "desc": "{schema}", + "desc": "Update our birthday, see here \u00bb for more info.", "params": { - "birthday": "", + "birthday": "Birthday.", "flags": "Flags, see TL conditional fields" } }, "account.UpdateBusinessAwayMessage": { - "desc": "{schema}", + "desc": "Set a list of Telegram Business away messages.", "params": { "flags": "Flags, see TL conditional fields", - "message": "" + "message": "Away message configuration and contents." } }, "account.UpdateBusinessGreetingMessage": { - "desc": "{schema}", + "desc": "Set a list of Telegram Business greeting messages.", "params": { "flags": "Flags, see TL conditional fields", - "message": "" + "message": "Greeting message configuration and contents." } }, "account.UpdateBusinessIntro": { - "desc": "{schema}", + "desc": "Set or remove the Telegram Business introduction \u00bb.", "params": { "flags": "Flags, see TL conditional fields", - "intro": "" + "intro": "Telegram Business introduction, to remove it call the method without setting this flag." } }, "account.UpdateBusinessLocation": { - "desc": "{schema}", + "desc": "Businesses \u00bb may advertise their location using this method, see here \u00bb for more info.", "params": { - "address": "", + "address": "Mandatory when setting/updating the location, contains a textual description of the address (max 96 UTF-8 chars).", "flags": "Flags, see TL conditional fields", - "geo_point": "" + "geo_point": "Optional, contains a set of geographical coordinates." } }, "account.UpdateBusinessWorkHours": { - "desc": "{schema}", + "desc": "Specify a set of Telegram Business opening hours.\nThis info will be contained in userFull.business_work_hours.", "params": { - "business_work_hours": "", + "business_work_hours": "Opening hours (optional, if not set removes all opening hours).", "flags": "Flags, see TL conditional fields" } }, "account.UpdateColor": { "desc": "Update the accent color and background custom emoji \u00bb of the current account.", "params": { - "background_emoji_id": "Custom emoji ID used in the accent color pattern.", "color": "ID of the accent color palette \u00bb to use (not RGB24, see here \u00bb for more info).", "flags": "Flags, see TL conditional fields", "for_profile": "Whether to change the accent color emoji pattern of the profile page; otherwise, the accent color and emoji pattern of messages will be changed." } }, "account.UpdateConnectedBot": { - "desc": "{schema}", + "desc": "Connect a business bot \u00bb to the current account, or to change the current connection settings.", "params": { - "bot": "", - "can_reply": "", - "deleted": "", + "bot": "The bot to connect or disconnect", + "deleted": "Whether to fully disconnect the bot from the current account.", "flags": "Flags, see TL conditional fields", - "recipients": "" + "recipients": "Configuration for the business connection", + "rights": "Business bot rights." } }, "account.UpdateDeviceLocked": { @@ -11157,9 +13407,9 @@ } }, "account.UpdatePersonalChannel": { - "desc": "{schema}", + "desc": "Associate (or remove) a personal channel \u00bb, that will be listed on our personal profile page \u00bb.", "params": { - "channel": "" + "channel": "The channel, pass inputChannelEmpty to remove it." } }, "account.UpdateProfile": { @@ -11260,6 +13510,14 @@ "phone_number": "Phone number" } }, + "auth.CheckPaidAuth": { + "desc": "Checks the status of a login payment.", + "params": { + "form_id": "The payment form ID passed to payments.sendPaymentForm.", + "phone_code_hash": "The phone code hash obtained from auth.sendCode", + "phone_number": "Phone number" + } + }, "auth.CheckPassword": { "desc": "Try logging to an account protected by a 2FA password.", "params": { @@ -11292,6 +13550,15 @@ "except_ids": "List of already logged-in user IDs, to prevent logging in twice with the same user" } }, + "auth.FinishPasskeyLogin": { + "desc": "{schema}", + "params": { + "credential": "", + "flags": "Flags, see TL conditional fields", + "from_auth_key_id": "", + "from_dc_id": "" + } + }, "auth.ImportAuthorization": { "desc": "Logs in a user using a key transmitted from his native data-center.", "params": { @@ -11322,6 +13589,13 @@ "web_auth_token": "The authorization token" } }, + "auth.InitPasskeyLogin": { + "desc": "{schema}", + "params": { + "api_hash": "", + "api_id": "" + } + }, "auth.LogOut": { "desc": "Logs out the user.", "params": {} @@ -11335,11 +13609,11 @@ } }, "auth.ReportMissingCode": { - "desc": "{schema}", + "desc": "Official apps only, reports that the SMS authentication code wasn't delivered.", "params": { - "mnc": "", - "phone_code_hash": "", - "phone_number": "" + "mnc": "MNC of the current network operator.", + "phone_code_hash": "The phone code hash obtained from auth.sendCode", + "phone_number": "Phone number where we were supposed to receive the code" } }, "auth.RequestFirebaseSms": { @@ -11349,7 +13623,7 @@ "ios_push_secret": "Secret token received via an apple push notification", "phone_code_hash": "Phone code hash returned by auth.sendCode", "phone_number": "Phone number", - "play_integrity_token": "", + "play_integrity_token": "On Android, an object obtained as described in the auth documentation \u00bb", "safety_net_token": "On Android, a JWS object obtained as described in the auth documentation \u00bb" } }, @@ -11363,7 +13637,7 @@ "flags": "Flags, see TL conditional fields", "phone_code_hash": "The phone code hash obtained from auth.sendCode", "phone_number": "The phone number", - "reason": "" + "reason": "Official clients only, used if the device integrity verification failed, and no secret could be obtained to invoke auth.requestFirebaseSms: in this case, the device integrity verification failure reason must be passed here." } }, "auth.ResetAuthorizations": { @@ -11402,11 +13676,19 @@ "first_name": "New user first name", "flags": "Flags, see TL conditional fields", "last_name": "New user last name", - "no_joined_notifications": "", + "no_joined_notifications": "If set, users on Telegram that have already added phone_number to their contacts will not receive signup notifications about this user.", "phone_code_hash": "SMS-message ID", "phone_number": "Phone number in the international format" } }, + "bots.AddPreviewMedia": { + "desc": "Add a main mini app preview, see here \u00bb for more info.", + "params": { + "bot": "The bot that owns the Main Mini App.", + "lang_code": "ISO 639-1 language code, indicating the localization of the preview to add.", + "media": "The photo/video preview, uploaded using messages.uploadMedia." + } + }, "bots.AllowSendMessage": { "desc": "Allow the specified bot to send us messages", "params": { @@ -11426,6 +13708,35 @@ "bot": "The bot" } }, + "bots.CheckDownloadFileParams": { + "desc": "Check if a mini app can request the download of a specific file: called when handling web_app_request_file_download events \u00bb", + "params": { + "bot": "The bot that owns the mini app that requested the download", + "file_name": "The filename from the web_app_request_file_download event \u00bb", + "url": "The url from the web_app_request_file_download event \u00bb" + } + }, + "bots.DeletePreviewMedia": { + "desc": "Delete a main mini app preview, see here \u00bb for more info.", + "params": { + "bot": "The bot that owns the Main Mini App.", + "lang_code": "ISO 639-1 language code, indicating the localization of the preview to delete.", + "media": "The photo/video preview to delete, previously fetched as specified here \u00bb." + } + }, + "bots.EditPreviewMedia": { + "desc": "Edit a main mini app preview, see here \u00bb for more info.", + "params": { + "bot": "The bot that owns the Main Mini App.", + "lang_code": "ISO 639-1 language code, indicating the localization of the preview to edit.", + "media": "The photo/video preview to replace, previously fetched as specified here \u00bb.", + "new_media": "The new photo/video preview, uploaded using messages.uploadMedia." + } + }, + "bots.GetAdminedBots": { + "desc": "Get a list of bots owned by the current user", + "params": {} + }, "bots.GetBotCommands": { "desc": "Obtain a list of bot commands for the specified bot scope and language code", "params": { @@ -11447,6 +13758,32 @@ "user_id": "User ID or empty for the default menu button." } }, + "bots.GetBotRecommendations": { + "desc": "Obtain a list of similarly themed bots, selected based on similarities in their subscriber bases, see here \u00bb for more info.", + "params": { + "bot": "The method will return bots related to the passed bot." + } + }, + "bots.GetPopularAppBots": { + "desc": "Fetch popular Main Mini Apps, to be used in the apps tab of global search \u00bb.", + "params": { + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination, initially an empty string, then re-use the next_offset returned by the previous query." + } + }, + "bots.GetPreviewInfo": { + "desc": "Bot owners only, fetch main mini app preview information, see here \u00bb for more info.", + "params": { + "bot": "The bot that owns the Main Mini App.", + "lang_code": "Fetch previews for the specified ISO 639-1 language code." + } + }, + "bots.GetPreviewMedias": { + "desc": "Fetch main mini app previews, see here \u00bb for more info.", + "params": { + "bot": "The bot that owns the Main Mini App." + } + }, "bots.InvokeWebViewCustomMethod": { "desc": "Send a custom request from a mini bot app, triggered by a web_app_invoke_custom_method event \u00bb.", "params": { @@ -11455,6 +13792,14 @@ "params": "Method parameters" } }, + "bots.ReorderPreviewMedias": { + "desc": "Reorder a main mini app previews, see here \u00bb for more info.", + "params": { + "bot": "The bot that owns the Main Mini App.", + "lang_code": "ISO 639-1 language code, indicating the localization of the previews to reorder.", + "order": "New order of the previews." + } + }, "bots.ReorderUsernames": { "desc": "Reorder usernames associated to a bot we own.", "params": { @@ -11514,6 +13859,23 @@ "user_id": "User ID" } }, + "bots.SetCustomVerification": { + "desc": "Verify a user or chat on behalf of an organization \u00bb.", + "params": { + "bot": "Must not be set if invoked by a bot, must be set to the ID of an owned bot if invoked by a user.", + "custom_description": "Custom description for the verification, the UTF-8 length limit for this field is contained in bot_verification_description_length_limit \u00bb. If not set, Was verified by organization \"organization_name\" will be used as description.", + "enabled": "If set, adds the verification; otherwise removes verification.", + "flags": "Flags, see TL conditional fields", + "peer": "The peer to verify" + } + }, + "bots.ToggleUserEmojiStatusPermission": { + "desc": "Allow or prevent a bot from changing our emoji status \u00bb", + "params": { + "bot": "The bot", + "enabled": "Whether to allow or prevent the bot from changing our emoji status" + } + }, "bots.ToggleUsername": { "desc": "Activate or deactivate a purchased fragment.com username associated to a bot we own.", "params": { @@ -11522,6 +13884,29 @@ "username": "Username" } }, + "bots.UpdateStarRefProgram": { + "desc": "Create, edit or delete the affiliate program of a bot we own", + "params": { + "bot": "The bot", + "commission_permille": "The permille commission rate: it indicates the share of Telegram Stars received by affiliates for every transaction made by users they referred inside of the bot. The minimum and maximum values for this parameter are contained in the starref_min_commission_permille and starref_max_commission_permille client configuration parameters. Can be 0 to terminate the affiliate program. Both the duration and the commission may only be raised after creation of the program: to lower them, the program must first be terminated and a new one created.", + "duration_months": "Indicates the duration of the affiliate program; if not set, there is no expiration date.", + "flags": "Flags, see TL conditional fields" + } + }, + "bots.UpdateUserEmojiStatus": { + "desc": "Change the emoji status of a user (invoked by bots, see here \u00bb for more info on the full flow)", + "params": { + "emoji_status": "The emoji status", + "user_id": "The user whose emoji status should be changed" + } + }, + "channels.CheckSearchPostsFlood": { + "desc": "Check if the specified global post search \u00bb requires payment.", + "params": { + "flags": "Flags, see TL conditional fields", + "query": "The query." + } + }, "channels.CheckUsername": { "desc": "Check if a username is free and can be assigned to a channel/supergroup", "params": { @@ -11529,13 +13914,6 @@ "username": "The username to check" } }, - "channels.ClickSponsoredMessage": { - "desc": "Informs the server that the user has either:", - "params": { - "channel": "Channel where the sponsored message was posted", - "random_id": "Message ID" - } - }, "channels.ConvertToGigagroup": { "desc": "Convert a supergroup to a gigagroup, when requested by channel suggestions.", "params": { @@ -11557,18 +13935,6 @@ "ttl_period": "Time-to-live of all messages that will be sent in the supergroup: once message.date+message.ttl_period === time(), the message will be deleted on the server, and must be deleted locally as well. You can use messages.setDefaultHistoryTTL to edit this value later." } }, - "channels.CreateForumTopic": { - "desc": "Create a forum topic; requires manage_topics rights.", - "params": { - "channel": "The forum", - "flags": "Flags, see TL conditional fields", - "icon_color": "If no custom emoji icon is specified, specifies the color of the fallback topic icon (RGB), one of 0x6FB9F0, 0xFFD67E, 0xCB86DB, 0x8EEE98, 0xFF93B2, or 0xFB6F5F.", - "icon_emoji_id": "ID of the custom emoji used as topic icon. Telegram Premium users can use any custom emoji, other users can only use the custom emojis contained in the inputStickerSetEmojiDefaultTopicIcons emoji pack.", - "random_id": "Unique client message ID to prevent duplicate sending of the same event", - "send_as": "Create the topic as the specified peer", - "title": "Topic title (maximum UTF-8 length: 128)" - } - }, "channels.DeactivateAllUsernames": { "desc": "Disable all purchased usernames of a supergroup or channel", "params": { @@ -11604,18 +13970,12 @@ "participant": "The participant whose messages should be deleted" } }, - "channels.DeleteTopicHistory": { - "desc": "Delete message history of a forum topic", - "params": { - "channel": "Forum", - "top_msg_id": "Topic ID" - } - }, "channels.EditAdmin": { "desc": "Modify the admin rights of a user in a supergroup/channel.", "params": { "admin_rights": "The admin rights", "channel": "The supergroup/channel.", + "flags": "Flags, see TL conditional fields", "rank": "Indicates the role (rank) of the admin in the group: just an arbitrary string", "user_id": "The ID of the user whose admin rights should be modified" } @@ -11628,26 +13988,6 @@ "participant": "Participant to ban" } }, - "channels.EditCreator": { - "desc": "Transfer channel ownership", - "params": { - "channel": "Channel", - "password": "2FA password of account", - "user_id": "New channel owner" - } - }, - "channels.EditForumTopic": { - "desc": "Edit forum topic; requires manage_topics rights.", - "params": { - "channel": "Supergroup", - "closed": "If present, will update the open/closed status of the topic.", - "flags": "Flags, see TL conditional fields", - "hidden": "If present, will hide/unhide the topic (only valid for the \"General\" topic, id=1).", - "icon_emoji_id": "If present, updates the custom emoji used as topic icon. Telegram Premium users can use any custom emoji, other users can only use the custom emojis contained in the inputStickerSetEmojiDefaultTopicIcons emoji pack. Pass 0 to switch to the fallback topic icon.", - "title": "If present, will update the topic title (maximum UTF-8 length: 128).", - "topic_id": "Topic ID" - } - }, "channels.EditLocation": { "desc": "Edit location of geogroup, see here \u00bb for more info on geogroups.", "params": { @@ -11699,13 +14039,13 @@ "by_location": "Get geogroups", "check_limit": "If set and the user has reached the limit of owned public channels/supergroups/geogroups, instead of returning the channel list one of the specified errors will be returned.Useful to check if a new public channel can indeed be created, even before asking the user to enter a channel username to use in channels.checkUsername/channels.updateUsername.", "flags": "Flags, see TL conditional fields", - "for_personal": "" + "for_personal": "Set this flag to only fetch the full list of channels that may be passed to account.updatePersonalChannel to display them on our profile page." } }, "channels.GetChannelRecommendations": { "desc": "Obtain a list of similarly themed public channels, selected based on similarities in their subscriber bases.", "params": { - "channel": "The method will return channels related to the passed channel.", + "channel": "The method will return channels related to the passed channel. If not set, the method will returns channels related to channels the user has joined.", "flags": "Flags, see TL conditional fields" } }, @@ -11715,25 +14055,6 @@ "id": "IDs of channels/supergroups to get info about" } }, - "channels.GetForumTopics": { - "desc": "Get topics of a forum", - "params": { - "channel": "Supergroup", - "flags": "Flags, see TL conditional fields", - "limit": "Maximum number of results to return, see pagination. For optimal performance, the number of returned topics is chosen by the server and can be smaller than the specified limit.", - "offset_date": "Offsets for pagination, for more info click here, date of the last message of the last found topic. Use 0 or any date in the future to get results from the last topic.", - "offset_id": "Offsets for pagination, for more info click here, ID of the last message of the last found topic (or initially 0).", - "offset_topic": "Offsets for pagination, for more info click here, ID of the last found topic (or initially 0).", - "q": "Search query" - } - }, - "channels.GetForumTopicsByID": { - "desc": "Get forum topics by their ID", - "params": { - "channel": "Forum", - "topics": "Topic IDs" - } - }, "channels.GetFullChannel": { "desc": "Get full info about a supergroup, gigagroup or channel", "params": { @@ -11754,6 +14075,13 @@ "offset": "Offset for pagination" } }, + "channels.GetMessageAuthor": { + "desc": "Can only be invoked by non-bot admins of a monoforum \u00bb, obtains the original sender of a message sent by other monoforum admins to the monoforum, on behalf of the channel associated to the monoforum.", + "params": { + "channel": "ID of the monoforum.", + "id": "ID of the message sent by a monoforum admin." + } + }, "channels.GetMessages": { "desc": "Get channel/supergroup messages", "params": { @@ -11781,15 +14109,12 @@ "channels.GetSendAs": { "desc": "Obtains a list of peers that can be used to send messages in a specific group", "params": { + "flags": "Flags, see TL conditional fields", + "for_live_stories": "", + "for_paid_reactions": "If set, fetches the list of peers that can be used to send paid reactions to messages of a specific peer.", "peer": "The group where we intend to send messages" } }, - "channels.GetSponsoredMessages": { - "desc": "Get a list of sponsored messages", - "params": { - "channel": "Peer" - } - }, "channels.InviteToChannel": { "desc": "Invite users to a channel/supergroup", "params": { @@ -11817,21 +14142,12 @@ } }, "channels.ReadMessageContents": { - "desc": "Mark channel/supergroup message contents as read", + "desc": "Mark channel/supergroup message contents as read, emitting an updateChannelReadMessagesContents.", "params": { "channel": "Channel/supergroup", "id": "IDs of messages whose contents should be marked as read" } }, - "channels.ReorderPinnedForumTopics": { - "desc": "Reorder pinned forum topics", - "params": { - "channel": "Supergroup ID", - "flags": "Flags, see TL conditional fields", - "force": "If not set, the order of only the topics present both server-side and in order will be changed (i.e. mentioning topics not pinned server-side in order will not pin them, and not mentioning topics pinned server-side will not unpin them). If set, the entire server-side pinned topic list will be replaced with order (i.e. mentioning topics not pinned server-side in order will pin them, and not mentioning topics pinned server-side will unpin them)", - "order": "Topic IDs \u00bb" - } - }, "channels.ReorderUsernames": { "desc": "Reorder active usernames", "params": { @@ -11854,36 +14170,31 @@ "participant": "Participant whose messages should be reported" } }, - "channels.ReportSponsoredMessage": { - "desc": "{schema}", - "params": { - "channel": "", - "option": "", - "random_id": "" - } - }, "channels.RestrictSponsoredMessages": { - "desc": "{schema}", + "desc": "Disable ads on the specified channel, for all users.", "params": { - "channel": "", - "restricted": "" + "channel": "The channel.", + "restricted": "Whether to disable or re-enable ads." } }, "channels.SearchPosts": { - "desc": "{schema}", + "desc": "Globally search for posts from public channels \u00bb (including those we aren't a member of) containing either a specific hashtag, or a full text query.", "params": { - "hashtag": "", + "allow_paid_stars": "For full text post searches (query), allows payment of the specified amount of Stars for the search, see here \u00bb for more info on the full flow.", + "flags": "Flags, see TL conditional fields", + "hashtag": "The hashtag to search, without the # character.", "limit": "Maximum number of results to return, see pagination", "offset_id": "Offsets for pagination, for more info click here", - "offset_peer": "", - "offset_rate": "" + "offset_peer": "Offsets for pagination, for more info click here", + "offset_rate": "Initially 0, then set to the next_rate parameter of messages.messagesSlice, or if that is absent, the date of the last returned message.", + "query": "The full text query: each user has a limited amount of free full text search slots, after which payment is required, see here \u00bb for more info on the full flow." } }, "channels.SetBoostsToUnblockRestrictions": { - "desc": "{schema}", + "desc": "Admins with ban_users admin rights \u00bb may allow users that apply a certain number of booosts \u00bb to the group to bypass slow mode \u00bb and other \u00bb supergroup restrictions, see here \u00bb for more info.", "params": { - "boosts": "", - "channel": "" + "boosts": "The number of required boosts (1-8, 0 to disable).", + "channel": "The supergroup." } }, "channels.SetDiscussionGroup": { @@ -11894,10 +14205,17 @@ } }, "channels.SetEmojiStickers": { - "desc": "{schema}", + "desc": "Set a custom emoji stickerset for supergroups. Only usable after reaching at least the boost level \u00bb specified in the group_emoji_stickers_level_min \u00bb config parameter.", + "params": { + "channel": "The supergroup", + "stickerset": "The custom emoji stickerset to associate to the supergroup" + } + }, + "channels.SetMainProfileTab": { + "desc": "Changes the main profile tab of a channel, see here \u00bb for more info.", "params": { - "channel": "", - "stickerset": "" + "channel": "The channel.", + "tab": "The tab to set as main tab." } }, "channels.SetStickers": { @@ -11914,11 +14232,19 @@ "enabled": "Enable or disable the native antispam system." } }, + "channels.ToggleAutotranslation": { + "desc": "Toggle autotranslation in a channel, for all users: see here \u00bb for more info.", + "params": { + "channel": "The channel where to toggle autotranslation.", + "enabled": "Whether to enable or disable autotranslation." + } + }, "channels.ToggleForum": { "desc": "Enable or disable forum functionality in a supergroup.", "params": { "channel": "Supergroup ID", - "enabled": "Enable or disable forum functionality" + "enabled": "Enable or disable forum functionality", + "tabs": "If true enables the tabbed forum UI, otherwise enables the list-based forum UI." } }, "channels.ToggleJoinRequest": { @@ -11953,7 +14279,9 @@ "desc": "Enable/disable message signatures in channels", "params": { "channel": "Channel", - "enabled": "Value" + "flags": "Flags, see TL conditional fields", + "profiles_enabled": "If set, messages from channel admins will link to their profiles, just like for group messages: can only be set if the signatures_enabled flag is set.", + "signatures_enabled": "If set, enables message signatures." } }, "channels.ToggleSlowMode": { @@ -11985,22 +14313,23 @@ "channel": "Channel whose accent color should be changed.", "color": "ID of the accent color palette \u00bb to use (not RGB24, see here \u00bb for more info); if not set, the default palette is used.", "flags": "Flags, see TL conditional fields", - "for_profile": "Whether to change the accent color emoji pattern of the profile page; otherwise, the accent color and emoji pattern of messages will be changed." + "for_profile": "Whether to change the accent color emoji pattern of the profile page; otherwise, the accent color and emoji pattern of messages will be changed. Channels can change both message and profile palettes; supergroups can only change the profile palette, of course after reaching the appropriate boost level." } }, "channels.UpdateEmojiStatus": { - "desc": "Set an emoji status for a channel.", + "desc": "Set an emoji status for a channel or supergroup.", "params": { - "channel": "The channel, must have at least channel_emoji_status_level_min boosts.", + "channel": "The channel/supergroup, must have at least channel_emoji_status_level_min/group_emoji_status_level_min boosts.", "emoji_status": "Emoji status to set" } }, - "channels.UpdatePinnedForumTopic": { - "desc": "Pin or unpin forum topics", + "channels.UpdatePaidMessagesPrice": { + "desc": "Enable or disable paid messages \u00bb in this supergroup or monoforum.", "params": { - "channel": "Supergroup ID", - "pinned": "Whether to pin or unpin the topic", - "topic_id": "Forum topic ID" + "broadcast_messages_allowed": "Only usable for channels, enables or disables the associated monoforum aka direct messages.", + "channel": "Pass the supergroup ID for supergroups and the ID of the channel to modify the setting in the associated monoforum.", + "flags": "Flags, see TL conditional fields", + "send_paid_messages_stars": "Specifies the required amount of Telegram Stars users must pay to send messages to the supergroup or monoforum." } }, "channels.UpdateUsername": { @@ -12010,13 +14339,6 @@ "username": "New username, pass an empty string to remove the username" } }, - "channels.ViewSponsoredMessage": { - "desc": "Mark a specific sponsored message as read", - "params": { - "channel": "Peer", - "random_id": "Message ID" - } - }, "chatlists.CheckChatlistInvite": { "desc": "Obtain information about a chat folder deep link \u00bb.", "params": { @@ -12107,6 +14429,7 @@ "flags": "Flags, see TL conditional fields", "id": "Telegram ID of the other user", "last_name": "Last name", + "note": "", "phone": "User's phone number, may be omitted to simply add the user to the contact list, without a phone number." } }, @@ -12151,7 +14474,7 @@ "params": {} }, "contacts.GetBirthdays": { - "desc": "{schema}", + "desc": "Fetch all users with birthdays that fall within +1/-1 days, relative to the current day: this method should be invoked by clients every 6-8 hours, and if the result is non-empty, it should be used to appropriately update locally cached birthday information in user.birthday.", "params": {} }, "contacts.GetBlocked": { @@ -12166,13 +14489,13 @@ "contacts.GetContactIDs": { "desc": "Get the telegram IDs of all contacts.\nReturns an array of Telegram user IDs for all contacts (0 if a contact does not have an associated Telegram account or have hidden their account using privacy settings).", "params": { - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "contacts.GetContacts": { "desc": "Returns the current user's contact list.", "params": { - "hash": "Hash for pagination, for more info click here.Note that the hash is computed using the usual algorithm, passing to the algorithm first the previously returned contacts.contacts.saved_count field, then max 100000 sorted user IDs from the contact list, including the ID of the currently logged in user if it is saved as a contact. Example: tdlib implementation." + "hash": "Hash used for caching, for more info click here.Note that the hash is computed using the usual algorithm, passing to the algorithm first the previously returned contacts.contacts.saved_count field, then max 100000 sorted user IDs from the contact list, including the ID of the currently logged in user if it is saved as a contact. Example: tdlib implementation." } }, "contacts.GetLocated": { @@ -12188,6 +14511,12 @@ "desc": "Get all contacts, requires a takeout session, see here \u00bb for more info.", "params": {} }, + "contacts.GetSponsoredPeers": { + "desc": "Obtain a list of sponsored peer search results for a given query", + "params": { + "q": "The query" + } + }, "contacts.GetStatuses": { "desc": "Use this method to obtain the online statuses of all contacts with an accessible Telegram account.", "params": {} @@ -12195,6 +14524,7 @@ "contacts.GetTopPeers": { "desc": "Get most used peers", "params": { + "bots_app": "Most frequently used Main Mini Bot Apps.", "bots_inline": "Most used inline bots", "bots_pm": "Most used bots", "channels": "Most frequently visited channels", @@ -12203,7 +14533,7 @@ "forward_chats": "Chats to which the users often forwards messages to", "forward_users": "Users to which the users often forwards messages to", "groups": "Often-opened groups and supergroups", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "limit": "Maximum number of results to return, see pagination", "offset": "Offset for pagination", "phone_calls": "Most frequently called users" @@ -12241,6 +14571,8 @@ "contacts.ResolveUsername": { "desc": "Resolve a @username to get peer info", "params": { + "flags": "Flags, see TL conditional fields", + "referer": "Referrer ID from referral links \u00bb.", "username": "@username to resolve" } }, @@ -12274,6 +14606,13 @@ "my_stories_from": "Whether the peer should be removed from the story blocklist; if not set, the peer will be removed from the main blocklist, see here \u00bb for more info." } }, + "contacts.UpdateContactNote": { + "desc": "{schema}", + "params": { + "id": "", + "note": "" + } + }, "folders.EditPeerFolders": { "desc": "Edit peers in peer folder", "params": { @@ -12281,9 +14620,9 @@ } }, "fragment.GetCollectibleInfo": { - "desc": "{schema}", + "desc": "Fetch information about a fragment collectible, see here \u00bb for more info on the full flow.", "params": { - "collectible": "" + "collectible": "Collectible to fetch info about." } }, "help.AcceptTermsOfService": { @@ -12310,7 +14649,7 @@ "help.GetAppConfig": { "desc": "Get app-specific configuration, see client configuration for more info on the result.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the help.appConfig.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "help.GetAppUpdate": { @@ -12330,7 +14669,7 @@ "help.GetCountriesList": { "desc": "Get name, ISO code, localized name and phone codes/patterns of all available countries", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the help.countriesList.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "lang_code": "Language code of the current user" } }, @@ -12351,19 +14690,19 @@ "help.GetPassportConfig": { "desc": "Get passport configuration", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the help.passportConfig.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "help.GetPeerColors": { "desc": "Get the set of accent color palettes \u00bb that can be used for message accents.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the help.peerColors.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "help.GetPeerProfileColors": { "desc": "Get the set of accent color palettes \u00bb that can be used in profile page backgrounds.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the help.peerColors.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "help.GetPremiumPromo": { @@ -12371,7 +14710,7 @@ "params": {} }, "help.GetPromoData": { - "desc": "Get MTProxy/Public Service Announcement information", + "desc": "Returns a set of useful suggestions and PSA/MTProxy sponsored peers, see here \u00bb for more info.", "params": {} }, "help.GetRecentMeUrls": { @@ -12393,9 +14732,9 @@ "params": {} }, "help.GetTimezonesList": { - "desc": "{schema}", + "desc": "Returns timezone information that may be used elsewhere in the API, such as to set Telegram Business opening hours \u00bb.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the help.timezonesList.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "help.GetUserInfo": { @@ -12427,36 +14766,36 @@ "desc": "Get new strings in language pack", "params": { "from_version": "Previous localization pack version", - "lang_code": "Language code", - "lang_pack": "Language pack" + "lang_code": "Either an ISO 639-1 language code or a language pack name obtained from a language pack link.", + "lang_pack": "Platform identifier (i.e. android, tdesktop, etc)." } }, "langpack.GetLangPack": { "desc": "Get localization pack strings", "params": { - "lang_code": "Language code", - "lang_pack": "Language pack name, usually obtained from a language pack link" + "lang_code": "Either an ISO 639-1 language code or a language pack name obtained from a language pack link.", + "lang_pack": "Platform identifier (i.e. android, tdesktop, etc)." } }, "langpack.GetLanguage": { "desc": "Get information about a language in a localization pack", "params": { - "lang_code": "Language code", - "lang_pack": "Language pack name, usually obtained from a language pack link" + "lang_code": "Either an ISO 639-1 language code or a language pack name obtained from a language pack link.", + "lang_pack": "Platform identifier (i.e. android, tdesktop, etc)." } }, "langpack.GetLanguages": { "desc": "Get information about all languages in a localization pack", "params": { - "lang_pack": "Language pack" + "lang_pack": "Platform identifier (i.e. android, tdesktop, etc)." } }, "langpack.GetStrings": { "desc": "Get strings from a language pack", "params": { "keys": "Strings to get", - "lang_code": "Language code", - "lang_pack": "Language pack name, usually obtained from a language pack link" + "lang_code": "Either an ISO 639-1 language code or a language pack name obtained from a language pack link.", + "lang_pack": "Platform identifier (i.e. android, tdesktop, etc)." } }, "messages.AcceptEncryption": { @@ -12472,8 +14811,10 @@ "params": { "button_id": "ID of the login button", "flags": "Flags, see TL conditional fields", + "match_code": "", "msg_id": "Message ID of the message with the login button", "peer": "The location of the message", + "share_phone_number": "", "url": "URL used for link URL authorization, click here for more info \u00bb", "write_allowed": "Set this flag to allow the bot to send messages to you (if requested)" } @@ -12486,6 +14827,14 @@ "user_id": "User ID to be added" } }, + "messages.AppendTodoList": { + "desc": "Appends one or more items to a todo list \u00bb.", + "params": { + "list": "Items to append.", + "msg_id": "ID of the message with the todo list.", + "peer": "Peer where the todo list was posted." + } + }, "messages.CheckChatInvite": { "desc": "Check the validity of a chat invite link and get basic info about it", "params": { @@ -12505,9 +14854,16 @@ } }, "messages.CheckQuickReplyShortcut": { + "desc": "Before offering the user the choice to add a message to a quick reply shortcut, to make sure that none of the limits specified here \u00bb were reached.", + "params": { + "shortcut": "Shorcut name (not ID!)." + } + }, + "messages.CheckUrlAuthMatchCode": { "desc": "{schema}", "params": { - "shortcut": "" + "match_code": "", + "url": "" } }, "messages.ClearAllDrafts": { @@ -12525,6 +14881,15 @@ "flags": "Flags, see TL conditional fields" } }, + "messages.ClickSponsoredMessage": { + "desc": "Informs the server that the user has interacted with a sponsored message in one of the ways listed here \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "fullscreen": "The user expanded the video to full screen, and then clicked on it.", + "media": "The user clicked on the media", + "random_id": "The ad's unique ID." + } + }, "messages.CreateChat": { "desc": "Creates a new chat.", "params": { @@ -12534,6 +14899,25 @@ "users": "List of user IDs to be invited" } }, + "messages.CreateForumTopic": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "icon_color": "", + "icon_emoji_id": "", + "peer": "", + "random_id": "", + "send_as": "", + "title": "", + "title_missing": "" + } + }, + "messages.DeclineUrlAuth": { + "desc": "{schema}", + "params": { + "url": "" + } + }, "messages.DeleteChat": { "desc": "Delete a chat", "params": { @@ -12557,10 +14941,10 @@ } }, "messages.DeleteFactCheck": { - "desc": "{schema}", + "desc": "Delete a fact-check from a message.", "params": { - "msg_id": "", - "peer": "" + "msg_id": "Message ID", + "peer": "Peer where the message was sent." } }, "messages.DeleteHistory": { @@ -12591,16 +14975,16 @@ } }, "messages.DeleteQuickReplyMessages": { - "desc": "{schema}", + "desc": "Delete one or more messages from a quick reply shortcut. This will also emit an updateDeleteQuickReplyMessages update.", "params": { - "id": "", - "shortcut_id": "" + "id": "IDs of shortcut messages to delete.", + "shortcut_id": "Shortcut ID." } }, "messages.DeleteQuickReplyShortcut": { - "desc": "{schema}", + "desc": "Completely delete a quick reply shortcut.\nThis will also emit an updateDeleteQuickReply update to other logged-in sessions (and no updateDeleteQuickReplyMessages updates, even if all the messages in the shortcuts are also deleted by this method).", "params": { - "shortcut_id": "" + "shortcut_id": "Shortcut ID" } }, "messages.DeleteRevokedExportedChatInvites": { @@ -12611,13 +14995,14 @@ } }, "messages.DeleteSavedHistory": { - "desc": "Deletes messages forwarded from a specific peer to saved messages \u00bb.", + "desc": "Deletes messages from a monoforum topic \u00bb, or deletes messages forwarded from a specific peer to saved messages \u00bb.", "params": { "flags": "Flags, see TL conditional fields", "max_date": "Delete all messages older than this UNIX timestamp", "max_id": "Maximum ID of message to delete", "min_date": "Delete all messages newer than this UNIX timestamp", - "peer": "Peer, whose messages will be deleted from saved messages \u00bb" + "parent_peer": "If set, affects the messages of the passed monoforum topic \u00bb, otherwise affects saved messages \u00bb.", + "peer": "Peer, whose messages will be deleted from saved messages \u00bb, or the ID of the topic." } }, "messages.DeleteScheduledMessages": { @@ -12627,6 +15012,13 @@ "peer": "Peer" } }, + "messages.DeleteTopicHistory": { + "desc": "{schema}", + "params": { + "peer": "", + "top_msg_id": "" + } + }, "messages.DiscardEncryption": { "desc": "Cancels a request for creation and/or delete info on secret chat.", "params": { @@ -12650,6 +15042,14 @@ "user_id": "The user to make admin" } }, + "messages.EditChatCreator": { + "desc": "{schema}", + "params": { + "password": "", + "peer": "", + "user_id": "" + } + }, "messages.EditChatDefaultBannedRights": { "desc": "Edit the default banned rights of a channel/supergroup/group.", "params": { @@ -12657,6 +15057,14 @@ "peer": "The peer" } }, + "messages.EditChatParticipantRank": { + "desc": "{schema}", + "params": { + "participant": "", + "peer": "", + "rank": "" + } + }, "messages.EditChatPhoto": { "desc": "Changes chat photo and sends a service message on it", "params": { @@ -12685,11 +15093,23 @@ } }, "messages.EditFactCheck": { + "desc": "Edit/create a fact-check on a message.", + "params": { + "msg_id": "Message ID", + "peer": "Peer where the message was sent", + "text": "Fact-check (maximum UTF-8 length specified in appConfig.factcheck_length_limit)." + } + }, + "messages.EditForumTopic": { "desc": "{schema}", "params": { - "msg_id": "", + "closed": "", + "flags": "Flags, see TL conditional fields", + "hidden": "", + "icon_emoji_id": "", "peer": "", - "text": "" + "title": "", + "topic_id": "" } }, "messages.EditInlineBotMessage": { @@ -12716,16 +15136,17 @@ "message": "New message", "no_webpage": "Disable webpage preview", "peer": "Where was the message sent", - "quick_reply_shortcut_id": "", + "quick_reply_shortcut_id": "If specified, edits a quick reply shortcut message, instead \u00bb.", "reply_markup": "Reply markup for inline keyboards", - "schedule_date": "Scheduled message date for scheduled messages" + "schedule_date": "Scheduled message date for scheduled messages", + "schedule_repeat_period": "" } }, "messages.EditQuickReplyShortcut": { - "desc": "{schema}", + "desc": "Rename a quick reply shortcut.\nThis will emit an updateQuickReplies update to other logged-in sessions.", "params": { - "shortcut": "", - "shortcut_id": "" + "shortcut": "New shortcut name.", + "shortcut_id": "Shortcut ID." } }, "messages.ExportChatInvite": { @@ -12736,6 +15157,7 @@ "legacy_revoke_permanent": "Legacy flag, reproducing legacy behavior of this method: if set, revokes all previous links before creating a new one. Kept for bot API BC, should not be used by modern clients.", "peer": "Chat", "request_needed": "Whether admin confirmation is required before admitting each separate user into the chat", + "subscription_pricing": "For Telegram Star subscriptions \u00bb, contains the pricing of the subscription the user must activate to join the private channel.", "title": "Description of the invite link, visible only to administrators", "usage_limit": "Maximum number of users that can join using this link" } @@ -12750,20 +15172,27 @@ "messages.ForwardMessages": { "desc": "Forwards messages by their IDs.", "params": { + "allow_paid_floodskip": "Bots only: if set, allows sending up to 1000 messages per second, ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.", + "allow_paid_stars": "For paid messages \u00bb, specifies the amount of Telegram Stars the user has agreed to pay in order to send the message.", "background": "Whether to send the message in background", "drop_author": "Whether to forward messages without quoting the original author", "drop_media_captions": "Whether to strip captions from media", + "effect": "", "flags": "Flags, see TL conditional fields", "from_peer": "Source of messages", "id": "IDs of messages", "noforwards": "Only for bots, disallows further re-forwarding and saving of the messages, even if the destination chat doesn't have content protection enabled", - "quick_reply_shortcut": "", + "quick_reply_shortcut": "Add the messages to the specified quick reply shortcut \u00bb, instead.", "random_id": "Random ID to prevent resending of messages", + "reply_to": "Can only contain an inputReplyToMonoForum, to forward messages to a monoforum topic (mutually exclusive with top_msg_id).", "schedule_date": "Scheduled message date for scheduled messages", + "schedule_repeat_period": "", "send_as": "Forward the messages as the specified peer", "silent": "Whether to send messages silently (no notification will be triggered on the destination clients)", + "suggested_post": "Used to suggest a post to a channel, see here \u00bb for more info on the full flow.", "to_peer": "Destination peer", "top_msg_id": "Destination forum topic", + "video_timestamp": "Start playing the video at the specified timestamp (seconds).", "with_my_score": "When forwarding games, whether to include your score in the game" } }, @@ -12780,7 +15209,7 @@ "messages.GetAllStickers": { "desc": "Get all installed stickers", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.allStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetArchivedStickers": { @@ -12802,7 +15231,7 @@ "messages.GetAttachMenuBots": { "desc": "Returns installed attachment menu bot mini apps \u00bb", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the attachMenuBots.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetAttachedStickers": { @@ -12812,22 +15241,22 @@ } }, "messages.GetAvailableEffects": { - "desc": "{schema}", + "desc": "Fetch the full list of usable animated message effects \u00bb.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.availableEffects.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetAvailableReactions": { "desc": "Obtain available message reactions \u00bb", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.availableReactions.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetBotApp": { "desc": "Obtain information about a direct link Mini App", "params": { "app": "Bot app information obtained from a Direct Mini App deep link \u00bb.", - "hash": "Hash for pagination, for more info click here" + "hash": "Hash used for caching, for more info click here" } }, "messages.GetBotCallbackAnswer": { @@ -12851,7 +15280,8 @@ "offset_user": "User ID for pagination: if set, offset_date must also be set.", "peer": "Chat", "q": "Search for a user in the pending join requests \u00bb list: only available when the requested flag is set, cannot be used together with a specific link.", - "requested": "If set, only returns info about users with pending join requests \u00bb" + "requested": "If set, only returns info about users with pending join requests \u00bb", + "subscription_expired": "Set this flag if the link is a Telegram Star subscription link \u00bb and only members with already expired subscription must be returned." } }, "messages.GetChats": { @@ -12881,7 +15311,7 @@ "messages.GetDefaultTagReactions": { "desc": "Fetch a default recommended list of saved message tag reactions.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.reactions.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetDhConfig": { @@ -12897,7 +15327,10 @@ }, "messages.GetDialogUnreadMarks": { "desc": "Get dialogs manually marked as unread", - "params": {} + "params": { + "flags": "Flags, see TL conditional fields", + "parent_peer": "Can be equal to the ID of a monoforum, to fetch monoforum topics manually marked as unread." + } }, "messages.GetDialogs": { "desc": "Returns the current user dialog list.", @@ -12905,7 +15338,7 @@ "exclude_pinned": "Exclude pinned dialogs", "flags": "Flags, see TL conditional fields", "folder_id": "Peer folder ID, for more info click here", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "limit": "Number of list elements to be returned", "offset_date": "Offsets for pagination, for more info click here", "offset_id": "Offsets for pagination, for more info click here (top_message ID used for pagination)", @@ -12927,10 +15360,14 @@ "size": "Size of the file in bytes" } }, + "messages.GetEmojiGameInfo": { + "desc": "{schema}", + "params": {} + }, "messages.GetEmojiGroups": { - "desc": "Represents a list of emoji categories, to be used when selecting custom emojis.", + "desc": "Represents a list of emoji categories.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.emojiGroups.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetEmojiKeywords": { @@ -12955,25 +15392,25 @@ "messages.GetEmojiProfilePhotoGroups": { "desc": "Represents a list of emoji categories, to be used when selecting custom emojis to set as profile picture.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.emojiGroups.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetEmojiStatusGroups": { "desc": "Represents a list of emoji categories, to be used when selecting custom emojis to set as custom emoji status.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.emojiGroups.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetEmojiStickerGroups": { - "desc": "{schema}", + "desc": "Represents a list of emoji categories, to be used when choosing a sticker.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.emojiGroups.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetEmojiStickers": { "desc": "Gets the list of currently installed custom emoji stickersets.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.allStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetEmojiURL": { @@ -13002,35 +15439,54 @@ } }, "messages.GetExtendedMedia": { - "desc": "Get information about extended media", + "desc": "Fetch updated information about paid media, see here \u00bb for the full flow.", "params": { - "id": "Message IDs", - "peer": "Peer" + "id": "IDs of currently visible messages containing paid media.", + "peer": "Peer with visible paid media messages." } }, "messages.GetFactCheck": { - "desc": "{schema}", + "desc": "Fetch one or more factchecks, see here \u00bb for the full flow.", "params": { - "msg_id": "", - "peer": "" + "msg_id": "Messages that have associated factCheck constructors with the need_check flag set.", + "peer": "Peer where the messages were sent." } }, "messages.GetFavedStickers": { "desc": "Get faved stickers", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.favedStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetFeaturedEmojiStickers": { "desc": "Gets featured custom emoji stickersets.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.featuredStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetFeaturedStickers": { "desc": "Get featured stickers", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.featuredStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." + } + }, + "messages.GetForumTopics": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "limit": "Maximum number of results to return, see pagination", + "offset_date": "Offsets for pagination, for more info click here", + "offset_id": "Offsets for pagination, for more info click here", + "offset_topic": "", + "peer": "", + "q": "" + } + }, + "messages.GetForumTopicsByID": { + "desc": "{schema}", + "params": { + "peer": "", + "topics": "" } }, "messages.GetFullChat": { @@ -13039,6 +15495,12 @@ "chat_id": "Basic group ID." } }, + "messages.GetFutureChatCreatorAfterLeave": { + "desc": "{schema}", + "params": { + "peer": "" + } + }, "messages.GetGameHighScores": { "desc": "Get highscores of a game", "params": { @@ -13081,7 +15543,7 @@ "messages.GetMaskStickers": { "desc": "Get installed mask stickers", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.allStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetMessageEditData": { @@ -13131,7 +15593,7 @@ } }, "messages.GetMyStickers": { - "desc": "{schema}", + "desc": "Fetch all stickersets \u00bb owned by the current user.", "params": { "limit": "Maximum number of results to return, see pagination", "offset_id": "Offsets for pagination, for more info click here" @@ -13140,7 +15602,7 @@ "messages.GetOldFeaturedStickers": { "desc": "Method for fetching previously featured stickers", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.featuredStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "limit": "Maximum number of results to return, see pagination", "offset": "Offset" } @@ -13152,12 +15614,16 @@ } }, "messages.GetOutboxReadDate": { - "desc": "{schema}", + "desc": "Get the exact read date of one of our messages, sent to a private chat with another user.", "params": { - "msg_id": "", - "peer": "" + "msg_id": "The message ID.", + "peer": "The user to whom we sent the message." } }, + "messages.GetPaidReactionPrivacy": { + "desc": "Fetches an updatePaidReactionPrivacy update with the current default paid reaction privacy, see here \u00bb for more info.", + "params": {} + }, "messages.GetPeerDialogs": { "desc": "Get dialog info of specified peers", "params": { @@ -13184,7 +15650,8 @@ "desc": "Get poll results", "params": { "msg_id": "Message ID of poll message", - "peer": "Peer where the poll was found" + "peer": "Peer where the poll was found", + "poll_hash": "Pass the poll.hash from the last received poll state; the server skips returning unchanged data" } }, "messages.GetPollVotes": { @@ -13198,25 +15665,32 @@ "peer": "Chat where the poll was sent" } }, + "messages.GetPreparedInlineMessage": { + "desc": "Obtain a prepared inline message generated by a mini app: invoked when handling web_app_send_prepared_message events", + "params": { + "bot": "The bot that owns the mini app that emitted the web_app_send_prepared_message event", + "id": "The id from the web_app_send_prepared_message event" + } + }, "messages.GetQuickReplies": { - "desc": "{schema}", + "desc": "Fetch basic info about all existing quick reply shortcuts.", "params": { - "hash": "Hash for pagination, for more info click here" + "hash": "Hash for pagination, generated as specified here \u00bb (not the usual algorithm used for hash generation.)" } }, "messages.GetQuickReplyMessages": { - "desc": "{schema}", + "desc": "Fetch (a subset or all) messages in a quick reply shortcut \u00bb.", "params": { "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here", - "id": "", - "shortcut_id": "" + "hash": "Hash for pagination, generated as specified here \u00bb (not the usual algorithm used for hash generation).", + "id": "IDs of the messages to fetch, if empty fetches all of them.", + "shortcut_id": "Quick reply shortcut ID." } }, "messages.GetRecentLocations": { "desc": "Get live location history of a certain user", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "limit": "Maximum number of results to return, see pagination", "peer": "User" } @@ -13224,7 +15698,7 @@ "messages.GetRecentReactions": { "desc": "Get recently used message reactions", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.reactions.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "limit": "Maximum number of results to return, see pagination" } }, @@ -13233,14 +15707,14 @@ "params": { "attached": "Get stickers recently attached to photo or video files", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.recentStickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetReplies": { "desc": "Get messages in a reply thread", "params": { "add_offset": "Offsets for pagination, for more info click here", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "limit": "Maximum number of results to return, see pagination", "max_id": "If a positive value was transferred, the method will return only messages with ID smaller than max_id", "min_id": "If a positive value was transferred, the method will return only messages with ID bigger than min_id", @@ -13251,48 +15725,59 @@ } }, "messages.GetSavedDialogs": { - "desc": "Returns the current saved dialog list, see here \u00bb for more info.", + "desc": "Returns the current saved dialog list \u00bb or monoforum topic list \u00bb.", "params": { "exclude_pinned": "Exclude pinned dialogs", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "limit": "Number of list elements to be returned", "offset_date": "Offsets for pagination, for more info click here", "offset_id": "Offsets for pagination, for more info click here (top_message ID used for pagination)", - "offset_peer": "Offset peer for pagination" + "offset_peer": "Offset peer for pagination", + "parent_peer": "If set, fetches the topic list of the passed monoforum, otherwise fetches the saved dialog list." + } + }, + "messages.GetSavedDialogsByID": { + "desc": "Obtain information about specific saved message dialogs \u00bb or monoforum topics \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "ids": "IDs of dialogs (topics) to fetch.", + "parent_peer": "If set, fetches monoforum topics \u00bb, otherwise fetches saved message dialogs \u00bb." } }, "messages.GetSavedGifs": { "desc": "Get saved GIFs.", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.savedGifs.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetSavedHistory": { - "desc": "Returns saved messages \u00bb forwarded from a specific peer", + "desc": "Fetch saved messages \u00bb forwarded from a specific peer, or fetch messages from a monoforum topic \u00bb.", "params": { "add_offset": "Number of list elements to be skipped, negative values are also accepted.", + "flags": "Flags, see TL conditional fields", "hash": "Result hash", "limit": "Number of results to return", "max_id": "If a positive value was transferred, the method will return only messages with IDs less than max_id", "min_id": "If a positive value was transferred, the method will return only messages with IDs more than min_id", "offset_date": "Only return messages sent before the specified date", "offset_id": "Only return messages starting from the specified message ID", - "peer": "Target peer" + "parent_peer": "If set, fetches messages from the specified monoforum, otherwise fetches from saved messages.", + "peer": "Target peer (or topic)" } }, "messages.GetSavedReactionTags": { "desc": "Fetch the full list of saved message tags created by the user.", "params": { "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.savedReactionTags.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "peer": "If set, returns tags only used in the specified saved message dialog." } }, "messages.GetScheduledHistory": { "desc": "Get scheduled messages", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here. To generate the hash, populate the ids array with the id, edit_date (0 if unedited) and date (in this order) of the previously returned messages (in order, i.e. ids = [id1, (edit_date1 ?? 0), date1, id2, (edit_date2 ?? 0), date2, ...]).", "peer": "Peer" } }, @@ -13339,10 +15824,18 @@ "desc": "Get message ranges for saving the user's chat history", "params": {} }, + "messages.GetSponsoredMessages": { + "desc": "Get a list of sponsored messages for a peer, see here \u00bb for more info.", + "params": { + "flags": "Flags, see TL conditional fields", + "msg_id": "Must be set when fetching sponsored messages to show on channel videos \u00bb.", + "peer": "The currently open channel/bot." + } + }, "messages.GetStickerSet": { "desc": "Get info about a stickerset", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here", "stickerset": "Stickerset" } }, @@ -13350,7 +15843,7 @@ "desc": "Get stickers by emoji", "params": { "emoticon": "The emoji", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.stickers.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.GetSuggestedDialogFilters": { @@ -13360,7 +15853,7 @@ "messages.GetTopReactions": { "desc": "Got popular message reactions", "params": { - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.reactions.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "limit": "Maximum number of results to return, see pagination" } }, @@ -13387,13 +15880,14 @@ "min_id": "Only return reactions for messages starting from this message ID", "offset_id": "Offsets for pagination, for more info click here", "peer": "Peer", + "saved_peer_id": "If set, must be equal to the ID of a monoforum topic: will affect that topic in the monoforum passed in peer.", "top_msg_id": "If set, considers only reactions to messages within the specified forum topic" } }, "messages.GetWebPage": { "desc": "Get instant view page", "params": { - "hash": "Hash for pagination, for more info click here", + "hash": "Hash used for caching, for more info click here. Note: the usual hash generation algorithm cannot be used in this case, please re-use the webPage.hash field returned by a previous call to the method, or pass 0 if this is the first call or if the previous call did not return a webPage.", "url": "URL of IV page to fetch" } }, @@ -13454,6 +15948,7 @@ "desc": "Manually mark dialog as unread", "params": { "flags": "Flags, see TL conditional fields", + "parent_peer": "If set, must be equal to the ID of a monoforum, and will affect the monoforum topic passed in peer.", "peer": "Dialog", "unread": "Mark as unread/read" } @@ -13522,7 +16017,7 @@ } }, "messages.ReadMessageContents": { - "desc": "Notifies the sender about the recipient having listened a voice message or watched a video.", + "desc": "Notifies the sender about the recipient having listened a voice message or watched a video, emitting an updateReadMessagesContents.", "params": { "id": "Message ID list" } @@ -13532,9 +16027,18 @@ "params": { "flags": "Flags, see TL conditional fields", "peer": "Peer", + "saved_peer_id": "If set, must be equal to the ID of a monoforum topic: will affect that topic in the monoforum passed in peer.", "top_msg_id": "Mark as read only reactions to messages within the specified forum topic" } }, + "messages.ReadSavedHistory": { + "desc": "Mark messages as read in a monoforum topic \u00bb.", + "params": { + "max_id": "If a positive value is passed, only messages with identifiers less or equal than the given one will be read.", + "parent_peer": "ID of the monoforum group.", + "peer": "ID of the topic." + } + }, "messages.ReceivedMessages": { "desc": "Confirms receipt of messages by a client, cancels PUSH-notification sending.", "params": { @@ -13556,6 +16060,15 @@ "order": "New dialog order" } }, + "messages.ReorderPinnedForumTopics": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "force": "", + "order": "", + "peer": "" + } + }, "messages.ReorderPinnedSavedDialogs": { "desc": "Reorder pinned saved message dialogs \u00bb.", "params": { @@ -13565,9 +16078,9 @@ } }, "messages.ReorderQuickReplies": { - "desc": "{schema}", + "desc": "Reorder quick reply shortcuts.", "params": { - "order": "" + "order": "IDs of all created quick reply shortcuts, in the desired order." } }, "messages.ReorderStickerSets": { @@ -13584,8 +16097,8 @@ "params": { "id": "IDs of messages to report", "message": "Comment for report moderation", - "peer": "Peer", - "reason": "Why are these messages being reported" + "option": "Menu option, intially empty", + "peer": "Peer" } }, "messages.ReportEncryptedSpam": { @@ -13594,6 +16107,15 @@ "peer": "The secret chat to report" } }, + "messages.ReportMessagesDelivery": { + "desc": "Used for Telegram Gateway verification messages \u00bb: indicate to the server that one or more messages were received by the client, if requested by the message.report_delivery_until_date flag or the equivalent flag in push notifications.", + "params": { + "flags": "Flags, see TL conditional fields", + "id": "The IDs of the received messages.", + "peer": "The peer where the messages were received.", + "push": "Must be set if the messages were received from a push notification." + } + }, "messages.ReportReaction": { "desc": "Report a message reaction", "params": { @@ -13608,11 +16130,20 @@ "peer": "Peer to report" } }, + "messages.ReportSponsoredMessage": { + "desc": "Report a sponsored message \u00bb, see here \u00bb for more info on the full flow.", + "params": { + "option": "Chosen report option, initially an empty string, see here \u00bb for more info on the full flow.", + "random_id": "The ad's unique ID." + } + }, "messages.RequestAppWebView": { "desc": "Open a bot mini app from a direct Mini App deep link, sending over user information after user confirmation.", "params": { "app": "The app obtained by invoking messages.getBotApp as specified in the direct Mini App deep link docs.", + "compact": "If set, requests to open the mini app in compact mode (as opposed to normal or fullscreen mode). Must be set if the mode parameter of the direct Mini App deep link is equal to compact.", "flags": "Flags, see TL conditional fields", + "fullscreen": "If set, requests to open the mini app in fullscreen mode (as opposed to compact or normal mode). Must be set if the mode parameter of the direct Mini App deep link is equal to fullscreen.", "peer": "If the client has clicked on the link in a Telegram chat, pass the chat's peer information; otherwise pass the bot's peer information, instead.", "platform": "Short name of the application; 0-64 English letters, digits, and underscores", "start_param": "If the startapp query string parameter is present in the direct Mini App deep link, pass it to start_param.", @@ -13628,15 +16159,30 @@ "user_id": "User ID" } }, + "messages.RequestMainWebView": { + "desc": "Open a Main Mini App.", + "params": { + "bot": "Bot that owns the main mini app.", + "compact": "If set, requests to open the mini app in compact mode (as opposed to normal or fullscreen mode). Must be set if the mode parameter of the Main Mini App link is equal to compact.", + "flags": "Flags, see TL conditional fields", + "fullscreen": "If set, requests to open the mini app in fullscreen mode (as opposed to compact or normal mode). Must be set if the mode parameter of the Main Mini App link is equal to fullscreen.", + "peer": "Currently open chat, may be inputPeerEmpty if no chat is currently open.", + "platform": "Short name of the application; 0-64 English letters, digits, and underscores", + "start_param": "Start parameter, if opening from a Main Mini App link \u00bb.", + "theme_params": "Theme parameters \u00bb" + } + }, "messages.RequestSimpleWebView": { "desc": "Open a bot mini app.", "params": { "bot": "Bot that owns the mini app", + "compact": "Deprecated.", "flags": "Flags, see TL conditional fields", - "from_side_menu": "Set this flag if opening the Mini App from the installed side menu entry \u00bb or from a Mini App link \u00bb.", + "from_side_menu": "Set this flag if opening the Mini App from the installed side menu entry \u00bb.", "from_switch_webview": "Whether the webapp was opened by clicking on the switch_webview button shown on top of the inline results list returned by messages.getInlineBotResults.", + "fullscreen": "Requests to open the app in fullscreen mode.", "platform": "Short name of the application; 0-64 English letters, digits, and underscores", - "start_param": "Start parameter, if opening from a Mini App link \u00bb.", + "start_param": "Deprecated.", "theme_params": "Theme parameters \u00bb", "url": "Web app URL, if opening from a keyboard button or inline result" } @@ -13646,6 +16192,7 @@ "params": { "button_id": "The ID of the button with the authorization request", "flags": "Flags, see TL conditional fields", + "in_app_origin": "", "msg_id": "The message", "peer": "Peer where the message is located", "url": "URL used for link URL authorization, click here for more info \u00bb" @@ -13655,8 +16202,10 @@ "desc": "Open a bot mini app, sending over user information after user confirmation.", "params": { "bot": "Bot that owns the web app", + "compact": "If set, requests to open the mini app in compact mode (as opposed to normal or fullscreen mode). Must be set if the mode parameter of the attachment menu deep link is equal to compact.", "flags": "Flags, see TL conditional fields", "from_bot_menu": "Whether the webview was opened by clicking on the bot's menu button \u00bb.", + "fullscreen": "If set, requests to open the mini app in fullscreen mode (as opposed to normal or compact mode). Must be set if the mode parameter of the attachment menu deep link is equal to fullscreen.", "peer": "Dialog where the web app is being opened, and where the resulting message will be sent (see the docs for more info \u00bb).", "platform": "Short name of the application; 0-64 English letters, digits, and underscores", "reply_to": "If set, indicates that the inline message that will be sent by the bot on behalf of the user once the web app interaction is terminated should be sent in reply to the specified message or story.", @@ -13677,6 +16226,7 @@ "messages.SaveDraft": { "desc": "Save a message draft associated to a chat.", "params": { + "effect": "Specifies a message effect \u00bb to use for the message.", "entities": "Message entities for styled text", "flags": "Flags, see TL conditional fields", "invert_media": "If set, any eventual webpage preview will be shown on top of the message instead of at the bottom.", @@ -13684,7 +16234,8 @@ "message": "The draft", "no_webpage": "Disable generation of the webpage preview", "peer": "Destination of the message that should be sent", - "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story." + "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story.", + "suggested_post": "Used to suggest a post to a channel, see here \u00bb for more info on the full flow." } }, "messages.SaveGif": { @@ -13694,6 +16245,15 @@ "unsave": "Whether to remove GIF from saved gifs list" } }, + "messages.SavePreparedInlineMessage": { + "desc": "Save a prepared inline message, to be shared by the user of the mini app using a web_app_send_prepared_message event", + "params": { + "flags": "Flags, see TL conditional fields", + "peer_types": "Types of chats where this message can be sent", + "result": "The message", + "user_id": "The user to whom the web_app_send_prepared_message event event will be sent" + } + }, "messages.SaveRecentSticker": { "desc": "Add/remove sticker from recent stickers list", "params": { @@ -13711,7 +16271,7 @@ "flags": "Flags, see TL conditional fields", "from_id": "Only return messages sent by the specified user ID", "hash": "Hash", - "limit": "Number of results to return", + "limit": "Number of results to return, can be 0 to only return the message counter.", "max_date": "If a positive value was transferred, only messages with a sending date smaller than the transferred one will be returned", "max_id": "Maximum message ID to return", "min_date": "If a positive value was transferred, only messages with a sending date bigger than the transferred one will be returned", @@ -13720,7 +16280,7 @@ "peer": "User or chat, histories with which are searched, or (inputPeerEmpty) constructor to search in all private chats and normal groups (not channels) \u00bb. Use messages.searchGlobal to search globally in all chats, groups, supergroups and channels.", "q": "Text search request", "saved_peer_id": "Search within the saved message dialog \u00bb with this ID.", - "saved_reaction": "", + "saved_reaction": "You may search for saved messages tagged \u00bb with one or more reactions using this flag.", "top_msg_id": "Thread ID" } }, @@ -13728,7 +16288,7 @@ "desc": "Look for custom emojis associated to a UTF8 emoji", "params": { "emoticon": "The emoji", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the emojiList.hash field returned by a previous call to the method, or pass 0 if this is the first call." + "hash": "Hash used for caching, for more info click here." } }, "messages.SearchEmojiStickerSets": { @@ -13736,24 +16296,26 @@ "params": { "exclude_featured": "Exclude featured stickersets from results", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.foundStickerSets.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "q": "Query string" } }, "messages.SearchGlobal": { "desc": "Search for messages and peers globally", "params": { - "broadcasts_only": "", + "broadcasts_only": "If set, only returns results from channels (used in the global channel search tab \u00bb).", "filter": "Global search filter", "flags": "Flags, see TL conditional fields", "folder_id": "Peer folder ID, for more info click here", + "groups_only": "Whether to search only in groups", "limit": "Offsets for pagination, for more info click here", "max_date": "If a positive value was transferred, the method will return only messages with date smaller than max_date", "min_date": "If a positive value was specified, the method will return only messages with date bigger than min_date", "offset_id": "Offsets for pagination, for more info click here", "offset_peer": "Offsets for pagination, for more info click here", - "offset_rate": "Initially 0, then set to the next_rate parameter of messages.messagesSlice", - "q": "Query" + "offset_rate": "Initially 0, then set to the next_rate parameter of messages.messagesSlice, or if that is absent, the date of the last returned message.", + "q": "Query", + "users_only": "Whether to search only in private chats" } }, "messages.SearchSentMedia": { @@ -13769,10 +16331,23 @@ "params": { "exclude_featured": "Exclude featured stickersets from results", "flags": "Flags, see TL conditional fields", - "hash": "Hash for pagination, for more info click here.Note: the usual hash generation algorithm cannot be used in this case, please re-use the messages.foundStickerSets.hash field returned by a previous call to the method, or pass 0 if this is the first call.", + "hash": "Hash used for caching, for more info click here.", "q": "Query string" } }, + "messages.SearchStickers": { + "desc": "Search for stickers using AI-powered keyword search", + "params": { + "emojis": "If set, returns custom emoji stickers", + "emoticon": "Space-separated list of emojis to search for", + "flags": "Flags, see TL conditional fields", + "hash": "Hash used for caching, for more info click here. The hash may be generated locally by using the ids of the returned or stored sticker documents.", + "lang_code": "List of possible IETF language tags of the user's input language; may be empty if unknown", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination", + "q": "The search term" + } + }, "messages.SendBotRequestedPeer": { "desc": "Send one or more chosen peers, as requested by a keyboardButtonRequestPeer button.", "params": { @@ -13814,6 +16389,7 @@ "messages.SendInlineBotResult": { "desc": "Send a result obtained using messages.getInlineBotResults.", "params": { + "allow_paid_stars": "For paid messages \u00bb, specifies the amount of Telegram Stars the user has agreed to pay in order to send the message.", "background": "Whether to send the message in background", "clear_draft": "Whether to clear the draft", "flags": "Flags, see TL conditional fields", @@ -13821,7 +16397,7 @@ "id": "Result ID from messages.getInlineBotResults", "peer": "Destination", "query_id": "Query ID from messages.getInlineBotResults", - "quick_reply_shortcut": "", + "quick_reply_shortcut": "Add the message to the specified quick reply shortcut \u00bb, instead.", "random_id": "Random ID to avoid resending the same query", "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story.", "schedule_date": "Scheduled message date for scheduled messages", @@ -13832,9 +16408,11 @@ "messages.SendMedia": { "desc": "Send a media", "params": { + "allow_paid_floodskip": "Bots only: if set, allows sending up to 1000 messages per second, ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.", + "allow_paid_stars": "For paid messages \u00bb, specifies the amount of Telegram Stars the user has agreed to pay in order to send the message.", "background": "Send message in background", "clear_draft": "Clear the draft", - "effect": "", + "effect": "Specifies a message effect \u00bb to use for the message.", "entities": "Message entities for styled text", "flags": "Flags, see TL conditional fields", "invert_media": "If set, any eventual webpage preview will be shown on top of the message instead of at the bottom.", @@ -13842,22 +16420,26 @@ "message": "Caption", "noforwards": "Only for bots, disallows forwarding and saving of the messages, even if the destination chat doesn't have content protection enabled", "peer": "Destination", - "quick_reply_shortcut": "", + "quick_reply_shortcut": "Add the message to the specified quick reply shortcut \u00bb, instead.", "random_id": "Random ID to avoid resending the same message", "reply_markup": "Reply markup for bot keyboards", "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story.", "schedule_date": "Scheduled message date for scheduled messages", + "schedule_repeat_period": "", "send_as": "Send this message as the specified peer", "silent": "Send message silently (no notification should be triggered)", + "suggested_post": "Used to suggest a post to a channel, see here \u00bb for more info on the full flow.", "update_stickersets_order": "Whether to move used stickersets to top, see here for more info on this flag \u00bb" } }, "messages.SendMessage": { "desc": "Sends a message to a chat", "params": { + "allow_paid_floodskip": "Bots only: if set, allows sending up to 1000 messages per second, ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.", + "allow_paid_stars": "For paid messages \u00bb, specifies the amount of Telegram Stars the user has agreed to pay in order to send the message.", "background": "Send this message as background message", "clear_draft": "Clear the draft field", - "effect": "", + "effect": "Specifies a message effect \u00bb to use for the message.", "entities": "Message entities for sending styled text", "flags": "Flags, see TL conditional fields", "invert_media": "If set, any eventual webpage preview will be shown on top of the message instead of at the bottom.", @@ -13865,28 +16447,32 @@ "no_webpage": "Set this flag to disable generation of the webpage preview", "noforwards": "Only for bots, disallows forwarding and saving of the messages, even if the destination chat doesn't have content protection enabled", "peer": "The destination where the message will be sent", - "quick_reply_shortcut": "", + "quick_reply_shortcut": "Add the message to the specified quick reply shortcut \u00bb, instead.", "random_id": "Unique client message ID required to prevent message resending", "reply_markup": "Reply markup for sending bot buttons", "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story. Also used to quote other messages.", "schedule_date": "Scheduled message date for scheduled messages", + "schedule_repeat_period": "", "send_as": "Send this message as the specified peer", "silent": "Send this message silently (no notifications for the receivers)", + "suggested_post": "Used to suggest a post to a channel, see here \u00bb for more info on the full flow.", "update_stickersets_order": "Whether to move used stickersets to top, see here for more info on this flag \u00bb" } }, "messages.SendMultiMedia": { "desc": "Send an album or grouped media", "params": { + "allow_paid_floodskip": "Bots only: if set, allows sending up to 1000 messages per second, ignoring broadcasting limits for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.", + "allow_paid_stars": "For paid messages \u00bb, specifies the amount of Telegram Stars the user has agreed to pay in order to send the message.", "background": "Send in background?", "clear_draft": "Whether to clear drafts", - "effect": "", + "effect": "Specifies a message effect \u00bb to use for the message.", "flags": "Flags, see TL conditional fields", "invert_media": "If set, any eventual webpage preview will be shown on top of the message instead of at the bottom.", "multi_media": "The medias to send: note that they must be separately uploaded using messages.uploadMedia first, using raw inputMediaUploaded* constructors is not supported.", "noforwards": "Only for bots, disallows forwarding and saving of the messages, even if the destination chat doesn't have content protection enabled", "peer": "The destination chat", - "quick_reply_shortcut": "", + "quick_reply_shortcut": "Add the message to the specified quick reply shortcut \u00bb, instead.", "reply_to": "If set, indicates that the message should be sent in reply to the specified message or story.", "schedule_date": "Scheduled message date for scheduled messages", "send_as": "Send this message as the specified peer", @@ -13894,13 +16480,24 @@ "update_stickersets_order": "Whether to move used stickersets to top, see here for more info on this flag \u00bb" } }, + "messages.SendPaidReaction": { + "desc": "Sends one or more paid Telegram Star reactions \u00bb, transferring Telegram Stars \u00bb to a channel's balance.", + "params": { + "count": "The number of stars to send (each will increment the reaction counter by one).", + "flags": "Flags, see TL conditional fields", + "msg_id": "The message to react to", + "peer": "The channel", + "private": "Each post with star reactions has a leaderboard with the top senders, but users can opt out of appearing there if they prefer more privacy. Not populating this field will use the default reaction privacy, stored on the server and synced to clients using updatePaidReactionPrivacy (see here for more info).", + "random_id": "Unique client message ID required to prevent message resending. Note: this argument must be composed of a 64-bit integer where the lower 32 bits are random, and the higher 32 bits are equal to the current unixtime, i.e. uint64_t random_id = (time() << 32) | ((uint64_t)random_uint32_t()): this differs from the random_id format of all other methods in the API, which just take 64 random bits." + } + }, "messages.SendQuickReplyMessages": { - "desc": "{schema}", + "desc": "Send a quick reply shortcut \u00bb.", "params": { - "id": "", - "peer": "", - "random_id": "", - "shortcut_id": "" + "id": "Specify a subset of messages from the shortcut to send; if empty, defaults to all of them.", + "peer": "The peer where to send the shortcut (users only, for now).", + "random_id": "Unique client IDs required to prevent message resending, one for each message we're sending, may be empty (but not recommended).", + "shortcut_id": "The ID of the quick reply shortcut to send." } }, "messages.SendReaction": { @@ -13911,7 +16508,7 @@ "flags": "Flags, see TL conditional fields", "msg_id": "Message ID to react to", "peer": "Peer", - "reaction": "A list of reactions" + "reaction": "A list of reactions (doesn't accept reactionPaid constructors, use messages.sendPaidReaction to send paid reactions, instead)." } }, "messages.SendScheduledMessages": { @@ -13987,15 +16584,16 @@ "params": { "available_reactions": "Allowed reaction emojis", "flags": "Flags, see TL conditional fields", + "paid_enabled": "If this flag is set and a Bool is passed, the method will enable or disable paid message reactions \u00bb. If this flag is not set, the previously stored setting will not be changed.", "peer": "Group where to apply changes", - "reactions_limit": "" + "reactions_limit": "This flag may be used to impose a custom limit of unique reactions (i.e. a customizable version of appConfig.reactions_uniq_max); this field and the other info set by the method will then be available to users in channelFull and chatFull. If this flag is not set, the previously configured reactions_limit will not be altered." } }, "messages.SetChatTheme": { - "desc": "Change the chat theme of a certain chat", + "desc": "Change the chat theme of a certain chat, see here \u00bb for more info.", "params": { - "emoticon": "Emoji, identifying a specific chat theme; a list of chat themes can be fetched using account.getChatThemes", - "peer": "Private chat where to change theme" + "peer": "Private chat where to change theme", + "theme": "The theme to set." } }, "messages.SetChatWallPaper": { @@ -14013,7 +16611,7 @@ "messages.SetDefaultHistoryTTL": { "desc": "Changes the default value of the Time-To-Live setting, applied to all new chats.", "params": { - "period": "The new default Time-To-Live of all messages sent in new chats." + "period": "The new default Time-To-Live of all messages sent in new chats, in seconds." } }, "messages.SetDefaultReaction": { @@ -14098,6 +16696,15 @@ "peer": "The Telegram chat where the messages should be imported, click here for more info \u00bb" } }, + "messages.SummarizeText": { + "desc": "{schema}", + "params": { + "flags": "Flags, see TL conditional fields", + "id": "", + "peer": "", + "to_lang": "" + } + }, "messages.ToggleBotInAttachMenu": { "desc": "Enable or disable web bot attachment menu \u00bb", "params": { @@ -14108,9 +16715,9 @@ } }, "messages.ToggleDialogFilterTags": { - "desc": "{schema}", + "desc": "Enable or disable folder tags \u00bb.", "params": { - "enabled": "" + "enabled": "Enable or disable folder tags." } }, "messages.ToggleDialogPin": { @@ -14125,7 +16732,17 @@ "desc": "Enable or disable content protection on a channel or chat", "params": { "enabled": "Enable or disable content protection", - "peer": "The chat or channel" + "flags": "Flags, see TL conditional fields", + "peer": "The chat or channel", + "request_msg_id": "" + } + }, + "messages.TogglePaidReactionPrivacy": { + "desc": "Changes the privacy of already sent paid reactions on a specific message.", + "params": { + "msg_id": "The ID of the message to which we sent the paid reactions", + "peer": "The channel", + "private": "If true, makes the current anonymous in the top sender leaderboard for this message; otherwise, does the opposite." } }, "messages.TogglePeerTranslations": { @@ -14154,6 +16771,26 @@ "uninstall": "Uninstall the specified stickersets" } }, + "messages.ToggleSuggestedPostApproval": { + "desc": "Approve or reject a suggested post \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "msg_id": "ID of the suggestion message.", + "peer": "Both for users and channels, must contain the ID of the direct messages monoforum \u00bb (for channels, the topic ID is extracted automatically from the msg_id).", + "reject": "Reject the suggested post.", + "reject_comment": "Optional comment for rejections (can only be used if reject is set).", + "schedule_date": "Custom scheduling date." + } + }, + "messages.ToggleTodoCompleted": { + "desc": "Mark one or more items of a todo list \u00bb as completed or not completed.", + "params": { + "completed": "Items to mark as completed.", + "incompleted": "Items to mark as not completed.", + "msg_id": "ID of the message with the todo list.", + "peer": "Peer where the todo list was posted." + } + }, "messages.TranscribeAudio": { "desc": "Transcribe voice message", "params": { @@ -14182,6 +16819,7 @@ "params": { "flags": "Flags, see TL conditional fields", "peer": "Chat where to unpin", + "saved_peer_id": "If set, must be equal to the ID of a monoforum topic, and will unpin all messages pinned in the passed monoforum topic.", "top_msg_id": "Forum topic where to unpin" } }, @@ -14199,6 +16837,14 @@ "order": "New folder order" } }, + "messages.UpdatePinnedForumTopic": { + "desc": "{schema}", + "params": { + "peer": "", + "pinned": "", + "topic_id": "" + } + }, "messages.UpdatePinnedMessage": { "desc": "Pin a message", "params": { @@ -14219,7 +16865,7 @@ } }, "messages.UploadEncryptedFile": { - "desc": "Upload encrypted file and associate it to a secret chat", + "desc": "Upload encrypted file and associate it to a secret chat (without actually sending it to the chat).", "params": { "file": "The file", "peer": "The secret chat to associate the file to" @@ -14237,12 +16883,18 @@ "messages.UploadMedia": { "desc": "Upload a file and associate it to a chat (without actually sending it to the chat)", "params": { - "business_connection_id": "", + "business_connection_id": "Whether the media will be used only in the specified business connection \u00bb, and not directly by the bot.", "flags": "Flags, see TL conditional fields", "media": "File uploaded in chunks as described in files \u00bb", "peer": "The chat, can be inputPeerEmpty for bots and inputPeerSelf for users." } }, + "messages.ViewSponsoredMessage": { + "desc": "Mark a specific sponsored message \u00bb as read", + "params": { + "random_id": "The ad's unique ID." + } + }, "payments.ApplyGiftCode": { "desc": "Apply a Telegram Premium giftcode \u00bb", "params": { @@ -14263,10 +16915,34 @@ "receipt": "Receipt" } }, - "payments.CanPurchasePremium": { - "desc": "Checks whether Telegram Premium purchase is possible. Must be called before in-store Premium purchase, official apps only.", + "payments.BotCancelStarsSubscription": { + "desc": "Cancel a bot subscription", + "params": { + "charge_id": "The provider_charge_id from the messageActionPaymentSentMe service message sent to the bot for the first subscription payment.", + "flags": "Flags, see TL conditional fields", + "restore": "If not set, disables autorenewal of the subscriptions, and prevents the user from reactivating the subscription once the current period expires: a subscription cancelled by the bot will have the starsSubscription.bot_canceled flag set. The bot can can partially undo this operation by setting this flag: this will allow the user to reactivate the subscription.", + "user_id": "The ID of the user whose subscription should be (un)cancelled" + } + }, + "payments.CanPurchaseStore": { + "desc": "Checks whether a purchase is possible. Must be called before in-store purchase, official apps only.", "params": { - "purpose": "Payment purpose" + "purpose": "Payment purpose." + } + }, + "payments.ChangeStarsSubscription": { + "desc": "Activate or deactivate a Telegram Star subscription \u00bb.", + "params": { + "canceled": "Whether to cancel or reactivate the subscription.", + "flags": "Flags, see TL conditional fields", + "peer": "Always pass inputPeerSelf.", + "subscription_id": "ID of the subscription." + } + }, + "payments.CheckCanSendGift": { + "desc": "Check if the specified gift \u00bb can be sent.", + "params": { + "gift_id": "Gift ID." } }, "payments.CheckGiftCode": { @@ -14283,18 +16959,93 @@ "info": "Clear the last order settings saved by the user" } }, + "payments.ConnectStarRefBot": { + "desc": "Join a bot's affiliate program, becoming an affiliate \u00bb", + "params": { + "bot": "The bot that offers the affiliate program", + "peer": "The peer that will become the affiliate: star commissions will be transferred to this peer's star balance." + } + }, + "payments.ConvertStarGift": { + "desc": "Convert a received gift \u00bb into Telegram Stars: this will permanently destroy the gift, converting it into starGift.convert_stars Telegram Stars, added to the user's balance.", + "params": { + "stargift": "The gift to convert." + } + }, + "payments.CraftStarGift": { + "desc": "{schema}", + "params": { + "stargift": "" + } + }, + "payments.CreateStarGiftCollection": { + "desc": "Create a star gift collection \u00bb.", + "params": { + "peer": "Peer where to create the collection.", + "stargift": "Gifts added to the collection.", + "title": "Title of the collection." + } + }, + "payments.DeleteStarGiftCollection": { + "desc": "Delete a star gift collection \u00bb.", + "params": { + "collection_id": "ID of the collection.", + "peer": "Peer that owns the collection." + } + }, + "payments.EditConnectedStarRefBot": { + "desc": "Leave a bot's affiliate program \u00bb", + "params": { + "flags": "Flags, see TL conditional fields", + "link": "The affiliate link to revoke", + "peer": "The peer that was affiliated", + "revoked": "If set, leaves the bot's affiliate program" + } + }, "payments.ExportInvoice": { "desc": "Generate an invoice deep link", "params": { "invoice_media": "Invoice" } }, + "payments.FulfillStarsSubscription": { + "desc": "Re-join a private channel associated to an active Telegram Star subscription \u00bb.", + "params": { + "peer": "Always pass inputPeerSelf.", + "subscription_id": "ID of the subscription." + } + }, "payments.GetBankCardData": { "desc": "Get info about a credit card", "params": { "number": "Credit card number" } }, + "payments.GetConnectedStarRefBot": { + "desc": "Fetch info about a specific bot affiliation \u00bb", + "params": { + "bot": "The bot that offers the affiliate program", + "peer": "The affiliated peer" + } + }, + "payments.GetConnectedStarRefBots": { + "desc": "Fetch all affiliations we have created for a certain peer", + "params": { + "flags": "Flags, see TL conditional fields", + "limit": "Maximum number of results to return, see pagination", + "offset_date": "If set, returns only results older than the specified unixtime", + "offset_link": "Offset for pagination, taken from the last returned connectedBotStarRef.url (initially empty)", + "peer": "The affiliated peer" + } + }, + "payments.GetCraftStarGifts": { + "desc": "{schema}", + "params": { + "gift_id": "", + "limit": "Maximum number of results to return, see pagination", + "offset": "" + } + }, "payments.GetGiveawayInfo": { "desc": "Obtain information about a Telegram Premium giveaway \u00bb.", "params": { @@ -14307,7 +17058,7 @@ "params": { "flags": "Flags, see TL conditional fields", "invoice": "Invoice", - "theme_params": "A JSON object with the following keys, containing color theme information (integers, RGB24) to pass to the payment provider, to apply in eventual verification pages: bg_color - Background color text_color - Text color hint_color - Hint text color link_color - Link color button_color - Button color button_text_color - Button text color" + "theme_params": "Theme parameters \u00bb" } }, "payments.GetPaymentReceipt": { @@ -14324,28 +17075,202 @@ "flags": "Flags, see TL conditional fields" } }, + "payments.GetResaleStarGifts": { + "desc": "Get collectible gifts of a specific type currently on resale, see here \u00bb for more info.", + "params": { + "attributes": "Optionally filter gifts with the specified attributes. If no attributes of a specific type are specified, all attributes of that type are allowed.", + "attributes_hash": "If a previous call to the method was made and payments.resaleStarGifts.attributes_hash was set, pass it here to avoid returning any results if they haven't changed. Otherwise, set this flag and pass 0 to return payments.resaleStarGifts.attributes_hash and payments.resaleStarGifts.attributes, these two fields will not be set if this flag is not set.", + "flags": "Flags, see TL conditional fields", + "for_craft": "", + "gift_id": "Mandatory identifier of the base gift from which the collectible gift was upgraded.", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination. If not equal to an empty string, payments.resaleStarGifts.counters will not be set to avoid returning the counters every time a new page is fetched.", + "sort_by_num": "Sort gifts by number (ascending).", + "sort_by_price": "Sort gifts by price (ascending).", + "stars_only": "" + } + }, "payments.GetSavedInfo": { "desc": "Get saved payment information", "params": {} }, - "payments.GetStarsStatus": { + "payments.GetSavedStarGift": { + "desc": "Fetch info about specific gifts owned by a peer we control.", + "params": { + "stargift": "List of gifts to fetch info about." + } + }, + "payments.GetSavedStarGifts": { + "desc": "Fetch the full list of gifts owned by a peer.", + "params": { + "collection_id": "Only returns gifts within the specified collection \u00bb.", + "exclude_hosted": "", + "exclude_saved": "Exclude gifts pinned on the profile.", + "exclude_unique": "Exclude collectible gifts \u00bb.", + "exclude_unlimited": "Exclude gifts that do not have the starGift.limited flag set.", + "exclude_unsaved": "Exclude gifts not pinned on the profile.", + "exclude_unupgradable": "Exclude gifts that cannot be upgraded to collectible gifts \u00bb.", + "exclude_upgradable": "Exclude gifts that can be upgraded to collectible gifts \u00bb.", + "flags": "Flags, see TL conditional fields", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination.", + "peer": "Fetch only gifts owned by the specified peer, such as: a user, with peer=inputPeerUser; a channel, with peer=inputPeerChannel; a connected business user (when executing the method as a bot, over the business connection), with peer=inputPeerUser.", + "peer_color_available": "", + "sort_by_value": "If set, sorts the gifts by price instead of reception date." + } + }, + "payments.GetStarGiftActiveAuctions": { "desc": "{schema}", "params": { - "peer": "" + "hash": "" } }, - "payments.GetStarsTopupOptions": { + "payments.GetStarGiftAuctionAcquiredGifts": { + "desc": "{schema}", + "params": { + "gift_id": "" + } + }, + "payments.GetStarGiftAuctionState": { "desc": "{schema}", + "params": { + "auction": "", + "version": "" + } + }, + "payments.GetStarGiftCollections": { + "desc": "Fetches all star gift collections \u00bb of a peer.", + "params": { + "hash": "Hash (generated as specified here \u00bb) using the starGiftCollection.hash field (not the collection_id field) of all collections returned by a previous method call, to avoid refetching the result if it hasn't changed.", + "peer": "The peer." + } + }, + "payments.GetStarGiftUpgradeAttributes": { + "desc": "{schema}", + "params": { + "gift_id": "" + } + }, + "payments.GetStarGiftUpgradePreview": { + "desc": "Obtain a preview of the possible attributes (chosen randomly) a gift \u00bb can receive after upgrading it to a collectible gift \u00bb, see here \u00bb for more info.", + "params": { + "gift_id": "The gift to upgrade." + } + }, + "payments.GetStarGiftWithdrawalUrl": { + "desc": "Convert a collectible gift \u00bb to an NFT on the TON blockchain.", + "params": { + "password": "The current user's 2FA password, passed as specified here \u00bb.", + "stargift": "The collectible gift to export." + } + }, + "payments.GetStarGifts": { + "desc": "Get a list of available gifts, see here \u00bb for more info.", + "params": { + "hash": "Hash used for caching, for more info click here.The hash may be generated locally by using the ids of the returned or stored sticker starGifts." + } + }, + "payments.GetStarsGiftOptions": { + "desc": "Obtain a list of Telegram Stars gift options \u00bb as starsGiftOption constructors.", + "params": { + "flags": "Flags, see TL conditional fields", + "user_id": "Receiver of the gift (optional)." + } + }, + "payments.GetStarsGiveawayOptions": { + "desc": "Fetch a list of star giveaway options \u00bb.", + "params": {} + }, + "payments.GetStarsRevenueAdsAccountUrl": { + "desc": "Returns a URL for a Telegram Ad platform account that can be used to set up advertisements for channel/bot in peer, paid using the Telegram Stars owned by the specified peer, see here \u00bb for more info.", + "params": { + "peer": "Channel or bot that owns the stars." + } + }, + "payments.GetStarsRevenueStats": { + "desc": "Get Telegram Star revenue statistics \u00bb.", + "params": { + "dark": "Whether to enable dark theme for graph colors", + "flags": "Flags, see TL conditional fields", + "peer": "Get statistics for the specified bot, channel or ourselves (inputPeerSelf).", + "ton": "If set, fetches channel/bot ad revenue statistics in TON." + } + }, + "payments.GetStarsRevenueWithdrawalUrl": { + "desc": "Withdraw funds from a channel or bot's star balance \u00bb.", + "params": { + "amount": "The amount of stars or nanotons to withdraw.", + "flags": "Flags, see TL conditional fields", + "password": "2FA password, see here \u00bb for more info.", + "peer": "Channel or bot from which to withdraw funds.", + "ton": "If set, withdraws channel/ad revenue in TON." + } + }, + "payments.GetStarsStatus": { + "desc": "Get the current Telegram Stars balance of the current account (with peer=inputPeerSelf), or the stars balance of the bot specified in peer.", + "params": { + "flags": "Flags, see TL conditional fields", + "peer": "Peer of which to get the balance.", + "ton": "If set, returns the channel/ad revenue balance in nanotons." + } + }, + "payments.GetStarsSubscriptions": { + "desc": "Obtain a list of active, expired or cancelled Telegram Star subscriptions \u00bb.", + "params": { + "flags": "Flags, see TL conditional fields", + "missing_balance": "Whether to return only subscriptions expired due to an excessively low Telegram Star balance.", + "offset": "Offset for pagination, taken from payments.starsStatus.subscriptions_next_offset.", + "peer": "Always pass inputPeerSelf." + } + }, + "payments.GetStarsTopupOptions": { + "desc": "Obtain a list of Telegram Stars topup options \u00bb as starsTopupOption constructors.", "params": {} }, "payments.GetStarsTransactions": { - "desc": "{schema}", + "desc": "Fetch Telegram Stars transactions.", "params": { + "ascending": "Return transactions in ascending order by date (instead of descending order by date).", "flags": "Flags, see TL conditional fields", - "inbound": "", - "offset": "", - "outbound": "", - "peer": "" + "inbound": "If set, fetches only incoming transactions.", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination, obtained from the returned next_offset, initially an empty string \u00bb.", + "outbound": "If set, fetches only outgoing transactions.", + "peer": "Fetch the transaction history of the peer (inputPeerSelf or a bot we own).", + "subscription_id": "If set, fetches only transactions for the specified Telegram Star subscription \u00bb.", + "ton": "If set, returns the channel/ad revenue transactions in nanotons, instead." + } + }, + "payments.GetStarsTransactionsByID": { + "desc": "Obtain info about Telegram Star transactions \u00bb using specific transaction IDs.", + "params": { + "flags": "Flags, see TL conditional fields", + "id": "Transaction IDs.", + "peer": "Channel or bot.", + "ton": "If set, returns channel/bot ad revenue transactions in nanotons." + } + }, + "payments.GetSuggestedStarRefBots": { + "desc": "Obtain a list of suggested mini apps with available affiliate programs", + "params": { + "flags": "Flags, see TL conditional fields", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination, taken from payments.suggestedStarRefBots.next_offset, initially empty.", + "order_by_date": "If set, orders results by the creation date of the affiliate program", + "order_by_revenue": "If set, orders results by the expected revenue", + "peer": "The peer that will become the affiliate: star commissions will be transferred to this peer's star balance." + } + }, + "payments.GetUniqueStarGift": { + "desc": "Obtain info about a collectible gift \u00bb using a slug obtained from a collectible gift link \u00bb.", + "params": { + "slug": "The slug." + } + }, + "payments.GetUniqueStarGiftValueInfo": { + "desc": "Get information about the value of a collectible gift \u00bb.", + "params": { + "slug": "slug from a starGiftUnique." } }, "payments.LaunchPrepaidGiveaway": { @@ -14357,10 +17282,33 @@ } }, "payments.RefundStarsCharge": { + "desc": "Refund a Telegram Stars transaction, see here \u00bb for more info.", + "params": { + "charge_id": "Transaction ID.", + "user_id": "User to refund." + } + }, + "payments.ReorderStarGiftCollections": { + "desc": "Reorder the star gift collections \u00bb on an owned peer's profile.", + "params": { + "order": "New collection order.", + "peer": "The owned peer." + } + }, + "payments.ResolveStarGiftOffer": { "desc": "{schema}", "params": { - "charge_id": "", - "user_id": "" + "decline": "", + "flags": "Flags, see TL conditional fields", + "offer_msg_id": "" + } + }, + "payments.SaveStarGift": { + "desc": "Display or remove a received gift \u00bb from our profile.", + "params": { + "flags": "Flags, see TL conditional fields", + "stargift": "The gift to display or remove.", + "unsave": "If set, hides the gift from our profile." } }, "payments.SendPaymentForm": { @@ -14375,12 +17323,72 @@ "tip_amount": "Tip, in the smallest units of the currency (integer, not float/double). For example, for a price of US$ 1.45 pass amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies)." } }, - "payments.SendStarsForm": { + "payments.SendStarGiftOffer": { "desc": "{schema}", "params": { + "allow_paid_stars": "", + "duration": "", + "flags": "Flags, see TL conditional fields", + "peer": "", + "price": "", + "random_id": "", + "slug": "" + } + }, + "payments.SendStarsForm": { + "desc": "Make a payment using Telegram Stars, see here \u00bb for more info.", + "params": { + "form_id": "Payment form ID", + "invoice": "Invoice" + } + }, + "payments.ToggleChatStarGiftNotifications": { + "desc": "Enables or disables the reception of notifications every time a gift \u00bb is received by the specified channel, can only be invoked by admins with post_messages admin rights.", + "params": { + "enabled": "Whether to enable or disable reception of notifications in the form of messageActionStarGiftUnique and messageActionStarGift service messages from the channel.", + "flags": "Flags, see TL conditional fields", + "peer": "The channel for which to receive or not receive notifications." + } + }, + "payments.ToggleStarGiftsPinnedToTop": { + "desc": "Pins a received gift on top of the profile of the user or owned channels by using payments.toggleStarGiftsPinnedToTop.", + "params": { + "peer": "The peer where to pin the gift.", + "stargift": "The gift to pin." + } + }, + "payments.TransferStarGift": { + "desc": "Transfer a collectible gift to another user or channel: can only be used if transfer is free (i.e. messageActionStarGiftUnique.transfer_stars is not set); see here \u00bb for more info on the full flow (including the different flow to use in case the transfer isn't free).", + "params": { + "stargift": "The gift to transfer.", + "to_id": "Destination peer." + } + }, + "payments.UpdateStarGiftCollection": { + "desc": "Add or remove gifts from a star gift collection \u00bb, or rename the collection.", + "params": { + "add_stargift": "Can contain a list of gifts to add to the collection.", + "collection_id": "Collection ID.", + "delete_stargift": "Can contain a list of gifts to remove from the collection.", "flags": "Flags, see TL conditional fields", - "form_id": "", - "invoice": "" + "order": "Can contain the new gift order.", + "peer": "Peer that owns the collection.", + "title": "Title of the collection, to rename the collection." + } + }, + "payments.UpdateStarGiftPrice": { + "desc": "A collectible gift we own \u00bb can be put up for sale on the gift marketplace \u00bb with this method, see here \u00bb for more info.", + "params": { + "resell_amount": "Resale price of the gift.", + "stargift": "The gift to resell." + } + }, + "payments.UpgradeStarGift": { + "desc": "Upgrade a gift to a collectible gift: can only be used if the upgrade was already paid by the gift sender; see here \u00bb for more info on the full flow (including the different flow to use in case the upgrade was not paid by the gift sender).", + "params": { + "flags": "Flags, see TL conditional fields", + "keep_original_details": "Set this flag to keep the original gift text, sender and receiver in the upgraded gift as a starGiftAttributeOriginalDetails attribute.", + "stargift": "The gift to upgrade" } }, "payments.ValidateRequestedInfo": { @@ -14416,6 +17424,19 @@ "protocol": "Phone call settings" } }, + "phone.CreateConferenceCall": { + "desc": "Create and optionally join a new conference call.", + "params": { + "block": "Initial blockchain block (can only be used if join is set).", + "flags": "Flags, see TL conditional fields", + "join": "If set, also join the call, otherwise just create the call link.", + "muted": "If set, mute our microphone when joining the call (can only be used if join is set).", + "params": "Parameters from tgcalls (can only be used if join is set).", + "public_key": "Public key (can only be used if join is set).", + "random_id": "Unique client message ID required to prevent creation of duplicate group calls.", + "video_stopped": "If set, our video stream is disabled (can only be used if join is set)." + } + }, "phone.CreateGroupCall": { "desc": "Create a group call or livestream", "params": { @@ -14427,6 +17448,41 @@ "title": "Call title" } }, + "phone.DeclineConferenceCallInvite": { + "desc": "Declines a conference call invite.", + "params": { + "msg_id": "The ID of the messageActionConferenceCall to decline." + } + }, + "phone.DeleteConferenceCallParticipants": { + "desc": "Remove participants from a conference call.", + "params": { + "block": "The block containing an appropriate e2e.chain.changeSetGroupState event", + "call": "The conference call.", + "flags": "Flags, see TL conditional fields", + "ids": "IDs of users to remove.", + "kick": "Whether this is a forced removal of active members in a conference call.", + "only_left": "Whether this is a removal of members that already left the conference call." + } + }, + "phone.DeleteGroupCallMessages": { + "desc": "{schema}", + "params": { + "call": "", + "flags": "Flags, see TL conditional fields", + "messages": "", + "report_spam": "" + } + }, + "phone.DeleteGroupCallParticipantMessages": { + "desc": "{schema}", + "params": { + "call": "", + "flags": "Flags, see TL conditional fields", + "participant": "", + "report_spam": "" + } + }, "phone.DiscardCall": { "desc": "Refuse or end running call", "params": { @@ -14484,12 +17540,27 @@ "limit": "Maximum number of results to return, see pagination" } }, + "phone.GetGroupCallChainBlocks": { + "desc": "Fetch the blocks of a conference blockchain \u00bb.", + "params": { + "call": "The conference.", + "limit": "Maximum number of blocks to return in this call, see pagination", + "offset": "Offset for pagination.", + "sub_chain_id": "Subchain ID." + } + }, "phone.GetGroupCallJoinAs": { "desc": "Get a list of peers that can be used to join a group call, presenting yourself as a specific user/channel.", "params": { "peer": "The dialog whose group call or livestream we're trying to join" } }, + "phone.GetGroupCallStars": { + "desc": "{schema}", + "params": { + "call": "" + } + }, "phone.GetGroupCallStreamChannels": { "desc": "Get info about RTMP streams in a group call or livestream.\nThis method should be invoked to the same group/channel-related DC used for downloading livestream chunks.\nAs usual, the media DC is preferred, if available.", "params": { @@ -14499,6 +17570,8 @@ "phone.GetGroupCallStreamRtmpUrl": { "desc": "Get RTMP URL and stream key for RTMP livestreams. Can be used even before creating the actual RTMP livestream with phone.createGroupCall (the rtmp_stream flag must be set).", "params": { + "flags": "Flags, see TL conditional fields", + "live_story": "", "peer": "Peer to livestream into", "revoke": "Whether to revoke the previous stream key or simply return the existing one" } @@ -14513,6 +17586,15 @@ "sources": "If specified, will fetch group participant info about the specified WebRTC source IDs" } }, + "phone.InviteConferenceCallParticipant": { + "desc": "Invite a user to a conference call.", + "params": { + "call": "The conference call.", + "flags": "Flags, see TL conditional fields", + "user_id": "The user to invite.", + "video": "Invite the user to also turn on their video feed." + } + }, "phone.InviteToGroupCall": { "desc": "Invite a set of users to a group call.", "params": { @@ -14523,12 +17605,14 @@ "phone.JoinGroupCall": { "desc": "Join a group call", "params": { + "block": "The block containing an appropriate e2e.chain.changeSetGroupState event.", "call": "The group call", "flags": "Flags, see TL conditional fields", "invite_hash": "The invitation hash from the invite link \u00bb, if provided allows speaking in a livestream or muted group chat.", "join_as": "Join the group call, presenting yourself as the specified user/channel", "muted": "If set, the user will be muted by default upon joining.", "params": "WebRTC parameters", + "public_key": "For conference calls, your public key.", "video_stopped": "If set, the user's video will be disabled by default upon joining." } }, @@ -14590,6 +17674,38 @@ "peer": "The dialog" } }, + "phone.SaveDefaultSendAs": { + "desc": "{schema}", + "params": { + "call": "", + "send_as": "" + } + }, + "phone.SendConferenceCallBroadcast": { + "desc": "Broadcast a blockchain block to all members of a conference call, see here \u00bb for more info.", + "params": { + "block": "The block to broadcast.", + "call": "The conference where to broadcast the block." + } + }, + "phone.SendGroupCallEncryptedMessage": { + "desc": "{schema}", + "params": { + "call": "", + "encrypted_message": "" + } + }, + "phone.SendGroupCallMessage": { + "desc": "{schema}", + "params": { + "allow_paid_stars": "", + "call": "", + "flags": "Flags, see TL conditional fields", + "message": "", + "random_id": "", + "send_as": "" + } + }, "phone.SendSignalingData": { "desc": "Send VoIP signaling data", "params": { @@ -14630,7 +17746,9 @@ "call": "Group call", "flags": "Flags, see TL conditional fields", "join_muted": "Whether all users will that join this group call are muted by default upon joining the group call", - "reset_invite_hash": "Invalidate existing invite links" + "messages_enabled": "", + "reset_invite_hash": "Invalidate existing invite links", + "send_paid_messages_stars": "" } }, "phone.ToggleGroupCallStartSubscription": { @@ -14669,7 +17787,7 @@ "params": { "file": "Profile photo", "flags": "Flags, see TL conditional fields", - "save": "If set, removes a previously set personal profile picture (does not affect suggested profile pictures, to remove them simply deleted the messageActionSuggestProfilePhoto service message with messages.deleteMessages).", + "save": "If set, removes a previously set personal profile picture (does not affect suggested profile pictures, to remove them simply delete the messageActionSuggestProfilePhoto service message with messages.deleteMessages).", "suggest": "If set, will send a messageActionSuggestProfilePhoto service message to user_id, suggesting them to use the specified profile picture; otherwise, will set a personal profile picture for the user (only visible to the current user).", "user_id": "The contact", "video": "Animated profile picture video", @@ -14698,17 +17816,17 @@ } }, "premium.GetBoostsList": { - "desc": "Obtains info about the boosts that were applied to a certain channel (admins only)", + "desc": "Obtains info about the boosts that were applied to a certain channel or supergroup (admins only)", "params": { "flags": "Flags, see TL conditional fields", - "gifts": "Whether to return only info about boosts received from gift codes and giveaways created by the channel \u00bb", + "gifts": "Whether to return only info about boosts received from gift codes and giveaways created by the channel/supergroup \u00bb", "limit": "Maximum number of results to return, see pagination", "offset": "Offset for pagination, obtained from premium.boostsList.next_offset", - "peer": "The channel" + "peer": "The channel/supergroup" } }, "premium.GetBoostsStatus": { - "desc": "Gets the current number of boosts of a channel.", + "desc": "Gets the current number of boosts of a channel/supergroup.", "params": { "peer": "The peer." } @@ -14718,72 +17836,49 @@ "params": {} }, "premium.GetUserBoosts": { - "desc": "Returns the lists of boost that were applied to a channel by a specific user (admins only)", + "desc": "Returns the lists of boost that were applied to a channel/supergroup by a specific user (admins only)", "params": { - "peer": "The channel", + "peer": "The channel/supergroup", "user_id": "The user" } }, "smsjobs.FinishJob": { - "desc": "{schema}", + "desc": "Finish an SMS job (official clients only).", "params": { - "error": "", + "error": "If failed, the error.", "flags": "Flags, see TL conditional fields", - "job_id": "" + "job_id": "Job ID." } }, "smsjobs.GetSmsJob": { - "desc": "{schema}", + "desc": "Get info about an SMS job (official clients only).", "params": { - "job_id": "" + "job_id": "Job ID" } }, "smsjobs.GetStatus": { - "desc": "{schema}", + "desc": "Get SMS jobs status (official clients only).", "params": {} }, "smsjobs.IsEligibleToJoin": { - "desc": "{schema}", + "desc": "Check if we can process SMS jobs (official clients only).", "params": {} }, "smsjobs.Join": { - "desc": "{schema}", + "desc": "Enable SMS jobs (official clients only).", "params": {} }, "smsjobs.Leave": { - "desc": "{schema}", + "desc": "Disable SMS jobs (official clients only).", "params": {} }, "smsjobs.UpdateSettings": { - "desc": "{schema}", - "params": { - "allow_international": "", - "flags": "Flags, see TL conditional fields" - } - }, - "stats.GetBroadcastRevenueStats": { - "desc": "{schema}", + "desc": "Update SMS job settings (official clients only).", "params": { - "channel": "", - "dark": "", + "allow_international": "Allow international numbers?", "flags": "Flags, see TL conditional fields" } }, - "stats.GetBroadcastRevenueTransactions": { - "desc": "{schema}", - "params": { - "channel": "", - "limit": "Maximum number of results to return, see pagination", - "offset": "" - } - }, - "stats.GetBroadcastRevenueWithdrawalUrl": { - "desc": "{schema}", - "params": { - "channel": "", - "password": "" - } - }, "stats.GetBroadcastStats": { "desc": "Get channel statistics", "params": { @@ -14845,14 +17940,14 @@ } }, "stickers.AddStickerToSet": { - "desc": "Add a sticker to a stickerset, bots only. The sticker set must have been created by the bot.", + "desc": "Add a sticker to a stickerset. The sticker set must have been created by the current user/bot.", "params": { "sticker": "The sticker", "stickerset": "The stickerset" } }, "stickers.ChangeSticker": { - "desc": "Update the keywords, emojis or mask coordinates of a sticker, bots only.", + "desc": "Update the keywords, emojis or mask coordinates of a sticker.", "params": { "emoji": "If set, updates the emoji list associated to the sticker", "flags": "Flags, see TL conditional fields", @@ -14862,7 +17957,7 @@ } }, "stickers.ChangeStickerPosition": { - "desc": "Changes the absolute position of a sticker in the set to which it belongs; for bots only. The sticker set must have been created by the bot", + "desc": "Changes the absolute position of a sticker in the set to which it belongs. The sticker set must have been created by the current user/bot.", "params": { "position": "The new position of the sticker, zero-based", "sticker": "The sticker" @@ -14875,7 +17970,7 @@ } }, "stickers.CreateStickerSet": { - "desc": "Create a stickerset, bots only.", + "desc": "Create a stickerset.", "params": { "emojis": "Whether this is a custom emoji stickerset.", "flags": "Flags, see TL conditional fields", @@ -14890,29 +17985,29 @@ } }, "stickers.DeleteStickerSet": { - "desc": "Deletes a stickerset we created, bots only.", + "desc": "Deletes a stickerset we created.", "params": { "stickerset": "Stickerset to delete" } }, "stickers.RemoveStickerFromSet": { - "desc": "Remove a sticker from the set where it belongs, bots only. The sticker set must have been created by the bot.", + "desc": "Remove a sticker from the set where it belongs. The sticker set must have been created by the current user/bot.", "params": { "sticker": "The sticker to remove" } }, "stickers.RenameStickerSet": { - "desc": "Renames a stickerset, bots only.", + "desc": "Renames a stickerset.", "params": { "stickerset": "Stickerset to rename", "title": "New stickerset title" } }, "stickers.ReplaceSticker": { - "desc": "{schema}", + "desc": "Replace a sticker in a stickerset \u00bb.", "params": { - "new_sticker": "", - "sticker": "" + "new_sticker": "New sticker.", + "sticker": "Old sticker document." } }, "stickers.SetStickerSetThumb": { @@ -14944,6 +18039,21 @@ "peer": "The peer from which we wish to post stories." } }, + "stories.CreateAlbum": { + "desc": "Creates a story album.", + "params": { + "peer": "The owned peer where to create the album.", + "stories": "Stories to add to the album.", + "title": "Album name." + } + }, + "stories.DeleteAlbum": { + "desc": "Delete a story album.", + "params": { + "album_id": "ID of the album to delete.", + "peer": "Owned peer where the album is located." + } + }, "stories.DeleteStories": { "desc": "Deletes some posted stories.", "params": { @@ -14971,6 +18081,22 @@ "peer": "Peer where the story was posted" } }, + "stories.GetAlbumStories": { + "desc": "Get stories in a story album \u00bb.", + "params": { + "album_id": "ID of the album.", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination.", + "peer": "Peer where the album is posted." + } + }, + "stories.GetAlbums": { + "desc": "Get story albums created by a peer.", + "params": { + "hash": "The hash from a previously returned stories.albums, to avoid returning any results if they haven't changed.", + "peer": "The peer." + } + }, "stories.GetAllReadPeerStories": { "desc": "Obtain the latest read story ID for all peers when first logging in, returned as a list of updateReadStories updates, see here \u00bb for more info.", "params": {} @@ -15070,13 +18196,31 @@ "peer": "The peer whose stories should be marked as read." } }, + "stories.ReorderAlbums": { + "desc": "Reorder story albums on a profile \u00bb.", + "params": { + "order": "New order of the albums.", + "peer": "Peer where the albums are located." + } + }, "stories.Report": { "desc": "Report a story.", "params": { "id": "IDs of the stories to report.", "message": "Comment for report moderation", - "peer": "The peer that uploaded the story.", - "reason": "Why are these storeis being reported." + "option": "Menu option, intially empty", + "peer": "The peer that uploaded the story." + } + }, + "stories.SearchPosts": { + "desc": "Globally search for stories using a hashtag or a location media area, see here \u00bb for more info on the full flow.", + "params": { + "area": "A mediaAreaGeoPoint or a mediaAreaVenue. Note mediaAreaGeoPoint areas may be searched only if they have an associated address.", + "flags": "Flags, see TL conditional fields", + "hashtag": "Hashtag (without the #)", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination: initially an empty string, then the next_offset from the previously returned stories.foundStories.", + "peer": "If set, returns only stories posted by this peer." } }, "stories.SendReaction": { @@ -15092,6 +18236,7 @@ "stories.SendStory": { "desc": "Uploads a Telegram Story.", "params": { + "albums": "If set, adds the story to the specified albums.", "caption": "Story caption.", "entities": "Message entities for styled text, if allowed by the stories_entities client configuration parameter \u00bb.", "flags": "Flags, see TL conditional fields", @@ -15108,6 +18253,22 @@ "random_id": "Unique client message ID required to prevent message resending." } }, + "stories.StartLive": { + "desc": "{schema}", + "params": { + "caption": "", + "entities": "Message entities for styled text", + "flags": "Flags, see TL conditional fields", + "messages_enabled": "", + "noforwards": "", + "peer": "", + "pinned": "", + "privacy_rules": "", + "random_id": "", + "rtmp_stream": "", + "send_paid_messages_stars": "" + } + }, "stories.ToggleAllStoriesHidden": { "desc": "Hide the active stories of a specific peer, preventing them from being displayed on the action bar on the homescreen.", "params": { @@ -15130,10 +18291,22 @@ } }, "stories.TogglePinnedToTop": { - "desc": "{schema}", + "desc": "Pin some stories to the top of the profile, see here \u00bb for more info.", "params": { - "id": "", - "peer": "" + "id": "IDs of the stories to pin (max stories_pinned_to_top_count_max).", + "peer": "Peer where to pin stories." + } + }, + "stories.UpdateAlbum": { + "desc": "Rename a story albums \u00bb, or add, delete or reorder stories in it.", + "params": { + "add_stories": "If set, adds the specified stories to the album.", + "album_id": "Album ID.", + "delete_stories": "If set, deletes the specified stories from the album.", + "flags": "Flags, see TL conditional fields", + "order": "If set, reorders the stories in the album by their IDs.", + "peer": "Peer where the album is posted.", + "title": "New album title." } }, "updates.GetChannelDifference": { @@ -15234,10 +18407,26 @@ "id": "User ID" } }, - "users.GetIsPremiumRequiredToContact": { - "desc": "{schema}", + "users.GetRequirementsToContact": { + "desc": "Check whether we can write to the specified users, used to implement bulk checks for Premium-only messages \u00bb and paid messages \u00bb.", "params": { - "id": "" + "id": "Users to check." + } + }, + "users.GetSavedMusic": { + "desc": "Get songs pinned to the user's profile, see here \u00bb for more info.", + "params": { + "hash": "Hash \u00bb of the IDs of previously added songs, to avoid returning any result if there was no change.", + "id": "The ID of the user.", + "limit": "Maximum number of results to return, see pagination", + "offset": "Offset for pagination." + } + }, + "users.GetSavedMusicByID": { + "desc": "Check if the passed songs are still pinned to the user's profile, or refresh the file references of songs pinned on a user's profile see here \u00bb for more info.", + "params": { + "documents": "The songs (here, file_reference can be empty to refresh file references).", + "id": "The ID of the user." } }, "users.GetUsers": { @@ -15252,15 +18441,19 @@ "errors": "Errors", "id": "The user" } + }, + "users.SuggestBirthday": { + "desc": "{schema}", + "params": { + "birthday": "", + "id": "" + } } }, "type": { "AccountDaysTTL": { "desc": "Time-to-live of current account" }, - "AppWebViewResult": { - "desc": "Contains the link that must be used to open a direct link Mini App." - }, "AttachMenuBot": { "desc": "Represents a bot mini app that can be launched from the attachment menu \u00bb" }, @@ -15279,6 +18472,9 @@ "AttachMenuPeerType": { "desc": "Indicates a supported peer type for a bot mini app attachment menu" }, + "AuctionBidLevel": { + "desc": "" + }, "Authorization": { "desc": "Represents a logged-in session" }, @@ -15292,7 +18488,7 @@ "desc": "Media autosave settings" }, "AvailableEffect": { - "desc": "" + "desc": "Describes a message effect \u00bb." }, "AvailableReaction": { "desc": "Animations associated with a message reaction" @@ -15304,7 +18500,7 @@ "desc": "Basic theme settings" }, "Birthday": { - "desc": "" + "desc": "Birthday information for a user." }, "Bool": { "desc": "Boolean type." @@ -15315,8 +18511,11 @@ "BotApp": { "desc": "Contains information about a direct link Mini App." }, + "BotAppSettings": { + "desc": "Mini app \u00bb settings" + }, "BotBusinessConnection": { - "desc": "" + "desc": "Contains info about a bot business connection." }, "BotCommand": { "desc": "Describes a bot command that can be used in a chat" @@ -15336,41 +18535,47 @@ "BotMenuButton": { "desc": "Indicates the action to execute when pressing the in-UI menu button for bots" }, - "BroadcastRevenueBalances": { - "desc": "" + "BotPreviewMedia": { + "desc": "Represents a Main Mini App preview media, see here \u00bb for more info." }, - "BroadcastRevenueTransaction": { - "desc": "" + "BotVerification": { + "desc": "Describes a bot verification icon \u00bb." + }, + "BotVerifierSettings": { + "desc": "Info about the current verifier bot \u00bb." }, "BusinessAwayMessage": { - "desc": "" + "desc": "Describes a Telegram Business away message, automatically sent to users writing to us when we're offline, during closing hours, while we're on vacation, or in some other custom time period when we cannot immediately answer to the user." }, "BusinessAwayMessageSchedule": { - "desc": "" + "desc": "Specifies when should the Telegram Business away messages be sent." }, "BusinessBotRecipients": { - "desc": "" + "desc": "Specifies the private chats that a connected business bot \u00bb may receive messages and interact with." + }, + "BusinessBotRights": { + "desc": "Business bot rights." }, "BusinessChatLink": { - "desc": "" + "desc": "Contains info about a business chat deep link \u00bb created by the current account." }, "BusinessGreetingMessage": { - "desc": "" + "desc": "Describes a Telegram Business greeting, automatically sent to new users writing to us in private for the first time, or after a certain inactivity period." }, "BusinessIntro": { - "desc": "" + "desc": "Telegram Business introduction \u00bb." }, "BusinessLocation": { - "desc": "" + "desc": "Represents the location of a Telegram Business \u00bb." }, "BusinessRecipients": { - "desc": "" + "desc": "Specifies the chats that can receive Telegram Business away \u00bb and greeting \u00bb messages." }, "BusinessWeeklyOpen": { - "desc": "" + "desc": "A time interval, indicating the opening hours of a Telegram Business." }, "BusinessWorkHours": { - "desc": "" + "desc": "Specifies a set of Telegram Business opening hours." }, "CdnConfig": { "desc": "Configuration for CDN file downloads." @@ -15435,6 +18640,9 @@ "ChatReactions": { "desc": "Available chat reactions" }, + "ChatTheme": { + "desc": "A chat theme" + }, "CodeSettings": { "desc": "Settings for the code type to send" }, @@ -15442,13 +18650,16 @@ "desc": "Object contains info on API configuring parameters." }, "ConnectedBot": { - "desc": "" + "desc": "Contains info about a connected business bot \u00bb." + }, + "ConnectedBotStarRef": { + "desc": "Info about an active affiliate program we have with a Mini App" }, "Contact": { "desc": "A contact of the current user." }, "ContactBirthday": { - "desc": "" + "desc": "Birthday information of a contact." }, "ContactStatus": { "desc": "Contact status: online / offline." @@ -15474,6 +18685,9 @@ "DialogPeer": { "desc": "Peer, or all peers in a folder" }, + "DisallowedGiftsSettings": { + "desc": "Disallow the reception of specific gift types." + }, "Document": { "desc": "A document." }, @@ -15535,7 +18749,7 @@ "desc": "Represents a story deep link" }, "FactCheck": { - "desc": "" + "desc": "Represents a fact-check \u00bb created by an independent fact-checker." }, "FileHash": { "desc": "Hash of an uploaded file, to be checked for validity after download" @@ -15549,18 +18763,30 @@ "ForumTopic": { "desc": "Contains information about a forum topic" }, + "FoundStory": { + "desc": "A story found using global story search \u00bb." + }, "Game": { "desc": "Indicates an already sent game" }, "GeoPoint": { "desc": "Object defines a GeoPoint." }, + "GeoPointAddress": { + "desc": "Address optionally associated to a geoPoint." + }, "GlobalPrivacySettings": { "desc": "Global privacy settings" }, "GroupCall": { "desc": "A group call" }, + "GroupCallDonor": { + "desc": "" + }, + "GroupCallMessage": { + "desc": "" + }, "GroupCallParticipant": { "desc": "Info about a group call participant" }, @@ -15604,22 +18830,22 @@ "desc": "Inline bot result" }, "InputBusinessAwayMessage": { - "desc": "" + "desc": "Describes a Telegram Business away message, automatically sent to users writing to us when we're offline, during closing hours, while we're on vacation, or in some other custom time period when we cannot immediately answer to the user." }, "InputBusinessBotRecipients": { - "desc": "" + "desc": "Specifies the private chats that a connected business bot \u00bb may interact with." }, "InputBusinessChatLink": { - "desc": "" + "desc": "Contains info about a business chat deep link \u00bb to be created by the current account." }, "InputBusinessGreetingMessage": { - "desc": "" + "desc": "Describes a Telegram Business greeting, automatically sent to new users writing to us in private for the first time, or after a certain inactivity period." }, "InputBusinessIntro": { - "desc": "" + "desc": "Telegram Business introduction \u00bb." }, "InputBusinessRecipients": { - "desc": "" + "desc": "Specifies the chats that can receive Telegram Business away \u00bb and greeting \u00bb messages." }, "InputChannel": { "desc": "Represents a channel" @@ -15627,17 +18853,20 @@ "InputChatPhoto": { "desc": "Defines a new group profile photo." }, + "InputChatTheme": { + "desc": "Specifies a chat theme \u00bb." + }, "InputChatlist": { "desc": "Represents a folder" }, "InputCheckPasswordSRP": { - "desc": "Constructors for checking the validity of a 2FA SRP password" + "desc": "Constructors for checking the validity of a 2FA SRP password." }, "InputClientProxy": { "desc": "Info about an MTProxy used to connect." }, "InputCollectible": { - "desc": "" + "desc": "Represents a Fragment collectible \u00bb." }, "InputContact": { "desc": "Object defines a contact from the user's phone book." @@ -15684,6 +18913,12 @@ "InputNotifyPeer": { "desc": "Object defines the set of users and/or groups that generate notifications." }, + "InputPasskeyCredential": { + "desc": "" + }, + "InputPasskeyResponse": { + "desc": "" + }, "InputPaymentCredentials": { "desc": "Payment credentials" }, @@ -15706,11 +18941,14 @@ "desc": "Privacy rules indicate who can or can't do something and are specified by a PrivacyRule, and its input counterpart InputPrivacyRule." }, "InputQuickReplyShortcut": { - "desc": "" + "desc": "Represents a quick reply shortcut \u00bb." }, "InputReplyTo": { "desc": "Contains info about a message or story to reply to." }, + "InputSavedStarGift": { + "desc": "Points to a gift \u00bb." + }, "InputSecureFile": { "desc": "Secure passport file, for more info see the passport docs \u00bb" }, @@ -15720,6 +18958,12 @@ "InputSingleMedia": { "desc": "A single media in an album or grouped media sent with messages.sendMultiMedia." }, + "InputStarGiftAuction": { + "desc": "" + }, + "InputStarsTransaction": { + "desc": "Used to fetch info about a Telegram Star transaction \u00bb." + }, "InputStickerSet": { "desc": "Represents a stickerset" }, @@ -15765,6 +19009,9 @@ "KeyboardButtonRow": { "desc": "Bot or inline keyboard rows" }, + "KeyboardButtonStyle": { + "desc": "" + }, "LabeledPrice": { "desc": "Labeled pricetag" }, @@ -15784,7 +19031,7 @@ "desc": "Represents a story media area \u00bb" }, "MediaAreaCoordinates": { - "desc": "Coordinates and size of a clicable rectangular area on top of a story." + "desc": "Coordinates and size of a clickable rectangular area on top of a story." }, "Message": { "desc": "Object describing a message." @@ -15796,7 +19043,7 @@ "desc": "Message entities, representing styled text in a message" }, "MessageExtendedMedia": { - "desc": "Extended media" + "desc": "Paid media, see here \u00bb for more info." }, "MessageFwdHeader": { "desc": "Info about a forwarded message" @@ -15816,12 +19063,18 @@ "MessageReactions": { "desc": "Message reactions \u00bb" }, + "MessageReactor": { + "desc": "Info about a user in the paid Star reactions leaderboard for a message." + }, "MessageReplies": { "desc": "Info about post comments (for channels) or message replies (for groups)" }, "MessageReplyHeader": { "desc": "Reply information" }, + "MessageReportOption": { + "desc": "Report menu option" + }, "MessageViews": { "desc": "View, forward counter + info about replies of a specific message" }, @@ -15829,7 +19082,7 @@ "desc": "Object describes message filter." }, "MissingInvitee": { - "desc": "" + "desc": "Info about why a specific user could not be invited \u00bb." }, "MyBoost": { "desc": "Contains information about a single boost slot \u00bb." @@ -15844,7 +19097,7 @@ "desc": "Object defines the set of users and/or groups that generate notifications." }, "OutboxReadDate": { - "desc": "" + "desc": "Exact read date of a private message we sent to another user." }, "Page": { "desc": "Instant view page" @@ -15870,6 +19123,12 @@ "PageTableRow": { "desc": "Table row" }, + "PaidReactionPrivacy": { + "desc": "Paid reaction privacy settings \u00bb" + }, + "Passkey": { + "desc": "" + }, "PasswordKdfAlgo": { "desc": "Key derivation function to use when generating the password hash for SRP two-factor authorization" }, @@ -15886,7 +19145,7 @@ "desc": "Saved payment credentials" }, "Peer": { - "desc": "Chat partner or group." + "desc": "Identifier of a private chat, basic group, group or channel (see here \u00bb for more info)." }, "PeerBlocked": { "desc": "Info about a blocked user" @@ -15906,6 +19165,9 @@ "PeerStories": { "desc": "Stories associated to a peer" }, + "PendingSuggestion": { + "desc": "Represents a custom pending suggestion \u00bb." + }, "PhoneCall": { "desc": "Phone call" }, @@ -15948,9 +19210,6 @@ "PremiumGiftCodeOption": { "desc": "Giveaway option." }, - "PremiumGiftOption": { - "desc": "Telegram Premium gift option" - }, "PremiumSubscriptionOption": { "desc": "Telegram Premium subscription option" }, @@ -15963,11 +19222,14 @@ "PrivacyRule": { "desc": "Privacy rules together with privacy keys indicate what can or can't someone do and are specified by a PrivacyRule constructor, and its input counterpart InputPrivacyRule." }, + "ProfileTab": { + "desc": "Represents a tab of a profile page \u00bb." + }, "PublicForward": { "desc": "Contains info about the forwards of a story as a message to public chats and reposts by public channels." }, "QuickReply": { - "desc": "" + "desc": "A quick reply shortcut." }, "Reaction": { "desc": "Message reaction" @@ -15976,10 +19238,10 @@ "desc": "Number of users that reacted with a certain emoji" }, "ReactionNotificationsFrom": { - "desc": "" + "desc": "Reaction notification settings" }, "ReactionsNotifySettings": { - "desc": "" + "desc": "Reaction notification settings, see here \u00bb for more info." }, "ReadParticipantDate": { "desc": "Contains info about when a certain participant has read a message" @@ -15990,17 +19252,26 @@ "RecentMeUrl": { "desc": "Recent t.me urls" }, + "RecentStory": { + "desc": "" + }, "ReplyMarkup": { "desc": "Reply markup for bot and inline keyboards" }, "ReportReason": { "desc": "Report reason" }, + "ReportResult": { + "desc": "Represents a report menu or result" + }, "RequestPeerType": { "desc": "Filtering criteria to use for the peer selection list shown to the user." }, "RequestedPeer": { - "desc": "" + "desc": "Info about a peer, shared by a user with the currently logged in bot using messages.sendBotRequestedPeer." + }, + "RequirementToContact": { + "desc": "Specifies a requirement that must be satisfied in order to contact a user." }, "RestrictionReason": { "desc": "Restriction reason" @@ -16015,7 +19286,13 @@ "desc": "Represents a saved message dialog \u00bb." }, "SavedReactionTag": { - "desc": "" + "desc": "Info about a saved message reaction tag \u00bb." + }, + "SavedStarGift": { + "desc": "Represents a gift owned by a peer." + }, + "SearchPostsFlood": { + "desc": "Indicates if the specified global post search \u00bb requires payment." }, "SearchResultsCalendarPeriod": { "desc": "Information about found messages sent on a specific day, used to split the messages in messages.searchResultsCalendar constructors by days." @@ -16065,27 +19342,93 @@ "ShippingOption": { "desc": "Shipping options" }, - "SimpleWebViewResult": { - "desc": "Contains the webview URL with appropriate theme parameters added" - }, "SmsJob": { - "desc": "" + "desc": "Info about an SMS job." }, "SponsoredMessage": { "desc": "A sponsored message" }, "SponsoredMessageReportOption": { + "desc": "A report option for a sponsored message \u00bb." + }, + "SponsoredPeer": { + "desc": "A sponsored peer." + }, + "StarGift": { + "desc": "Represents a star gift, see here \u00bb for more info." + }, + "StarGiftActiveAuctionState": { "desc": "" }, - "StarsTopupOption": { + "StarGiftAttribute": { + "desc": "An attribute of a collectible gift \u00bb." + }, + "StarGiftAttributeCounter": { + "desc": "Indicates the total number of gifts that have the specified attribute." + }, + "StarGiftAttributeId": { + "desc": "Represents the identifier of a collectible gift attribute." + }, + "StarGiftAttributeRarity": { "desc": "" }, - "StarsTransaction": { + "StarGiftAuctionAcquiredGift": { "desc": "" }, - "StarsTransactionPeer": { + "StarGiftAuctionRound": { + "desc": "" + }, + "StarGiftAuctionState": { + "desc": "" + }, + "StarGiftAuctionUserState": { + "desc": "" + }, + "StarGiftBackground": { + "desc": "" + }, + "StarGiftCollection": { + "desc": "Represents a star gift collection \u00bb." + }, + "StarGiftUpgradePrice": { "desc": "" }, + "StarRefProgram": { + "desc": "Indo about an affiliate program offered by a bot" + }, + "StarsAmount": { + "desc": "Describes a real (i.e. possibly decimal) amount of Telegram Stars." + }, + "StarsGiftOption": { + "desc": "Telegram Stars gift option." + }, + "StarsGiveawayOption": { + "desc": "Contains info about a Telegram Star giveaway option." + }, + "StarsGiveawayWinnersOption": { + "desc": "Represents a possible option for the number of winners in a star giveaway" + }, + "StarsRating": { + "desc": "Represents the profile's star rating, see here \u00bb for more info." + }, + "StarsRevenueStatus": { + "desc": "Describes Telegram Star revenue balances \u00bb." + }, + "StarsSubscription": { + "desc": "Represents a Telegram Star subscription \u00bb." + }, + "StarsSubscriptionPricing": { + "desc": "Pricing of a Telegram Star subscription \u00bb." + }, + "StarsTopupOption": { + "desc": "Telegram Stars topup option." + }, + "StarsTransaction": { + "desc": "Represents a Telegram Stars transaction \u00bb." + }, + "StarsTransactionPeer": { + "desc": "Source of an incoming Telegram Star transaction, or its recipient for outgoing Telegram Star transactions." + }, "StatsAbsValueAndPrev": { "desc": "Channel statistics value pair" }, @@ -16125,6 +19468,9 @@ "StoriesStealthMode": { "desc": "Story stealth mode status" }, + "StoryAlbum": { + "desc": "Represents a story album \u00bb." + }, "StoryFwdHeader": { "desc": "Contains info about the original poster of a reposted story." }, @@ -16140,6 +19486,9 @@ "StoryViews": { "desc": "Aggregated view and reaction information of a story" }, + "SuggestedPost": { + "desc": "Contains info about a suggested post \u00bb." + }, "TextWithEntities": { "desc": "Styled text with message entities" }, @@ -16150,7 +19499,16 @@ "desc": "Theme settings" }, "Timezone": { - "desc": "" + "desc": "Timezone information." + }, + "TodoCompletion": { + "desc": "A completed todo list \u00bb item." + }, + "TodoItem": { + "desc": "An item of a todo list \u00bb." + }, + "TodoList": { + "desc": "Represents a todo list \u00bb." }, "TopPeer": { "desc": "Top peer" @@ -16228,10 +19586,13 @@ "desc": "Contains media autosave settings" }, "account.BusinessChatLinks": { - "desc": "" + "desc": "Contains info about business chat deep links \u00bb created by the current account." + }, + "account.ChatThemes": { + "desc": "Available chat themes" }, "account.ConnectedBots": { - "desc": "" + "desc": "Info about currently connected business bots." }, "account.ContentSettings": { "desc": "Sensitive content settings" @@ -16242,6 +19603,15 @@ "account.EmojiStatuses": { "desc": "A list of emoji statuses" }, + "account.PaidMessagesRevenue": { + "desc": "Total number of non-refunded Telegram Stars a user has spent on sending us messages either directly or through a channel, see here \u00bb for more info on paid messages." + }, + "account.PasskeyRegistrationOptions": { + "desc": "" + }, + "account.Passkeys": { + "desc": "" + }, "account.Password": { "desc": "Configuration for two-factor authorization" }, @@ -16258,7 +19628,10 @@ "desc": "Result of an account.resetPassword request." }, "account.ResolvedBusinessChatLinks": { - "desc": "" + "desc": "Contains info about a single resolved business chat deep link \u00bb." + }, + "account.SavedMusicIds": { + "desc": "List of IDs of songs (document.ids) currently pinned on our profile, see here \u00bb for more info." }, "account.SavedRingtone": { "desc": "Contains information about a saved notification sound" @@ -16299,6 +19672,9 @@ "auth.LoginToken": { "desc": "Login token (for QR code login)" }, + "auth.PasskeyLoginOptions": { + "desc": "" + }, "auth.PasswordRecovery": { "desc": "Recovery info of a 2FA password, only for accounts with a recovery email configured." }, @@ -16311,6 +19687,12 @@ "bots.BotInfo": { "desc": "Localized name, about text and description of a bot." }, + "bots.PopularAppBots": { + "desc": "Popular Main Mini Apps, to be used in the apps tab of global search \u00bb." + }, + "bots.PreviewInfo": { + "desc": "Contains info about Main Mini App previews, see here \u00bb for more info." + }, "channels.AdminLogResults": { "desc": "Admin log events" }, @@ -16324,7 +19706,7 @@ "desc": "A list of peers that can be used to send messages in a specific group" }, "channels.SponsoredMessageReportResult": { - "desc": "" + "desc": "Status of the method call used to report a sponsored message \u00bb." }, "chatlists.ChatlistInvite": { "desc": "Info about a chat folder deep link \u00bb." @@ -16342,7 +19724,7 @@ "desc": "Info on users from the current user's black list." }, "contacts.ContactBirthdays": { - "desc": "" + "desc": "Birthday information of our contacts." }, "contacts.Contacts": { "desc": "Info on the current user's contact list." @@ -16356,11 +19738,14 @@ "contacts.ResolvedPeer": { "desc": "Peer returned after resolving a @username" }, + "contacts.SponsoredPeers": { + "desc": "A list of sponsored peers." + }, "contacts.TopPeers": { "desc": "Top peers" }, "fragment.CollectibleInfo": { - "desc": "" + "desc": "Info about a fragment collectible." }, "help.AppConfig": { "desc": "Contains various client configuration parameters" @@ -16417,7 +19802,7 @@ "desc": "Update of Telegram's terms of service" }, "help.TimezonesList": { - "desc": "" + "desc": "Timezone information that may be used elsewhere in the API, such as to set Telegram Business opening hours \u00bb." }, "help.UserInfo": { "desc": "User info" @@ -16444,7 +19829,7 @@ "desc": "Archived stickers" }, "messages.AvailableEffects": { - "desc": "" + "desc": "Full list of usable animated message effects \u00bb." }, "messages.AvailableReactions": { "desc": "Animations and metadata associated with message reactions \u00bb" @@ -16455,6 +19840,9 @@ "messages.BotCallbackAnswer": { "desc": "Callback answer of bot" }, + "messages.BotPreparedInlineMessage": { + "desc": "Represents a prepared inline message saved by a bot, to be sent to the user via a web app \u00bb" + }, "messages.BotResults": { "desc": "Result of a query to an inline bot" }, @@ -16477,7 +19865,7 @@ "desc": "Contains Diffie-Hellman key generation protocol parameters." }, "messages.DialogFilters": { - "desc": "" + "desc": "Folder information" }, "messages.Dialogs": { "desc": "Object contains a list of chats with messages and auxiliary data." @@ -16485,6 +19873,12 @@ "messages.DiscussionMessage": { "desc": "Info about a message thread" }, + "messages.EmojiGameInfo": { + "desc": "" + }, + "messages.EmojiGameOutcome": { + "desc": "" + }, "messages.EmojiGroups": { "desc": "Represents a list of emoji categories." }, @@ -16506,6 +19900,9 @@ "messages.FoundStickerSets": { "desc": "Found stickersets" }, + "messages.FoundStickers": { + "desc": "Found stickers" + }, "messages.HighScores": { "desc": "High scores (in games)" }, @@ -16519,7 +19916,7 @@ "desc": "Inactive chat list" }, "messages.InvitedUsers": { - "desc": "" + "desc": "Contains info about successfully or unsuccessfully invited \u00bb users." }, "messages.MessageEditData": { "desc": "Message edit data for media" @@ -16534,7 +19931,7 @@ "desc": "Object contains information on list of messages with auxiliary data." }, "messages.MyStickers": { - "desc": "" + "desc": "The list of stickersets owned by the current account \u00bb." }, "messages.PeerDialogs": { "desc": "List of dialogs" @@ -16542,8 +19939,11 @@ "messages.PeerSettings": { "desc": "Peer settings" }, + "messages.PreparedInlineMessage": { + "desc": "Represents a prepared inline message received via a bot's mini app, that can be sent to some chats \u00bb" + }, "messages.QuickReplies": { - "desc": "" + "desc": "Info about quick reply shortcuts \u00bb." }, "messages.Reactions": { "desc": "A set of message reactions" @@ -16558,7 +19958,7 @@ "desc": "Saved GIFs" }, "messages.SavedReactionTags": { - "desc": "" + "desc": "List of reaction tag \u00bb names assigned by the user." }, "messages.SearchCounter": { "desc": "Number of results that would be returned by a search" @@ -16596,12 +19996,21 @@ "messages.WebPage": { "desc": "Contains an instant view webpage." }, + "messages.WebPagePreview": { + "desc": "Represents a webpage preview." + }, "payments.BankCardData": { "desc": "Credit card info, provided by the card's bank(s)" }, + "payments.CheckCanSendGiftResult": { + "desc": "Specifies if a gift can or cannot be sent." + }, "payments.CheckedGiftCode": { "desc": "Info about a Telegram Premium Giftcode." }, + "payments.ConnectedStarRefBots": { + "desc": "Active affiliations" + }, "payments.ExportedInvoice": { "desc": "Exported invoice" }, @@ -16617,12 +20026,60 @@ "payments.PaymentResult": { "desc": "Payment result" }, + "payments.ResaleStarGifts": { + "desc": "List of gifts currently on resale \u00bb." + }, "payments.SavedInfo": { "desc": "Saved payment info" }, - "payments.StarsStatus": { + "payments.SavedStarGifts": { + "desc": "Represents a list of gifts." + }, + "payments.StarGiftActiveAuctions": { + "desc": "" + }, + "payments.StarGiftAuctionAcquiredGifts": { + "desc": "" + }, + "payments.StarGiftAuctionState": { + "desc": "" + }, + "payments.StarGiftCollections": { + "desc": "Represents a list of star gift collections \u00bb." + }, + "payments.StarGiftUpgradeAttributes": { "desc": "" }, + "payments.StarGiftUpgradePreview": { + "desc": "A preview of the possible attributes (chosen randomly) a gift \u00bb can receive after upgrading it to a collectible gift \u00bb, see here \u00bb for more info." + }, + "payments.StarGiftWithdrawalUrl": { + "desc": "A URL that can be used to import the exported NFT on Fragment." + }, + "payments.StarGifts": { + "desc": "Available gifts \u00bb." + }, + "payments.StarsRevenueAdsAccountUrl": { + "desc": "Contains a URL leading to a page where the user will be able to place ads for the channel/bot, paying using Telegram Stars." + }, + "payments.StarsRevenueStats": { + "desc": "Star revenue statistics, see here \u00bb for more info." + }, + "payments.StarsRevenueWithdrawalUrl": { + "desc": "Contains the URL to use to withdraw Telegram Star revenue." + }, + "payments.StarsStatus": { + "desc": "Info about the current Telegram Star subscriptions, balance and transaction history \u00bb." + }, + "payments.SuggestedStarRefBots": { + "desc": "A list of suggested mini apps with available affiliate programs" + }, + "payments.UniqueStarGift": { + "desc": "Represents a collectible gift \u00bb." + }, + "payments.UniqueStarGiftValueInfo": { + "desc": "Information about the value of a collectible gift \u00bb." + }, "payments.ValidatedRequestedInfo": { "desc": "Validated requested info" }, @@ -16632,6 +20089,9 @@ "phone.GroupCall": { "desc": "Contains info about a group call, and partial info about its participants." }, + "phone.GroupCallStars": { + "desc": "" + }, "phone.GroupCallStreamChannels": { "desc": "Info about RTMP streams in a group call or livestream" }, @@ -16663,19 +20123,10 @@ "desc": "A list of peers we are currently boosting, and how many boost slots we have left." }, "smsjobs.EligibilityToJoin": { - "desc": "" + "desc": "SMS jobs eligibility" }, "smsjobs.Status": { - "desc": "" - }, - "stats.BroadcastRevenueStats": { - "desc": "" - }, - "stats.BroadcastRevenueTransactions": { - "desc": "" - }, - "stats.BroadcastRevenueWithdrawalUrl": { - "desc": "" + "desc": "Status" }, "stats.BroadcastStats": { "desc": "Channel statistics" @@ -16698,9 +20149,18 @@ "storage.FileType": { "desc": "Object describes the file type." }, + "stories.Albums": { + "desc": "Represents a list of story albums \u00bb." + }, "stories.AllStories": { "desc": "Full list of active (or active and hidden) stories." }, + "stories.CanSendStoryCount": { + "desc": "Contains the number of available active story slots (equal to the value of the story_expiring_limit_* client configuration parameter minus the number of currently active stories)." + }, + "stories.FoundStories": { + "desc": "Stories found using global story search \u00bb." + }, "stories.PeerStories": { "desc": "Active story list of a specific peer." }, @@ -16734,8 +20194,14 @@ "upload.WebFile": { "desc": "Remote file" }, + "users.SavedMusic": { + "desc": "List of songs (document.ids) currently pinned on a user's profile, see here \u00bb for more info." + }, "users.UserFull": { "desc": "Full user information, with attached context peers for reactions" + }, + "users.Users": { + "desc": "Describes a list of users (or bots)." } } -} \ No newline at end of file +} diff --git a/compiler/api/source/main_api.tl b/compiler/api/source/main_api.tl index 44c9ef033..36478c8ff 100644 --- a/compiler/api/source/main_api.tl +++ b/compiler/api/source/main_api.tl @@ -1,5 +1,4 @@ -// https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/mtproto/scheme/api.tl -// https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/mtproto/scheme/layer.tl +// Source and checksums: provenance.json /////////////////////////////// ///////// Main application API @@ -9,13 +8,9 @@ // boolFalse#bc799737 = Bool; // Parsed manually // boolTrue#997275b5 = Bool; // Parsed manually - // true#3fedd339 = True; // Not used - // vector#1cb5c415 {t:Type} # [ t ] = Vector t; // Parsed manually - // error#c4b9f9bb code:int text:string = Error; // Not used - // null#56730bcc = Null; // Parsed manually inputPeerEmpty#7f3b18ea = InputPeer; @@ -31,28 +26,32 @@ inputUserSelf#f7c1b13f = InputUser; inputUser#f21158c6 user_id:long access_hash:long = InputUser; inputUserFromMessage#1da448e2 peer:InputPeer msg_id:int user_id:long = InputUser; -inputPhoneContact#f392b7f4 client_id:long phone:string first_name:string last_name:string = InputContact; +inputPhoneContact#6a1dc4be flags:# client_id:long phone:string first_name:string last_name:string note:flags.0?TextWithEntities = InputContact; inputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile; inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile; +inputFileStoryDocument#62dc8b48 id:InputDocument = InputFile; inputMediaEmpty#9664f57f = InputMedia; -inputMediaUploadedPhoto#1e287d04 flags:# spoiler:flags.2?true file:InputFile stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaPhoto#b3ba0635 flags:# spoiler:flags.1?true id:InputPhoto ttl_seconds:flags.0?int = InputMedia; +inputMediaUploadedPhoto#7d8375da flags:# spoiler:flags.2?true live_photo:flags.3?true file:InputFile stickers:flags.0?Vector ttl_seconds:flags.1?int video:flags.3?InputDocument = InputMedia; +inputMediaPhoto#e3af4434 flags:# spoiler:flags.1?true live_photo:flags.2?true id:InputPhoto ttl_seconds:flags.0?int video:flags.2?InputDocument = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia; -inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaDocument#33473058 flags:# spoiler:flags.2?true id:InputDocument ttl_seconds:flags.0?int query:flags.1?string = InputMedia; +inputMediaUploadedDocument#37c9330 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector video_cover:flags.6?InputPhoto video_timestamp:flags.7?int ttl_seconds:flags.1?int = InputMedia; +inputMediaDocument#a8763ab5 flags:# spoiler:flags.2?true id:InputDocument video_cover:flags.3?InputPhoto video_timestamp:flags.4?int ttl_seconds:flags.0?int query:flags.1?string = InputMedia; inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; -inputMediaDocumentExternal#fb52dc99 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; +inputMediaDocumentExternal#779600f9 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int video_cover:flags.2?InputPhoto video_timestamp:flags.3?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#405fef0d flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:flags.3?string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; -inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; +inputMediaPoll#883a4108 flags:# poll:Poll correct_answers:flags.0?Vector attached_media:flags.3?InputMedia solution:flags.1?string solution_entities:flags.1?Vector solution_media:flags.2?InputMedia = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia; +inputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector payload:flags.0?string = InputMedia; +inputMediaTodo#9fc55fde todo:TodoList = InputMedia; +inputMediaStakeDice#f3a9244a game_hash:string ton_amount:long client_seed:bytes = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; @@ -91,7 +90,7 @@ storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; -user#215c4438 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor = User; +user#b1b8cc83 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?true bot_can_manage_bots:flags2.18?true bot_guestchat:flags2.19?true bot_guard:flags2.20?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long linked_community_id:flags2.21?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -106,15 +105,18 @@ userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#aadfc8f flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int = Chat; -channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; +channel#7ad15240 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true in_news:flags2.21?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?RecentStory color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long linked_community_id:flags2.20?long pinned_msg_id:flags2.22?int = Chat; +channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true monoforum:flags.10?true id:long access_hash:long title:string until_date:flags.16?int = Chat; +communityForbidden#fd3cdab8 flags:# id:long access_hash:flags.13?long title:string = Chat; +community#65efe954 flags:# creator:flags.0?true left:flags.2?true min:flags.12?true flags2:# collapsed_in_dialogs:flags2.20?true id:long access_hash:flags.13?long title:string photo:ChatPhoto date:int admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; -chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; -channelFull#bbab348d flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet = ChatFull; +chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true has_welcome_messages:flags.21?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; +channelFull#c4c4dd40 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true paid_messages_available:flags2.20?true has_welcome_messages:flags2.24?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab guard_bot_id:flags2.23?long share_theme:flags2.25?ShareTheme = ChatFull; +communityFull#cbb7a507 flags:# id:long about:string chat_photo:Photo linked_peers:Vector admins_count:flags.1?int kicked_count:flags.2?int peer_link_requests_pending:flags.0?int = ChatFull; -chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; -chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; -chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant; +chatParticipant#38e79fde flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant; +chatParticipantCreator#e1f867b8 flags:# user_id:long rank:flags.0?string = ChatParticipant; +chatParticipantAdmin#360d5d2 flags:# user_id:long inviter_id:long date:int rank:flags.0?string = ChatParticipant; chatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants; chatParticipants#3cbc93f8 chat_id:long participants:Vector version:int = ChatParticipants; @@ -123,25 +125,28 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#94345242 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck = Message; -messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; +message#7600b9d3 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int from_rank:flags2.12?string peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long guestchat_via_from:flags2.19?Peer reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string rich_message:flags2.13?RichMessage = Message; +messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; -messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; +messageMediaPhoto#e216eb63 flags:# spoiler:flags.3?true live_photo:flags.4?true photo:flags.0?Photo ttl_seconds:flags.2?int video:flags.4?Document = MessageMedia; messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#4cf4d72d flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true document:flags.0?Document alt_document:flags.5?Document ttl_seconds:flags.2?int = MessageMedia; +messageMediaDocument#52d8ccd9 flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true document:flags.0?Document alt_documents:flags.5?Vector video_cover:flags.9?Photo video_timestamp:flags.10?int ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#ddf10c3b flags:# force_large_media:flags.0?true force_small_media:flags.1?true manual:flags.3?true safe:flags.4?true webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia; messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia; -messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; -messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; +messageMediaPoll#773f4e66 flags:# poll:Poll results:PollResults attached_media:flags.0?MessageMedia = MessageMedia; +messageMediaDice#8cbec07 flags:# value:int emoticon:string game_outcome:flags.0?messages.EmojiGameOutcome = MessageMedia; messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; -messageMediaGiveaway#daad85b0 flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector countries_iso2:flags.1?Vector prize_description:flags.3?string quantity:int months:int until_date:int = MessageMedia; -messageMediaGiveawayResults#c6991068 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector months:int prize_description:flags.1?string until_date:int = MessageMedia; +messageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector countries_iso2:flags.1?Vector prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia; +messageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia; +messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector = MessageMedia; +messageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector = MessageMedia; +messageMediaVideoStream#ca5cab89 flags:# rtmp_stream:flags.0?true call:InputGroupCall = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; @@ -157,8 +162,8 @@ messageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageActi messageActionPinMessage#94bd38ed = MessageAction; messageActionHistoryClear#9fbab604 = MessageAction; messageActionGameScore#92a72876 game_id:long score:int = MessageAction; -messageActionPaymentSentMe#8f31b327 flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction; -messageActionPaymentSent#96163f56 flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long invoice_slug:flags.0?string = MessageAction; +messageActionPaymentSentMe#ffa00ccc flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge subscription_until_date:flags.4?int = MessageAction; +messageActionPaymentSent#c624b16e flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long invoice_slug:flags.0?string subscription_until_date:flags.4?int = MessageAction; messageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionScreenshotTaken#4792929b = MessageAction; messageActionCustomAction#fae69f56 message:string = MessageAction; @@ -171,24 +176,54 @@ messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int messageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector = MessageAction; messageActionSetMessagesTTL#3c134d7b flags:# period:int auto_setting_from:flags.0?long = MessageAction; messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; -messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; +messageActionSetChatTheme#b91bbd3a theme:ChatTheme = MessageAction; messageActionChatJoinedByRequest#ebbca3cb = MessageAction; messageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction; messageActionWebViewDataSent#b4c38cb5 text:string = MessageAction; -messageActionGiftPremium#c83d6aec flags:# currency:string amount:long months:int crypto_currency:flags.0?string crypto_amount:flags.0?long = MessageAction; -messageActionTopicCreate#d999256 flags:# title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction; +messageActionGiftPremium#48e91302 flags:# currency:string amount:long days:int crypto_currency:flags.0?string crypto_amount:flags.0?long message:flags.1?TextWithEntities = MessageAction; +messageActionTopicCreate#d999256 flags:# title_missing:flags.1?true title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction; messageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction; messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; messageActionRequestedPeer#31518e9b button_id:int peers:Vector = MessageAction; messageActionSetChatWallPaper#5060a3f4 flags:# same:flags.0?true for_both:flags.1?true wallpaper:WallPaper = MessageAction; -messageActionGiftCode#678c2e09 flags:# via_giveaway:flags.0?true unclaimed:flags.2?true boost_peer:flags.1?Peer months:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long = MessageAction; -messageActionGiveawayLaunch#332ba9ed = MessageAction; -messageActionGiveawayResults#2a9fadc5 winners_count:int unclaimed_count:int = MessageAction; +messageActionGiftCode#31c48347 flags:# via_giveaway:flags.0?true unclaimed:flags.5?true boost_peer:flags.1?Peer days:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long message:flags.4?TextWithEntities = MessageAction; +messageActionGiveawayLaunch#a80f51e4 flags:# stars:flags.0?long = MessageAction; +messageActionGiveawayResults#87e2f155 flags:# stars:flags.0?true winners_count:int unclaimed_count:int = MessageAction; messageActionBoostApply#cc02aa6d boosts:int = MessageAction; messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector = MessageAction; - -dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; +messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction; +messageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction; +messageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long transaction_id:string boost_peer:Peer giveaway_msg_id:int = MessageAction; +messageActionStarGift#ea2c31d3 flags:# name_hidden:flags.0?true saved:flags.2?true converted:flags.3?true upgraded:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true prepaid_upgrade:flags.13?true upgrade_separate:flags.16?true auction_acquired:flags.17?true gift:StarGift message:flags.1?TextWithEntities convert_stars:flags.4?long upgrade_msg_id:flags.5?int upgrade_stars:flags.8?long from_id:flags.11?Peer peer:flags.12?Peer saved_id:flags.12?long prepaid_upgrade_hash:flags.14?string gift_msg_id:flags.15?int to_id:flags.18?Peer gift_num:flags.19?int = MessageAction; +messageActionStarGiftUnique#7e1c1187 flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true prepaid_upgrade:flags.11?true assigned:flags.13?true from_offer:flags.14?true craft:flags.16?true name_hidden:flags.17?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long resale_amount:flags.8?StarsAmount can_transfer_at:flags.9?int can_resell_at:flags.10?int drop_original_details_stars:flags.12?long can_craft_at:flags.15?int message:flags.18?TextWithEntities = MessageAction; +messageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction; +messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction; +messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector = MessageAction; +messageActionTodoCompletions#cc7c5c89 completed:Vector incompleted:Vector = MessageAction; +messageActionTodoAppendTasks#c7edbc83 list:Vector = MessageAction; +messageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction; +messageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction; +messageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction; +messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction; +messageActionSuggestBirthday#2c8f2a25 birthday:Birthday = MessageAction; +messageActionStarGiftPurchaseOffer#774278d4 flags:# accepted:flags.0?true declined:flags.1?true gift:StarGift price:StarsAmount expires_at:int = MessageAction; +messageActionStarGiftPurchaseOfferDeclined#73ada76b flags:# expired:flags.0?true gift:StarGift price:StarsAmount = MessageAction; +messageActionNewCreatorPending#b07ed085 new_creator_id:long = MessageAction; +messageActionChangeCreator#e188503b new_creator_id:long = MessageAction; +messageActionNoForwardsToggle#bf7d6572 prev_value:Bool new_value:Bool = MessageAction; +messageActionNoForwardsRequest#3e2793ba flags:# expired:flags.0?true prev_value:Bool new_value:Bool = MessageAction; +messageActionPollAppendAnswer#9da1cd6c answer:PollAnswer = MessageAction; +messageActionPollDeleteAnswer#399674dc answer:PollAnswer = MessageAction; +messageActionManagedBotCreated#16605e3e bot_id:long = MessageAction; +messageActionChangeCommunity#5d20bae8 flags:# community_id:flags.0?long = MessageAction; +messageActionChatJoinedViaCommunity#4a8bfe80 community_id:long = MessageAction; +messageActionGramTransfer#9329ea33 flags:# comment_encrypted:flags.1?true amount:long peer_address:string transaction_id:string comment:flags.0?string = MessageAction; +messageActionWalletTonConnectRequest#69b8c4bf flags:# accepted:flags.2?true declined:flags.3?true session_id:long expires:int topic:flags.0?string trace_id:flags.1?string dapp_name:flags.4?string = MessageAction; +messageActionNoPaidMessagesTokenUsed#c1d2f1c6 = MessageAction; + +dialog#fc89f7f3 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int unread_poll_votes_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; +dialogCommunity#f78a0973 flags:# pinned:flags.2?true community_id:long notify_settings:PeerNotifySettings = Dialog; photoEmpty#2331b22d id:long = Photo; photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector video_sizes:flags.1?Vector dc_id:int = Photo; @@ -205,6 +240,7 @@ geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radiu auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; auth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode; +auth.sentCodePaymentRequired#f8827ebf store_product:string phone_code_hash:string support_email_address:string support_email_subject:string premium_days:int currency:string amount:long = auth.SentCode; auth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization; auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; @@ -216,12 +252,13 @@ inputNotifyUsers#193b4417 = InputNotifyPeer; inputNotifyChats#4a95e84e = InputNotifyPeer; inputNotifyBroadcasts#b1db7c7e = InputNotifyPeer; inputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer; +inputNotifyCommunity#27bb1adc community:InputChannel = InputNotifyPeer; inputPeerNotifySettings#cacb6ae2 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_sound:flags.8?NotificationSound = InputPeerNotifySettings; peerNotifySettings#99622c0c flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_ios_sound:flags.8?NotificationSound stories_android_sound:flags.9?NotificationSound stories_other_sound:flags.10?NotificationSound = PeerNotifySettings; -peerSettings#acd66c5e flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true business_bot_paused:flags.11?true business_bot_can_reply:flags.12?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int business_bot_id:flags.13?long business_bot_manage_url:flags.13?string = PeerSettings; +peerSettings#f47741f7 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true business_bot_paused:flags.11?true business_bot_can_reply:flags.12?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int business_bot_id:flags.13?long business_bot_manage_url:flags.13?string charge_paid_message_stars:flags.14?long registration_month:flags.15?string phone_country:flags.16?string name_change_date:flags.17?int photo_change_date:flags.18?int = PeerSettings; wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; @@ -237,7 +274,7 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; -userFull#cc997720 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int = UserFull; +userFull#b90c2afd flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true display_gifts_button:flags2.16?true noforwards_my_enabled:flags2.23?true noforwards_peer_enabled:flags2.24?true unofficial_security_risk:flags2.26?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme:flags.15?ChatTheme private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating stars_my_pending_rating:flags2.18?StarsRating stars_my_pending_rating_date:flags2.18?int main_tab:flags2.20?ProfileTab saved_music:flags2.21?Document note:flags2.22?TextWithEntities bot_manager_id:flags2.25?long wallet_card:flags2.19?wallet.Card share_theme:flags2.27?ShareTheme = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -257,8 +294,8 @@ messages.dialogs#15ba6c40 dialogs:Vector messages:Vector chats: messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs; -messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesSlice#3a54685e flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int messages:Vector chats:Vector users:Vector = messages.Messages; +messages.messages#1d73e7ea messages:Vector topics:Vector chats:Vector users:Vector = messages.Messages; +messages.messagesSlice#5f206716 flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int search_flood:flags.3?SearchPostsFlood messages:Vector topics:Vector chats:Vector users:Vector = messages.Messages; messages.channelMessages#c776ba4e flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector topics:Vector chats:Vector users:Vector = messages.Messages; messages.messagesNotModified#74535f21 count:int = messages.Messages; @@ -286,11 +323,12 @@ inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; inputMessagesFilterGeo#e7026d0d = MessagesFilter; inputMessagesFilterContacts#e062db83 = MessagesFilter; inputMessagesFilterPinned#1bb00451 = MessagesFilter; +inputMessagesFilterPoll#fa2bc90a = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; updateDeleteMessages#a20db0e5 messages:Vector pts:int pts_count:int = Update; -updateUserTyping#c01e857f user_id:long action:SendMessageAction = Update; +updateUserTyping#2a17bf5c flags:# user_id:long top_msg_id:flags.0?int action:SendMessageAction = Update; updateChatUserTyping#83487af0 chat_id:long from_id:Peer action:SendMessageAction = Update; updateChatParticipants#7761198 participants:ChatParticipants = Update; updateUserStatus#e5bdf8de user_id:long status:UserStatus = Update; @@ -307,7 +345,7 @@ updateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings updateServiceNotification#ebe46819 flags:# popup:flags.0?true invert_media:flags.2?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; updatePrivacy#ee3b272a key:PrivacyKey rules:Vector = Update; updateUserPhone#5492a13 user_id:long phone:string = Update; -updateReadHistoryInbox#9c974fdf flags:# folder_id:flags.0?int peer:Peer max_id:int still_unread_count:int pts:int pts_count:int = Update; +updateReadHistoryInbox#9e84bc99 flags:# folder_id:flags.0?int peer:Peer top_msg_id:flags.1?int max_id:int still_unread_count:int pts:int pts_count:int = Update; updateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update; updateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update; updateReadMessagesContents#f8227181 flags:# messages:Vector pts:int pts_count:int date:flags.0?int = Update; @@ -329,7 +367,7 @@ updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update; -updateDraftMessage#1b49ec6d flags:# peer:Peer top_msg_id:flags.0?int draft:DraftMessage = Update; +updateDraftMessage#edfc111e flags:# peer:Peer top_msg_id:flags.0?int saved_peer_id:flags.1?Peer draft:DraftMessage = Update; updateReadFeaturedStickers#571d2742 = Update; updateRecentStickers#9a422c20 = Update; updateConfig#a229dd06 = Update; @@ -345,21 +383,21 @@ updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; updateLangPackTooLong#46560264 lang_code:string = Update; updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; -updateChannelReadMessagesContents#ea29055d flags:# channel_id:long top_msg_id:flags.0?int messages:Vector = Update; +updateChannelReadMessagesContents#25f324f7 flags:# channel_id:long top_msg_id:flags.0?int saved_peer_id:flags.1?Peer messages:Vector = Update; updateContactsReset#7084a7be = Update; updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; -updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; -updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; +updateDialogUnreadMark#b658f23e flags:# unread:flags.0?true peer:DialogPeer saved_peer_id:flags.1?Peer = Update; +updateMessagePoll#d64c522b flags:# peer:flags.1?Peer msg_id:flags.1?int top_msg_id:flags.2?int poll_id:long poll:flags.0?Poll results:PollResults = Update; updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int = Update; updatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update; updatePeerLocated#b4afcfb0 peers:Vector = Update; updateNewScheduledMessage#39a51dfb message:Message = Update; -updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; +updateDeleteScheduledMessages#f2a71983 flags:# peer:Peer messages:Vector sent_messages:flags.0?Vector = Update; updateTheme#8216fba3 theme:Theme = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateLoginToken#564fe691 = Update; -updateMessagePollVote#24f40e77 poll_id:long peer:Peer options:Vector qts:int = Update; +updateMessagePollVote#7699f014 poll_id:long peer:Peer options:Vector positions:Vector qts:int = Update; updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update; updateDialogFilterOrder#a5d72105 order:Vector = Update; updateDialogFilters#3504914f = Update; @@ -373,7 +411,7 @@ updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vec updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; updateChat#f89a6a4e chat_id:long = Update; updateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector version:int = Update; -updateGroupCall#14b24500 chat_id:long call:GroupCall = Update; +updateGroupCall#9d2216e0 flags:# live_story:flags.2?true peer:flags.1?Peer call:GroupCall = Update; updatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update; updateChatParticipant#d087663a flags:# chat_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update; updateChannelParticipant#985d3abb flags:# via_chatlist:flags.3?true channel_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; @@ -381,8 +419,8 @@ updateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update; updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector = Update; updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector = Update; -updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; -updateMessageReactions#5e1b3cb8 flags:# peer:Peer msg_id:int top_msg_id:flags.0?int reactions:MessageReactions = Update; +updateBotChatInviteRequester#7cb34d79 flags:# peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int query_id:flags.0?long = Update; +updateMessageReactions#1e297bfa flags:# peer:Peer msg_id:int top_msg_id:flags.0?int saved_peer_id:flags.1?Peer reactions:MessageReactions = Update; updateAttachMenuBots#17b7a20b = Update; updateWebViewResultSent#1592b79d query_id:long = Update; updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update; @@ -393,9 +431,7 @@ updateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update; updateRecentEmojiStatuses#30f443db = Update; updateRecentReactions#6f7863f4 = Update; updateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update; -updateMessageExtendedMedia#5a73a98c peer:Peer msg_id:int extended_media:MessageExtendedMedia = Update; -updateChannelPinnedTopic#192efbe3 flags:# pinned:flags.0?true channel_id:long topic_id:int = Update; -updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector = Update; +updateMessageExtendedMedia#d5a41724 peer:Peer msg_id:int extended_media:Vector = Update; updateUser#20529438 user_id:long = Update; updateAutoSaveSettings#ec05b097 = Update; updateStory#75b3b798 peer:Peer story:StoryItem = Update; @@ -422,8 +458,43 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; -updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; -updateStarsBalance#fb85198 balance:long = Update; +updateStarsBalance#4e80a379 balance:StarsAmount = Update; +updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update; +updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; +updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update; +updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; +updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update; +updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector next_offset:int = Update; +updateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update; +updateMonoForumNoPaidException#9f812b08 flags:# exception:flags.0?true channel_id:long saved_peer_id:Peer = Update; +updateGroupCallMessage#d8326f0d call:InputGroupCall message:GroupCallMessage = Update; +updateGroupCallEncryptedMessage#c957a766 call:InputGroupCall from_id:Peer encrypted_message:bytes = Update; +updatePinnedForumTopic#683b2c52 flags:# pinned:flags.0?true peer:Peer topic_id:int = Update; +updatePinnedForumTopics#def143d0 flags:# peer:Peer order:flags.0?Vector = Update; +updateDeleteGroupCallMessages#3e85e92c call:InputGroupCall messages:Vector = Update; +updateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Update; +updateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update; +updateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update; +updateStarGiftCraftFail#ac072444 = Update; +updateChatParticipantRank#bd8367b9 chat_id:long user_id:long rank:string version:int = Update; +updateManagedBot#4880ed9a user_id:long bot_id:long qts:int = Update; +updateBotGuestChatQuery#cdd4093d flags:# query_id:long message:Message reference_messages:flags.0?Vector qts:int = Update; +updateAiComposeTones#8c0f91fb = Update; +updateJoinChatWebViewDecision#bdac7e70 peer:Peer query_id:long result:JoinChatBotResult = Update; +updateNewBotConnection#b22083a6 flags:# confirmed:flags.0?true bot_id:long date:flags.1?int device:flags.1?string location:flags.1?string = Update; +updateWebBrowserSettings#c39a2ade flags:# open_external_browser:flags.0?true display_close_button:flags.1?true = Update; +updateWebBrowserException#140502d1 flags:# delete:flags.1?true open_external_browser:flags.0?Bool exception:WebDomainException = Update; +updateNewEphemeralMessage#20bcbba1 message:EphemeralMessage = Update; +updateDeleteEphemeralMessages#56dbfcf8 peer:Peer ids:Vector = Update; +updateEditEphemeralMessage#4bbb8f01 message:EphemeralMessage = Update; +updateEphemeralBotCallbackQuery#7c1079d6 flags:# query_id:long user_id:long peer:flags.0?Peer msg_id:int data:bytes chat_instance:flags.1?long message:EphemeralMessage = Update; +updateBotStarsSubscription#6c0d8e23 flags:# canceled:flags.0?true payment_failed:flags.1?true restored:flags.2?true user_id:long payload:bytes qts:int = Update; +updateWalletState#6ac3f2aa state:WalletState = Update; +updateSentWalletTransaction#b1434d4a flags:# gasless:flags.0?true msg_hash:string transaction:flags.1?WalletTransaction = Update; +updateWalletGaslessInfo#a8ae1cac flags:# available:flags.0?true left:int reset_at:int min_amount:long relayer_address:string = Update; +updateWalletTonConnectSession#50a38e0e session:TonConnectSession = Update; +updateWalletTonConnectPendingDisconnect#d1ee8199 session_ids:Vector = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -497,6 +568,7 @@ notifyUsers#b4c83b4c = NotifyPeer; notifyChats#c007cec3 = NotifyPeer; notifyBroadcasts#d612e8ef = NotifyPeer; notifyForumTopic#226e6308 peer:Peer top_msg_id:int = NotifyPeer; +notifyCommunity#be376999 community_id:long = NotifyPeer; sendMessageTypingAction#16bf744e = SendMessageAction; sendMessageCancelAction#fd5ec8f5 = SendMessageAction; @@ -516,6 +588,10 @@ sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; sendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction; sendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction; +sendMessageTextDraftAction#3630b85a flags:# can_stop:flags.0?true keep_on_stop:flags.1?true random_id:long text:TextWithEntities = SendMessageAction; +inputSendMessageRichMessageDraftAction#a937c7be flags:# can_stop:flags.0?true keep_on_stop:flags.1?true random_id:long rich_message:InputRichMessage = SendMessageAction; +sendMessageRichMessageDraftAction#52564893 flags:# can_stop:flags.0?true keep_on_stop:flags.1?true random_id:long rich_message:RichMessage = SendMessageAction; +sendMessageStopDraftAction#fbf902b0 random_id:long = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -530,6 +606,9 @@ inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey; inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey; +inputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey; +inputPrivacyKeyNoPaidMessages#bdc597b4 = InputPrivacyKey; +inputPrivacyKeySavedMusic#4dbe9226 = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; @@ -542,6 +621,9 @@ privacyKeyAddedByPhone#42ffd42b = PrivacyKey; privacyKeyVoiceMessages#697f414 = PrivacyKey; privacyKeyAbout#a486b761 = PrivacyKey; privacyKeyBirthday#2000a518 = PrivacyKey; +privacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey; +privacyKeyNoPaidMessages#17d348d2 = PrivacyKey; +privacyKeySavedMusic#ff7a571b = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; @@ -553,6 +635,8 @@ inputPrivacyValueAllowChatParticipants#840649cf chats:Vector = InputPrivac inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector = InputPrivacyRule; inputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule; inputPrivacyValueAllowPremium#77cdc9f1 = InputPrivacyRule; +inputPrivacyValueAllowBots#5a4fcce5 = InputPrivacyRule; +inputPrivacyValueDisallowBots#c4e57915 = InputPrivacyRule; privacyValueAllowContacts#fffe1bac = PrivacyRule; privacyValueAllowAll#65427b82 = PrivacyRule; @@ -564,6 +648,8 @@ privacyValueAllowChatParticipants#6b134e8e chats:Vector = PrivacyRule; privacyValueDisallowChatParticipants#41c87565 chats:Vector = PrivacyRule; privacyValueAllowCloseFriends#f7e8d89b = PrivacyRule; privacyValueAllowPremium#ece9814b = PrivacyRule; +privacyValueAllowBots#21461b5d = PrivacyRule; +privacyValueDisallowBots#f6a5f82f = PrivacyRule; account.privacyRules#50a04e45 rules:Vector chats:Vector users:Vector = account.PrivacyRules; @@ -572,7 +658,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#d38ff1c2 flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int = DocumentAttribute; +documentAttributeVideo#43c57c48 flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int video_start_ts:flags.4?double video_codec:flags.5?string = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; @@ -590,7 +676,7 @@ messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMess webPageEmpty#211a1788 flags:# id:long url:flags.0?string = WebPage; webPagePending#b0d13e47 flags:# id:long url:flags.0?string date:int = WebPage; -webPage#e89c45b2 flags:# has_large_media:flags.13?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; +webPage#e89c45b2 flags:# has_large_media:flags.13?true video_cover_photo:flags.14?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage; authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true unconfirmed:flags.5?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; @@ -607,11 +693,11 @@ auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; -chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int title:flags.8?string = ExportedChatInvite; +chatInviteExported#a22cbd96 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int subscription_expired:flags.10?int title:flags.8?string subscription_pricing:flags.9?StarsSubscriptionPricing = ExportedChatInvite; chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#cde0ec40 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector color:int = ChatInvite; +chatInvite#5c9d3702 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true can_refulfill_subscription:flags.11?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector color:int subscription_pricing:flags.10?StarsSubscriptionPricing subscription_form_id:flags.12?long bot_verification:flags.13?BotVerification = ChatInvite; chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; inputStickerSetEmpty#ffb62b95 = InputStickerSet; @@ -625,40 +711,25 @@ inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet; inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet; inputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet; +inputStickerSetTonGifts#1cf671a0 = InputStickerSet; stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true creator:flags.11?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; messages.stickerSet#6e153f16 set:StickerSet packs:Vector keywords:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; -botCommand#c27ac8c7 command:string description:string = BotCommand; - -botInfo#8f300b57 flags:# user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton = BotInfo; - -keyboardButton#a2fa4880 text:string = KeyboardButton; -keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; -keyboardButtonCallback#35bbdb6b flags:# requires_password:flags.0?true text:string data:bytes = KeyboardButton; -keyboardButtonRequestPhone#b16a6c29 text:string = KeyboardButton; -keyboardButtonRequestGeoLocation#fc796b3f text:string = KeyboardButton; -keyboardButtonSwitchInline#93b9fbb5 flags:# same_peer:flags.0?true text:string query:string peer_types:flags.1?Vector = KeyboardButton; -keyboardButtonGame#50f41ccf text:string = KeyboardButton; -keyboardButtonBuy#afd93fbb text:string = KeyboardButton; -keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; -inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; -keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton; -inputKeyboardButtonUserProfile#e988037b text:string user_id:InputUser = KeyboardButton; -keyboardButtonUserProfile#308660c1 text:string user_id:long = KeyboardButton; -keyboardButtonWebView#13767230 text:string url:string = KeyboardButton; -keyboardButtonSimpleWebView#a0c0505c text:string url:string = KeyboardButton; -keyboardButtonRequestPeer#53d7bfd8 text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; -inputKeyboardButtonRequestPeer#c9662d05 flags:# name_requested:flags.0?true username_requested:flags.1?true photo_requested:flags.2?true text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; +botCommand#9852d6d2 flags:# ephemeral:flags.0?true command:string description:string = BotCommand; + +botInfo#4d8a0299 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string app_settings:flags.8?BotAppSettings verifier_settings:flags.9?BotVerifierSettings = BotInfo; + +keyboardButton#2f67a72f flags:# style:flags.10?KeyboardButtonStyle text:string type:ButtonType = KeyboardButton; keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; replyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup; replyKeyboardForceReply#86b40b08 flags:# single_use:flags.1?true selective:flags.2?true placeholder:flags.3?string = ReplyMarkup; -replyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true persistent:flags.4?true rows:Vector placeholder:flags.3?string = ReplyMarkup; -replyInlineMarkup#48a30254 rows:Vector = ReplyMarkup; +replyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true persistent:flags.4?true force_reply:flags.5?true rows:Vector placeholder:flags.3?string = ReplyMarkup; +replyInlineMarkup#b2b15770 flags:# force_reply:flags.5?true rows:Vector = ReplyMarkup; messageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity; messageEntityMention#fa04579d offset:int length:int = MessageEntity; @@ -681,6 +752,11 @@ messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; +messageEntityFormattedDate#904ac7c7 flags:# relative:flags.0?true short_time:flags.1?true long_time:flags.2?true short_date:flags.3?true long_date:flags.4?true day_of_week:flags.5?true offset:int length:int date:int = MessageEntity; +messageEntityDiffInsert#71777116 offset:int length:int = MessageEntity; +messageEntityDiffReplace#c6c1e5a7 offset:int length:int old_text:string = MessageEntity; +messageEntityDiffDelete#652c1c5 offset:int length:int = MessageEntity; +messageEntityTonAddress#fdabbfcd offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -697,11 +773,11 @@ updates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:fl channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; -channelParticipant#c00c07c0 user_id:long date:int = ChannelParticipant; -channelParticipantSelf#35a8bfa7 flags:# via_request:flags.0?true user_id:long inviter_id:long date:int = ChannelParticipant; +channelParticipant#1bd54456 flags:# user_id:long date:int subscription_until_date:flags.0?int rank:flags.2?string = ChannelParticipant; +channelParticipantSelf#a9478a1a flags:# via_request:flags.0?true user_id:long inviter_id:long date:int subscription_until_date:flags.1?int rank:flags.2?string = ChannelParticipant; channelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; channelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; -channelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant; +channelParticipantBanned#d5f0ad91 flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights rank:flags.2?string = ChannelParticipant; channelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant; channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; @@ -731,6 +807,7 @@ inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_nam inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaWebPage#bddcc510 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true optional:flags.6?true message:string entities:flags.1?Vector url:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageRichMessage#b43df56c flags:# reply_markup:flags.2?ReplyMarkup rich_message:InputRichMessage = InputBotInlineMessage; inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; @@ -744,6 +821,7 @@ botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:st botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaInvoice#354a9b09 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument currency:string total_amount:long reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaWebPage#809ad9a6 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true manual:flags.7?true safe:flags.8?true message:string entities:flags.1?Vector url:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageRichMessage#a617e7b flags:# reply_markup:flags.2?ReplyMarkup rich_message:RichMessage = BotInlineMessage; botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult; botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; @@ -768,7 +846,7 @@ auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeTyp auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; -auth.sentCodeTypeFirebaseSms#13c90f17 flags:# nonce:flags.0?bytes play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; +auth.sentCodeTypeFirebaseSms#9fd736 flags:# nonce:flags.0?bytes play_integrity_project_id:flags.2?long play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType; auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType; @@ -793,6 +871,8 @@ topPeerCategoryChannels#161d9628 = TopPeerCategory; topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory; topPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory; topPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory; +topPeerCategoryBotsApp#fd9e7bec = TopPeerCategory; +topPeerCategoryBotsGuestChat#6c24f3dd = TopPeerCategory; topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector = TopPeerCategoryPeers; @@ -801,7 +881,7 @@ contacts.topPeers#70b772a8 categories:Vector chats:Vector< contacts.topPeersDisabled#b52c939d = contacts.TopPeers; draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; -draftMessage#3fccf7ef flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector media:flags.5?InputMedia date:int = DraftMessage; +draftMessage#60fe3294 flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost rich_message:flags.9?RichMessage = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector unread:Vector = messages.FeaturedStickers; @@ -849,6 +929,22 @@ textMarked#34b8621 text:RichText = RichText; textPhone#1ccb966a text:RichText phone:string = RichText; textImage#81ccf4f document_id:long w:int h:int = RichText; textAnchor#35553762 text:RichText name:string = RichText; +textMath#9d2eac97 source:string = RichText; +textCustomEmoji#a26156c0 document_id:long alt:string = RichText; +textSpoiler#4c2a5d62 text:RichText = RichText; +textMention#cd24cf44 text:RichText = RichText; +textHashtag#519524ea text:RichText = RichText; +textBotCommand#2ff29d3 text:RichText = RichText; +textCashtag#7b9e1801 text:RichText = RichText; +textAutoUrl#ac6a83aa text:RichText = RichText; +textAutoEmail#c556a45d text:RichText = RichText; +textAutoPhone#24c26789 text:RichText = RichText; +textBankCard#b956812d text:RichText = RichText; +textMentionName#1a9fbfc text:RichText user_id:long = RichText; +textDate#a5b45e2b flags:# relative:flags.0?true short_time:flags.1?true long_time:flags.2?true short_date:flags.3?true long_date:flags.4?true day_of_week:flags.5?true text:RichText date:int = RichText; +textDiff#9686cb50 text:RichText old_text:RichText = RichText; +textButton#afc79cd6 flags:# text:RichText type:InlineButtonType style:flags.0?RichButtonStyle = RichText; +textTonAddress#3cd2a36a text:RichText = RichText; pageBlockUnsupported#13567e8a = PageBlock; pageBlockTitle#70abc3fd text:RichText = PageBlock; @@ -862,10 +958,10 @@ pageBlockFooter#48870999 text:RichText = PageBlock; pageBlockDivider#db20b188 = PageBlock; pageBlockAnchor#ce0d37b0 name:string = PageBlock; pageBlockList#e4e88011 items:Vector = PageBlock; -pageBlockBlockquote#263d7c26 text:RichText caption:RichText = PageBlock; +pageBlockBlockquote#66d1670b flags:# collapsed:flags.0?true text:RichText caption:RichText = PageBlock; pageBlockPullquote#4f4456d3 text:RichText caption:RichText = PageBlock; -pageBlockPhoto#1759c560 flags:# photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock; -pageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true video_id:long caption:PageCaption = PageBlock; +pageBlockPhoto#1759c560 flags:# spoiler:flags.1?true photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock; +pageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true spoiler:flags.2?true video_id:long caption:PageCaption = PageBlock; pageBlockCover#39f23300 cover:PageBlock = PageBlock; pageBlockEmbed#a8718dc5 flags:# full_width:flags.0?true allow_scrolling:flags.3?true url:flags.1?string html:flags.2?string poster_photo_id:flags.4?long w:flags.5?int h:flags.5?int caption:PageCaption = PageBlock; pageBlockEmbedPost#f259a80b url:string webpage_id:long author_photo_id:long author:string date:int blocks:Vector caption:PageCaption = PageBlock; @@ -874,22 +970,35 @@ pageBlockSlideshow#31f9590 items:Vector caption:PageCaption = PageBlo pageBlockChannel#ef1751b5 channel:Chat = PageBlock; pageBlockAudio#804361ea audio_id:long caption:PageCaption = PageBlock; pageBlockKicker#1e148390 text:RichText = PageBlock; -pageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true title:RichText rows:Vector = PageBlock; -pageBlockOrderedList#9a8ae1e1 items:Vector = PageBlock; +pageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true compact:flags.2?true title:RichText rows:Vector = PageBlock; +pageBlockOrderedList#1fd6f6c1 flags:# reversed:flags.2?true items:Vector start:flags.0?int type:flags.1?string = PageBlock; pageBlockDetails#76768bed flags:# open:flags.0?true blocks:Vector title:RichText = PageBlock; pageBlockRelatedArticles#16115a96 title:RichText articles:Vector = PageBlock; pageBlockMap#a44f3ef6 geo:GeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock; +pageBlockHeading1#baff072f text:RichText = PageBlock; +pageBlockHeading2#96b2aec text:RichText = PageBlock; +pageBlockHeading3#67e731ad text:RichText = PageBlock; +pageBlockHeading4#b532772b text:RichText = PageBlock; +pageBlockHeading5#dbbe6c6a text:RichText = PageBlock; +pageBlockHeading6#682a41a9 text:RichText = PageBlock; +pageBlockMath#59080c20 source:string = PageBlock; +pageBlockThinking#3c29a3e2 text:RichText = PageBlock; +inputPageBlockMap#574b617f geo:InputGeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock; +pageBlockBlockquoteBlocks#e6e47c4 blocks:Vector caption:RichText = PageBlock; +pageBlockButtonRow#6d640318 flags:# align_left:flags.0?true align_center:flags.1?true align_right:flags.2?true buttons:Vector = PageBlock; +pageBlockDocument#38fa3ba3 document_id:long caption:PageCaption = PageBlock; phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason; phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason; phoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason; phoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason; +phoneCallDiscardReasonMigrateConferenceCall#9fbbf1f7 slug:string = PhoneCallDiscardReason; dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; -invoice#5db95a15 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string = Invoice; +invoice#49ee584 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string subscription_period:flags.11?int = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; @@ -912,6 +1021,7 @@ upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mti payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector users:Vector = payments.PaymentForm; payments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector = payments.PaymentForm; +payments.paymentFormStarGift#b425cfe1 form_id:long invoice:Invoice = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; @@ -940,7 +1050,7 @@ phoneCallEmpty#5366c915 id:long = PhoneCall; phoneCallWaiting#c5226f17 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall; phoneCallRequested#14b0ed0c flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall; phoneCallAccepted#3660c311 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_b:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCall#30535af5 flags:# p2p_allowed:flags.5?true video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int custom_parameters:flags.7?DataJSON = PhoneCall; +phoneCall#30535af5 flags:# p2p_allowed:flags.5?true video:flags.6?true conference_supported:flags.8?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int custom_parameters:flags.7?DataJSON = PhoneCall; phoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.6?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall; phoneConnection#9cc123c7 flags:# tcp:flags.0?true id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection; @@ -1013,12 +1123,16 @@ channelAdminLogEventActionChangeProfilePeerColor#5e477b25 prev_value:PeerColor n channelAdminLogEventActionChangeWallpaper#31bb5d52 prev_value:WallPaper new_value:WallPaper = ChannelAdminLogEventAction; channelAdminLogEventActionChangeEmojiStatus#3ea9feb1 prev_value:EmojiStatus new_value:EmojiStatus = ChannelAdminLogEventAction; channelAdminLogEventActionChangeEmojiStickerSet#46d840ab prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction; +channelAdminLogEventActionToggleSignatureProfiles#60a79c79 new_value:Bool = ChannelAdminLogEventAction; +channelAdminLogEventActionParticipantSubExtend#64642db3 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; +channelAdminLogEventActionToggleAutotranslation#c517f77e new_value:Bool = ChannelAdminLogEventAction; +channelAdminLogEventActionParticipantEditRank#5806b4ec user_id:long prev_rank:string new_rank:string = ChannelAdminLogEventAction; channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; -channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true forums:flags.17?true = ChannelAdminLogEventsFilter; +channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true forums:flags.17?true sub_extend:flags.18?true edit_rank:flags.19?true = ChannelAdminLogEventsFilter; popularContact#5ce14175 client_id:long importers:int = PopularContact; @@ -1046,9 +1160,11 @@ inputMessageCallbackQuery#acfa1a7e id:int query_id:long = InputMessage; inputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer; inputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer; +inputDialogPeerCommunity#69ef72c4 community:InputChannel = InputDialogPeer; dialogPeer#e56dbf05 peer:Peer = DialogPeer; dialogPeerFolder#514519e2 folder_id:int = DialogPeer; +dialogPeerCommunity#2f65c8e4 community_id:long = DialogPeer; messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets; messages.foundStickerSets#8af09dd2 hash:long sets:Vector = messages.FoundStickerSets; @@ -1149,11 +1265,11 @@ pageTableRow#e0c0c5e5 cells:Vector = PageTableRow; pageCaption#6f747657 text:RichText credit:RichText = PageCaption; -pageListItemText#b92fb6cd text:RichText = PageListItem; -pageListItemBlocks#25e073fc blocks:Vector = PageListItem; +pageListItemText#2f58683c flags:# checkbox:flags.0?true checked:flags.1?true text:RichText = PageListItem; +pageListItemBlocks#63ca67aa flags:# checkbox:flags.0?true checked:flags.1?true blocks:Vector = PageListItem; -pageListOrderedItemText#5e068047 num:string text:RichText = PageListOrderedItem; -pageListOrderedItemBlocks#98dd8936 num:string blocks:Vector = PageListOrderedItem; +pageListOrderedItemText#15031189 flags:# checkbox:flags.0?true checked:flags.1?true num:flags.2?string text:RichText value:flags.3?int type:flags.4?string = PageListOrderedItem; +pageListOrderedItemBlocks#8ff2d5f0 flags:# checkbox:flags.0?true checked:flags.1?true num:flags.2?string blocks:Vector value:flags.3?int type:flags.4?string = PageListOrderedItem; pageRelatedArticle#b390dc08 flags:# url:string webpage_id:long title:flags.0?string description:flags.1?string photo_id:flags.2?long author:flags.3?string published_date:flags.4?int = PageRelatedArticle; @@ -1164,21 +1280,22 @@ help.supportName#8c05f1c9 name:string = help.SupportName; help.userInfoEmpty#f3ae2eed = help.UserInfo; help.userInfo#1eb3758 message:string entities:Vector author:string date:int = help.UserInfo; -pollAnswer#ff16e2ca text:TextWithEntities option:bytes = PollAnswer; +pollAnswer#4b7d786a flags:# text:TextWithEntities option:bytes media:flags.0?MessageMedia added_by:flags.1?Peer date:flags.1?int = PollAnswer; +inputPollAnswer#199fed96 flags:# text:TextWithEntities media:flags.0?InputMedia = PollAnswer; -poll#58747131 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:TextWithEntities answers:Vector close_period:flags.4?int close_date:flags.5?int = Poll; +poll#966e2dbf id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true open_answers:flags.6?true revoting_disabled:flags.7?true shuffle_answers:flags.8?true hide_results_until_close:flags.9?true creator:flags.10?true subscribers_only:flags.11?true question:TextWithEntities answers:Vector close_period:flags.4?int close_date:flags.5?int countries_iso2:flags.12?Vector hash:long = Poll; -pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters; +pollAnswerVoters#3645230a flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:flags.2?int recent_voters:flags.2?Vector = PollAnswerVoters; -pollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; +pollResults#ba7bb15e flags:# min:flags.0?true has_unread_votes:flags.6?true can_view_stats:flags.7?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector solution_media:flags.5?MessageMedia = PollResults; chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights; +chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true manage_ranks:flags.18?true manage_linked_peers:flags.19?true manage_welcome_messages:flags.20?true = ChatAdminRights; -chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; +chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true edit_rank:flags.26?true send_reactions:flags.27?true manage_linked_peers:flags.28?true until_date:int = ChatBannedRights; inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; inputWallPaperSlug#72091c80 slug:string = InputWallPaper; @@ -1212,8 +1329,8 @@ folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; -urlAuthResultRequest#92d33a0e flags:# request_write_access:flags.0?true bot:User domain:string = UrlAuthResult; -urlAuthResultAccepted#8f8c0e4e url:string = UrlAuthResult; +urlAuthResultRequest#bdca6167 flags:# request_write_access:flags.0?true request_phone_number:flags.1?true match_codes_first:flags.5?true is_app:flags.6?true request_wallet:flags.8?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string match_codes:flags.3?Vector user_id_hint:flags.4?long verified_app_name:flags.7?string tc_session_id:flags.9?long tc_client_id:flags.10?string = UrlAuthResult; +urlAuthResultAccepted#623a8fa0 flags:# url:flags.0?string = UrlAuthResult; urlAuthResultDefault#a9d6db1f = UrlAuthResult; channelLocationEmpty#bfb5ad8b = ChannelLocation; @@ -1253,6 +1370,10 @@ themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:B webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; webPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute; webPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags.1?true stickers:Vector = WebPageAttribute; +webPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute; +webPageAttributeStarGiftCollection#31cad303 icons:Vector = WebPageAttribute; +webPageAttributeStarGiftAuction#1c641c2 gift:StarGift end_date:int = WebPageAttribute; +webPageAttributeAiComposeTone#7781fe18 emoji_id:long = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; @@ -1260,9 +1381,9 @@ bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; payments.bankCardData#3e24e573 title:string open_urls:Vector = payments.BankCardData; -dialogFilter#5fb5523b flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string color:flags.27?int pinned_peers:Vector include_peers:Vector exclude_peers:Vector = DialogFilter; +dialogFilter#aa472651 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true title_noanimate:flags.28?true id:int title:TextWithEntities emoticon:flags.25?string color:flags.27?int pinned_peers:Vector include_peers:Vector exclude_peers:Vector = DialogFilter; dialogFilterDefault#363293ae = DialogFilter; -dialogFilterChatlist#9fe28ea4 flags:# has_my_invites:flags.26?true id:int title:string emoticon:flags.25?string color:flags.27?int pinned_peers:Vector include_peers:Vector = DialogFilter; +dialogFilterChatlist#96537bd7 flags:# has_my_invites:flags.26?true title_noanimate:flags.28?true id:int title:TextWithEntities emoticon:flags.25?string color:flags.27?int pinned_peers:Vector include_peers:Vector = DialogFilter; dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; @@ -1279,7 +1400,7 @@ statsGraph#8ea464b6 flags:# json:DataJSON zoom_token:flags.0?string = StatsGraph stats.broadcastStats#396ca5fc period:StatsDateRangeDays followers:StatsAbsValueAndPrev views_per_post:StatsAbsValueAndPrev shares_per_post:StatsAbsValueAndPrev reactions_per_post:StatsAbsValueAndPrev views_per_story:StatsAbsValueAndPrev shares_per_story:StatsAbsValueAndPrev reactions_per_story:StatsAbsValueAndPrev enabled_notifications:StatsPercentValue growth_graph:StatsGraph followers_graph:StatsGraph mute_graph:StatsGraph top_hours_graph:StatsGraph interactions_graph:StatsGraph iv_interactions_graph:StatsGraph views_by_source_graph:StatsGraph new_followers_by_source_graph:StatsGraph languages_graph:StatsGraph reactions_by_emotion_graph:StatsGraph story_interactions_graph:StatsGraph story_reactions_by_emotion_graph:StatsGraph recent_posts_interactions:Vector = stats.BroadcastStats; help.promoDataEmpty#98f6ac75 expires:int = help.PromoData; -help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector users:Vector psa_type:flags.1?string psa_message:flags.2?string = help.PromoData; +help.promoData#8a4d87a flags:# proxy:flags.0?true expires:int peer:flags.3?Peer psa_type:flags.1?string psa_message:flags.2?string pending_suggestions:Vector dismissed_suggestions:Vector custom_pending_suggestion:flags.4?PendingSuggestion chats:Vector users:Vector = help.PromoData; videoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize; videoSizeEmojiMarkup#f85c413c emoji_id:long background_colors:Vector = VideoSize; @@ -1293,7 +1414,7 @@ statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInvite stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; -globalPrivacySettings#734c4ccb flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true = GlobalPrivacySettings; +globalPrivacySettings#fe41b34f flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true display_gifts_button:flags.7?true noncontact_peers_paid_stars:flags.5?long disallowed_gifts:flags.6?DisallowedGiftsSettings = GlobalPrivacySettings; help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector patterns:flags.1?Vector = help.CountryCode; @@ -1308,7 +1429,7 @@ messages.messageViews#b6c4f543 views:Vector chats:Vector use messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; -messageReplyHeader#afbc09db flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int = MessageReplyHeader; +messageReplyHeader#ddb5ab75 flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_ephemeral:flags.13?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int todo_item_id:flags.11?int poll_option:flags.12?bytes quote_rich:flags.14?Vector = MessageReplyHeader; messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; @@ -1318,11 +1439,13 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#7fe91c14 views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; +groupCall#efb2b617 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true conference:flags.14?true creator:flags.15?true messages_enabled:flags.17?true can_change_messages_enabled:flags.18?true min:flags.19?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int invite_link:flags.16?string send_paid_messages_stars:flags.20?long default_send_as:flags.21?Peer = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; +inputGroupCallSlug#fe06823f slug:string = InputGroupCall; +inputGroupCallInviteMessage#8c10603f msg_id:int = InputGroupCall; -groupCallParticipant#eba636fe flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo = GroupCallParticipant; +groupCallParticipant#2a3dc7ac flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo paid_stars_total:flags.16?long = GroupCallParticipant; phone.groupCall#9e727aad call:GroupCall participants:Vector participants_next_offset:string chats:Vector users:Vector = phone.GroupCall; @@ -1378,9 +1501,15 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; -sponsoredMessage#bdedf566 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector photo:flags.6?Photo color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; +chatTheme#c3dffc04 emoticon:string = ChatTheme; +chatThemeUniqueGift#3458f9c8 gift:StarGift theme_settings:Vector = ChatTheme; + +account.chatThemesNotModified#e011e1c4 = account.ChatThemes; +account.chatThemes#be098173 flags:# hash:long themes:Vector chats:Vector users:Vector next_offset:flags.0?string = account.ChatThemes; + +sponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage; -messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; +messages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages; searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod; @@ -1401,7 +1530,7 @@ auth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut reactionCount#a3d1cb80 flags:# chosen_order:flags.0?int reaction:Reaction count:int = ReactionCount; -messageReactions#4f2b9479 flags:# min:flags.0?true can_see_list:flags.2?true reactions_as_tags:flags.3?true results:Vector recent_reactions:flags.1?Vector = MessageReactions; +messageReactions#a339f0b flags:# min:flags.0?true can_see_list:flags.2?true reactions_as_tags:flags.3?true results:Vector recent_reactions:flags.1?Vector top_reactors:flags.4?Vector = MessageReactions; messages.messageReactionsList#31bd492d flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; @@ -1429,9 +1558,7 @@ attachMenuBots#3c4301c0 hash:long bots:Vector users:Vector attachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector = AttachMenuBotsBot; -webViewResultUrl#c14557c query_id:long url:string = WebViewResult; - -simpleWebViewResultUrl#882f76bb url:string = SimpleWebViewResult; +webViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true fullscreen:flags.2?true same_origin:flags.3?true query_id:flags.0?long url:string = WebViewResult; webViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent; @@ -1459,7 +1586,18 @@ attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; -inputInvoiceStars#1da33ad8 option:StarsTopupOption = InputInvoice; +inputInvoiceStars#65f00ce3 purpose:InputStorePaymentPurpose = InputInvoice; +inputInvoiceChatInviteSubscription#34e793f1 hash:string = InputInvoice; +inputInvoiceStarGift#e8625e92 flags:# hide_name:flags.0?true include_upgrade:flags.2?true peer:InputPeer gift_id:long message:flags.1?TextWithEntities = InputInvoice; +inputInvoiceStarGiftUpgrade#4d818d5d flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = InputInvoice; +inputInvoiceStarGiftTransfer#4a5f5bd9 stargift:InputSavedStarGift to_id:InputPeer = InputInvoice; +inputInvoicePremiumGiftStars#dabab2ef flags:# user_id:InputUser months:int message:flags.0?TextWithEntities = InputInvoice; +inputInvoiceBusinessBotTransferStars#f4997e42 bot:InputUser stars:long = InputInvoice; +inputInvoiceStarGiftResale#e9b0c658 flags:# ton:flags.0?true show_name:flags.2?true slug:string to_id:InputPeer message:flags.1?TextWithEntities = InputInvoice; +inputInvoiceStarGiftPrepaidUpgrade#9a0b48b8 peer:InputPeer hash:string = InputInvoice; +inputInvoicePremiumAuthCode#3e77f614 purpose:InputStorePaymentPurpose = InputInvoice; +inputInvoiceStarGiftDropOriginalDetails#923d8d1 stargift:InputSavedStarGift = InputInvoice; +inputInvoiceStarGiftAuctionBid#1ecafa10 flags:# hide_name:flags.0?true update_bid:flags.2?true peer:flags.3?InputPeer gift_id:long bid_amount:long message:flags.1?TextWithEntities = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; @@ -1469,17 +1607,19 @@ help.premiumPromo#5334759c status_text:string status_entities:Vector boost_peer:flags.0?InputPeer currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentPremiumGiftCode#fb790393 flags:# users:Vector boost_peer:flags.0?InputPeer currency:string amount:long message:flags.1?TextWithEntities = InputStorePaymentPurpose; inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; -inputStorePaymentStars#4f0ee8df flags:# stars:long currency:string amount:long = InputStorePaymentPurpose; - -premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; +inputStorePaymentStarsTopup#f9a2a6cb flags:# stars:long currency:string amount:long spend_purpose_peer:flags.0?InputPeer = InputStorePaymentPurpose; +inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose; +inputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose; +inputStorePaymentAuthCode#3fc18057 flags:# restore:flags.0?true phone_number:string phone_code_hash:string premium_days:int currency:string amount:long = InputStorePaymentPurpose; paymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod; emojiStatusEmpty#2de11aae = EmojiStatus; -emojiStatus#929b619d document_id:long = EmojiStatus; -emojiStatusUntil#fa30a8c7 document_id:long until:int = EmojiStatus; +emojiStatus#e7ff068a flags:# document_id:long until:flags.0?int = EmojiStatus; +emojiStatusCollectible#7184603b flags:# collectible_id:long document_id:long title:string slug:string pattern_document_id:long center_color:int edge_color:int pattern_color:int text_color:int until:flags.0?int = EmojiStatus; +inputEmojiStatusCollectible#7141dbf flags:# collectible_id:long until:flags.0?int = EmojiStatus; account.emojiStatusesNotModified#d08ce645 = account.EmojiStatuses; account.emojiStatuses#90c467d1 hash:long statuses:Vector = account.EmojiStatuses; @@ -1487,6 +1627,7 @@ account.emojiStatuses#90c467d1 hash:long statuses:Vector = account. reactionEmpty#79f5d419 = Reaction; reactionEmoji#1b2286b8 emoticon:string = Reaction; reactionCustomEmoji#8935fc73 document_id:long = Reaction; +reactionPaid#523da4eb = Reaction; chatReactionsNone#eafc32bc = ChatReactions; chatReactionsAll#52928bca flags:# allow_custom:flags.0?true = ChatReactions; @@ -1515,10 +1656,10 @@ messageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia; stickerKeyword#fcfeb29c document_id:long keyword:Vector = StickerKeyword; -username#b4073647 flags:# editable:flags.0?true active:flags.1?true username:string = Username; +username#b4073647 flags:# editable:flags.0?true active:flags.1?true deletable:flags.2?true expired:flags.3?true username:string = Username; forumTopicDeleted#23f109b id:int = ForumTopic; -forumTopic#71701da9 flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true short:flags.5?true hidden:flags.6?true id:int date:int title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int from_id:Peer notify_settings:PeerNotifySettings draft:flags.4?DraftMessage = ForumTopic; +forumTopic#fcdad815 flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true short:flags.5?true hidden:flags.6?true title_missing:flags.7?true id:int date:int peer:Peer title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int unread_poll_votes_count:int from_id:Peer notify_settings:PeerNotifySettings draft:flags.4?DraftMessage = ForumTopic; messages.forumTopics#367617d3 flags:# order_by_create_date:flags.0?true count:int topics:Vector messages:Vector chats:Vector users:Vector pts:int = messages.ForumTopics; @@ -1529,6 +1670,7 @@ exportedContactToken#41bf109b url:string expires:int = ExportedContactToken; requestPeerTypeUser#5f3b8a00 flags:# bot:flags.0?Bool premium:flags.1?Bool = RequestPeerType; requestPeerTypeChat#c9f06e1b flags:# creator:flags.0?true bot_participant:flags.5?true has_username:flags.3?Bool forum:flags.4?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType; requestPeerTypeBroadcast#339bef6c flags:# creator:flags.0?true has_username:flags.3?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType; +requestPeerTypeCreateBot#3e81e078 flags:# bot_managed:flags.0?true suggested_name:flags.1?string suggested_username:flags.2?string = RequestPeerType; emojiListNotModified#481eadfa = EmojiList; emojiList#7a1e11d1 hash:long document_id:Vector = EmojiList; @@ -1561,8 +1703,6 @@ botApp#95fcd1d6 flags:# id:long access_hash:long short_name:string title:string messages.botApp#eb50adf5 flags:# inactive:flags.0?true request_write_access:flags.1?true has_settings:flags.2?true app:BotApp = messages.BotApp; -appWebViewResultUrl#3c1b4f0d url:string = AppWebViewResult; - inlineBotWebView#b57295d5 text:string url:string = InlineBotWebView; readParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate; @@ -1576,7 +1716,7 @@ chatlists.exportedChatlistInvite#10e6e3a6 filter:DialogFilter invite:ExportedCha chatlists.exportedInvites#10ab6dc7 invites:Vector chats:Vector users:Vector = chatlists.ExportedInvites; chatlists.chatlistInviteAlready#fa87f659 filter_id:int missing_peers:Vector already_peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; -chatlists.chatlistInvite#1dcd839d flags:# title:string emoticon:flags.0?string peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; +chatlists.chatlistInvite#f10ece2f flags:# title_noanimate:flags.1?true title:TextWithEntities emoticon:flags.0?string peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; chatlists.chatlistUpdates#93bd878d missing_peers:Vector chats:Vector users:Vector = chatlists.ChatlistUpdates; @@ -1589,8 +1729,8 @@ messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = Mess storyViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector reactions_count:flags.4?int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; -storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; -storyItem#79b26a24 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int from_id:flags.18?Peer fwd_from:flags.17?StoryFwdHeader expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; +storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true live:flags.9?true id:int date:int expire_date:int = StoryItem; +storyItem#16a4b93c flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int from_id:flags.18?Peer fwd_from:flags.17?StoryFwdHeader expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction albums:flags.19?Vector music:flags.20?Document = StoryItem; stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; stories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector chats:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; @@ -1605,21 +1745,26 @@ stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; -inputReplyToMessage#22c0f6d5 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int = InputReplyTo; +inputReplyToMessage#75c1ad70 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int poll_option:flags.7?bytes quote_rich:flags.8?Vector = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; +inputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo; +inputReplyToEphemeralMessage#4119b95e id:int = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; storiesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode; -mediaAreaCoordinates#3d1ea4e x:double y:double w:double h:double rotation:double = MediaAreaCoordinates; +mediaAreaCoordinates#cfc9e002 flags:# x:double y:double w:double h:double rotation:double radius:flags.0?double = MediaAreaCoordinates; mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; -mediaAreaGeoPoint#df8b3b22 coordinates:MediaAreaCoordinates geo:GeoPoint = MediaArea; +mediaAreaGeoPoint#cad5452d flags:# coordinates:MediaAreaCoordinates geo:GeoPoint address:flags.0?GeoPointAddress = MediaArea; mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea; mediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea; inputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea; +mediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea; +mediaAreaWeather#49a6549c coordinates:MediaAreaCoordinates emoji:string temperature_c:double color:int = MediaArea; +mediaAreaStarGift#5787686d coordinates:MediaAreaCoordinates slug:string = MediaArea; peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; @@ -1629,14 +1774,15 @@ messages.webPage#fd5e12bd webpage:WebPage chats:Vector users:Vector premiumGiftCodeOption#257e962b flags:# users:int months:int store_product:flags.0?string store_quantity:flags.1?int currency:string amount:long = PremiumGiftCodeOption; -payments.checkedGiftCode#284a1096 flags:# via_giveaway:flags.2?true from_id:flags.4?Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int months:int used_date:flags.1?int chats:Vector users:Vector = payments.CheckedGiftCode; +payments.checkedGiftCode#eb983f8f flags:# via_giveaway:flags.2?true from_id:flags.4?Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int days:int used_date:flags.1?int chats:Vector users:Vector = payments.CheckedGiftCode; payments.giveawayInfo#4367daa0 flags:# participating:flags.0?true preparing_results:flags.3?true start_date:int joined_too_early_date:flags.1?int admin_disallowed_chat_id:flags.2?long disallowed_country:flags.4?string = payments.GiveawayInfo; -payments.giveawayInfoResults#cd5570 flags:# winner:flags.0?true refunded:flags.1?true start_date:int gift_code_slug:flags.0?string finish_date:int winners_count:int activated_count:int = payments.GiveawayInfo; +payments.giveawayInfoResults#e175e66f flags:# winner:flags.0?true refunded:flags.1?true start_date:int gift_code_slug:flags.3?string stars_prize:flags.4?long finish_date:int winners_count:int activated_count:flags.2?int = payments.GiveawayInfo; prepaidGiveaway#b2539d54 id:long months:int quantity:int date:int = PrepaidGiveaway; +prepaidStarsGiveaway#9a9d77e0 id:long stars:long quantity:int boosts:int date:int = PrepaidGiveaway; -boost#2a1c8c71 flags:# gift:flags.1?true giveaway:flags.2?true unclaimed:flags.3?true id:string user_id:flags.0?long giveaway_msg_id:flags.2?int date:int expires:int used_gift_slug:flags.4?string multiplier:flags.5?int = Boost; +boost#4b3e14d6 flags:# gift:flags.1?true giveaway:flags.2?true unclaimed:flags.3?true id:string user_id:flags.0?long giveaway_msg_id:flags.2?int date:int expires:int used_gift_slug:flags.4?string multiplier:flags.5?int stars:flags.6?long = Boost; premium.boostsList#86f8613c flags:# count:int boosts:Vector next_offset:flags.0?string users:Vector = premium.BoostsList; @@ -1659,6 +1805,8 @@ publicForwardStory#edf3add0 peer:Peer story:StoryItem = PublicForward; stats.publicForwards#93037e20 flags:# count:int forwards:Vector next_offset:flags.0?string chats:Vector users:Vector = stats.PublicForwards; peerColor#b54b5acf flags:# color:flags.0?int background_emoji_id:flags.1?long = PeerColor; +peerColorCollectible#b9c0639a flags:# collectible_id:long gift_emoji_id:long background_emoji_id:long accent_color:int colors:Vector dark_accent_color:flags.0?int dark_colors:flags.1?Vector = PeerColor; +inputPeerColorCollectible#b8ea86a9 collectible_id:long = PeerColor; help.peerColorSet#26219a58 colors:Vector = help.PeerColorSet; help.peerColorProfileSet#767d61eb palette_colors:Vector bg_colors:Vector story_colors:Vector = help.PeerColorSet; @@ -1675,6 +1823,7 @@ storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; +monoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; @@ -1728,7 +1877,7 @@ inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; messages.quickReplies#c68d6695 quick_replies:Vector messages:Vector chats:Vector users:Vector = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; -connectedBot#bd068601 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessBotRecipients = ConnectedBot; +connectedBot#33ed001 flags:# bot_id:long recipients:BusinessBotRecipients rights:BusinessBotRights device:flags.0?string date:flags.1?int location:flags.2?string = ConnectedBot; account.connectedBots#17d7f87b connected_bots:Vector users:Vector = account.ConnectedBots; @@ -1736,7 +1885,7 @@ messages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector birthday#6c8e1e06 flags:# day:int month:int year:flags.0?int = Birthday; -botBusinessConnection#896433b4 flags:# can_reply:flags.0?true disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int = BotBusinessConnection; +botBusinessConnection#8f34b2f5 flags:# disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int rights:flags.2?BusinessBotRights = BotBusinessConnection; inputBusinessIntro#9c469cd flags:# title:string description:string sticker:flags.0?InputDocument = InputBusinessIntro; @@ -1779,22 +1928,10 @@ channels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options: channels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult; channels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult; -stats.broadcastRevenueStats#5407e297 top_hours_graph:StatsGraph revenue_graph:StatsGraph balances:BroadcastRevenueBalances usd_rate:double = stats.BroadcastRevenueStats; - -stats.broadcastRevenueWithdrawalUrl#ec659737 url:string = stats.BroadcastRevenueWithdrawalUrl; - -broadcastRevenueTransactionProceeds#557e2cc4 amount:long from_date:int to_date:int = BroadcastRevenueTransaction; -broadcastRevenueTransactionWithdrawal#5a590978 flags:# pending:flags.0?true failed:flags.2?true amount:long date:int provider:string transaction_date:flags.1?int transaction_url:flags.1?string = BroadcastRevenueTransaction; -broadcastRevenueTransactionRefund#42d30d2e amount:long date:int provider:string = BroadcastRevenueTransaction; - -stats.broadcastRevenueTransactions#87158466 count:int transactions:Vector = stats.BroadcastRevenueTransactions; - reactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom; reactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom; -reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings; - -broadcastRevenueBalances#8438f1c6 current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances; +reactionsNotifySettings#71e4ea58 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom poll_votes_notify_from:flags.2?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings; availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect; @@ -1809,12 +1946,440 @@ starsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer; starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; +starsTransactionPeerAds#60682812 = StarsTransactionPeer; +starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#cc7079b2 flags:# refund:flags.3?true id:string stars:long date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument = StarsTransaction; +starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true posts_search:flags.24?true stargift_prepaid_upgrade:flags.25?true stargift_drop_original_details:flags.26?true phonegroup_message:flags.27?true stargift_auction_bid:flags.28?true offer:flags.29?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; + +payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; + +foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory; + +stories.foundStories#e2de7737 flags:# count:int stories:Vector next_offset:flags.0?string chats:Vector users:Vector = stories.FoundStories; + +geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress; + +starsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus; + +payments.starsRevenueStats#6c207376 flags:# top_hours_graph:flags.0?StatsGraph revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats; + +payments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl; + +payments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAdsAccountUrl; + +inputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction; + +starsGiftOption#5e0589f1 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsGiftOption; + +bots.popularAppBots#1991b13b flags:# next_offset:flags.0?string users:Vector = bots.PopularAppBots; + +botPreviewMedia#23e91ba3 date:int media:MessageMedia = BotPreviewMedia; + +bots.previewInfo#ca71d64 media:Vector lang_codes:Vector = bots.PreviewInfo; + +starsSubscriptionPricing#5416d58 period:int amount:long = StarsSubscriptionPricing; + +starsSubscription#2e6eab1a flags:# canceled:flags.0?true can_refulfill:flags.1?true missing_balance:flags.2?true bot_canceled:flags.7?true id:string peer:Peer until_date:int pricing:StarsSubscriptionPricing chat_invite_hash:flags.3?string title:flags.4?string photo:flags.5?WebDocument invoice_slug:flags.6?string = StarsSubscription; + +messageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor; + +starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true stars:long yearly_boosts:int store_product:flags.2?string currency:string amount:long winners:Vector = StarsGiveawayOption; + +starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; + +starGift#313a9547 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true require_premium:flags.7?true limited_per_user:flags.8?true peer_color_available:flags.10?true auction:flags.11?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer per_user_total:flags.8?int per_user_remains:flags.8?int locked_until_date:flags.9?int auction_slug:flags.11?string gifts_per_round:flags.11?int auction_start_date:flags.11?int upgrade_variants:flags.12?int background:flags.13?StarGiftBackground = StarGift; +starGiftUnique#85f0a9cd flags:# require_premium:flags.6?true resale_ton_only:flags.7?true theme_available:flags.9?true burned:flags.14?true crafted:flags.15?true id:long gift_id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string resell_amount:flags.4?Vector released_by:flags.5?Peer value_amount:flags.8?long value_currency:flags.8?string value_usd_amount:flags.8?long theme_peer:flags.10?Peer peer_color:flags.11?PeerColor host_id:flags.12?Peer offer_min_stars:flags.13?int craft_chance_permille:flags.16?int = StarGift; + +payments.starGiftsNotModified#a388a368 = payments.StarGifts; +payments.starGifts#2ed82995 hash:int gifts:Vector chats:Vector users:Vector = payments.StarGifts; + +messageReportOption#7903e3d9 text:string option:bytes = MessageReportOption; + +reportResultChooseOption#f0e4e0b6 title:string options:Vector = ReportResult; +reportResultAddComment#6f09ac31 flags:# optional:flags.0?true option:bytes = ReportResult; +reportResultReported#8db33c4b = ReportResult; + +messages.botPreparedInlineMessage#8ecf0511 id:string expire_date:int = messages.BotPreparedInlineMessage; + +messages.preparedInlineMessage#ff57708d query_id:long result:BotInlineResult peer_types:Vector cache_time:int users:Vector = messages.PreparedInlineMessage; + +botAppSettings#c99b1950 flags:# placeholder_path:flags.0?bytes background_color:flags.1?int background_dark_color:flags.2?int header_color:flags.3?int header_dark_color:flags.4?int = BotAppSettings; + +starRefProgram#dd0c66f2 flags:# bot_id:long commission_permille:int duration_months:flags.0?int end_date:flags.1?int daily_revenue_per_user:flags.2?StarsAmount = StarRefProgram; + +connectedBotStarRef#19a13f71 flags:# revoked:flags.1?true url:string date:int bot_id:long commission_permille:int duration_months:flags.0?int participants:long revenue:long = ConnectedBotStarRef; + +payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector users:Vector = payments.ConnectedStarRefBots; + +payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector users:Vector next_offset:flags.0?string = payments.SuggestedStarRefBots; + +starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount; +starsTonAmount#74aee3e0 amount:long = StarsAmount; + +messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers; +messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector = messages.FoundStickers; + +botVerifierSettings#a12aefff flags:# can_modify_custom_description:flags.1?true icon:long company:string custom_description:flags.0?TextWithEntities = BotVerifierSettings; + +botVerification#f72e0ed9 bot_id:long icon:long description:TextWithEntities = BotVerification; + +starGiftAttributeModel#565251e2 flags:# crafted:flags.0?true name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributePattern#4e7085ea name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributeBackdrop#9f2504e4 name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity:StarGiftAttributeRarity = StarGiftAttribute; +starGiftAttributeOriginalDetails#e0bff26c flags:# sender_id:flags.0?Peer recipient_id:Peer date:int message:flags.1?TextWithEntities = StarGiftAttribute; + +payments.starGiftUpgradePreview#3de1dfed sample_attributes:Vector prices:Vector next_prices:Vector = payments.StarGiftUpgradePreview; + +users.users#62d706b8 users:Vector = users.Users; +users.usersSlice#315a4974 count:int users:Vector = users.Users; + +payments.uniqueStarGift#416c56e8 gift:StarGift chats:Vector users:Vector = payments.UniqueStarGift; + +messages.webPagePreview#8c9a88ac media:MessageMedia chats:Vector users:Vector = messages.WebPagePreview; + +savedStarGift#41df43fc flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true upgrade_separate:flags.17?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector prepaid_upgrade_hash:flags.16?string drop_original_details_stars:flags.18?long gift_num:flags.19?int can_craft_at:flags.20?int = SavedStarGift; + +payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; + +inputSavedStarGiftUser#69279795 msg_id:int = InputSavedStarGift; +inputSavedStarGiftChat#f101aa7f peer:InputPeer saved_id:long = InputSavedStarGift; +inputSavedStarGiftSlug#2085c238 slug:string = InputSavedStarGift; + +payments.starGiftWithdrawalUrl#84aa3a9c url:string = payments.StarGiftWithdrawalUrl; + +paidReactionPrivacyDefault#206ad49e = PaidReactionPrivacy; +paidReactionPrivacyAnonymous#1f0c1ad9 = PaidReactionPrivacy; +paidReactionPrivacyPeer#dc6cfcf0 peer:InputPeer = PaidReactionPrivacy; + +account.paidMessagesRevenue#1e109708 stars_amount:long = account.PaidMessagesRevenue; + +requirementToContactEmpty#50a9839 = RequirementToContact; +requirementToContactPremium#e581e4e9 = RequirementToContact; +requirementToContactPaidMessages#b4f67e93 stars_amount:long = RequirementToContact; + +businessBotRights#a0624cf7 flags:# reply:flags.0?true read_messages:flags.1?true delete_sent_messages:flags.2?true delete_received_messages:flags.3?true edit_name:flags.4?true edit_bio:flags.5?true edit_profile_photo:flags.6?true edit_username:flags.7?true view_gifts:flags.8?true sell_gifts:flags.9?true change_gift_settings:flags.10?true transfer_and_upgrade_gifts:flags.11?true transfer_stars:flags.12?true manage_stories:flags.13?true = BusinessBotRights; + +disallowedGiftsSettings#71f276c4 flags:# disallow_unlimited_stargifts:flags.0?true disallow_limited_stargifts:flags.1?true disallow_unique_stargifts:flags.2?true disallow_premium_gifts:flags.3?true disallow_stargifts_from_channels:flags.4?true = DisallowedGiftsSettings; + +sponsoredPeer#c69708d3 flags:# random_id:bytes peer:Peer sponsor_info:flags.0?string additional_info:flags.1?string = SponsoredPeer; + +contacts.sponsoredPeersEmpty#ea32b4b1 = contacts.SponsoredPeers; +contacts.sponsoredPeers#eb032884 peers:Vector chats:Vector users:Vector = contacts.SponsoredPeers; + +starGiftAttributeIdModel#48aaae3c document_id:long = StarGiftAttributeId; +starGiftAttributeIdPattern#4a162433 document_id:long = StarGiftAttributeId; +starGiftAttributeIdBackdrop#1f01c757 backdrop_id:int = StarGiftAttributeId; + +starGiftAttributeCounter#2eb1b658 attribute:StarGiftAttributeId count:int = StarGiftAttributeCounter; + +payments.resaleStarGifts#947a12df flags:# count:int gifts:Vector next_offset:flags.0?string attributes:flags.1?Vector attributes_hash:flags.1?long chats:Vector counters:flags.2?Vector users:Vector = payments.ResaleStarGifts; + +stories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount; + +pendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion; + +todoItem#cba9a52f id:int title:TextWithEntities = TodoItem; + +todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector = TodoList; + +todoCompletion#221bb5e4 id:int completed_by:Peer date:int = TodoCompletion; + +suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost; + +starsRating#1b0e4f07 flags:# level:int current_level_stars:long stars:long next_level_stars:flags.0?long = StarsRating; + +starGiftCollection#9d6b13b0 flags:# collection_id:int title:string icon:flags.0?Document gifts_count:int hash:long = StarGiftCollection; + +payments.starGiftCollectionsNotModified#a0ba4f17 = payments.StarGiftCollections; +payments.starGiftCollections#8a2932f3 collections:Vector = payments.StarGiftCollections; + +storyAlbum#9325705a flags:# album_id:int title:string icon_photo:flags.0?Photo icon_video:flags.1?Document = StoryAlbum; + +stories.albumsNotModified#564edaeb = stories.Albums; +stories.albums#c3987a3a hash:long albums:Vector = stories.Albums; + +searchPostsFlood#3e0b5b6a flags:# query_is_free:flags.0?true total_daily:int remains:int wait_till:flags.1?int stars_amount:long = SearchPostsFlood; + +payments.uniqueStarGiftValueInfo#512fe446 flags:# last_sale_on_fragment:flags.1?true value_is_average:flags.6?true currency:string value:long initial_sale_date:int initial_sale_stars:long initial_sale_price:long last_sale_date:flags.0?int last_sale_price:flags.0?long floor_price:flags.2?long average_price:flags.3?long listed_count:flags.4?int fragment_listed_count:flags.5?int fragment_listed_url:flags.5?string = payments.UniqueStarGiftValueInfo; + +profileTabPosts#b98cd696 = ProfileTab; +profileTabGifts#4d4bd46a = ProfileTab; +profileTabMedia#72c64955 = ProfileTab; +profileTabFiles#ab339c00 = ProfileTab; +profileTabMusic#9f27d26e = ProfileTab; +profileTabVoice#e477092e = ProfileTab; +profileTabLinks#d3656499 = ProfileTab; +profileTabGifs#a2c0f695 = ProfileTab; + +users.savedMusicNotModified#e3878aa4 count:int = users.SavedMusic; +users.savedMusic#34a2f297 count:int documents:Vector = users.SavedMusic; + +account.savedMusicIdsNotModified#4fc81d6e = account.SavedMusicIds; +account.savedMusicIds#998d6636 ids:Vector = account.SavedMusicIds; + +payments.checkCanSendGiftResultOk#374fa7ad = payments.CheckCanSendGiftResult; +payments.checkCanSendGiftResultFail#d5e58274 reason:TextWithEntities = payments.CheckCanSendGiftResult; + +inputChatThemeEmpty#83268483 = InputChatTheme; +inputChatTheme#c93de95c emoticon:string = InputChatTheme; +inputChatThemeUniqueGift#87e5dfe4 slug:string = InputChatTheme; + +starGiftUpgradePrice#99ea331d date:int upgrade_stars:long = StarGiftUpgradePrice; + +groupCallMessage#1a8afc7e flags:# from_admin:flags.1?true id:int from_id:Peer date:int message:TextWithEntities paid_message_stars:flags.0?long = GroupCallMessage; + +groupCallDonor#ee430c85 flags:# top:flags.0?true my:flags.1?true peer_id:flags.3?Peer stars:long = GroupCallDonor; + +phone.groupCallStars#9d1dbd26 total_stars:long top_donors:Vector chats:Vector users:Vector = phone.GroupCallStars; + +recentStory#711d692d flags:# live:flags.0?true max_id:flags.1?int = RecentStory; + +auctionBidLevel#310240cc pos:int amount:long date:int = AuctionBidLevel; + +starGiftAuctionStateNotModified#fe333952 = StarGiftAuctionState; +starGiftAuctionState#771a4e66 version:int start_date:int end_date:int min_bid_amount:long bid_levels:Vector top_bidders:Vector next_round_at:int last_gift_num:int gifts_left:int current_round:int total_rounds:int rounds:Vector = StarGiftAuctionState; +starGiftAuctionStateFinished#972dabbf flags:# start_date:int end_date:int average_price:long listed_count:flags.0?int fragment_listed_count:flags.1?int fragment_listed_url:flags.1?string = StarGiftAuctionState; + +starGiftAuctionUserState#2eeed1c4 flags:# returned:flags.1?true bid_amount:flags.0?long bid_date:flags.0?int min_bid_amount:flags.0?long bid_peer:flags.0?Peer acquired_count:int = StarGiftAuctionUserState; + +payments.starGiftAuctionState#6b39f4ec gift:StarGift state:StarGiftAuctionState user_state:StarGiftAuctionUserState timeout:int users:Vector chats:Vector = payments.StarGiftAuctionState; + +starGiftAuctionAcquiredGift#42b00348 flags:# name_hidden:flags.0?true peer:Peer date:int bid_amount:long round:int pos:int message:flags.1?TextWithEntities gift_num:flags.2?int = StarGiftAuctionAcquiredGift; + +payments.starGiftAuctionAcquiredGifts#7d5bd1f0 gifts:Vector users:Vector chats:Vector = payments.StarGiftAuctionAcquiredGifts; + +starGiftActiveAuctionState#d31bc45d gift:StarGift state:StarGiftAuctionState user_state:StarGiftAuctionUserState = StarGiftActiveAuctionState; + +payments.starGiftActiveAuctionsNotModified#db33dad0 = payments.StarGiftActiveAuctions; +payments.starGiftActiveAuctions#aef6abbc auctions:Vector users:Vector chats:Vector = payments.StarGiftActiveAuctions; + +inputStarGiftAuction#2e16c98 gift_id:long = InputStarGiftAuction; +inputStarGiftAuctionSlug#7ab58308 slug:string = InputStarGiftAuction; + +passkey#98613ebf flags:# id:string name:string date:int software_emoji_id:flags.0?long last_usage_date:flags.1?int = Passkey; + +account.passkeys#f8e0aa1c passkeys:Vector = account.Passkeys; + +account.passkeyRegistrationOptions#e16b5ce1 options:DataJSON = account.PasskeyRegistrationOptions; + +auth.passkeyLoginOptions#e2037789 options:DataJSON = auth.PasskeyLoginOptions; + +inputPasskeyResponseRegister#3e63935c client_data:DataJSON attestation_data:bytes = InputPasskeyResponse; +inputPasskeyResponseLogin#c31fc14a client_data:DataJSON authenticator_data:bytes signature:bytes user_handle:string = InputPasskeyResponse; + +inputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential; + +starGiftBackground#aff56398 center_color:int edge_color:int text_color:int = StarGiftBackground; + +starGiftAuctionRound#3aae0528 num:int duration:int = StarGiftAuctionRound; +starGiftAuctionRoundExtendable#aa021e5 num:int duration:int extend_top:int extend_window:int = StarGiftAuctionRound; + +payments.starGiftUpgradeAttributes#46c6e36f attributes:Vector = payments.StarGiftUpgradeAttributes; + +messages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:long = messages.EmojiGameOutcome; + +messages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo; +messages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector plays_left:flags.0?int = messages.EmojiGameInfo; + +starGiftAttributeRarity#36437737 permille:int = StarGiftAttributeRarity; +starGiftAttributeRarityUncommon#dbce6389 = StarGiftAttributeRarity; +starGiftAttributeRarityRare#f08d516b = StarGiftAttributeRarity; +starGiftAttributeRarityEpic#78fbf3a8 = StarGiftAttributeRarity; +starGiftAttributeRarityLegendary#cef7e7a8 = StarGiftAttributeRarity; + +keyboardButtonStyle#4fdd3430 flags:# bg_primary:flags.0?true bg_danger:flags.1?true bg_success:flags.2?true icon:flags.3?long = KeyboardButtonStyle; + +inputMessageReadMetric#402b4495 msg_id:int view_id:long time_in_view_ms:int active_time_in_view_ms:int height_to_viewport_ratio_permille:int seen_range_ratio_permille:int = InputMessageReadMetric; + +bots.exportedBotToken#3c60b621 token:string = bots.ExportedBotToken; + +bots.requestedButton#f13bbcd7 webapp_req_id:string = bots.RequestedButton; + +messages.composedMessageWithAI#90d7adfa flags:# result_text:TextWithEntities diff_text:flags.0?TextWithEntities = messages.ComposedMessageWithAI; + +stats.pollStats#2999beed votes_graph:StatsGraph = stats.PollStats; + +inputAiComposeToneDefault#1fe9a9bf tone:string = InputAiComposeTone; +inputAiComposeToneID#773c080 id:long access_hash:long = InputAiComposeTone; +inputAiComposeToneSlug#1fa01357 slug:string = InputAiComposeTone; +inputAiComposeToneSingleUse#e0c35af custom_prompt:string = InputAiComposeTone; + +aiComposeTone#cff63ea9 flags:# creator:flags.0?true id:long access_hash:long slug:string title:string emoji_id:flags.1?long prompt:flags.4?string installs_count:flags.2?int author_id:flags.3?long example_english:flags.5?AiComposeToneExample = AiComposeTone; +aiComposeToneDefault#9bad6414 tone:string emoji_id:long title:string = AiComposeTone; + +aicompose.tonesNotModified#c1f46103 = aicompose.Tones; +aicompose.tones#6c9d0efe hash:long tones:Vector users:Vector = aicompose.Tones; + +aiComposeToneExample#f1d628ec from:TextWithEntities to:TextWithEntities = AiComposeToneExample; + +bots.accessSettings#dd1fbf93 flags:# restricted:flags.0?true add_users:flags.1?Vector = bots.AccessSettings; + +messages.chatInviteJoinResultOk#445663a7 updates:Updates = messages.ChatInviteJoinResult; +messages.chatInviteJoinResultWebView#61ca29d3 bot_id:long query_id:long users:Vector = messages.ChatInviteJoinResult; + +joinChatBotResultApproved#ae152a69 = JoinChatBotResult; +joinChatBotResultDeclined#efa0194 = JoinChatBotResult; +joinChatBotResultQueued#98a3a840 = JoinChatBotResult; +joinChatBotResultWebView#d6e3b813 url:string = JoinChatBotResult; + +webDomainException#933ca597 flags:# domain:string url:string title:string favicon:flags.0?long = WebDomainException; + +account.webBrowserSettingsNotModified#c31c8f4e = account.WebBrowserSettings; +account.webBrowserSettings#79eb8cb3 flags:# open_external_browser:flags.0?true display_close_button:flags.1?true external_exceptions:Vector inapp_exceptions:Vector hash:long = account.WebBrowserSettings; + +inputRichFilePhoto#9b00622b id:string photo:InputPhoto = InputRichFile; +inputRichFileDocument#83281dbd id:string document:InputDocument = InputRichFile; + +inputRichMessage#e4c449fc flags:# rtl:flags.0?true noautolink:flags.1?true blocks:Vector photos:flags.2?Vector documents:flags.3?Vector users:flags.4?Vector = InputRichMessage; +inputRichMessageHTML#dacb836a flags:# rtl:flags.0?true noautolink:flags.1?true html:string files:flags.2?Vector = InputRichMessage; +inputRichMessageMarkdown#4b572c flags:# rtl:flags.0?true noautolink:flags.1?true markdown:string files:flags.2?Vector = InputRichMessage; + +richMessage#baf39d8b flags:# rtl:flags.0?true part:flags.1?true blocks:Vector photos:Vector documents:Vector = RichMessage; + +communityPeer#76141ebd flags:# can_view_history:flags.2?true visible:flags.0?Bool peer:Peer = CommunityPeer; + +communityPeerRequest#7beafa85 flags:# visible:flags.0?true peer:Peer requested_by:long date:int = CommunityPeerRequest; + +communities.peerLinkRequests#2244afad flags:# total_count:int requests:Vector next_offset:flags.0?string chats:Vector users:Vector = communities.PeerLinkRequests; + +ephemeralMessage#dd27bee9 flags:# out:flags.0?true welcome_template:flags.5?true invert_media:flags.7?true noforwards:flags.12?true id:int from_id:Peer peer_id:flags.9?Peer receiver_id:long top_msg_id:flags.1?int date:int message:string entities:flags.2?Vector media:flags.3?MessageMedia reply_markup:flags.4?ReplyMarkup reply_to:flags.6?MessageReplyHeader rich_message:flags.8?RichMessage chat_instance:flags.10?long anchor_msg_id:flags.11?int = EphemeralMessage; + +communities.participantJoinedChats#8d78512a creator_chat_ids:Vector joined_chat_ids:Vector chats:Vector users:Vector = communities.ParticipantJoinedChats; + +messages.translatedRichMessage#4203998f result:Vector = messages.TranslatedRichMessage; + +messages.composedRichMessageWithAI#4c4537c8 result:RichMessage = messages.ComposedRichMessageWithAI; + +buttonTypeDefault#c9dd90e9 = ButtonType; +buttonTypeRequestPhone#df3d36f9 = ButtonType; +buttonTypeRequestGeoLocation#9beee140 = ButtonType; +buttonTypeRequestPoll#aacfff84 flags:# quiz:flags.0?Bool = ButtonType; +buttonTypeRequestPeer#4f58a237 flags:# button_id:int peer_type:RequestPeerType max_quantity:int = ButtonType; +inputButtonTypeRequestPeer#3fe268fe flags:# name_requested:flags.0?true username_requested:flags.1?true photo_requested:flags.2?true button_id:int peer_type:RequestPeerType max_quantity:int = ButtonType; +buttonTypeSimpleWebView#c01a597a url:string = ButtonType; + +inlineButtonTypeUrl#eca4f8d4 url:string = InlineButtonType; +inlineButtonTypeUrlAuth#bfd02da2 flags:# fwd_text:flags.0?string url:string button_id:int = InlineButtonType; +inputInlineButtonTypeUrlAuth#9961bcb4 flags:# request_write_access:flags.0?true fwd_text:flags.1?string url:string bot:flags.2?InputUser = InlineButtonType; +inlineButtonTypeWebView#3bcab5b4 url:string = InlineButtonType; +inlineButtonTypeCallback#2955bc38 flags:# requires_password:flags.0?true data:bytes = InlineButtonType; +inlineButtonTypeGame#5cd3709d = InlineButtonType; +inlineButtonTypeBuy#48bad7a5 = InlineButtonType; +inlineButtonTypeSwitchInline#93773ff5 flags:# same_peer:flags.0?true query:string peer_types:flags.1?Vector = InlineButtonType; +inlineButtonTypeUserProfile#3fa33fcf user_id:long = InlineButtonType; +inputInlineButtonTypeUserProfile#53f3ce5a user_id:InputUser = InlineButtonType; +inlineButtonTypeCopy#b41d3272 copy_text:string = InlineButtonType; +inlineButtonTypeDisabled#a438619d = InlineButtonType; + +keyboardInlineButton#11c1a322 flags:# style:flags.10?KeyboardButtonStyle text:string type:InlineButtonType = KeyboardInlineButton; + +keyboardInlineButtonRow#19420af6 buttons:Vector = KeyboardInlineButtonRow; + +richButtonStyle#3c610bd flags:# bg_primary:flags.0?true bg_danger:flags.1?true bg_success:flags.2?true link:flags.3?true = RichButtonStyle; + +pageButton#692a5488 flags:# text:RichText type:InlineButtonType style:flags.0?RichButtonStyle = PageButton; + +ephemeral.welcomeMessagesNotModified#59ffdb31 = ephemeral.WelcomeMessages; +ephemeral.welcomeMessages#104fc872 hash:long messages:Vector = ephemeral.WelcomeMessages; + +currencyRate#30d9743c currency:string rate:double = CurrencyRate; + +payments.currencyRates#bbcce4c2 rates:Vector = payments.CurrencyRates; + +toncenter.apiResponse#ac8dfe19 response:DataJSON = toncenter.ApiResponse; + +toncenter.streamingUrl#19887601 url:string expires:int = toncenter.StreamingUrl; + +onrampMethodAvailability#9ec8c6a8 flags:# available:flags.0?true payment_method:string = OnrampMethodAvailability; + +onrampProviderInfo#dc27582 flags:# supports_base_currencies:flags.0?true supports_limits:flags.1?true supports_quote:flags.2?true id:string name:string crypto_currencies:Vector = OnrampProviderInfo; + +onrampAvailability#f9c84973 flags:# allowed:flags.0?true buy_allowed:flags.1?true country_code:string state:flags.2?string methods:Vector = OnrampAvailability; + +onrampLimits#7be2677e base_currency:string base_min_amount:string base_max_amount:string crypto_min_amount:string crypto_max_amount:string payment_method:string = OnrampLimits; + +onrampQuote#7a8011e9 base_currency:string base_amount:string crypto_currency:string crypto_amount:string crypto_price:string fee_amount:string extra_fee_amount:string network_fee_amount:string total_amount:string payment_method:string expires_date:int = OnrampQuote; + +onrampSession#d1e42e24 provider:string session_id:string url:string expires_date:int = OnrampSession; + +walletStateEmpty#9cb9b2ec flags:# creating:flags.0?true = WalletState; +walletState#95c5346b flags:# backup_enabled:flags.0?true can_export_phrase:flags.1?true can_enable_backup:flags.2?true address:string public_key:bytes balance:long = WalletState; + +walletUserAddress#fe78eecc flags:# user_id:flags.0?long address:string public_key:bytes = WalletUserAddress; + +wallet.userAddresses#928e7b55 addresses:Vector users:Vector = wallet.UserAddresses; + +walletTransactionPeerUser#d4ea706d flags:# user_id:long address:string domain:flags.0?string = WalletTransactionPeer; +walletTransactionPeerAddress#62cc1bc flags:# address:string domain:flags.0?string = WalletTransactionPeer; +walletTransactionPeerOnramp#e595186e flags:# address:string domain:flags.0?string provider_name:string = WalletTransactionPeer; +walletTransactionPeerUnsupported#728bed5a = WalletTransactionPeer; +walletTransactionPeerChannel#54e70c31 flags:# channel_id:long address:string domain:flags.0?string = WalletTransactionPeer; +walletTransactionPeerTelegramGift#f4729499 flags:# own_profile:flags.0?true = WalletTransactionPeer; + +walletTransaction#952dc143 flags:# incoming:flags.0?true gasless:flags.1?true failed:flags.2?true key_change:flags.5?true comment_encrypted:flags.6?true id:string amount:long fee:long date:int peer:WalletTransactionPeer comment:flags.3?string tx_hash:flags.4?string nft:flags.7?wallet.NftItem = WalletTransaction; + +wallet.transactions#4322d5a5 flags:# balance:long transactions:Vector next_offset:flags.0?string chats:Vector users:Vector = wallet.Transactions; + +wallet.secretPhraseParts#e6d0ef01 token:string dcs:Vector = wallet.SecretPhraseParts; + +wallet.encryptedSecretPhrasePart#18e82537 data:bytes = wallet.EncryptedSecretPhrasePart; + +wallet.holderDc#f9d612ef dc:int public_key:bytes = wallet.HolderDc; + +wallet.proofChallenge#99e41707 payload:string expires:int domain:string = wallet.ProofChallenge; + +walletOwnershipProof#60bccb0d timestamp:int signature:bytes = WalletOwnershipProof; + +inputWalletNew#63a440dc = InputWalletReplacement; +inputWalletImported#66a66a3b flags:# public_key:bytes anchor_public_key:flags.0?bytes proof:WalletOwnershipProof = InputWalletReplacement; + +tonConnectManifest#12229264 flags:# url:string name:string icon:flags.0?WebDocument = TonConnectManifest; + +tonConnectSession#126556c6 flags:# pending:flags.0?true closing:flags.1?true closed:flags.2?true id:long dapp_client_id:string client_id:flags.3?string nonce:bytes manifest:flags.4?TonConnectManifest manifest_error:flags.5?int date:int = TonConnectSession; + +wallet.tonConnectChallenge#4bc89693 challenge:bytes event_id:long = wallet.TonConnectChallenge; + +tonConnectNextEventId#582464e3 event_id:long = TonConnectNextEventId; + +tonConnectRequest#a15f8513 flags:# session_id:long msg_id:int body:bytes expires:int topic:flags.0?string trace_id:flags.1?string = TonConnectRequest; + +wallet.tonConnectPending#85c0f124 session:TonConnectSession requests:Vector = wallet.TonConnectPending; + +wallet.tonConnectSessions#e1f6896 sessions:Vector = wallet.TonConnectSessions; + +wallet.existingBalance#bde90a8d flags:# has_balance:flags.0?true url:string = wallet.ExistingBalance; + +wallet.nftAttribute#4c1ef10e trait_type:string value:string = wallet.NftAttribute; + +wallet.nftItem#173dfaf3 flags:# collection_address:flags.0?string address:string owner_address:string index:string name:flags.1?string description:flags.2?string image:flags.3?WebDocument image_small:flags.4?WebDocument content_url:flags.5?WebDocument lottie:flags.6?WebDocument attributes:flags.7?Vector extra:flags.8?DataJSON gift:flags.9?StarGift = wallet.NftItem; + +wallet.nftItems#794d486f flags:# items:Vector next_offset:flags.0?string = wallet.NftItems; + +auth.firebasePnvIntent#df5ac00c nonce:string digital_credential_payload:string = auth.FirebasePnvIntent; + +inputTonConnectOauthSession#629f66ce session_id:long challenge_answer:bytes body:bytes proof:WalletOwnershipProof = InputTonConnectOauthSession; + +exportedNoPaidMessagesToken#695dd042 flags:# token:string url:string expires:flags.0?int = ExportedNoPaidMessagesToken; + +shareThemeDefault#95ace84f = ShareTheme; +shareThemeEmojiBackdrop#64b334e1 flags:# backdrop_id:flags.0?int emojis:flags.1?Vector = ShareTheme; +shareThemeGift#ebece66d stargift:StarGift = ShareTheme; +inputShareThemeGift#3fb92b5b stargift:InputSavedStarGift = ShareTheme; + +shareThemeBackdropOption#45c1d652 backdrop_id:int name:string center_color:int edge_color:int pattern_color:int text_color:int = ShareThemeBackdropOption; + +shareThemeBackdropsNotModified#7a3f6dff = ShareThemeBackdrops; +shareThemeBackdrops#aaeadce0 hash:int backdrops:Vector = ShareThemeBackdrops; + +inputWalletCardSticker#57df45a7 coordinates:MediaAreaCoordinates nft_address:string = InputWalletCardSticker; + +wallet.cardSticker#f3010cba coordinates:MediaAreaCoordinates nft:wallet.NftItem = wallet.CardSticker; + +wallet.card#8ed043b1 flags:# stickers:flags.0?Vector bg_nft_item:flags.1?wallet.NftItem address:string balance:long = wallet.Card; -payments.starsStatus#8cf4ee60 flags:# balance:long history:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; +payments.starGiftDepositPayload#725bb4eb nft_address:string payload:bytes amount:long = payments.StarGiftDepositPayload; ---functions--- @@ -1828,6 +2393,7 @@ invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X; invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X; invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X; +invokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; @@ -1852,6 +2418,13 @@ auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_to auth.requestFirebaseSms#8e39261e flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string play_integrity_token:flags.2?string ios_push_secret:flags.1?string = Bool; auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode; auth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool; +auth.checkPaidAuth#56e59f9c phone_number:string phone_code_hash:string form_id:long = auth.SentCode; +auth.initPasskeyLogin#518ad0b7 api_id:int api_hash:string = auth.PasskeyLoginOptions; +auth.finishPasskeyLogin#9857ad07 flags:# credential:InputPasskeyCredential from_dc_id:flags.0?int from_auth_key_id:flags.0?long = auth.Authorization; +auth.cancelWebTokenAuthorization#490dada1 web_auth_token:string = Bool; +auth.initFirebasePnvLogin#644f15c0 flags:# except_ids:flags.0?Vector api_id:int api_hash:string = auth.FirebasePnvIntent; +auth.finishFirebasePnvLogin#2c85094c google_token:string = auth.Authorization; +auth.firebasePnvSignUp#783f6b56 flags:# no_joined_notifications:flags.0?true first_name:string last_name:string = auth.Authorization; account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector = Bool; @@ -1941,7 +2514,7 @@ account.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings; account.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool; account.deleteAutoSaveExceptions#53bc0020 = Bool; account.invalidateSignInCodes#ca8ae8ba codes:Vector = Bool; -account.updateColor#7cefa15d flags:# for_profile:flags.1?true color:flags.2?int background_emoji_id:flags.0?long = Bool; +account.updateColor#684d214e flags:# for_profile:flags.1?true color:flags.2?PeerColor = Bool; account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; account.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses; account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; @@ -1949,7 +2522,7 @@ account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?Bus account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; -account.updateConnectedBot#43d8521d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessBotRecipients = Updates; +account.updateConnectedBot#66a08c7e flags:# deleted:flags.1?true rights:flags.0?BusinessBotRights bot:InputUser recipients:InputBusinessBotRecipients = Updates; account.getConnectedBots#4ea4c80f = account.ConnectedBots; account.getBotBusinessConnection#76a86270 connection_id:string = Updates; account.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool; @@ -1965,11 +2538,30 @@ account.updatePersonalChannel#d94305e0 channel:InputChannel = Bool; account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings; account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings; +account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; +account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue; +account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool; +account.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool; +account.saveMusic#b26732a9 flags:# unsave:flags.0?true id:InputDocument after_id:flags.1?InputDocument = Bool; +account.getSavedMusicIds#e09d5faf hash:long = account.SavedMusicIds; +account.getUniqueGiftChatThemes#e42ce9c9 offset:string limit:int hash:long = account.ChatThemes; +account.initPasskeyRegistration#429547e8 = account.PasskeyRegistrationOptions; +account.registerPasskey#55b41fd6 credential:InputPasskeyCredential = Passkey; +account.getPasskeys#ea1f0c52 = account.Passkeys; +account.deletePasskey#f5b5563f id:string = Bool; +account.confirmBotConnection#67ed1f68 bot_id:InputUser = Bool; +account.getWebBrowserSettings#56655768 hash:long = account.WebBrowserSettings; +account.updateWebBrowserSettings#9adf82fe flags:# open_external_browser:flags.0?true display_close_button:flags.1?true = account.WebBrowserSettings; +account.toggleWebBrowserSettingsException#60ed4229 flags:# delete:flags.1?true open_external_browser:flags.0?Bool url:string = Updates; +account.deleteWebBrowserSettingsExceptions#86a0765d = account.WebBrowserSettings; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; -users.getIsPremiumRequiredToContact#a622aa10 id:Vector = Vector; +users.getRequirementsToContact#d89a83a3 id:Vector = Vector; +users.getSavedMusic#788d7fe3 id:InputUser offset:int limit:int hash:long = users.SavedMusic; +users.getSavedMusicByID#7573a4e9 id:InputUser documents:Vector = users.SavedMusic; +users.suggestBirthday#fc533372 id:InputUser birthday:Birthday = Updates; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; @@ -1980,14 +2572,14 @@ contacts.deleteByPhones#1013fd9e phones:Vector = Bool; contacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; -contacts.search#11f812d8 q:string limit:int = contacts.Found; -contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; -contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; +contacts.search#5f58d0f flags:# broadcasts:flags.0?true bots:flags.1?true q:string limit:int = contacts.Found; +contacts.resolveUsername#725afbbc flags:# username:string referer:flags.0?string = contacts.ResolvedPeer; +contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true bots_app:flags.16?true bots_guestchat:flags.17?true offset:int limit:int hash:long = contacts.TopPeers; contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; contacts.getSaved#82f1e39f = Vector; contacts.toggleTopPeers#8514bdda enabled:Bool = Bool; -contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates; +contacts.addContact#d9ba2e54 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string note:flags.1?TextWithEntities = Updates; contacts.acceptContact#f831a20f id:InputUser = Updates; contacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoPoint self_expires:flags.0?int = Updates; contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates; @@ -1997,6 +2589,10 @@ contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; contacts.getBirthdays#daeda864 = contacts.ContactBirthdays; +contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers; +contacts.updateContactNote#139f63fb id:InputUser note:TextWithEntities = Bool; +contacts.exportNoPaidMessagesToken#83c586ae flags:# revoke:flags.0?true temporary:flags.1?true = ExportedNoPaidMessagesToken; +contacts.importNoPaidMessagesToken#35a7c6ca user_id:InputUser token:string = User; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -2007,12 +2603,12 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; -messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; -messages.forwardMessages#d5039208 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendMessage#fef48f62 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost rich_message:flags.23?InputRichMessage = Updates; +messages.sendMedia#330e77f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates; +messages.forwardMessages#13704a7c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_ephemeral:flags.25?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; -messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; +messages.report#fc78af9b peer:InputPeer id:Vector option:bytes message:string = ReportResult; messages.getChats#49e9528f id:Vector = messages.Chats; messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull; messages.editChatTitle#73783ffd chat_id:long title:string = Updates; @@ -2034,10 +2630,10 @@ messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool; messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages; messages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers; messages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers; -messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia; -messages.exportChatInvite#a02ce5d5 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string = ExportedChatInvite; +messages.getWebPagePreview#570d6f6f flags:# message:string entities:flags.3?Vector = messages.WebPagePreview; +messages.exportChatInvite#a455de90 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string subscription_pricing:flags.5?StarsSubscriptionPricing = ExportedChatInvite; messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; -messages.importChatInvite#6c50051c hash:string = Updates; +messages.importChatInvite#de91436e hash:string = messages.ChatInviteJoinResult; messages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; @@ -2045,21 +2641,21 @@ messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_par messages.getMessagesViews#5784d3e1 peer:InputPeer id:Vector increment:Bool = messages.MessageViews; messages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = Bool; messages.migrateChat#a2875319 chat_id:long = Updates; -messages.searchGlobal#4bc6589a flags:# broadcasts_only:flags.1?true folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; +messages.searchGlobal#6126a43c flags:# broadcasts_only:flags.1?true groups_only:flags.2?true users_only:flags.3?true folder_id:flags.0?int community:flags.4?InputChannel q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; messages.reorderStickerSets#78337739 flags:# masks:flags.0?true emojis:flags.1?true order:Vector = Bool; messages.getDocumentByHash#b1f2061f sha256:bytes size:long mime_type:string = Document; messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#3ebee86a flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; +messages.sendInlineBotResult#c0cf7646 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut allow_paid_stars:flags.21?long = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates; -messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; +messages.editMessage#b106e66c flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int schedule_repeat_period:flags.18?int quick_reply_shortcut_id:flags.17?int rich_message:flags.23?InputRichMessage = Updates; +messages.editInlineBotMessage#a423bb51 flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector rich_message:flags.23?InputRichMessage = Bool; messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; -messages.saveDraft#7ff3b806 flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector media:flags.5?InputMedia = Bool; +messages.saveDraft#ad0fa15c flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost rich_message:flags.9?InputRichMessage = Bool; messages.getAllDrafts#6a3f8d65 = Updates; messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector = Bool; @@ -2087,16 +2683,16 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; +messages.sendMultiMedia#1bf89d74 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; -messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; -messages.getDialogUnreadMarks#22e24e22 = Vector; +messages.markDialogUnread#8c5006f8 flags:# unread:flags.0?true parent_peer:flags.1?InputPeer peer:InputDialogPeer = Bool; +messages.getDialogUnreadMarks#21202222 flags:# parent_peer:flags.0?InputPeer = Vector; messages.clearAllDrafts#7e58ee9c = Bool; messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector = Updates; -messages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates; +messages.getPollResults#eda3e33b peer:InputPeer msg_id:int poll_hash:long = Updates; messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines; messages.editChatAbout#def60797 peer:InputPeer about:string = Bool; messages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates; @@ -2105,8 +2701,8 @@ messages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector; messages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL; messages.getSearchCounters#1bbcf300 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer top_msg_id:flags.0?int filters:Vector = Vector; -messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; -messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; +messages.requestUrlAuth#894cc99c flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string in_app_origin:flags.3?string = UrlAuthResult; +messages.acceptUrlAuth#18dd4e92 flags:# write_allowed:flags.0?true share_phone_number:flags.3?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string match_code:flags.4?string tonconnect_session:flags.5?InputTonConnectOauthSession = UrlAuthResult; messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; messages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages; messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector = messages.Messages; @@ -2122,7 +2718,7 @@ messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messag messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; -messages.unpinAllMessages#ee22b9a8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.unpinAllMessages#62dd747 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory; messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages; messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed; @@ -2135,33 +2731,33 @@ messages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:Input messages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool; messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool; messages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithInvites; -messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; +messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true subscription_expired:flags.3?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; -messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; +messages.setChatTheme#81202c9 peer:InputPeer theme:InputChatTheme = Updates; messages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector; messages.getSearchResultsCalendar#6aa3f6bd flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; messages.getSearchResultsPositions#9c7f2f10 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates; messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates; -messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; +messages.toggleNoForwards#b2081a35 flags:# peer:InputPeer enabled:Bool request_msg_id:flags.0?int = Updates; messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; messages.sendReaction#d30d78d4 flags:# big:flags.1?true add_to_recent:flags.2?true peer:InputPeer msg_id:int reaction:flags.0?Vector = Updates; messages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector = Updates; messages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = messages.MessageReactionsList; -messages.setChatAvailableReactions#5a150bd4 flags:# peer:InputPeer available_reactions:ChatReactions reactions_limit:flags.0?int = Updates; +messages.setChatAvailableReactions#864b2581 flags:# peer:InputPeer available_reactions:ChatReactions reactions_limit:flags.0?int paid_enabled:flags.1?Bool = Updates; messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool; -messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector text:flags.1?Vector to_lang:string = messages.TranslatedText; -messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.translateText#a5eec345 flags:# peer:flags.0?InputPeer id:flags.0?Vector text:flags.1?Vector to_lang:string tone:flags.2?string = messages.TranslatedText; +messages.getUnreadReactions#bd7f90ac flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.readReactions#9ec44f93 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory; messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; messages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool; -messages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult; +messages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult; messages.prolongWebView#b0d81a83 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = Bool; -messages.requestSimpleWebView#1a46500a flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true bot:InputUser url:flags.3?string start_param:flags.4?string theme_params:flags.0?DataJSON platform:string = SimpleWebViewResult; +messages.requestSimpleWebView#413a3e73 flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true compact:flags.7?true fullscreen:flags.8?true bot:InputUser url:flags.3?string start_param:flags.4?string theme_params:flags.0?DataJSON platform:string = WebViewResult; messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent; messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates; messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio; @@ -2176,19 +2772,19 @@ messages.clearRecentReactions#9dfeefb4 = Bool; messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector = Updates; messages.setDefaultHistoryTTL#9eb51445 period:int = Bool; messages.getDefaultHistoryTTL#658b7188 = DefaultHistoryTTL; -messages.sendBotRequestedPeer#91b2d060 peer:InputPeer msg_id:int button_id:int requested_peers:Vector = Updates; +messages.sendBotRequestedPeer#6c5cf2a7 flags:# peer:InputPeer msg_id:flags.0?int webapp_req_id:flags.1?string button_id:int requested_peers:Vector = Updates; messages.getEmojiGroups#7488ce5b hash:int = messages.EmojiGroups; messages.getEmojiStatusGroups#2ecd56cd hash:int = messages.EmojiGroups; messages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups; messages.searchCustomEmoji#2c11c0d7 emoticon:string hash:long = EmojiList; messages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool; messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; -messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult; +messages.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = WebViewResult; messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; -messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; -messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; +messages.getSavedDialogs#1e91fc99 flags:# exclude_pinned:flags.0?true parent_peer:flags.1?InputPeer offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; +messages.getSavedHistory#998ab009 flags:# parent_peer:flags.0?InputPeer peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.deleteSavedHistory#4dc5085f flags:# parent_peer:flags.0?InputPeer peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector = Bool; @@ -2211,6 +2807,54 @@ messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates; messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; +messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; +messages.sendPaidReaction#58bbcb50 flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?PaidReactionPrivacy = Updates; +messages.togglePaidReactionPrivacy#435885b5 peer:InputPeer msg_id:int private:PaidReactionPrivacy = Bool; +messages.getPaidReactionPrivacy#472455aa = Updates; +messages.viewSponsoredMessage#269e3643 random_id:bytes = Bool; +messages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool; +messages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult; +messages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages; +messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector = messages.BotPreparedInlineMessage; +messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; +messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector offset:int limit:int hash:long = messages.FoundStickers; +messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector = Bool; +messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector = messages.SavedDialogs; +messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool; +messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector incompleted:Vector = Updates; +messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector = Updates; +messages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates; +messages.getForumTopics#3ba47bff flags:# peer:InputPeer q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; +messages.getForumTopicsByID#af0a4a08 peer:InputPeer topics:Vector = messages.ForumTopics; +messages.editForumTopic#cecc1134 flags:# peer:InputPeer topic_id:int title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = Updates; +messages.updatePinnedForumTopic#175df251 peer:InputPeer topic_id:int pinned:Bool = Updates; +messages.reorderPinnedForumTopics#e7841f0 flags:# force:flags.0?true peer:InputPeer order:Vector = Updates; +messages.createForumTopic#2f98c3d5 flags:# title_missing:flags.4?true peer:InputPeer title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates; +messages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.AffectedHistory; +messages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo; +messages.summarizeText#abbbd346 flags:# peer:InputPeer id:int to_lang:flags.0?string tone:flags.2?string = TextWithEntities; +messages.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates; +messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; +messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates; +messages.declineUrlAuth#35436bbc url:string = Bool; +messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool; +messages.composeMessageWithAI#daecc589 flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string tone:flags.2?InputAiComposeTone = messages.ComposedMessageWithAI; +messages.reportReadMetrics#4067c5e6 peer:InputPeer metrics:Vector = Bool; +messages.reportMusicListen#ddbcd819 id:InputDocument listened_duration:int = Bool; +messages.addPollAnswer#19bc4b6d peer:InputPeer msg_id:int answer:PollAnswer = Updates; +messages.deletePollAnswer#ac8505a5 peer:InputPeer msg_id:int option:bytes = Updates; +messages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; +messages.setBotGuestChatResult#b8f106e3 query_id:long result:InputBotInlineResult = InputBotInlineMessageID; +messages.deleteParticipantReactions#a0b80cf8 peer:InputPeer participant:InputPeer = Bool; +messages.deleteParticipantReaction#e3b7f82c peer:InputPeer msg_id:int participant:InputPeer = Updates; +messages.getPersonalChannelHistory#55fb0996 user_id:InputUser limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.getRichMessage#501569cf peer:InputPeer id:int = messages.Messages; +messages.translateRichMessage#1a542004 flags:# peer:flags.0?InputPeer id:flags.0?Vector text:flags.1?Vector to_lang:string tone:flags.2?string = messages.TranslatedRichMessage; +messages.composeRichMessageWithAI#8d7ae6af flags:# proofread:flags.0?true emojify:flags.3?true text:flags.4?InputRichMessage translate_to_lang:flags.1?string tone:flags.2?InputAiComposeTone = messages.ComposedRichMessageWithAI; +messages.requestChatJoinWebView#ba9ee679 flags:# query_id:long theme_params:flags.0?DataJSON platform:string = WebViewResult; +messages.setShareTheme#698a9b1a peer:InputPeer theme:ShareTheme = Bool; +messages.getShareThemeBackdrops#f4599ef2 hash:int = ShareThemeBackdrops; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; @@ -2266,18 +2910,18 @@ channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = ch channels.getChannels#a7f6bbb id:Vector = messages.Chats; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; channels.createChannel#91006707 flags:# broadcast:flags.0?true megagroup:flags.1?true for_import:flags.3?true forum:flags.5?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string ttl_period:flags.4?int = Updates; -channels.editAdmin#d33c8902 channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:string = Updates; +channels.editAdmin#9a98ad68 flags:# channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:flags.0?string = Updates; channels.editTitle#566decd0 channel:InputChannel title:string = Updates; channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; channels.updateUsername#3514b3de channel:InputChannel username:string = Bool; -channels.joinChannel#24b524c5 channel:InputChannel = Updates; +channels.joinChannel#7f6a1e22 channel:InputChannel = messages.ChatInviteJoinResult; channels.leaveChannel#f836aa95 channel:InputChannel = Updates; channels.inviteToChannel#c9e33d54 channel:InputChannel users:Vector = messages.InvitedUsers; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; channels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink; -channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; -channels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true for_personal:flags.2?true = messages.Chats; +channels.toggleSignatures#418d549c flags:# signatures_enabled:flags.0?true profiles_enabled:flags.1?true channel:InputChannel = Updates; +channels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true for_personal:flags.2?true for_community_peer:flags.3?true = messages.Chats; channels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates; channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector max_id:long min_id:long limit:int = channels.AdminLogResults; channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool; @@ -2287,41 +2931,34 @@ channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Upd channels.getLeftChannels#8341ecc0 offset:int = messages.Chats; channels.getGroupsForDiscussion#f5dad378 = messages.Chats; channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool; -channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates; channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool; channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; channels.getInactiveChannels#11e831ee = messages.InactiveChats; channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; -channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool; -channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages; -channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; +channels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true for_live_stories:flags.1?true peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; -channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; +channels.toggleJoinRequest#ecc2618 flags:# apply_to_invites:flags.1?true channel:InputChannel enabled:Bool guard_bot:flags.0?InputUser = Updates; channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector = Bool; channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool; channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool; -channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates; -channels.createForumTopic#f40c0224 flags:# channel:InputChannel title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates; -channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; -channels.getForumTopicsByID#b0831eb9 channel:InputChannel topics:Vector = messages.ForumTopics; -channels.editForumTopic#f4dfa185 flags:# channel:InputChannel topic_id:int title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = Updates; -channels.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinned:Bool = Updates; -channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; -channels.reorderPinnedForumTopics#2950a18f flags:# force:flags.0?true channel:InputChannel order:Vector = Updates; +channels.toggleForum#3ff75734 channel:InputChannel enabled:Bool tabs:Bool = Updates; channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; -channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool; channels.updateColor#d8aa3671 flags:# for_profile:flags.1?true channel:InputChannel color:flags.2?int background_emoji_id:flags.0?long = Updates; channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates; channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats; channels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatus = Updates; channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int = Updates; channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; -channels.reportSponsoredMessage#af8ff6b9 channel:InputChannel random_id:bytes option:bytes = channels.SponsoredMessageReportResult; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; -channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; +channels.searchPosts#f2c4f24d flags:# hashtag:flags.0?string query:flags.1?string offset_rate:int offset_peer:InputPeer offset_id:int limit:int allow_paid_stars:flags.2?long = messages.Messages; +channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; +channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; +channels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User; +channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; +channels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -2339,6 +2976,30 @@ bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; +bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots; +bots.addPreviewMedia#17aeb75a bot:InputUser lang_code:string media:InputMedia = BotPreviewMedia; +bots.editPreviewMedia#8525606f bot:InputUser lang_code:string media:InputMedia new_media:InputMedia = BotPreviewMedia; +bots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector = Bool; +bots.reorderPreviewMedias#b627f3aa bot:InputUser lang_code:string order:Vector = Bool; +bots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo; +bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; +bots.updateUserEmojiStatus#ed9f30c5 user_id:InputUser emoji_status:EmojiStatus = Bool; +bots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool; +bots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool; +bots.getAdminedBots#b0711d83 = Vector; +bots.updateStarRefProgram#778b5ab3 flags:# bot:InputUser commission_permille:int duration_months:flags.0?int = StarRefProgram; +bots.setCustomVerification#f4946757 flags:# enabled:flags.1?true bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?TextWithEntities = Bool; +bots.getBotRecommendations#a1b70815 bot:InputUser = users.Users; +bots.checkUsername#e3ac4c61 flags:# additional:flags.0?true username:string = Bool; +bots.createBot#e5b17f2b flags:# via_deeplink:flags.0?true name:string username:string manager_id:InputUser = User; +bots.exportBotToken#bd0d99eb bot:InputUser revoke:Bool = bots.ExportedBotToken; +bots.requestWebViewButton#31a2a35e user_id:InputUser button:KeyboardButton = bots.RequestedButton; +bots.getRequestedWebViewButton#bf25b7f3 bot:InputUser webapp_req_id:string = KeyboardButton; +bots.getAccessSettings#213853a3 bot:InputUser = bots.AccessSettings; +bots.editAccessSettings#31813cd8 flags:# restricted:flags.0?true bot:InputUser add_users:flags.1?Vector = Bool; +bots.setJoinChatResults#e71a4810 query_id:long result:JoinChatBotResult = Bool; +bots.addUsername#3501c08a bot:InputUser username:string = Bool; +bots.removeUsername#c704147d bot:InputUser username:string = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -2350,17 +3011,72 @@ payments.getBankCardData#2e79d779 number:string = payments.BankCardData; payments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice; payments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates; payments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates; -payments.canPurchasePremium#9fc19eb6 purpose:InputStorePaymentPurpose = Bool; payments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector; payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; payments.getStarsTopupOptions#c00ec7d3 = Vector; -payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; -payments.getStarsTransactions#673ac2f9 flags:# inbound:flags.0?true outbound:flags.1?true peer:InputPeer offset:string = payments.StarsStatus; -payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payments.PaymentResult; +payments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus; +payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; +payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; +payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true ton:flags.1?true peer:InputPeer = payments.StarsRevenueStats; +payments.getStarsRevenueWithdrawalUrl#2433dc92 flags:# ton:flags.0?true peer:InputPeer amount:flags.1?long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl; +payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl; +payments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector = payments.StarsStatus; +payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector; +payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus; +payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool; +payments.fulfillStarsSubscription#cc5bebb3 peer:InputPeer subscription_id:string = Bool; +payments.getStarsGiveawayOptions#bd1efd3e = Vector; +payments.getStarGifts#c4563590 hash:int = payments.StarGifts; +payments.saveStarGift#2a2a697c flags:# unsave:flags.0?true stargift:InputSavedStarGift = Bool; +payments.convertStarGift#74bf076b stargift:InputSavedStarGift = Bool; +payments.botCancelStarsSubscription#6dfa0622 flags:# restore:flags.0?true user_id:InputUser charge_id:string = Bool; +payments.getConnectedStarRefBots#5869a553 flags:# peer:InputPeer offset_date:flags.2?int offset_link:flags.2?string limit:int = payments.ConnectedStarRefBots; +payments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; +payments.getSuggestedStarRefBots#d6b48f7 flags:# order_by_revenue:flags.0?true order_by_date:flags.1?true peer:InputPeer offset:string limit:int = payments.SuggestedStarRefBots; +payments.connectStarRefBot#7ed5348a peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; +payments.editConnectedStarRefBot#e4fca4a3 flags:# revoked:flags.0?true peer:InputPeer link:string = payments.ConnectedStarRefBots; +payments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgradePreview; +payments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates; +payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates; +payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; +payments.getSavedStarGifts#a319e569 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_unique:flags.4?true sort_by_value:flags.5?true exclude_upgradable:flags.7?true exclude_unupgradable:flags.8?true peer_color_available:flags.9?true exclude_hosted:flags.10?true peer:InputPeer collection_id:flags.6?int offset:string limit:int = payments.SavedStarGifts; +payments.getSavedStarGift#b455a106 stargift:Vector = payments.SavedStarGifts; +payments.getStarGiftWithdrawalUrl#c8ca817b flags:# stargift:InputSavedStarGift tmp_password:bytes = payments.StarGiftWithdrawalUrl; +payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; +payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector = Bool; +payments.canPurchaseStore#4fdc5ea7 purpose:InputStorePaymentPurpose = Bool; +payments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true for_craft:flags.4?true stars_only:flags.5?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector offset:string limit:int = payments.ResaleStarGifts; +payments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates; +payments.createStarGiftCollection#1f4a0e87 peer:InputPeer title:string stargift:Vector = StarGiftCollection; +payments.updateStarGiftCollection#4fddbee7 flags:# peer:InputPeer collection_id:int title:flags.0?string delete_stargift:flags.1?Vector add_stargift:flags.2?Vector order:flags.3?Vector = StarGiftCollection; +payments.reorderStarGiftCollections#c32af4cc peer:InputPeer order:Vector = Bool; +payments.deleteStarGiftCollection#ad5648e8 peer:InputPeer collection_id:int = Bool; +payments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections; +payments.getUniqueStarGiftValueInfo#4365af6b slug:string = payments.UniqueStarGiftValueInfo; +payments.checkCanSendGift#c0c4edc9 gift_id:long = payments.CheckCanSendGiftResult; +payments.getStarGiftAuctionState#5c9ff4d6 auction:InputStarGiftAuction version:int = payments.StarGiftAuctionState; +payments.getStarGiftAuctionAcquiredGifts#6ba2cbec gift_id:long = payments.StarGiftAuctionAcquiredGifts; +payments.getStarGiftActiveAuctions#a5d0514d hash:long = payments.StarGiftActiveAuctions; +payments.resolveStarGiftOffer#e9ce781c flags:# decline:flags.0?true offer_msg_id:int = Updates; +payments.sendStarGiftOffer#8fb86b41 flags:# peer:InputPeer slug:string price:StarsAmount duration:int random_id:long allow_paid_stars:flags.0?long = Updates; +payments.getStarGiftUpgradeAttributes#6d038b58 gift_id:long = payments.StarGiftUpgradeAttributes; +payments.getCraftStarGifts#fd05dd00 gift_id:long offset:string limit:int = payments.SavedStarGifts; +payments.craftStarGift#b0f9684f stargift:Vector = Updates; +payments.getCurrencyRates#d35983e8 = payments.CurrencyRates; +payments.getOnrampProviders#3f3e00dc flags:# crypto_currency:flags.0?string = Vector; +payments.getOnrampBaseCurrencies#1c8bedf3 provider:string crypto_currency:string = Vector; +payments.getOnrampAvailability#2ec29951 flags:# provider:string crypto_currency:string base_currency:flags.0?string = OnrampAvailability; +payments.getOnrampLimits#80e851fd flags:# provider:string crypto_currency:string base_currency:string payment_method:flags.0?string = OnrampLimits; +payments.getOnrampQuote#e06645fa flags:# provider:string crypto_currency:string base_currency:string base_amount:flags.0?string crypto_amount:flags.1?string payment_method:flags.2?string = OnrampQuote; +payments.createOnrampSession#a2197cf6 flags:# provider:string crypto_currency:string address:string payment_method:flags.0?string base_currency:flags.1?string base_amount:flags.2?string memo:flags.3?string theme:flags.4?string success_return_url:flags.5?string fail_return_url:flags.6?string crypto_amount:flags.7?string = OnrampSession; +payments.withdrawStarGift#ec2030ae flags:# keep_on_profile:flags.0?true stargift:InputSavedStarGift tmp_password:bytes = Bool; +payments.depositStarGift#c0a2ea9f flags:# hide_sender:flags.0?true peer:InputPeer slug:string = payments.StarGiftDepositPayload; +payments.displayStarGift#4aa5f0fe flags:# peer:flags.0?InputPeer slug:string = Bool; +payments.deleteStarGift#3cdaf8aa stargift:InputSavedStarGift = Bool; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; @@ -2384,11 +3100,11 @@ phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhon phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; phone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; -phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates; +phone.joinGroupCall#8fb53057 flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string public_key:flags.3?int256 block:flags.3?bytes params:DataJSON = Updates; phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; phone.discardGroupCall#7a777135 call:InputGroupCall = Updates; -phone.toggleGroupCallSettings#74bbb43d flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool = Updates; +phone.toggleGroupCallSettings#974392f2 flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool messages_enabled:flags.2?Bool send_paid_messages_stars:flags.3?long = Updates; phone.getGroupCall#41845db call:InputGroupCall limit:int = phone.GroupCall; phone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector sources:Vector offset:string limit:int = phone.GroupParticipants; phone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector = Vector; @@ -2403,8 +3119,20 @@ phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Boo phone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates; phone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates; phone.getGroupCallStreamChannels#1ab21940 call:InputGroupCall = phone.GroupCallStreamChannels; -phone.getGroupCallStreamRtmpUrl#deb3abbf peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl; +phone.getGroupCallStreamRtmpUrl#5af4c73a flags:# live_story:flags.0?true peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl; phone.saveCallLog#41248786 peer:InputPhoneCall file:InputFile = Bool; +phone.createConferenceCall#7d0444bb flags:# muted:flags.0?true video_stopped:flags.2?true join:flags.3?true random_id:int public_key:flags.3?int256 block:flags.3?bytes params:flags.3?DataJSON = Updates; +phone.deleteConferenceCallParticipants#8ca60525 flags:# only_left:flags.0?true kick:flags.1?true call:InputGroupCall ids:Vector block:bytes = Updates; +phone.sendConferenceCallBroadcast#c6701900 call:InputGroupCall block:bytes = Updates; +phone.inviteConferenceCallParticipant#bcf22685 flags:# video:flags.0?true call:InputGroupCall user_id:InputUser = Updates; +phone.declineConferenceCallInvite#3c479971 msg_id:int = Updates; +phone.getGroupCallChainBlocks#ee9f88a6 call:InputGroupCall sub_chain_id:int offset:int limit:int = Updates; +phone.sendGroupCallMessage#b1d11410 flags:# call:InputGroupCall random_id:long message:TextWithEntities allow_paid_stars:flags.0?long send_as:flags.1?InputPeer = Updates; +phone.sendGroupCallEncryptedMessage#e5afa56d call:InputGroupCall encrypted_message:bytes = Bool; +phone.deleteGroupCallMessages#f64f54f7 flags:# report_spam:flags.0?true call:InputGroupCall messages:Vector = Updates; +phone.deleteGroupCallParticipantMessages#1dbfeca0 flags:# report_spam:flags.0?true call:InputGroupCall participant:InputPeer = Updates; +phone.getGroupCallStars#6f636302 call:InputGroupCall = phone.GroupCallStars; +phone.saveDefaultSendAs#4167add1 call:InputGroupCall send_as:InputPeer = Bool; langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; @@ -2421,9 +3149,7 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; -stats.getBroadcastRevenueStats#75dfb671 flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastRevenueStats; -stats.getBroadcastRevenueWithdrawalUrl#2a65ef73 channel:InputChannel password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl; -stats.getBroadcastRevenueTransactions#69280f channel:InputChannel offset:int limit:int = stats.BroadcastRevenueTransactions; +stats.getPollStats#c27dfa68 flags:# dark:flags.0?true peer:InputPeer msg_id:int = stats.PollStats; chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector = chatlists.ExportedChatlistInvite; chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; @@ -2437,9 +3163,9 @@ chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; -stories.canSendStory#c7dfdfdd peer:InputPeer = Bool; -stories.sendStory#e4e6694b flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int = Updates; -stories.editStory#b583ba46 flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; +stories.canSendStory#30eb63f0 peer:InputPeer = stories.CanSendStoryCount; +stories.sendStory#8f9e6898 flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int albums:flags.8?Vector music:flags.9?InputDocument = Updates; +stories.editStory#2c63a72b flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector music:flags.4?InputDocument = Updates; stories.deleteStories#ae59db5f peer:InputPeer id:Vector = Vector; stories.togglePinned#9a75a1ef peer:InputPeer id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; @@ -2452,16 +3178,24 @@ stories.incrementStoryViews#b2028afb peer:InputPeer id:Vector = Bool; stories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true forwards_first:flags.3?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; stories.getStoriesViews#28e16cc8 peer:InputPeer id:Vector = stories.StoryViews; stories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink; -stories.report#1923fa8c peer:InputPeer id:Vector reason:ReportReason message:string = Bool; +stories.report#19d8eb45 peer:InputPeer id:Vector option:bytes message:string = ReportResult; stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; stories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates; stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories; stories.getAllReadPeerStories#9b5ae7f9 = Updates; -stories.getPeerMaxIDs#535983c3 id:Vector = Vector; +stories.getPeerMaxIDs#78499170 id:Vector = Vector; stories.getChatsToSend#a56a8b60 = messages.Chats; stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; stories.getStoryReactionsList#b9b2881f flags:# forwards_first:flags.2?true peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = stories.StoryReactionsList; stories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector = Bool; +stories.searchPosts#d1810907 flags:# hashtag:flags.0?string area:flags.1?MediaArea peer:flags.2?InputPeer offset:string limit:int = stories.FoundStories; +stories.createAlbum#a36396e5 peer:InputPeer title:string stories:Vector = StoryAlbum; +stories.updateAlbum#5e5259b6 flags:# peer:InputPeer album_id:int title:flags.0?string delete_stories:flags.1?Vector add_stories:flags.2?Vector order:flags.3?Vector = StoryAlbum; +stories.reorderAlbums#8535fbd9 peer:InputPeer order:Vector = Bool; +stories.deleteAlbum#8d3456d0 peer:InputPeer album_id:int = Bool; +stories.getAlbums#25b3eac7 peer:InputPeer hash:long = stories.Albums; +stories.getAlbumStories#ac806d61 peer:InputPeer album_id:int offset:int limit:int = stories.Stories; +stories.startLive#d069ccde flags:# pinned:flags.2?true noforwards:flags.4?true rtmp_stream:flags.5?true peer:InputPeer caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long messages_enabled:flags.6?Bool send_paid_messages_stars:flags.7?long = Updates; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getMyBoosts#be77b4a = premium.MyBoosts; @@ -2479,4 +3213,61 @@ smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; -// LAYER 181 +aicompose.createTone#4aa83913 flags:# display_author:flags.0?true emoji_id:long title:string prompt:string = AiComposeTone; +aicompose.updateTone#903bcf59 flags:# tone:InputAiComposeTone display_author:flags.0?Bool emoji_id:flags.1?long title:flags.2?string prompt:flags.3?string = AiComposeTone; +aicompose.saveTone#1782cbb1 tone:InputAiComposeTone unsave:Bool = Bool; +aicompose.deleteTone#dd39316a tone:InputAiComposeTone = Bool; +aicompose.getTone#b2e8ba03 tone:InputAiComposeTone = aicompose.Tones; +aicompose.getTones#abd59201 hash:long = aicompose.Tones; +aicompose.getToneExample#d1b4ab14 tone:InputAiComposeTone num:int = AiComposeToneExample; + +communities.create#a63859ec flags:# hidden:flags.1?true title:string about:flags.0?string peer:InputPeer = Updates; +communities.togglePeerLink#736dcfea flags:# visible:flags.0?true hidden:flags.1?true deleted:flags.2?true community:InputChannel peer:InputPeer = Bool; +communities.getJoinedCommunities#a663e830 = messages.Chats; +communities.toggleCommunityCollapsedInDialogs#d766e3ea flags:# collapsed:flags.0?true community:InputChannel = Updates; +communities.getPeerLinkRequests#93773344 community:InputChannel offset:string limit:int = communities.PeerLinkRequests; +communities.togglePeerLinkRequestApproval#8c8219a8 flags:# reject:flags.0?true community:InputChannel peer:InputPeer = Bool; +communities.toggleAllPeerLinkRequestApproval#bfe3dd3d flags:# reject:flags.0?true community:InputChannel = Bool; +communities.toggleParticipantBanned#9967ad0f flags:# unban:flags.0?true community:InputChannel participant:InputPeer = Bool; +communities.getParticipantJoinedChats#f87eabab community:InputChannel participant:InputPeer = communities.ParticipantJoinedChats; + +ephemeral.sendMessage#ba8d5f35 flags:# invert_media:flags.6?true welcome:flags.7?true anchor:flags.9?true noforwards:flags.10?true peer:flags.8?InputPeer receiver_id:InputUser query_id:flags.0?long message:string entities:flags.1?Vector media:flags.2?InputMedia reply_markup:flags.3?ReplyMarkup rich_message:flags.4?InputRichMessage random_id:long reply_to:flags.5?InputReplyTo = Updates; +ephemeral.deleteMessage#92f6e797 flags:# peer:flags.0?InputPeer receiver_id:InputUser id:int = Bool; +ephemeral.reportMessage#8704f2bf peer:InputPeer id:int option:bytes message:string = ReportResult; +ephemeral.getCallbackAnswer#3fa464c8 flags:# peer:InputPeer id:int data:flags.1?bytes = messages.BotCallbackAnswer; +ephemeral.editMessage#cf9c725b flags:# invert_media:flags.5?true welcome:flags.6?true peer:flags.7?InputPeer receiver_id:InputUser id:int message:flags.0?string media:flags.3?InputMedia entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup rich_message:flags.4?InputRichMessage = Updates; +ephemeral.deleteWelcomeMessage#e882a9e1 peer:InputPeer id:int = Bool; +ephemeral.deleteAllWelcomeMessages#734f9721 peer:InputPeer = Bool; +ephemeral.getWelcomeMessages#db9ac18d peer:InputPeer hash:long = ephemeral.WelcomeMessages; + +toncenter.performApiRequest#8d7bdd61 flags:# post:flags.0?true endpoint:string query:flags.1?string payload:flags.2?string = toncenter.ApiResponse; +toncenter.getStreamingUrl#cdaf63c7 = toncenter.StreamingUrl; + +wallet.getState#ab83b33a = WalletState; +wallet.getUserAddresses#5275dfdd flags:# force:flags.0?true id:Vector addresses:Vector = wallet.UserAddresses; +wallet.getTransactions#a8830a83 flags:# inbound:flags.0?true outbound:flags.1?true offset:string limit:int = wallet.Transactions; +wallet.getTransactionsByIDs#811ceab6 id:Vector = wallet.Transactions; +wallet.getTransactionsByMsgHash#a6bb795d msg_hash:Vector = wallet.Transactions; +wallet.exportSecretPhrase#9077c9ac flags:# password:flags.0?InputCheckPasswordSRP = wallet.SecretPhraseParts; +wallet.fetchEncryptedSecretPhrasePart#f53925cf token:string public_key:bytes = wallet.EncryptedSecretPhrasePart; +wallet.replaceWallet#d8c72eec flags:# wallet:InputWalletReplacement password:flags.0?InputCheckPasswordSRP = WalletState; +wallet.disableBackup#b0bf0da flags:# password:flags.0?InputCheckPasswordSRP new_public_key:flags.1?bytes proof:flags.2?WalletOwnershipProof = WalletState; +wallet.getBackupHolderDcs#d179d494 = Vector; +wallet.enableBackup#4500c525 flags:# parts:Vector new_public_key:flags.1?bytes proof:flags.2?WalletOwnershipProof = WalletState; +wallet.getProofChallenge#2025e697 = wallet.ProofChallenge; +wallet.sendTransfer#d9e5d9e2 flags:# data_normal:bytes data_gasless:flags.0?bytes user_id:InputUser random_id:long = Updates; +wallet.getGaslessInfo#742df0a1 = Updates; +wallet.getExistingWaltBalance#691f4360 = wallet.ExistingBalance; +wallet.getNfts#8c12f3a4 offset:string limit:int = wallet.NftItems; +wallet.tonConnectCreateSession#cc931046 dapp_client_id:string manifest_url:string = TonConnectSession; +wallet.tonConnectRegisterKey#338bcf1c session_id:long client_id:string = wallet.TonConnectChallenge; +wallet.tonConnectSubmitConnectResult#c2c00779 flags:# error:flags.0?true session_id:long challenge_answer:bytes body:bytes trace_id:flags.1?string = Bool; +wallet.tonConnectGetPending#11aee065 flags:# dapp_client_id:flags.0?string session_id:flags.1?long = wallet.TonConnectPending; +wallet.tonConnectClaimRequest#66b57b41 flags:# declined:flags.1?true session_id:long msg_id:int app_request_id:string challenge_answer:flags.0?bytes = Bool; +wallet.tonConnectSubmitResponse#fac67d3c flags:# session_id:long msg_id:int body:bytes trace_id:flags.0?string = Bool; +wallet.tonConnectNextEventId#775d7244 session_id:long = TonConnectNextEventId; +wallet.tonConnectCloseSession#d63f61ce flags:# session_id:long body:flags.0?bytes = Bool; +wallet.tonConnectGetSessions#a7ffb56e = wallet.TonConnectSessions; +wallet.setCardStickers#68509ac flags:# stickers:Vector bg_nft_address:flags.0?string = wallet.Card; + +// LAYER 232 diff --git a/compiler/api/source/provenance.json b/compiler/api/source/provenance.json new file mode 100644 index 000000000..396cc8399 --- /dev/null +++ b/compiler/api/source/provenance.json @@ -0,0 +1,23 @@ +{ + "comparison": { + "changed": [], + "missing_from_tdlib": [], + "shared": 2578 + }, + "desktop": { + "path": "Telegram/SourceFiles/mtproto/scheme/api.tl", + "repository": "telegramdesktop/tdesktop", + "revision": "f985723b552b30fe1c17d377d57f44e61a15a8c4", + "sha256": "76922ce265441d3f2f5188f56b69b41e1cb11354657ea547707507f8c3b93826", + "url": "https://raw.githubusercontent.com/telegramdesktop/tdesktop/f985723b552b30fe1c17d377d57f44e61a15a8c4/Telegram/SourceFiles/mtproto/scheme/api.tl" + }, + "layer": 232, + "normalized_sha256": "bae214875b52d4d1a52482ca3c644c5e5ce295306f7c43037b494313e3152782", + "tdlib": { + "path": "td/generate/scheme/telegram_api.tl", + "repository": "tdlib/td", + "revision": "4c6377aa7386c39217878a9a56d581922ad37ca4", + "sha256": "6628b4c4457112b819f4bd35104170b3610822eed4a54240a304adb28cb66ec6", + "url": "https://raw.githubusercontent.com/tdlib/td/4c6377aa7386c39217878a9a56d581922ad37ca4/td/generate/scheme/telegram_api.tl" + } +} diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 03ec4054a..bd0961045 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -17,7 +17,6 @@ # along with Hydrogram. If not, see . import ast -import os import re import shutil from pathlib import Path @@ -48,8 +47,8 @@ def generate(source_path: Path, base_name: str): def build(path: Path, level=0): last = path.name - for i in os.listdir(path): - if not i.startswith("__"): + for i in path.iterdir(): + if not i.name.startswith("__"): item_path = path / i if item_path.is_dir(): build(item_path, level=level + 1) @@ -97,7 +96,7 @@ def build(path: Path, level=0): v = sorted(v) entities = [] - entities = [f'{i} <{snake(i).replace("_", "-")}>' for i in v] + entities = [f"{i} <{snake(i).replace('_', '-')}>" for i in v] if k != base_name: inner_path = Path(base_name, k, "index.rst") @@ -238,6 +237,7 @@ def get_title_list(s: str) -> list[str]: get_send_as_chats set_send_as_chat set_chat_protected_content + transfer_chat_ownership """, "users": """ Users @@ -332,6 +332,12 @@ def get_title_list(s: str) -> list[str]: resolve_peer save_file """, + "phone": """ + Phone: + create_video_chat + discard_group_call + invite_group_call_members + """, } root = API_DOCS_DEST_PATH / "methods" @@ -488,6 +494,7 @@ def get_title_list(s: str) -> list[str]: "input_message_content": """ InputMessageContent InputMessageContent + InputPollOption InputTextMessageContent """, "authorization": """ diff --git a/compiler/docs/template/methods.rst b/compiler/docs/template/methods.rst index e9673f82d..dde59efea 100644 --- a/compiler/docs/template/methods.rst +++ b/compiler/docs/template/methods.rst @@ -151,6 +151,19 @@ Authorization {authorization} +Phone +----- + +.. autosummary:: + :nosignatures: + + {phone} + +.. toctree:: + :hidden: + + {phone} + Advanced -------- diff --git a/compiler/errors/compiler.py b/compiler/errors/compiler.py index a0d0017ac..4b6693b51 100644 --- a/compiler/errors/compiler.py +++ b/compiler/errors/compiler.py @@ -18,7 +18,6 @@ # along with Hydrogram. If not, see . import csv -import os import re import shutil from pathlib import Path @@ -45,7 +44,7 @@ def start(): shutil.rmtree(ERRORS_DEST_PATH, ignore_errors=True) ERRORS_DEST_PATH.mkdir(parents=True) - files = os.listdir(f"{ERRORS_HOME_PATH}/source") + files = sorted(i.name for i in (ERRORS_HOME_PATH / "source").glob("*.tsv")) with NOTICE_PATH.open(encoding="utf-8") as f: notice = [f"# {line}".strip() for line in f] @@ -82,7 +81,7 @@ def start(): super_class = camel(name) name = " ".join([ - i.capitalize() for i in re.sub(r"_", " ", name).lower().split(" ") + i.capitalize() for i in name.replace(r"_", " ").lower().split(" ") ]) sub_classes = [] @@ -100,9 +99,9 @@ def start(): error_id, error_message = row - sub_class = camel(re.sub(r"_X", "_", error_id)) + sub_class = camel(error_id.replace(r"_X", "_")) sub_class = re.sub(r"^2", "Two", sub_class) - sub_class = re.sub(r" ", "", sub_class) + sub_class = sub_class.replace(r" ", "") f_all.write(f' "{error_id}": "{sub_class}",\n') @@ -144,7 +143,7 @@ def start(): content = f.read() with (ERRORS_DEST_PATH / "all.py").open("w", encoding="utf-8") as f: - f.write(re.sub("{count}", str(count), content)) # noqa: RUF027 + f.write(re.sub(r"{count}", str(count), content)) # noqa: RUF027 if __name__ == "__main__": diff --git a/compiler/errors/source/406_NOT_ACCEPTABLE.tsv b/compiler/errors/source/406_NOT_ACCEPTABLE.tsv index 6f69c7289..8e283bee6 100644 --- a/compiler/errors/source/406_NOT_ACCEPTABLE.tsv +++ b/compiler/errors/source/406_NOT_ACCEPTABLE.tsv @@ -11,6 +11,8 @@ GIFTCODE_NOT_ALLOWED Giftcode not allowed INVITE_HASH_EXPIRED The chat the user tried to join has expired and is not valid anymore PHONE_NUMBER_INVALID The phone number is invalid PHONE_PASSWORD_FLOOD You have tried to log-in too many times +POLL_COUNTRY_RESTRICTED Your country is not allowed to vote in this poll. +POLL_MEMBER_RESTRICTED Only channel subscribers can vote in this poll. PREMIUM_CURRENTLY_UNAVAILABLE Premium currently unavailable PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN Similar to a flood wait, must wait {value} minutes SEND_CODE_UNAVAILABLE Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend) diff --git a/dev_tools/check_api_schema_updates.py b/dev_tools/check_api_schema_updates.py new file mode 100755 index 000000000..0920d29dd --- /dev/null +++ b/dev_tools/check_api_schema_updates.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +"""Check schema drift (0=current, 2=drift, 1=error); apply only after validation.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import TYPE_CHECKING + +import requests + +if TYPE_CHECKING: + from collections.abc import Mapping + +REPO_HOME = Path(__file__).resolve().parent.parent +SCHEMA_PATH = Path("compiler/api/source/main_api.tl") +PROVENANCE_PATH = Path("compiler/api/source/provenance.json") +DESKTOP_REPO = "telegramdesktop/tdesktop" +DESKTOP_PATH = "Telegram/SourceFiles/mtproto/scheme/api.tl" +TDLIB_REPO = "tdlib/td" +TDLIB_PATH = "td/generate/scheme/telegram_api.tl" +LAYER_RE = re.compile(r"^//\s*LAYER\s+(\d+)\s*$", re.MULTILINE) +DEFINITION_RE = re.compile(r"^([\w.]+)#([0-9a-f]{1,8})\s+.*?=\s*([\w.<> ]+);$") +BASIC_DECLARATIONS = { + "boolFalse": "boolFalse#bc799737 = Bool;", + "boolTrue": "boolTrue#997275b5 = Bool;", + "true": "true#3fedd339 = True;", + "vector": "vector#1cb5c415 {t:Type} # [ t ] = Vector t;", + "error": "error#c4b9f9bb code:int text:string = Error;", + "null": "null#56730bcc = Null;", +} +BASIC_NAMES = BASIC_DECLARATIONS.keys() +GENERATED_PATHS = ( + "hydrogram/raw/all.py", + "hydrogram/raw/base", + "hydrogram/raw/types", + "hydrogram/raw/functions", + "hydrogram/errors/exceptions", +) +SCHEMA_HEADER = """// Source and checksums: provenance.json + +/////////////////////////////// +///////// Main application API +/////////////////////////////// + +---types--- + +// boolFalse#bc799737 = Bool; // Parsed manually +// boolTrue#997275b5 = Bool; // Parsed manually +// true#3fedd339 = True; // Not used +// vector#1cb5c415 {t:Type} # [ t ] = Vector t; // Parsed manually +// error#c4b9f9bb code:int text:string = Error; // Not used +// null#56730bcc = Null; // Parsed manually +""" + + +def sha256(content: bytes) -> str: + return hashlib.sha256(content).hexdigest() + + +def get_layer(schema: str) -> int: + layers = LAYER_RE.findall(schema) + if len(layers) != 1: + raise ValueError("Expected exactly one numeric // LAYER marker") + return int(layers[0]) + + +def definitions(schema: str, *, strict: bool = True) -> dict[str, str]: + """Parse declarations without silently dropping unsupported TL syntax.""" + result = {} + ids = set() + for original in schema.splitlines(): + line = original.split("//", 1)[0].strip() + if not line or line in {"---types---", "---functions---"}: + continue + match = DEFINITION_RE.fullmatch(line) + if not match: + if strict: + raise ValueError(f"Unsupported TL declaration: {line}") + continue # TDLib has extra primitive/test declarations without IDs. + name, constructor_id, _ = match.groups() + if name in result or (strict and int(constructor_id, 16) in ids): + raise ValueError(f"Duplicate TL name or constructor ID: {name}") + ids.add(int(constructor_id, 16)) + result[name] = " ".join(line.split()) + if not result: + raise ValueError("Schema contains no declarations") + return result + + +def normalize_schema(schema: str) -> str: + """Keep wire declarations verbatim; Hydrogram handles basic types itself.""" + layer = get_layer(schema) + if schema.count("---functions---") != 1 or schema.count("---types---") > 1: + raise ValueError("Expected one functions section and at most one types section") + declarations = definitions(schema) + if not declarations.keys() >= BASIC_NAMES: + raise ValueError("Upstream schema is missing basic types") + if any(declarations[name] != value for name, value in BASIC_DECLARATIONS.items()): + raise ValueError("Manually handled basic types changed upstream") + lines = [] + for original in schema.splitlines(): + line = original.strip() + if line == "---types---" or LAYER_RE.fullmatch(line): + continue + if (match := DEFINITION_RE.fullmatch(line)) and match[1] in BASIC_NAMES: + continue + lines.append(original.rstrip()) + content = "\n".join(lines).strip() + return f"{SCHEMA_HEADER}\n{content}\n\n// LAYER {layer}\n" + + +def fetch_source(repository: str, path: str, revision: str) -> tuple[str, dict]: + """Resolve a branch once, then download only from an immutable commit.""" + response = requests.get( + f"https://api.github.com/repos/{repository}/commits/{revision}", timeout=30 + ) + response.raise_for_status() + commit = response.json()["sha"] + if not re.fullmatch(r"[0-9a-f]{40}", commit): + raise ValueError("Invalid upstream commit SHA") + url = f"https://raw.githubusercontent.com/{repository}/{commit}/{path}" + response = requests.get(url, timeout=30) + response.raise_for_status() + data = response.content + return data.decode("utf-8"), { + "repository": repository, + "revision": commit, + "path": path, + "url": url, + "sha256": sha256(data), + } + + +def compare_sources(desktop: str, tdlib: str) -> dict: + primary = definitions(desktop) + secondary = definitions(tdlib, strict=False) + shared = primary.keys() & secondary.keys() + changed = sorted(name for name in shared if primary[name] != secondary[name]) + missing = sorted(primary.keys() - secondary.keys() - {"null"}) + return {"shared": len(shared), "changed": changed, "missing_from_tdlib": missing} + + +def schema_diff(old: str, new: str) -> dict[str, dict]: + before, after = definitions(old), definitions(new) + return { + "added": {k: after[k] for k in sorted(after.keys() - before.keys())}, + "removed": {k: before[k] for k in sorted(before.keys() - after.keys())}, + "changed": { + k: {"before": before[k], "after": after[k]} + for k in sorted(before.keys() & after.keys()) + if before[k] != after[k] + }, + } + + +def render_report(old: str, new: str, provenance: dict) -> str: + diff = schema_diff(old, new) + source = provenance["desktop"] + comparison = provenance["comparison"] + lines = [ + f"# MTProto schema: layer {get_layer(old)} → {get_layer(new)}", + "", + f"Source: {source['url']}", + f"SHA-256: `{source['sha256']}`", + f"TDLib: {provenance['tdlib']['url']}", + f"Shared declarations: {comparison['shared']}; disagreements: {comparison['changed']}; " + f"missing from TDLib: {comparison['missing_from_tdlib']}.", + "", + "Raw schema coverage does not imply high-level Client method or parser coverage.", + "Review changed IDs, flags, argument types, return types, and handwritten callers.", + "Review RPC errors separately against https://core.telegram.org/api/errors and affected " + "method pages; errors are not part of TL and have no complete layer-pinned catalogue.", + "Review compiler/api/docs.json against affected signatures; documentation may lag TL.", + "", + ] + for kind, entries in diff.items(): + lines.extend([f"## {kind.title()} ({len(entries)})", ""]) + for name, value in entries.items(): + lines.extend([f"### {name}", "```tl"]) + if kind == "changed": + lines.extend([value["before"], value["after"]]) + else: + lines.append(value) + lines.extend(["```", ""]) + lines.extend([ + "## Adoption", + "", + "Run `uv run python dev_tools/check_api_schema_updates.py --apply " + f"--revision {source['revision']} --tdlib-revision {provenance['tdlib']['revision']}`.", + "Application validates generation and the full offline test suite in a temporary checkout " + "before replacing inputs. If it fails, fix the compiler/callers and add regression tests; " + "rerun the pinned command. Add a news fragment and review the resulting diff.", + "Generated modules are ignored; only compiler inputs and handwritten changes belong in Git.", + "", + ]) + return "\n".join(lines) + + +def generated_hashes(root: Path) -> dict[str, str]: + result = {} + for relative in GENERATED_PATHS: + path = root / relative + files = [path] if path.is_file() else sorted(path.rglob("*.py")) + for file in files: + result[file.relative_to(root).as_posix()] = sha256(file.read_bytes()) + return result + + +def validate_generation(root: Path) -> None: + """Check repeatability both in-process and across different hash seeds.""" + generation = ( + "from compiler.api.compiler import start as api; " + "from compiler.errors.compiler import start as errors; api(); errors()" + ) + env = {**os.environ, "PYTHONPATH": str(root), "PYTHONHASHSEED": "1"} + repeated = ( + generation + + "; " + + ( + "from dev_tools.check_api_schema_updates import generated_hashes; " + "from pathlib import Path; before = generated_hashes(Path.cwd()); " + ) + + generation + + "; assert before == generated_hashes(Path.cwd()), 'In-process generation differs'" + ) + subprocess.run([sys.executable, "-c", repeated], cwd=root, env=env, check=True, timeout=120) + before = generated_hashes(root) + env["PYTHONHASHSEED"] = "2" + subprocess.run([sys.executable, "-c", generation], cwd=root, env=env, check=True, timeout=120) + if before != generated_hashes(root): + raise ValueError("Generated output differs across hash seeds") + subprocess.run( + [sys.executable, "-m", "pytest", "-q", "tests"], cwd=root, env=env, check=True, timeout=300 + ) + + +def validate_candidate(repo: Path, replacements: Mapping[Path, str]) -> None: + """Never overwrite the working inputs with an unvalidated network response.""" + with tempfile.TemporaryDirectory(prefix="hydrogram-schema-") as directory: + root = Path(directory) + for name in ("compiler", "hydrogram", "tests", "dev_tools"): + shutil.copytree( + repo / name, root / name, ignore=shutil.ignore_patterns("__pycache__", "*.pyc") + ) + for name in ("NOTICE", "pyproject.toml"): + shutil.copy2(repo / name, root / name) + for path, content in replacements.items(): + (root / path).write_text(content, encoding="utf-8") + validate_generation(root) + + +def check_provenance(repo: Path) -> None: + provenance = json.loads((repo / PROVENANCE_PATH).read_text(encoding="utf-8")) + content = (repo / SCHEMA_PATH).read_bytes() + if provenance["normalized_sha256"] != sha256(content): + raise ValueError("Schema checksum differs from provenance.json") + if provenance["layer"] != get_layer(content.decode("utf-8")): + raise ValueError("Schema layer differs from provenance.json") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--apply", action="store_true", help="Validate and apply the candidate") + parser.add_argument("--validate", action="store_true", help="Validate local inputs offline") + parser.add_argument("--revision", default="dev", help="Telegram Desktop branch or commit") + parser.add_argument("--tdlib-revision", default="master", help="TDLib branch or commit") + parser.add_argument("--report", type=Path, help="Write a Markdown adoption report") + parser.add_argument( + "--json-report", type=Path, help="Write provenance and declaration changes" + ) + args = parser.parse_args(argv) + try: + if args.validate: + check_provenance(REPO_HOME) + validate_candidate(REPO_HOME, {}) + return 0 + old = (REPO_HOME / SCHEMA_PATH).read_text(encoding="utf-8") + desktop, source = fetch_source(DESKTOP_REPO, DESKTOP_PATH, args.revision) + tdlib, secondary = fetch_source(TDLIB_REPO, TDLIB_PATH, args.tdlib_revision) + new = normalize_schema(desktop) + provenance = { + "layer": get_layer(new), + "desktop": source, + "tdlib": secondary, + "comparison": compare_sources(desktop, tdlib), + "normalized_sha256": sha256(new.encode("utf-8")), + } + if args.json_report: + args.json_report.write_text( + json.dumps({"provenance": provenance, "diff": schema_diff(old, new)}, indent=2) + + "\n", + encoding="utf-8", + ) + report = render_report(old, new, provenance) + if args.report: + args.report.write_text(report, encoding="utf-8") + else: + print(report) + if get_layer(new) < get_layer(old): + raise ValueError("Refusing a schema layer downgrade") + comparison = provenance["comparison"] + if comparison["changed"] or comparison["missing_from_tdlib"]: + raise ValueError( + "Telegram Desktop and TDLib disagree; review the report before adoption" + ) + drift = old != new + if not args.apply: + if not drift: + check_provenance(REPO_HOME) + return 2 if drift else 0 + # A metadata-only upstream commit must not create endless schema updates. + if not drift: + check_provenance(REPO_HOME) + validate_candidate(REPO_HOME, {}) + return 0 + replacements = { + SCHEMA_PATH: new, + PROVENANCE_PATH: json.dumps(provenance, indent=2, sort_keys=True) + "\n", + } + validate_candidate(REPO_HOME, replacements) + for path, content in replacements.items(): + destination = REPO_HOME / path + temporary = destination.with_suffix(destination.suffix + ".tmp") + temporary.write_text(content, encoding="utf-8") + temporary.replace(destination) + if args.report: + with args.report.open("a", encoding="utf-8") as report_file: + report_file.write( + "\nValidation: deterministic generation and all offline tests passed.\n" + ) + except ( + requests.RequestException, + ValueError, + KeyError, + OSError, + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + ) as error: + message = f"Schema update blocked: {error}" + print(message, file=sys.stderr) + if args.report: + with args.report.open("a", encoding="utf-8") as report_file: + report_file.write(f"\nValidation failed: {message}\nInputs were not updated.\n") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dev_tools/compare_to_bot_api.py b/dev_tools/compare_to_bot_api.py new file mode 100755 index 000000000..99c7c928a --- /dev/null +++ b/dev_tools/compare_to_bot_api.py @@ -0,0 +1,328 @@ +#!/bin/env python +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import inspect +import re +from typing import Literal + +import httpx +from lxml import html + +from hydrogram import Client, types + +# Item can be ignored entirely (True) or have specific fields ignored with optional aliases +IgnoreSpec = Literal[True] | dict[str, str | Literal[True]] + +# dict of items to ignore with optional field aliases +# fmt: off +ignored_items: dict[str, IgnoreSpec] = { + "*": { + "type": True, + "from": "from_user", + "thumbnail": "thumb", + }, + "Message": { + "message_id": "id", + }, + # Polling and webhook related methods and objects. Ignored for us. + "get_updates": True, + "set_webhook": True, + "delete_webhook": True, + "get_webhook_info": True, + "Update": True, + "WebhookInfo": True, +} +# fmt: on + + +def snake(s: str) -> str: + s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", s) + return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s).lower() + + +def check_field_ignored(field: str, ignore_spec: IgnoreSpec, hydrogram_fields: list[str]) -> bool: + """Check if a field should be ignored based on ignore specs""" + if field in hydrogram_fields: + return True + + if isinstance(ignore_spec, dict): + alias = ignore_spec.get(field) + if isinstance(alias, bool): + return True + if alias and alias in hydrogram_fields: + return True + + wildcard_spec = ignored_items.get("*", {}) + if isinstance(wildcard_spec, dict): + alias = wildcard_spec.get(field) + if isinstance(alias, bool): + return True + if alias and alias in hydrogram_fields: + return True + + return False + + +class BotAPISchema: + def __init__(self): + self.client = httpx.Client() + self.methods: dict[str, list[str]] = {} + self.objects: dict[str, list[str]] = {} + + def finish_parsing(self, obj_name: str, obj_fields: list[str]): + """Detects whether we're parsing a method or an object and stores it accordingly.""" + if obj_name[0].isupper(): + self.objects[obj_name] = obj_fields + else: + self.methods[snake(obj_name)] = obj_fields + + def parse(self): + """Parses the Telegram Bot API documentation to extract methods and objects.""" + response = self.client.get("https://core.telegram.org/bots/api") + response.raise_for_status() + + tree = html.fromstring(response.content) + content = tree.xpath('//div[@id="dev_page_content"]')[0] + + currently_parsing = "" + last_sec_fields: list[str] = [] + scraping = False + + for tag in content: + if not scraping and tag.xpath('.//a[@name="getting-updates"]'): + scraping = True + elif not scraping: + continue + + if tag.tag == "h4": + tag_content = tag.text_content().strip() + + # Telegram places other stuff alongside with methods and objects, just ignore them. + if " " in tag_content: + continue + + if currently_parsing and currently_parsing != tag_content: + self.finish_parsing(currently_parsing, last_sec_fields) + last_sec_fields = [] + + currently_parsing = tag_content + + elif tag.tag == "table": + rows = tag.xpath(".//tbody/tr") + for row in rows: + cells = row.xpath(".//td") + field_name = cells[0].text_content().strip() + last_sec_fields.append(field_name) + + if currently_parsing: + self.finish_parsing(currently_parsing, last_sec_fields) + + def get_schema(self): + """Returns the parsed methods and objects.""" + self.parse() + return self.methods, self.objects + + +class HydrogramSchema: + def __init__(self): + self.methods: dict[str, list[str]] = {} + self.objects: dict[str, list[str]] = {} + + def parse_methods(self): + """Parses methods in hydrogram.Client and extracts their arguments.""" + client_methods = inspect.getmembers(Client, predicate=inspect.isfunction) + for method_name, method in client_methods: + sig = inspect.signature(method) + args = [param.name for param in sig.parameters.values() if param.name != "self"] + self.methods[method_name] = args + + def parse_objects(self): + """Parses classes in hydrogram.types and extracts their arguments.""" + types_classes = inspect.getmembers(types, predicate=inspect.isclass) + for class_name, cls in types_classes: + if hasattr(cls, "__init__"): + sig = inspect.signature(cls.__init__) + args = [param.name for param in sig.parameters.values() if param.name != "self"] + self.objects[class_name] = args + + def parse(self): + """Parses both methods and objects.""" + self.parse_methods() + self.parse_objects() + + def get_schema(self): + """Returns the parsed methods and objects.""" + self.parse() + return self.methods, self.objects + + +def compare_item( + bot_api_item: str, + bot_api_fields: list[str], + hydrogram_fields: list[str], + ignore_spec: IgnoreSpec, +) -> list[str]: + """Compare fields between bot API and hydrogram implementations""" + if ignore_spec is True: + return [] + + return [ + field + for field in bot_api_fields + if not check_field_ignored(field, ignore_spec, hydrogram_fields) + ] + + +def compare_schemas(bot_api_methods, bot_api_objects, hydrogram_methods, hydrogram_objects): + missing_methods = {} + missing_objects = {} + method_mismatches = {} + object_mismatches = {} + + # Compare methods + for method_name, bot_api_args in bot_api_methods.items(): + ignore_spec = ignored_items.get(method_name, {}) + if ignore_spec is True: + continue + + if method_name not in hydrogram_methods: + missing_methods[method_name] = bot_api_args + else: + missing_args = compare_item( + method_name, bot_api_args, hydrogram_methods[method_name], ignore_spec + ) + if missing_args: + method_mismatches[method_name] = missing_args + + # Compare objects + for obj_name, bot_api_fields in bot_api_objects.items(): + ignore_spec = ignored_items.get(obj_name, {}) + if ignore_spec is True: + continue + + if obj_name not in hydrogram_objects: + missing_objects[obj_name] = bot_api_fields + else: + missing_fields = compare_item( + obj_name, bot_api_fields, hydrogram_objects[obj_name], ignore_spec + ) + if missing_fields: + object_mismatches[obj_name] = missing_fields + + return { + "missing_methods": missing_methods, + "missing_objects": missing_objects, + "method_mismatches": method_mismatches, + "object_mismatches": object_mismatches, + } + + +def generate_implementation_stats(total: int, unimplemented: int, partial: int) -> tuple[int, int]: + """Calculate implementation statistics""" + implemented = total - unimplemented + fully_implemented = implemented - partial + return implemented, fully_implemented + + +def generate_report( + comparison_results, bot_api_methods, bot_api_objects, hydrogram_methods, hydrogram_objects +): + report = [] + + partially_implemented_methods = comparison_results["method_mismatches"] + partially_implemented_objects = comparison_results["object_mismatches"] + unimplemented_methods = comparison_results["missing_methods"] + unimplemented_objects = comparison_results["missing_objects"] + + report.append("## ❌ Unimplemented features") + + if unimplemented_methods: + report.append("\n### Methods") + report.extend(f"- {method_name}" for method_name in unimplemented_methods) + + if unimplemented_objects: + report.append("\n### Objects") + report.extend(f"- {obj_name}" for obj_name in unimplemented_objects) + + total_methods = len(bot_api_methods) + total_objects = len(bot_api_objects) + + implemented_methods, fully_implemented_methods = generate_implementation_stats( + total_methods, len(unimplemented_methods), len(partially_implemented_methods) + ) + + implemented_objects, fully_implemented_objects = generate_implementation_stats( + total_objects, len(unimplemented_objects), len(partially_implemented_objects) + ) + + report.append("## 🚧 Partially implemented features") + + if partially_implemented_methods: + report.append("\n### Methods") + for method_name, missing_args in partially_implemented_methods.items(): + total_args = len(bot_api_methods[method_name]) + implemented_args = total_args - len(missing_args) + report.append(f"\n#### {method_name} ({implemented_args}/{total_args}):") + report.extend(f"- {arg}" for arg in missing_args) + report.append("") + + if partially_implemented_objects: + report.append("\n### Objects") + for obj_name, missing_fields in partially_implemented_objects.items(): + total_fields = len(bot_api_objects[obj_name]) + implemented_fields = total_fields - len(missing_fields) + report.append(f"\n#### {obj_name} ({implemented_fields}/{total_fields}):") + report.extend(f"- {field}" for field in missing_fields) + report.append("") + + report.append("") + + report.extend([ + f"At least partially implemented methods: {implemented_methods}/{total_methods} ({(implemented_methods / total_methods) * 100:.0f}%)", + f"At least partially implemented objects: {implemented_objects}/{total_objects} ({(implemented_objects / total_objects) * 100:.0f}%)", + ]) + + report.append("") + + report.extend([ + f"Fully implemented methods: {fully_implemented_methods}/{total_methods} ({(fully_implemented_methods / total_methods) * 100:.0f}%)", + f"Fully implemented objects: {fully_implemented_objects}/{total_objects} ({(fully_implemented_objects / total_objects) * 100:.0f}%)", + ]) + + return "\n".join(report) + + +if __name__ == "__main__": + bot_api_parser = BotAPISchema() + bot_api_parser.parse() + bot_api_methods, bot_api_objects = bot_api_parser.get_schema() + + hydrogram_parser = HydrogramSchema() + hydrogram_parser.parse() + hydrogram_methods, hydrogram_objects = hydrogram_parser.get_schema() + + comparison_results = compare_schemas( + bot_api_methods, bot_api_objects, hydrogram_methods, hydrogram_objects + ) + report = generate_report( + comparison_results, bot_api_methods, bot_api_objects, hydrogram_methods, hydrogram_objects + ) + print(report) diff --git a/dev_tools/generate_docs_json.py b/dev_tools/generate_docs_json.py index 88f18bd2d..0da0b14f6 100755 --- a/dev_tools/generate_docs_json.py +++ b/dev_tools/generate_docs_json.py @@ -89,7 +89,12 @@ async def get_object_data(it_type: str, it_name: str, doc_dict: dict[str, dict]) tree = html.fromstring(request.text) - page_content = tree.xpath("//div[@id='dev_page_content'][1]")[0] + page_content_xp = tree.xpath("//div[@id='dev_page_content'][1]") + if not page_content_xp: + print(f"No page content for {it_type}/{it_name}") + return + + page_content = page_content_xp[0] # Get the description of the object - always used desc_xp = page_content.xpath("./p[1]") @@ -98,27 +103,27 @@ async def get_object_data(it_type: str, it_name: str, doc_dict: dict[str, dict]) desc = desc_xp[0].text_content().strip() else: print(f"No description for {it_type}/{it_name}") - desc = None + desc = "" if it_type == "type": doc_dict["type"][it_name] = {"desc": desc} elif it_type in {"constructor", "method"}: - params_xp = ( - page_content.xpath("./h3/a[@id='parameters'][1]")[0] - .getparent() - .getnext() - .xpath("./tbody[1]") - ) - - if params_xp: - params = { - x.getchildren()[0].text_content().strip(): x.getchildren()[2] - .text_content() - .strip() - for x in params_xp[0].xpath("./tr") - } + params_link_xp = page_content.xpath("./h3/a[@id='parameters'][1]") + if params_link_xp: + params_xp = params_link_xp[0].getparent().getnext().xpath("./tbody[1]") + if params_xp: + params = { + x.getchildren()[0].text_content().strip(): x + .getchildren()[2] + .text_content() + .strip() + for x in params_xp[0].xpath("./tr") + } + else: + print(f"No parameters for {it_type}/{it_name}") + params = {} else: - print(f"No parameters for {it_type}/{it_name}") + print(f"No parameters section for {it_type}/{it_name}") params = {} doc_dict[it_type][it_name] = {"desc": desc, "params": params} diff --git a/docs/source/api/decorators.rst b/docs/source/api/decorators.rst index 6ddd9ddce..26db3fe14 100644 --- a/docs/source/api/decorators.rst +++ b/docs/source/api/decorators.rst @@ -61,3 +61,4 @@ Details .. autodecorator:: hydrogram.Client.on_poll() .. autodecorator:: hydrogram.Client.on_disconnect() .. autodecorator:: hydrogram.Client.on_raw_update() +.. autodecorator:: hydrogram.Client.on_error() diff --git a/docs/source/conf.py b/docs/source/conf.py index a15a2f52d..9a49b849b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,17 +1,38 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + import datetime import sys from pathlib import Path +# Setup path for imports docs_dir = Path(__file__).parent.parent sys.path.insert(0, docs_dir.resolve().as_posix()) import hydrogram # noqa: E402 +# Project information project = "Hydrogram" author = "Hydrogram" copyright = f"{datetime.date.today().year}, {author}" release = hydrogram.__version__ +# Sphinx extensions extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", @@ -22,44 +43,104 @@ "sphinxcontrib.towncrier.ext", ] +# Intersphinx mapping for external documentation intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "aiosqlite": ("https://aiosqlite.omnilib.dev/en/stable/", None), } +# Basic configuration +master_doc = "index" +source_suffix = ".rst" +pygments_style = "friendly" + +# Documentation generation settings +autodoc_member_order = "bysource" +autodoc_typehints = "none" html_use_modindex = False html_use_index = False +# Napoleon settings (for Google-style docstrings) napoleon_use_rtype = False napoleon_use_param = False -master_doc = "index" -source_suffix = ".rst" -autodoc_member_order = "bysource" -autodoc_typehints = "none" - +# Towncrier settings towncrier_draft_autoversion_mode = "draft" towncrier_draft_include_empty = True towncrier_draft_working_directory = Path(__file__).parent.parent.parent -pygments_style = "friendly" - +# HTML theme settings html_theme = "furo" html_title = f"{project} v{release} Documentation" html_last_updated_fmt = ( f"{datetime.datetime.now(tz=datetime.UTC).strftime('%d/%m/%Y, %H:%M:%S')} UTC" ) - html_copy_source = False - html_static_path = ["_static"] html_css_files = [ "css/all.min.css", "css/custom.css", ] +# Icons for footer (from https://react-icons.github.io/react-icons) +TELEGRAM_ICON = ( + '' + '' +) + +GITHUB_ICON = ( + '' + "" +) + +WEBSITE_ICON = ( + '' + '' +) + +# Theme options configuration html_theme_options = { "navigation_with_keys": True, + # CSS variables "dark_css_variables": { "admonition-title-font-size": "0.95rem", "admonition-font-size": "0.92rem", @@ -68,73 +149,27 @@ "admonition-title-font-size": "0.95rem", "admonition-font-size": "0.92rem", }, + # Logo configuration "light_logo": "hydrogram-light.png", "dark_logo": "hydrogram-dark.png", - "footer_icons": [ # all these icons are from https://react-icons.github.io/react-icons + # Footer icons + "footer_icons": [ { "name": "Telegram Channel", "url": "https://t.me/HydrogramNews/", - "html": ( - '' - '' - ), + "html": TELEGRAM_ICON, "class": "", }, { "name": "GitHub Organization", "url": "https://github.com/hydrogram/", - "html": ( - '' - "" - ), + "html": GITHUB_ICON, "class": "", }, { "name": "Hydrogram Website", "url": "https://hydrogram.org/", - "html": ( - '' - '' - ), + "html": WEBSITE_ICON, "class": "", }, ], diff --git a/docs/source/faq/socket-send-oserror-timeouterror-connection-lost-reset.rst b/docs/source/faq/socket-send-oserror-timeouterror-connection-lost-reset.rst index c011f8b65..73a47d60e 100644 --- a/docs/source/faq/socket-send-oserror-timeouterror-connection-lost-reset.rst +++ b/docs/source/faq/socket-send-oserror-timeouterror-connection-lost-reset.rst @@ -10,3 +10,70 @@ You can consider the following: - Use shorter non-asynchronous processing loops. - Use ``asyncio.sleep()`` instead of ``time.sleep()``. - Use a stable network connection. + +Connection recovery +------------------- + +Hydrogram coordinates recovery through one task per MTProto session. A closed +transport, connection reset, failed write, request timeout, or missing ping reply +starts recovery. Concurrent requests wait for that same task. Recovery preserves +the authorization key and never logs out, revokes a session, or deletes a session +file. + +Connection attempts use exponential backoff with jitter. The session allows five +consecutive initialization attempts, including connections that fail again within +30 seconds. Backoff starts between 0.5 and 1 second, doubles between attempts, and +is capped at 30 seconds. Each initialization can make up to three transport +connection attempts. A connection that remains healthy for 30 seconds restores the +recovery budget. Exhaustion leaves the session in the ``failed`` state and raises +``ConnectionError`` to callers. Further RPCs fail promptly instead of silently +starting another unlimited retry loop; an explicit session start or client +reconnection is needed to try again. + +A lack of updates is normal for an idle client. Periodic pings require a pong and +provide the transport liveness check. The update watchdog can probe update state +after an idle period, but inactivity alone does not trigger a reconnect. + +Request outcomes +---------------- + +Requests that have not been sent can proceed after recovery. Requests already +sent either return their available response or fail promptly with +:class:`~hydrogram.errors.ConnectionLostError`, a ``ConnectionError`` subclass. +A request whose response times out raises +:class:`~hydrogram.errors.RequestTimeoutError`, a ``TimeoutError`` subclass. +Both expose ``delivery_uncertain``: when true, Telegram may have processed the +request. Check its outcome before deciding whether to retry a write operation. +Hydrogram does not automatically replay these requests or RPCs returning server +errors, since repeating an arbitrary method could duplicate side effects. + +An explicit bad-server-salt rejection permits a bounded resend with the corrected +salt. Sequence-number errors rebuild only the ephemeral MTProto session and +surface the rejected request to its caller. Authentication failures such as +``AUTH_KEY_DUPLICATED`` or ``SESSION_REVOKED`` stop recovery and surface the error +without changing stored credentials. Clock-related bad-message errors require +correcting the local clock; they are not retried automatically. Two-factor login +prompts remain available on the current connection. + +Connection diagnostics +---------------------- + +``app.connection_health`` returns a snapshot without making a network request: + +.. code-block:: python + + health = app.connection_health + print(health["state"], health["is_connected"], health["last_error"]) + +The snapshot contains ``state`` (``stopped``, ``connecting``, ``reconnecting``, +``connected`` or ``failed``), ``is_connected``, ``dc_id``, ``reconnect_attempt``, +``pending_requests``, ``last_received_at`` and ``last_error``. The last-received +timestamp includes protocol traffic such as pongs, not just updates. +``app.is_connected`` continues to describe the client lifecycle; use the health +snapshot to inspect transport readiness during recovery. + +The ``hydrogram.session.session`` logger records connection loss, retry delays, +successful recovery, and exhausted attempts. Retry records include ``dc_id``, +``connection_state``, ``reconnect_attempt`` and ``reconnect_delay`` as structured +logging fields. Stopping the client cancels its recovery, ping, receive, update +watchdog, and owned update-processing tasks. diff --git a/docs/source/faq/why-is-the-api-key-needed-for-bots.rst b/docs/source/faq/why-is-the-api-key-needed-for-bots.rst index 65f08873f..b3256d5d5 100644 --- a/docs/source/faq/why-is-the-api-key-needed-for-bots.rst +++ b/docs/source/faq/why-is-the-api-key-needed-for-bots.rst @@ -4,7 +4,7 @@ Why is the API key needed for bots? Requests against the official bot API endpoints are made via JSON/HTTP and are handled by an intermediate server application that implements the MTProto protocol and uses its own API key to communicate with the MTProto servers. -.. figure:: //_static/img/mtproto-vs-bot-api.png +.. figure:: /_static/img/mtproto-vs-bot-api.png :align: center Using MTProto is the only way to communicate with the actual Telegram servers, and the main API requires developers to diff --git a/docs/source/hydrogram-vs-pyrogram.rst b/docs/source/hydrogram-vs-pyrogram.rst index 58b53160a..9f74825c9 100644 --- a/docs/source/hydrogram-vs-pyrogram.rst +++ b/docs/source/hydrogram-vs-pyrogram.rst @@ -26,7 +26,7 @@ More technical changes - Declared the special variable `__all__` in all files so now we have a better IDE support. - Specify optional parameters in methods as keyword-only to prevent potential breakage in the future when introducing new parameters. -- Migrated from setuptools to hatchling with rye. +- Uses `Hatchling `_ for builds and `uv `_ for project management. - Transitioned from using `os.path` to utilizing `Pathlib`. - Start utilizing towncrier to generate changelogs. diff --git a/docs/source/index.rst b/docs/source/index.rst index e560f2a08..6b7148bed 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -153,10 +153,12 @@ Meta topics/serializing topics/proxy topics/scheduling + topics/message-idempotency topics/mtproto-vs-botapi topics/debugging topics/test-servers topics/advanced-usage + topics/schema-updates .. toctree:: :hidden: diff --git a/docs/source/intro/install.rst b/docs/source/intro/install.rst index ec5935c04..2ba90e527 100644 --- a/docs/source/intro/install.rst +++ b/docs/source/intro/install.rst @@ -1,7 +1,7 @@ Install Guide ============= -Being a modern Python framework, Hydrogram requires an up to date version of Python to be installed in your system. +Hydrogram requires Python 3.11 or later to be installed in your system. We recommend using the latest versions of both Python 3 and pip. ----- @@ -15,16 +15,21 @@ Install Hydrogram $ pip3 install -U hydrogram -- or, with :doc:`TgCrypto <../topics/speedups>` as extra requirement (recommended): +- or, with :doc:`TgCrypto and uvloop <../topics/speedups>` as extra requirements (recommended for better performance): .. code-block:: text - $ pip3 install -U hydrogram tgcrypto + $ pip3 install -U "hydrogram[fast]" Bleeding Edge ------------- -You can install the development version from the git ``dev`` branch using this command: +The development version from the git ``dev`` branch contains the latest features and fixes, but it +may also include unfinished changes, bugs, or unstable code. Using this version can lead to unexpected +behavior, or compatibility issues. It is recommended only for advanced users who want to +test new features and are comfortable troubleshooting problems. + +You can install the development version using this command: .. code-block:: text diff --git a/docs/source/intro/quickstart.rst b/docs/source/intro/quickstart.rst index ca0f86efb..404a2f29a 100644 --- a/docs/source/intro/quickstart.rst +++ b/docs/source/intro/quickstart.rst @@ -1,54 +1,79 @@ Quick Start =========== -The next few steps serve as a quick start to see Hydrogram in action as fast as possible. +Welcome to Hydrogram! This guide will help you set up and run your first Telegram bot or client application in minutes. -Get Hydrogram Real Fast ------------------------ +What is Hydrogram? +------------------ -1. Install Hydrogram with ``pip3 install -U hydrogram``. +Hydrogram is a modern, elegant Python framework that allows you to interact with the Telegram API. With Hydrogram, you can: -2. Obtain the API key by following Telegram's instructions and rules at https://core.telegram.org/api/obtaining_api_id. +- Create Telegram bots with advanced features +- Build user clients (userbot) +- Access and manage your Telegram account programmatically +- Automate Telegram-related tasks +- Handle updates and respond to messages + +Let's get started with a simple example: + +Getting Started +--------------- + +1. **Install Hydrogram** with ``pip3 install -U hydrogram``. + +2. **Obtain your API credentials** from Telegram: + + - Go to https://my.telegram.org/apps and log in with your phone number + - Create a new application to get your ``api_id`` and ``api_hash`` .. note:: - Make sure you understand and abide to the rules for third-party clients and libraries explained in the link above. + Make sure you understand and abide by Telegram's terms of service and API usage rules explained at https://core.telegram.org/api/obtaining_api_id. -3. Open the text editor of your choice and paste the following: +3. **Create your first script** by opening your favorite text editor and pasting the following code: .. code-block:: python import asyncio from hydrogram import Client + # Replace these with your own values api_id = 12345 api_hash = "0123456789abcdef0123456789abcdef" async def main(): + # Create a new client instance async with Client("my_account", api_id, api_hash) as app: + # Send a message to yourself await app.send_message("me", "Greetings from **Hydrogram**!") + # Get information about yourself + me = await app.get_me() + print(f"Successfully logged in as {me.first_name} ({me.id})") + asyncio.run(main()) -4. Replace *api_id* and *api_hash* values with your own. +4. **Replace** the placeholder ``api_id`` and ``api_hash`` values with your own. + +5. **Save the file** as ``hello.py``. -5. Save the file as ``hello.py``. +6. **Run the script** with ``python3 hello.py`` -6. Run the script with ``python3 hello.py`` +7. **Log in to your account** by following the prompts. You'll only need to do this once. -7. Follow the instructions on your terminal to login. +8. **Watch Hydrogram in action** as it sends a message to your Saved Messages. -8. Watch Hydrogram send a message to yourself. +What's Next? +------------ -Enjoy the API -------------- +This was just a brief introduction to get you started quickly. Hydrogram offers many more powerful features for building Telegram applications. -That was just a quick overview. In the next few pages of the introduction, we'll take a much more in-depth look of what -we have just done above. +- Learn about different ways to :doc:`../start/invoking` from the API +- Explore how to :doc:`../start/updates` from Telegram +- Check out complete :doc:`../start/examples/index` to see what's possible -If you are feeling eager to continue you can take a shortcut to :doc:`../start/invoking` and come back -later to learn some more details. +Join our `community`_ on Telegram for support and updates. .. _community: https://t.me/HydrogramNews diff --git a/docs/source/topics/client-settings.rst b/docs/source/topics/client-settings.rst index 31c0f3382..5814d5404 100644 --- a/docs/source/topics/client-settings.rst +++ b/docs/source/topics/client-settings.rst @@ -39,3 +39,36 @@ With the following code we make Telegram know we want it to speak in Italian (it "my_account", lang_code="it", ) + +Limit Media Transfers +--------------------- + +``max_concurrent_transmissions`` is a positive integer limiting the **combined** +number of active uploads and downloads per client (default: 1). For example, +``Client("my_account", max_concurrent_transmissions=2)`` permits two uploads, +two downloads, or one of each. Queued downloads wait before opening their files. +Normal API requests do not acquire a transfer slot. + +Uploads send one 512 KiB chunk at a time per transfer and report progress after +Telegram acknowledges it. Downloads stream chunks of up to 1 MiB; only +``in_memory=True`` intentionally retains the entire download. Synchronous progress +callbacks still run in the client's executor. Callbacks should return promptly +and must not wait for another transfer when all transfer slots are occupied. + +Cancellation closes library-owned upload files and removes incomplete downloads. +Caller-provided upload streams remain open. An in-flight file operation finishes +before its handle is closed and the slot is released. Upload errors propagate; +``download_media`` continues to return ``None`` on ordinary download failures. +Uncertain transport failures are not automatically replayed by the transfer layer. + +A media stream holds its slot until exhausted or explicitly closed. Use +``contextlib.aclosing`` when stopping iteration early: + +.. code-block:: python + + from contextlib import aclosing + + async with aclosing(app.stream_media(message)) as chunks: + async for chunk in chunks: + process(chunk) + break diff --git a/docs/source/topics/message-idempotency.rst b/docs/source/topics/message-idempotency.rst new file mode 100644 index 000000000..4c686a4e8 --- /dev/null +++ b/docs/source/topics/message-idempotency.rst @@ -0,0 +1,91 @@ +Retrying Message Sends +====================== + +Background queues can retry a job after Telegram accepted a message but before the +worker received its response. Supply the same ``random_id`` on each attempt of the +same logical send to let Telegram recognize the repeated submission. Omitting it, +or passing ``None``, keeps Hydrogram's existing automatic ID generation. + +``random_id`` is a keyword-only ``int`` in the inclusive range +``-9223372036854775808`` to ``9223372036854775807``. Booleans and other types raise +``TypeError``; out-of-range integers raise ``ValueError``, before resolving peers +or uploading files. Hydrogram accepts zero as a signed 64-bit integer, though +Telegram may reject it with ``RANDOM_ID_EMPTY``. Prefer a nonzero ID. + +Supported methods +----------------- + +The parameter is available on :meth:`~hydrogram.Client.send_message`, +:meth:`~hydrogram.Client.send_animation`, :meth:`~hydrogram.Client.send_audio`, +:meth:`~hydrogram.Client.send_cached_media`, :meth:`~hydrogram.Client.send_contact`, +:meth:`~hydrogram.Client.send_dice`, :meth:`~hydrogram.Client.send_document`, +:meth:`~hydrogram.Client.send_location`, :meth:`~hydrogram.Client.send_photo`, +:meth:`~hydrogram.Client.send_poll`, :meth:`~hydrogram.Client.send_sticker`, +:meth:`~hydrogram.Client.send_venue`, :meth:`~hydrogram.Client.send_video`, +:meth:`~hydrogram.Client.send_video_note`, :meth:`~hydrogram.Client.send_voice`, +:meth:`~hydrogram.Client.send_game`, and :meth:`~hydrogram.Client.send_inline_bot_result`. +It goes directly into the raw send request; the poll object's internal ID is +still generated separately. A supplied ID is also reused during media upload +part retries within a call. + +Albums and forwarding require a separate ID for each outgoing message, so +``send_media_group``, ``forward_messages`` and copy helpers do not expose this +single-ID parameter. Actions and reactions do not use it either. + +Job queue example +----------------- + +Generate the ID once when creating the job, and persist it with the destination +and payload before the first attempt. Every retry, including after a worker +restart, must load that same record. The following functions illustrate a queue +adapter: ``queue.enqueue`` must durably store the job and redeliver its unchanged +payload when the worker raises a retryable error. + +.. code-block:: python + + import secrets + + + async def enqueue_message(queue, chat_id, text): + job = { + "chat_id": chat_id, + "text": text, + "random_id": secrets.randbelow((1 << 63) - 1) + 1, + } + await queue.enqueue(job) + + + async def process_message(app, job): + # The queue calls this again with the persisted job on a retry. + return await app.send_message( + job["chat_id"], job["text"], random_id=job["random_id"] + ) + +Use a new ID for every new logical message. Store IDs as integers without loss of +precision; some queue serializers represent large numbers as floating point, so +use a decimal string in that case and convert it back to ``int`` in the worker. +Coordinate ID assignment across producers and accounts as appropriate for your +application; random generation makes collisions unlikely but does not prove +uniqueness. Keep the destination and payload unchanged across retries. + +Scope and limitations +--------------------- + +Telegram performs the deduplication on its servers. Its +`random ID documentation `_ +describes deduplication for the same account and destination peer, across sessions +and send method types, with stored IDs that do not expire. Reusing an ID for a +different payload can therefore refer to an earlier message rather than send the +new content. Do not rely on reuse across different accounts or destinations. + +An overlapping duplicate request can raise ``RANDOM_ID_DUPLICATE``. Hydrogram +continues to propagate Telegram RPC errors and parse responses normally; supplying +an ID does not guarantee that a retry returns the original high-level ``Message`` +object. A timeout alone does not establish whether the message was delivered. +Reconcile ambiguous results using your stored job state and Telegram updates. + +This parameter does not persist job IDs, acknowledge queue jobs, deduplicate +application side effects, or provide an exactly-once transaction between Telegram +and your database. Keep durable job/completion records and apply your queue's +retry policy. Retrying a media send can still upload data again even when Telegram +deduplicates the final message submission. diff --git a/docs/source/topics/more-on-updates.rst b/docs/source/topics/more-on-updates.rst index 0a738ad63..8a1d4c5cd 100644 --- a/docs/source/topics/more-on-updates.rst +++ b/docs/source/topics/more-on-updates.rst @@ -219,3 +219,77 @@ The output of both (equivalent) examples will be: 0 1 2 + +Recovering missed updates +------------------------- + +Enable recovery explicitly when constructing a client: + +.. code-block:: python + + from hydrogram import Client + + app = Client("my_account", catch_up=True) + +``catch_up`` defaults to ``False``, preserving the existing update handling +behavior. It is ignored when ``no_updates=True`` or ``takeout=True``. +Register handlers before starting the client so they can receive recovered +updates as soon as initialization finishes. + +With recovery enabled, Hydrogram stores the common ``pts``, ``qts``, ``date`` +and ``seq`` cursors and separate channel/supergroup ``pts`` cursors. A single +worker serializes incoming containers and recovery requests, applies updates +in sequence, and discards events already covered by the appropriate cursor. +It requests differences at startup, after reconnecting or a server session +reset, when a counter gap is detected, on ``UpdatesTooLong`` and +``UpdateChannelTooLong``, and after a period without updates. Channel recovery +normally follows Telegram's channel notifications instead of polling every +known channel. Handler callbacks still run with the configured number of +``workers``; their completion order is independent of update enqueue order. + +The first run establishes a current baseline rather than replaying arbitrary +account history. New channels start from their first observed update or the +cursor supplied by Telegram. If Telegram supplies neither, Hydrogram fetches +current channel information. Subsequent runs with the same file session resume +from saved cursors. Existing SQLite sessions are migrated automatically without +changing their credentials or session-string format. In-memory sessions and +exported session strings do not retain update cursors across processes. +Custom storage engines must implement ``BaseStorage.get_update_states`` and +``BaseStorage.set_update_state`` to support this option; existing engines +remain usable with recovery disabled. Keep recovery enabled consistently: +updates processed while it is disabled do not advance its saved cursors and +may be replayed if it is later enabled again. + +Recovery is bounded and respects Telegram's available update history. Common +difference requests use a total limit of 10,000 counter increments, channel +requests fetch up to 100 updates per page, and recovery yields after at most +100 pages before resuming from its checkpoint with backoff. Failed requests +retain their cursors and retry with backoff. Recovery pauses when the dispatcher +has 10,000 queued updates; its incoming buffer holds up to 1,000 containers. +Overflow requests another difference and logs a warning, since ephemeral +updates such as typing notifications cannot be replayed reliably. + +Both user and bot accounts can use Telegram's difference methods. Bot privacy, +channel membership, permissions, and the kinds of events Telegram retains still +apply. An inaccessible channel is logged and skipped so other updates can +continue. If Telegram rejects a cursor or returns ``DifferenceTooLong`` or +``ChannelDifferenceTooLong``, Hydrogram logs the unrecoverable gap and +reconciles to current state. A channel history snapshot is not dispatched as +new messages. A reset of the common state also reconciles known channels, +since their notifications may have been lost. Recovery cannot recreate expired +or deleted updates, and it does not scan message history to invent events. + +Cursors record handoff to the dispatcher, not successful application-side +processing. Graceful shutdown cancels recovery, finishes an in-flight checkpoint +when possible, and drains dispatched updates. There is no transactional +exactly-once guarantee across a forced process kill, storage failure, or a +handler's external side effects: a crash can interrupt a callback or its +checkpoint. Applications requiring stronger delivery guarantees should use +idempotent handlers and persist their own processing results. Secret-chat data +is forwarded to raw handlers; recovery does not add secret-chat decryption. + +See Telegram's `update handling rules `_, +`updates.getDifference `_ +and `updates.getChannelDifference +`_ for the +underlying protocol and retention limits. diff --git a/docs/source/topics/mtproto-vs-botapi.rst b/docs/source/topics/mtproto-vs-botapi.rst index cf06112eb..2ea5faafc 100644 --- a/docs/source/topics/mtproto-vs-botapi.rst +++ b/docs/source/topics/mtproto-vs-botapi.rst @@ -32,7 +32,7 @@ special accounts that are authorized via tokens instead of phone numbers. The Bo main Telegram API, but runs on an intermediate server application that in turn communicates with the actual Telegram servers using MTProto. -.. figure:: //_static/img/mtproto-vs-bot-api.png +.. figure:: /_static/img/mtproto-vs-bot-api.png :align: center .. _Bot API: https://core.telegram.org/bots/api diff --git a/docs/source/topics/proxy.rst b/docs/source/topics/proxy.rst index 5f11e2254..79764bcd3 100644 --- a/docs/source/topics/proxy.rst +++ b/docs/source/topics/proxy.rst @@ -1,29 +1,91 @@ Proxy Settings ============== -Hydrogram supports proxies with and without authentication. This feature allows Hydrogram to exchange data with Telegram -through an intermediate SOCKS 4/5 or HTTP (CONNECT) proxy server. +Hydrogram supports SOCKS 4/5, HTTP (CONNECT), and native Telegram MTProto proxies +(MTProxy). Configure them with the ``proxy`` parameter of :class:`~hydrogram.Client`. +Existing SOCKS and HTTP settings continue to work without changes. ------ +SOCKS and HTTP +-------------- -Usage ------ - -To use Hydrogram with a proxy, use the *proxy* parameter in the Client class. If your proxy doesn't require authorization -you can omit ``username`` and ``password``. +Omit ``username`` and ``password`` if your proxy does not require authentication. .. code-block:: python from hydrogram import Client proxy = { - "scheme": "socks5", # "socks4", "socks5" and "http" are supported + "scheme": "socks5", # "socks4", "socks5", or "http" "hostname": "11.22.33.44", "port": 1234, "username": "username", - "password": "password" + "password": "password", } - app = Client("my_account", proxy=proxy) + app = Client("my_account", proxy=proxy) + app.run() + +MTProxy +------- + +Use the server, port, and secret supplied by your MTProxy operator. For a +``tg://proxy?server=...&port=...&secret=...`` link, copy those three values into +the dictionary; passing a proxy URL directly is not supported. + +.. code-block:: python + + import os + + from hydrogram import Client + + app = Client( + "my_account", + proxy={ + "scheme": "mtproxy", + "hostname": "proxy.example.com", + "port": 443, + "secret": os.environ["TELEGRAM_PROXY_SECRET"], + }, + ) + app.run() + +Scheme names are case-insensitive; ``mtproto`` is an alias for ``mtproxy``. +``hostname`` must be a non-empty hostname or IP address, and ``port`` must be +an integer from 1 to 65535. Proxy hostnames are resolved independently of the +``ipv6`` setting for Telegram DCs, allowing IPv4 and IPv6 proxy endpoints. +MTProxy uses ``secret`` instead of ``username`` and ``password``; do not supply +username/password credentials with this scheme. + +Hydrogram automatically selects secret-authenticated, obfuscated abridged TCP +transport for MTProxy, overriding ``protocol_factory``. Production, test, media, +and CDN DC routing is encoded in the transport handshake. The proxy endpoint +replaces the direct DC socket address. Normal MTProto authorization and session +encryption still take place through that connection. + +Supported secrets +~~~~~~~~~~~~~~~~~ + +* **Standard secrets:** exactly 32 hexadecimal characters representing 16 bytes. + Uppercase and lowercase hexadecimal are accepted, including standard secrets + whose first byte happens to be ``dd`` or ``ee``. +* **Padded intermediate secrets:** a ``dd`` prefix followed by a 16-byte secret + (34 hexadecimal characters total) are currently unsupported. +* **Fake-TLS secrets:** an ``ee`` prefix, secret, and camouflage domain are + currently unsupported. +* Base64-encoded secrets, byte strings, and full proxy links are unsupported. + +Invalid MTProxy settings raise ``ValueError`` before opening a connection. +Error messages do not include the secret. A correctly formatted but incorrect +secret cannot be authenticated locally: the proxy may close the connection or +remain silent, and normal session timeout/retry handling applies. MTProxy has no +separate success reply to its obfuscated transport handshake. + +Padded intermediate transport and fake-TLS emulation are separate follow-up +work. They require their own framing/padding and TLS camouflage handshakes, +respectively; removing their prefixes does not enable those modes. Choose a +proxy endpoint that accepts standard obfuscated abridged connections. - app.run() +See Telegram's `MTProxy guide `_ and +`transport obfuscation specification +`_ +for protocol details. diff --git a/docs/source/topics/schema-updates.rst b/docs/source/topics/schema-updates.rst new file mode 100644 index 000000000..d9697b158 --- /dev/null +++ b/docs/source/topics/schema-updates.rst @@ -0,0 +1,209 @@ +Updating the MTProto Schema +=========================== + +Raw protocol support and the Client API +--------------------------------------- + +``hydrogram.raw.all.layer`` identifies the application API layer generated from +``compiler/api/source/main_api.tl``. Raw constructors and functions are available +through ``hydrogram.raw`` and :meth:`~hydrogram.Client.invoke`. A layer upgrade +does not automatically add Client methods, public object fields, or parsers for +all new Telegram features. New high-level features need their own implementation +and tests. + +The layer 223 to 232 update preserves the existing poll and keyboard interfaces. +Poll creation uses input answers and integer quiz-answer indices; received poll +option identifiers remain opaque bytes. Poll questions and answers are exposed +as strings. Partial vote results are matched by option identifier. Existing +keyboards use Telegram's new button-type wrappers internally. Successful chat +joins unwrap the new response and cache its peers. Joins requiring interactive +bot verification raise ``NotAcceptable`` with instructions to complete the +verification in an official Telegram client. + +Optional raw vectors distinguish an absent field (``None``) from a present empty +vector (``[]``). Applications consuming raw objects must handle both. High-level +parsers normalize absent vectors as needed. This distinction is necessary to +serialize shared flag groups such as a quiz solution and its empty entity vector. + +Sources and the current audit +----------------------------- + +The previous checkout declared layer 223 and recorded only a mutable Desktop +``dev`` URL. The updater fetched without a timeout, wrote before validation, and +reported no change as exit 2; the workflow's fail-fast shell could stop before +handling that status. CI attempted to stage ignored generated files and force-push +an update branch without import, wire, compatibility, or determinism checks. +The raw compiler retained reference maps between runs, and error generation used +filesystem enumeration order. These behaviors are replaced by the checks below. + +On 2026-10-28, the latest downloaded Telegram Desktop schema declared layer 232: + +* `Telegram Desktop api.tl at f985723b552b30fe1c17d377d57f44e61a15a8c4 + `_. +* `TDLib telegram_api.tl at 4c6377aa7386c39217878a9a56d581922ad37ca4 + `_. + +All 2,578 shared upstream declarations agree. TDLib has additional primitive, +legacy, configuration, and invocation-prefix declarations; Desktop additionally +includes ``null``. The Telegram `schema page `_ +still advertised layer 225 when checked, while individual poll documentation pages +already described the updated poll signatures. The updater therefore resolves +Desktop's ``dev`` branch and compares its immutable schema with TDLib, rather than +hardcoding a target or relying on the website's displayed layer alone. + +The application schema adds 294 declarations, removes 18, and changes 73 relative +to this checkout's layer 223. Changes include polls, keyboards, join responses, +rich messages, communities, wallet operations, and additional flags. These counts +exclude manually handled basic declarations. New feature families have raw +support; they do not imply equivalent high-level coverage. + +``compiler/api/source/provenance.json`` records source commit IDs, download URLs, +SHA-256 checksums of the original files, the normalized local checksum, and the +cross-source comparison. The local adapter comments out basic constructors that +Hydrogram implements by hand and keeps one types/functions section. Generated +Python files and exceptions remain ignored by Git and are always regenerated +from compiler inputs. No generated code from other client forks is used. + +The audit also compared ``auth_key.tl`` and ``sys_msgs.tl`` with Desktop's +``mtproto.tl`` at the same revision. Shared wire declarations are unchanged; +Hydrogram intentionally uses binary ``bytes`` for DH payloads that Desktop labels +``string`` (identical TL wire encoding), and retains its manual container, salt, +RPC-result, and gzip handlers. The generator now preserves the bare vector encoding used by +configuration responses. These transport schemas were not replaced with the +application schema. + +Checking and applying updates +----------------------------- + +Install the locked development environment with ``uv sync --locked --all-extras --dev`` +and work from the repository root. +First inspect ``git status --short`` and preserve any local compiler input edits. +The check is read-only by default: + +.. code-block:: console + + uv run --locked python dev_tools/check_api_schema_updates.py \ + --report /tmp/schema-update.md --json-report /tmp/schema-update.json + +Exit codes are **0** for current inputs, **2** for drift (including same-layer +constructor or signature changes), and **1** for errors or source disagreements. +This replaces the old updater's implicit writes and inverted no-change status. +``make check-api-schema`` uses the same read-only check; make reports a nonzero +exit when drift is present. + +The report lists added, removed, and changed declarations with complete before +and after signatures, immutable provenance, and a pinned adoption command. +Review IDs, flags, required parameters, argument types, and return types. Search +handwritten callers and parsers before adopting removals or changed signatures. +Apply with the exact revisions from that report: + +.. code-block:: console + + uv run --locked python dev_tools/check_api_schema_updates.py --apply \ + --revision DESKTOP_COMMIT --tdlib-revision TDLIB_COMMIT \ + --report /tmp/schema-update.md + +``--apply`` returns **0** on success or no change, and **1** on failure. It first +validates in a temporary copy of the current compiler, library, and tests. Only a +successful candidate replaces the schema and provenance inputs. Invalid schemas, +missing layers, duplicate IDs, changed manual primitives, layer downgrades, +TDLib disagreements, generation failures, and failing tests block adoption. +No validation bypass or force-update option is provided. ``make update-api-schema`` +selects and validates the latest mutually verified upstream schema. + +If a candidate fails, inputs stay unchanged and the report explains the failure; +the workflow log contains test details. Make the necessary compiler or caller +changes, add regression tests, and rerun the pinned command. For extensive work, +use a disposable checkout to develop against the candidate schema, retaining the +report's exact revisions and checksums. Do not accept a partial upgrade that +breaks existing parsers. Document any unsupported high-level feature explicitly. + +Regenerate the working checkout and validate it offline: + +.. code-block:: console + + uv run --locked make api + uv run --locked make validate-api-schema + uv run --locked ruff check . + uv run --locked ruff format --check . + uv run --locked make docs + git diff --check + +Validation regenerates raw bindings and errors twice in one Python process, +then again in a fresh process with a different hash seed, comparing every +output Python file and path. It imports generated modules and runs the complete +offline test suite. Tests cover every schema constructor and function with +optional fields absent and present, wire serialization/deserialization, explicit +empty vectors, registry coverage, handwritten references and call signatures, +and affected high-level parsers. Generation subprocesses have a 120-second limit, +and the test subprocess has a 300-second limit; timeouts block adoption. Tests +also verify the byte layout of bare transport vectors. These checks do not replace authenticated +integration testing against Telegram; no credentials are needed by the offline +suite. + +RPC errors and documentation +---------------------------- + +RPC error definitions in ``compiler/errors/source/*.tsv`` are independently +curated. Telegram's TL schemas contain no complete error catalogue, and documented +errors are not strictly layer-versioned. Review the official +`error documentation `_ and each affected +method's error table. Add verified entries to the appropriate status-code TSV, +preserve existing definitions, and regenerate with ``make api``. The poll audit +added ``POLL_COUNTRY_RESTRICTED`` and ``POLL_MEMBER_RESTRICTED`` (406); the existing +``REVOTE_NOT_ALLOWED`` (400) entry was retained. Regression tests verify dispatch. + +``compiler/api/docs.json`` is a separately maintained description cache, not a +protocol source. This update selectively refreshed eight poll-related entries +from official pages whose signatures exactly matched the pinned schema. Existing +unaffected entries were preserved. The poll-voter page claimed mutual exclusion +for two fields sharing one flag; that contradictory phrase was omitted. No full +scrape was adopted: some newer pages, including keyboard and join-webview pages, +were unavailable. Undocumented new raw fields retain generic descriptions. + +When using ``make generate-docs-json``, first save an exact copy of the existing +JSON outside the checkout. A successful scraper exit can still contain missing +pages or descriptions. Compare keys, nonempty descriptions, and parameter names +against both the old cache and target signatures. Restore the saved file on +unreviewed content losses, or merge only individually verified entries, then +regenerate bindings and build the documentation. + +Known high-level gaps predating this upgrade +-------------------------------------------- + +The static caller audit found these references already absent from the layer 223 +schema. They are explicitly tracked as a baseline in ``tests/test_raw_schema.py`` +and remain separate maintenance work: + +* Forum-topic methods reference ``channels.EditForumTopic``, + ``channels.CreateForumTopic``, ``channels.DeleteTopicHistory``, + ``channels.GetForumTopics``, and ``channels.GetForumTopicsByID``; the raw schema + exposes their equivalents in ``messages`` with ``peer`` parameters. +* Ownership transfer references the absent ``channels.EditCreator``. +* The chat-background parser annotation spells ``WallPaper`` as ``Wallpaper``. + +The baseline permits only those existing references. Newly missing raw references +or incompatible direct call signatures fail validation. It does not claim complete +semantic coverage of every high-level method or raw feature. + +A Python 3.11 run with only ``--dev`` dependencies stalled in the existing +reconnect-test fixture and was interrupted. Validation passes with the locked +``--all-extras`` environment used by the test matrix and schema workflow; the +default-event-loop reconnect issue remains outside this schema update. + +CI reports and review +--------------------- + +``.github/workflows/check_api_updates.yml`` runs every twelve hours, on demand, +and for changes to pipeline inputs. It validates checked-in generation offline, +checks live drift, and validates any candidate using the revisions captured in +its JSON report. Drift fails the job to request maintainer review. The job summary +and ``mtproto-schema-report`` artifact contain Markdown and JSON reports; a +candidate patch is included only after validation succeeds. Failed candidates +still produce reports and retain their validation logs. The workflow uses read-only +repository permissions and does not create branches, commits, or pull requests. + +Review the schema, provenance, affected callers, documentation, and tests together. +Add a ``news/.misc.rst`` fragment. Generated modules must not be staged. +Commit or publish only after review; an upstream layer number alone is not evidence +that a candidate is compatible. diff --git a/hatch_build.py b/hatch_build.py index cc5ee33d8..b1f93c579 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,21 +1,37 @@ -import sys +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . -from hatchling.builders.hooks.plugin.interface import BuildHookInterface +"""Generate Telegram API bindings for wheel and editable builds.""" -# Add the current directory to the path, so we can import the compiler. -sys.path.insert(0, ".") +from __future__ import annotations +import subprocess +import sys +from typing import TYPE_CHECKING -class CustomHook(BuildHookInterface): - """A custom build hook for hydrogram.""" +from hatchling.builders.hooks.plugin.interface import BuildHookInterface - def initialize(self, version, build_data): - """Initialize the hook.""" - if self.target_name not in {"wheel", "install"}: - return +if TYPE_CHECKING: + from typing import Any - from compiler.api.compiler import start as compile_api # noqa: PLC0415 - from compiler.errors.compiler import start as compile_errors # noqa: PLC0415 - compile_api() - compile_errors() +class CustomHook(BuildHookInterface): + def initialize(self, version: str, build_data: dict[str, Any]) -> None: + # Subprocesses avoid mutating sys.path or retaining compiler module state. + for module in ("compiler.api.compiler", "compiler.errors.compiler"): + subprocess.run([sys.executable, "-m", module], cwd=self.root, check=True) diff --git a/hydrogram/__init__.py b/hydrogram/__init__.py index 93702bc74..a36afafd7 100644 --- a/hydrogram/__init__.py +++ b/hydrogram/__init__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -__version__ = "0.2.0" +__version__ = "0.2.1.dev" __license__ = "GNU Lesser General Public License v3.0 (LGPL-3.0)" __copyright__ = "Copyright (C) 2023-present Hydrogram " diff --git a/hydrogram/client.py b/hydrogram/client.py index 251789897..b5b93581d 100644 --- a/hydrogram/client.py +++ b/hydrogram/client.py @@ -38,7 +38,7 @@ from io import BytesIO, StringIO from mimetypes import MimeTypes from pathlib import Path -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING import hydrogram from hydrogram import __license__, __version__, enums, raw, utils @@ -48,16 +48,21 @@ BadRequest, CDNFileHashMismatch, ChannelPrivate, + RPCError, SessionPasswordNeeded, - VolumeLocNotFound, ) from hydrogram.handlers.handler import Handler from hydrogram.methods import Methods from hydrogram.session import Auth, Session from hydrogram.storage import BaseStorage, SQLiteStorage +from hydrogram.transmission import finish, open_file, run_sync from hydrogram.types import ListenerTypes, TermsOfService, User +from hydrogram.update_manager import UpdateManager from hydrogram.utils import ainput +from .connection import Connection +from .connection.proxy import validate_mtproxy +from .connection.transport import TCP, TCPAbridged from .dispatcher import Dispatcher from .file_id import FileId, FileType, ThumbnailSource from .mime_types import mime_types @@ -65,7 +70,8 @@ from .session.internals import MsgId if TYPE_CHECKING: - from collections.abc import AsyncGenerator + import builtins + from collections.abc import AsyncGenerator, Callable log = logging.getLogger(__name__) @@ -109,6 +115,10 @@ class Client(Methods): The Proxy settings as dict. E.g.: *dict(scheme="socks5", hostname="11.22.33.44", port=1234, username="user", password="pass")*. The *username* and *password* can be omitted if the proxy doesn't require authorization. + For MTProxy, use *dict(scheme="mtproxy", hostname="proxy.example.com", port=443, + secret="0123456789abcdef0123456789abcdef")*. Only standard 32-character hexadecimal + secrets are supported; padded (``dd``) and fake-TLS (``ee``) secrets are unsupported. + MTProxy selects obfuscated abridged transport automatically. See :doc:`/topics/proxy`. test_mode (``bool``, *optional*): Enable or disable login to the test servers. @@ -168,6 +178,11 @@ class Client(Methods): Useful for batch programs that don't need to deal with updates. Defaults to False (updates enabled and received). + catch_up (``bool``, *optional*): + Recover missed updates and MTProto update gaps using persisted session cursors. + Defaults to False. Ignored with *no_updates* or *takeout*. + See :doc:`/topics/more-on-updates` for delivery guarantees and API limits. + takeout (``bool``, *optional*): Pass True to let the client use a takeout session instead of a normal one, implies *no_updates=True*. Useful for exporting Telegram data. Methods invoked inside a takeout session (such as get_chat_history, @@ -186,10 +201,22 @@ class Client(Methods): Defaults to False, because ``getpass`` (the library used) is known to be problematic in some terminal environments. - max_concurrent_transmissions (``bool``, *optional*): - Set the maximum amount of concurrent transmissions (uploads & downloads). + max_concurrent_transmissions (``int``, *optional*): + Set the maximum total number of concurrent uploads and downloads. + Must be a positive integer. Streams hold a slot until exhausted or closed. A value that is too high may result in network related issues. Defaults to 1. + + connection_factory (:obj:`~hydrogram.connection.Connection`, *optional*): + Pass a custom connection factory to the client. + + protocol_factory (:obj:`~hydrogram.connection.transport.TCP`, *optional*): + Pass a custom protocol factory to the client. MTProxy settings automatically + select obfuscated abridged transport instead. + + message_cache_size (``int``, *optional*): + Size of the message cache used to store already processed messages. + Defaults to 1000. """ APP_VERSION = f"Hydrogram {__version__}" @@ -242,6 +269,10 @@ def __init__( sleep_threshold: int = Session.SLEEP_THRESHOLD, hide_password: bool = False, max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS, + connection_factory: builtins.type[Connection] = Connection, + protocol_factory: builtins.type[TCP] = TCPAbridged, + message_cache_size: int = 1000, + catch_up: bool = False, ): super().__init__() @@ -253,6 +284,7 @@ def __init__( self.system_version = system_version self.lang_code = lang_code.lower() self.ipv6 = ipv6 + validate_mtproxy(proxy) self.proxy = proxy self.test_mode = test_mode self.bot_token = bot_token @@ -266,10 +298,23 @@ def __init__( self.plugins = plugins self.parse_mode = parse_mode self.no_updates = no_updates + self.catch_up = catch_up + self.update_manager = ( + UpdateManager(self) if catch_up and not no_updates and not takeout else None + ) self.takeout = takeout self.sleep_threshold = sleep_threshold self.hide_password = hide_password + if ( + isinstance(max_concurrent_transmissions, bool) + or not isinstance(max_concurrent_transmissions, int) + or max_concurrent_transmissions < 1 + ): + raise ValueError("max_concurrent_transmissions must be a positive integer") self.max_concurrent_transmissions = max_concurrent_transmissions + self.connection_factory = connection_factory + self.protocol_factory = protocol_factory + self.message_cache_size = message_cache_size self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler") @@ -297,7 +342,7 @@ def __init__( self.file_lock = asyncio.Lock() self.save_file_semaphore = asyncio.Semaphore(self.max_concurrent_transmissions) - self.get_file_semaphore = asyncio.Semaphore(self.max_concurrent_transmissions) + self.get_file_semaphore = self.save_file_semaphore self.is_connected = None self.is_initialized = None @@ -308,7 +353,7 @@ def __init__( self.me: User | None = None - self.message_cache = Cache(10000) + self.message_cache = Cache(message_cache_size) # Sometimes, for some reason, the server will stop sending updates and will only respond to pings. # This watchdog will invoke updates.GetState in order to wake up the server and enable it sending updates again @@ -317,23 +362,52 @@ def __init__( self.updates_watchdog_event = asyncio.Event() self.last_update_time = datetime.now() - self.loop = asyncio.get_event_loop() self.listeners = {listener_type: [] for listener_type in ListenerTypes} async def __aenter__(self): return await self.start() + @property + def connection_health(self) -> dict: + """Snapshot of the main session's connection health. + + Includes ``state``, ``is_connected``, ``dc_id``, ``reconnect_attempt``, + ``pending_requests``, ``last_received_at`` and ``last_error``. + This property performs no network requests. Use it to inspect transient + recovery independently of the client lifecycle flag ``is_connected``. + """ + if self.session is None: + return { + "state": "stopped", + "is_connected": False, + "dc_id": None, + "reconnect_attempt": 0, + "pending_requests": 0, + "last_received_at": None, + "last_error": None, + } + return self.session.health + async def __aexit__(self, *args): with contextlib.suppress(ConnectionError): await self.stop() + @functools.cached_property + def loop(self): + try: + return asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + async def updates_watchdog(self): while True: try: await asyncio.wait_for( self.updates_watchdog_event.wait(), self.UPDATES_WATCHDOG_INTERVAL ) - except asyncio.TimeoutError: + except TimeoutError: pass else: break @@ -341,7 +415,15 @@ async def updates_watchdog(self): if datetime.now() - self.last_update_time > timedelta( seconds=self.UPDATES_WATCHDOG_INTERVAL ): - await self.invoke(raw.functions.updates.GetState()) + if self.update_manager is not None: + self.update_manager.request_recovery() + else: + try: + await self.invoke(raw.functions.updates.GetState()) + except (TimeoutError, OSError, RPCError): + # An idle period is not a transport failure. Session pings + # own liveness checks and coordinate any necessary recovery. + log.warning("Update watchdog probe failed", exc_info=True) async def authorize(self) -> User: if self.bot_token: @@ -558,6 +640,10 @@ async def fetch_peers( async def handle_updates(self, updates): self.last_update_time = datetime.now() + if self.update_manager is not None: + self.update_manager.submit(updates) + return + if isinstance(updates, (raw.types.Updates, raw.types.UpdatesCombined)): is_min = any(( await self.fetch_peers(updates.users), @@ -849,48 +935,58 @@ async def handle_download(self, packet): progress_args, ) = packet - None if in_memory else Path(directory).mkdir(parents=True, exist_ok=True) - file_path = Path(directory).resolve() / file_name - - random_suffix = "".join(random.choices(string.ascii_letters + string.digits, k=8)) - temp_file_path = file_path.with_name(file_path.stem + "_" + random_suffix + ".temp") - - file = BytesIO() if in_memory else Path(temp_file_path).open("wb") # noqa: SIM115 file is closed manually - - try: - async for chunk in self.get_file(file_id, file_size, 0, 0, progress, progress_args): - file.write(chunk) - except BaseException as e: - if not in_memory: - file.close() - Path(temp_file_path).unlink() - - if isinstance(e, asyncio.CancelledError): - raise e - - if isinstance(e, hydrogram.errors.FloodWait): - raise e - - return None - else: - if in_memory: - file.name = file_name - return file - - file.close() - - async with self.file_lock: - final_file_path: Path = file_path - counter = 1 - while final_file_path.exists(): - final_file_path = file_path.with_name( - f"{file_path.stem}({counter}){file_path.suffix}" - ) - counter += 1 - - shutil.move(temp_file_path, final_file_path) - - return final_file_path + async with self.get_file_semaphore: + file_path = (await run_sync(Path(directory).resolve)) / file_name + random_suffix = "".join(random.choices(string.ascii_letters + string.digits, k=8)) + temp_file_path = file_path.with_name(file_path.stem + "_" + random_suffix + ".temp") + file = None + completed_file_path = None + try: + async with contextlib.AsyncExitStack() as stack: + if in_memory: + file = BytesIO() + else: + await run_sync(Path(directory).mkdir, parents=True, exist_ok=True) + file = await stack.enter_async_context(open_file(temp_file_path, "wb")) + async with contextlib.aclosing( + self._get_file(file_id, file_size, 0, 0, progress, progress_args) + ) as chunks: + async for chunk in chunks: + await run_sync(file.write, chunk) + + if in_memory: + file.name = file_name + return file + + async with self.file_lock: + + def move_file(): + nonlocal completed_file_path + final_file_path = file_path + counter = 1 + while final_file_path.exists(): + final_file_path = file_path.with_name( + f"{file_path.stem}({counter}){file_path.suffix}" + ) + counter += 1 + completed_file_path = final_file_path + shutil.move(temp_file_path, final_file_path) + return final_file_path + + return await run_sync(move_file) + except BaseException as e: + if in_memory: + if file is not None: + file.close() + else: + await run_sync(temp_file_path.unlink, missing_ok=True) + if completed_file_path is not None: + await run_sync(completed_file_path.unlink, missing_ok=True) + if isinstance(e, (asyncio.CancelledError, hydrogram.errors.FloodWait)): + raise + if not isinstance(e, Exception): + raise + return None async def get_file( self, @@ -901,47 +997,59 @@ async def get_file( progress: Callable | None = None, progress_args: tuple = (), ) -> AsyncGenerator[bytes, None]: - async with self.get_file_semaphore: - file_type = file_id.file_type - if file_type == FileType.CHAT_PHOTO: - if file_id.chat_id > 0: - peer = raw.types.InputPeerUser( - user_id=file_id.chat_id, access_hash=file_id.chat_access_hash - ) - elif file_id.chat_access_hash == 0: - peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id) - else: - peer = raw.types.InputPeerChannel( - channel_id=utils.get_channel_id(file_id.chat_id), - access_hash=file_id.chat_access_hash, - ) - location = raw.types.InputPeerPhotoFileLocation( - peer=peer, - photo_id=file_id.media_id, - big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG, - ) - elif file_type == FileType.PHOTO: - location = raw.types.InputPhotoFileLocation( - id=file_id.media_id, - access_hash=file_id.access_hash, - file_reference=file_id.file_reference, - thumb_size=file_id.thumbnail_size, + async with ( + self.get_file_semaphore, + contextlib.aclosing( + self._get_file(file_id, file_size, limit, offset, progress, progress_args) + ) as chunks, + ): + async for chunk in chunks: + yield chunk + + async def _get_file( + self, file_id, file_size=0, limit=0, offset=0, progress=None, progress_args=() + ): + file_type = file_id.file_type + if file_type == FileType.CHAT_PHOTO: + if file_id.chat_id > 0: + peer = raw.types.InputPeerUser( + user_id=file_id.chat_id, access_hash=file_id.chat_access_hash ) + elif file_id.chat_access_hash == 0: + peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id) else: - location = raw.types.InputDocumentFileLocation( - id=file_id.media_id, - access_hash=file_id.access_hash, - file_reference=file_id.file_reference, - thumb_size=file_id.thumbnail_size, + peer = raw.types.InputPeerChannel( + channel_id=utils.get_channel_id(file_id.chat_id), + access_hash=file_id.chat_access_hash, ) + location = raw.types.InputPeerPhotoFileLocation( + peer=peer, + photo_id=file_id.media_id, + big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG, + ) + elif file_type == FileType.PHOTO: + location = raw.types.InputPhotoFileLocation( + id=file_id.media_id, + access_hash=file_id.access_hash, + file_reference=file_id.file_reference, + thumb_size=file_id.thumbnail_size, + ) + else: + location = raw.types.InputDocumentFileLocation( + id=file_id.media_id, + access_hash=file_id.access_hash, + file_reference=file_id.file_reference, + thumb_size=file_id.thumbnail_size, + ) - current = 0 - total = abs(limit) or (1 << 31) - 1 - chunk_size = 1024 * 1024 - offset_bytes = abs(offset) * chunk_size - dc_id = file_id.dc_id + current = 0 + total = abs(limit) or (1 << 31) - 1 + chunk_size = 1024 * 1024 + offset_bytes = abs(offset) * chunk_size + dc_id = file_id.dc_id - try: + try: + async with self.media_sessions_lock: session = self.media_sessions.get(dc_id) if not session: auth_key = ( @@ -949,150 +1057,156 @@ async def get_file( if dc_id != await self.storage.dc_id() else await self.storage.auth_key() ) - session = self.media_sessions[dc_id] = Session( + session = Session( self, dc_id, auth_key, await self.storage.test_mode(), is_media=True ) - await session.start() + try: + await session.start() - if dc_id != await self.storage.dc_id(): - for _ in range(3): - exported_auth = await self.invoke( - raw.functions.auth.ExportAuthorization(dc_id=dc_id) - ) - try: - await session.invoke( - raw.functions.auth.ImportAuthorization( - id=exported_auth.id, bytes=exported_auth.bytes - ) + if dc_id != await self.storage.dc_id(): + for _ in range(3): + exported_auth = await self.invoke( + raw.functions.auth.ExportAuthorization(dc_id=dc_id) ) - break - except AuthBytesInvalid: - continue - else: - raise AuthBytesInvalid - - while True: - r = await session.invoke( - raw.functions.upload.GetFile( - location=location, offset=offset_bytes, limit=chunk_size - ), - sleep_threshold=30, - ) - - if isinstance(r, raw.types.upload.File): - chunk = r.bytes - yield chunk + try: + await session.invoke( + raw.functions.auth.ImportAuthorization( + id=exported_auth.id, bytes=exported_auth.bytes + ) + ) + break + except AuthBytesInvalid: + continue + else: + raise AuthBytesInvalid - current += 1 - offset_bytes += chunk_size + except BaseException: + await finish(session.stop()) + raise + self.media_sessions[dc_id] = session - if progress: - func = functools.partial( - progress, - min(offset_bytes, file_size) if file_size != 0 else offset_bytes, - file_size, - *progress_args, - ) + while True: + r = await session.invoke( + raw.functions.upload.GetFile( + location=location, offset=offset_bytes, limit=chunk_size + ), + sleep_threshold=30, + ) - if inspect.iscoroutinefunction(progress): - await func() - else: - await self.loop.run_in_executor(self.executor, func) + if isinstance(r, raw.types.upload.File): + chunk = r.bytes + yield chunk - if len(chunk) < chunk_size or current >= total: - break + current += 1 + offset_bytes += chunk_size - elif isinstance(r, raw.types.upload.FileCdnRedirect): - cdn_session = Session( - self, - r.dc_id, - await Auth(self, r.dc_id, await self.storage.test_mode()).create(), - await self.storage.test_mode(), - is_media=True, - is_cdn=True, + if progress: + func = functools.partial( + progress, + min(offset_bytes, file_size) if file_size != 0 else offset_bytes, + file_size, + *progress_args, ) - try: - await cdn_session.start() + if inspect.iscoroutinefunction(progress): + await func() + else: + await finish(self.loop.run_in_executor(self.executor, func)) + + if len(chunk) < chunk_size or current >= total: + break + + elif isinstance(r, raw.types.upload.FileCdnRedirect): + cdn_session = Session( + self, + r.dc_id, + await Auth(self, r.dc_id, await self.storage.test_mode()).create(), + await self.storage.test_mode(), + is_media=True, + is_cdn=True, + ) - while True: - r2 = await cdn_session.invoke( - raw.functions.upload.GetCdnFile( + try: + await cdn_session.start() + + while True: + r2 = await cdn_session.invoke( + raw.functions.upload.GetCdnFile( + file_token=r.file_token, + offset=offset_bytes, + limit=chunk_size, + ) + ) + + if isinstance(r2, raw.types.upload.CdnFileReuploadNeeded): + await session.invoke( + raw.functions.upload.ReuploadCdnFile( file_token=r.file_token, - offset=offset_bytes, - limit=chunk_size, + request_token=r2.request_token, ) ) + continue - if isinstance(r2, raw.types.upload.CdnFileReuploadNeeded): - try: - await session.invoke( - raw.functions.upload.ReuploadCdnFile( - file_token=r.file_token, - request_token=r2.request_token, - ) - ) - except VolumeLocNotFound: - break - else: - continue - - chunk = r2.bytes - - # https://core.telegram.org/cdn#decrypting-files - decrypted_chunk = aes.ctr256_decrypt( - chunk, - r.encryption_key, - bytearray( - r.encryption_iv[:-4] - + (offset_bytes // 16).to_bytes(4, "big") - ), - ) + chunk = r2.bytes - hashes = await session.invoke( - raw.functions.upload.GetCdnFileHashes( - file_token=r.file_token, offset=offset_bytes - ) + # https://core.telegram.org/cdn#decrypting-files + decrypted_chunk = await run_sync( + aes.ctr256_decrypt, + chunk, + r.encryption_key, + bytearray( + r.encryption_iv[:-4] + (offset_bytes // 16).to_bytes(4, "big") + ), + ) + + hashes = await session.invoke( + raw.functions.upload.GetCdnFileHashes( + file_token=r.file_token, offset=offset_bytes ) + ) - # https://core.telegram.org/cdn#verifying-files - for i, h in enumerate(hashes): - cdn_chunk = decrypted_chunk[h.limit * i : h.limit * (i + 1)] - CDNFileHashMismatch.check( - h.hash == sha256(cdn_chunk).digest(), - "h.hash == sha256(cdn_chunk).digest()", - ) + # https://core.telegram.org/cdn#verifying-files + for i, h in enumerate(hashes): + cdn_chunk = decrypted_chunk[h.limit * i : h.limit * (i + 1)] + CDNFileHashMismatch.check( + h.hash == sha256(cdn_chunk).digest(), + "h.hash == sha256(cdn_chunk).digest()", + ) - yield decrypted_chunk + yield decrypted_chunk - current += 1 - offset_bytes += chunk_size + current += 1 + offset_bytes += chunk_size - if progress: - func = functools.partial( - progress, - min(offset_bytes, file_size) - if file_size != 0 - else offset_bytes, - file_size, - *progress_args, - ) + if progress: + func = functools.partial( + progress, + min(offset_bytes, file_size) + if file_size != 0 + else offset_bytes, + file_size, + *progress_args, + ) - if inspect.iscoroutinefunction(progress): - await func() - else: - await self.loop.run_in_executor(self.executor, func) + if inspect.iscoroutinefunction(progress): + await func() + else: + await finish(self.loop.run_in_executor(self.executor, func)) - if len(chunk) < chunk_size or current >= total: - break - finally: - await cdn_session.stop() - except hydrogram.StopTransmission: - raise - except hydrogram.errors.FloodWait: - raise - except Exception as e: - log.exception(e) + if len(chunk) < chunk_size or current >= total: + break + finally: + await finish(cdn_session.stop()) + break + else: + raise OSError("Unexpected media download response") + except hydrogram.StopTransmission: + raise + except hydrogram.errors.FloodWait: + raise + except Exception: + log.exception("Media download failed") + raise def guess_mime_type(self, filename: str) -> str | None: return self.mimetypes.guess_type(filename)[0] diff --git a/hydrogram/connection/connection.py b/hydrogram/connection/connection.py index 810e01b81..5ab0d8852 100644 --- a/hydrogram/connection/connection.py +++ b/hydrogram/connection/connection.py @@ -21,10 +21,17 @@ import asyncio import logging +import random +from typing import TYPE_CHECKING from hydrogram.session.internals import DataCenter +from .proxy import is_mtproxy, validate_mtproxy from .transport import TCP, TCPAbridged +from .transport.tcp.tcp_mtproxy import TCPMTProxy + +if TYPE_CHECKING: + from .transport.tcp.tcp import Proxy log = logging.getLogger(__name__) @@ -32,27 +39,48 @@ class Connection: MAX_CONNECTION_ATTEMPTS = 3 - def __init__(self, dc_id: int, test_mode: bool, ipv6: bool, proxy: dict, media: bool = False): + def __init__( + self, + dc_id: int, + test_mode: bool, + ipv6: bool, + proxy: Proxy, + media: bool = False, + protocol_factory: type[TCP] = TCPAbridged, + ) -> None: + validate_mtproxy(proxy) self.dc_id = dc_id self.test_mode = test_mode self.ipv6 = ipv6 self.proxy = proxy self.media = media + self.protocol_factory = protocol_factory self.address = DataCenter(dc_id, test_mode, ipv6, media) - self.protocol: TCP = None + self.protocol: TCP | None = None - async def connect(self): - for i in range(Connection.MAX_CONNECTION_ATTEMPTS): - self.protocol = TCPAbridged(self.ipv6, self.proxy) + async def connect(self) -> None: + for i in range(self.MAX_CONNECTION_ATTEMPTS): + if is_mtproxy(self.proxy): + dc_id = self.dc_id + (10000 if self.test_mode else 0) + if self.media: + dc_id = -dc_id + self.protocol = TCPMTProxy(ipv6=self.ipv6, proxy=self.proxy, dc_id=dc_id) + else: + self.protocol = self.protocol_factory(ipv6=self.ipv6, proxy=self.proxy) try: log.info("Connecting...") await self.protocol.connect(self.address) - except OSError as e: + except (TimeoutError, OSError) as e: log.warning("Unable to connect due to network issues: %s", e) await self.protocol.close() - await asyncio.sleep(1) + if i + 1 == self.MAX_CONNECTION_ATTEMPTS: + raise ConnectionError("Transport connection attempts exhausted") from e + await asyncio.sleep(random.uniform(0.5, 1.0) * 2**i) + except BaseException: + await self.protocol.close() + raise else: log.info( "Connected! %s DC%s%s - IPv%s", @@ -63,15 +91,19 @@ async def connect(self): ) break else: - log.warning("Connection failed! Trying again...") - raise ConnectionError + raise ConnectionError("No transport connection attempts configured") - async def close(self): - await self.protocol.close() + async def close(self) -> None: + if self.protocol is not None: + await self.protocol.close() log.info("Disconnected") - async def send(self, data: bytes): + async def send(self, data: bytes) -> None: + if self.protocol is None: + raise ConnectionError("Transport is not connected") await self.protocol.send(data) async def recv(self) -> bytes | None: + if self.protocol is None: + return None return await self.protocol.recv() diff --git a/hydrogram/connection/proxy.py b/hydrogram/connection/proxy.py new file mode 100644 index 000000000..88f4b2e8a --- /dev/null +++ b/hydrogram/connection/proxy.py @@ -0,0 +1,58 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .transport.tcp.tcp import Proxy + + +def is_mtproxy(proxy: Proxy | None) -> bool: + return bool(proxy) and str(proxy.get("scheme", "")).lower() in {"mtproxy", "mtproto"} + + +def parse_mtproxy_secret(secret: str) -> bytes: + """Decode a standard 16-byte hexadecimal MTProxy secret without exposing it in errors.""" + if not isinstance(secret, str): + raise ValueError("MTProxy secret must be a 32-character hexadecimal string") + if len(secret) > 32 and secret[:2].lower() == "ee": + raise ValueError("MTProxy fake-TLS (ee) secrets are not supported") + if len(secret) == 34 and secret[:2].lower() == "dd": + raise ValueError("MTProxy padded intermediate (dd) secrets are not supported") + if re.fullmatch(r"[0-9a-fA-F]{32}", secret) is None: + raise ValueError("MTProxy secret must be a 32-character hexadecimal string") + return bytes.fromhex(secret) + + +def validate_mtproxy(proxy: Proxy | None) -> None: + """Validate MTProxy settings; leave legacy SOCKS/HTTP configuration unchanged.""" + if not is_mtproxy(proxy): + return + hostname = proxy.get("hostname") + if not isinstance(hostname, str) or not hostname or any(c.isspace() for c in hostname): + raise ValueError("MTProxy hostname must be a non-empty hostname or IP address") + port = proxy.get("port") + if type(port) is not int or not 1 <= port <= 65535: + raise ValueError("MTProxy port must be an integer between 1 and 65535") + if proxy.get("username") is not None or proxy.get("password") is not None: + raise ValueError("MTProxy uses a secret instead of username/password authentication") + parse_mtproxy_secret(proxy.get("secret")) diff --git a/hydrogram/connection/transport/tcp/__init__.py b/hydrogram/connection/transport/tcp/__init__.py index 9c9a42d25..06f435ca2 100644 --- a/hydrogram/connection/transport/tcp/__init__.py +++ b/hydrogram/connection/transport/tcp/__init__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from .tcp import TCP +from .tcp import TCP, Proxy from .tcp_abridged import TCPAbridged from .tcp_abridged_o import TCPAbridgedO from .tcp_full import TCPFull @@ -26,6 +26,7 @@ __all__ = [ "TCP", + "Proxy", "TCPAbridged", "TCPAbridgedO", "TCPFull", diff --git a/hydrogram/connection/transport/tcp/tcp.py b/hydrogram/connection/transport/tcp/tcp.py index 8bf6364d7..3f9a115d5 100644 --- a/hydrogram/connection/transport/tcp/tcp.py +++ b/hydrogram/connection/transport/tcp/tcp.py @@ -17,107 +17,161 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +from __future__ import annotations + import asyncio import ipaddress import logging import socket -from concurrent.futures import ThreadPoolExecutor +from typing import NotRequired, TypedDict import socks log = logging.getLogger(__name__) +proxy_type_by_scheme: dict[str, int] = { + "SOCKS4": socks.SOCKS4, + "SOCKS5": socks.SOCKS5, + "HTTP": socks.HTTP, +} -class TCP: - TIMEOUT = 10 - def __init__(self, ipv6: bool, proxy: dict): - self.socket = None +class Proxy(TypedDict): + scheme: str + hostname: str + port: int + username: NotRequired[str | None] + password: NotRequired[str | None] + secret: NotRequired[str] - self.reader = None - self.writer = None - self.lock = asyncio.Lock() - self.loop = asyncio.get_event_loop() +class TCP: + TIMEOUT = 10 + def __init__(self, ipv6: bool, proxy: Proxy) -> None: + self.ipv6 = ipv6 self.proxy = proxy - if proxy: - hostname = proxy.get("hostname") - - try: - ip_address = ipaddress.ip_address(hostname) - except ValueError: - self.socket = socks.socksocket(socket.AF_INET) - else: - if isinstance(ip_address, ipaddress.IPv6Address): - self.socket = socks.socksocket(socket.AF_INET6) - else: - self.socket = socks.socksocket(socket.AF_INET) - - self.socket.set_proxy( - proxy_type=getattr(socks, proxy.get("scheme").upper()), - addr=hostname, - port=proxy.get("port"), - username=proxy.get("username"), - password=proxy.get("password"), - ) + self.reader: asyncio.StreamReader | None = None + self.writer: asyncio.StreamWriter | None = None - self.socket.settimeout(TCP.TIMEOUT) + self.lock = asyncio.Lock() + self.loop = asyncio.get_running_loop() + self._closed = True + + @property + def closed(self) -> bool: + return ( + self._closed + or self.writer is None + or self.writer.is_closing() + or self.reader is None + or self.reader.at_eof() + ) + + async def _connect_via_proxy(self, destination: tuple[str, int]) -> None: + scheme = self.proxy.get("scheme") + if scheme is None: + raise ValueError("No scheme specified") + + proxy_type = proxy_type_by_scheme.get(scheme.upper()) + if proxy_type is None: + raise ValueError(f"Unknown proxy type {scheme}") + + hostname = self.proxy.get("hostname") + port = self.proxy.get("port") + username = self.proxy.get("username") + password = self.proxy.get("password") - log.info("Using proxy %s", hostname) + try: + ip_address = ipaddress.ip_address(hostname) + except ValueError: + is_proxy_ipv6 = False else: - self.socket = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET) + is_proxy_ipv6 = isinstance(ip_address, ipaddress.IPv6Address) + + proxy_family = socket.AF_INET6 if is_proxy_ipv6 else socket.AF_INET + sock = socks.socksocket(proxy_family) - self.socket.setblocking(False) + sock.set_proxy( + proxy_type=proxy_type, addr=hostname, port=port, username=username, password=password + ) + sock.settimeout(TCP.TIMEOUT) - async def connect(self, address: tuple): + try: + await self.loop.sock_connect(sock=sock, address=destination) + sock.setblocking(False) + self.reader, self.writer = await asyncio.open_connection(sock=sock) + except BaseException: + sock.close() + raise + + async def _connect_via_direct(self, destination: tuple[str, int]) -> None: + host, port = destination + family = socket.AF_INET6 if self.ipv6 else socket.AF_INET + self.reader, self.writer = await asyncio.open_connection( + host=host, port=port, family=family + ) + + async def _connect(self, destination: tuple[str, int]) -> None: if self.proxy: - with ThreadPoolExecutor(1) as executor: - await self.loop.run_in_executor(executor, self.socket.connect, address) + await self._connect_via_proxy(destination) else: - try: - await asyncio.wait_for( - asyncio.get_event_loop().sock_connect(self.socket, address), - TCP.TIMEOUT, - ) - except ( - asyncio.TimeoutError - ) as e: # Re-raise as TimeoutError. asyncio.TimeoutError is deprecated in 3.11 - raise TimeoutError("Connection timed out") from e - - self.reader, self.writer = await asyncio.open_connection(sock=self.socket) - - async def close(self): + await self._connect_via_direct(destination) + + async def connect(self, address: tuple[str, int]) -> None: try: - if self.writer is not None: - self.writer.close() - await asyncio.wait_for(self.writer.wait_closed(), TCP.TIMEOUT) + await asyncio.wait_for(self._connect(address), TCP.TIMEOUT) + self._closed = False + except TimeoutError: + self._closed = True + raise TimeoutError("Connection timed out") + + async def close(self) -> None: + if self.writer is None: + self._closed = True + return + + try: + self.writer.close() + await asyncio.wait_for(self.writer.wait_closed(), TCP.TIMEOUT) except Exception as e: log.info("Close exception: %s %s", type(e).__name__, e) + finally: + self._closed = True - async def send(self, data: bytes): + async def send(self, data: bytes) -> None: async with self.lock: + if self.closed: + raise OSError("Connection is closed") try: - if self.writer is not None: - self.writer.write(data) - await self.writer.drain() + self.writer.write(data) + await asyncio.wait_for(self.writer.drain(), self.TIMEOUT) except Exception as e: log.info("Send exception: %s %s", type(e).__name__, e) + self._closed = True raise OSError(e) from e - async def recv(self, length: int = 0): + async def recv(self, length: int = 0) -> bytes | None: + if self._closed or self.reader is None: + return None + data = b"" while len(data) < length: try: chunk = await asyncio.wait_for(self.reader.read(length - len(data)), TCP.TIMEOUT) - except (OSError, asyncio.TimeoutError): + except TimeoutError: + # A quiet socket is normal. Session pings detect unresponsive peers. + continue + except OSError: + self._closed = True return None else: if chunk: data += chunk else: + self._closed = True return None return data diff --git a/hydrogram/connection/transport/tcp/tcp_abridged.py b/hydrogram/connection/transport/tcp/tcp_abridged.py index 92010f1eb..e4a6d7450 100644 --- a/hydrogram/connection/transport/tcp/tcp_abridged.py +++ b/hydrogram/connection/transport/tcp/tcp_abridged.py @@ -21,20 +21,20 @@ import logging -from .tcp import TCP +from .tcp import TCP, Proxy log = logging.getLogger(__name__) class TCPAbridged(TCP): - def __init__(self, ipv6: bool, proxy: dict): + def __init__(self, ipv6: bool, proxy: Proxy) -> None: super().__init__(ipv6, proxy) - async def connect(self, address: tuple): + async def connect(self, address: tuple[str, int]) -> None: await super().connect(address) await super().send(b"\xef") - async def send(self, data: bytes, *args): + async def send(self, data: bytes, *args) -> None: length = len(data) // 4 await super().send( diff --git a/hydrogram/connection/transport/tcp/tcp_abridged_o.py b/hydrogram/connection/transport/tcp/tcp_abridged_o.py index 3cab96afd..61ac51dab 100644 --- a/hydrogram/connection/transport/tcp/tcp_abridged_o.py +++ b/hydrogram/connection/transport/tcp/tcp_abridged_o.py @@ -25,7 +25,7 @@ import hydrogram from hydrogram.crypto import aes -from .tcp import TCP +from .tcp import TCP, Proxy log = logging.getLogger(__name__) @@ -33,13 +33,13 @@ class TCPAbridgedO(TCP): RESERVED = (b"HEAD", b"POST", b"GET ", b"OPTI", b"\xee" * 4) - def __init__(self, ipv6: bool, proxy: dict): + def __init__(self, ipv6: bool, proxy: Proxy) -> None: super().__init__(ipv6, proxy) self.encrypt = None self.decrypt = None - async def connect(self, address: tuple): + async def connect(self, address: tuple[str, int]) -> None: await super().connect(address) while True: @@ -62,7 +62,7 @@ async def connect(self, address: tuple): await super().send(nonce) - async def send(self, data: bytes, *args): + async def send(self, data: bytes, *args) -> None: length = len(data) // 4 data = ( bytes([length]) if length <= 126 else b"\x7f" + length.to_bytes(3, "little") diff --git a/hydrogram/connection/transport/tcp/tcp_full.py b/hydrogram/connection/transport/tcp/tcp_full.py index 0e39f561e..49dedec6f 100644 --- a/hydrogram/connection/transport/tcp/tcp_full.py +++ b/hydrogram/connection/transport/tcp/tcp_full.py @@ -23,22 +23,22 @@ from binascii import crc32 from struct import pack, unpack -from .tcp import TCP +from .tcp import TCP, Proxy log = logging.getLogger(__name__) class TCPFull(TCP): - def __init__(self, ipv6: bool, proxy: dict): + def __init__(self, ipv6: bool, proxy: Proxy) -> None: super().__init__(ipv6, proxy) - self.seq_no = None + self.seq_no: int | None = None - async def connect(self, address: tuple): + async def connect(self, address: tuple[str, int]) -> None: await super().connect(address) self.seq_no = 0 - async def send(self, data: bytes, *args): + async def send(self, data: bytes, *args) -> None: data = pack(" None: super().__init__(ipv6, proxy) - async def connect(self, address: tuple): + async def connect(self, address: tuple[str, int]) -> None: await super().connect(address) await super().send(b"\xee" * 4) - async def send(self, data: bytes, *args): + async def send(self, data: bytes, *args) -> None: await super().send(pack(" bytes | None: diff --git a/hydrogram/connection/transport/tcp/tcp_intermediate_o.py b/hydrogram/connection/transport/tcp/tcp_intermediate_o.py index 4297058df..1c6daaca7 100644 --- a/hydrogram/connection/transport/tcp/tcp_intermediate_o.py +++ b/hydrogram/connection/transport/tcp/tcp_intermediate_o.py @@ -25,7 +25,7 @@ from hydrogram.crypto import aes -from .tcp import TCP +from .tcp import TCP, Proxy log = logging.getLogger(__name__) @@ -33,13 +33,13 @@ class TCPIntermediateO(TCP): RESERVED = (b"HEAD", b"POST", b"GET ", b"OPTI", b"\xee" * 4) - def __init__(self, ipv6: bool, proxy: dict): + def __init__(self, ipv6: bool, proxy: Proxy) -> None: super().__init__(ipv6, proxy) self.encrypt = None self.decrypt = None - async def connect(self, address: tuple): + async def connect(self, address: tuple[str, int]) -> None: await super().connect(address) while True: @@ -62,7 +62,7 @@ async def connect(self, address: tuple): await super().send(nonce) - async def send(self, data: bytes, *args): + async def send(self, data: bytes, *args) -> None: await super().send(aes.ctr256_encrypt(pack(" bytes | None: diff --git a/hydrogram/connection/transport/tcp/tcp_mtproxy.py b/hydrogram/connection/transport/tcp/tcp_mtproxy.py new file mode 100644 index 000000000..210eb7d52 --- /dev/null +++ b/hydrogram/connection/transport/tcp/tcp_mtproxy.py @@ -0,0 +1,95 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +import os +from hashlib import sha256 +from typing import TYPE_CHECKING + +from hydrogram.connection.proxy import parse_mtproxy_secret, validate_mtproxy +from hydrogram.crypto import aes + +from .tcp import TCP +from .tcp_abridged_o import TCPAbridgedO + +if TYPE_CHECKING: + from .tcp import Proxy + + +class TCPMTProxy(TCPAbridgedO): + """Secret-authenticated obfuscated abridged transport for standard MTProxies.""" + + RESERVED = (*TCPAbridgedO.RESERVED, b"\xdd" * 4, b"\x16\x03\x01\x02") + + def __init__(self, ipv6: bool, proxy: Proxy, dc_id: int) -> None: + validate_mtproxy(proxy) + super().__init__(ipv6, proxy) + self.secret = parse_mtproxy_secret(proxy.get("secret")) + self.dc_id = dc_id + self._send_lock = asyncio.Lock() + + def _obfuscation_header(self) -> bytes: + # https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation + while True: + nonce = bytearray(os.urandom(64)) + if nonce[0] != 0xEF and nonce[:4] not in self.RESERVED and any(nonce[4:8]): + break + nonce[56:60] = b"\xef" * 4 + nonce[60:62] = self.dc_id.to_bytes(2, "little", signed=True) + reverse = nonce[55:7:-1] + self.encrypt = (sha256(nonce[8:40] + self.secret).digest(), nonce[40:56], bytearray(1)) + self.decrypt = (sha256(reverse[:32] + self.secret).digest(), reverse[32:48], bytearray(1)) + encrypted = aes.ctr256_encrypt(nonce, *self.encrypt) + return bytes(nonce[:56]) + encrypted[56:64] + + async def _connect(self, destination: tuple[str, int]) -> None: + # Resolve the proxy independently of the destination DC's IPv4/IPv6 preference. + self.reader, self.writer = await asyncio.open_connection( + self.proxy["hostname"], self.proxy["port"] + ) + + async def connect(self, address: tuple[str, int]) -> None: + try: + await TCP.connect(self, address) + await TCP.send(self, self._obfuscation_header()) + except BaseException: + await self.close() + raise + + async def send(self, data: bytes, *args) -> None: + # CTR state and socket writes must advance in the same order, including + # concurrent sends. Cancellation during executor encryption invalidates it. + async with self._send_lock: + try: + await super().send(data, *args) + except BaseException: + await self.close() + raise + + async def recv(self, length: int = 0) -> bytes | None: + try: + data = await super().recv(length) + except BaseException: + await self.close() + raise + if data is None: + await self.close() + return data diff --git a/hydrogram/crypto/aes.py b/hydrogram/crypto/aes.py index 609be9642..205a28014 100644 --- a/hydrogram/crypto/aes.py +++ b/hydrogram/crypto/aes.py @@ -50,6 +50,7 @@ def xor(a: bytes, b: bytes) -> bytes: len(a), "big", ) + except ImportError: import pyaes diff --git a/hydrogram/dispatcher.py b/hydrogram/dispatcher.py index 7cb04989d..1739ecdd9 100644 --- a/hydrogram/dispatcher.py +++ b/hydrogram/dispatcher.py @@ -17,13 +17,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +from __future__ import annotations + import asyncio import inspect import logging from collections import OrderedDict +from typing import TYPE_CHECKING, TypeVar, cast import hydrogram -from hydrogram import utils +from hydrogram import raw, types, utils from hydrogram.handlers import ( CallbackQueryHandler, ChatJoinRequestHandler, @@ -57,6 +60,16 @@ UpdateUserStatus, ) +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + + from hydrogram.handlers.handler import Handler + + UpdateType = TypeVar("UpdateType", bound=types.Update) + HandlerType = TypeVar("HandlerType", bound=type["Handler"]) + RawUpdateType = TypeVar("RawUpdateType", bound=raw.core.TLObject) + + log = logging.getLogger(__name__) @@ -72,34 +85,51 @@ class Dispatcher: CHOSEN_INLINE_RESULT_UPDATES = (UpdateBotInlineSend,) CHAT_JOIN_REQUEST_UPDATES = (UpdateBotChatInviteRequester,) - def __init__(self, client: "hydrogram.Client"): + def __init__(self, client: hydrogram.Client): self.client = client - self.loop = asyncio.get_event_loop() - self.handler_worker_tasks = [] - self.locks_list = [] + self.handler_worker_tasks: list[asyncio.Task] = [] + self.locks_list: list[asyncio.Lock] = [] self.updates_queue = asyncio.Queue() - self.groups = OrderedDict() + self.groups: dict[int, list[Handler]] = OrderedDict() + self.error_handlers: list[ErrorHandler] = [] self._init_update_parsers() - self.error_handlers = [] - - def _init_update_parsers(self): - self.update_parsers = { - Dispatcher.NEW_MESSAGE_UPDATES: self._message_parser, - Dispatcher.EDIT_MESSAGE_UPDATES: self._edited_message_parser, - Dispatcher.DELETE_MESSAGES_UPDATES: self._deleted_messages_parser, - Dispatcher.CALLBACK_QUERY_UPDATES: self._callback_query_parser, - Dispatcher.USER_STATUS_UPDATES: self._user_status_parser, - Dispatcher.BOT_INLINE_QUERY_UPDATES: self._inline_query_parser, - Dispatcher.POLL_UPDATES: self._poll_parser, - Dispatcher.CHOSEN_INLINE_RESULT_UPDATES: self._chosen_inline_result_parser, - Dispatcher.CHAT_MEMBER_UPDATES: self._chat_member_updated_parser, - Dispatcher.CHAT_JOIN_REQUEST_UPDATES: self._chat_join_request_parser, - } - self.update_parsers = { - key: value for key_tuple, value in self.update_parsers.items() for key in key_tuple + + def _init_update_parsers(self) -> None: + update_parsers = { + ( + UpdateNewMessage, + UpdateNewChannelMessage, + UpdateNewScheduledMessage, + ): self._message_parser, + (UpdateEditMessage, UpdateEditChannelMessage): self._edited_message_parser, + (UpdateDeleteMessages, UpdateDeleteChannelMessages): self._deleted_messages_parser, + (UpdateBotCallbackQuery, UpdateInlineBotCallbackQuery): self._callback_query_parser, + (UpdateUserStatus,): self._user_status_parser, + (UpdateBotInlineQuery,): self._inline_query_parser, + (UpdateMessagePoll,): self._poll_parser, + (UpdateBotInlineSend,): self._chosen_inline_result_parser, + (UpdateChatParticipant, UpdateChannelParticipant): self._chat_member_updated_parser, + (UpdateBotChatInviteRequester,): self._chat_join_request_parser, } - async def _message_parser(self, update, users, chats): + self.update_parsers: dict[ + type[raw.core.TLObject], + Callable[ + ..., + Awaitable[tuple[types.Update, type[Handler]]] | tuple[types.Update, type[Handler]], + ], + ] = {} + + for key_tuple, parser in update_parsers.items(): + for key in key_tuple: + self.update_parsers[key] = parser + + async def _message_parser( + self, + update: UpdateNewMessage, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.Message | None, type[MessageHandler]]: return ( await hydrogram.types.Message._parse( client=self.client, @@ -111,51 +141,96 @@ async def _message_parser(self, update, users, chats): MessageHandler, ) - async def _edited_message_parser(self, update, users, chats): + async def _edited_message_parser( + self, + update: UpdateNewMessage, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.Message | None, type[EditedMessageHandler]]: parsed, _ = await self._message_parser(update, users, chats) return parsed, EditedMessageHandler - def _deleted_messages_parser(self, update, users, chats): + def _deleted_messages_parser( + self, + update: UpdateDeleteMessages | UpdateDeleteChannelMessages, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[list[types.Message], type[DeletedMessagesHandler]]: return utils.parse_deleted_messages(self.client, update), DeletedMessagesHandler - async def _callback_query_parser(self, update, users, chats): + async def _callback_query_parser( + self, + update: UpdateBotCallbackQuery | UpdateInlineBotCallbackQuery, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.CallbackQuery, type[CallbackQueryHandler]]: return await hydrogram.types.CallbackQuery._parse( self.client, update, users ), CallbackQueryHandler - def _user_status_parser(self, update, users, chats): + def _user_status_parser( + self, + update: UpdateUserStatus, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.User, type[UserStatusHandler]]: return hydrogram.types.User._parse_user_status(self.client, update), UserStatusHandler - def _inline_query_parser(self, update, users, chats): + def _inline_query_parser( + self, + update: UpdateBotInlineQuery, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.InlineQuery, type[InlineQueryHandler]]: return hydrogram.types.InlineQuery._parse(self.client, update, users), InlineQueryHandler - def _poll_parser(self, update, users, chats): + def _poll_parser( + self, + update: UpdateMessagePoll, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.Poll | None, type[PollHandler]]: return hydrogram.types.Poll._parse_update(self.client, update), PollHandler - def _chosen_inline_result_parser(self, update, users, chats): + def _chosen_inline_result_parser( + self, + update: UpdateBotInlineSend, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.ChosenInlineResult, type[ChosenInlineResultHandler]]: return hydrogram.types.ChosenInlineResult._parse( self.client, update, users ), ChosenInlineResultHandler - def _chat_member_updated_parser(self, update, users, chats): + def _chat_member_updated_parser( + self, + update: UpdateChatParticipant | UpdateChannelParticipant, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.ChatMemberUpdated, type[ChatMemberUpdatedHandler]]: return hydrogram.types.ChatMemberUpdated._parse( self.client, update, users, chats ), ChatMemberUpdatedHandler - def _chat_join_request_parser(self, update, users, chats): + def _chat_join_request_parser( + self, + update: UpdateBotChatInviteRequester, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + ) -> tuple[types.ChatJoinRequest, type[ChatJoinRequestHandler]]: return hydrogram.types.ChatJoinRequest._parse( self.client, update, users, chats ), ChatJoinRequestHandler - async def start(self): + async def start(self) -> None: if not self.client.no_updates: self.locks_list = [asyncio.Lock() for _ in range(self.client.workers)] self.handler_worker_tasks = [ - self.loop.create_task(self.handler_worker(lock)) for lock in self.locks_list + self.client.loop.create_task(self.handler_worker(lock)) for lock in self.locks_list ] log.info("Started %s HandlerTasks", self.client.workers) - async def stop(self): + async def stop(self) -> None: if not self.client.no_updates: for _ in range(self.client.workers): self.updates_queue.put_nowait(None) @@ -166,62 +241,58 @@ async def stop(self): log.info("Stopped %s HandlerTasks", self.client.workers) - def add_handler(self, handler, group: int): - async def fn(): - async with asyncio.Lock(): - if isinstance(handler, ErrorHandler): - if handler not in self.error_handlers: - self.error_handlers.append(handler) - else: - if group not in self.groups: - self.groups[group] = [] - self.groups = OrderedDict(sorted(self.groups.items())) - self.groups[group].append(handler) - - self.loop.create_task(fn()) - - def remove_handler(self, handler, group: int): - async def fn(): - async with asyncio.Lock(): - if isinstance(handler, ErrorHandler): - if handler not in self.error_handlers: - raise ValueError( - f"Error handler {handler} does not exist. Handler was not removed." - ) - - self.error_handlers.remove(handler) - else: - if group not in self.groups: - raise ValueError(f"Group {group} does not exist. Handler was not removed.") - self.groups[group].remove(handler) - - self.loop.create_task(fn()) + def add_handler(self, handler: Handler, group: int) -> None: + if isinstance(handler, ErrorHandler): + if handler not in self.error_handlers: + self.error_handlers.append(handler) + else: + if group not in self.groups: + self.groups[group] = [] + self.groups = OrderedDict(sorted(self.groups.items())) + self.groups[group].append(handler) + + def remove_handler(self, handler: Handler, group: int) -> None: + if isinstance(handler, ErrorHandler): + if handler not in self.error_handlers: + raise ValueError( + f"Error handler {handler} does not exist. Handler was not removed." + ) + + self.error_handlers.remove(handler) + else: + if group not in self.groups: + raise ValueError(f"Group {group} does not exist. Handler was not removed.") + self.groups[group].remove(handler) - async def handler_worker(self, lock): + async def handler_worker(self, lock: asyncio.Lock) -> None: while True: packet = await self.updates_queue.get() if packet is None: break await self._process_packet(packet, lock) - async def _process_packet(self, packet, lock): + async def _process_packet( + self, + packet: tuple[raw.core.TLObject, dict[int, raw.types.User], dict[int, raw.types.Chat]], + lock: asyncio.Lock, + ) -> None: try: update, users, chats = packet parser = self.update_parsers.get(type(update)) - if not parser: - return - if inspect.iscoroutinefunction(parser): - parsed_update, handler_type = await parser(update, users, chats) + if parser is not None: + parsed_result = parser(update, users, chats) + if inspect.isawaitable(parsed_result): + parsed_update, handler_type = await parsed_result + else: + parsed_update, handler_type = cast( + "tuple[types.Update, type[Handler]]", parsed_result + ) else: - parsed_update, handler_type = parser(update, users, chats) - - async with lock: - for group in self.groups.values(): - for handler in group: - await self._handle_update( - handler, handler_type, parsed_update, update, users, chats - ) + parsed_update = None + handler_type = None + + await self._process_update(lock, update, users, chats, parsed_update, handler_type) except hydrogram.StopPropagation: pass except Exception as e: @@ -229,40 +300,58 @@ async def _process_packet(self, packet, lock): finally: self.updates_queue.task_done() - async def _handle_update(self, handler, handler_type, parsed_update, update, users, chats): - try: - if isinstance(handler, handler_type): - if await handler.check(self.client, parsed_update): - await self._execute_callback(handler, parsed_update) - elif isinstance(handler, RawUpdateHandler): - await self._execute_callback(handler, update, users, chats) - except hydrogram.StopPropagation: - raise - except hydrogram.ContinuePropagation: - pass - except Exception as e: - handled_error = False - for error_handler in self.error_handlers: - try: - if await error_handler.check(self.client, e): - await error_handler.callback(self.client, e) - handled_error = True - break - except hydrogram.StopPropagation: - raise - except hydrogram.ContinuePropagation: - continue - except Exception as e: - log.exception(e) - continue - - if not handled_error: - log.exception(e) - - async def _execute_callback(self, handler, *args): + async def _process_update( + self, + lock: asyncio.Lock, + raw_update: raw.core.TLObject, + users: dict[int, raw.types.User], + chats: dict[int, raw.types.Chat], + parsed_update: types.Update | None, + handler_type: type[Handler] | None, + ) -> None: + async with lock: + for group in self.groups.values(): + for handler in group: + try: + if isinstance(handler, RawUpdateHandler): + await self._execute_callback(handler, raw_update, users, chats) + continue + if ( + parsed_update is not None + and isinstance(handler, handler_type) + and await handler.check(self.client, parsed_update) + ): + await self._execute_callback(handler, parsed_update) + break + except (hydrogram.StopPropagation, hydrogram.ContinuePropagation) as e: + if isinstance(e, hydrogram.StopPropagation): + raise + except Exception as exception: + if parsed_update is not None: + await self._handle_exception(parsed_update, exception) + + async def _handle_exception(self, parsed_update: types.Update, exception: Exception) -> None: + handled_error = False + + for error_handler in self.error_handlers: + try: + if await error_handler.check(self.client, parsed_update, exception): + handled_error = True + break + except hydrogram.StopPropagation: + raise + except hydrogram.ContinuePropagation: + continue + except Exception as inner_exception: + log.exception("Error in error handler: %s", inner_exception) + + if not handled_error: + log.exception("Unhandled exception: %s", exception) + + async def _execute_callback(self, handler: Handler, *args) -> None: if inspect.iscoroutinefunction(handler.callback): await handler.callback(self.client, *args) else: - await self.loop.run_in_executor( + await self.client.loop.run_in_executor( self.client.executor, handler.callback, self.client, *args ) diff --git a/hydrogram/errors/__init__.py b/hydrogram/errors/__init__.py index 040a45899..a04120ee2 100644 --- a/hydrogram/errors/__init__.py +++ b/hydrogram/errors/__init__.py @@ -21,484 +21,25 @@ from typing import ClassVar -from .exceptions import ( - AboutTooLong, - AccessTokenExpired, - AccessTokenInvalid, - ActiveUserRequired, - AdminRankEmojiNotAllowed, - AdminRankInvalid, - AdminsTooMuch, - AlbumPhotosTooMany, - ApiCallError, - ApiIdInvalid, - ApiIdPublishedFlood, - ArticleTitleEmpty, - AudioTitleEmpty, - AuthBytesInvalid, - AuthKeyDuplicated, - AuthKeyInvalid, - AuthKeyPermEmpty, - AuthKeyUnregistered, - AuthRestart, - AuthTokenAlreadyAccepted, - AuthTokenExpired, - AuthTokenInvalid, - AutoarchiveNotAvailable, - BadRequest, - BankCardNumberInvalid, - BannedRightsInvalid, - BasePortLocInvalid, - BotChannelsNa, - BotCommandDescriptionInvalid, - BotDomainInvalid, - BotGamesDisabled, - BotGroupsBlocked, - BotInlineDisabled, - BotInvalid, - BotMethodInvalid, - BotMissing, - BotOnesideNotAvail, - BotPaymentsDisabled, - BotPollsDisabled, - BotResponseTimeout, - BotScoreNotModified, - BotsTooMuch, - BroadcastForbidden, - BroadcastIdInvalid, - BroadcastPublicVotersForbidden, - BroadcastRequired, - ButtonDataInvalid, - ButtonTypeInvalid, - ButtonUrlInvalid, - ButtonUserPrivacyRestricted, - CallAlreadyAccepted, - CallAlreadyDeclined, - CallOccupyFailed, - CallPeerInvalid, - CallProtocolFlagsInvalid, - CdnMethodInvalid, - ChannelAddInvalid, - ChannelBanned, - ChannelInvalid, - ChannelPrivate, - ChannelPublicGroupNa, - ChannelsAdminPublicTooMuch, - ChannelsTooMuch, - ChannelTooLarge, - ChatAboutNotModified, - ChatAboutTooLong, - ChatAdminInviteRequired, - ChatAdminRequired, - ChatDiscussionUnallowed, - ChatForbidden, - ChatForwardsRestricted, - ChatIdEmpty, - ChatIdGenerateFailed, - ChatIdInvalid, - ChatInvalid, - ChatInvitePermanent, - ChatLinkExists, - ChatNotModified, - ChatOccupyLocFailed, - ChatOccupyUsernameFailed, - ChatRestricted, - ChatSendGifsForbidden, - ChatSendInlineForbidden, - ChatSendMediaForbidden, - ChatSendPollForbidden, - ChatSendStickersForbidden, - ChatTitleEmpty, - ChatTooBig, - ChatWriteForbidden, - ChpCallFail, - CodeEmpty, - CodeHashInvalid, - CodeInvalid, - ConnectionApiIdInvalid, - ConnectionAppVersionEmpty, - ConnectionDeviceModelEmpty, - ConnectionLangPackInvalid, - ConnectionLayerInvalid, - ConnectionNotInited, - ConnectionSystemEmpty, - ConnectionSystemLangCodeEmpty, - ContactAddMissing, - ContactIdInvalid, - ContactNameEmpty, - ContactReqMissing, - DataInvalid, - DataJsonInvalid, - DataTooLong, - DateEmpty, - DcIdInvalid, - DhGAInvalid, - DocumentInvalid, - EditBotInviteForbidden, - EmailHashExpired, - EmailInvalid, - EmailUnconfirmed, - EmailVerifyExpired, - EmoticonEmpty, - EmoticonInvalid, - EmoticonStickerpackMissing, - EncryptedMessageInvalid, - EncryptionAlreadyAccepted, - EncryptionAlreadyDeclined, - EncryptionDeclined, - EncryptionIdInvalid, - EncryptionOccupyAdminFailed, - EncryptionOccupyFailed, - EntitiesTooLong, - EntityBoundsInvalid, - EntityMentionUserInvalid, - ErrorTextEmpty, - ExpireDateInvalid, - ExportCardInvalid, - ExternalUrlInvalid, - FieldNameEmpty, - FieldNameInvalid, - FileIdInvalid, - FileMigrate, - FilePartEmpty, - FilePartInvalid, - FilePartLengthInvalid, - FilePartMissing, - FilePartsInvalid, - FilePartSizeChanged, - FilePartSizeInvalid, - FilePartTooBig, - FileReferenceEmpty, - FileReferenceExpired, - FileReferenceInvalid, - FilerefUpgradeNeeded, - FilterIdInvalid, - FirstnameInvalid, - Flood, - FloodTestPhoneWait, - FloodWait, - FolderDeacAutofixAll, - FolderIdEmpty, - FolderIdInvalid, - Forbidden, - FreshChangeAdminsForbidden, - FreshChangePhoneForbidden, - FreshResetAuthorisationForbidden, - FromMessageBotDisabled, - FromPeerInvalid, - GameBotInvalid, - GeoPointInvalid, - GifContentTypeInvalid, - GifIdInvalid, - GraphInvalidReload, - GraphOutdatedReload, - GroupcallAddParticipantsFailed, - GroupCallInvalid, - GroupcallSsrcDuplicateMuch, - GroupedIdOccupyFailed, - GroupedMediaInvalid, - HashInvalid, - HistoryGetFailed, - ImageEngineDown, - ImageProcessFailed, - ImportFileInvalid, - ImportFormatUnrecognized, - ImportIdInvalid, - InlineBotRequired, - InlineResultExpired, - InputConstructorInvalid, - InputFetchError, - InputFetchFail, - InputFilterInvalid, - InputLayerInvalid, - InputMethodInvalid, - InputRequestTooLong, - InputUserDeactivated, - InterdcCallError, - InterdcCallRichError, - InternalServerError, - InviteHashEmpty, - InviteHashExpired, - InviteHashInvalid, - InviteRequestSent, - InviteRevokedMissing, - LangPackInvalid, - LastnameInvalid, - LimitInvalid, - LinkNotModified, - LocationInvalid, - MaxIdInvalid, - MaxQtsInvalid, - Md5ChecksumInvalid, - MediaCaptionTooLong, - MediaEmpty, - MediaFileInvalid, - MediaInvalid, - MediaNewInvalid, - MediaPrevInvalid, - MediaTtlInvalid, - MediaVideoStoryMissing, - MegagroupIdInvalid, - MegagroupPrehistoryHidden, - MegagroupRequired, - MemberFetchFailed, - MemberNoLocation, - MemberOccupyPrimaryLocFailed, - MemberOccupyUsernameFailed, - MessageAuthorRequired, - MessageDeleteForbidden, - MessageEditTimeExpired, - MessageEmpty, - MessageIdInvalid, - MessageIdsEmpty, - MessageNotModified, - MessagePollClosed, - MessageTooLong, - MethodInvalid, - MsgidDecreaseRetry, - MsgIdInvalid, - MsgRangeUnsync, - MsgVoiceMissing, - MsgWaitFailed, - MtSendQueueTooLong, - MultiMediaTooLong, - NeedChatInvalid, - NeedMemberInvalid, - NetworkMigrate, - NewSaltInvalid, - NewSettingsInvalid, - NextOffsetInvalid, - NotAcceptable, - NoWorkersRunning, - OffsetInvalid, - OffsetPeerIdInvalid, - OptionInvalid, - OptionsTooMuch, - PackShortNameInvalid, - PackShortNameOccupied, - PackTitleInvalid, - ParticipantCallFailed, - ParticipantsTooFew, - ParticipantVersionOutdated, - PasswordEmpty, - PasswordHashInvalid, - PasswordMissing, - PasswordRecoveryNa, - PasswordRequired, - PasswordTooFresh, - PaymentProviderInvalid, - PeerFlood, - PeerIdInvalid, - PeerIdNotSupported, - PersistentTimestampEmpty, - PersistentTimestampInvalid, - PersistentTimestampOutdated, - PhoneCodeEmpty, - PhoneCodeExpired, - PhoneCodeHashEmpty, - PhoneCodeInvalid, - PhoneMigrate, - PhoneNumberAppSignupForbidden, - PhoneNumberBanned, - PhoneNumberFlood, - PhoneNumberInvalid, - PhoneNumberOccupied, - PhoneNumberUnoccupied, - PhonePasswordFlood, - PhonePasswordProtected, - PhotoContentTypeInvalid, - PhotoContentUrlEmpty, - PhotoCreateFailed, - PhotoCropFileMissing, - PhotoCropSizeSmall, - PhotoExtInvalid, - PhotoFileMissing, - PhotoIdInvalid, - PhotoInvalid, - PhotoInvalidDimensions, - PhotoSaveFileInvalid, - PhotoThumbUrlEmpty, - PhotoThumbUrlInvalid, - PinnedDialogsTooMuch, - PinRestricted, - PollAnswersInvalid, - PollOptionDuplicate, - PollOptionInvalid, - PollQuestionInvalid, - PollUnsupported, - PollVoteRequired, - PostponedTimeout, - PremiumAccountRequired, - PrivacyKeyInvalid, - PrivacyTooLong, - PrivacyValueInvalid, - PtsChangeEmpty, - QueryIdEmpty, - QueryIdInvalid, - QueryTooShort, - QuizCorrectAnswerInvalid, - QuizCorrectAnswersEmpty, - QuizCorrectAnswersTooMuch, - QuizMultipleInvalid, - RandomIdDuplicate, - RandomIdEmpty, - RandomIdInvalid, - RandomLengthInvalid, - RangesInvalid, - ReactionEmpty, - ReactionInvalid, - ReflectorNotAvailable, - RegIdGenerateFailed, - ReplyMarkupBuyEmpty, - ReplyMarkupGameEmpty, - ReplyMarkupInvalid, - ReplyMarkupTooLong, - ResultIdDuplicate, - ResultIdEmpty, - ResultIdInvalid, - ResultsTooMuch, - ResultTypeInvalid, - RevoteNotAllowed, - RightForbidden, - RpcCallFail, - RpcConnectFailed, - RPCError, - RpcMcgetFail, - RsaDecryptFailed, - ScheduleBotNotAllowed, - ScheduleDateInvalid, - ScheduleDateTooLate, - ScheduleStatusPrivate, - ScheduleTooMuch, - SearchQueryEmpty, - SecondsInvalid, - SeeOther, - SendMessageMediaInvalid, - SendMessageTypeInvalid, - SensitiveChangeForbidden, - ServiceUnavailable, - SessionExpired, - SessionPasswordNeeded, - SessionRevoked, - SessionTooFresh, - SettingsInvalid, - Sha256HashInvalid, - ShortnameOccupyFailed, - SignInFailed, - SlowmodeMultiMsgsDisabled, - SlowmodeWait, - SmsCodeCreateFailed, - SrpIdInvalid, - SrpPasswordChanged, - StartParamEmpty, - StartParamInvalid, - StartParamTooLong, - StatsMigrate, - StickerDocumentInvalid, - StickerEmojiInvalid, - StickerFileInvalid, - StickerIdInvalid, - StickerInvalid, - StickerPngDimensions, - StickerPngNopng, - StickersEmpty, - StickersetInvalid, - StickersetNotModified, - StickersetOwnerAnonymous, - StickersTooMuch, - StickerTgsNotgs, - StickerThumbPngNopng, - StickerVideoNowebm, - StorageCheckFailed, - StoreInvalidScalarType, - StoriesTooMuch, - TakeoutInitDelay, - TakeoutInvalid, - TakeoutRequired, - TempAuthKeyEmpty, - ThemeFileInvalid, - ThemeFormatInvalid, - ThemeInvalid, - ThemeMimeInvalid, - Timeout, - TmpPasswordDisabled, - TmpPasswordInvalid, - TokenInvalid, - TopicClosed, - TopicDeleted, - TranscriptionFailed, - TtlDaysInvalid, - TtlMediaInvalid, - TtlPeriodInvalid, - TwoFaConfirmWait, - TypeConstructorInvalid, - TypesEmpty, - Unauthorized, - UnknownMethod, - UntilDateInvalid, - UploadNoVolume, - UrlInvalid, - UsageLimitInvalid, - UserAdminInvalid, - UserAlreadyParticipant, - UserBannedInChannel, - UserBlocked, - UserBot, - UserBotInvalid, - UserBotRequired, - UserChannelsTooMuch, - UserCreator, - UserDeactivated, - UserDeactivatedBan, - UserIdInvalid, - UserInvalid, - UserIsBlocked, - UserIsBot, - UserKicked, - UserMigrate, - UsernameInvalid, - UsernameNotModified, - UsernameNotOccupied, - UsernameOccupied, - UserNotMutualContact, - UserNotParticipant, - UserpicUploadRequired, - UserPrivacyRestricted, - UserRestricted, - UsersTooFew, - UsersTooMuch, - VideoContentTypeInvalid, - VideoFileInvalid, - VoiceMessagesForbidden, - VolumeLocNotFound, - WallpaperFileInvalid, - WallpaperInvalid, - WallpaperMimeInvalid, - WcConvertUrlInvalid, - WebdocumentInvalid, - WebdocumentMimeInvalid, - WebdocumentSizeTooBig, - WebdocumentUrlEmpty, - WebdocumentUrlInvalid, - WebpageCurlFailed, - WebpageMediaEmpty, - WorkerBusyTooLongRetry, - WpIdGenerateFailed, - YouBlockedUser, - all, - bad_request_400, - flood_420, - forbidden_403, - internal_server_error_500, - not_acceptable_406, - see_other_303, - service_unavailable_503, - unauthorized_401, -) +from .exceptions import * # noqa: F403 from .pyromod import ListenerStopped, ListenerTimeout from .rpc_error import UnknownError +class ConnectionLostError(ConnectionError): + """The connection was lost; a sent request's delivery may be uncertain.""" + + def __init__(self, message: str, *, delivery_uncertain: bool = True) -> None: + self.delivery_uncertain = delivery_uncertain + super().__init__(message) + + +class RequestTimeoutError(TimeoutError): + """A sent request timed out. It may have been processed by Telegram.""" + + delivery_uncertain: bool = True + + class BadMsgNotification(Exception): # noqa: N818 descriptions: ClassVar = { 16: "The msg_id is too low, the client time has to be synchronized.", @@ -545,479 +86,13 @@ def __init__(self, msg: str | None = None): __all__ = [ - "AboutTooLong", - "AccessTokenExpired", - "AccessTokenInvalid", - "ActiveUserRequired", - "AdminRankEmojiNotAllowed", - "AdminRankInvalid", - "AdminsTooMuch", - "AlbumPhotosTooMany", - "ApiCallError", - "ApiIdInvalid", - "ApiIdPublishedFlood", - "ArticleTitleEmpty", - "AudioTitleEmpty", - "AuthBytesInvalid", - "AuthKeyDuplicated", - "AuthKeyInvalid", - "AuthKeyPermEmpty", - "AuthKeyUnregistered", - "AuthRestart", - "AuthTokenAlreadyAccepted", - "AuthTokenExpired", - "AuthTokenInvalid", - "AutoarchiveNotAvailable", - "BadRequest", - "BankCardNumberInvalid", - "BannedRightsInvalid", - "BasePortLocInvalid", - "BotChannelsNa", - "BotCommandDescriptionInvalid", - "BotDomainInvalid", - "BotGamesDisabled", - "BotGroupsBlocked", - "BotInlineDisabled", - "BotInvalid", - "BotMethodInvalid", - "BotMissing", - "BotOnesideNotAvail", - "BotPaymentsDisabled", - "BotPollsDisabled", - "BotResponseTimeout", - "BotScoreNotModified", - "BotsTooMuch", - "BroadcastForbidden", - "BroadcastIdInvalid", - "BroadcastPublicVotersForbidden", - "BroadcastRequired", - "ButtonDataInvalid", - "ButtonTypeInvalid", - "ButtonUrlInvalid", - "ButtonUserPrivacyRestricted", - "CallAlreadyAccepted", - "CallAlreadyDeclined", - "CallOccupyFailed", - "CallPeerInvalid", - "CallProtocolFlagsInvalid", - "CdnMethodInvalid", - "ChannelAddInvalid", - "ChannelBanned", - "ChannelInvalid", - "ChannelPrivate", - "ChannelPublicGroupNa", - "ChannelTooLarge", - "ChannelsAdminPublicTooMuch", - "ChannelsTooMuch", - "ChatAboutNotModified", - "ChatAboutTooLong", - "ChatAdminInviteRequired", - "ChatAdminRequired", - "ChatDiscussionUnallowed", - "ChatForbidden", - "ChatForwardsRestricted", - "ChatIdEmpty", - "ChatIdGenerateFailed", - "ChatIdInvalid", - "ChatInvalid", - "ChatInvitePermanent", - "ChatLinkExists", - "ChatNotModified", - "ChatOccupyLocFailed", - "ChatOccupyUsernameFailed", - "ChatRestricted", - "ChatSendGifsForbidden", - "ChatSendInlineForbidden", - "ChatSendMediaForbidden", - "ChatSendPollForbidden", - "ChatSendStickersForbidden", - "ChatTitleEmpty", - "ChatTooBig", - "ChatWriteForbidden", - "ChpCallFail", - "CodeEmpty", - "CodeHashInvalid", - "CodeInvalid", - "ConnectionApiIdInvalid", - "ConnectionAppVersionEmpty", - "ConnectionDeviceModelEmpty", - "ConnectionLangPackInvalid", - "ConnectionLayerInvalid", - "ConnectionNotInited", - "ConnectionSystemEmpty", - "ConnectionSystemLangCodeEmpty", - "ContactAddMissing", - "ContactIdInvalid", - "ContactNameEmpty", - "ContactReqMissing", - "DataInvalid", - "DataJsonInvalid", - "DataTooLong", - "DateEmpty", - "DcIdInvalid", - "DhGAInvalid", - "DocumentInvalid", - "EditBotInviteForbidden", - "EmailHashExpired", - "EmailInvalid", - "EmailUnconfirmed", - "EmailVerifyExpired", - "EmoticonEmpty", - "EmoticonInvalid", - "EmoticonStickerpackMissing", - "EncryptedMessageInvalid", - "EncryptionAlreadyAccepted", - "EncryptionAlreadyDeclined", - "EncryptionDeclined", - "EncryptionIdInvalid", - "EncryptionOccupyAdminFailed", - "EncryptionOccupyFailed", - "EntitiesTooLong", - "EntityBoundsInvalid", - "EntityMentionUserInvalid", - "ErrorTextEmpty", - "ExpireDateInvalid", - "ExportCardInvalid", - "ExternalUrlInvalid", - "FieldNameEmpty", - "FieldNameInvalid", - "FileIdInvalid", - "FileMigrate", - "FilePartEmpty", - "FilePartInvalid", - "FilePartLengthInvalid", - "FilePartMissing", - "FilePartSizeChanged", - "FilePartSizeInvalid", - "FilePartTooBig", - "FilePartsInvalid", - "FileReferenceEmpty", - "FileReferenceExpired", - "FileReferenceInvalid", - "FilerefUpgradeNeeded", - "FilterIdInvalid", - "FirstnameInvalid", - "Flood", - "FloodTestPhoneWait", - "FloodWait", - "FolderDeacAutofixAll", - "FolderIdEmpty", - "FolderIdInvalid", - "Forbidden", - "FreshChangeAdminsForbidden", - "FreshChangePhoneForbidden", - "FreshResetAuthorisationForbidden", - "FromMessageBotDisabled", - "FromPeerInvalid", - "GameBotInvalid", - "GeoPointInvalid", - "GifContentTypeInvalid", - "GifIdInvalid", - "GraphInvalidReload", - "GraphOutdatedReload", - "GroupCallInvalid", - "GroupcallAddParticipantsFailed", - "GroupcallSsrcDuplicateMuch", - "GroupedIdOccupyFailed", - "GroupedMediaInvalid", - "HashInvalid", - "HistoryGetFailed", - "ImageEngineDown", - "ImageProcessFailed", - "ImportFileInvalid", - "ImportFormatUnrecognized", - "ImportIdInvalid", - "InlineBotRequired", - "InlineResultExpired", - "InputConstructorInvalid", - "InputFetchError", - "InputFetchFail", - "InputFilterInvalid", - "InputLayerInvalid", - "InputMethodInvalid", - "InputRequestTooLong", - "InputUserDeactivated", - "InterdcCallError", - "InterdcCallRichError", - "InternalServerError", - "InviteHashEmpty", - "InviteHashExpired", - "InviteHashInvalid", - "InviteRequestSent", - "InviteRevokedMissing", - "LangPackInvalid", - "LastnameInvalid", - "LimitInvalid", - "LinkNotModified", + "BadMsgNotification", + "CDNFileHashMismatch", + "ConnectionLostError", "ListenerStopped", "ListenerTimeout", - "LocationInvalid", - "MaxIdInvalid", - "MaxQtsInvalid", - "Md5ChecksumInvalid", - "MediaCaptionTooLong", - "MediaEmpty", - "MediaFileInvalid", - "MediaInvalid", - "MediaNewInvalid", - "MediaPrevInvalid", - "MediaTtlInvalid", - "MediaVideoStoryMissing", - "MegagroupIdInvalid", - "MegagroupPrehistoryHidden", - "MegagroupRequired", - "MemberFetchFailed", - "MemberNoLocation", - "MemberOccupyPrimaryLocFailed", - "MemberOccupyUsernameFailed", - "MessageAuthorRequired", - "MessageDeleteForbidden", - "MessageEditTimeExpired", - "MessageEmpty", - "MessageIdInvalid", - "MessageIdsEmpty", - "MessageNotModified", - "MessagePollClosed", - "MessageTooLong", - "MethodInvalid", - "MsgIdInvalid", - "MsgRangeUnsync", - "MsgVoiceMissing", - "MsgWaitFailed", - "MsgidDecreaseRetry", - "MtSendQueueTooLong", - "MultiMediaTooLong", - "NeedChatInvalid", - "NeedMemberInvalid", - "NetworkMigrate", - "NewSaltInvalid", - "NewSettingsInvalid", - "NextOffsetInvalid", - "NoWorkersRunning", - "NotAcceptable", - "OffsetInvalid", - "OffsetPeerIdInvalid", - "OptionInvalid", - "OptionsTooMuch", - "PackShortNameInvalid", - "PackShortNameOccupied", - "PackTitleInvalid", - "ParticipantCallFailed", - "ParticipantVersionOutdated", - "ParticipantsTooFew", - "PasswordEmpty", - "PasswordHashInvalid", - "PasswordMissing", - "PasswordRecoveryNa", - "PasswordRequired", - "PasswordTooFresh", - "PaymentProviderInvalid", - "PeerFlood", - "PeerIdInvalid", - "PeerIdNotSupported", - "PersistentTimestampEmpty", - "PersistentTimestampInvalid", - "PersistentTimestampOutdated", - "PhoneCodeEmpty", - "PhoneCodeExpired", - "PhoneCodeHashEmpty", - "PhoneCodeInvalid", - "PhoneMigrate", - "PhoneNumberAppSignupForbidden", - "PhoneNumberBanned", - "PhoneNumberFlood", - "PhoneNumberInvalid", - "PhoneNumberOccupied", - "PhoneNumberUnoccupied", - "PhonePasswordFlood", - "PhonePasswordProtected", - "PhotoContentTypeInvalid", - "PhotoContentUrlEmpty", - "PhotoCreateFailed", - "PhotoCropFileMissing", - "PhotoCropSizeSmall", - "PhotoExtInvalid", - "PhotoFileMissing", - "PhotoIdInvalid", - "PhotoInvalid", - "PhotoInvalidDimensions", - "PhotoSaveFileInvalid", - "PhotoThumbUrlEmpty", - "PhotoThumbUrlInvalid", - "PinRestricted", - "PinnedDialogsTooMuch", - "PollAnswersInvalid", - "PollOptionDuplicate", - "PollOptionInvalid", - "PollQuestionInvalid", - "PollUnsupported", - "PollVoteRequired", - "PostponedTimeout", - "PremiumAccountRequired", - "PrivacyKeyInvalid", - "PrivacyTooLong", - "PrivacyValueInvalid", - "PtsChangeEmpty", - "QueryIdEmpty", - "QueryIdInvalid", - "QueryTooShort", - "QuizCorrectAnswerInvalid", - "QuizCorrectAnswersEmpty", - "QuizCorrectAnswersTooMuch", - "QuizMultipleInvalid", - "RPCError", - "RandomIdDuplicate", - "RandomIdEmpty", - "RandomIdInvalid", - "RandomLengthInvalid", - "RangesInvalid", - "ReactionEmpty", - "ReactionInvalid", - "ReflectorNotAvailable", - "RegIdGenerateFailed", - "ReplyMarkupBuyEmpty", - "ReplyMarkupGameEmpty", - "ReplyMarkupInvalid", - "ReplyMarkupTooLong", - "ResultIdDuplicate", - "ResultIdEmpty", - "ResultIdInvalid", - "ResultTypeInvalid", - "ResultsTooMuch", - "RevoteNotAllowed", - "RightForbidden", - "RpcCallFail", - "RpcConnectFailed", - "RpcMcgetFail", - "RsaDecryptFailed", - "ScheduleBotNotAllowed", - "ScheduleDateInvalid", - "ScheduleDateTooLate", - "ScheduleStatusPrivate", - "ScheduleTooMuch", - "SearchQueryEmpty", - "SecondsInvalid", - "SeeOther", - "SendMessageMediaInvalid", - "SendMessageTypeInvalid", - "SensitiveChangeForbidden", - "ServiceUnavailable", - "SessionExpired", - "SessionPasswordNeeded", - "SessionRevoked", - "SessionTooFresh", - "SettingsInvalid", - "Sha256HashInvalid", - "ShortnameOccupyFailed", - "SignInFailed", - "SlowmodeMultiMsgsDisabled", - "SlowmodeWait", - "SmsCodeCreateFailed", - "SrpIdInvalid", - "SrpPasswordChanged", - "StartParamEmpty", - "StartParamInvalid", - "StartParamTooLong", - "StatsMigrate", - "StickerDocumentInvalid", - "StickerEmojiInvalid", - "StickerFileInvalid", - "StickerIdInvalid", - "StickerInvalid", - "StickerPngDimensions", - "StickerPngNopng", - "StickerTgsNotgs", - "StickerThumbPngNopng", - "StickerVideoNowebm", - "StickersEmpty", - "StickersTooMuch", - "StickersetInvalid", - "StickersetNotModified", - "StickersetOwnerAnonymous", - "StorageCheckFailed", - "StoreInvalidScalarType", - "StoriesTooMuch", - "TakeoutInitDelay", - "TakeoutInvalid", - "TakeoutRequired", - "TempAuthKeyEmpty", - "ThemeFileInvalid", - "ThemeFormatInvalid", - "ThemeInvalid", - "ThemeMimeInvalid", - "Timeout", - "TmpPasswordDisabled", - "TmpPasswordInvalid", - "TokenInvalid", - "TopicClosed", - "TopicDeleted", - "TranscriptionFailed", - "TtlDaysInvalid", - "TtlMediaInvalid", - "TtlPeriodInvalid", - "TwoFaConfirmWait", - "TypeConstructorInvalid", - "TypesEmpty", - "Unauthorized", + "RequestTimeoutError", + "SecurityCheckMismatch", + "SecurityError", "UnknownError", - "UnknownMethod", - "UntilDateInvalid", - "UploadNoVolume", - "UrlInvalid", - "UsageLimitInvalid", - "UserAdminInvalid", - "UserAlreadyParticipant", - "UserBannedInChannel", - "UserBlocked", - "UserBot", - "UserBotInvalid", - "UserBotRequired", - "UserChannelsTooMuch", - "UserCreator", - "UserDeactivated", - "UserDeactivatedBan", - "UserIdInvalid", - "UserInvalid", - "UserIsBlocked", - "UserIsBot", - "UserKicked", - "UserMigrate", - "UserNotMutualContact", - "UserNotParticipant", - "UserPrivacyRestricted", - "UserRestricted", - "UsernameInvalid", - "UsernameNotModified", - "UsernameNotOccupied", - "UsernameOccupied", - "UserpicUploadRequired", - "UsersTooFew", - "UsersTooMuch", - "VideoContentTypeInvalid", - "VideoFileInvalid", - "VoiceMessagesForbidden", - "VolumeLocNotFound", - "WallpaperFileInvalid", - "WallpaperInvalid", - "WallpaperMimeInvalid", - "WcConvertUrlInvalid", - "WebdocumentInvalid", - "WebdocumentMimeInvalid", - "WebdocumentSizeTooBig", - "WebdocumentUrlEmpty", - "WebdocumentUrlInvalid", - "WebpageCurlFailed", - "WebpageMediaEmpty", - "WorkerBusyTooLongRetry", - "WpIdGenerateFailed", - "YouBlockedUser", - "all", - "bad_request_400", - "flood_420", - "forbidden_403", - "internal_server_error_500", - "not_acceptable_406", - "see_other_303", - "service_unavailable_503", - "unauthorized_401", ] diff --git a/hydrogram/file_id.py b/hydrogram/file_id.py index f00c1700a..4335b835b 100644 --- a/hydrogram/file_id.py +++ b/hydrogram/file_id.py @@ -66,18 +66,17 @@ def rle_encode(s: bytes) -> bytes: Returns: ``bytes``: The encoded bytes """ - r: list[int] = [] - n: int = 0 + r = bytearray() + n = 0 for b in s: - if not b: - n += 1 - else: + if b: if n: r.extend((0, n)) n = 0 - r.append(b) + else: + n += 1 if n: r.extend((0, n)) @@ -95,7 +94,7 @@ def rle_decode(s: bytes) -> bytes: Returns: ``bytes``: The decoded bytes """ - r: list[int] = [] + r = bytearray() z: bool = False for b in s: @@ -104,7 +103,7 @@ def rle_decode(s: bytes) -> bytes: continue if z: - r.extend((0,) * b) + r.extend([0] * b) z = False else: r.append(b) @@ -155,8 +154,9 @@ class ThumbnailSource(IntEnum): } DOCUMENT_TYPES = set(FileType) - PHOTO_TYPES -# Since the file type values are small enough to fit them in few bits, Telegram thought it would be a good idea to -# encode extra information about web url and file reference existence as flag inside the 4 bytes allocated for the field +# Since the file type values are small enough to fit them in few bits, Telegram thought it would +# be a good idea to encode extra information about web url and file reference existence as flag +# inside the 4 bytes allocated for the field WEB_LOCATION_FLAG = 1 << 24 FILE_REFERENCE_FLAG = 1 << 25 @@ -165,6 +165,27 @@ class FileId: MAJOR = 4 MINOR = 30 + __slots__ = ( + "access_hash", + "chat_access_hash", + "chat_id", + "dc_id", + "file_reference", + "file_type", + "local_id", + "major", + "media_id", + "minor", + "secret", + "sticker_set_access_hash", + "sticker_set_id", + "thumbnail_file_type", + "thumbnail_size", + "thumbnail_source", + "url", + "volume_id", + ) + def __init__( self, *, @@ -177,8 +198,8 @@ def __init__( media_id: int | None = None, access_hash: int | None = None, volume_id: int | None = None, - thumbnail_source: ThumbnailSource = None, - thumbnail_file_type: FileType = None, + thumbnail_source: ThumbnailSource | None = None, + thumbnail_file_type: FileType | None = None, thumbnail_size: str = "", secret: int | None = None, local_id: int | None = None, @@ -230,8 +251,7 @@ def decode(file_id: str): has_file_reference = bool(file_type & FILE_REFERENCE_FLAG) # Remove flags to restore the actual type id value - file_type &= ~WEB_LOCATION_FLAG - file_type &= ~FILE_REFERENCE_FLAG + file_type &= ~(WEB_LOCATION_FLAG | FILE_REFERENCE_FLAG) # endregion try: @@ -422,7 +442,22 @@ def encode(self, *, major: int | None = None, minor: int | None = None): return b64_encode(rle_encode(buffer.getvalue())) def __str__(self): - return str({k: v for k, v in self.__dict__.items() if v is not None}) + order = [ + "major", + "minor", + "file_type", + "dc_id", + "file_reference", + "media_id", + "access_hash", + "thumbnail_size", + ] + result = {} + for key in order: + value = getattr(self, key) + if value is not None: + result[key] = value + return str(result) class FileUniqueType(IntEnum): @@ -437,6 +472,8 @@ class FileUniqueType(IntEnum): class FileUniqueId: + __slots__ = ("file_unique_type", "local_id", "media_id", "url", "volume_id") + def __init__( self, *, @@ -485,12 +522,14 @@ def decode(file_unique_id: str): # TODO: Missing decoder for SECURE, ENCRYPTED and TEMP raise ValueError( - f"Unknown decoder for file_unique_type {file_unique_type} of file_unique_id {file_unique_id}" + f"Unknown decoder for file_unique_type {file_unique_type} " + f"of file_unique_id {file_unique_id}" ) def encode(self): if self.file_unique_type == FileUniqueType.WEB: - string = struct.pack(" Awaitable[bool]: raise NotImplementedError - def __invert__(self): + def __invert__(self) -> InvertFilter: return InvertFilter(self) - def __and__(self, other): + def __and__(self, other: Filter) -> AndFilter: return AndFilter(self, other) - def __or__(self, other): + def __or__(self, other: Filter) -> OrFilter: return OrFilter(self, other) class InvertFilter(Filter): - def __init__(self, base): + def __init__(self, base: Filter) -> None: self.base = base - async def __call__(self, client: hydrogram.Client, update: Update): + async def __call__(self, client: hydrogram.Client, update: Update) -> bool: if inspect.iscoroutinefunction(self.base.__call__): x = await self.base(client, update) else: @@ -64,17 +72,16 @@ async def __call__(self, client: hydrogram.Client, update: Update): class AndFilter(Filter): - def __init__(self, base, other): + def __init__(self, base: Filter, other: Filter) -> None: self.base = base self.other = other - async def __call__(self, client: hydrogram.Client, update: Update): + async def __call__(self, client: hydrogram.Client, update: Update) -> bool: if inspect.iscoroutinefunction(self.base.__call__): x = await self.base(client, update) else: x = await client.loop.run_in_executor(client.executor, self.base, client, update) - # short circuit if not x: return False @@ -83,21 +90,20 @@ async def __call__(self, client: hydrogram.Client, update: Update): else: y = await client.loop.run_in_executor(client.executor, self.other, client, update) - return x and y + return bool(x) and bool(y) class OrFilter(Filter): - def __init__(self, base, other): + def __init__(self, base: Filter, other: Filter) -> None: self.base = base self.other = other - async def __call__(self, client: hydrogram.Client, update: Update): + async def __call__(self, client: hydrogram.Client, update: Update) -> bool: if inspect.iscoroutinefunction(self.base.__call__): x = await self.base(client, update) else: x = await client.loop.run_in_executor(client.executor, self.base, client, update) - # short circuit if x: return True @@ -106,44 +112,66 @@ async def __call__(self, client: hydrogram.Client, update: Update): else: y = await client.loop.run_in_executor(client.executor, self.other, client, update) - return x or y - + return bool(x) or bool(y) -CUSTOM_FILTER_NAME = "CustomFilter" - -def create(func: Callable, name: str | None = None, **kwargs) -> Filter: +def create( + func: Callable[..., bool | Awaitable[bool]], + name: str | None = None, + **kwargs: Any, +) -> Filter: """Easily create a custom filter. - Custom filters give you extra control over which updates are allowed or not to be processed by your handlers. + Custom filters give you extra control over which updates are allowed or not to be processed + by your handlers. Parameters: func (``Callable``): - A function that accepts three positional arguments *(filter, client, update)* and returns a boolean: True if the - update should be handled, False otherwise. - The *filter* argument refers to the filter itself and can be used to access keyword arguments (read below). - The *client* argument refers to the :obj:`~hydrogram.Client` that received the update. - The *update* argument type will vary depending on which `Handler `_ is coming from. - For example, in a :obj:`~hydrogram.handlers.MessageHandler` the *update* argument will be a :obj:`~hydrogram.types.Message`; in a :obj:`~hydrogram.handlers.CallbackQueryHandler` the *update* will be a :obj:`~hydrogram.types.CallbackQuery`. - Your function body can then access the incoming update attributes and decide whether to allow it or not. + A function that accepts three positional arguments *(filter, client, update)* and + returns a boolean: True if the update should be handled, False otherwise. + The *filter* argument refers to the filter itself and can be used to access + keyword arguments (read below). The *client* argument refers to the + :obj:`~hydrogram.Client` that received the update. The *update* argument type + will vary depending on which `Handler `_ is coming from. For example, in + a :obj:`~hydrogram.handlers.MessageHandler` the *update* argument will be a + :obj:`~hydrogram.types.Message`; in a :obj:`~hydrogram.handlers.CallbackQueryHandler` + the *update* will be a :obj:`~hydrogram.types.CallbackQuery`. Your function body + can then access the incoming update attributes and decide whether to allow it or not. name (``str``, *optional*): Your filter's name. Can be anything you like. Defaults to "CustomFilter". **kwargs (``any``, *optional*): - Any keyword argument you would like to pass. Useful when creating parameterized custom filters, such as - :meth:`~hydrogram.filters.command` or :meth:`~hydrogram.filters.regex`. + Any keyword argument you would like to pass. Useful when creating parameterized + custom filters, such as :meth:`~hydrogram.filters.command` or + :meth:`~hydrogram.filters.regex`. """ return type( - name or func.__name__ or CUSTOM_FILTER_NAME, + name or func.__name__ or "CustomFilter", (Filter,), {"__call__": func, **kwargs}, )() -# region all_filter -def all_filter(_, __, ___): +def _attribute_filter(attribute: str) -> Callable[[Filter, hydrogram.Client, Message], bool]: + def func(_: Filter, __: hydrogram.Client, m: Message) -> bool: + return bool(getattr(m, attribute, None)) + + return func + + +def _chat_type_filter(chat_types: set[enums.ChatType], m: CallbackQuery | Message) -> bool: + if isinstance(m, Message): + value = m.chat + elif isinstance(m, CallbackQuery): + value = m.message.chat if m.message else None + else: + raise ValueError(f"Chat type filter doesn't work with {type(m)}") + return bool(value and value.type in chat_types) + + +def all_filter(_: Filter, __: hydrogram.Client, ___: Update) -> bool: return True @@ -151,11 +179,7 @@ def all_filter(_, __, ___): """Filter all messages.""" -# endregion - - -# region me_filter -def me_filter(_, __, m: Message): +def me_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return bool(m.from_user.is_self if m.from_user else getattr(m, "outgoing", False)) @@ -163,11 +187,7 @@ def me_filter(_, __, m: Message): """Filter messages generated by you yourself.""" -# endregion - - -# region bot_filter -def bot_filter(_, __, m: Message): +def bot_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return bool(m.from_user and m.from_user.is_bot) @@ -175,485 +195,158 @@ def bot_filter(_, __, m: Message): """Filter messages coming from bots.""" -# endregion - - -# region incoming_filter -def incoming_filter(_, __, m: Message): +def incoming_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return not m.outgoing incoming = create(incoming_filter) -"""Filter incoming messages. Messages sent to your own chat (Saved Messages) are also recognised as incoming.""" - - -# endregion +"""Filter incoming messages. Messages sent to your own chat (Saved Messages) are also +recognised as incoming. +""" -# region outgoing_filter -def outgoing_filter(_, __, m: Message): - return m.outgoing +def outgoing_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: + return bool(m.outgoing) outgoing = create(outgoing_filter) -"""Filter outgoing messages. Messages sent to your own chat (Saved Messages) are not recognized as outgoing.""" - - -# endregion - - -# region text_filter -def text_filter(_, __, m: Message): - return bool(m.text) +"""Filter outgoing messages. Messages sent to your own chat (Saved Messages) +are not recognized as outgoing. +""" -text = create(text_filter) +text = create(_attribute_filter("text"), "text_filter") """Filter text messages.""" - -# endregion - - -# region reply_filter -def reply_filter(_, __, m: Message): - return bool(m.reply_to_message_id) - - -reply = create(reply_filter) +reply = create(_attribute_filter("reply_to_message_id"), "reply_filter") """Filter messages that are replies to other messages.""" - -# endregion - - -# region forwarded_filter -def forwarded_filter(_, __, m: Message): - return bool(m.forward_date) - - -forwarded = create(forwarded_filter) +forwarded = create(_attribute_filter("forward_date"), "forwarded_filter") """Filter messages that are forwarded.""" - -# endregion - - -# region caption_filter -def caption_filter(_, __, m: Message): - return bool(m.caption) - - -caption = create(caption_filter) +caption = create(_attribute_filter("caption"), "caption_filter") """Filter media messages that contain captions.""" - -# endregion - - -# region audio_filter -def audio_filter(_, __, m: Message): - return bool(m.audio) - - -audio = create(audio_filter) +audio = create(_attribute_filter("audio"), "audio_filter") """Filter messages that contain :obj:`~hydrogram.types.Audio` objects.""" - -# endregion - - -# region document_filter -def document_filter(_, __, m: Message): - return bool(m.document) - - -document = create(document_filter) +document = create(_attribute_filter("document"), "document_filter") """Filter messages that contain :obj:`~hydrogram.types.Document` objects.""" - -# endregion - - -# region photo_filter -def photo_filter(_, __, m: Message): - return bool(m.photo) - - -photo = create(photo_filter) +photo = create(_attribute_filter("photo"), "photo_filter") """Filter messages that contain :obj:`~hydrogram.types.Photo` objects.""" - -# endregion - - -# region sticker_filter -def sticker_filter(_, __, m: Message): - return bool(m.sticker) - - -sticker = create(sticker_filter) +sticker = create(_attribute_filter("sticker"), "sticker_filter") """Filter messages that contain :obj:`~hydrogram.types.Sticker` objects.""" - -# endregion - - -# region animation_filter -def animation_filter(_, __, m: Message): - return bool(m.animation) - - -animation = create(animation_filter) +animation = create(_attribute_filter("animation"), "animation_filter") """Filter messages that contain :obj:`~hydrogram.types.Animation` objects.""" - -# endregion - - -# region game_filter -def game_filter(_, __, m: Message): - return bool(m.game) - - -game = create(game_filter) +game = create(_attribute_filter("game"), "game_filter") """Filter messages that contain :obj:`~hydrogram.types.Game` objects.""" - -# endregion - - -# region video_filter -def video_filter(_, __, m: Message): - return bool(m.video) - - -video = create(video_filter) +video = create(_attribute_filter("video"), "video_filter") """Filter messages that contain :obj:`~hydrogram.types.Video` objects.""" - -# endregion - - -# region media_group_filter -def media_group_filter(_, __, m: Message): - return bool(m.media_group_id) - - -media_group = create(media_group_filter) +media_group = create(_attribute_filter("media_group_id"), "media_group_filter") """Filter messages containing photos or videos being part of an album.""" - -# endregion - - -# region voice_filter -def voice_filter(_, __, m: Message): - return bool(m.voice) - - -voice = create(voice_filter) +voice = create(_attribute_filter("voice"), "voice_filter") """Filter messages that contain :obj:`~hydrogram.types.Voice` note objects.""" - -# endregion - - -# region video_note_filter -def video_note_filter(_, __, m: Message): - return bool(m.video_note) - - -video_note = create(video_note_filter) +video_note = create(_attribute_filter("video_note"), "video_note_filter") """Filter messages that contain :obj:`~hydrogram.types.VideoNote` objects.""" - -# endregion - - -# region contact_filter -def contact_filter(_, __, m: Message): - return bool(m.contact) - - -contact = create(contact_filter) +contact = create(_attribute_filter("contact"), "contact_filter") """Filter messages that contain :obj:`~hydrogram.types.Contact` objects.""" - -# endregion - - -# region location_filter -def location_filter(_, __, m: Message): - return bool(m.location) - - -location = create(location_filter) +location = create(_attribute_filter("location"), "location_filter") """Filter messages that contain :obj:`~hydrogram.types.Location` objects.""" - -# endregion - - -# region venue_filter -def venue_filter(_, __, m: Message): - return bool(m.venue) - - -venue = create(venue_filter) +venue = create(_attribute_filter("venue"), "venue_filter") """Filter messages that contain :obj:`~hydrogram.types.Venue` objects.""" - -# endregion - - -# region web_page_filter -def web_page_filter(_, __, m: Message): - return bool(m.web_page) - - -web_page = create(web_page_filter) +web_page = create(_attribute_filter("web_page"), "web_page_filter") """Filter messages sent with a webpage preview.""" - -# endregion - - -# region poll_filter -def poll_filter(_, __, m: Message): - return bool(m.poll) - - -poll = create(poll_filter) +poll = create(_attribute_filter("poll"), "poll_filter") """Filter messages that contain :obj:`~hydrogram.types.Poll` objects.""" - -# endregion - - -# region dice_filter -def dice_filter(_, __, m: Message): - return bool(m.dice) - - -dice = create(dice_filter) +dice = create(_attribute_filter("dice"), "dice_filter") """Filter messages that contain :obj:`~hydrogram.types.Dice` objects.""" - -# endregion - - -# region media_spoiler -def media_spoiler_filter(_, __, m: Message): - return bool(m.has_media_spoiler) - - -media_spoiler = create(media_spoiler_filter) +media_spoiler = create(_attribute_filter("has_media_spoiler"), "media_spoiler_filter") """Filter media messages that contain a spoiler.""" -# endregion - - -# region private_filter -def private_filter(_, __, m: CallbackQuery | Message): - if isinstance(m, Message): - value = m.chat - elif isinstance(m, CallbackQuery): - value = m.message.chat - else: - raise ValueError(f"Private filter doesn't work with {type(m)}") - return bool(value and value.type in {enums.ChatType.PRIVATE, enums.ChatType.BOT}) +def private_filter(_: Filter, __: hydrogram.Client, m: CallbackQuery | Message) -> bool: + return _chat_type_filter({enums.ChatType.PRIVATE, enums.ChatType.BOT}, m) private = create(private_filter) """Filter messages sent in private chats.""" -# endregion - - -# region group_filter -def group_filter(_, __, m: CallbackQuery | Message): - if isinstance(m, Message): - value = m.chat - elif isinstance(m, CallbackQuery): - value = m.message.chat - else: - raise ValueError(f"Group filter doesn't work with {type(m)}") - return bool(value and value.type in {enums.ChatType.GROUP, enums.ChatType.SUPERGROUP}) +def group_filter(_: Filter, __: hydrogram.Client, m: CallbackQuery | Message) -> bool: + return _chat_type_filter({enums.ChatType.GROUP, enums.ChatType.SUPERGROUP}, m) group = create(group_filter) """Filter messages sent in group or supergroup chats.""" -# endregion - - -# region channel_filter -def channel_filter(_, __, m: CallbackQuery | Message): - if isinstance(m, Message): - value = m.chat - elif isinstance(m, CallbackQuery): - value = m.message.chat - else: - raise ValueError(f"Channel filter doesn't work with {type(m)}") - return bool(value and value.type == enums.ChatType.CHANNEL) +def channel_filter(_: Filter, __: hydrogram.Client, m: CallbackQuery | Message) -> bool: + return _chat_type_filter({enums.ChatType.CHANNEL}, m) channel = create(channel_filter) """Filter messages sent in channels.""" -# endregion - - -# region new_chat_members_filter -def new_chat_members_filter(_, __, m: Message): - return bool(m.new_chat_members) - - -new_chat_members = create(new_chat_members_filter) +new_chat_members = create(_attribute_filter("new_chat_members"), "new_chat_members_filter") """Filter service messages for new chat members.""" - -# endregion - - -# region left_chat_member_filter -def left_chat_member_filter(_, __, m: Message): - return bool(m.left_chat_member) - - -left_chat_member = create(left_chat_member_filter) +left_chat_member = create(_attribute_filter("left_chat_member"), "left_chat_member_filter") """Filter service messages for members that left the chat.""" - -# endregion - - -# region new_chat_title_filter -def new_chat_title_filter(_, __, m: Message): - return bool(m.new_chat_title) - - -new_chat_title = create(new_chat_title_filter) +new_chat_title = create(_attribute_filter("new_chat_title"), "new_chat_title_filter") """Filter service messages for new chat titles.""" - -# endregion - - -# region new_chat_photo_filter -def new_chat_photo_filter(_, __, m: Message): - return bool(m.new_chat_photo) - - -new_chat_photo = create(new_chat_photo_filter) +new_chat_photo = create(_attribute_filter("new_chat_photo"), "new_chat_photo_filter") """Filter service messages for new chat photos.""" - -# endregion - - -# region delete_chat_photo_filter -def delete_chat_photo_filter(_, __, m: Message): - return bool(m.delete_chat_photo) - - -delete_chat_photo = create(delete_chat_photo_filter) +delete_chat_photo = create(_attribute_filter("delete_chat_photo"), "delete_chat_photo_filter") """Filter service messages for deleted photos.""" - -# endregion - - -# region group_chat_created_filter -def group_chat_created_filter(_, __, m: Message): - return bool(m.group_chat_created) - - -group_chat_created = create(group_chat_created_filter) +group_chat_created = create(_attribute_filter("group_chat_created"), "group_chat_created_filter") """Filter service messages for group chat creations.""" - -# endregion - - -# region supergroup_chat_created_filter -def supergroup_chat_created_filter(_, __, m: Message): - return bool(m.supergroup_chat_created) - - -supergroup_chat_created = create(supergroup_chat_created_filter) +supergroup_chat_created = create( + _attribute_filter("supergroup_chat_created"), "supergroup_chat_created_filter" +) """Filter service messages for supergroup chat creations.""" - -# endregion - - -# region channel_chat_created_filter -def channel_chat_created_filter(_, __, m: Message): - return bool(m.channel_chat_created) - - -channel_chat_created = create(channel_chat_created_filter) +channel_chat_created = create( + _attribute_filter("channel_chat_created"), "channel_chat_created_filter" +) """Filter service messages for channel chat creations.""" - -# endregion - - -# region migrate_to_chat_id_filter -def migrate_to_chat_id_filter(_, __, m: Message): - return bool(m.migrate_to_chat_id) - - -migrate_to_chat_id = create(migrate_to_chat_id_filter) +migrate_to_chat_id = create(_attribute_filter("migrate_to_chat_id"), "migrate_to_chat_id_filter") """Filter service messages that contain migrate_to_chat_id.""" - -# endregion - - -# region migrate_from_chat_id_filter -def migrate_from_chat_id_filter(_, __, m: Message): - return bool(m.migrate_from_chat_id) - - -migrate_from_chat_id = create(migrate_from_chat_id_filter) +migrate_from_chat_id = create( + _attribute_filter("migrate_from_chat_id"), "migrate_from_chat_id_filter" +) """Filter service messages that contain migrate_from_chat_id.""" - -# endregion - - -# region pinned_message_filter -def pinned_message_filter(_, __, m: Message): - return bool(m.pinned_message) - - -pinned_message = create(pinned_message_filter) +pinned_message = create(_attribute_filter("pinned_message"), "pinned_message_filter") """Filter service messages for pinned messages.""" - -# endregion - - -# region game_high_score_filter -def game_high_score_filter(_, __, m: Message): - return bool(m.game_high_score) - - -game_high_score = create(game_high_score_filter) +game_high_score = create(_attribute_filter("game_high_score"), "game_high_score_filter") """Filter service messages for game high scores.""" -# endregion - - -# region reply_keyboard_filter -def reply_keyboard_filter(_, __, m: Message): +def reply_keyboard_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return isinstance(m.reply_markup, ReplyKeyboardMarkup) @@ -661,11 +354,7 @@ def reply_keyboard_filter(_, __, m: Message): """Filter messages containing reply keyboard markups""" -# endregion - - -# region inline_keyboard_filter -def inline_keyboard_filter(_, __, m: Message): +def inline_keyboard_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return isinstance(m.reply_markup, InlineKeyboardMarkup) @@ -673,71 +362,25 @@ def inline_keyboard_filter(_, __, m: Message): """Filter messages containing inline keyboard markups""" -# endregion - - -# region mentioned_filter -def mentioned_filter(_, __, m: Message): - return bool(m.mentioned) - - -mentioned = create(mentioned_filter) +mentioned = create(_attribute_filter("mentioned"), "mentioned_filter") """Filter messages containing mentions""" - -# endregion - - -# region via_bot_filter -def via_bot_filter(_, __, m: Message): - return bool(m.via_bot) - - -via_bot = create(via_bot_filter) +via_bot = create(_attribute_filter("via_bot"), "via_bot_filter") """Filter messages sent via inline bots""" - -# endregion - - -# region video_chat_started_filter -def video_chat_started_filter(_, __, m: Message): - return bool(m.video_chat_started) - - -video_chat_started = create(video_chat_started_filter) +video_chat_started = create(_attribute_filter("video_chat_started"), "video_chat_started_filter") """Filter messages for started video chats""" - -# endregion - - -# region video_chat_ended_filter -def video_chat_ended_filter(_, __, m: Message): - return bool(m.video_chat_ended) - - -video_chat_ended = create(video_chat_ended_filter) +video_chat_ended = create(_attribute_filter("video_chat_ended"), "video_chat_ended_filter") """Filter messages for ended video chats""" - -# endregion - - -# region video_chat_members_invited_filter -def video_chat_members_invited_filter(_, __, m: Message): - return bool(m.video_chat_members_invited) - - -video_chat_members_invited = create(video_chat_members_invited_filter) +video_chat_members_invited = create( + _attribute_filter("video_chat_members_invited"), "video_chat_members_invited_filter" +) """Filter messages for voice chat invited members""" -# endregion - - -# region service_filter -def service_filter(_, __, m: Message): +def service_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return bool(m.service) @@ -745,57 +388,33 @@ def service_filter(_, __, m: Message): """Filter service messages. A service message contains any of the following fields set: *left_chat_member*, -*new_chat_title*, *new_chat_photo*, *delete_chat_photo*, *group_chat_created*, *supergroup_chat_created*, -*channel_chat_created*, *migrate_to_chat_id*, *migrate_from_chat_id*, *pinned_message*, *game_score*, -*video_chat_started*, *video_chat_ended*, *video_chat_members_invited*. +*new_chat_title*, *new_chat_photo*, *delete_chat_photo*, *group_chat_created*, +*supergroup_chat_created*, *channel_chat_created*, *migrate_to_chat_id*, +*migrate_from_chat_id*, *pinned_message*, *game_score*, *video_chat_started*, +*video_chat_ended*, *video_chat_members_invited*. """ -# endregion - - -# region media_filter -def media_filter(_, __, m: Message): +def media_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return bool(m.media) media = create(media_filter) """Filter media messages. -A media message contains any of the following fields set: *audio*, *document*, *photo*, *sticker*, *video*, -*animation*, *voice*, *video_note*, *contact*, *location*, *venue*, *poll*. +A media message contains any of the following fields set: *audio*, *document*, *photo*, +*sticker*, *video*, *animation*, *voice*, *video_note*, *contact*, *location*, *venue*, *poll*. """ -# endregion - - -# region scheduled_filter -def scheduled_filter(_, __, m: Message): - return bool(m.scheduled) - - -scheduled = create(scheduled_filter) +scheduled = create(_attribute_filter("scheduled"), "scheduled_filter") """Filter messages that have been scheduled (not yet sent).""" - -# endregion - - -# region from_scheduled_filter -def from_scheduled_filter(_, __, m: Message): - return bool(m.from_scheduled) - - -from_scheduled = create(from_scheduled_filter) +from_scheduled = create(_attribute_filter("from_scheduled"), "from_scheduled_filter") """Filter new automatically sent messages that were previously scheduled.""" -# endregion - - -# region linked_channel_filter -def linked_channel_filter(_, __, m: Message): +def linked_channel_filter(_: Filter, __: hydrogram.Client, m: Message) -> bool: return bool(m.forward_from_chat and not m.from_user) @@ -803,15 +422,11 @@ def linked_channel_filter(_, __, m: Message): """Filter messages that are automatically forwarded from the linked channel to the group chat.""" -# endregion - - -# region command_filter def command( commands: str | list[str], prefixes: str | list[str] = "/", case_sensitive: bool = False, -): +) -> Filter: """Filter commands, i.e.: text messages starting with "/" or any other custom prefix. Parameters: @@ -832,8 +447,8 @@ def command( """ command_re = re.compile(r"([\"'])(.*?)(? bool: + username = client.me.username or "" # type: ignore text = message.text or message.caption message.command = None @@ -862,10 +477,6 @@ def func(flt, client: hydrogram.Client, message: Message): flags=0 if flt.case_sensitive else re.IGNORECASE, ) - # match.groups are 1-indexed, group(1) is the quote, group(2) is the text - # between the quotes, group(3) is unquoted, whitespace-split text - - # Remove the escape character from the arguments message.command = [cmd] + [ re.sub(r"\\([\"'])", r"\1", m.group(2) or m.group(3) or "") for m in command_re.finditer(without_command) @@ -875,26 +486,27 @@ def func(flt, client: hydrogram.Client, message: Message): return False - commands = commands if isinstance(commands, list) else [commands] - commands = {c if case_sensitive else c.lower() for c in commands} + commands_list = [commands] if isinstance(commands, str) else commands + commands_set = {c if case_sensitive else c.lower() for c in commands_list} - prefixes = [] if prefixes is None else prefixes - prefixes = prefixes if isinstance(prefixes, list) else [prefixes] - prefixes = set(prefixes) if prefixes else {""} + if prefixes is None: + prefixes_list = [] + elif isinstance(prefixes, str): + prefixes_list = [prefixes] + else: + prefixes_list = prefixes + prefixes_set = set(prefixes_list) if prefixes_list else {""} return create( func, "CommandFilter", - commands=commands, - prefixes=prefixes, + commands=commands_set, + prefixes=prefixes_set, case_sensitive=case_sensitive, ) -# endregion - - -def regex(pattern: str | Pattern, flags: int = 0): +def regex(pattern: str | Pattern, flags: int = 0) -> Filter: """Filter updates that match a given regular expression pattern. Can be applied to handlers that receive one of the following updates: @@ -903,7 +515,8 @@ def regex(pattern: str | Pattern, flags: int = 0): - :obj:`~hydrogram.types.CallbackQuery`: The filter will match ``data``. - :obj:`~hydrogram.types.InlineQuery`: The filter will match ``query``. - When a pattern matches, all the `Match Objects `_ are + When a pattern matches, all the + `Match Objects `_ are stored in the ``matches`` field of the update object itself. Parameters: @@ -914,7 +527,7 @@ def regex(pattern: str | Pattern, flags: int = 0): Regex flags. """ - def func(flt, _, update: Update): + def func(flt: Filter, __: hydrogram.Client, update: Update) -> bool: if isinstance(update, Message): value = update.text or update.caption elif isinstance(update, CallbackQuery): @@ -939,8 +552,8 @@ def func(flt, _, update: Update): class user(Filter, set): # noqa: N801 """Filter messages coming from one or more users. - You can use `set bound methods `_ to manipulate the - users container. + You can use `set bound methods `_ + to manipulate the users container. Parameters: users (``int`` | ``str`` | ``list``): @@ -968,8 +581,8 @@ async def __call__(self, _, message: Message): class chat(Filter, set): # noqa: N801 """Filter messages coming from one or more chats. - You can use `set bound methods `_ to manipulate the - chats container. + You can use `set bound methods `_ + to manipulate the chats container. Parameters: chats (``int`` | ``str`` | ``list``): diff --git a/hydrogram/handlers/callback_query_handler.py b/hydrogram/handlers/callback_query_handler.py index b36d01bc1..bf5b998e9 100644 --- a/hydrogram/handlers/callback_query_handler.py +++ b/hydrogram/handlers/callback_query_handler.py @@ -20,7 +20,7 @@ from __future__ import annotations from asyncio import iscoroutinefunction -from typing import Callable +from typing import TYPE_CHECKING import hydrogram from hydrogram.types import CallbackQuery, Identifier, Listener, ListenerTypes @@ -28,6 +28,11 @@ from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class CallbackQueryHandler(Handler): """The CallbackQuery handler class. Used to handle callback queries coming from inline buttons. @@ -53,7 +58,7 @@ class CallbackQueryHandler(Handler): The received callback query. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): self.original_callback = callback super().__init__(self.resolve_future_or_callback, filters) diff --git a/hydrogram/handlers/chat_join_request_handler.py b/hydrogram/handlers/chat_join_request_handler.py index acb602b47..dddc28346 100644 --- a/hydrogram/handlers/chat_join_request_handler.py +++ b/hydrogram/handlers/chat_join_request_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class ChatJoinRequestHandler(Handler): """The ChatJoinRequest handler class. Used to handle join chat requests. @@ -46,5 +53,5 @@ class ChatJoinRequestHandler(Handler): The received chat join request. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/handlers/chat_member_updated_handler.py b/hydrogram/handlers/chat_member_updated_handler.py index be7736927..ee8e42f70 100644 --- a/hydrogram/handlers/chat_member_updated_handler.py +++ b/hydrogram/handlers/chat_member_updated_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class ChatMemberUpdatedHandler(Handler): """The ChatMemberUpdated handler class. Used to handle changes in the status of a chat member. @@ -46,5 +53,5 @@ class ChatMemberUpdatedHandler(Handler): The received chat member update. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/handlers/chosen_inline_result_handler.py b/hydrogram/handlers/chosen_inline_result_handler.py index 5bec55c25..c74bd80bf 100644 --- a/hydrogram/handlers/chosen_inline_result_handler.py +++ b/hydrogram/handlers/chosen_inline_result_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class ChosenInlineResultHandler(Handler): """The ChosenInlineResultHandler handler class. Used to handle chosen inline results coming from inline queries. @@ -47,5 +54,5 @@ class ChosenInlineResultHandler(Handler): The received chosen inline result. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/handlers/deleted_messages_handler.py b/hydrogram/handlers/deleted_messages_handler.py index 26107f5a6..3001ffdfa 100644 --- a/hydrogram/handlers/deleted_messages_handler.py +++ b/hydrogram/handlers/deleted_messages_handler.py @@ -17,15 +17,18 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import TYPE_CHECKING, Callable +from __future__ import annotations -from hydrogram.filters import Filter -from hydrogram.types import Message +from typing import TYPE_CHECKING from .handler import Handler if TYPE_CHECKING: + from collections.abc import Callable + import hydrogram + from hydrogram.filters import Filter + from hydrogram.types import Message class DeletedMessagesHandler(Handler): @@ -52,10 +55,10 @@ class DeletedMessagesHandler(Handler): The deleted messages, as list. """ - def __init__(self, callback: Callable, filters: Filter = None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) - async def check(self, client: "hydrogram.Client", messages: list[Message]): + async def check(self, client: hydrogram.Client, messages: list[Message]): # Every message should be checked, if at least one matches the filter True is returned # otherwise, or if the list is empty, False is returned for message in messages: diff --git a/hydrogram/handlers/disconnect_handler.py b/hydrogram/handlers/disconnect_handler.py index 06a119d87..00ade423b 100644 --- a/hydrogram/handlers/disconnect_handler.py +++ b/hydrogram/handlers/disconnect_handler.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from collections.abc import Callable from .handler import Handler diff --git a/hydrogram/handlers/edited_message_handler.py b/hydrogram/handlers/edited_message_handler.py index 8c5b43b8e..84132a960 100644 --- a/hydrogram/handlers/edited_message_handler.py +++ b/hydrogram/handlers/edited_message_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class EditedMessageHandler(Handler): """The EditedMessage handler class. Used to handle edited messages. @@ -46,5 +53,5 @@ class EditedMessageHandler(Handler): The received edited message. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/handlers/error_handler.py b/hydrogram/handlers/error_handler.py index 026ecbafc..fc624a705 100644 --- a/hydrogram/handlers/error_handler.py +++ b/hydrogram/handlers/error_handler.py @@ -18,13 +18,14 @@ from __future__ import annotations -from collections.abc import Iterable -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING from .handler import Handler if TYPE_CHECKING: import hydrogram + from hydrogram.types import Update class ErrorHandler(Handler): @@ -39,7 +40,7 @@ class ErrorHandler(Handler): Pass a function that will be called when a new Error arrives. It takes *(client, error)* as positional arguments (look at the section below for a detailed description). - errors (``Exception`` | Iterable of ``Exception``, *optional*): + exceptions (``Exception`` | Iterable of ``Exception``, *optional*): Pass one or more exception classes to allow only a subset of errors to be passed in your callback function. @@ -47,26 +48,32 @@ class ErrorHandler(Handler): client (:obj:`~hydrogram.Client`): The Client itself, useful when you want to call other API methods inside the error handler. - error (``Exception``): - The error that was raised. - update (:obj:`~hydrogram.Update`): The update that caused the error. + + error (``Exception``): + The error that was raised. """ def __init__( - self, callback: Callable, errors: type[Exception] | Iterable[type[Exception]] | None = None + self, + callback: Callable, + exceptions: type[Exception] | Iterable[type[Exception]] | None = None, ): - if errors is None: - errors = [Exception] - elif not isinstance(errors, Iterable): - errors = [errors] - - self.errors = errors + self.exceptions = ( + tuple(exceptions) + if isinstance(exceptions, Iterable) + else (exceptions,) + if exceptions + else (Exception,) + ) super().__init__(callback) - async def check(self, client: hydrogram.Client, error: Exception): - return any(isinstance(error, e) for e in self.errors) + async def check(self, client: hydrogram.Client, update: Update, exception: Exception) -> bool: + if isinstance(exception, self.exceptions): + await self.callback(client, update, exception) + return True + return False - def check_remove(self, error: Exception): - return self.errors == error or any(isinstance(error, e) for e in self.errors) + def check_remove(self, error: type[Exception] | Iterable[type[Exception]]) -> bool: + return isinstance(error, self.exceptions) diff --git a/hydrogram/handlers/handler.py b/hydrogram/handlers/handler.py index cf8e57092..5c3b2106d 100644 --- a/hydrogram/handlers/handler.py +++ b/hydrogram/handlers/handler.py @@ -17,22 +17,25 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -import inspect -from typing import TYPE_CHECKING, Callable +from __future__ import annotations -from hydrogram.filters import Filter -from hydrogram.types import Update +import inspect +from typing import TYPE_CHECKING if TYPE_CHECKING: + from collections.abc import Callable + import hydrogram + from hydrogram.filters import Filter + from hydrogram.types import Update class Handler: - def __init__(self, callback: Callable, filters: Filter = None): + def __init__(self, callback: Callable, filters: Filter | None = None): self.callback = callback self.filters = filters - async def check(self, client: "hydrogram.Client", update: Update): + async def check(self, client: hydrogram.Client, update: Update): if callable(self.filters): if inspect.iscoroutinefunction(self.filters.__call__): return await self.filters(client, update) diff --git a/hydrogram/handlers/inline_query_handler.py b/hydrogram/handlers/inline_query_handler.py index 3d54d1797..3ad0ff9f7 100644 --- a/hydrogram/handlers/inline_query_handler.py +++ b/hydrogram/handlers/inline_query_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class InlineQueryHandler(Handler): """The InlineQuery handler class. Used to handle inline queries. @@ -46,5 +53,5 @@ class InlineQueryHandler(Handler): The received inline query. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/handlers/message_handler.py b/hydrogram/handlers/message_handler.py index fbcea445d..d697c8f36 100644 --- a/hydrogram/handlers/message_handler.py +++ b/hydrogram/handlers/message_handler.py @@ -20,13 +20,18 @@ from __future__ import annotations from inspect import iscoroutinefunction -from typing import Callable +from typing import TYPE_CHECKING import hydrogram from hydrogram.types import Identifier, Listener, ListenerTypes, Message from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class MessageHandler(Handler): """The Message handler class. Used to handle new messages. @@ -52,7 +57,7 @@ class MessageHandler(Handler): The received message. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): self.original_callback = callback super().__init__(self.resolve_future_or_callback, filters) diff --git a/hydrogram/handlers/poll_handler.py b/hydrogram/handlers/poll_handler.py index e6e33b72f..90ee38056 100644 --- a/hydrogram/handlers/poll_handler.py +++ b/hydrogram/handlers/poll_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class PollHandler(Handler): """The Poll handler class. Used to handle polls updates. @@ -47,5 +54,5 @@ class PollHandler(Handler): The received poll. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/handlers/raw_update_handler.py b/hydrogram/handlers/raw_update_handler.py index 4da664063..40258d4c2 100644 --- a/hydrogram/handlers/raw_update_handler.py +++ b/hydrogram/handlers/raw_update_handler.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from collections.abc import Callable from .handler import Handler diff --git a/hydrogram/handlers/user_status_handler.py b/hydrogram/handlers/user_status_handler.py index 8e275c664..3af4bc13e 100644 --- a/hydrogram/handlers/user_status_handler.py +++ b/hydrogram/handlers/user_status_handler.py @@ -17,10 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from typing import TYPE_CHECKING from .handler import Handler +if TYPE_CHECKING: + from collections.abc import Callable + + from hydrogram.filters import Filter + class UserStatusHandler(Handler): """The UserStatus handler class. Used to handle user status updates (user going online or offline). @@ -44,5 +51,5 @@ class UserStatusHandler(Handler): The user containing the updated status. """ - def __init__(self, callback: Callable, filters=None): + def __init__(self, callback: Callable, filters: Filter | None = None): super().__init__(callback, filters) diff --git a/hydrogram/methods/__init__.py b/hydrogram/methods/__init__.py index d77eade20..913af423e 100644 --- a/hydrogram/methods/__init__.py +++ b/hydrogram/methods/__init__.py @@ -26,6 +26,7 @@ from .invite_links import InviteLinks from .messages import Messages from .password import Password +from .phone import Phone from .pyromod import Pyromod from .users import Users from .utilities import Utilities @@ -44,5 +45,6 @@ class Methods( Decorators, Utilities, InviteLinks, + Phone, ): pass diff --git a/hydrogram/methods/advanced/invoke.py b/hydrogram/methods/advanced/invoke.py index 2929e8af4..91146960c 100644 --- a/hydrogram/methods/advanced/invoke.py +++ b/hydrogram/methods/advanced/invoke.py @@ -59,7 +59,8 @@ async def invoke( The API Schema function filled with proper arguments. retries (``int``): - Number of retries. + Attempt limit for requests not yet sent and explicit flood waits. + Requests with uncertain delivery and server errors are not replayed. timeout (``float``): Timeout in seconds. @@ -72,6 +73,10 @@ async def invoke( Raises: RPCError: In case of a Telegram RPC error. + ConnectionLostError: If the transport fails. Check ``delivery_uncertain`` + before deciding whether the request can be safely retried. + RequestTimeoutError: If Telegram does not respond in time. The request + may have been processed; check its outcome before retrying. """ if not self.is_connected: raise ConnectionError("Client has not been started yet") @@ -89,7 +94,9 @@ async def invoke( (sleep_threshold if sleep_threshold is not None else self.sleep_threshold), ) - await self.fetch_peers(getattr(r, "users", [])) - await self.fetch_peers(getattr(r, "chats", [])) + # Join methods wrap Updates in a ChatInviteJoinResult at newer layers. + peers = r.updates if isinstance(r, raw.types.messages.ChatInviteJoinResultOk) else r + await self.fetch_peers(getattr(peers, "users", None) or []) + await self.fetch_peers(getattr(peers, "chats", None) or []) return r diff --git a/hydrogram/methods/advanced/save_file.py b/hydrogram/methods/advanced/save_file.py index 599bf54cd..06bb9ec32 100644 --- a/hydrogram/methods/advanced/save_file.py +++ b/hydrogram/methods/advanced/save_file.py @@ -19,21 +19,22 @@ from __future__ import annotations -import asyncio import functools import inspect import io -import logging import math +from contextlib import AsyncExitStack from hashlib import md5 -from pathlib import Path, PurePath -from typing import BinaryIO, Callable +from pathlib import PurePath +from typing import TYPE_CHECKING, BinaryIO import hydrogram -from hydrogram import StopTransmission, raw +from hydrogram import raw from hydrogram.session import Session +from hydrogram.transmission import finish, open_file, run_sync -log = logging.getLogger(__name__) +if TYPE_CHECKING: + from collections.abc import Callable class SaveFile: @@ -95,26 +96,13 @@ async def save_file( Raises: RPCError: In case of a Telegram RPC error. """ - async with self.save_file_semaphore: - if path is None: - return None - - async def worker(session): - while True: - data = await queue.get() - - if data is None: - return - - try: - await session.invoke(data) - except Exception as e: - log.exception(e) + if path is None: + return None + async with self.save_file_semaphore, AsyncExitStack() as stack: part_size = 512 * 1024 - if isinstance(path, (str, PurePath)): - fp = Path(path).open("rb") # noqa: SIM115 + fp = await stack.enter_async_context(open_file(path, "rb")) elif isinstance(path, io.IOBase): fp = path else: @@ -122,24 +110,18 @@ async def worker(session): "Invalid file. Expected a file path as string or a binary (not text) file pointer" ) - fp.seek(0, io.SEEK_END) - - file_size = fp.tell() - + await run_sync(fp.seek, 0, io.SEEK_END) + file_size = await run_sync(fp.tell) if file_size == 0: raise ValueError("File size equals to 0 B") file_name = getattr(fp, "name", "file.jpg") - fp.seek(0) - file_size_limit_mib = 4000 if self.me.is_premium else 2000 - if file_size > file_size_limit_mib * 1024 * 1024: raise ValueError(f"Can't upload files bigger than {file_size_limit_mib} MiB") - file_total_parts = int(math.ceil(file_size / part_size)) + file_total_parts = math.ceil(file_size / part_size) is_big = file_size > 10 * 1024 * 1024 - workers_count = 4 if is_big else 1 is_missing_part = file_id is not None file_id = file_id or self.rnd_id() md5_sum = md5() if not is_big and not is_missing_part else None @@ -150,22 +132,10 @@ async def worker(session): await self.storage.test_mode(), is_media=True, ) - workers = [self.loop.create_task(worker(session)) for _ in range(workers_count)] - queue = asyncio.Queue(1) - try: await session.start() - - fp.seek(part_size * file_part) - - while True: - chunk = fp.read(part_size) - - if not chunk: - if not is_big and not is_missing_part: - md5_sum = "".join([f"{i:x}".zfill(2) for i in md5_sum.digest()]) - break - + await run_sync(fp.seek, part_size * file_part) + while chunk := await run_sync(fp.read, part_size): if is_big: rpc = raw.functions.upload.SaveBigFilePart( file_id=file_id, @@ -178,16 +148,16 @@ async def worker(session): file_id=file_id, file_part=file_part, bytes=chunk ) - await queue.put(rpc) - + # Backpressure: retain only one chunk and wait for its acknowledgement. + # Session.invoke handles safe pre-delivery recovery; ambiguous failures + # must escape without replaying the upload or reporting success. + if not await session.invoke(rpc): + raise OSError("Telegram rejected an uploaded file part") if is_missing_part: return None - - if not is_big and not is_missing_part: + if md5_sum is not None: md5_sum.update(chunk) - file_part += 1 - if progress: func = functools.partial( progress, @@ -195,35 +165,20 @@ async def worker(session): file_size, *progress_args, ) - if inspect.iscoroutinefunction(progress): await func() else: - await self.loop.run_in_executor(self.executor, func) - except StopTransmission: - raise - except Exception as e: - log.exception(e) - else: + await finish(self.loop.run_in_executor(self.executor, func)) + if is_big: return raw.types.InputFileBig( - id=file_id, - parts=file_total_parts, - name=file_name, + id=file_id, parts=file_total_parts, name=file_name ) return raw.types.InputFile( id=file_id, parts=file_total_parts, name=file_name, - md5_checksum=md5_sum, + md5_checksum=md5_sum.hexdigest(), ) finally: - for _ in workers: - await queue.put(None) - - await asyncio.gather(*workers) - - await session.stop() - - if isinstance(path, (str, PurePath)): - fp.close() + await finish(session.stop()) diff --git a/hydrogram/methods/auth/__init__.py b/hydrogram/methods/auth/__init__.py index aee2cf388..60e0d8281 100644 --- a/hydrogram/methods/auth/__init__.py +++ b/hydrogram/methods/auth/__init__.py @@ -22,6 +22,7 @@ from .connect import Connect from .disconnect import Disconnect from .get_password_hint import GetPasswordHint +from .get_sessions import GetSessions from .initialize import Initialize from .log_out import LogOut from .recover_password import RecoverPassword @@ -50,5 +51,6 @@ class Auth( SignInBot, SignUp, Terminate, + GetSessions, ): pass diff --git a/hydrogram/methods/auth/connect.py b/hydrogram/methods/auth/connect.py index 52e443ee8..b536b3297 100644 --- a/hydrogram/methods/auth/connect.py +++ b/hydrogram/methods/auth/connect.py @@ -51,7 +51,12 @@ async def connect( await self.storage.test_mode(), ) - await self.session.start() + try: + await self.session.start() + except BaseException: + await self.session.stop() + await self.storage.close() + raise self.is_connected = True diff --git a/hydrogram/methods/auth/get_sessions.py b/hydrogram/methods/auth/get_sessions.py new file mode 100644 index 000000000..51ce67c0b --- /dev/null +++ b/hydrogram/methods/auth/get_sessions.py @@ -0,0 +1,38 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +import logging + +import hydrogram +from hydrogram import raw, types + +log = logging.getLogger(__name__) + + +class GetSessions: + async def get_sessions( + self: "hydrogram.Client", + ) -> list["types.Session"]: + """Get your info data by other sessions . + + Returns: + List[:obj:`~hydrogram.types.Session`]: List of active sessions. + """ + + authorizations = await self.invoke(raw.functions.account.GetAuthorizations()) + return [types.Session._parse(auth) for auth in authorizations.authorizations] diff --git a/hydrogram/methods/auth/initialize.py b/hydrogram/methods/auth/initialize.py index e0a7d7a15..cbaeb3ca1 100644 --- a/hydrogram/methods/auth/initialize.py +++ b/hydrogram/methods/auth/initialize.py @@ -48,6 +48,9 @@ async def initialize( self.load_plugins() + if self.update_manager is not None: + await self.update_manager.start() + await self.dispatcher.start() self.updates_watchdog_task = asyncio.create_task(self.updates_watchdog()) diff --git a/hydrogram/methods/auth/terminate.py b/hydrogram/methods/auth/terminate.py index aa0b34d4d..4660c7a0d 100644 --- a/hydrogram/methods/auth/terminate.py +++ b/hydrogram/methods/auth/terminate.py @@ -17,10 +17,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +import asyncio import logging import hydrogram from hydrogram import raw +from hydrogram.errors import RPCError log = logging.getLogger(__name__) @@ -41,8 +43,16 @@ async def terminate( raise ConnectionError("Client is already terminated") if self.takeout_id: - await self.invoke(raw.functions.account.FinishTakeoutSession()) - log.info("Takeout session %s finished", self.takeout_id) + try: + await self.invoke(raw.functions.account.FinishTakeoutSession()) + except (TimeoutError, OSError, RPCError): + log.warning("Could not finish takeout session during shutdown", exc_info=True) + else: + log.info("Takeout session %s finished", self.takeout_id) + self.takeout_id = None + + if self.update_manager is not None: + await self.update_manager.stop() await self.storage.save() await self.dispatcher.stop() @@ -55,7 +65,9 @@ async def terminate( self.updates_watchdog_event.set() if self.updates_watchdog_task is not None: - await self.updates_watchdog_task + self.updates_watchdog_task.cancel() + await asyncio.gather(self.updates_watchdog_task, return_exceptions=True) + self.updates_watchdog_task = None self.updates_watchdog_event.clear() diff --git a/hydrogram/methods/bots/answer_inline_query.py b/hydrogram/methods/bots/answer_inline_query.py index 69e6886e5..c82bde644 100644 --- a/hydrogram/methods/bots/answer_inline_query.py +++ b/hydrogram/methods/bots/answer_inline_query.py @@ -91,7 +91,9 @@ async def answer_inline_query( await app.answer_inline_query( inline_query_id, results=[ - InlineQueryResultArticle("Title", InputTextMessageContent("Message content")) + InlineQueryResultArticle( + "Title", InputTextMessageContent("Message content") + ) ], ) """ diff --git a/hydrogram/methods/bots/send_game.py b/hydrogram/methods/bots/send_game.py index 4ef0e0039..32d05a8a3 100644 --- a/hydrogram/methods/bots/send_game.py +++ b/hydrogram/methods/bots/send_game.py @@ -37,6 +37,7 @@ async def send_game( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message: """Send a game. @@ -69,6 +70,15 @@ async def send_game( An object for an inline keyboard. If empty, one ‘Play game_title’ button will be shown automatically. If not empty, the first button must launch the game. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent game message is returned. @@ -77,6 +87,8 @@ async def send_game( await app.send_game(chat_id, "gamename") """ + + utils.validate_random_id(random_id) reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) r = await self.invoke( @@ -90,7 +102,7 @@ async def send_game( message="", silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, ) diff --git a/hydrogram/methods/bots/send_inline_bot_result.py b/hydrogram/methods/bots/send_inline_bot_result.py index cafb7e6e9..77c57a57d 100644 --- a/hydrogram/methods/bots/send_inline_bot_result.py +++ b/hydrogram/methods/bots/send_inline_bot_result.py @@ -33,6 +33,7 @@ async def send_inline_bot_result( message_thread_id: int | None = None, disable_notification: bool | None = None, reply_to_message_id: int | None = None, + random_id: int | None = None, ) -> raw.base.Updates: """Send an inline bot result. Bot results can be retrieved using :meth:`~hydrogram.Client.get_inline_bot_results` @@ -62,6 +63,15 @@ async def send_inline_bot_result( reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.raw.base.Updates`: Currently, on success, a raw result is returned. @@ -71,6 +81,8 @@ async def send_inline_bot_result( await app.send_inline_bot_result(chat_id, query_id, result_id) """ + utils.validate_random_id(random_id) + reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) return await self.invoke( @@ -78,7 +90,7 @@ async def send_inline_bot_result( peer=await self.resolve_peer(chat_id), query_id=query_id, id=result_id, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, silent=disable_notification or None, reply_to=reply_to, ) diff --git a/hydrogram/methods/chats/__init__.py b/hydrogram/methods/chats/__init__.py index 4f56d35c9..182c0b904 100644 --- a/hydrogram/methods/chats/__init__.py +++ b/hydrogram/methods/chats/__init__.py @@ -63,6 +63,7 @@ from .set_chat_username import SetChatUsername from .set_send_as_chat import SetSendAsChat from .set_slow_mode import SetSlowMode +from .transfer_chat_ownership import TransferChatOwnership from .unarchive_chats import UnarchiveChats from .unban_chat_member import UnbanChatMember from .unhide_general_topic import UnhideGeneralTopic @@ -122,5 +123,6 @@ class Chats( GetSendAsChats, SetSendAsChat, SetChatProtectedContent, + TransferChatOwnership, ): pass diff --git a/hydrogram/methods/chats/get_forum_topics.py b/hydrogram/methods/chats/get_forum_topics.py index 2b721b08a..2457df1d8 100644 --- a/hydrogram/methods/chats/get_forum_topics.py +++ b/hydrogram/methods/chats/get_forum_topics.py @@ -67,5 +67,5 @@ async def get_forum_topics( r = await self.invoke(rpc, sleep_threshold=-1) - for _topic in r.topics: - yield types.ForumTopic._parse(_topic) + for topic in r.topics: + yield types.ForumTopic._parse(topic) diff --git a/hydrogram/methods/chats/join_chat.py b/hydrogram/methods/chats/join_chat.py index d76ed45b9..043731a37 100644 --- a/hydrogram/methods/chats/join_chat.py +++ b/hydrogram/methods/chats/join_chat.py @@ -20,7 +20,7 @@ from __future__ import annotations import hydrogram -from hydrogram import raw, types +from hydrogram import errors, raw, types class JoinChat: @@ -51,6 +51,7 @@ async def join_chat(self: hydrogram.Client, chat_id: int | str) -> types.Chat: """ if match := self.INVITE_LINK_RE.match(str(chat_id)): chat = await self.invoke(raw.functions.messages.ImportChatInvite(hash=match.group(1))) + chat = self._unwrap_chat_join_result(chat) if isinstance(chat.chats[0], raw.types.Chat): return types.Chat._parse_chat_chat(self, chat.chats[0]) if isinstance(chat.chats[0], raw.types.Channel): @@ -61,4 +62,16 @@ async def join_chat(self: hydrogram.Client, chat_id: int | str) -> types.Chat: raw.functions.channels.JoinChannel(channel=await self.resolve_peer(chat_id)) ) + chat = self._unwrap_chat_join_result(chat) return types.Chat._parse_channel_chat(self, chat.chats[0]) + + @staticmethod + def _unwrap_chat_join_result( + result: raw.base.messages.ChatInviteJoinResult, + ) -> raw.base.Updates: + if isinstance(result, raw.types.messages.ChatInviteJoinResultOk): + return result.updates + raise errors.NotAcceptable( + "Joining this chat requires bot verification in a Telegram webview. " + "Complete verification in an official Telegram client and try again." + ) diff --git a/hydrogram/methods/chats/restrict_chat_member.py b/hydrogram/methods/chats/restrict_chat_member.py index 1630ab10b..ee494ec0b 100644 --- a/hydrogram/methods/chats/restrict_chat_member.py +++ b/hydrogram/methods/chats/restrict_chat_member.py @@ -77,7 +77,9 @@ async def restrict_chat_member( ) # Chat member can only send text messages - await app.restrict_chat_member(chat_id, user_id, ChatPermissions(can_send_messages=True)) + await app.restrict_chat_member( + chat_id, user_id, ChatPermissions(can_send_messages=True) + ) """ r = await self.invoke( raw.functions.channels.EditBanned( diff --git a/hydrogram/methods/chats/transfer_chat_ownership.py b/hydrogram/methods/chats/transfer_chat_ownership.py new file mode 100644 index 000000000..670fabce1 --- /dev/null +++ b/hydrogram/methods/chats/transfer_chat_ownership.py @@ -0,0 +1,75 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import hydrogram +from hydrogram import raw +from hydrogram.utils import compute_password_check + + +class TransferChatOwnership: + async def transfer_chat_ownership( + self: hydrogram.Client, + chat_id: int | str, + user_id: int | str, + password: str, + ) -> bool: + """Set new chat owner. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + + user_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the new owner. The ownership can't be transferred to a bot or to a deleted user. + + password (``str``): + The 2-step verification password of the current user. + + Returns: + ``bool``: True on success. + + Example: + .. code-block:: python + + await app.transfer_chat_ownership(chat_id, user_id, "password") + """ + + peer_channel = await self.resolve_peer(chat_id) + peer_user = await self.resolve_peer(user_id) + + if not isinstance(peer_channel, raw.types.InputPeerChannel): + raise ValueError("The chat_id must belong to a channel/supergroup.") + + if not isinstance(peer_user, raw.types.InputPeerUser): + raise ValueError("The user_id must belong to a user.") + + r = await self.invoke( + raw.functions.channels.EditCreator( + channel=peer_channel, + user_id=peer_user, + password=compute_password_check( + await self.invoke(raw.functions.account.GetPassword()), password + ), + ) + ) + + return bool(r) diff --git a/hydrogram/methods/decorators/on_callback_query.py b/hydrogram/methods/decorators/on_callback_query.py index b27caf2ed..eba67ca29 100644 --- a/hydrogram/methods/decorators/on_callback_query.py +++ b/hydrogram/methods/decorators/on_callback_query.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnCallbackQuery: - def on_callback_query(self=None, filters=None, group: int = 0) -> Callable: + def on_callback_query( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling callback queries. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_callback_query(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.CallbackQueryHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_chat_join_request.py b/hydrogram/methods/decorators/on_chat_join_request.py index 3bdac34c5..676c55121 100644 --- a/hydrogram/methods/decorators/on_chat_join_request.py +++ b/hydrogram/methods/decorators/on_chat_join_request.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnChatJoinRequest: - def on_chat_join_request(self=None, filters=None, group: int = 0) -> Callable: + def on_chat_join_request( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling chat join requests. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -32,13 +41,14 @@ def on_chat_join_request(self=None, filters=None, group: int = 0) -> Callable: Parameters: filters (:obj:`~hydrogram.filters`, *optional*): - Pass one or more filters to allow only a subset of updates to be passed in your function. + Pass one or more filters to allow only a subset of chat join requests to be passed + in your function. group (``int``, *optional*): The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.ChatJoinRequestHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_chat_member_updated.py b/hydrogram/methods/decorators/on_chat_member_updated.py index 15e1013fa..ebe1fba35 100644 --- a/hydrogram/methods/decorators/on_chat_member_updated.py +++ b/hydrogram/methods/decorators/on_chat_member_updated.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnChatMemberUpdated: - def on_chat_member_updated(self=None, filters=None, group: int = 0) -> Callable: + def on_chat_member_updated( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling event changes on chat members. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -38,7 +47,7 @@ def on_chat_member_updated(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.ChatMemberUpdatedHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_chosen_inline_result.py b/hydrogram/methods/decorators/on_chosen_inline_result.py index c85ccdd1a..9734b9adf 100644 --- a/hydrogram/methods/decorators/on_chosen_inline_result.py +++ b/hydrogram/methods/decorators/on_chosen_inline_result.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnChosenInlineResult: - def on_chosen_inline_result(self=None, filters=None, group: int = 0) -> Callable: + def on_chosen_inline_result( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling chosen inline results. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_chosen_inline_result(self=None, filters=None, group: int = 0) -> Callable The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler( hydrogram.handlers.ChosenInlineResultHandler(func, filters), group diff --git a/hydrogram/methods/decorators/on_deleted_messages.py b/hydrogram/methods/decorators/on_deleted_messages.py index 20dcc9989..0716dcda0 100644 --- a/hydrogram/methods/decorators/on_deleted_messages.py +++ b/hydrogram/methods/decorators/on_deleted_messages.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnDeletedMessages: - def on_deleted_messages(self=None, filters=None, group: int = 0) -> Callable: + def on_deleted_messages( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling deleted messages. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_deleted_messages(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.DeletedMessagesHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_disconnect.py b/hydrogram/methods/decorators/on_disconnect.py index aacdceee8..07c685105 100644 --- a/hydrogram/methods/decorators/on_disconnect.py +++ b/hydrogram/methods/decorators/on_disconnect.py @@ -17,20 +17,25 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram +F = TypeVar("F", bound=Callable) + class OnDisconnect: - def on_disconnect(self=None) -> Callable: + def on_disconnect(self: hydrogram.Client | None | None = None) -> Callable[[F], F]: # type: ignore """Decorator for handling disconnections. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the :obj:`~hydrogram.handlers.DisconnectHandler`. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.DisconnectHandler(func)) else: diff --git a/hydrogram/methods/decorators/on_edited_message.py b/hydrogram/methods/decorators/on_edited_message.py index 608644ff5..0b05b2c04 100644 --- a/hydrogram/methods/decorators/on_edited_message.py +++ b/hydrogram/methods/decorators/on_edited_message.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnEditedMessage: - def on_edited_message(self=None, filters=None, group: int = 0) -> Callable: + def on_edited_message( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling edited messages. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_edited_message(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.EditedMessageHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_error.py b/hydrogram/methods/decorators/on_error.py index eb3c2dfd4..69ac06e0b 100644 --- a/hydrogram/methods/decorators/on_error.py +++ b/hydrogram/methods/decorators/on_error.py @@ -16,18 +16,25 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram -from hydrogram.filters import Filter + +F = TypeVar("F", bound=Callable) class OnError: - def on_error(self=None, errors=None) -> Callable: + def on_error( + self: hydrogram.Client | type[Exception] | None = None, # type: ignore + errors: type[Exception] | list[type[Exception]] | None = None, + ) -> Callable[[F], F]: """Decorator for handling new errors. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the - :obj:`~hydrogram.handlers.MessageHandler`. + :obj:`~hydrogram.handlers.ErrorHandler`. Parameters: errors (:obj:`~Exception`, *optional*): @@ -35,10 +42,10 @@ def on_error(self=None, errors=None) -> Callable: in your function. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.ErrorHandler(func, errors), 0) - elif isinstance(self, Filter) or self is None: + elif (isinstance(self, type) and issubclass(self, Exception)) or self is None: if not hasattr(func, "handlers"): func.handlers = [] diff --git a/hydrogram/methods/decorators/on_inline_query.py b/hydrogram/methods/decorators/on_inline_query.py index 4b8685cc1..bce7bcac4 100644 --- a/hydrogram/methods/decorators/on_inline_query.py +++ b/hydrogram/methods/decorators/on_inline_query.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnInlineQuery: - def on_inline_query(self=None, filters=None, group: int = 0) -> Callable: + def on_inline_query( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling inline queries. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_inline_query(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.InlineQueryHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_message.py b/hydrogram/methods/decorators/on_message.py index 488bb06cd..8f3a19443 100644 --- a/hydrogram/methods/decorators/on_message.py +++ b/hydrogram/methods/decorators/on_message.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnMessage: - def on_message(self=None, filters=None, group: int = 0) -> Callable: + def on_message( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling new messages. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_message(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.MessageHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_poll.py b/hydrogram/methods/decorators/on_poll.py index 438ae2eee..b3f8ac4d3 100644 --- a/hydrogram/methods/decorators/on_poll.py +++ b/hydrogram/methods/decorators/on_poll.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnPoll: - def on_poll(self=None, filters=None, group: int = 0) -> Callable: + def on_poll( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling poll updates. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -39,7 +48,7 @@ def on_poll(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.PollHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/decorators/on_raw_update.py b/hydrogram/methods/decorators/on_raw_update.py index 78f329345..393168a80 100644 --- a/hydrogram/methods/decorators/on_raw_update.py +++ b/hydrogram/methods/decorators/on_raw_update.py @@ -17,13 +17,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram +F = TypeVar("F", bound=Callable) + class OnRawUpdate: - def on_raw_update(self=None, group: int = 0) -> Callable: + def on_raw_update( + self: hydrogram.Client | None = None, # type: ignore + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling raw updates. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the @@ -34,7 +42,7 @@ def on_raw_update(self=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.RawUpdateHandler(func), group) else: diff --git a/hydrogram/methods/decorators/on_user_status.py b/hydrogram/methods/decorators/on_user_status.py index e66881003..46a0412ca 100644 --- a/hydrogram/methods/decorators/on_user_status.py +++ b/hydrogram/methods/decorators/on_user_status.py @@ -17,14 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -from typing import Callable +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar import hydrogram from hydrogram.filters import Filter +F = TypeVar("F", bound=Callable) + class OnUserStatus: - def on_user_status(self=None, filters=None, group: int = 0) -> Callable: + def on_user_status( + self: hydrogram.Client | Filter | None = None, # type: ignore + filters: Filter | None = None, + group: int = 0, + ) -> Callable[[F], F]: """Decorator for handling user status updates. This does the same thing as :meth:`~hydrogram.Client.add_handler` using the :obj:`~hydrogram.handlers.UserStatusHandler`. @@ -37,7 +46,7 @@ def on_user_status(self=None, filters=None, group: int = 0) -> Callable: The group identifier, defaults to 0. """ - def decorator(func: Callable) -> Callable: + def decorator(func: F) -> F: if isinstance(self, hydrogram.Client): self.add_handler(hydrogram.handlers.UserStatusHandler(func, filters), group) elif isinstance(self, Filter) or self is None: diff --git a/hydrogram/methods/messages/copy_media_group.py b/hydrogram/methods/messages/copy_media_group.py index 6a77bc31e..4e40b06c6 100644 --- a/hydrogram/methods/messages/copy_media_group.py +++ b/hydrogram/methods/messages/copy_media_group.py @@ -93,7 +93,9 @@ async def copy_media_group( await app.copy_media_group(to_chat, from_chat, 123, captions="single caption") - await app.copy_media_group(to_chat, from_chat, 123, captions=["caption 1", None, ""]) + await app.copy_media_group( + to_chat, from_chat, 123, captions=["caption 1", None, ""] + ) """ media_group = await self.get_media_group(from_chat_id, message_id) @@ -160,6 +162,7 @@ async def copy_media_group( r.updates, ) ], + topics=r.topics, users=r.users, chats=r.chats, ), diff --git a/hydrogram/methods/messages/copy_message.py b/hydrogram/methods/messages/copy_message.py index 187d3bbf1..4e690ee14 100644 --- a/hydrogram/methods/messages/copy_message.py +++ b/hydrogram/methods/messages/copy_message.py @@ -42,6 +42,7 @@ async def copy_message( message_thread_id: int | None = None, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, disable_notification: bool | None = None, reply_to_message_id: int | None = None, schedule_date: datetime | None = None, @@ -84,6 +85,9 @@ async def copy_message( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the new caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the media. + disable_notification (``bool``, *optional*): Sends the message silently. Users will receive a notification with no sound. @@ -122,6 +126,7 @@ async def copy_message( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, disable_notification=disable_notification, message_thread_id=message_thread_id, reply_to_message_id=reply_to_message_id, diff --git a/hydrogram/methods/messages/download_media.py b/hydrogram/methods/messages/download_media.py index 25df3c853..f0cf751f3 100644 --- a/hydrogram/methods/messages/download_media.py +++ b/hydrogram/methods/messages/download_media.py @@ -19,16 +19,18 @@ from __future__ import annotations -import asyncio import os from datetime import datetime from pathlib import Path -from typing import BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import types from hydrogram.file_id import PHOTO_TYPES, FileId, FileType +if TYPE_CHECKING: + from collections.abc import Callable + DEFAULT_DOWNLOAD_DIR = "downloads/" @@ -182,7 +184,7 @@ async def progress(current, total): else: extension = ".unknown" - file_name = f'{FileType(file_id_obj.file_type).name.lower()}_{(date or datetime.now()).strftime("%Y-%m-%d_%H-%M-%S")}_{self.rnd_id()}{extension}' + file_name = f"{FileType(file_id_obj.file_type).name.lower()}_{(date or datetime.now()).strftime('%Y-%m-%d_%H-%M-%S')}_{self.rnd_id()}{extension}" downloader = self.handle_download(( file_id_obj, @@ -196,5 +198,5 @@ async def progress(current, total): if block: return await downloader - asyncio.get_event_loop().create_task(downloader) + self.loop.create_task(downloader) return None diff --git a/hydrogram/methods/messages/edit_inline_media.py b/hydrogram/methods/messages/edit_inline_media.py index 3e24be1c1..2271e6514 100644 --- a/hydrogram/methods/messages/edit_inline_media.py +++ b/hydrogram/methods/messages/edit_inline_media.py @@ -82,7 +82,7 @@ async def edit_inline_media( is_bytes_io = isinstance(media.media, io.BytesIO) is_uploaded_file = is_bytes_io or Path(media.media).is_file() - is_external_url = not is_uploaded_file and re.match("^https?://", media.media) + is_external_url = not is_uploaded_file and re.match(r"^https?://", media.media) if is_bytes_io and not hasattr(media.media, "name"): media.media.name = "media" diff --git a/hydrogram/methods/messages/edit_inline_reply_markup.py b/hydrogram/methods/messages/edit_inline_reply_markup.py index fd2fd93ba..a46549753 100644 --- a/hydrogram/methods/messages/edit_inline_reply_markup.py +++ b/hydrogram/methods/messages/edit_inline_reply_markup.py @@ -51,7 +51,9 @@ async def edit_inline_reply_markup( # Bots only await app.edit_inline_reply_markup( inline_message_id, - InlineKeyboardMarkup([[InlineKeyboardButton("New button", callback_data="new_data")]]), + InlineKeyboardMarkup([ + [InlineKeyboardButton("New button", callback_data="new_data")] + ]), ) """ diff --git a/hydrogram/methods/messages/edit_inline_text.py b/hydrogram/methods/messages/edit_inline_text.py index d39e87890..38c57b33a 100644 --- a/hydrogram/methods/messages/edit_inline_text.py +++ b/hydrogram/methods/messages/edit_inline_text.py @@ -67,7 +67,9 @@ async def edit_inline_text( await app.edit_inline_text(inline_message_id, "new text") # Take the same text message, remove the web page preview only - await app.edit_inline_text(inline_message_id, message.text, disable_web_page_preview=True) + await app.edit_inline_text( + inline_message_id, message.text, disable_web_page_preview=True + ) """ unpacked = utils.unpack_inline_message_id(inline_message_id) diff --git a/hydrogram/methods/messages/edit_message_media.py b/hydrogram/methods/messages/edit_message_media.py index 3b83baf50..9273dd1f7 100644 --- a/hydrogram/methods/messages/edit_message_media.py +++ b/hydrogram/methods/messages/edit_message_media.py @@ -108,7 +108,7 @@ async def edit_message_media( ), spoiler=media.has_spoiler, ) - elif re.match("^https?://", media.media): + elif re.match(r"^https?://", media.media): media = raw.types.InputMediaPhotoExternal( url=media.media, spoiler=media.has_spoiler ) @@ -148,7 +148,7 @@ async def edit_message_media( ), spoiler=media.has_spoiler, ) - elif re.match("^https?://", media.media): + elif re.match(r"^https?://", media.media): media = raw.types.InputMediaDocumentExternal( url=media.media, spoiler=media.has_spoiler ) @@ -185,7 +185,7 @@ async def edit_message_media( file_reference=media.document.file_reference, ) ) - elif re.match("^https?://", media.media): + elif re.match(r"^https?://", media.media): media = raw.types.InputMediaDocumentExternal(url=media.media) else: media = utils.get_input_media_from_file_id(media.media, FileType.AUDIO) @@ -224,7 +224,7 @@ async def edit_message_media( ), spoiler=media.has_spoiler, ) - elif re.match("^https?://", media.media): + elif re.match(r"^https?://", media.media): media = raw.types.InputMediaDocumentExternal( url=media.media, spoiler=media.has_spoiler ) @@ -256,7 +256,7 @@ async def edit_message_media( file_reference=media.document.file_reference, ) ) - elif re.match("^https?://", media.media): + elif re.match(r"^https?://", media.media): media = raw.types.InputMediaDocumentExternal(url=media.media) else: media = utils.get_input_media_from_file_id(media.media, FileType.DOCUMENT) diff --git a/hydrogram/methods/messages/edit_message_reply_markup.py b/hydrogram/methods/messages/edit_message_reply_markup.py index e0f403dd4..2ee749603 100644 --- a/hydrogram/methods/messages/edit_message_reply_markup.py +++ b/hydrogram/methods/messages/edit_message_reply_markup.py @@ -58,7 +58,9 @@ async def edit_message_reply_markup( await app.edit_message_reply_markup( chat_id, message_id, - InlineKeyboardMarkup([[InlineKeyboardButton("New button", callback_data="new_data")]]), + InlineKeyboardMarkup([ + [InlineKeyboardButton("New button", callback_data="new_data")] + ]), ) """ r = await self.invoke( diff --git a/hydrogram/methods/messages/search_global.py b/hydrogram/methods/messages/search_global.py index 224f93708..db40722a2 100644 --- a/hydrogram/methods/messages/search_global.py +++ b/hydrogram/methods/messages/search_global.py @@ -72,7 +72,9 @@ async def search_global( print(message.text) # Search for recent photos from Global. Get the first 20 results - async for message in app.search_global(filter=enums.MessagesFilter.PHOTO, limit=20): + async for message in app.search_global( + filter=enums.MessagesFilter.PHOTO, limit=20 + ): print(message.photo) """ current = 0 diff --git a/hydrogram/methods/messages/search_messages.py b/hydrogram/methods/messages/search_messages.py index 5ba3d7d95..ee59008f1 100644 --- a/hydrogram/methods/messages/search_messages.py +++ b/hydrogram/methods/messages/search_messages.py @@ -114,7 +114,9 @@ async def search_messages( print(message.text) # Search for pinned messages in chat - async for message in app.search_messages(chat_id, filter=enums.MessagesFilter.PINNED): + async for message in app.search_messages( + chat_id, filter=enums.MessagesFilter.PINNED + ): print(message.text) # Search for messages containing "hello" sent by yourself in chat diff --git a/hydrogram/methods/messages/send_animation.py b/hydrogram/methods/messages/send_animation.py index af0dd5918..acd032d62 100644 --- a/hydrogram/methods/messages/send_animation.py +++ b/hydrogram/methods/messages/send_animation.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, enums, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -43,6 +44,7 @@ async def send_animation( unsave: bool = False, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, has_spoiler: bool | None = None, duration: int = 0, width: int = 0, @@ -59,6 +61,7 @@ async def send_animation( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send animation files (animation or H.264/MPEG-4 AVC video without sound). @@ -95,6 +98,9 @@ async def send_animation( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the animation. + has_spoiler (``bool``, *optional*): Pass True if the animation needs to be covered with a spoiler animation. @@ -145,6 +151,11 @@ async def send_animation( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -156,6 +167,10 @@ async def send_animation( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent animation message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is @@ -181,6 +196,8 @@ async def progress(current, total): await app.send_animation("me", "animation.gif", progress=progress) """ + + utils.validate_random_id(random_id) file = None try: @@ -208,7 +225,7 @@ async def progress(current, total): raw.types.DocumentAttributeAnimated(), ], ) - elif re.match("^https?://", animation): + elif re.match(r"^https?://", animation): media = raw.types.InputMediaDocumentExternal( url=animation, spoiler=has_spoiler ) @@ -246,9 +263,10 @@ async def progress(current, total): media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, + invert_media=show_caption_above_media, reply_markup=await reply_markup.write(self) if reply_markup else None, **await utils.parse_text_entities( self, caption, parse_mode, caption_entities diff --git a/hydrogram/methods/messages/send_audio.py b/hydrogram/methods/messages/send_audio.py index 96c056564..a011dbd44 100644 --- a/hydrogram/methods/messages/send_audio.py +++ b/hydrogram/methods/messages/send_audio.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, enums, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -42,6 +43,7 @@ async def send_audio( message_thread_id: int | None = None, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, duration: int = 0, performer: str | None = None, title: str | None = None, @@ -57,6 +59,7 @@ async def send_audio( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send audio files. @@ -91,6 +94,9 @@ async def send_audio( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the audio. + duration (``int``, *optional*): Duration of the audio in seconds. @@ -138,6 +144,11 @@ async def send_audio( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -149,6 +160,10 @@ async def send_audio( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent audio message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is returned. @@ -163,7 +178,9 @@ async def send_audio( await app.send_audio("me", "audio.mp3", caption="audio caption") # Set audio metadata - await app.send_audio("me", "audio.mp3", title="Title", performer="Performer", duration=234) + await app.send_audio( + "me", "audio.mp3", title="Title", performer="Performer", duration=234 + ) # Keep track of the progress while uploading @@ -173,6 +190,8 @@ async def progress(current, total): await app.send_audio("me", "audio.mp3", progress=progress) """ + + utils.validate_random_id(random_id) file = None try: @@ -195,7 +214,7 @@ async def progress(current, total): ), ], ) - elif re.match("^https?://", audio): + elif re.match(r"^https?://", audio): media = raw.types.InputMediaDocumentExternal(url=audio) else: media = utils.get_input_media_from_file_id(audio, FileType.AUDIO) @@ -224,9 +243,10 @@ async def progress(current, total): media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, + invert_media=show_caption_above_media, reply_markup=await reply_markup.write(self) if reply_markup else None, **await utils.parse_text_entities( self, caption, parse_mode, caption_entities diff --git a/hydrogram/methods/messages/send_cached_media.py b/hydrogram/methods/messages/send_cached_media.py index 92632f251..df4dcbe70 100644 --- a/hydrogram/methods/messages/send_cached_media.py +++ b/hydrogram/methods/messages/send_cached_media.py @@ -38,6 +38,7 @@ async def send_cached_media( message_thread_id: int | None = None, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, disable_notification: bool | None = None, reply_to_message_id: int | None = None, schedule_date: datetime | None = None, @@ -46,6 +47,7 @@ async def send_cached_media( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message | None: """Send any media stored on the Telegram servers using a file_id. @@ -79,6 +81,9 @@ async def send_cached_media( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the media. + disable_notification (``bool``, *optional*): Sends the message silently. Users will receive a notification with no sound. @@ -96,6 +101,15 @@ async def send_cached_media( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent media message is returned. @@ -105,6 +119,8 @@ async def send_cached_media( await app.send_cached_media("me", file_id) """ + utils.validate_random_id(random_id) + reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) r = await self.invoke( @@ -113,9 +129,10 @@ async def send_cached_media( media=utils.get_input_media_from_file_id(file_id), silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, + invert_media=show_caption_above_media, reply_markup=await reply_markup.write(self) if reply_markup else None, **await utils.parse_text_entities(self, caption, parse_mode, caption_entities), ) diff --git a/hydrogram/methods/messages/send_contact.py b/hydrogram/methods/messages/send_contact.py index a5d4e7c7c..9adbd8bd9 100644 --- a/hydrogram/methods/messages/send_contact.py +++ b/hydrogram/methods/messages/send_contact.py @@ -46,6 +46,7 @@ async def send_contact( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message: """Send phone contacts. @@ -90,6 +91,15 @@ async def send_contact( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent contact message is returned. @@ -99,6 +109,8 @@ async def send_contact( await app.send_contact("me", "+1-123-456-7890", "Name") """ + utils.validate_random_id(random_id) + reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) r = await self.invoke( @@ -113,7 +125,7 @@ async def send_contact( message="", silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_dice.py b/hydrogram/methods/messages/send_dice.py index 9cc3db308..bc32d066d 100644 --- a/hydrogram/methods/messages/send_dice.py +++ b/hydrogram/methods/messages/send_dice.py @@ -43,6 +43,7 @@ async def send_dice( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message | None: """Send a dice with a random value from 1 to 6. @@ -82,6 +83,15 @@ async def send_dice( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent dice message is returned. @@ -98,6 +108,8 @@ async def send_dice( await app.send_dice(chat_id, "🏀") """ + utils.validate_random_id(random_id) + reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) r = await self.invoke( @@ -106,7 +118,7 @@ async def send_dice( media=raw.types.InputMediaDice(emoticon=emoji), silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_document.py b/hydrogram/methods/messages/send_document.py index 4ff8ea909..a627a1d1f 100644 --- a/hydrogram/methods/messages/send_document.py +++ b/hydrogram/methods/messages/send_document.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, enums, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -55,6 +56,7 @@ async def send_document( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send generic files. @@ -130,6 +132,11 @@ async def send_document( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -141,6 +148,10 @@ async def send_document( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent document message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is returned. @@ -162,6 +173,8 @@ async def progress(current, total): await app.send_document("me", "document.zip", progress=progress) """ + + utils.validate_random_id(random_id) file = None try: @@ -182,7 +195,7 @@ async def progress(current, total): ) ], ) - elif re.match("^https?://", document): + elif re.match(r"^https?://", document): media = raw.types.InputMediaDocumentExternal(url=document) else: media = utils.get_input_media_from_file_id(document, FileType.DOCUMENT) @@ -211,7 +224,7 @@ async def progress(current, total): media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_location.py b/hydrogram/methods/messages/send_location.py index 9a5c177d6..7317c70f5 100644 --- a/hydrogram/methods/messages/send_location.py +++ b/hydrogram/methods/messages/send_location.py @@ -36,6 +36,7 @@ async def send_location( longitude: float, *, message_thread_id: int | None = None, + horizontal_accuracy: int | None = None, disable_notification: bool | None = None, reply_to_message_id: int | None = None, schedule_date: datetime | None = None, @@ -44,6 +45,7 @@ async def send_location( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message: """Send points on the map. @@ -65,6 +67,9 @@ async def send_location( Unique identifier for the target message thread (topic) of the forum. for forum supergroups only. + horizontal_accuracy (``int``, *optional*): + The estimated horizontal accuracy of the location, in meters. + disable_notification (``bool``, *optional*): Sends the message silently. Users will receive a notification with no sound. @@ -82,6 +87,15 @@ async def send_location( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent location message is returned. @@ -91,18 +105,22 @@ async def send_location( app.send_location("me", latitude, longitude) """ + utils.validate_random_id(random_id) + reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) r = await self.invoke( raw.functions.messages.SendMedia( peer=await self.resolve_peer(chat_id), media=raw.types.InputMediaGeoPoint( - geo_point=raw.types.InputGeoPoint(lat=latitude, long=longitude) + geo_point=raw.types.InputGeoPoint( + lat=latitude, long=longitude, accuracy_radius=horizontal_accuracy + ) ), message="", silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_media_group.py b/hydrogram/methods/messages/send_media_group.py index f541e6a45..f7787c49b 100644 --- a/hydrogram/methods/messages/send_media_group.py +++ b/hydrogram/methods/messages/send_media_group.py @@ -123,7 +123,7 @@ async def send_media_group( ), spoiler=i.has_spoiler, ) - elif re.match("^https?://", i.media): + elif re.match(r"^https?://", i.media): media = await self.invoke( raw.functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), @@ -173,6 +173,7 @@ async def send_media_group( thumb=await self.save_file(i.thumb), spoiler=i.has_spoiler, mime_type=self.guess_mime_type(i.media) or "video/mp4", + nosound_video=True, attributes=[ raw.types.DocumentAttributeVideo( supports_streaming=i.supports_streaming or None, @@ -196,7 +197,7 @@ async def send_media_group( ), spoiler=i.has_spoiler, ) - elif re.match("^https?://", i.media): + elif re.match(r"^https?://", i.media): media = await self.invoke( raw.functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), @@ -228,6 +229,7 @@ async def send_media_group( getattr(i.media, "name", "video.mp4") ) or "video/mp4", + nosound_video=True, attributes=[ raw.types.DocumentAttributeVideo( supports_streaming=i.supports_streaming or None, @@ -282,7 +284,7 @@ async def send_media_group( file_reference=media.document.file_reference, ) ) - elif re.match("^https?://", i.media): + elif re.match(r"^https?://", i.media): media = await self.invoke( raw.functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), @@ -357,7 +359,7 @@ async def send_media_group( file_reference=media.document.file_reference, ) ) - elif re.match("^https?://", i.media): + elif re.match(r"^https?://", i.media): media = await self.invoke( raw.functions.messages.UploadMedia( peer=await self.resolve_peer(chat_id), @@ -445,6 +447,7 @@ async def send_media_group( r.updates, ) ], + topics=r.topics, users=r.users, chats=r.chats, ), diff --git a/hydrogram/methods/messages/send_message.py b/hydrogram/methods/messages/send_message.py index 868366eea..37554411d 100644 --- a/hydrogram/methods/messages/send_message.py +++ b/hydrogram/methods/messages/send_message.py @@ -46,6 +46,7 @@ async def send_message( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message: """Send text messages. @@ -91,6 +92,15 @@ async def send_message( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent text message is returned. @@ -101,7 +111,9 @@ async def send_message( await app.send_message("me", "Message sent with **Hydrogram**!") # Disable web page previews - await app.send_message("me", "https://docs.hydrogram.org", disable_web_page_preview=True) + await app.send_message( + "me", "https://docs.hydrogram.org", disable_web_page_preview=True + ) # Reply to a message using its id await app.send_message("me", "this is a reply", reply_to_message_id=123) @@ -110,7 +122,11 @@ async def send_message( # For bots only, send messages with keyboards attached - from hydrogram.types import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton + from hydrogram.types import ( + ReplyKeyboardMarkup, + InlineKeyboardMarkup, + InlineKeyboardButton, + ) # Send a normal keyboard await app.send_message( @@ -128,6 +144,8 @@ async def send_message( ) """ + utils.validate_random_id(random_id) + message, entities = ( await utils.parse_text_entities(self, text, parse_mode, entities) ).values() @@ -140,7 +158,7 @@ async def send_message( no_webpage=disable_web_page_preview or None, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), reply_markup=await reply_markup.write(self) if reply_markup else None, message=message, diff --git a/hydrogram/methods/messages/send_photo.py b/hydrogram/methods/messages/send_photo.py index 7b6585ff6..11cffb1e6 100644 --- a/hydrogram/methods/messages/send_photo.py +++ b/hydrogram/methods/messages/send_photo.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import enums, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -42,6 +43,7 @@ async def send_photo( message_thread_id: int | None = None, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, has_spoiler: bool | None = None, ttl_seconds: int | None = None, disable_notification: bool | None = None, @@ -54,6 +56,7 @@ async def send_photo( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send photos. @@ -86,6 +89,9 @@ async def send_photo( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the photo. + has_spoiler (``bool``, *optional*): Pass True if the photo needs to be covered with a spoiler animation. @@ -122,6 +128,11 @@ async def send_photo( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -133,6 +144,10 @@ async def send_photo( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent photo message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is returned. @@ -152,6 +167,8 @@ async def send_photo( # Send self-destructing photo await app.send_photo("me", "photo.jpg", ttl_seconds=10) """ + + utils.validate_random_id(random_id) file = None try: @@ -165,7 +182,7 @@ async def send_photo( elif ( isinstance(photo, str) and not Path(photo).is_file() - and re.match("^https?://", photo) + and re.match(r"^https?://", photo) ): media = raw.types.InputMediaPhotoExternal( url=photo, ttl_seconds=ttl_seconds, spoiler=has_spoiler @@ -185,9 +202,10 @@ async def send_photo( media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, + invert_media=show_caption_above_media, reply_markup=await reply_markup.write(self) if reply_markup else None, **await utils.parse_text_entities( self, caption, parse_mode, caption_entities diff --git a/hydrogram/methods/messages/send_poll.py b/hydrogram/methods/messages/send_poll.py index 9d57ec122..f1c25834e 100644 --- a/hydrogram/methods/messages/send_poll.py +++ b/hydrogram/methods/messages/send_poll.py @@ -33,8 +33,10 @@ async def send_poll( self: hydrogram.Client, chat_id: int | str, question: str, - options: list[str], + options: list[types.InputPollOption], *, + question_parse_mode: enums.ParseMode = None, + question_entities: list[types.MessageEntity] | None = None, message_thread_id: int | None = None, is_anonymous: bool = True, type: enums.PollType = enums.PollType.REGULAR, @@ -54,6 +56,7 @@ async def send_poll( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message: """Send a new poll. @@ -68,8 +71,15 @@ async def send_poll( question (``str``): Poll question, 1-255 characters. - options (List of ``str``): - List of answer options, 2-10 strings 1-100 characters each. + options (List of :obj:`~hydrogram.types.InputPollOption`): + List of answer options, 2-10 answer options, 1-100 characters for each option. + + question_parse_mode (:obj:`~hydrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + question_entities (List of :obj:`~hydrogram.types.MessageEntity`): + List of special entities that appear in the poll question, which can be specified instead of *question_parse_mode*. message_thread_id (``int``, *optional*): Unique identifier for the target message thread (topic) of the forum. @@ -132,6 +142,15 @@ async def send_poll( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent poll message is returned. @@ -141,6 +160,8 @@ async def send_poll( await app.send_poll(chat_id, "Is this a poll question?", ["Yes", "No", "Maybe"]) """ + utils.validate_random_id(random_id) + solution, solution_entities = ( ( await utils.parse_text_entities( @@ -153,17 +174,41 @@ async def send_poll( reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) + question, question_entities = ( + await utils.parse_text_entities(self, question, question_parse_mode, question_entities) + ).values() + if not question_entities: + question_entities = [] + + answers = [] + for answer_ in options: + if isinstance(answer_, str): + answer, answer_entities = answer_, [] + else: + answer, answer_entities = ( + await utils.parse_text_entities( + self, answer_.text, answer_.text_parse_mode, answer_.text_entities + ) + ).values() + if not answer_entities: + answer_entities = [] + answers.append( + raw.types.InputPollAnswer( + text=raw.types.TextWithEntities(text=answer, entities=answer_entities), + ) + ) + r = await self.invoke( raw.functions.messages.SendMedia( peer=await self.resolve_peer(chat_id), media=raw.types.InputMediaPoll( poll=raw.types.Poll( id=self.rnd_id(), - question=question, - answers=[ - raw.types.PollAnswer(text=text, option=bytes([i])) - for i, text in enumerate(options) - ], + hash=0, + question=raw.types.TextWithEntities( + text=question, entities=question_entities + ), + answers=answers, closed=is_closed, public_voters=not is_anonymous, multiple_choice=allows_multiple_answers, @@ -171,16 +216,14 @@ async def send_poll( close_period=open_period, close_date=utils.datetime_to_timestamp(close_date), ), - correct_answers=[bytes([correct_option_id])] - if correct_option_id is not None - else None, + correct_answers=[correct_option_id] if correct_option_id is not None else None, solution=solution, solution_entities=None if solution is None else (solution_entities or []), ), message="", silent=disable_notification, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_sticker.py b/hydrogram/methods/messages/send_sticker.py index f3de9d2f4..de0187b08 100644 --- a/hydrogram/methods/messages/send_sticker.py +++ b/hydrogram/methods/messages/send_sticker.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -49,6 +50,7 @@ async def send_sticker( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send static .webp or animated .tgs stickers. @@ -99,6 +101,11 @@ async def send_sticker( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -110,6 +117,10 @@ async def send_sticker( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent sticker message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is @@ -124,6 +135,8 @@ async def send_sticker( # Send sticker using file_id await app.send_sticker("me", file_id) """ + + utils.validate_random_id(random_id) file = None try: @@ -139,7 +152,7 @@ async def send_sticker( raw.types.DocumentAttributeFilename(file_name=Path(sticker).name) ], ) - elif re.match("^https?://", sticker): + elif re.match(r"^https?://", sticker): media = raw.types.InputMediaDocumentExternal(url=sticker) else: media = utils.get_input_media_from_file_id(sticker, FileType.STICKER) @@ -163,7 +176,7 @@ async def send_sticker( media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_venue.py b/hydrogram/methods/messages/send_venue.py index 5bd37881a..09746d8d5 100644 --- a/hydrogram/methods/messages/send_venue.py +++ b/hydrogram/methods/messages/send_venue.py @@ -48,6 +48,7 @@ async def send_venue( | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove | types.ForceReply = None, + random_id: int | None = None, ) -> types.Message: """Send information about a venue. @@ -99,6 +100,15 @@ async def send_venue( Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message`: On success, the sent venue message is returned. @@ -108,6 +118,8 @@ async def send_venue( app.send_venue("me", latitude, longitude, "Venue title", "Venue address") """ + utils.validate_random_id(random_id) + reply_to = utils.get_reply_head_fm(message_thread_id, reply_to_message_id) r = await self.invoke( @@ -124,7 +136,7 @@ async def send_venue( message="", silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_video.py b/hydrogram/methods/messages/send_video.py index e115e72bc..f3d0b9435 100644 --- a/hydrogram/methods/messages/send_video.py +++ b/hydrogram/methods/messages/send_video.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, enums, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -42,7 +43,9 @@ async def send_video( message_thread_id: int | None = None, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, has_spoiler: bool | None = None, + no_sound: bool | None = None, ttl_seconds: int | None = None, duration: int = 0, width: int = 0, @@ -60,6 +63,7 @@ async def send_video( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send video files. @@ -92,9 +96,16 @@ async def send_video( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the video. + has_spoiler (``bool``, *optional*): Pass True if the video needs to be covered with a spoiler animation. + no_sound (``bool``, *optional*): + Pass True if the video you are uploading is a video message with no sound. + Does not work for external links. + ttl_seconds (``int``, *optional*): Self-Destruct Timer. If you set a timer, the video will self-destruct in *ttl_seconds* @@ -151,6 +162,11 @@ async def send_video( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -162,6 +178,10 @@ async def send_video( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent video message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is returned. @@ -186,6 +206,8 @@ async def progress(current, total): await app.send_video("me", "video.mp4", progress=progress) """ + + utils.validate_random_id(random_id) file = None try: @@ -201,6 +223,7 @@ async def progress(current, total): ttl_seconds=ttl_seconds, spoiler=has_spoiler, thumb=thumb, + nosound_video=no_sound, attributes=[ raw.types.DocumentAttributeVideo( supports_streaming=supports_streaming or None, @@ -213,7 +236,7 @@ async def progress(current, total): ), ], ) - elif re.match("^https?://", video): + elif re.match(r"^https?://", video): media = raw.types.InputMediaDocumentExternal( url=video, ttl_seconds=ttl_seconds, spoiler=has_spoiler ) @@ -230,6 +253,7 @@ async def progress(current, total): ttl_seconds=ttl_seconds, spoiler=has_spoiler, thumb=thumb, + nosound_video=no_sound, attributes=[ raw.types.DocumentAttributeVideo( supports_streaming=supports_streaming or None, @@ -251,9 +275,10 @@ async def progress(current, total): media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, + invert_media=show_caption_above_media, reply_markup=await reply_markup.write(self) if reply_markup else None, **await utils.parse_text_entities( self, caption, parse_mode, caption_entities diff --git a/hydrogram/methods/messages/send_video_note.py b/hydrogram/methods/messages/send_video_note.py index 6ba665b4d..990c58791 100644 --- a/hydrogram/methods/messages/send_video_note.py +++ b/hydrogram/methods/messages/send_video_note.py @@ -20,7 +20,7 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, raw, types, utils @@ -28,6 +28,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -51,6 +52,7 @@ async def send_video_note( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send video messages. @@ -113,6 +115,11 @@ async def send_video_note( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -124,6 +131,10 @@ async def send_video_note( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent video note message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is @@ -138,6 +149,8 @@ async def send_video_note( # Set video note length await app.send_video_note("me", "video_note.mp4", length=25) """ + + utils.validate_random_id(random_id) file = None try: @@ -188,7 +201,7 @@ async def send_video_note( media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/send_voice.py b/hydrogram/methods/messages/send_voice.py index 5bae2c1e3..00119a931 100644 --- a/hydrogram/methods/messages/send_voice.py +++ b/hydrogram/methods/messages/send_voice.py @@ -21,7 +21,7 @@ import re from pathlib import Path -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import StopTransmission, enums, raw, types, utils @@ -29,6 +29,7 @@ from hydrogram.file_id import FileType if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime @@ -53,6 +54,7 @@ async def send_voice( | types.ForceReply = None, progress: Callable | None = None, progress_args: tuple = (), + random_id: int | None = None, ) -> types.Message | None: """Send audio files. @@ -116,6 +118,11 @@ async def send_voice( You can pass anything you need to be available in the progress callback scope; for example, a Message object or a Client instance in order to edit the message with the updated progress status. + random_id (``int``, *optional*): + Client message ID for retrying the same send. Must be a signed 64-bit integer + (booleans are not accepted). Defaults to an automatically generated ID. + See :doc:`/topics/message-idempotency` for retry scope and limitations. + Other Parameters: current (``int``): The amount of bytes transmitted so far. @@ -127,6 +134,10 @@ async def send_voice( Extra custom arguments as defined in the ``progress_args`` parameter. You can either keep ``*args`` or add every single extra argument in your function signature. + Raises: + TypeError: If ``random_id`` is neither an integer nor ``None``, or is a boolean. + ValueError: If ``random_id`` is outside the signed 64-bit range. + Returns: :obj:`~hydrogram.types.Message` | ``None``: On success, the sent voice message is returned, otherwise, in case the upload is deliberately stopped with :meth:`~hydrogram.Client.stop_transmission`, None is returned. @@ -143,6 +154,8 @@ async def send_voice( # Set voice note duration await app.send_voice("me", "voice.ogg", duration=20) """ + + utils.validate_random_id(random_id) file = None try: @@ -158,7 +171,7 @@ async def send_voice( raw.types.DocumentAttributeAudio(voice=True, duration=duration) ], ) - elif re.match("^https?://", voice): + elif re.match(r"^https?://", voice): media = raw.types.InputMediaDocumentExternal(url=voice) else: media = utils.get_input_media_from_file_id(voice, FileType.VOICE) @@ -180,7 +193,7 @@ async def send_voice( media=media, silent=disable_notification or None, reply_to=reply_to, - random_id=self.rnd_id(), + random_id=self.rnd_id() if random_id is None else random_id, schedule_date=utils.datetime_to_timestamp(schedule_date), noforwards=protect_content, reply_markup=await reply_markup.write(self) if reply_markup else None, diff --git a/hydrogram/methods/messages/stop_poll.py b/hydrogram/methods/messages/stop_poll.py index 3500b6f20..fc1c31451 100644 --- a/hydrogram/methods/messages/stop_poll.py +++ b/hydrogram/methods/messages/stop_poll.py @@ -63,7 +63,13 @@ async def stop_poll( peer=await self.resolve_peer(chat_id), id=message_id, media=raw.types.InputMediaPoll( - poll=raw.types.Poll(id=int(poll.id), closed=True, question="", answers=[]) + poll=raw.types.Poll( + id=int(poll.id), + hash=0, + closed=True, + question=raw.types.TextWithEntities(text="", entities=[]), + answers=[], + ) ), reply_markup=await reply_markup.write(self) if reply_markup else None, ) diff --git a/hydrogram/methods/messages/stream_media.py b/hydrogram/methods/messages/stream_media.py index 5f5617a6c..b0f391ff1 100644 --- a/hydrogram/methods/messages/stream_media.py +++ b/hydrogram/methods/messages/stream_media.py @@ -20,6 +20,7 @@ from __future__ import annotations import math +from contextlib import aclosing from typing import BinaryIO import hydrogram @@ -113,5 +114,6 @@ async def stream_media( chunks = math.ceil(file_size / 1024 / 1024) offset += chunks - async for chunk in self.get_file(file_id_obj, file_size, limit, offset): - yield chunk + async with aclosing(self.get_file(file_id_obj, file_size, limit, offset)) as chunks: + async for chunk in chunks: + yield chunk diff --git a/hydrogram/methods/password/change_cloud_password.py b/hydrogram/methods/password/change_cloud_password.py index d8517e55d..41eac867c 100644 --- a/hydrogram/methods/password/change_cloud_password.py +++ b/hydrogram/methods/password/change_cloud_password.py @@ -58,7 +58,9 @@ async def change_cloud_password( await app.change_cloud_password("current_password", "new_password") # Change password and hint - await app.change_cloud_password("current_password", "new_password", new_hint="hint") + await app.change_cloud_password( + "current_password", "new_password", new_hint="hint" + ) """ r = await self.invoke(raw.functions.account.GetPassword()) diff --git a/hydrogram/methods/phone/__init__.py b/hydrogram/methods/phone/__init__.py new file mode 100644 index 000000000..c0f395712 --- /dev/null +++ b/hydrogram/methods/phone/__init__.py @@ -0,0 +1,25 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from .create_video_chat import CreateVideoChat +from .discard_group_call import DiscardGroupCall +from .invite_group_call_members import InviteGroupCallMembers + + +class Phone(CreateVideoChat, DiscardGroupCall, InviteGroupCallMembers): + pass diff --git a/hydrogram/methods/phone/create_video_chat.py b/hydrogram/methods/phone/create_video_chat.py new file mode 100644 index 000000000..8e1a949ef --- /dev/null +++ b/hydrogram/methods/phone/create_video_chat.py @@ -0,0 +1,97 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import hydrogram +from hydrogram import raw, types, utils + +if TYPE_CHECKING: + from datetime import datetime + + +class CreateVideoChat: + async def create_video_chat( + self: hydrogram.Client, + chat_id: int | str, + title: str | None = None, + start_date: datetime = utils.zero_datetime(), + is_rtmp_stream: bool | None = None, + ) -> types.Message: + """Creates a video chat (a group call bound to a chat). + + Available only for basic groups, supergroups and channels; requires can_manage_video_chats administrator right. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat in which the video chat will be created. A chat can be either a basic group, supergroup or a channel. + + title (``str``, *optional*): + Group call title; if empty, chat title will be used. + + start_date (:py:obj:`~datetime.datetime`, *optional*): + Point in time (Unix timestamp) when the group call is supposed to be started by an administrator; 0 to start the video chat immediately. The date must be at least 10 seconds and at most 8 days in the future. + + is_rtmp_stream (``bool``, *optional*): + Pass true to create an RTMP stream instead of an ordinary video chat; requires owner privileges. + + Returns: + :obj:`~hydrogram.types.Message`: On success, the sent service message is returned. + + Example: + .. code-block:: python + + await app.create_video_chat(chat_id) + + """ + peer = await self.resolve_peer(chat_id) + + if not isinstance(peer, (raw.types.InputPeerChat, raw.types.InputPeerChannel)): + raise ValueError("Target chat should be group, supergroup or channel.") + + r = await self.invoke( + raw.functions.phone.CreateGroupCall( + rtmp_stream=is_rtmp_stream, + peer=peer, + random_id=self.rnd_id() >> 32, + title=title, + schedule_date=utils.datetime_to_timestamp(start_date), + ) + ) + + for i in r.updates: + if isinstance( + i, + ( + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewMessage, + raw.types.UpdateNewScheduledMessage, + ), + ): + return await types.Message._parse( + client=self, + message=i.message, + users={i.id: i for i in r.users}, + chats={i.id: i for i in r.chats}, + is_scheduled=isinstance(i, raw.types.UpdateNewScheduledMessage), + ) + return None diff --git a/hydrogram/methods/phone/discard_group_call.py b/hydrogram/methods/phone/discard_group_call.py new file mode 100644 index 000000000..733077e5e --- /dev/null +++ b/hydrogram/methods/phone/discard_group_call.py @@ -0,0 +1,79 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import hydrogram +from hydrogram import raw, types + + +class DiscardGroupCall: + async def discard_group_call( + self: hydrogram.Client, + chat_id: int | str, + ) -> types.Message: + """Terminate a group/channel call or livestream + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. A chat can be either a basic group, supergroup or a channel. + + Returns: + :obj:`~hydrogram.types.Message`: On success, the sent service message is returned. + + Example: + .. code-block:: python + + await app.discard_group_call(chat_id) + + """ + peer = await self.resolve_peer(chat_id) + + if isinstance(peer, raw.types.InputPeerChannel): + r = await self.invoke(raw.functions.channels.GetFullChannel(channel=peer)) + elif isinstance(peer, raw.types.InputPeerChat): + r = await self.invoke(raw.functions.messages.GetFullChat(chat_id=peer.chat_id)) + else: + raise ValueError("Target chat should be group, supergroup or channel.") + + call = r.full_chat.call + + if call is None: + raise ValueError("No active group call at this chat.") + + r = await self.invoke(raw.functions.phone.DiscardGroupCall(call=call)) + + for i in r.updates: + if isinstance( + i, + ( + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewMessage, + raw.types.UpdateNewScheduledMessage, + ), + ): + return await types.Message._parse( + client=self, + message=i.message, + users={i.id: i for i in r.users}, + chats={i.id: i for i in r.chats}, + is_scheduled=isinstance(i, raw.types.UpdateNewScheduledMessage), + ) + return None diff --git a/hydrogram/methods/phone/invite_group_call_members.py b/hydrogram/methods/phone/invite_group_call_members.py new file mode 100644 index 000000000..5096b938f --- /dev/null +++ b/hydrogram/methods/phone/invite_group_call_members.py @@ -0,0 +1,91 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import hydrogram +from hydrogram import raw, types + + +class InviteGroupCallMembers: + async def invite_group_call_members( + self: hydrogram.Client, + chat_id: int | str, + user_ids: int | str | list[int | str], + ) -> types.Message: + """Invites users to an active group call. Sends a service message of type :obj:`~pyrogram.enums.MessageServiceType.VIDEO_CHAT_PARTICIPANTS_INVITED` for video chats. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. A chat can be either a basic group or a supergroup. + + user_ids (``int`` | ``str`` | List of ``int`` or ``str``): + Users identifiers to invite to group call in the chat. + You can pass an ID (int) or username (str). + At most 10 users can be invited simultaneously. + + Returns: + :obj:`~pyrogram.types.Message`: On success, the sent service message is returned. + + Example: + .. code-block:: python + + await app.invite_group_call_members(chat_id, user_id) + + """ + peer = await self.resolve_peer(chat_id) + + if isinstance(peer, raw.types.InputPeerChannel): + r = await self.invoke(raw.functions.channels.GetFullChannel(channel=peer)) + elif isinstance(peer, raw.types.InputPeerChat): + r = await self.invoke(raw.functions.messages.GetFullChat(chat_id=peer.chat_id)) + else: + raise ValueError("Target chat should be group, supergroup or channel.") + + call = r.full_chat.call + + if call is None: + raise ValueError("No active group call at this chat.") + + user_ids = [user_ids] if not isinstance(user_ids, list) else user_ids + + r = await self.invoke( + raw.functions.phone.InviteToGroupCall( + call=call, users=[await self.resolve_peer(i) for i in user_ids] + ) + ) + + for i in r.updates: + if isinstance( + i, + ( + raw.types.UpdateNewChannelMessage, + raw.types.UpdateNewMessage, + raw.types.UpdateNewScheduledMessage, + ), + ): + return await types.Message._parse( + client=self, + message=i.message, + users={i.id: i for i in r.users}, + chats={i.id: i for i in r.chats}, + is_scheduled=isinstance(i, raw.types.UpdateNewScheduledMessage), + ) + return None diff --git a/hydrogram/methods/pyromod/listen.py b/hydrogram/methods/pyromod/listen.py index fa9d2e831..c47bdad7b 100644 --- a/hydrogram/methods/pyromod/listen.py +++ b/hydrogram/methods/pyromod/listen.py @@ -84,8 +84,7 @@ async def listen( inline_message_id=inline_message_id, ) - loop = asyncio.get_event_loop() - future = loop.create_future() + future = self.loop.create_future() listener = Listener( future=future, diff --git a/hydrogram/methods/pyromod/register_next_step_handler.py b/hydrogram/methods/pyromod/register_next_step_handler.py index a54d44b60..156bcc550 100644 --- a/hydrogram/methods/pyromod/register_next_step_handler.py +++ b/hydrogram/methods/pyromod/register_next_step_handler.py @@ -19,11 +19,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING from hydrogram.types import Identifier, Listener, ListenerTypes if TYPE_CHECKING: + from collections.abc import Callable + import hydrogram from hydrogram.filters import Filter diff --git a/hydrogram/methods/utilities/idle.py b/hydrogram/methods/utilities/idle.py index 451437e96..4b718987a 100644 --- a/hydrogram/methods/utilities/idle.py +++ b/hydrogram/methods/utilities/idle.py @@ -70,7 +70,7 @@ async def main(): def signal_handler(signum, __): logging.info("Stop signal received (%s). Exiting...", signals[signum]) - asyncio.get_event_loop().run_in_executor(None, task.cancel) + asyncio.get_running_loop().run_in_executor(None, task.cancel) for s in (SIGINT, SIGTERM, SIGABRT): signal_fn(s, signal_handler) diff --git a/hydrogram/methods/utilities/remove_error_handler.py b/hydrogram/methods/utilities/remove_error_handler.py index e70ee17d1..ae92ba9b7 100644 --- a/hydrogram/methods/utilities/remove_error_handler.py +++ b/hydrogram/methods/utilities/remove_error_handler.py @@ -28,14 +28,19 @@ class RemoveErrorHandler: def remove_error_handler( - self: hydrogram.Client, error: type[Exception] | Iterable[type[Exception]] = Exception + self: hydrogram.Client, + exception: type[Exception] | Iterable[type[Exception]] = Exception, ): - """Remove a previously-registered error handler. (using exception classes) + """Remove a previously registered error handler using exception classes. Parameters: - error (``Exception`` | Iterable of ``Exception``, *optional*): - The error(s) for handlers to be removed. + exception (``Exception`` | Iterable of ``Exception``, *optional*): + The error(s) for handlers to be removed. Defaults to Exception. """ - for handler in self.dispatcher.error_handlers: - if handler.check_remove(error): - self.dispatcher.error_handlers.remove(handler) + to_remove = [ + handler + for handler in self.dispatcher.error_handlers + if handler.check_remove(exception) + ] + for handler in to_remove: + self.dispatcher.error_handlers.remove(handler) diff --git a/hydrogram/methods/utilities/run.py b/hydrogram/methods/utilities/run.py index 25e0e88f3..5f39950d8 100644 --- a/hydrogram/methods/utilities/run.py +++ b/hydrogram/methods/utilities/run.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -import asyncio from typing import TYPE_CHECKING from hydrogram.methods.utilities.idle import idle @@ -70,13 +69,12 @@ async def main(): app.run(main()) """ - loop = asyncio.get_event_loop() - run = loop.run_until_complete + run = self.loop.run_until_complete if coroutine is not None: run(coroutine) else: - if loop.is_running(): + if self.loop.is_running(): raise RuntimeError( "You must call client.run() method outside of an asyncio event loop. " "Otherwise, you can use client.start(), client.idle(), and client.stop() " diff --git a/hydrogram/methods/utilities/start.py b/hydrogram/methods/utilities/start.py index 3d1e1707e..51e331133 100644 --- a/hydrogram/methods/utilities/start.py +++ b/hydrogram/methods/utilities/start.py @@ -66,12 +66,19 @@ async def main(): ).id log.info("Takeout session %s initiated", self.takeout_id) - await self.invoke(raw.functions.updates.GetState()) + if self.update_manager is None: + await self.invoke(raw.functions.updates.GetState()) except (Exception, KeyboardInterrupt): await self.disconnect() raise else: self.me = await self.get_me() - await self.initialize() + try: + await self.initialize() + except BaseException: + if self.update_manager is not None: + await self.update_manager.stop() + await self.disconnect() + raise return self diff --git a/hydrogram/methods/utilities/stop.py b/hydrogram/methods/utilities/stop.py index ee479c4d9..2c681281b 100644 --- a/hydrogram/methods/utilities/stop.py +++ b/hydrogram/methods/utilities/stop.py @@ -59,6 +59,14 @@ async def main(): """ async def do_it(): + # Do not make worker shutdown wait for an in-flight reconnect budget. + # Pending RPCs must be released before waiting on their handlers. + if ( + self.session is not None + and self.session.recovery_task is not None + and not self.session.recovery_task.done() + ): + await self.session.stop() await self.terminate() await self.disconnect() diff --git a/hydrogram/methods/utilities/stop_transmission.py b/hydrogram/methods/utilities/stop_transmission.py index ca94aa8b8..3cbeb1db8 100644 --- a/hydrogram/methods/utilities/stop_transmission.py +++ b/hydrogram/methods/utilities/stop_transmission.py @@ -38,6 +38,8 @@ async def progress(current, total, client): async with app: - await app.send_document("me", "file.zip", progress=progress, progress_args=(app,)) + await app.send_document( + "me", "file.zip", progress=progress, progress_args=(app,) + ) """ raise hydrogram.StopTransmission diff --git a/hydrogram/parser/utils.py b/hydrogram/parser/utils.py index ca849ef68..5afe46e11 100644 --- a/hydrogram/parser/utils.py +++ b/hydrogram/parser/utils.py @@ -27,8 +27,9 @@ def add_surrogates(text): # Replace each SMP code point with a surrogate pair return SMP_RE.sub( - lambda match: # Split SMP in two surrogates - "".join(chr(i) for i in unpack(" "GzipPacked": # Return the Object itself instead of a GzipPacked wrapping it - return cast(GzipPacked, TLObject.read(BytesIO(decompress(Bytes.read(data))))) + return cast("GzipPacked", TLObject.read(BytesIO(decompress(Bytes.read(data))))) def write(self, *args: Any) -> bytes: b = BytesIO() diff --git a/hydrogram/raw/core/primitives/double.py b/hydrogram/raw/core/primitives/double.py index d52a20895..03fd45d98 100644 --- a/hydrogram/raw/core/primitives/double.py +++ b/hydrogram/raw/core/primitives/double.py @@ -27,7 +27,7 @@ class Double(bytes, TLObject): @classmethod def read(cls, data: BytesIO, *args: Any) -> float: - return cast(float, unpack("d", data.read(8))[0]) + return cast("float", unpack("d", data.read(8))[0]) def __new__(cls, value: float) -> bytes: # type: ignore return pack("d", value) diff --git a/hydrogram/raw/core/primitives/string.py b/hydrogram/raw/core/primitives/string.py index 2030038e8..7f6a4c61f 100644 --- a/hydrogram/raw/core/primitives/string.py +++ b/hydrogram/raw/core/primitives/string.py @@ -26,7 +26,7 @@ class String(Bytes): @classmethod def read(cls, data: BytesIO, *args) -> str: # type: ignore - return cast(bytes, super(String, String).read(data)).decode(errors="replace") + return cast("bytes", super(String, String).read(data)).decode(errors="replace") def __new__(cls, value: str) -> bytes: # type: ignore return super().__new__(cls, value.encode()) diff --git a/hydrogram/raw/core/primitives/vector.py b/hydrogram/raw/core/primitives/vector.py index f80a5214c..0d93ce56f 100644 --- a/hydrogram/raw/core/primitives/vector.py +++ b/hydrogram/raw/core/primitives/vector.py @@ -59,5 +59,5 @@ def read(cls, data: BytesIO, t: Any = None, *args: Any) -> List: def __new__(cls, value: list, t: Any = None) -> bytes: # type: ignore return b"".join( [Int(cls.ID, False), Int(len(value))] - + [cast(bytes, t(i)) if t else i.write() for i in value] + + [cast("bytes", t(i)) if t else i.write() for i in value] ) diff --git a/hydrogram/raw/core/tl_object.py b/hydrogram/raw/core/tl_object.py index dbe391bca..ead323ba2 100644 --- a/hydrogram/raw/core/tl_object.py +++ b/hydrogram/raw/core/tl_object.py @@ -35,7 +35,7 @@ class TLObject: @classmethod def read(cls, b: BytesIO, *args: Any) -> Any: - return cast(TLObject, objects[int.from_bytes(b.read(4), "little")]).read(b, *args) + return cast("TLObject", objects[int.from_bytes(b.read(4), "little")]).read(b, *args) def write(self, *args: Any) -> bytes: pass @@ -59,7 +59,7 @@ def __str__(self) -> str: def __repr__(self) -> str: return ( - f'hydrogram.raw.{self.QUALNAME}({", ".join(f"{attr}={getattr(self, attr)!r}" for attr in self.__slots__ if getattr(self, attr) is not None)})' + f"hydrogram.raw.{self.QUALNAME}({', '.join(f'{attr}={getattr(self, attr)!r}' for attr in self.__slots__ if getattr(self, attr) is not None)})" if hasattr(self, "QUALNAME") else repr(self) ) diff --git a/hydrogram/session/auth.py b/hydrogram/session/auth.py index 3defbf693..f1b8d4114 100644 --- a/hydrogram/session/auth.py +++ b/hydrogram/session/auth.py @@ -17,35 +17,42 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +from __future__ import annotations + import asyncio import logging import time from hashlib import sha1 from io import BytesIO from os import urandom +from typing import TYPE_CHECKING import hydrogram from hydrogram import raw -from hydrogram.connection import Connection from hydrogram.crypto import aes, prime, rsa from hydrogram.errors import SecurityCheckMismatch from hydrogram.raw.core import Int, Long, TLObject from .internals import MsgId +if TYPE_CHECKING: + from hydrogram.connection import Connection + log = logging.getLogger(__name__) class Auth: MAX_RETRIES = 5 - def __init__(self, client: "hydrogram.Client", dc_id: int, test_mode: bool): + def __init__(self, client: hydrogram.Client, dc_id: int, test_mode: bool): self.dc_id = dc_id self.test_mode = test_mode self.ipv6 = client.ipv6 self.proxy = client.proxy + self.connection_factory = client.connection_factory + self.protocol_factory = client.protocol_factory - self.connection = None + self.connection: Connection | None = None @staticmethod def pack(data: TLObject) -> bytes: @@ -59,7 +66,8 @@ def unpack(b: BytesIO): async def invoke(self, data: TLObject): data = self.pack(data) await self.connection.send(data) - response = BytesIO(await self.connection.recv()) + # Authentication runs before the session's ping monitor is available. + response = BytesIO(await asyncio.wait_for(self.connection.recv(), 15)) return self.unpack(response) @@ -73,7 +81,14 @@ async def create(self): # The server may close the connection at any time, causing the auth key creation to fail. # If that happens, just try again up to MAX_RETRIES times. while True: - self.connection = Connection(self.dc_id, self.test_mode, self.ipv6, self.proxy) + self.connection = self.connection_factory( + dc_id=self.dc_id, + test_mode=self.test_mode, + ipv6=self.ipv6, + proxy=self.proxy, + media=False, + protocol_factory=self.protocol_factory, + ) try: log.info("Start creating a new auth key on DC%s", self.dc_id) diff --git a/hydrogram/session/session.py b/hydrogram/session/session.py index 6d0b0c5a9..5d9222702 100644 --- a/hydrogram/session/session.py +++ b/hydrogram/session/session.py @@ -17,34 +17,47 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +from __future__ import annotations + import asyncio import bisect -import contextlib import logging import os -from datetime import datetime, timedelta +import random +from datetime import datetime from hashlib import sha1 from io import BytesIO -from typing import ClassVar +from typing import TYPE_CHECKING, ClassVar import hydrogram from hydrogram import raw -from hydrogram.connection import Connection from hydrogram.crypto import mtproto from hydrogram.errors import ( AuthKeyDuplicated, + AuthKeyInvalid, + AuthKeyPermEmpty, + AuthKeyUnregistered, BadMsgNotification, + ConnectionLostError, FloodWait, InternalServerError, + RequestTimeoutError, RPCError, SecurityCheckMismatch, ServiceUnavailable, + SessionExpired, + SessionRevoked, + UserDeactivated, + UserDeactivatedBan, ) from hydrogram.raw.all import layer from hydrogram.raw.core import FutureSalts, Int, MsgContainer, TLObject from .internals import MsgFactory, MsgId +if TYPE_CHECKING: + from hydrogram.connection import Connection + log = logging.getLogger(__name__) @@ -52,6 +65,7 @@ class Result: def __init__(self): self.value = None self.event = asyncio.Event() + self.error = None class Session: @@ -62,7 +76,10 @@ class Session: ACKS_THRESHOLD = 10 PING_INTERVAL = 5 STORED_MSG_IDS_MAX_SIZE = 1000 * 2 - RECONNECT_THRESHOLD = timedelta(seconds=10) + MAX_RECONNECT_ATTEMPTS = 5 + RECONNECT_DELAY = 1.0 + MAX_RECONNECT_DELAY = 30.0 + STABLE_CONNECTION_INTERVAL = 30.0 TRANSPORT_ERRORS: ClassVar = { 404: "auth key not found", @@ -72,7 +89,7 @@ class Session: def __init__( self, - client: "hydrogram.Client", + client: hydrogram.Client, dc_id: int, auth_key: bytes, test_mode: bool, @@ -86,7 +103,7 @@ def __init__( self.is_media = is_media self.is_cdn = is_cdn - self.connection = None + self.connection: Connection | None = None self.auth_key_id = sha1(auth_key).digest()[-8:] @@ -102,111 +119,316 @@ def __init__( self.stored_msg_ids = [] self.ping_task = None - self.ping_task_event = asyncio.Event() - self.recv_task = None self.is_started = asyncio.Event() + self.recovery_task = None + self._background_tasks = set() + self._stopped = True + self._state = "stopped" + self._last_error = None + self._terminal_error = None + self._attempt_error = None + self._reset_session = False + self._connected_at = None + self._send_lock = asyncio.Lock() + self._lifecycle_lock = asyncio.Lock() + self.reconnect_attempt = 0 + self.last_received_at = None + + @property + def health(self) -> dict: + """Return a snapshot of transport readiness, without probing the network.""" + return { + "state": self._state, + "is_connected": self.is_started.is_set() and not self._connection_closed(), + "dc_id": self.dc_id, + "reconnect_attempt": self.reconnect_attempt, + "pending_requests": len(self.results), + "last_received_at": self.last_received_at, + "last_error": str(self._last_error) if self._last_error else None, + } + + def _connection_closed(self): + return ( + self.connection is None + or self.connection.protocol is None + or self.connection.protocol.closed + ) - self.loop = asyncio.get_event_loop() + def _fail_pending(self, reason): + for result in self.results.values(): + if not result.event.is_set(): + result.error = ConnectionLostError( + f"Connection lost ({reason}); request delivery is uncertain" + ) + result.event.set() - self.last_reconnect_attempt = None + def _background_done(self, task): + self._background_tasks.discard(task) + if not task.cancelled() and task.exception() is not None: + log.error("Session background task failed", exc_info=task.exception()) - async def start(self): - while True: - self.connection = Connection( - self.dc_id, - self.test_mode, - self.client.ipv6, - self.client.proxy, - self.is_media, - ) + def _create_background_task(self, coro): + task = self.client.loop.create_task(coro) + self._background_tasks.add(task) + task.add_done_callback(self._background_done) + async def _notify_disconnect(self): + if not self.is_media and callable(self.client.disconnect_handler): try: - await self.connection.connect() - - self.recv_task = self.loop.create_task(self.recv_worker()) - - await self.send(raw.functions.Ping(ping_id=0), timeout=self.START_TIMEOUT) - - if not self.is_cdn: - await self.send( - raw.functions.InvokeWithLayer( - layer=layer, - query=raw.functions.InitConnection( - api_id=await self.client.storage.api_id(), - app_version=self.client.app_version, - device_model=self.client.device_model, - system_version=self.client.system_version, - system_lang_code=self.client.lang_code, - lang_code=self.client.lang_code, - lang_pack="", - query=raw.functions.help.GetConfig(), - ), - ), - timeout=self.START_TIMEOUT, - ) + await self.client.disconnect_handler(self.client) + except Exception: + log.exception("Disconnect handler failed") + + def _request_recovery(self, reason, *, connection=None, terminal=False): + # A late failure on an old transport must not tear down its replacement. + if self._stopped or (connection is not None and connection is not self.connection): + return None + if self._state == "failed": + return None + if ( + self._connected_at is not None + and self.client.loop.time() - self._connected_at >= self.STABLE_CONNECTION_INTERVAL + ): + self.reconnect_attempt = 0 + self._connected_at = None + self._last_error = reason + self._attempt_error = reason + if terminal: + self._terminal_error = reason + was_started = self.is_started.is_set() + self.is_started.clear() + self._fail_pending(reason) + if self.recovery_task is None or self.recovery_task.done(): + if self._state != "connecting": + self._state = "reconnecting" + self.recovery_task = self.client.loop.create_task(self._recover()) + # Observe background failures even when there are no RPC callers. + self.recovery_task.add_done_callback(self._recovery_done) + if was_started: + log.warning( + "Session connection lost: dc=%s error=%s", + self.dc_id, + type(reason).__name__, + extra={ + "dc_id": self.dc_id, + "connection_state": self._state, + "connection_error": str(reason), + }, + ) + self._create_background_task(self._notify_disconnect()) + return self.recovery_task - self.ping_task = self.loop.create_task(self.ping_worker()) + @staticmethod + def _recovery_done(task): + if not task.cancelled(): + task.exception() - log.info("Session initialized: Layer %s", layer) - log.info("Device: %s - %s", self.client.device_model, self.client.app_version) - log.info("System: %s (%s)", self.client.system_version, self.client.lang_code) - except AuthKeyDuplicated as e: - await self.stop() - raise e - except (OSError, RPCError): - await self.stop() - except Exception as e: + async def start(self): + async with self._lifecycle_lock: + if self.is_started.is_set() and not self._connection_closed(): + return + # An explicit start permits a new budget after terminal failure/stop. + self._stopped = False + if self._state in {"stopped", "failed"}: + self._state = "connecting" + self._terminal_error = None + self.reconnect_attempt = 0 + task = self.recovery_task + if task is None or task.done(): + task = self._request_recovery(ConnectionError("Session starting")) + try: + await asyncio.shield(task) + except asyncio.CancelledError: + if not self._stopped and self.recovery_task is task: await self.stop() - raise e - else: - break - + raise + + async def _connect(self): + self._attempt_error = None + if self._reset_session: + # Reset only the ephemeral MTProto session, keeping the authorization key. + self.session_id = os.urandom(8) + self.msg_factory = MsgFactory() + self.stored_msg_ids.clear() + self._reset_session = False + self.connection = self.client.connection_factory( + dc_id=self.dc_id, + test_mode=self.test_mode, + ipv6=self.client.ipv6, + proxy=self.client.proxy, + media=self.is_media and not self.is_cdn, + protocol_factory=self.client.protocol_factory, + ) + await self.connection.connect() + self.recv_task = self.client.loop.create_task(self.recv_worker()) + await self.send(raw.functions.Ping(ping_id=0), timeout=self.START_TIMEOUT) + if not self.is_cdn: + await self.send( + raw.functions.InvokeWithLayer( + layer=layer, + query=raw.functions.InitConnection( + api_id=await self.client.storage.api_id(), + app_version=self.client.app_version, + device_model=self.client.device_model, + system_version=self.client.system_version, + system_lang_code=self.client.lang_code, + lang_code=self.client.lang_code, + lang_pack="", + query=raw.functions.help.GetConfig(), + ), + ), + timeout=self.START_TIMEOUT, + ) + if self._attempt_error is not None: + raise self._attempt_error self.is_started.set() + self._state = "connected" + self._connected_at = self.client.loop.time() + self._last_error = None + self.ping_task = self.client.loop.create_task(self.ping_worker()) + if ( + not self.is_media + and not self.is_cdn + and self.client.update_manager is not None + and self.client.is_initialized + ): + self.client.update_manager.request_recovery() - log.info("Session started") + async def _recover(self): + try: + if self._terminal_error is not None: + raise self._terminal_error + for _ in range(self.MAX_RECONNECT_ATTEMPTS - self.reconnect_attempt): + await self._close_transport() + if self._terminal_error is not None: + raise self._terminal_error + if self.reconnect_attempt: + ceiling = min( + self.MAX_RECONNECT_DELAY, + self.RECONNECT_DELAY * 2 ** (self.reconnect_attempt - 1), + ) + delay = random.uniform(ceiling / 2, ceiling) + log.warning( + "Session reconnect backoff: dc=%s attempt=%s delay=%.2fs", + self.dc_id, + self.reconnect_attempt + 1, + delay, + extra={ + "dc_id": self.dc_id, + "reconnect_attempt": self.reconnect_attempt + 1, + "reconnect_delay": delay, + "connection_state": self._state, + }, + ) + await asyncio.sleep(delay) + self.reconnect_attempt += 1 + try: + await self._connect() + except ( + TimeoutError, + OSError, + InternalServerError, + ServiceUnavailable, + BadMsgNotification, + ) as e: + if self._terminal_error is not None: + raise self._terminal_error from e + if isinstance(e, BadMsgNotification) and not self._reset_session: + raise + self._last_error = e + log.warning( + "Session connection attempt failed: dc=%s attempt=%s error=%s", + self.dc_id, + self.reconnect_attempt, + type(e).__name__, + extra={ + "dc_id": self.dc_id, + "reconnect_attempt": self.reconnect_attempt, + "connection_state": self._state, + "connection_error": str(e), + }, + ) + else: + log.info( + "Session connected: dc=%s attempt=%s", self.dc_id, self.reconnect_attempt + ) + return + raise ConnectionError( + f"Session recovery failed after {self.MAX_RECONNECT_ATTEMPTS} attempts" + ) from self._last_error + except BaseException as e: + self.is_started.clear() + self._state = "stopped" if self._stopped else "failed" + self._last_error = e + self._fail_pending(e) + await self._close_transport() + if not isinstance(e, asyncio.CancelledError): + log.error( + "Session recovery stopped: dc=%s error=%s", + self.dc_id, + type(e).__name__, + extra={ + "dc_id": self.dc_id, + "connection_state": self._state, + "connection_error": str(e), + }, + ) + raise + + async def _close_transport(self): + tasks = [ + task + for task in (self.ping_task, self.recv_task) + if task is not None and task is not asyncio.current_task() + ] + for task in tasks: + task.cancel() + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + self.ping_task = self.recv_task = None + if self.connection is not None: + await self.connection.close() + self.pending_acks.clear() async def stop(self): - self.is_started.clear() + async with self._lifecycle_lock: + was_stopped = self._stopped + await self._stop() + if not was_stopped: + await self._notify_disconnect() + log.info("Session stopped") + async def _stop(self): + self._stopped = True + self.is_started.clear() + self._state = "stopped" + self._connected_at = None + self._fail_pending("Session stopped") + if self.recovery_task is not None and self.recovery_task is not asyncio.current_task(): + self.recovery_task.cancel() + await asyncio.gather(self.recovery_task, return_exceptions=True) + await self._close_transport() self.stored_msg_ids.clear() - - self.ping_task_event.set() - - if self.ping_task is not None: - await self.ping_task - - self.ping_task_event.clear() - - await self.connection.close() - - if self.recv_task: - await self.recv_task - - if not self.is_media and callable(self.client.disconnect_handler): - try: - await self.client.disconnect_handler(self.client) - except Exception as e: - log.exception(e) - - log.info("Session stopped") + tasks = [task for task in self._background_tasks if task is not asyncio.current_task()] + for task in tasks: + task.cancel() + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) async def restart(self): - now = datetime.now() - if ( - self.last_reconnect_attempt - and now - self.last_reconnect_attempt < self.RECONNECT_THRESHOLD - ): - log.info("Reconnecting too frequently, sleeping for a while") - await asyncio.sleep(5) - - self.last_reconnect_attempt = now - await self.stop() - await self.start() + if self._stopped: + raise ConnectionError("Session is stopped; call start() to connect") + task = self._request_recovery(ConnectionError("Session restart requested")) + if task is None: + raise ConnectionError( + "Session recovery failed; call start() to retry" + ) from self._last_error + await asyncio.shield(task) async def handle_packet(self, packet): - data = await self.loop.run_in_executor( + data = await self.client.loop.run_in_executor( hydrogram.crypto_executor, mtproto.unpack, BytesIO(packet), @@ -245,18 +467,21 @@ async def handle_packet(self, packet): if time_diff > 30: raise SecurityCheckMismatch( "The msg_id belongs to over 30 seconds in the future. " - "Most likely the client time has to be synchronized." + "This usually means your system clock is ahead of the actual time. " + "Please synchronize your system time with an NTP server to avoid " + "this error in Hydrogram." ) if time_diff < -300: raise SecurityCheckMismatch( "The msg_id belongs to over 300 seconds in the past. " - "Most likely the client time has to be synchronized." + "This usually means your system clock is behind the actual time. " + "Please synchronize your system time with an NTP server to avoid " + "this error in Hydrogram." ) except SecurityCheckMismatch as e: log.info("Discarding packet: %s", e) - await self.connection.close() - return + continue else: bisect.insort(self.stored_msg_ids, msg.msg_id) @@ -265,6 +490,13 @@ async def handle_packet(self, packet): continue if isinstance(msg.body, raw.types.NewSessionCreated): + self.salt = msg.body.server_salt + if ( + not self.is_media + and not self.is_cdn + and self.client.update_manager is not None + ): + self.client.update_manager.request_recovery() continue msg_id = None @@ -276,7 +508,31 @@ async def handle_packet(self, packet): elif isinstance(msg.body, raw.types.Pong): msg_id = msg.body.msg_id elif self.client is not None: - self.loop.create_task(self.client.handle_updates(msg.body)) + if self.client.update_manager is not None: + self.client.last_update_time = datetime.now() + self.client.update_manager.submit(msg.body) + else: + self._create_background_task(self.client.handle_updates(msg.body)) + + if ( + isinstance(msg.body, raw.types.RpcResult) + and not self.is_media + and not self.is_cdn + and self.client.update_manager is not None + and isinstance( + msg.body.result, + ( + raw.types.Updates, + raw.types.UpdatesCombined, + raw.types.UpdateShort, + raw.types.UpdateShortMessage, + raw.types.UpdateShortChatMessage, + raw.types.UpdateShortSentMessage, + raw.types.UpdatesTooLong, + ), + ) + ): + self.client.update_manager.submit(msg.body.result) if msg_id in self.results: self.results[msg_id].value = getattr(msg.body, "result", msg.body) @@ -293,112 +549,173 @@ async def handle_packet(self, packet): self.pending_acks.clear() async def ping_worker(self): - log.info("PingTask started") - - while True: - try: - await asyncio.wait_for(self.ping_task_event.wait(), self.PING_INTERVAL) - except asyncio.TimeoutError: - pass - else: - break - - with contextlib.suppress(OSError, RPCError): + try: + while True: + await asyncio.sleep(self.PING_INTERVAL) + # Require a pong; successful writes alone do not prove liveness. await self.send( raw.functions.PingDelayDisconnect( - ping_id=0, disconnect_delay=self.WAIT_TIMEOUT + 10 + ping_id=0, + disconnect_delay=int(self.WAIT_TIMEOUT + self.PING_INTERVAL + 10), ), - False, + timeout=self.WAIT_TIMEOUT, ) - - log.info("PingTask stopped") + except (TimeoutError, OSError, RPCError) as e: + self._request_recovery(e, connection=self.connection) + except Exception as e: + log.exception("Session ping monitor failed: dc=%s", self.dc_id) + self._request_recovery(e, connection=self.connection, terminal=True) async def recv_worker(self): - log.info("NetworkTask started") - - while True: - packet = await self.connection.recv() - - if packet is None or len(packet) == 4: - if packet: - error_code = -Int.read(BytesIO(packet)) - - log.warning( - "Server sent transport error: %s (%s)", - error_code, - Session.TRANSPORT_ERRORS.get(error_code, "unknown error"), + connection = self.connection + try: + while True: + packet = await connection.recv() + if packet is None: + raise ConnectionError("Transport closed by peer") + if len(packet) == 4: + code = -Int.read(BytesIO(packet)) + error = ConnectionError( + f"Telegram transport error {code}: " + f"{self.TRANSPORT_ERRORS.get(code, 'unknown error')}" ) - - if self.is_started.is_set(): - self.loop.create_task(self.restart()) - - break - - self.loop.create_task(self.handle_packet(packet)) - - log.info("NetworkTask stopped") + self._request_recovery( + error, connection=connection, terminal=code in {404, 444} + ) + return + self.last_received_at = datetime.now() + # Keep packet processing ordered and owned by the receiver task. + await self.handle_packet(packet) + except (TimeoutError, OSError, SecurityCheckMismatch) as e: + self._request_recovery(e, connection=connection) + except Exception as e: + log.exception("Session packet processing failed: dc=%s", self.dc_id) + self._request_recovery(e, connection=connection, terminal=True) async def send( self, data: TLObject, wait_response: bool = True, timeout: float = WAIT_TIMEOUT ): - message = self.msg_factory(data) - msg_id = message.msg_id - - if wait_response: - self.results[msg_id] = Result() - - log.debug("Sent: %s", message) - - payload = await self.loop.run_in_executor( - hydrogram.crypto_executor, - mtproto.pack, - message, - self.salt, - self.session_id, - self.auth_key, - self.auth_key_id, - ) - + # Only an explicit rejection for a bad salt permits automatic resending. + for _ in range(self.MAX_RETRIES): + result = await self._send_once(data, wait_response, timeout) + if not isinstance(result, raw.types.BadServerSalt): + return result + self.salt = result.new_server_salt + raise BadMsgNotification(48) + + async def _write(self, data, wait_response, timeout): + # Serialize message numbering, encryption and writing, but never wait for + # a reply under this lock. Executor completion order can otherwise reorder + # MTProto sequence numbers on the wire under concurrent RPC load. + async with self._send_lock: + connection = self.connection + message = self.msg_factory(data) + payload = await self.client.loop.run_in_executor( + hydrogram.crypto_executor, + mtproto.pack, + message, + self.salt, + self.session_id, + self.auth_key, + self.auth_key_id, + ) + if self._stopped or connection is None or connection is not self.connection: + raise ConnectionLostError( + "Connection changed before sending", delivery_uncertain=False + ) + if self._attempt_error is not None: + raise ConnectionLostError("Connection is recovering", delivery_uncertain=False) + msg_id = message.msg_id + result = Result() + if wait_response: + self.results[msg_id] = result + try: + async with asyncio.timeout(timeout): + await connection.send(payload) + except (TimeoutError, OSError) as e: + self.results.pop(msg_id, None) + self._request_recovery(e, connection=connection) + raise ConnectionLostError("Send failed; request delivery is uncertain") from e + except BaseException: + self.results.pop(msg_id, None) + raise + return connection, msg_id, result + + async def _send_once(self, data, wait_response, timeout): + connection, msg_id, result = await self._write(data, wait_response, timeout) try: - await self.connection.send(payload) - except OSError as e: - self.results.pop(msg_id, None) - raise e - - if wait_response: - with contextlib.suppress(asyncio.TimeoutError): - await asyncio.wait_for(self.results[msg_id].event.wait(), timeout) - - result = self.results.pop(msg_id).value - - if result is None: - raise TimeoutError("Request timed out") - - if isinstance(result, raw.types.RpcError): - if isinstance( - data, - ( - raw.functions.InvokeWithoutUpdates, - raw.functions.InvokeWithTakeout, - ), - ): - data = data.query - - RPCError.raise_it(result, type(data)) - - if isinstance(result, raw.types.BadMsgNotification): - log.warning( - "%s: %s", - BadMsgNotification.__name__, - BadMsgNotification(result.error_code), + if not wait_response: + return None + try: + async with asyncio.timeout(timeout): + await result.event.wait() + except TimeoutError as e: + error = RequestTimeoutError( + "Request timed out; Telegram may have processed it. " + "Check the outcome before retrying." ) + self._request_recovery(error, connection=connection) + raise error from e + if result.error is not None: + raise result.error + value = result.value + finally: + self.results.pop(msg_id, None) - if isinstance(result, raw.types.BadServerSalt): - self.salt = result.new_server_salt - return await self.send(data, wait_response, timeout) - - return result - return None + if isinstance(value, raw.types.RpcError): + while isinstance( + data, (raw.functions.InvokeWithoutUpdates, raw.functions.InvokeWithTakeout) + ): + data = data.query + try: + RPCError.raise_it(value, type(data)) + except ( + AuthKeyDuplicated, + AuthKeyInvalid, + AuthKeyPermEmpty, + AuthKeyUnregistered, + SessionExpired, + SessionRevoked, + UserDeactivated, + UserDeactivatedBan, + ) as e: + self._request_recovery(e, connection=connection, terminal=True) + raise + if isinstance(value, raw.types.BadMsgNotification): + error = BadMsgNotification(value.error_code) + if value.error_code in {32, 33}: + if connection is self.connection: + self._reset_session = True + self._request_recovery(error, connection=connection) + else: + # Clock/configuration errors need caller intervention, not replay. + self._request_recovery(error, connection=connection, terminal=True) + raise error + return value + + async def _wait_connected(self): + if self._stopped: + raise ConnectionError("Session is stopped; call start() to connect") + if self._state == "failed": + raise ConnectionError( + "Session recovery failed; call start() to retry" + ) from self._last_error + if self.is_started.is_set() and not self._connection_closed(): + return + task = self.recovery_task + if task is None or task.done(): + task = self._request_recovery(ConnectionError("Transport is not ready")) + if task is None: + raise ConnectionError("Session is not connected") from self._last_error + # Caller cancellation must not cancel recovery shared with other RPCs. + try: + await asyncio.shield(task) + except asyncio.CancelledError: + if task.cancelled(): + raise ConnectionError("Session stopped during recovery") from None + raise + if self._stopped or not self.is_started.is_set(): + raise ConnectionError("Session stopped during recovery") async def invoke( self, @@ -407,47 +724,26 @@ async def invoke( timeout: float = WAIT_TIMEOUT, sleep_threshold: float = SLEEP_THRESHOLD, ): - with contextlib.suppress(asyncio.TimeoutError): - await asyncio.wait_for(self.is_started.wait(), self.WAIT_TIMEOUT) - - if isinstance( - query, (raw.functions.InvokeWithoutUpdates, raw.functions.InvokeWithTakeout) + if retries <= 0: + raise TimeoutError("Exceeded maximum number of retries") + inner_query = query + while isinstance( + inner_query, (raw.functions.InvokeWithoutUpdates, raw.functions.InvokeWithTakeout) ): - inner_query = query.query - else: - inner_query = query - + inner_query = inner_query.query query_name = ".".join(inner_query.QUALNAME.split(".")[1:]) - - while retries > 0: + for attempt in range(retries): + await self._wait_connected() try: return await self.send(query, timeout=timeout) except FloodWait as e: - amount = e.value - - if amount > sleep_threshold >= 0: + if e.value > sleep_threshold >= 0 or attempt + 1 >= retries: raise - - log.warning( - '[%s] Waiting for %s seconds before continuing (required by "%s")', - self.client.name, - amount, - query_name, - ) - - await asyncio.sleep(amount) - except (OSError, InternalServerError, ServiceUnavailable) as e: - retries -= 1 - if retries == 0: - raise e - - (log.warning if retries < 2 else log.info)( - '[%s] Retrying "%s" due to: %s', - Session.MAX_RETRIES - retries, - query_name, - str(e) or repr(e), - ) - - await asyncio.sleep(0.5) - - raise TimeoutError("Exceeded maximum number of retries") + log.warning('Waiting %s seconds for "%s"', e.value, query_name) + await asyncio.sleep(e.value) + except ConnectionLostError as e: + if e.delivery_uncertain or attempt + 1 >= retries: + raise + # RPC/server errors and ambiguous transport failures are surfaced. + # Retrying an arbitrary query here could duplicate side effects. + raise ConnectionError("Request could not be sent after recovery") diff --git a/hydrogram/storage/base.py b/hydrogram/storage/base.py index bc34a5d03..fe85d3a90 100644 --- a/hydrogram/storage/base.py +++ b/hydrogram/storage/base.py @@ -22,11 +22,10 @@ import base64 import struct from abc import ABC, abstractmethod -from typing import Union from hydrogram import raw -InputPeer = Union[raw.types.InputPeerUser, raw.types.InputPeerChat, raw.types.InputPeerChannel] +InputPeer = raw.types.InputPeerUser | raw.types.InputPeerChat | raw.types.InputPeerChannel class BaseStorage(ABC): @@ -208,6 +207,24 @@ async def is_bot(self, value: bool | None = None) -> bool: """ ... + async def get_update_states(self) -> dict[int, tuple[int, int, int, int]]: + """Load update cursors as ``{channel_id: (pts, qts, date, seq)}``. + + Channel ID 0 denotes the common state. This optional storage extension + is required only when ``Client(catch_up=True)`` is used. + """ + raise NotImplementedError("This storage engine does not support catch_up") + + async def set_update_state( + self, channel_id: int, pts: int, qts: int, date: int, seq: int + ) -> None: + """Atomically and durably replace one update cursor. + + Channel IDs are raw positive Telegram IDs; 0 denotes the common state. + The update date is independent of the session's last-save date. + """ + raise NotImplementedError("This storage engine does not support catch_up") + async def export_session_string(self) -> str: """Exports the session string for the current session. diff --git a/hydrogram/storage/sqlite_storage.py b/hydrogram/storage/sqlite_storage.py index 4aebc32b9..73b245747 100644 --- a/hydrogram/storage/sqlite_storage.py +++ b/hydrogram/storage/sqlite_storage.py @@ -32,6 +32,17 @@ from .base import BaseStorage, InputPeer +UPDATE_STATE_SCHEMA = """ +CREATE TABLE IF NOT EXISTS update_state +( + channel_id INTEGER PRIMARY KEY, + pts INTEGER NOT NULL, + qts INTEGER NOT NULL, + date INTEGER NOT NULL, + seq INTEGER NOT NULL +); +""" + SCHEMA = """ CREATE TABLE sessions ( @@ -87,7 +98,7 @@ def get_input_peer(peer_id: int, access_hash: int, peer_type: str) -> InputPeer: class SQLiteStorage(BaseStorage): - VERSION = 3 + VERSION = 4 USERNAME_TTL = 8 * 60 * 60 FILE_EXTENSION = ".session" @@ -122,6 +133,10 @@ async def update(self) -> None: await self.conn.execute("ALTER TABLE sessions ADD api_id INTEGER") version += 1 + if version == 3: + await self.conn.execute(UPDATE_STATE_SCHEMA) + version += 1 + await self.version(version) await self.conn.commit() @@ -130,7 +145,7 @@ async def create(self) -> None: logging.warning("Database connection is not available.") return - await self.conn.executescript(SCHEMA) + await self.conn.executescript(SCHEMA + UPDATE_STATE_SCHEMA) await self.conn.execute( "INSERT INTO version VALUES (?)", (self.VERSION,), @@ -260,6 +275,25 @@ async def get_peer_by_phone_number(self, phone_number: str) -> InputPeer | None: return get_input_peer(*r) + async def get_update_states(self) -> dict[int, tuple[int, int, int, int]]: + if self.conn is None: + raise RuntimeError("Storage is not open") + async with self.conn.execute( + "SELECT channel_id, pts, qts, date, seq FROM update_state" + ) as cursor: + return {row[0]: tuple(row[1:]) for row in await cursor.fetchall()} + + async def set_update_state( + self, channel_id: int, pts: int, qts: int, date: int, seq: int + ) -> None: + if self.conn is None: + raise RuntimeError("Storage is not open") + await self.conn.execute( + "REPLACE INTO update_state (channel_id, pts, qts, date, seq) VALUES (?, ?, ?, ?, ?)", + (channel_id, pts, qts, date, seq), + ) + await self.conn.commit() + async def _get(self, attr: str) -> Any: if not self.conn: logging.warning("Database connection is not available.") diff --git a/hydrogram/transmission.py b/hydrogram/transmission.py new file mode 100644 index 000000000..138377212 --- /dev/null +++ b/hydrogram/transmission.py @@ -0,0 +1,62 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +import functools +from contextlib import ExitStack, asynccontextmanager +from pathlib import Path + + +async def finish(awaitable): + """Finish owned cleanup/I/O before propagating caller cancellation. + + Executor calls cannot be cancelled once running. Keep their resources and + transmission slot alive until they finish, including on repeated cancellation. + """ + future = asyncio.ensure_future(awaitable) + cancelled = False + while not future.done(): + try: + await asyncio.shield(future) + except asyncio.CancelledError: + cancelled = True + if cancelled: + # Retrieve any exception before propagating cancellation. + if not future.cancelled(): + future.exception() + raise asyncio.CancelledError + return future.result() + + +async def run_sync(func, *args, **kwargs): + return await finish( + asyncio.get_running_loop().run_in_executor(None, functools.partial(func, *args, **kwargs)) + ) + + +@asynccontextmanager +async def open_file(path, mode): + # The stack owns the handle even if cancellation arrives during open(). + stack = ExitStack() + try: + yield await run_sync(lambda: stack.enter_context(Path(path).open(mode))) # noqa: SIM115 - owned by stack + finally: + await run_sync(stack.close) diff --git a/hydrogram/types/__init__.py b/hydrogram/types/__init__.py index 336feea4b..f22a3454d 100644 --- a/hydrogram/types/__init__.py +++ b/hydrogram/types/__init__.py @@ -19,6 +19,7 @@ from .authorization import ( SentCode, + Session, TermsOfService, sent_code, terms_of_service, @@ -135,8 +136,10 @@ ) from .input_message_content import ( InputMessageContent, + InputPollOption, InputTextMessageContent, input_message_content, + input_poll_option, input_text_message_content, ) from .list import List @@ -194,6 +197,7 @@ from .user_and_chats import ( Chat, ChatAdminWithInviteLinks, + ChatBackground, ChatEvent, ChatEventFilter, ChatInviteLink, @@ -266,6 +270,7 @@ "CallbackQuery", "Chat", "ChatAdminWithInviteLinks", + "ChatBackground", "ChatEvent", "ChatEventFilter", "ChatInviteLink", @@ -324,11 +329,11 @@ "InputMediaVideo", "InputMessageContent", "InputPhoneContact", + "InputPollOption", "InputTextMessageContent", "InviteLinkImporter", "KeyboardButton", "List", - "List", "Listener", "ListenerTypes", "Location", @@ -352,6 +357,7 @@ "Restriction", "SentCode", "SentWebAppMessage", + "Session", "Sticker", "StrippedThumbnail", "TermsOfService", @@ -435,7 +441,7 @@ "input_media_video", "input_message_content", "input_phone_contact", - "input_text_message_content", + "input_poll_option", "input_text_message_content", "invite_link_importer", "keyboard_button", diff --git a/hydrogram/types/authorization/__init__.py b/hydrogram/types/authorization/__init__.py index 03a135421..8b8693708 100644 --- a/hydrogram/types/authorization/__init__.py +++ b/hydrogram/types/authorization/__init__.py @@ -18,6 +18,7 @@ # along with Hydrogram. If not, see . from .sent_code import SentCode +from .session import Session from .terms_of_service import TermsOfService -__all__ = ["SentCode", "TermsOfService"] +__all__ = ["SentCode", "Session", "TermsOfService"] diff --git a/hydrogram/types/authorization/session.py b/hydrogram/types/authorization/session.py new file mode 100644 index 000000000..9ea225b3d --- /dev/null +++ b/hydrogram/types/authorization/session.py @@ -0,0 +1,152 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from hydrogram.types.object import Object + +if TYPE_CHECKING: + from hydrogram import raw + + +class Session(Object): + """Contains info about a device session. + + Parameters: + id (``int``): + Session id. + + device_model (``str``): + Device model used for authorization. + + platform (``str``): + Platform used for authorization. + + system_version (``str``): + System version used for authorization. + + api_id (``int``): + API ID used for authorization. + + app_name (``str``): + App name used for authorization. + + app_version (``str``): + App version used for authorization. + + date_created (``int``): + Date when authorization was created. + + date_active (``int``): + Date when authorization was last active. + + ip (``str``): + IP address used for authorization. + + country (``str``): + Country where authorization occurred. + + region (``str``): + Region where authorization occurred. + + is_current (``bool``): + Whether this is the current authorization. + + is_official_app (``bool``): + Whether this is an official app. + + is_password_pending (``bool``): + Whether a password is pending. + + accepts_secret_chats (``bool``): + Whether secret chat requests are allowed. + + accepts_calls (``bool``): + Whether call requests are allowed. + + is_confirmed (``bool``): + Whether the authorization is confirmed. + """ + + def __init__( + self, + *, + id: int, + device_model: str, + platform: str, + system_version: str, + api_id: int, + app_name: str, + app_version: str, + date_created: int, + date_active: int, + ip: str, + country: str, + region: str, + is_current: bool, + is_official_app: bool, + is_password_pending: bool, + accepts_secret_chats: bool, + accepts_calls: bool, + is_confirmed: bool, + ): + super().__init__() + + self.id = id + self.device_model = device_model + self.platform = platform + self.system_version = system_version + self.api_id = api_id + self.app_name = app_name + self.app_version = app_version + self.date_created = date_created + self.date_active = date_active + self.ip = ip + self.country = country + self.region = region + self.is_current = is_current + self.is_official_app = is_official_app + self.is_password_pending = is_password_pending + self.accepts_secret_chats = accepts_secret_chats + self.accepts_calls = accepts_calls + self.is_confirmed = is_confirmed + + @staticmethod + def _parse(authorization: raw.types.Authorization) -> Session: + return Session( + id=authorization.hash, + device_model=authorization.device_model, + platform=authorization.platform, + system_version=authorization.system_version, + api_id=authorization.api_id, + app_name=authorization.app_name, + app_version=authorization.app_version, + date_created=authorization.date_created, + date_active=authorization.date_active, + ip=authorization.ip, + country=authorization.country, + region=authorization.region, + is_current=authorization.current, + is_official_app=authorization.official_app, + is_password_pending=authorization.password_pending, + accepts_secret_chats=not authorization.encrypted_requests_disabled, + accepts_calls=not authorization.call_requests_disabled, + is_confirmed=not authorization.unconfirmed, + ) diff --git a/hydrogram/types/bots_and_keyboards/callback_query.py b/hydrogram/types/bots_and_keyboards/callback_query.py index 150b11f0c..8af28b758 100644 --- a/hydrogram/types/bots_and_keyboards/callback_query.py +++ b/hydrogram/types/bots_and_keyboards/callback_query.py @@ -99,7 +99,7 @@ async def _parse(client: hydrogram.Client, callback_query, users) -> CallbackQue chat_id = utils.get_peer_id(callback_query.peer) message_id = callback_query.msg_id - message = client.message_cache[(chat_id, message_id)] or await client.get_messages( + message = client.message_cache[chat_id, message_id] or await client.get_messages( chat_id, message_id ) elif isinstance(callback_query, raw.types.UpdateInlineBotCallbackQuery): diff --git a/hydrogram/types/bots_and_keyboards/inline_keyboard_button.py b/hydrogram/types/bots_and_keyboards/inline_keyboard_button.py index e951e3952..6a1ca6ee0 100644 --- a/hydrogram/types/bots_and_keyboards/inline_keyboard_button.py +++ b/hydrogram/types/bots_and_keyboards/inline_keyboard_button.py @@ -99,78 +99,62 @@ def __init__( # self.pay = pay @staticmethod - def read(b: raw.base.KeyboardButton): - if isinstance(b, raw.types.KeyboardButtonCallback): - # Try decode data to keep it as string, but if fails, fallback to bytes so we don't lose any information, - # instead of decoding by ignoring/replacing errors. + def read(b: raw.types.KeyboardInlineButton): + button_type = b.type + if isinstance(button_type, raw.types.InlineButtonTypeCallback): + # Preserve callback payloads that are not UTF-8. try: - data = b.data.decode() + data = button_type.data.decode() except UnicodeDecodeError: - data = b.data - + data = button_type.data return InlineKeyboardButton(text=b.text, callback_data=data) - - if isinstance(b, raw.types.KeyboardButtonUrl): - return InlineKeyboardButton(text=b.text, url=b.url) - - if isinstance(b, raw.types.KeyboardButtonUrlAuth): - return InlineKeyboardButton(text=b.text, login_url=types.LoginUrl.read(b)) - - if isinstance(b, raw.types.KeyboardButtonUserProfile): - return InlineKeyboardButton(text=b.text, user_id=b.user_id) - - if isinstance(b, raw.types.KeyboardButtonSwitchInline): - if b.same_peer: - return InlineKeyboardButton(text=b.text, switch_inline_query_current_chat=b.query) - return InlineKeyboardButton(text=b.text, switch_inline_query=b.query) - - if isinstance(b, raw.types.KeyboardButtonGame): + if isinstance(button_type, raw.types.InlineButtonTypeUrl): + return InlineKeyboardButton(text=b.text, url=button_type.url) + if isinstance(button_type, raw.types.InlineButtonTypeUrlAuth): + return InlineKeyboardButton(text=b.text, login_url=types.LoginUrl.read(button_type)) + if isinstance(button_type, raw.types.InlineButtonTypeUserProfile): + return InlineKeyboardButton(text=b.text, user_id=button_type.user_id) + if isinstance(button_type, raw.types.InlineButtonTypeSwitchInline): + if button_type.same_peer: + return InlineKeyboardButton( + text=b.text, switch_inline_query_current_chat=button_type.query + ) + return InlineKeyboardButton(text=b.text, switch_inline_query=button_type.query) + if isinstance(button_type, raw.types.InlineButtonTypeGame): return InlineKeyboardButton(text=b.text, callback_game=types.CallbackGame()) - - if isinstance(b, raw.types.KeyboardButtonWebView): - return InlineKeyboardButton(text=b.text, web_app=types.WebAppInfo(url=b.url)) + if isinstance(button_type, raw.types.InlineButtonTypeWebView): + return InlineKeyboardButton(text=b.text, web_app=types.WebAppInfo(url=button_type.url)) return None async def write(self, client: hydrogram.Client): if self.callback_data is not None: - # Telegram only wants bytes, but we are allowed to pass strings too, for convenience. data = ( - bytes(self.callback_data, "utf-8") + self.callback_data.encode() if isinstance(self.callback_data, str) else self.callback_data ) - - return raw.types.KeyboardButtonCallback(text=self.text, data=data) - - if self.url is not None: - return raw.types.KeyboardButtonUrl(text=self.text, url=self.url) - - if self.login_url is not None: + button_type = raw.types.InlineButtonTypeCallback(data=data) + elif self.url is not None: + button_type = raw.types.InlineButtonTypeUrl(url=self.url) + elif self.login_url is not None: return self.login_url.write( text=self.text, bot=await client.resolve_peer(self.login_url.bot_username or "self"), ) - - if self.user_id is not None: - return raw.types.InputKeyboardButtonUserProfile( - text=self.text, user_id=await client.resolve_peer(self.user_id) + elif self.user_id is not None: + button_type = raw.types.InputInlineButtonTypeUserProfile( + user_id=await client.resolve_peer(self.user_id) ) - - if self.switch_inline_query is not None: - return raw.types.KeyboardButtonSwitchInline( - text=self.text, query=self.switch_inline_query + elif self.switch_inline_query is not None: + button_type = raw.types.InlineButtonTypeSwitchInline(query=self.switch_inline_query) + elif self.switch_inline_query_current_chat is not None: + button_type = raw.types.InlineButtonTypeSwitchInline( + query=self.switch_inline_query_current_chat, same_peer=True ) - - if self.switch_inline_query_current_chat is not None: - return raw.types.KeyboardButtonSwitchInline( - text=self.text, - query=self.switch_inline_query_current_chat, - same_peer=True, - ) - - if self.callback_game is not None: - return raw.types.KeyboardButtonGame(text=self.text) - - if self.web_app is not None: - return raw.types.KeyboardButtonWebView(text=self.text, url=self.web_app.url) - return None + elif self.callback_game is not None: + button_type = raw.types.InlineButtonTypeGame() + elif self.web_app is not None: + button_type = raw.types.InlineButtonTypeWebView(url=self.web_app.url) + else: + return None + return raw.types.KeyboardInlineButton(text=self.text, type=button_type) diff --git a/hydrogram/types/bots_and_keyboards/inline_keyboard_markup.py b/hydrogram/types/bots_and_keyboards/inline_keyboard_markup.py index e8639346a..7b7d8a3c7 100644 --- a/hydrogram/types/bots_and_keyboards/inline_keyboard_markup.py +++ b/hydrogram/types/bots_and_keyboards/inline_keyboard_markup.py @@ -52,6 +52,6 @@ async def write(self, client: "hydrogram.Client"): for r in self.inline_keyboard: buttons = [await b.write(client) for b in r] - rows.append(raw.types.KeyboardButtonRow(buttons=buttons)) + rows.append(raw.types.KeyboardInlineButtonRow(buttons=buttons)) return raw.types.ReplyInlineMarkup(rows=rows) diff --git a/hydrogram/types/bots_and_keyboards/keyboard_button.py b/hydrogram/types/bots_and_keyboards/keyboard_button.py index e3ac1d507..380ceb4c5 100644 --- a/hydrogram/types/bots_and_keyboards/keyboard_button.py +++ b/hydrogram/types/bots_and_keyboards/keyboard_button.py @@ -65,25 +65,24 @@ def __init__( self.web_app = web_app @staticmethod - def read(b): - if isinstance(b, raw.types.KeyboardButton): + def read(b: raw.types.KeyboardButton): + if isinstance(b.type, raw.types.ButtonTypeDefault): return b.text - - if isinstance(b, raw.types.KeyboardButtonRequestPhone): + if isinstance(b.type, raw.types.ButtonTypeRequestPhone): return KeyboardButton(text=b.text, request_contact=True) - - if isinstance(b, raw.types.KeyboardButtonRequestGeoLocation): + if isinstance(b.type, raw.types.ButtonTypeRequestGeoLocation): return KeyboardButton(text=b.text, request_location=True) - - if isinstance(b, raw.types.KeyboardButtonSimpleWebView): - return KeyboardButton(text=b.text, web_app=types.WebAppInfo(url=b.url)) + if isinstance(b.type, raw.types.ButtonTypeSimpleWebView): + return KeyboardButton(text=b.text, web_app=types.WebAppInfo(url=b.type.url)) return None def write(self): if self.request_contact: - return raw.types.KeyboardButtonRequestPhone(text=self.text) - if self.request_location: - return raw.types.KeyboardButtonRequestGeoLocation(text=self.text) - if self.web_app: - return raw.types.KeyboardButtonSimpleWebView(text=self.text, url=self.web_app.url) - return raw.types.KeyboardButton(text=self.text) + button_type = raw.types.ButtonTypeRequestPhone() + elif self.request_location: + button_type = raw.types.ButtonTypeRequestGeoLocation() + elif self.web_app: + button_type = raw.types.ButtonTypeSimpleWebView(url=self.web_app.url) + else: + button_type = raw.types.ButtonTypeDefault() + return raw.types.KeyboardButton(text=self.text, type=button_type) diff --git a/hydrogram/types/bots_and_keyboards/login_url.py b/hydrogram/types/bots_and_keyboards/login_url.py index d0233fa46..759db123a 100644 --- a/hydrogram/types/bots_and_keyboards/login_url.py +++ b/hydrogram/types/bots_and_keyboards/login_url.py @@ -76,14 +76,16 @@ def __init__( self.button_id = button_id @staticmethod - def read(b: raw.types.KeyboardButtonUrlAuth) -> LoginUrl: + def read(b: raw.types.InlineButtonTypeUrlAuth) -> LoginUrl: return LoginUrl(url=b.url, forward_text=b.fwd_text, button_id=b.button_id) def write(self, text: str, bot: raw.types.InputUser): - return raw.types.InputKeyboardButtonUrlAuth( + return raw.types.KeyboardInlineButton( text=text, - url=self.url, - bot=bot, - fwd_text=self.forward_text, - request_write_access=self.request_write_access, + type=raw.types.InputInlineButtonTypeUrlAuth( + url=self.url, + bot=bot, + fwd_text=self.forward_text, + request_write_access=self.request_write_access, + ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_animation.py b/hydrogram/types/inline_mode/inline_query_result_animation.py index 9377ef3f3..6fc1fbe9d 100644 --- a/hydrogram/types/inline_mode/inline_query_result_animation.py +++ b/hydrogram/types/inline_mode/inline_query_result_animation.py @@ -71,6 +71,9 @@ class InlineQueryResultAnimation(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the animation. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -92,6 +95,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -108,6 +112,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -156,6 +161,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_audio.py b/hydrogram/types/inline_mode/inline_query_result_audio.py index cf5d908fd..52d1ad7e4 100644 --- a/hydrogram/types/inline_mode/inline_query_result_audio.py +++ b/hydrogram/types/inline_mode/inline_query_result_audio.py @@ -59,6 +59,9 @@ class InlineQueryResultAudio(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the audio. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): Inline keyboard attached to the message. @@ -76,6 +79,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -88,6 +92,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media async def write(self, client: hydrogram.Client): audio = raw.types.InputWebDocument( @@ -123,6 +128,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_cached_animation.py b/hydrogram/types/inline_mode/inline_query_result_cached_animation.py index 9b9cd6a36..04bdb6ef5 100644 --- a/hydrogram/types/inline_mode/inline_query_result_cached_animation.py +++ b/hydrogram/types/inline_mode/inline_query_result_cached_animation.py @@ -54,6 +54,9 @@ class InlineQueryResultCachedAnimation(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the animation. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -69,6 +72,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -79,6 +83,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -109,6 +114,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_cached_audio.py b/hydrogram/types/inline_mode/inline_query_result_cached_audio.py index 13932927c..d0c01315f 100644 --- a/hydrogram/types/inline_mode/inline_query_result_cached_audio.py +++ b/hydrogram/types/inline_mode/inline_query_result_cached_audio.py @@ -50,6 +50,9 @@ class InlineQueryResultCachedAudio(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the audio. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -64,6 +67,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -73,6 +77,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -102,6 +107,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_cached_document.py b/hydrogram/types/inline_mode/inline_query_result_cached_document.py index 3aa399a2d..d6de6b135 100644 --- a/hydrogram/types/inline_mode/inline_query_result_cached_document.py +++ b/hydrogram/types/inline_mode/inline_query_result_cached_document.py @@ -56,6 +56,9 @@ class InlineQueryResultCachedDocument(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the document. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -72,6 +75,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -83,6 +87,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -114,6 +119,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_cached_photo.py b/hydrogram/types/inline_mode/inline_query_result_cached_photo.py index 06a3e4d0d..fd3c4ec06 100644 --- a/hydrogram/types/inline_mode/inline_query_result_cached_photo.py +++ b/hydrogram/types/inline_mode/inline_query_result_cached_photo.py @@ -56,6 +56,9 @@ class InlineQueryResultCachedPhoto(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the photo. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -72,6 +75,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -83,6 +87,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -112,6 +117,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_cached_video.py b/hydrogram/types/inline_mode/inline_query_result_cached_video.py index 7281a0bda..0c9252a5c 100644 --- a/hydrogram/types/inline_mode/inline_query_result_cached_video.py +++ b/hydrogram/types/inline_mode/inline_query_result_cached_video.py @@ -57,6 +57,9 @@ class InlineQueryResultCachedVideo(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the video. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -73,6 +76,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -84,6 +88,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -115,6 +120,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_cached_voice.py b/hydrogram/types/inline_mode/inline_query_result_cached_voice.py index 4f1aec899..9136c0c96 100644 --- a/hydrogram/types/inline_mode/inline_query_result_cached_voice.py +++ b/hydrogram/types/inline_mode/inline_query_result_cached_voice.py @@ -54,6 +54,9 @@ class InlineQueryResultCachedVoice(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the voice message. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -69,6 +72,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -79,6 +83,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -109,6 +114,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_document.py b/hydrogram/types/inline_mode/inline_query_result_document.py index 3c00f5e2e..9967b90b6 100644 --- a/hydrogram/types/inline_mode/inline_query_result_document.py +++ b/hydrogram/types/inline_mode/inline_query_result_document.py @@ -56,6 +56,9 @@ class InlineQueryResultDocument(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the document. + description (``str``, *optional*): Short description of the result. @@ -84,6 +87,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, description: str = "", reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, @@ -99,6 +103,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.description = description self.thumb_url = thumb_url self.thumb_width = thumb_width @@ -144,6 +149,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_photo.py b/hydrogram/types/inline_mode/inline_query_result_photo.py index 096fd6d20..e1c53dced 100644 --- a/hydrogram/types/inline_mode/inline_query_result_photo.py +++ b/hydrogram/types/inline_mode/inline_query_result_photo.py @@ -67,6 +67,9 @@ class InlineQueryResultPhoto(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the photo. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): An InlineKeyboardMarkup object. @@ -86,6 +89,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -100,6 +104,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.reply_markup = reply_markup self.input_message_content = input_message_content @@ -142,6 +147,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_video.py b/hydrogram/types/inline_mode/inline_query_result_video.py index 80367b90e..c18854e6e 100644 --- a/hydrogram/types/inline_mode/inline_query_result_video.py +++ b/hydrogram/types/inline_mode/inline_query_result_video.py @@ -72,6 +72,9 @@ class InlineQueryResultVideo(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the video. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): Inline keyboard attached to the message @@ -94,6 +97,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -109,6 +113,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.mime_type = mime_type async def write(self, client: hydrogram.Client): @@ -151,6 +156,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/inline_mode/inline_query_result_voice.py b/hydrogram/types/inline_mode/inline_query_result_voice.py index e04289b17..db1c1559c 100644 --- a/hydrogram/types/inline_mode/inline_query_result_voice.py +++ b/hydrogram/types/inline_mode/inline_query_result_voice.py @@ -56,6 +56,9 @@ class InlineQueryResultVoice(InlineQueryResult): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (:obj:`bool`, *optional*): + Wether the caption should be shown above the voice message. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup`, *optional*): Inline keyboard attached to the message. @@ -72,6 +75,7 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, reply_markup: types.InlineKeyboardMarkup = None, input_message_content: types.InputMessageContent = None, ): @@ -83,6 +87,7 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media async def write(self, client: hydrogram.Client): audio = raw.types.InputWebDocument( @@ -117,6 +122,7 @@ async def write(self, client: hydrogram.Client): else None, message=message, entities=entities, + invert_media=self.show_caption_above_media, ) ), ) diff --git a/hydrogram/types/input_media/input_media.py b/hydrogram/types/input_media/input_media.py index 8d937de6e..67e4dc558 100644 --- a/hydrogram/types/input_media/input_media.py +++ b/hydrogram/types/input_media/input_media.py @@ -24,6 +24,7 @@ from hydrogram.types.object import Object if TYPE_CHECKING: + from hydrogram import enums from hydrogram.types.messages_and_media import MessageEntity @@ -43,8 +44,9 @@ def __init__( self, media: str | BinaryIO, caption: str = "", - parse_mode: str | None = None, + parse_mode: enums.ParseMode | None = None, caption_entities: list[MessageEntity] | None = None, + show_caption_above_media: bool | None = None, ): super().__init__() @@ -52,3 +54,4 @@ def __init__( self.caption = caption self.parse_mode = parse_mode self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media diff --git a/hydrogram/types/input_media/input_media_animation.py b/hydrogram/types/input_media/input_media_animation.py index b873a0fb8..366a9b9ca 100644 --- a/hydrogram/types/input_media/input_media_animation.py +++ b/hydrogram/types/input_media/input_media_animation.py @@ -56,6 +56,9 @@ class InputMediaAnimation(InputMedia): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the animation. + width (``int``, *optional*): Animation width. @@ -76,12 +79,13 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[MessageEntity] | None = None, + show_caption_above_media: bool | None = None, width: int = 0, height: int = 0, duration: int = 0, has_spoiler: bool | None = None, ): - super().__init__(media, caption, parse_mode, caption_entities) + super().__init__(media, caption, parse_mode, caption_entities, show_caption_above_media) self.thumb = thumb self.width = width diff --git a/hydrogram/types/input_media/input_media_audio.py b/hydrogram/types/input_media/input_media_audio.py index 231e70071..e82d45dba 100644 --- a/hydrogram/types/input_media/input_media_audio.py +++ b/hydrogram/types/input_media/input_media_audio.py @@ -58,6 +58,9 @@ class InputMediaAudio(InputMedia): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the audio. + duration (``int``, *optional*): Duration of the audio in seconds @@ -75,11 +78,12 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[MessageEntity] | None = None, + show_caption_above_media: bool | None = None, duration: int = 0, performer: str = "", title: str = "", ): - super().__init__(media, caption, parse_mode, caption_entities) + super().__init__(media, caption, parse_mode, caption_entities, show_caption_above_media) self.thumb = thumb self.duration = duration diff --git a/hydrogram/types/input_media/input_media_document.py b/hydrogram/types/input_media/input_media_document.py index b9089263c..cf66b8340 100644 --- a/hydrogram/types/input_media/input_media_document.py +++ b/hydrogram/types/input_media/input_media_document.py @@ -55,6 +55,9 @@ class InputMediaDocument(InputMedia): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the document. """ def __init__( @@ -64,7 +67,8 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[MessageEntity] | None = None, + show_caption_above_media: bool | None = None, ): - super().__init__(media, caption, parse_mode, caption_entities) + super().__init__(media, caption, parse_mode, caption_entities, show_caption_above_media) self.thumb = thumb diff --git a/hydrogram/types/input_media/input_media_photo.py b/hydrogram/types/input_media/input_media_photo.py index c00483efb..329df99da 100644 --- a/hydrogram/types/input_media/input_media_photo.py +++ b/hydrogram/types/input_media/input_media_photo.py @@ -51,6 +51,9 @@ class InputMediaPhoto(InputMedia): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the photo. + has_spoiler (``bool``, *optional*): Pass True if the photo needs to be covered with a spoiler animation. """ @@ -61,8 +64,9 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[MessageEntity] | None = None, + show_caption_above_media: bool | None = None, has_spoiler: bool | None = None, ): - super().__init__(media, caption, parse_mode, caption_entities) + super().__init__(media, caption, parse_mode, caption_entities, show_caption_above_media) self.has_spoiler = has_spoiler diff --git a/hydrogram/types/input_media/input_media_video.py b/hydrogram/types/input_media/input_media_video.py index df92616bd..638bb793c 100644 --- a/hydrogram/types/input_media/input_media_video.py +++ b/hydrogram/types/input_media/input_media_video.py @@ -57,6 +57,9 @@ class InputMediaVideo(InputMedia): caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Wether the caption should be shown above the video. + width (``int``, *optional*): Video width. @@ -71,6 +74,10 @@ class InputMediaVideo(InputMedia): has_spoiler (``bool``, *optional*): Pass True if the photo needs to be covered with a spoiler animation. + + no_sound (``bool``, *optional*): + Pass True if the video you are uploading is a video message with no sound. + Does not work for external links. """ def __init__( @@ -80,13 +87,15 @@ def __init__( caption: str = "", parse_mode: enums.ParseMode | None = None, caption_entities: list[MessageEntity] | None = None, + show_caption_above_media: bool | None = None, width: int = 0, height: int = 0, duration: int = 0, supports_streaming: bool = True, has_spoiler: bool | None = None, + no_sound: bool | None = None, ): - super().__init__(media, caption, parse_mode, caption_entities) + super().__init__(media, caption, parse_mode, caption_entities, show_caption_above_media) self.thumb = thumb self.width = width @@ -94,3 +103,4 @@ def __init__( self.duration = duration self.supports_streaming = supports_streaming self.has_spoiler = has_spoiler + self.no_sound = no_sound diff --git a/hydrogram/types/input_message_content/__init__.py b/hydrogram/types/input_message_content/__init__.py index 48927523a..c2b8a45f5 100644 --- a/hydrogram/types/input_message_content/__init__.py +++ b/hydrogram/types/input_message_content/__init__.py @@ -18,6 +18,7 @@ # along with Hydrogram. If not, see . from .input_message_content import InputMessageContent +from .input_poll_option import InputPollOption from .input_text_message_content import InputTextMessageContent -__all__ = ["InputMessageContent", "InputTextMessageContent"] +__all__ = ["InputMessageContent", "InputPollOption", "InputTextMessageContent"] diff --git a/hydrogram/types/input_message_content/input_poll_option.py b/hydrogram/types/input_message_content/input_poll_option.py new file mode 100644 index 000000000..3122d7ef2 --- /dev/null +++ b/hydrogram/types/input_message_content/input_poll_option.py @@ -0,0 +1,57 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from hydrogram.types.object import Object + +if TYPE_CHECKING: + import hydrogram + + +class InputPollOption(Object): + """This object contains information about one answer option in a poll to send. + + Parameters: + text (``str``): + Option text, 1-100 characters + + text_parse_mode (:obj:`~hydrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + Currently, only custom emoji entities are allowed. + + text_entities (List of :obj:`~hydrogram.types.MessageEntity`, *optional*): + List of special entities that appear in the poll option text, which can be specified instead of *text_parse_mode*. + + """ + + def __init__( + self, + *, + text: str, + text_parse_mode: hydrogram.enums.ParseMode = None, + text_entities: list[hydrogram.types.MessageEntity] | None = None, + ): + super().__init__() + + self.text = text + self.text_parse_mode = text_parse_mode + self.text_entities = text_entities diff --git a/hydrogram/types/messages_and_media/location.py b/hydrogram/types/messages_and_media/location.py index db3cb29f6..e6041efc4 100644 --- a/hydrogram/types/messages_and_media/location.py +++ b/hydrogram/types/messages_and_media/location.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +from __future__ import annotations import hydrogram from hydrogram import raw @@ -31,16 +32,32 @@ class Location(Object): latitude (``float``): Latitude as defined by sender. + + horizontal_accuracy (``int``, *optional*): + The estimated horizontal accuracy of the location, in meters, as defined by sender. """ - def __init__(self, *, client: "hydrogram.Client" = None, longitude: float, latitude: float): + def __init__( + self, + *, + client: hydrogram.Client = None, + longitude: float, + latitude: float, + horizontal_accuracy: int | None, + ): super().__init__(client) self.longitude = longitude self.latitude = latitude + self.horizontal_accuracy = horizontal_accuracy @staticmethod - def _parse(client, geo_point: "raw.types.GeoPoint") -> "Location": + def _parse(client, geo_point: raw.types.GeoPoint) -> Location: if isinstance(geo_point, raw.types.GeoPoint): - return Location(longitude=geo_point.long, latitude=geo_point.lat, client=client) + return Location( + longitude=geo_point.long, + latitude=geo_point.lat, + horizontal_accuracy=geo_point.accuracy_radius, + client=client, + ) return None diff --git a/hydrogram/types/messages_and_media/message.py b/hydrogram/types/messages_and_media/message.py index 23fa58c28..ca43faeb9 100644 --- a/hydrogram/types/messages_and_media/message.py +++ b/hydrogram/types/messages_and_media/message.py @@ -21,7 +21,7 @@ import logging from functools import partial -from typing import TYPE_CHECKING, BinaryIO, Callable +from typing import TYPE_CHECKING, BinaryIO import hydrogram from hydrogram import enums, filters, raw, types, utils @@ -33,6 +33,7 @@ from hydrogram.types.update import Update if TYPE_CHECKING: + from collections.abc import Callable from datetime import datetime from re import Match @@ -170,6 +171,9 @@ class Message(Object, Update): For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption. + show_caption_above_media (``bool``, *optional*): + Message's caption should be shown above the media. + audio (:obj:`~hydrogram.types.Audio`, *optional*): Message is an audio file, information about the file. @@ -378,6 +382,7 @@ def __init__( text: Str = None, entities: list[types.MessageEntity] | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, audio: types.Audio = None, document: types.Document = None, photo: types.Photo = None, @@ -462,6 +467,7 @@ def __init__( self.text = text self.entities = entities self.caption_entities = caption_entities + self.show_caption_above_media = show_caption_above_media self.audio = audio self.document = document self.photo = photo @@ -756,7 +762,7 @@ async def _parse( except MessageIdsEmpty: pass - client.message_cache[(parsed_message.chat.id, parsed_message.id)] = parsed_message + client.message_cache[parsed_message.chat.id, parsed_message.id] = parsed_message if message.reply_to and message.reply_to.forum_topic: if message.reply_to.reply_to_top_id: @@ -770,7 +776,8 @@ async def _parse( if isinstance(message, raw.types.Message): message_thread_id = None entities = [ - types.MessageEntity._parse(client, entity, users) for entity in message.entities + types.MessageEntity._parse(client, entity, users) + for entity in (message.entities or []) ] entities = types.List(filter(lambda x: x is not None, entities)) @@ -990,28 +997,29 @@ async def _parse( ) if message.reply_to: - if message.reply_to.forum_topic: - if message.reply_to.reply_to_top_id: - thread_id = message.reply_to.reply_to_top_id - parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id - else: - thread_id = message.reply_to.reply_to_msg_id - parsed_message.message_thread_id = thread_id - parsed_message.is_topic_message = True - if topics: - parsed_message.topics = types.ForumTopic._parse(topics[thread_id]) + if isinstance(message.reply_to, raw.types.MessageReplyHeader): + if message.reply_to.forum_topic: + if message.reply_to.reply_to_top_id: + thread_id = message.reply_to.reply_to_top_id + parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id + else: + thread_id = message.reply_to.reply_to_msg_id + parsed_message.message_thread_id = thread_id + parsed_message.is_topic_message = True + if topics: + parsed_message.topics = types.ForumTopic._parse(topics[thread_id]) + else: + try: + msg = await client.get_messages(parsed_message.chat.id, message.id) + if getattr(msg, "topics"): + parsed_message.topics = msg.topics + except Exception: + pass else: - try: - msg = await client.get_messages(parsed_message.chat.id, message.id) - if getattr(msg, "topics"): - parsed_message.topics = msg.topics - except Exception: - pass - else: - parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id - parsed_message.reply_to_top_message_id = message.reply_to.reply_to_top_id - if isinstance(message.reply_to, raw.types.MessageReplyStoryHeader): - parsed_message.reply_to_message_id = message.reply_to.story_id + parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id + parsed_message.reply_to_top_message_id = message.reply_to.reply_to_top_id + if isinstance(message.reply_to, raw.types.MessageReplyStoryHeader): + parsed_message.reply_to_message_id = message.reply_to.story_id if replies: try: @@ -1030,7 +1038,7 @@ async def _parse( pass if not parsed_message.poll: # Do not cache poll messages - client.message_cache[(parsed_message.chat.id, parsed_message.id)] = parsed_message + client.message_cache[parsed_message.chat.id, parsed_message.id] = parsed_message return parsed_message return None @@ -1648,7 +1656,9 @@ async def reply_cached_media( .. code-block:: python await client.send_cached_media( - chat_id=message.chat.id, message_thread_id=message.message_thread_id, file_id=file_id + chat_id=message.chat.id, + message_thread_id=message.message_thread_id, + file_id=file_id, ) Example: @@ -1860,7 +1870,9 @@ async def reply_document( .. code-block:: python await client.send_document( - chat_id=message.chat.id, message_thread_id=message.message_thread_id, document=document + chat_id=message.chat.id, + message_thread_id=message.message_thread_id, + document=document, ) Example: @@ -2375,7 +2387,9 @@ async def reply_photo( async def reply_poll( self, question: str, - options: list[str], + options: list[types.InputPollOption], + question_parse_mode: enums.ParseMode = None, + question_entities: list[types.MessageEntity] | None = None, is_anonymous: bool = True, type: enums.PollType = enums.PollType.REGULAR, allows_multiple_answers: bool | None = None, @@ -2404,22 +2418,39 @@ async def reply_poll( await client.send_poll( chat_id=message.chat.id, - message_thread_id=message.message_thread_id, question="This is a poll", - options=["A", "B", "C] + options=[ + InputPollOption(text="A"), + InputPollOption(text="B"), + InputPollOption(text="C"), + ], ) Example: .. code-block:: python - await message.reply_poll("This is a poll", ["A", "B", "C"]) + await message.reply_poll( + question="This is a poll", + options=[ + InputPollOption(text="A"), + InputPollOption(text="B"), + InputPollOption(text="C"), + ], + ) Parameters: question (``str``): Poll question, 1-255 characters. - options (List of ``str``): - List of answer options, 2-10 strings 1-100 characters each. + options (List of :obj:`~hydrogram.types.InputPollOption`): + List of answer options, 2-10 answer options, 1-100 characters for each option. + + question_parse_mode (:obj:`~hydrogram.enums.ParseMode`, *optional*): + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + question_entities (List of :obj:`~hydrogram.types.MessageEntity`): + List of special entities that appear in the poll question, which can be specified instead of *question_parse_mode*. is_anonymous (``bool``, *optional*): True, if the poll needs to be anonymous. @@ -2500,6 +2531,8 @@ async def reply_poll( message_thread_id=self.message_thread_id, question=question, options=options, + question_parse_mode=question_parse_mode, + question_entities=question_entities, is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, @@ -2537,7 +2570,9 @@ async def reply_sticker( .. code-block:: python await client.send_sticker( - chat_id=message.chat.id, message_thread_id=message.message_thread_id, sticker=sticker + chat_id=message.chat.id, + message_thread_id=message.message_thread_id, + sticker=sticker, ) Example: @@ -2729,6 +2764,7 @@ async def reply_video( supports_streaming: bool = True, disable_notification: bool | None = None, reply_to_message_id: int | None = None, + no_sound: bool | None = False, reply_markup: types.InlineKeyboardMarkup | types.ReplyKeyboardMarkup | types.ReplyKeyboardRemove @@ -2806,6 +2842,10 @@ async def reply_video( reply_to_message_id (``int``, *optional*): If the message is a reply, ID of the original message. + no_sound (``bool``, *optional*): + Pass True if the video you are uploading is a video message with no sound. + Does not work for external links. + reply_markup (:obj:`~hydrogram.types.InlineKeyboardMarkup` | :obj:`~hydrogram.types.ReplyKeyboardMarkup` | :obj:`~hydrogram.types.ReplyKeyboardRemove` | :obj:`~hydrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -2862,6 +2902,7 @@ async def reply_video( supports_streaming=supports_streaming, disable_notification=disable_notification, reply_to_message_id=reply_to_message_id, + no_sound=no_sound, reply_markup=reply_markup, progress=progress, progress_args=progress_args, @@ -3346,6 +3387,7 @@ async def copy( message_thread_id: int | None = None, parse_mode: enums.ParseMode | None = None, caption_entities: list[types.MessageEntity] | None = None, + show_caption_above_media: bool | None = None, disable_notification: bool | None = None, reply_to_message_id: int | None = None, schedule_date: datetime | None = None, @@ -3392,6 +3434,9 @@ async def copy( caption_entities (List of :obj:`~hydrogram.types.MessageEntity`): List of special entities that appear in the new caption, which can be specified instead of *parse_mode*. + show_caption_above_media (``bool``, *optional*): + Pass True if the caption should be shown above the media. + disable_notification (``bool``, *optional*): Sends the message silently. Users will receive a notification with no sound. @@ -3540,6 +3585,7 @@ async def copy( caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, message_thread_id=message_thread_id, ) raise ValueError("Can't copy this message") @@ -3600,7 +3646,9 @@ async def click( .. code-block:: python - await client.send_message(chat_id=message.chat.id, text=message.reply_markup[i][j].text) + await client.send_message( + chat_id=message.chat.id, text=message.reply_markup[i][j].text + ) Example: This method can be used in three different ways: diff --git a/hydrogram/types/messages_and_media/poll.py b/hydrogram/types/messages_and_media/poll.py index dd304dfb7..6f20405df 100644 --- a/hydrogram/types/messages_and_media/poll.py +++ b/hydrogram/types/messages_and_media/poll.py @@ -129,12 +129,12 @@ def _parse( correct_option_id = None options = [] + results_by_option = {result.option: result for result in results or []} for i, answer in enumerate(poll.answers): voter_count = 0 - if results: - result = results[i] - voter_count = result.voters + if (result := results_by_option.get(answer.option)) is not None: + voter_count = result.voters or 0 if result.chosen: chosen_option_id = i @@ -144,7 +144,7 @@ def _parse( options.append( types.PollOption( - text=answer.text, + text=answer.text.text, voter_count=voter_count, data=answer.option, client=client, @@ -153,9 +153,9 @@ def _parse( return Poll( id=str(poll.id), - question=poll.question, + question=poll.question.text, options=options, - total_voter_count=media_poll.results.total_voters, + total_voter_count=media_poll.results.total_voters or 0, is_closed=poll.closed, is_anonymous=not poll.public_voters, type=enums.PollType.QUIZ if poll.quiz else enums.PollType.REGULAR, @@ -178,7 +178,7 @@ def _parse_update(client, update: raw.types.UpdateMessagePoll): if update.poll is not None: return Poll._parse(client, update) - results = update.results.results + results = update.results.results or [] chosen_option_id = None correct_option_id = None options = [] @@ -193,7 +193,7 @@ def _parse_update(client, update: raw.types.UpdateMessagePoll): options.append( types.PollOption( text="", - voter_count=result.voters, + voter_count=result.voters or 0, data=result.option, client=client, ) @@ -203,7 +203,7 @@ def _parse_update(client, update: raw.types.UpdateMessagePoll): id=str(update.poll_id), question="", options=options, - total_voter_count=update.results.total_voters, + total_voter_count=update.results.total_voters or 0, is_closed=False, chosen_option_id=chosen_option_id, correct_option_id=correct_option_id, diff --git a/hydrogram/types/messages_and_media/reaction.py b/hydrogram/types/messages_and_media/reaction.py index 57b2d69af..982a32088 100644 --- a/hydrogram/types/messages_and_media/reaction.py +++ b/hydrogram/types/messages_and_media/reaction.py @@ -40,6 +40,9 @@ class Reaction(Object): chosen_order (``int``, *optional*): Chosen reaction order. Available for chosen reactions. + + is_paid (``bool``, *optional*): + True, if the reaction is a paid star reaction. """ def __init__( @@ -50,6 +53,7 @@ def __init__( custom_emoji_id: int | None = None, count: int | None = None, chosen_order: int | None = None, + is_paid: bool | None = None, ): super().__init__(client) @@ -57,6 +61,7 @@ def __init__( self.custom_emoji_id = custom_emoji_id self.count = count self.chosen_order = chosen_order + self.is_paid = is_paid @staticmethod def _parse(client: hydrogram.Client, reaction: raw.base.Reaction) -> Reaction: @@ -65,7 +70,11 @@ def _parse(client: hydrogram.Client, reaction: raw.base.Reaction) -> Reaction: if isinstance(reaction, raw.types.ReactionCustomEmoji): return Reaction(client=client, custom_emoji_id=reaction.document_id) - return None + + if isinstance(reaction, raw.types.ReactionPaid): + return Reaction(client=client, is_paid=True) + + return Reaction(client=client) @staticmethod def _parse_count(client: hydrogram.Client, reaction_count: raw.base.ReactionCount) -> Reaction: diff --git a/hydrogram/types/messages_and_media/sticker.py b/hydrogram/types/messages_and_media/sticker.py index 8432fd408..29e5fa2f4 100644 --- a/hydrogram/types/messages_and_media/sticker.py +++ b/hydrogram/types/messages_and_media/sticker.py @@ -137,7 +137,7 @@ async def _get_sticker_set_name(invoke, input_sticker_set_id): ) ).set.short_name - Sticker.cache[(set_id, set_access_hash)] = name + Sticker.cache[set_id, set_access_hash] = name if len(Sticker.cache) > 250: for _ in range(50): diff --git a/hydrogram/types/messages_and_media/thumbnail.py b/hydrogram/types/messages_and_media/thumbnail.py index ef29d2a91..efc1adfd8 100644 --- a/hydrogram/types/messages_and_media/thumbnail.py +++ b/hydrogram/types/messages_and_media/thumbnail.py @@ -79,7 +79,7 @@ def _parse(client, media: raw.types.Photo | raw.types.Document) -> list[Thumbnai file_type = FileType.PHOTO elif isinstance(media, raw.types.Document): - raw_thumbs = media.thumbs + raw_thumbs = media.thumbs or [] file_type = FileType.THUMBNAIL else: return None diff --git a/hydrogram/types/pyromod/listener.py b/hydrogram/types/pyromod/listener.py index f0945aa18..f7ac9f345 100644 --- a/hydrogram/types/pyromod/listener.py +++ b/hydrogram/types/pyromod/listener.py @@ -18,8 +18,9 @@ # along with Hydrogram. If not, see . from asyncio import Future +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING from .identifier import Identifier from .listener_types import ListenerTypes diff --git a/hydrogram/types/user_and_chats/chat.py b/hydrogram/types/user_and_chats/chat.py index e8d291b7f..fa6065972 100644 --- a/hydrogram/types/user_and_chats/chat.py +++ b/hydrogram/types/user_and_chats/chat.py @@ -246,14 +246,17 @@ def _parse_user_chat(client, user: raw.types.User) -> Chat: is_support=getattr(user, "support", None), username=user.usernames[0].username if user.usernames else user.username, active_usernames=types.List([ - username.username for username in user.usernames if username.active + username.username for username in (user.usernames or []) if username.active ]) or None, - usernames=types.List([types.Username._parse(r) for r in user.usernames]) or None, + usernames=types.List([types.Username._parse(r) for r in (user.usernames or [])]) + or None, first_name=user.first_name, last_name=user.last_name, photo=types.ChatPhoto._parse(client, user.photo, peer_id, user.access_hash), - restrictions=types.List([types.Restriction._parse(r) for r in user.restriction_reason]) + restrictions=types.List([ + types.Restriction._parse(r) for r in (user.restriction_reason or []) + ]) or None, dc_id=getattr(getattr(user, "photo", None), "dc_id", None), client=client, @@ -264,6 +267,9 @@ def _parse_chat_chat(client, chat: raw.types.Chat) -> Chat: peer_id = -chat.id usernames = getattr(chat, "usernames", []) + if isinstance(chat, raw.types.ChatForbidden): + return Chat(id=peer_id, type=enums.ChatType.GROUP, title=chat.title, client=client) + return Chat( id=peer_id, type=enums.ChatType.GROUP, @@ -282,6 +288,14 @@ def _parse_chat_chat(client, chat: raw.types.Chat) -> Chat: def _parse_channel_chat(client, channel: raw.types.Channel) -> Chat: peer_id = utils.get_channel_id(channel.id) + if isinstance(channel, raw.types.ChannelForbidden): + return Chat( + id=peer_id, + type=enums.ChatType.SUPERGROUP if channel.megagroup else enums.ChatType.CHANNEL, + title=channel.title, + client=client, + ) + return Chat( id=peer_id, type=enums.ChatType.SUPERGROUP if channel.megagroup else enums.ChatType.CHANNEL, @@ -295,14 +309,15 @@ def _parse_channel_chat(client, channel: raw.types.Channel) -> Chat: username=channel.username, photo=types.ChatPhoto._parse(client, channel.photo, peer_id, channel.access_hash), restrictions=types.List([ - types.Restriction._parse(r) for r in channel.restriction_reason + types.Restriction._parse(r) for r in (channel.restriction_reason or []) ]) or None, permissions=types.ChatPermissions._parse(channel.default_banned_rights), members_count=channel.participants_count, dc_id=getattr(channel.photo, "dc_id", None), has_protected_content=channel.noforwards, - usernames=types.List([types.Username._parse(r) for r in channel.usernames]) or None, + usernames=types.List([types.Username._parse(r) for r in (channel.usernames or [])]) + or None, client=client, ) diff --git a/hydrogram/types/user_and_chats/chat_preview.py b/hydrogram/types/user_and_chats/chat_preview.py index 9e49bc75c..97fc5c1b9 100644 --- a/hydrogram/types/user_and_chats/chat_preview.py +++ b/hydrogram/types/user_and_chats/chat_preview.py @@ -75,7 +75,8 @@ def _parse(client, chat_invite: raw.types.ChatInvite) -> ChatPreview: ), members_count=chat_invite.participants_count, photo=types.Photo._parse(client, chat_invite.photo), - members=[types.User._parse(client, user) for user in chat_invite.participants] or None, + members=[types.User._parse(client, user) for user in (chat_invite.participants or [])] + or None, client=client, ) diff --git a/hydrogram/types/user_and_chats/emoji_status.py b/hydrogram/types/user_and_chats/emoji_status.py index b4ea688ad..caca54e66 100644 --- a/hydrogram/types/user_and_chats/emoji_status.py +++ b/hydrogram/types/user_and_chats/emoji_status.py @@ -33,44 +33,97 @@ class EmojiStatus(Object): """A user emoji status. Parameters: - custom_emoji_id (``int``): + custom_emoji_id (``int``, *optional*): Custom emoji id. until_date (:py:obj:`~datetime.datetime`, *optional*): Valid until date. + + title (``str``, *optional*): + Title of the collectible. + + gift_id (``int``, *optional*): + Gift collectible id. + + name (``str``, *optional*): + Name of the collectible. + + pattern_custom_emoji_id (``int``, *optional*): + Pattern emoji id. + + center_color (``int``, *optional*): + Center color of the collectible emoji in decimal format. + + edge_color (``int``, *optional*): + Edge color of the collectible emoji in decimal format. + + pattern_color (``int``, *optional*): + Pattern color of the collectible emoji in decimal format. + + text_color (``int``, *optional*): + Text color of the collectible emoji in decimal format. """ def __init__( self, *, client: hydrogram.Client = None, - custom_emoji_id: int, + custom_emoji_id: int | None = None, + gift_id: int | None = None, until_date: datetime | None = None, + title: str | None = None, + name: str | None = None, + pattern_custom_emoji_id: int | None = None, + center_color: int | None = None, + edge_color: int | None = None, + pattern_color: int | None = None, + text_color: int | None = None, ): super().__init__(client) self.custom_emoji_id = custom_emoji_id + self.gift_id = gift_id self.until_date = until_date + self.title = title + self.name = name + self.pattern_custom_emoji_id = pattern_custom_emoji_id + self.center_color = center_color + self.edge_color = edge_color + self.pattern_color = pattern_color + self.text_color = text_color @staticmethod def _parse(client, emoji_status: raw.base.EmojiStatus) -> EmojiStatus | None: if isinstance(emoji_status, raw.types.EmojiStatus): - return EmojiStatus(client=client, custom_emoji_id=emoji_status.document_id) + return EmojiStatus( + client=client, + custom_emoji_id=emoji_status.document_id, + until_date=utils.timestamp_to_datetime(getattr(emoji_status, "until", None)), + ) - if isinstance(emoji_status, raw.types.EmojiStatusUntil): + if isinstance(emoji_status, raw.types.EmojiStatusCollectible): return EmojiStatus( client=client, custom_emoji_id=emoji_status.document_id, - until_date=utils.timestamp_to_datetime(emoji_status.until), + gift_id=emoji_status.collectible_id, + until_date=utils.timestamp_to_datetime(getattr(emoji_status, "until", None)), + title=emoji_status.title, + name=emoji_status.slug, + pattern_custom_emoji_id=emoji_status.pattern_document_id, + center_color=emoji_status.center_color, + edge_color=emoji_status.edge_color, + pattern_color=emoji_status.pattern_color, + text_color=emoji_status.text_color, ) return None def write(self): - if self.until_date: - return raw.types.EmojiStatusUntil( - document_id=self.custom_emoji_id, - until=utils.datetime_to_timestamp(self.until_date), + if self.gift_id: + return raw.types.InputEmojiStatusCollectible( + collectible_id=self.gift_id, until=utils.datetime_to_timestamp(self.until_date) ) - return raw.types.EmojiStatus(document_id=self.custom_emoji_id) + return raw.types.EmojiStatus( + document_id=self.custom_emoji_id, until=utils.datetime_to_timestamp(self.until_date) + ) diff --git a/hydrogram/types/user_and_chats/user.py b/hydrogram/types/user_and_chats/user.py index f8daa4cf3..3914adb4c 100644 --- a/hydrogram/types/user_and_chats/user.py +++ b/hydrogram/types/user_and_chats/user.py @@ -253,16 +253,19 @@ def _parse(client, user: raw.base.User) -> User | None: **User._parse_status(user.status, user.bot), username=user.usernames[0].username if user.usernames else user.username, active_usernames=types.List([ - username.username for username in user.usernames if username.active + username.username for username in (user.usernames or []) if username.active ]) or None, - usernames=types.List([types.Username._parse(r) for r in user.usernames]) or None, + usernames=types.List([types.Username._parse(r) for r in (user.usernames or [])]) + or None, language_code=user.lang_code, emoji_status=types.EmojiStatus._parse(client, user.emoji_status), dc_id=getattr(user.photo, "dc_id", None), phone_number=user.phone, photo=types.ChatPhoto._parse(client, user.photo, user.id, user.access_hash), - restrictions=types.List([types.Restriction._parse(r) for r in user.restriction_reason]) + restrictions=types.List([ + types.Restriction._parse(r) for r in (user.restriction_reason or []) + ]) or None, client=client, ) diff --git a/hydrogram/update_manager.py b/hydrogram/update_manager.py new file mode 100644 index 000000000..b97d65716 --- /dev/null +++ b/hydrogram/update_manager.py @@ -0,0 +1,495 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +import contextlib +import logging +from collections import OrderedDict +from hashlib import sha256 +from typing import TYPE_CHECKING + +from hydrogram import raw, utils +from hydrogram.errors import ( + ChannelInvalid, + ChannelPrivate, + ChannelPublicGroupNa, + PersistentTimestampInvalid, + UserNotParticipant, +) + +if TYPE_CHECKING: + from hydrogram import Client + from hydrogram.raw.core import TLObject + +log = logging.getLogger(__name__) + + +class UpdateGapError(RuntimeError): + """The server has not yet supplied a contiguous difference.""" + + +class UpdateManager: + """Serialize live updates and differences without blocking the MTProto reader. + + Only this worker owns the cursors. RPC responses must remain free to reach + Session.results while a difference request is in flight. + """ + + PAGE_LIMIT = 100 + TOTAL_LIMIT = 10000 + QUEUE_LIMIT = 1000 + MAX_PAGES = 100 + DISPATCH_LIMIT = 10000 + ZERO_PTS_CACHE_LIMIT = 2048 + + def __init__(self, client: Client): + self.client = client + self.states: dict[int, tuple[int, int, int, int]] = {} + self.queue: asyncio.Queue = asyncio.Queue(self.QUEUE_LIMIT) + self.task: asyncio.Task | None = None + self.accepting = True + self._inflight = False + self.recovery_requested = False + self.channel_gaps: set[int] = set() + self._zero_pts_seen: OrderedDict[bytes, None] = OrderedDict() + + async def start(self) -> None: + self.accepting = True + self.states = await self.client.storage.get_update_states() + if 0 not in self.states: + state = await self.client.invoke(raw.functions.updates.GetState()) + await self._set_common(state) + else: + self.recovery_requested = True + self.task = asyncio.create_task(self._run()) + + async def stop(self) -> None: + self.accepting = False + if self.task is not None: + self.task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self.task + self.task = None + while not self.queue.empty(): + self.queue.get_nowait() + self.queue.task_done() + + def submit(self, updates: TLObject) -> None: + if not self.accepting: + return + try: + self.queue.put_nowait(updates) + except asyncio.QueueFull: + # Ordered events can be fetched from the last cursor. Ephemeral + # events (typing, etc.) cannot be recovered by Telegram. + log.warning( + "Update buffer full; requesting a difference (ephemeral updates may be lost)" + ) + self.recovery_requested = True + + def request_recovery(self) -> None: + if self.accepting and not self.recovery_requested: + self.recovery_requested = True + with contextlib.suppress(asyncio.QueueFull): + self.queue.put_nowait(None) + + async def _run(self) -> None: + try: + await self._work() + finally: + if self._inflight: + self.queue.task_done() + self._inflight = False + + async def _work(self) -> None: + pending = None + delay = 1 + while True: + try: + if self.recovery_requested: + # Clear before the request so a reconnect during recovery + # cannot be overwritten when the request finishes. + self.recovery_requested = False + try: + await self._difference() + except Exception: + self.recovery_requested = True + raise + if pending is None: + for channel_id in tuple(self.channel_gaps): + await self._channel_difference(channel_id) + self.channel_gaps.discard(channel_id) + await self._backpressure() + if pending is None: + pending = await self.queue.get() + self._inflight = True + await self.process(pending) + self.queue.task_done() + self._inflight = False + pending = None + delay = 1 + except Exception: + log.exception( + "Update recovery failed; retaining cursor and retrying in %s seconds", delay + ) + await asyncio.sleep(delay) + delay = min(delay * 2, 60) + + async def _save(self, channel_id: int) -> None: + # Keep a failed checkpoint ahead of all further update processing, so + # a storage error cannot replay an already dispatched batch in memory. + delay = 1 + while True: + task = asyncio.create_task( + self.client.storage.set_update_state(channel_id, *self.states[channel_id]) + ) + try: + await asyncio.shield(task) + return + except asyncio.CancelledError: + # Give an in-flight commit a bounded chance to finish. Never + # leave it running after storage.close() during shutdown. + try: + await asyncio.wait_for(task, timeout=5) + except (Exception, asyncio.CancelledError): + task.cancel() + with contextlib.suppress(Exception, asyncio.CancelledError): + await task + log.warning( + "Update checkpoint interrupted during shutdown; replay is possible" + ) + raise + except Exception: + log.exception("Could not save update state; retrying in %s seconds", delay) + await asyncio.sleep(delay) + delay = min(delay * 2, 60) + + async def _backpressure(self) -> None: + queue = self.client.dispatcher.updates_queue + if queue.qsize() >= self.DISPATCH_LIMIT: + await queue.join() + + async def _set_common(self, state: raw.types.updates.State) -> None: + self.states[0] = (state.pts, state.qts, state.date, state.seq) + await self._save(0) + + def _dispatch(self, update: TLObject, users: dict, chats: dict) -> None: + # Zero-count PTS events do not advance a cursor. Remember a bounded + # window of exact payloads to suppress retransmissions at the same PTS + # without dropping distinct read/content updates sharing that cursor. + if getattr(update, "pts_count", None) == 0: + key = sha256(update.write()).digest() + if key in self._zero_pts_seen: + return + self._zero_pts_seen[key] = None + if len(self._zero_pts_seen) > self.ZERO_PTS_CACHE_LIMIT: + self._zero_pts_seen.popitem(last=False) + self.client.dispatcher.updates_queue.put_nowait((update, users, chats)) + + @staticmethod + def _channel_id(update: TLObject) -> int: + # Bot participant/business updates can mention a channel while still + # belonging to the account-wide QTS sequence. + if getattr(update, "qts", None) is not None or ( + getattr(update, "pts", None) is None + and not isinstance(update, raw.types.UpdateChannelTooLong) + ): + return 0 + return getattr(update, "channel_id", 0) or getattr( + getattr(getattr(update, "message", None), "peer_id", None), "channel_id", 0 + ) + + async def _peers(self, updates: TLObject) -> tuple[dict, dict]: + users = getattr(updates, "users", []) + chats = getattr(updates, "chats", []) + await self.client.fetch_peers(users) + await self.client.fetch_peers(chats) + return {u.id: u for u in users}, {c.id: c for c in chats} + + async def process(self, updates: TLObject) -> None: + """Apply one container; called only by the worker (or isolated tests).""" + if isinstance(updates, raw.types.UpdatesTooLong): + await self._difference() + return + if isinstance(updates, (raw.types.UpdateShortMessage, raw.types.UpdateShortChatMessage)): + # Fetch the full messages and all accompanying updates, rather than + # just the first message of a potentially sliced difference. + if updates.pts > self.states[0][0]: + await self._difference() + if updates.pts > self.states[0][0]: + raise UpdateGapError("Short message is not yet present in the difference") + return + if isinstance(updates, raw.types.UpdateShortSentMessage): + await self._apply(updates, {}, {}, dispatch=False) + return + if isinstance(updates, raw.types.UpdateShort): + await self._apply(updates.update, {}, {}) + return + if not isinstance(updates, (raw.types.Updates, raw.types.UpdatesCombined)): + return + + users, chats = await self._peers(updates) + # PTS/QTS sequences are independent of the enclosing SEQ sequence. + # In particular an old container can contain a new channel update. + remaining = [] + for update in updates.updates: + if ( + getattr(update, "pts", None) is not None + or getattr(update, "qts", None) is not None + or isinstance(update, raw.types.UpdateChannelTooLong) + ): + await self._apply(update, users, chats) + else: + remaining.append(update) + + seq_start = getattr(updates, "seq_start", updates.seq) + if seq_start and self.states[0][3] + 1 < seq_start: + await self._difference() + pts, qts, date, seq = self.states[0] + if seq_start and seq + 1 > seq_start: + return + if seq_start and seq + 1 < seq_start: + raise UpdateGapError("Unfilled SEQ gap") + for update in remaining: + self._dispatch(update, users, chats) + self.states[0] = (pts, qts, max(date, updates.date), updates.seq or seq) + await self._save(0) + + async def _apply( + self, update: TLObject, users: dict, chats: dict, *, dispatch: bool = True + ) -> None: + channel_id = self._channel_id(update) + if isinstance(update, raw.types.UpdateChannelTooLong): + if channel_id not in self.states: + if update.pts is None: + try: + await self._channel_baseline(channel_id) + except ( + ChannelInvalid, + ChannelPrivate, + ChannelPublicGroupNa, + UserNotParticipant, + ): + log.warning( + "Cannot establish update state for inaccessible channel %s", channel_id + ) + return + else: + self.states[channel_id] = (update.pts, 0, 0, 0) + await self._save(channel_id) + self.channel_gaps.add(channel_id) + await self._channel_difference(channel_id) + self.channel_gaps.discard(channel_id) + return + + remote_pts = getattr(update, "pts", None) + remote_qts = getattr(update, "qts", None) + if remote_pts is None and remote_qts is None: + if dispatch: + self._dispatch(update, users, chats) + return + if channel_id not in self.states: + # The first observed channel event defines its starting point. Do + # not replay arbitrary history for a newly discovered channel. + self.states[channel_id] = (remote_pts - update.pts_count, 0, 0, 0) + index = 0 if remote_pts is not None else 1 + remote = remote_pts if index == 0 else remote_qts + count = getattr(update, "pts_count", 0) if index == 0 else 1 + local = self.states[channel_id][index] + if local + count < remote: + if channel_id: + self.channel_gaps.add(channel_id) + recovered = await self._channel_difference(channel_id) + self.channel_gaps.discard(channel_id) + if not recovered: + return + else: + await self._difference() + local = self.states[channel_id][index] + if local + count > remote: + return + if local + count < remote: + raise UpdateGapError("Unfilled PTS/QTS gap") + if dispatch: + self._dispatch(update, users, chats) + state = list(self.states[channel_id]) + state[index] = remote + self.states[channel_id] = tuple(state) + await self._save(channel_id) + + async def _reset_common(self) -> None: + state = await self.client.invoke(raw.functions.updates.GetState()) + # The lost common difference may have contained channel notifications. + # Reconcile known channels before committing the reset so a failed or + # cancelled reset will retry them on the next startup as well. + for channel_id in tuple(self.states): + if channel_id: + await self._channel_difference(channel_id) + await self._set_common(state) + + async def _difference(self) -> None: + for _ in range(self.MAX_PAGES): + before = self.states[0] + pts, qts, date, _seq = before + try: + diff = await self.client.invoke( + raw.functions.updates.GetDifference( + pts=pts, + qts=qts, + date=date, + pts_limit=self.TOTAL_LIMIT, + pts_total_limit=self.TOTAL_LIMIT, + qts_limit=self.TOTAL_LIMIT, + ) + ) + except PersistentTimestampInvalid: + log.warning( + "Telegram rejected the common update cursor; reconciling current state" + ) + await self._reset_common() + continue + if isinstance(diff, raw.types.updates.DifferenceEmpty): + self.states[0] = (pts, qts, diff.date, diff.seq) + await self._save(0) + return + if isinstance(diff, raw.types.updates.DifferenceTooLong): + log.warning( + "Telegram cannot replay the common update gap; reconciling to current state" + ) + await self._reset_common() + continue + + users, chats = await self._peers(diff) + # Channel cursors must not be replaced by the common PTS. Complete + # independent channel work first so failed requests cannot replay + # already dispatched common messages on the next attempt. + for update in diff.other_updates: + if self._channel_id(update): + await self._apply(update, users, chats) + state = getattr(diff, "state", None) or diff.intermediate_state + if state.pts < pts or state.qts < qts: + raise UpdateGapError("Common difference moved the cursor backwards") + if ( + isinstance(diff, raw.types.updates.DifferenceSlice) + and (state.pts, state.qts, state.date, state.seq) == before + ): + raise UpdateGapError("Common difference made no progress") + for message in diff.new_messages: + self._dispatch( + raw.types.UpdateNewMessage(message=message, pts=state.pts, pts_count=0), + users, + chats, + ) + for message in diff.new_encrypted_messages: + self._dispatch( + raw.types.UpdateNewEncryptedMessage(message=message, qts=state.qts), + users, + chats, + ) + for update in diff.other_updates: + if not self._channel_id(update): + self._dispatch(update, users, chats) + await self._set_common(state) + await self._backpressure() + if isinstance(diff, raw.types.updates.Difference): + return + if self.states[0] == before: + raise UpdateGapError("Common difference made no progress") + raise UpdateGapError( + "Common difference page budget exhausted; will resume from checkpoint" + ) + + async def _input_channel(self, channel_id: int) -> raw.types.InputChannel: + peer = await self.client.resolve_peer(utils.get_channel_id(channel_id)) + return raw.types.InputChannel(channel_id=channel_id, access_hash=peer.access_hash) + + async def _channel_baseline(self, channel_id: int) -> None: + full = await self.client.invoke( + raw.functions.channels.GetFullChannel(channel=await self._input_channel(channel_id)) + ) + self.states[channel_id] = (full.full_chat.pts, 0, 0, 0) + await self._save(channel_id) + + async def _channel_difference(self, channel_id: int) -> bool: + try: + await self._fetch_channel_difference(channel_id) + except (ChannelInvalid, ChannelPrivate, ChannelPublicGroupNa, UserNotParticipant): + log.warning("Cannot recover updates for inaccessible channel %s", channel_id) + return False + return True + + async def _fetch_channel_difference(self, channel_id: int) -> None: + channel = await self._input_channel(channel_id) + for _ in range(self.MAX_PAGES): + before = self.states[channel_id][0] + try: + diff = await self.client.invoke( + raw.functions.updates.GetChannelDifference( + channel=channel, + filter=raw.types.ChannelMessagesFilterEmpty(), + pts=before, + limit=self.PAGE_LIMIT, + ) + ) + except PersistentTimestampInvalid: + log.warning( + "Telegram rejected the update cursor for channel %s; reconciling state", + channel_id, + ) + await self._channel_baseline(channel_id) + continue + if isinstance(diff, raw.types.updates.ChannelDifferenceTooLong): + # messages is a history snapshot, not a stream of new events. + log.warning( + "Telegram cannot replay the update gap for channel %s; reconciling state", + channel_id, + ) + pts = diff.dialog.pts + else: + pts = diff.pts + if pts < before: + raise UpdateGapError("Channel difference moved the cursor backwards") + if pts == before and not diff.final: + raise UpdateGapError("Channel difference made no progress") + if not isinstance(diff, raw.types.updates.ChannelDifferenceEmpty): + users, chats = await self._peers(diff) + for message in diff.new_messages: + self._dispatch( + raw.types.UpdateNewChannelMessage( + message=message, pts=pts, pts_count=0 + ), + users, + chats, + ) + for update in diff.other_updates: + self._dispatch(update, users, chats) + if pts is None or pts < before: + raise UpdateGapError("Invalid channel difference cursor") + self.states[channel_id] = (pts, 0, 0, 0) + await self._save(channel_id) + await self._backpressure() + if diff.final: + return + if pts == before: + raise UpdateGapError("Channel difference made no progress") + raise UpdateGapError( + "Channel difference page budget exhausted; will resume from checkpoint" + ) diff --git a/hydrogram/utils.py b/hydrogram/utils.py index 73a65c5da..e5398b0d2 100644 --- a/hydrogram/utils.py +++ b/hydrogram/utils.py @@ -26,7 +26,7 @@ import os import struct from concurrent.futures.thread import ThreadPoolExecutor -from datetime import datetime, timezone +from datetime import UTC, datetime from getpass import getpass from types import SimpleNamespace @@ -43,11 +43,21 @@ ) +def validate_random_id(random_id: int | None) -> None: + """Validate a caller-supplied MTProto message ID without changing generation state.""" + if random_id is None: + return + if isinstance(random_id, bool) or not isinstance(random_id, int): + raise TypeError("random_id must be an integer or None (booleans are not accepted)") + if not -(1 << 63) <= random_id < (1 << 63): + raise ValueError("random_id must be between -9223372036854775808 and 9223372036854775807") + + async def ainput(prompt: str = "", *, hide: bool = False): """Just like the built-in input, but async""" with ThreadPoolExecutor(1) as executor: func = functools.partial(getpass if hide else input, prompt) - return await asyncio.get_event_loop().run_in_executor(executor, func) + return await asyncio.get_running_loop().run_in_executor(executor, func) def get_input_media_from_file_id( @@ -348,7 +358,7 @@ async def parse_text_entities( def zero_datetime() -> datetime: - return datetime.fromtimestamp(0, timezone.utc) + return datetime.fromtimestamp(0, UTC) def timestamp_to_datetime(ts: int | None) -> datetime | None: diff --git a/news/27.feature.rst b/news/27.feature.rst new file mode 100644 index 000000000..febb93551 --- /dev/null +++ b/news/27.feature.rst @@ -0,0 +1,3 @@ +Add opt-in ``Client(catch_up=True)`` recovery for missed MTProto updates, +including persisted common and channel cursors, reconnect and gap reconciliation, +and duplicate suppression during recovery. diff --git a/news/29.bugfix.rst b/news/29.bugfix.rst new file mode 100644 index 000000000..c8fb241a2 --- /dev/null +++ b/news/29.bugfix.rst @@ -0,0 +1,4 @@ +Recover closed or unresponsive MTProto connections with coordinated, bounded +backoff and preserve authentication state. Fail uncertain RPCs without replaying +side effects, clean up recovery tasks on shutdown, and expose +``Client.connection_health`` for diagnostics. diff --git a/news/43.feature.rst b/news/43.feature.rst new file mode 100644 index 000000000..bdd223b59 --- /dev/null +++ b/news/43.feature.rst @@ -0,0 +1 @@ +Added the `transfer_chat_ownershipt` method to the `Client`. This method allows the owner of a chat to transfer ownership to another user. diff --git a/news/45.bugfix.rst b/news/45.bugfix.rst new file mode 100644 index 000000000..53c49ddde --- /dev/null +++ b/news/45.bugfix.rst @@ -0,0 +1 @@ +Fixes a bug that caused the chat parser to return `ChatForbidden` or `ChannelForbidden` which caused some methods like `get_chat_history` to throw `AttributeError`. diff --git a/news/49.misc.rst b/news/49.misc.rst new file mode 100644 index 000000000..e9eb89a5a --- /dev/null +++ b/news/49.misc.rst @@ -0,0 +1 @@ +Defer loop obtaining to when it's actually used. diff --git a/news/70.feature.rst b/news/70.feature.rst new file mode 100644 index 000000000..c2889f88f --- /dev/null +++ b/news/70.feature.rst @@ -0,0 +1,2 @@ +Add an optional, validated ``random_id`` to single-message sending methods so +background jobs can reuse a stable MTProto message ID across retries. diff --git a/news/75.feature.rst b/news/75.feature.rst new file mode 100644 index 000000000..a6572b880 --- /dev/null +++ b/news/75.feature.rst @@ -0,0 +1,3 @@ +Add native MTProxy connections through ``Client.proxy`` with standard hexadecimal +secrets and obfuscated abridged transport. Padded intermediate and fake-TLS secrets +are explicitly unsupported. diff --git a/news/dependency-updates.misc.rst b/news/dependency-updates.misc.rst new file mode 100644 index 000000000..6732e07b6 --- /dev/null +++ b/news/dependency-updates.misc.rst @@ -0,0 +1,3 @@ +Update runtime, development, and documentation dependencies and refresh the uv +lockfile while preserving Python 3.11 compatibility. Update uv and pre-commit hooks +and allow current Sphinx releases. diff --git a/news/hydrogram-audit-bot-api-skill.misc.rst b/news/hydrogram-audit-bot-api-skill.misc.rst new file mode 100644 index 000000000..c57ea0629 --- /dev/null +++ b/news/hydrogram-audit-bot-api-skill.misc.rst @@ -0,0 +1 @@ +Add a repository Codex skill for auditing Bot API feature coverage and investigating comparison results. diff --git a/news/hydrogram-update-api-skill.misc.rst b/news/hydrogram-update-api-skill.misc.rst new file mode 100644 index 000000000..7d309c29a --- /dev/null +++ b/news/hydrogram-update-api-skill.misc.rst @@ -0,0 +1 @@ +Add a repository Codex skill for updating the MTProto API schema and validating generated bindings. diff --git a/news/media-transmissions.bugfix.rst b/news/media-transmissions.bugfix.rst new file mode 100644 index 000000000..eceac851a --- /dev/null +++ b/news/media-transmissions.bugfix.rst @@ -0,0 +1,6 @@ +Apply the configured media transfer limit across uploads and downloads together. +Bound upload buffering, report progress only after acknowledgement, and propagate +upload failures instead of returning incomplete uploads as successful. Clean up +cancelled transfers and failed session startup, close streams deterministically, +and remove incomplete downloads on errors. Move file I/O and CDN decryption off +the event loop. diff --git a/news/mtproto-schema-pipeline.misc.rst b/news/mtproto-schema-pipeline.misc.rst new file mode 100644 index 000000000..ad29dfaca --- /dev/null +++ b/news/mtproto-schema-pipeline.misc.rst @@ -0,0 +1,6 @@ +Update the raw MTProto schema from layer 223 to the verified Telegram Desktop +layer 232, with pinned source checksums and a TDLib comparison. Preserve existing +poll, keyboard, and chat-join behavior; correct optional-vector serialization and +track raw support separately from high-level feature coverage. Schema updates now +require reproducible generation and offline tests, and CI publishes actionable +drift reports and validated candidate patches. diff --git a/news/python311-cancellation.bugfix.rst b/news/python311-cancellation.bugfix.rst new file mode 100644 index 000000000..535a6bfd8 --- /dev/null +++ b/news/python311-cancellation.bugfix.rst @@ -0,0 +1,2 @@ +Use Python 3.11 native timeouts for RPC writes and replies so cancelling an RPC +propagates cancellation and cleans up pending results reliably. diff --git a/news/python311.removal.rst b/news/python311.removal.rst new file mode 100644 index 000000000..f77b365c2 --- /dev/null +++ b/news/python311.removal.rst @@ -0,0 +1,2 @@ +Require Python 3.11 or later and drop Python 3.9 and 3.10 support. Keep Python 3.14 +as the default development interpreter and update dependencies and CI accordingly. diff --git a/news/ruff-configuration.misc.rst b/news/ruff-configuration.misc.rst new file mode 100644 index 000000000..b7951530e --- /dev/null +++ b/news/ruff-configuration.misc.rst @@ -0,0 +1,2 @@ +Align Ruff configuration across editors, pre-commit, and CI; pin its preview +version, exclude generated code, and apply only safe lint fixes automatically. diff --git a/news/uv-hatchling.misc.rst b/news/uv-hatchling.misc.rst new file mode 100644 index 000000000..9afe9c994 --- /dev/null +++ b/news/uv-hatchling.misc.rst @@ -0,0 +1,3 @@ +Modernize uv development dependencies and Hatchling packaging configuration, +including explicit generated API artifacts, compiler-aware caching, and installation +checks for source distributions, wheels, and editable installs. diff --git a/pyproject.toml b/pyproject.toml index 32521156a..02da12b90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,22 +3,22 @@ name = "Hydrogram" dynamic = ["version"] description = "Sleek, advanced, and asynchronous Telegram MTProto API framework in Python, designed for fluid user and bot interactions." authors = [{ name = "Hydrogram", email = "contact@hydrogram.org" }] -dependencies = ["pyaes==1.6.1", "pysocks==1.7.1", "aiosqlite>=0.19.0"] +dependencies = ["pyaes==1.6.1", "pysocks==1.7.1", "aiosqlite>=0.22.1"] readme = "README.md" license = "LGPL-3.0-or-later" -requires-python = ">=3.9" +license-files = ["COPYING", "COPYING.lesser", "NOTICE"] +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", @@ -31,118 +31,109 @@ classifiers = [ ] keywords = ["telegram chat messenger mtproto api client library python"] -[tool.hatch.version] -path = "hydrogram/__init__.py" - -# Used to call hatch_build.py -[tool.hatch.build.hooks.custom] - [project.urls] homepage = "https://hydrogram.org" tracker = "https://github.com/hydrogram/hydrogram/issues" community = "https://t.me/HydrogramNews" source = "https://github.com/hydrogram/hydrogram" documentation = "https://docs.hydrogram.org" +changelog = "https://docs.hydrogram.org/en/latest/releases.html" [build-system] -requires = ["hatchling"] +requires = ["hatchling>=1.33.0"] build-backend = "hatchling.build" -[tool.rye] -managed = true -dev-dependencies = [ - "ruff>=0.4.7", - "pytest>=7.4.3", - "pytest-asyncio>=0.23.2", - "pytest-cov>=4.1.0", - "twine>=4.0.2", - "pre-commit>=3.7.1", +[tool.uv] +required-version = ">=0.12.24" +# Local builds depend on the version source, hook, and compiler inputs. +cache-keys = [ + { file = "pyproject.toml" }, + { file = "hydrogram/__init__.py" }, + { file = "hatch_build.py" }, + { file = "compiler/__init__.py" }, + { file = "compiler/api/*.py" }, + { file = "compiler/api/docs.json" }, + { file = "compiler/api/source/*.tl" }, + { file = "compiler/api/template/*.txt" }, + { file = "compiler/errors/*.py" }, + { file = "compiler/errors/source/*.tsv" }, + { file = "compiler/errors/template/*.txt" }, + { file = "NOTICE" }, +] + +[dependency-groups] +# Keep lower bounds compatible with Python 3.11; uv locks newer versions where supported. +dev = [ + "ruff==0.17.3", + "pytest>=9.2.0", + "pytest-asyncio>=1.4.1", + "pytest-cov>=7.1.0", + "twine>=7.0.0", + "pre-commit>=4.7.0", + "httpx[http2]>=0.28.1", + "lxml>=6.1.3", + "requests>=2.34.2", ] [project.optional-dependencies] docs = [ - "Sphinx>=7.3.7", - "furo>=2024.5.6", - "sphinx-autobuild>=2024.4.16", + "sphinx>=9.0.4", + "furo>=2025.12.19", + "sphinx-autobuild>=2025.8.25", "sphinx-copybutton>=0.5.2", - "pygments>=2.18.0", - "towncrier>=23.11.0", - "sphinxcontrib-towncrier>=0.4.0a0", + "pygments>=2.21.0", + "towncrier>=26.9.0", + "sphinxcontrib-towncrier>=0.5.0a0", ] fast = [ "tgcrypto>=1.2.5", - "uvloop>=0.19.0; (sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy'", + "uvloop>=0.23.0; (sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy'", ] -[tool.hatch.metadata] -allow-direct-references = true +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" + +[tool.hatch.version] +path = "hydrogram/__init__.py" [tool.hatch.build.targets.sdist] +# Compatible with the packaging/Twine versions in uv.lock. +core-metadata-version = "2.4" +only-include = [ + "hydrogram", + "compiler", + "hatch_build.py", + "Makefile", + "README.md", + "COPYING", + "COPYING.lesser", + "NOTICE", +] +# Always regenerate these from the compiler inputs when building a wheel. exclude = [ - ".github/", - "dev_tools/", - "docs/", - "news/", - "tests/", - ".editorconfig", - ".gitignore", - ".pre-commit-config.yaml", - "CONTRIBUTING.md", - "NEWS.rst", - "requirements.lock", - "requirements-dev.lock", + "/hydrogram/errors/exceptions", + "/hydrogram/raw/all.py", + "/hydrogram/raw/base", + "/hydrogram/raw/functions", + "/hydrogram/raw/types", ] [tool.hatch.build.targets.wheel] -ignore-vcs = true -package = "hydrogram" - -[tool.ruff] -line-length = 99 -target-version = "py39" - -[tool.ruff.lint] -select = [ - "FURB", # refurb - "I", # isort - "E", # pycodestyle: error - "W", # pycodestyle: warning - "UP", # pyupgrade - "F", # pyflakes - "SIM", # flake8-simplify - "RET", # flake8-return - "C4", # flake8-comprehensions - "PTH", # flake8-use-pathlib - "PERF", # perflint - "N", # pep8-naming - "RUF", # ruff - "G", # flake8-logging-format - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "FA", # flake8-future-annotations - "PL", # pylint -] -ignore = [ - "RUF001", - "RUF002", - "RUF003", - "E203", - "PERF203", - "PLR09", - "PLR2004", - "PLR1702", - "PLW1514", - "PLW2901", - "PLW0603", +core-metadata-version = "2.4" +packages = ["hydrogram"] +artifacts = [ + "/hydrogram/errors/exceptions/**", + "/hydrogram/raw/all.py", + "/hydrogram/raw/base/**", + "/hydrogram/raw/functions/**", + "/hydrogram/raw/types/**", + "!**/__pycache__/**", + "!**/*.pyc", + "!**/*.pyo", ] -preview = true - -[tool.ruff.lint.isort] -known-first-party = ["hydrogram"] -[tool.ruff.format] -docstring-code-format = true -preview = true +[tool.hatch.build.targets.wheel.hooks.custom] +path = "hatch_build.py" [tool.towncrier] package = "hydrogram" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..7a8098b6d --- /dev/null +++ b/ruff.toml @@ -0,0 +1,72 @@ +line-length = 99 +# Keep preview behavior identical in editors, pre-commit, and the locked environment. +required-version = "==0.17.3" +# Keep compiler output untouched even when a hook or editor passes a file directly. +force-exclude = true +extend-exclude = [ + "hydrogram/errors/exceptions", + "hydrogram/raw/all.py", + "hydrogram/raw/base", + "hydrogram/raw/functions", + "hydrogram/raw/types", +] + +[lint] +select = [ + "FURB", # refurb + "I", # isort + "E", # pycodestyle: error + "W", # pycodestyle: warning + "UP", # pyupgrade + "F", # pyflakes + "SIM", # flake8-simplify + "RET", # flake8-return + "C4", # flake8-comprehensions + "PTH", # flake8-use-pathlib + "PERF", # perflint + "N", # pep8-naming + "RUF", # ruff + "G", # flake8-logging-format + "TID", # flake8-tidy-imports + "TC", # flake8-type-checking + "FA", # flake8-future-annotations + "PL", # pylint +] +ignore = [ + "line-too-long", # E501: the formatter wraps lines on a best-effort basis. + # These rules conflict with Ruff's formatter (the E and W families select them). + "indentation-with-invalid-multiple", # E111 + "indentation-with-invalid-multiple-comment", # E114 + "over-indented", # E117 + "tab-indentation", # W191 + "ambiguous-unicode-character-string", + "ambiguous-unicode-character-docstring", + "ambiguous-unicode-character-comment", + "non-empty-init-module", # Allow implementation code in __init__.py files (library design pattern) + "whitespace-before-punctuation", + "try-except-in-loop", + "PLR09", + "magic-value-comparison", + "too-many-nested-blocks", + "unspecified-encoding", + "redefined-loop-name", + "global-statement", + "subclass-builtin", # This rule is unsafe and not relevant for us. + "noqa-comments", # Preserve the project's existing suppression syntax. + "too-many-statements-in-try-clause", # Keep the existing complexity policy. +] +# Retain the project's existing preview rules; review changes when upgrading Ruff. +preview = true + +[lint.per-file-ignores] +# Decorators support calls on both Client classes and instances. +"hydrogram/methods/decorators/*.py" = ["method-receiver-default"] + +[lint.isort] +known-first-party = ["hydrogram"] + +[format] +docstring-code-format = true +line-ending = "lf" +# Retain the existing style without reformatting the codebase. +preview = true diff --git a/tests/filters/test_command.py b/tests/filters/test_command.py index 9f72c79bf..6bf3248ef 100644 --- a/tests/filters/test_command.py +++ b/tests/filters/test_command.py @@ -17,133 +17,122 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . -import pytest - from hydrogram import filters from tests.filters import Client, Message c = Client() -@pytest.mark.asyncio -async def test_single(): +def test_single(): f = filters.command("start") m = Message("/start") - assert await f(c, m) + assert f(c, m) -@pytest.mark.asyncio -async def test_multiple(): +def test_multiple(): f = filters.command(["start", "help"]) m = Message("/start") - assert await f(c, m) + assert f(c, m) m = Message("/help") - assert await f(c, m) + assert f(c, m) m = Message("/settings") - assert not await f(c, m) + assert not f(c, m) -@pytest.mark.asyncio -async def test_prefixes(): +def test_prefixes(): f = filters.command("start", prefixes=list(".!#")) m = Message(".start") - assert await f(c, m) + assert f(c, m) m = Message("!start") - assert await f(c, m) + assert f(c, m) m = Message("#start") - assert await f(c, m) + assert f(c, m) m = Message("/start") - assert not await f(c, m) + assert not f(c, m) -@pytest.mark.asyncio -async def test_case_sensitive(): +def test_case_sensitive(): f = filters.command("start", case_sensitive=True) m = Message("/start") - assert await f(c, m) + assert f(c, m) m = Message("/StArT") - assert not await f(c, m) + assert not f(c, m) -@pytest.mark.asyncio -async def test_case_insensitive(): +def test_case_insensitive(): f = filters.command("start", case_sensitive=False) m = Message("/start") - assert await f(c, m) + assert f(c, m) m = Message("/StArT") - assert await f(c, m) + assert f(c, m) -@pytest.mark.asyncio -async def test_with_mention(): +def test_with_mention(): f = filters.command("start") m = Message("/start@username") - assert await f(c, m) + assert f(c, m) m = Message("/start@UserName") - assert await f(c, m) + assert f(c, m) m = Message("/start@another") - assert not await f(c, m) + assert not f(c, m) -@pytest.mark.asyncio -async def test_with_args(): +def test_with_args(): f = filters.command("start") m = Message("/start") - await f(c, m) + f(c, m) assert m.command == ["start"] m = Message("/StArT") - await f(c, m) + f(c, m) assert m.command == ["start"] m = Message("/start@username") - await f(c, m) + f(c, m) assert m.command == ["start"] m = Message("/start a b c") - await f(c, m) + f(c, m) assert m.command == ["start", *list("abc")] m = Message("/start@username a b c") - await f(c, m) + f(c, m) assert m.command == ["start", *list("abc")] m = Message("/start 'a b' c") - await f(c, m) + f(c, m) assert m.command == ["start", "a b", "c"] m = Message('/start a b "c d"') - await f(c, m) + f(c, m) assert m.command == ["start", *list("ab"), "c d"] -@pytest.mark.asyncio -async def test_caption(): +def test_caption(): f = filters.command("start") m = Message(caption="/start") - assert await f(c, m) + assert f(c, m) -@pytest.mark.asyncio -async def test_no_text(): +def test_no_text(): f = filters.command("start") m = Message() - assert not await f(c, m) + assert not f(c, m) diff --git a/tests/test_connection_resilience.py b/tests/test_connection_resilience.py new file mode 100644 index 000000000..82de8d382 --- /dev/null +++ b/tests/test_connection_resilience.py @@ -0,0 +1,927 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest +import pytest_asyncio + +from hydrogram import Client, raw +from hydrogram.connection import Connection +from hydrogram.connection.transport.tcp.tcp import TCP +from hydrogram.crypto import mtproto +from hydrogram.errors import ( + AuthKeyDuplicated, + BadMsgNotification, + ConnectionLostError, + FloodWait, + InternalServerError, + RequestTimeoutError, + SessionPasswordNeeded, + SessionRevoked, +) +from hydrogram.session.internals import MsgId +from hydrogram.session.session import Session + + +class Transport: + """Inject EOF, resets, lost replies and MTProto service messages without sockets.""" + + def __init__(self, harness): + self.harness = harness + self.protocol = SimpleNamespace(closed=False) + self.queue = asyncio.Queue() + self.sent = [] + self.sent_event = asyncio.Event() + self.failure = None + self.drop_pongs = False + self.hold_requests = False + self.responses = [] + self.closed = False + self.connect = AsyncMock() + + async def close(self): + self.closed = True + self.protocol.closed = True + + async def recv(self): + packet = await self.queue.get() + if isinstance(packet, Exception): + raise packet + return packet + + def feed(self, body): + key = len(self.harness.frames).to_bytes(8, "little") + self.harness.frames[key] = SimpleNamespace(body=body, msg_id=MsgId() + 1, seq_no=0) + self.queue.put_nowait(key) + + async def send(self, payload): + message = self.harness.messages[payload] + self.sent.append(message.body) + self.sent_event.set() + if self.failure is not None: + raise self.failure + if isinstance(message.body, (raw.functions.Ping, raw.functions.PingDelayDisconnect)): + if not self.drop_pongs: + self.feed(raw.types.Pong(msg_id=message.msg_id, ping_id=0)) + elif isinstance(message.body, raw.functions.InvokeWithLayer) or not self.hold_requests: + result = self.responses.pop(0) if self.responses else True + if isinstance(result, (raw.types.BadServerSalt, raw.types.BadMsgNotification)): + result.bad_msg_id = message.msg_id + self.feed(result) + else: + self.feed(raw.types.RpcResult(req_msg_id=message.msg_id, result=result)) + + +class Harness: + def __init__(self, monkeypatch): + self.messages = {} + self.frames = {} + self.transports = [] + self.configure = lambda transport: None + # Keep crypto/executor boundaries mocked, retaining real framing dispatch. + + def pack(message, *_args): + payload = message.msg_id.to_bytes(8, "little") + self.messages[payload] = message + return payload + + monkeypatch.setattr(mtproto, "pack", pack) + monkeypatch.setattr(mtproto, "unpack", lambda stream, *_: self.frames[stream.read()]) + self.client = SimpleNamespace( + name="test", + loop=asyncio.get_running_loop(), + connection_factory=Mock(side_effect=self.factory), + ipv6=False, + proxy=None, + protocol_factory=None, + storage=SimpleNamespace(api_id=AsyncMock(return_value=1)), + app_version="test", + device_model="test", + system_version="test", + lang_code="en", + update_manager=Mock(), + is_initialized=True, + disconnect_handler=AsyncMock(), + handle_updates=AsyncMock(), + ) + self.session = Session(self.client, 2, b"x" * 256, False) + self.session.RECONNECT_DELAY = 0 + self.session.PING_INTERVAL = 3600 + + def factory(self, **_kwargs): + transport = Transport(self) + self.configure(transport) + self.transports.append(transport) + return transport + + @property + def transport(self): + return self.transports[-1] + + async def recovered(self, previous): + async def wait(): + while self.session.recovery_task is previous: + await asyncio.sleep(0) + await asyncio.shield(self.session.recovery_task) + + await asyncio.wait_for(wait(), 2) + + +@pytest_asyncio.fixture +async def harness(monkeypatch): + harness = Harness(monkeypatch) + await harness.session.start() + yield harness + await harness.session.stop() + assert not harness.session.results + assert not harness.session._background_tasks + assert harness.session.recv_task is None + assert harness.session.ping_task is None + assert harness.session.recovery_task.done() + + +def request(): + return raw.functions.messages.SendMessage( + peer=raw.types.InputPeerSelf(), + message="test", + random_id=123, + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("failure", [None, ConnectionResetError("reset")]) +async def test_disconnect_recovers_requests_and_updates(harness, failure): + session = harness.session + old = harness.transport + previous = session.recovery_task + old.queue.put_nowait(failure) + await harness.recovered(previous) + assert old.closed + assert len(harness.transports) == 2 + assert await session.invoke(request()) is True + updates = raw.types.UpdatesTooLong() + harness.transport.feed(updates) + + async def wait_update(): + while not harness.client.update_manager.submit.called: + await asyncio.sleep(0) + + await asyncio.wait_for(wait_update(), 1) + harness.client.update_manager.submit.assert_called_once_with(updates) + assert harness.client.update_manager.request_recovery.call_count == 2 + assert session.health["is_connected"] + assert session.auth_key == b"x" * 256 + + +@pytest.mark.asyncio +async def test_closed_transport_concurrent_rpcs_share_recovery(harness): + harness.transport.protocol.closed = True + results = await asyncio.wait_for( + asyncio.gather(*[harness.session.invoke(request()) for _ in range(20)]), 2 + ) + assert results == [True] * 20 + assert len(harness.transports) == 2 + assert ( + sum(isinstance(q, raw.functions.messages.SendMessage) for q in harness.transport.sent) + == 20 + ) + + +@pytest.mark.asyncio +async def test_disconnect_fails_pending_requests_without_replay(harness): + transport = harness.transport + transport.hold_requests = True + tasks = [asyncio.create_task(harness.session.invoke(request())) for _ in range(5)] + + async def wait_sent(): + while len(transport.sent) < 7: # Initial ping/config plus five application requests. + await asyncio.sleep(0) + + await asyncio.wait_for(wait_sent(), 1) + previous = harness.session.recovery_task + transport.queue.put_nowait(None) + errors = await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), 1) + assert all(isinstance(e, ConnectionLostError) and e.delivery_uncertain for e in errors) + await harness.recovered(previous) + assert not any( + isinstance(q, raw.functions.messages.SendMessage) for q in harness.transport.sent + ) + assert not harness.session.results + + +@pytest.mark.asyncio +@pytest.mark.parametrize("failure", [OSError("handler is closed"), TimeoutError("drain stalled")]) +async def test_ambiguous_send_failure_is_not_retried(harness, failure): + previous = harness.session.recovery_task + old = harness.transport + old.failure = failure + with pytest.raises(ConnectionLostError, match="delivery is uncertain"): + await harness.session.invoke(request()) + await harness.recovered(previous) + assert len(old.sent) == 3 + assert len(harness.transport.sent) == 2 + + +@pytest.mark.asyncio +async def test_timeout_recovers_without_replaying(harness): + previous = harness.session.recovery_task + harness.transport.hold_requests = True + with pytest.raises(RequestTimeoutError, match="may have processed"): + await harness.session.invoke(request(), timeout=0.01) + await harness.recovered(previous) + assert await harness.session.invoke(request()) is True + assert not harness.session.results + + +@pytest.mark.asyncio +async def test_cancelling_rpc_does_not_leave_pending_result(harness): + harness.transport.hold_requests = True + task = asyncio.create_task(harness.session.invoke(request())) + + async def wait_sent(): + while not harness.session.results: + await asyncio.sleep(0) + + await asyncio.wait_for(wait_sent(), 1) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + assert not harness.session.results + assert harness.session.health["is_connected"] + + +@pytest.mark.asyncio +async def test_stop_cancels_backoff_and_waiting_rpcs(harness): + failed = asyncio.Event() + + async def fail(): + failed.set() + await asyncio.sleep(0) + raise ConnectionResetError("offline") + + harness.configure = lambda transport: setattr(transport, "connect", fail) + harness.session.RECONNECT_DELAY = 3600 + harness.session._connected_at -= harness.session.STABLE_CONNECTION_INTERVAL + 1 + harness.transport.protocol.closed = True + task = asyncio.create_task(harness.session.invoke(request())) + await asyncio.wait_for(failed.wait(), 1) + await asyncio.wait_for(harness.session.stop(), 1) + with pytest.raises(ConnectionError, match="stopped"): + await task + assert harness.session.recovery_task.cancelled() + assert harness.session.health["state"] == "stopped" + count = len(harness.transports) + with pytest.raises(ConnectionError, match="stopped"): + await harness.session.invoke(request()) + assert len(harness.transports) == count + + +@pytest.mark.asyncio +async def test_cancelled_waiter_does_not_cancel_shared_recovery(harness): + connecting = asyncio.Event() + release = asyncio.Event() + + async def connect(): + connecting.set() + await release.wait() + + harness.configure = lambda transport: setattr(transport, "connect", connect) + harness.transport.protocol.closed = True + first = asyncio.create_task(harness.session.invoke(request())) + second = asyncio.create_task(harness.session.invoke(request())) + await asyncio.wait_for(connecting.wait(), 1) + first.cancel() + with pytest.raises(asyncio.CancelledError): + await first + release.set() + assert await asyncio.wait_for(second, 1) is True + assert len(harness.transports) == 2 + + +@pytest.mark.asyncio +async def test_initialization_retries_are_bounded(monkeypatch): + harness = Harness(monkeypatch) + harness.configure = lambda transport: setattr( + transport, "connect", AsyncMock(side_effect=OSError("offline")) + ) + session = harness.session + try: + with pytest.raises(ConnectionError, match="after 5 attempts"): + await asyncio.wait_for(session.start(), 2) + assert len(harness.transports) == session.MAX_RECONNECT_ATTEMPTS + assert all(t.closed for t in harness.transports) + with pytest.raises(ConnectionError, match="call start"): + await session.invoke(request()) + assert len(harness.transports) == session.MAX_RECONNECT_ATTEMPTS + harness.configure = lambda transport: None + await session.start() + assert session.health["is_connected"] + finally: + await session.stop() + + +@pytest.mark.asyncio +async def test_bad_salt_is_safely_retried_but_bounded(harness): + rejection = raw.types.BadServerSalt( + bad_msg_id=0, bad_msg_seqno=1, error_code=48, new_server_salt=123 + ) + harness.transport.responses = [rejection] + assert await harness.session.invoke(request()) is True + assert harness.session.salt == 123 + harness.transport.responses = [rejection] * harness.session.MAX_RETRIES + with pytest.raises(BadMsgNotification, match="48"): + await harness.session.invoke(request()) + assert not harness.session.results + + +@pytest.mark.asyncio +async def test_sequence_error_resets_only_ephemeral_session(harness): + session_id = harness.session.session_id + previous = harness.session.recovery_task + harness.transport.responses = [ + raw.types.BadMsgNotification(bad_msg_id=0, bad_msg_seqno=1, error_code=32) + ] + with pytest.raises(BadMsgNotification, match="32"): + await harness.session.invoke(request()) + await harness.recovered(previous) + assert harness.session.session_id != session_id + assert harness.session.auth_key == b"x" * 256 + assert await harness.session.invoke(request()) is True + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "error, expected", + [ + ( + raw.types.RpcError(error_code=406, error_message="AUTH_KEY_DUPLICATED"), + AuthKeyDuplicated, + ), + (raw.types.RpcError(error_code=401, error_message="SESSION_REVOKED"), SessionRevoked), + ], +) +async def test_authentication_errors_fail_without_reauthorizing(harness, error, expected): + previous = harness.session.recovery_task + count = len(harness.transports) + harness.transport.responses = [error] + with pytest.raises(expected): + await harness.session.invoke(request()) + with pytest.raises(expected): + await harness.recovered(previous) + assert len(harness.transports) == count + assert harness.session.auth_key == b"x" * 256 + assert harness.session.health["state"] == "failed" + + +@pytest.mark.asyncio +async def test_server_errors_are_not_replayed(harness): + harness.transport.responses = [raw.types.RpcError(error_code=500, error_message="INTERNAL")] + with pytest.raises(InternalServerError): + await harness.session.invoke(request()) + assert len(harness.transport.sent) == 3 + + +@pytest.mark.asyncio +async def test_flood_wait_retries_are_bounded(harness): + harness.transport.responses = [ + raw.types.RpcError(error_code=420, error_message="FLOOD_WAIT_0") + ] * 3 + with pytest.raises(FloodWait): + await harness.session.invoke(request(), retries=3) + assert len(harness.transport.sent) == 5 + + +@pytest.mark.asyncio +async def test_new_session_updates_salt(harness): + harness.transport.feed( + raw.types.NewSessionCreated(first_msg_id=1, unique_id=2, server_salt=456) + ) + + async def wait_salt(): + while harness.session.salt != 456: + await asyncio.sleep(0) + + await asyncio.wait_for(wait_salt(), 1) + + +@pytest.mark.asyncio +async def test_tcp_idle_read_is_not_failure(): + tcp = TCP(False, None) + tcp._closed = False + tcp.reader = SimpleNamespace(read=AsyncMock(side_effect=[TimeoutError(), b"x"])) + assert await tcp.recv(1) == b"x" + assert not tcp._closed + + +@pytest.mark.asyncio +async def test_tcp_detects_closing_writer_before_write(): + tcp = TCP(False, None) + tcp._closed = False + tcp.reader = Mock(at_eof=Mock(return_value=False)) + tcp.writer = Mock(is_closing=Mock(return_value=True), drain=AsyncMock()) + with pytest.raises(OSError, match="closed"): + await tcp.send(b"data") + tcp.writer.write.assert_not_called() + + +@pytest.mark.asyncio +async def test_connection_close_before_connect_is_safe(): + connection = Connection(2, False, False, None) + await connection.close() + with pytest.raises(ConnectionError, match="not connected"): + await connection.send(b"data") + assert await connection.recv() is None + + +def test_client_health_before_connection(): + client = Client("health", in_memory=True) + try: + assert client.connection_health["state"] == "stopped" + assert not client.connection_health["is_connected"] + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_repeated_short_connections_exhaust_one_budget(harness): + session = harness.session + for _ in range(session.MAX_RECONNECT_ATTEMPTS - 1): + previous = session.recovery_task + harness.transport.queue.put_nowait(None) + await harness.recovered(previous) + previous = session.recovery_task + harness.transport.queue.put_nowait(None) + with pytest.raises(ConnectionError, match="after 5 attempts"): + await harness.recovered(previous) + assert len(harness.transports) == 5 + assert session.health["state"] == "failed" + + +@pytest.mark.asyncio +async def test_backoff_grows_with_jitter_and_is_capped(harness, monkeypatch, caplog): + bounds = [] + + def jitter(low, high): + bounds.append((low, high)) + return 0 # Preserve scheduling without delaying the test. + + monkeypatch.setattr("hydrogram.session.session.random.uniform", jitter) + session = harness.session + session.RECONNECT_DELAY = 1 + session.MAX_RECONNECT_DELAY = 3 + harness.configure = lambda transport: setattr( + transport, "connect", AsyncMock(side_effect=OSError("offline")) + ) + harness.transport.protocol.closed = True + with pytest.raises(ConnectionError, match="after 5 attempts"): + await session.invoke(request()) + assert bounds == [(0.5, 1), (1, 2), (1.5, 3), (1.5, 3)] + records = [r for r in caplog.records if hasattr(r, "reconnect_delay")] + assert [r.reconnect_attempt for r in records] == [2, 3, 4, 5] + assert all(r.dc_id == 2 for r in records) + + +@pytest.mark.asyncio +async def test_stable_connection_restores_recovery_budget(harness): + session = harness.session + session.reconnect_attempt = session.MAX_RECONNECT_ATTEMPTS + session._connected_at -= session.STABLE_CONNECTION_INTERVAL + 1 + previous = session.recovery_task + harness.transport.queue.put_nowait(None) + await harness.recovered(previous) + assert session.reconnect_attempt == 1 + + +@pytest.mark.asyncio +async def test_ping_timeout_detects_unresponsive_peer(harness): + session = harness.session + session.ping_task.cancel() + await asyncio.gather(session.ping_task, return_exceptions=True) + session.PING_INTERVAL = 0 + session.WAIT_TIMEOUT = 0.01 + harness.transport.drop_pongs = True + previous = session.recovery_task + session.ping_task = asyncio.create_task(session.ping_worker()) + await harness.recovered(previous) + assert len(harness.transports) == 2 + assert session.health["is_connected"] + + +@pytest.mark.asyncio +async def test_idle_connection_with_pongs_remains_healthy(harness): + session = harness.session + session.ping_task.cancel() + await asyncio.gather(session.ping_task, return_exceptions=True) + session.PING_INTERVAL = 0.001 + session.ping_task = asyncio.create_task(session.ping_worker()) + + async def wait_pings(): + while ( + sum(isinstance(q, raw.functions.PingDelayDisconnect) for q in harness.transport.sent) + < 3 + ): + await asyncio.sleep(0) + + await asyncio.wait_for(wait_pings(), 1) + assert len(harness.transports) == 1 + assert session.health["is_connected"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("code", [404, 444]) +async def test_terminal_transport_errors_do_not_loop_or_discard_auth(harness, code): + session = harness.session + previous = session.recovery_task + harness.transport.queue.put_nowait((-code).to_bytes(4, "little", signed=True)) + with pytest.raises(ConnectionError, match=str(code)): + await harness.recovered(previous) + assert len(harness.transports) == 1 + assert harness.transport.closed + assert session.auth_key == b"x" * 256 + + +@pytest.mark.asyncio +async def test_transport_flood_uses_backoff(harness): + previous = harness.session.recovery_task + harness.transport.queue.put_nowait((-429).to_bytes(4, "little", signed=True)) + await harness.recovered(previous) + assert len(harness.transports) == 2 + + +@pytest.mark.asyncio +async def test_two_factor_login_error_keeps_session_usable(harness): + harness.transport.responses = [ + raw.types.RpcError(error_code=401, error_message="SESSION_PASSWORD_NEEDED") + ] + with pytest.raises(SessionPasswordNeeded): + await harness.session.invoke(request()) + assert harness.session.health["is_connected"] + assert await harness.session.invoke(request()) is True + assert len(harness.transports) == 1 + + +@pytest.mark.asyncio +async def test_stop_cancels_receiver_processing_and_update_tasks(harness): + processing = asyncio.Event() + + async def handle(_packet): + processing.set() + await asyncio.Event().wait() + + harness.session.handle_packet = handle + harness.transport.feed(raw.types.UpdatesTooLong()) + await asyncio.wait_for(processing.wait(), 1) + await asyncio.wait_for(harness.session.stop(), 1) + assert harness.session.recv_task is None + assert harness.transport.closed + + +@pytest.mark.asyncio +async def test_stale_failure_does_not_close_replacement(harness): + old = harness.transport + previous = harness.session.recovery_task + old.queue.put_nowait(None) + await harness.recovered(previous) + assert harness.session._request_recovery(OSError("late failure"), connection=old) is None + assert harness.session.health["is_connected"] + assert len(harness.transports) == 2 + + +@pytest.mark.asyncio +async def test_fallback_update_tasks_are_owned_and_cancelled(harness): + started = asyncio.Event() + + async def handle(_update): + started.set() + await asyncio.Event().wait() + + harness.client.update_manager = None + harness.client.handle_updates = handle + harness.transport.feed(raw.types.UpdatesTooLong()) + await asyncio.wait_for(started.wait(), 1) + assert harness.session._background_tasks + await asyncio.wait_for(harness.session.stop(), 1) + assert not harness.session._background_tasks + + +@pytest.mark.asyncio +async def test_initialization_auth_error_fails_once_and_cleans_up(monkeypatch): + harness = Harness(monkeypatch) + harness.configure = lambda transport: setattr( + transport, + "responses", + [raw.types.RpcError(error_code=406, error_message="AUTH_KEY_DUPLICATED")], + ) + session = harness.session + try: + with pytest.raises(AuthKeyDuplicated): + await asyncio.wait_for(session.start(), 1) + assert len(harness.transports) == 1 + assert harness.transport.closed + assert session.recv_task is None + assert session.ping_task is None + assert not session.results + finally: + await session.stop() + + +@pytest.mark.asyncio +async def test_cancelling_start_cleans_connect_attempt(monkeypatch): + harness = Harness(monkeypatch) + connecting = asyncio.Event() + + async def connect(): + connecting.set() + await asyncio.Event().wait() + + harness.configure = lambda transport: setattr(transport, "connect", connect) + task = asyncio.create_task(harness.session.start()) + await asyncio.wait_for(connecting.wait(), 1) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + assert harness.session.recovery_task.cancelled() + assert harness.transport.closed + assert harness.session.health["state"] == "stopped" + + +@pytest.mark.asyncio +async def test_tcp_stalled_drain_is_bounded(): + tcp = TCP(False, None) + tcp.TIMEOUT = 0.01 + tcp._closed = False + tcp.reader = Mock(at_eof=Mock(return_value=False)) + tcp.writer = Mock( + is_closing=Mock(return_value=False), drain=AsyncMock(side_effect=asyncio.Event().wait) + ) + with pytest.raises(OSError): + await asyncio.wait_for(tcp.send(b"data"), 1) + assert tcp._closed + + +@pytest.mark.asyncio +async def test_watchdog_survives_probe_failure(): + client = Client("watchdog", in_memory=True) + client.UPDATES_WATCHDOG_INTERVAL = 0.001 + probed = asyncio.Event() + + async def invoke(_query): + await asyncio.sleep(0) + if not probed.is_set(): + probed.set() + raise ConnectionLostError("lost") + client.updates_watchdog_event.set() + + client.invoke = invoke + try: + await asyncio.wait_for(client.updates_watchdog(), 1) + assert probed.is_set() + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_terminate_cancels_watchdog_waiting_on_recovery(): + client = Client("watchdog-stop", in_memory=True) + client.is_initialized = True + client.storage.save = AsyncMock() + client.dispatcher.stop = AsyncMock() + client.updates_watchdog_task = asyncio.create_task(asyncio.Event().wait()) + watchdog = client.updates_watchdog_task + try: + await asyncio.wait_for(client.terminate(), 1) + assert watchdog.cancelled() + assert client.updates_watchdog_task is None + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_initial_sequence_error_recovers_with_new_mtproto_session(monkeypatch): + harness = Harness(monkeypatch) + original_id = harness.session.session_id + + def configure(transport): + if not harness.transports: + transport.responses = [ + raw.types.BadMsgNotification( + bad_msg_id=0, + bad_msg_seqno=1, + error_code=32, + ) + ] + + harness.configure = configure + try: + await asyncio.wait_for(harness.session.start(), 1) + assert len(harness.transports) == 2 + assert harness.session.session_id != original_id + assert harness.session.health["is_connected"] + finally: + await harness.session.stop() + + +@pytest.mark.asyncio +async def test_initial_transport_disconnect_does_not_publish_readiness(monkeypatch): + harness = Harness(monkeypatch) + + def configure(transport): + transport.drop_pongs = True + transport.queue.put_nowait(None) + + harness.configure = configure + try: + with pytest.raises(ConnectionError, match="after 5 attempts"): + await asyncio.wait_for(harness.session.start(), 1) + assert len(harness.transports) == 5 + assert not harness.session.is_started.is_set() + assert all(t.closed for t in harness.transports) + assert not harness.session.results + finally: + await harness.session.stop() + + +@pytest.mark.asyncio +async def test_received_response_can_complete_even_if_connection_then_closes(harness): + previous = harness.session.recovery_task + transport = harness.transport + send = transport.send + + async def send_then_close(payload): + await send(payload) + transport.queue.put_nowait(None) + + transport.send = send_then_close + assert await harness.session.invoke(request()) is True + await harness.recovered(previous) + assert not harness.session.results + + +@pytest.mark.asyncio +async def test_concurrent_restarts_share_one_connection(harness): + await asyncio.wait_for(asyncio.gather(*[harness.session.restart() for _ in range(10)]), 1) + assert len(harness.transports) == 2 + assert harness.session.health["is_connected"] + + +@pytest.mark.asyncio +async def test_concurrent_stops_close_cleanly_once(harness): + await asyncio.wait_for(asyncio.gather(*[harness.session.stop() for _ in range(10)]), 1) + assert harness.transport.closed + harness.client.disconnect_handler.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_connection_connect_cancellation_closes_partially_open_protocol(): + started = asyncio.Event() + + async def connect(_address): + started.set() + await asyncio.Event().wait() + + protocol = SimpleNamespace(connect=connect, close=AsyncMock()) + connection = Connection(2, False, False, None, protocol_factory=Mock(return_value=protocol)) + task = asyncio.create_task(connection.connect()) + await asyncio.wait_for(started.wait(), 1) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + protocol.close.assert_awaited_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("error", [OSError("reset"), RuntimeError("the handler is closed")]) +async def test_tcp_write_errors_mark_transport_closed(error): + tcp = TCP(False, None) + tcp._closed = False + tcp.reader = Mock(at_eof=Mock(return_value=False)) + tcp.writer = Mock(is_closing=Mock(return_value=False), write=Mock(side_effect=error)) + with pytest.raises(OSError): + await tcp.send(b"data") + assert tcp.closed + + +@pytest.mark.asyncio +async def test_connection_attempts_close_failed_protocols_and_stop(monkeypatch): + protocols = [] + + def factory(**_kwargs): + protocol = SimpleNamespace( + connect=AsyncMock(side_effect=OSError("offline")), close=AsyncMock() + ) + protocols.append(protocol) + return protocol + + monkeypatch.setattr("hydrogram.connection.connection.random.uniform", lambda *_: 0) + connection = Connection(2, False, False, None, protocol_factory=factory) + with pytest.raises(ConnectionError, match="exhausted"): + await asyncio.wait_for(connection.connect(), 1) + assert len(protocols) == connection.MAX_CONNECTION_ATTEMPTS + assert all(p.close.await_count == 1 for p in protocols) + + +@pytest.mark.asyncio +async def test_client_connect_failure_closes_storage_and_session(monkeypatch): + client = Client("connect-failure", in_memory=True) + client.load_session = AsyncMock() + client.storage.dc_id = AsyncMock(return_value=2) + client.storage.auth_key = AsyncMock(return_value=b"x" * 256) + client.storage.test_mode = AsyncMock(return_value=False) + client.storage.close = AsyncMock() + session = SimpleNamespace( + start=AsyncMock(side_effect=ConnectionError("offline")), stop=AsyncMock() + ) + monkeypatch.setattr("hydrogram.methods.auth.connect.Session", Mock(return_value=session)) + try: + with pytest.raises(ConnectionError, match="offline"): + await client.connect() + session.stop.assert_awaited_once() + client.storage.close.assert_awaited_once() + assert not client.is_connected + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_client_stop_cancels_recovery_before_waiting_on_workers(harness): + session = harness.session + session.RECONNECT_DELAY = 3600 + session.MAX_RECONNECT_DELAY = 3600 + previous = session.recovery_task + harness.transport.queue.put_nowait(None) + + async def wait_recovery(): + while session.recovery_task is previous: + await asyncio.sleep(0) + + await asyncio.wait_for(wait_recovery(), 1) + client = Client("stop-recovery", in_memory=True) + client.session = session + client.is_connected = True + client.is_initialized = True + client.storage.save = AsyncMock() + client.storage.close = AsyncMock() + stopped = asyncio.Event() + + async def stop_dispatcher(): + await asyncio.sleep(0) + # A real handler may be awaiting shared recovery when shutdown starts. + assert session._stopped + stopped.set() + + client.dispatcher.stop = AsyncMock(side_effect=stop_dispatcher) + try: + await asyncio.wait_for(client.stop(), 1) + assert stopped.is_set() + assert session.recovery_task.cancelled() + assert not client.is_initialized + assert not client.is_connected + client.storage.close.assert_awaited_once() + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_terminate_still_cleans_workers_when_takeout_finish_fails(): + client = Client("takeout-stop", in_memory=True) + client.is_initialized = True + client.takeout_id = 123 + client.invoke = AsyncMock(side_effect=ConnectionLostError("offline")) + client.storage.save = AsyncMock() + client.dispatcher.stop = AsyncMock() + try: + await asyncio.wait_for(client.terminate(), 1) + client.dispatcher.stop.assert_awaited_once() + assert client.takeout_id is None + assert not client.is_initialized + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_zero_attempt_limit_does_not_send_request(harness): + count = len(harness.transport.sent) + with pytest.raises(TimeoutError, match="maximum number"): + await harness.session.invoke(request(), retries=0) + assert len(harness.transport.sent) == count diff --git a/tests/test_file_id.py b/tests/test_file_id.py index 4aebed16e..7a6aa7e7c 100644 --- a/tests/test_file_id.py +++ b/tests/test_file_id.py @@ -17,9 +17,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hydrogram. If not, see . +import base64 +import struct + import pytest -from hydrogram.file_id import FileId, FileType, FileUniqueId, FileUniqueType +from hydrogram.file_id import FileId, FileType, FileUniqueId, FileUniqueType, rle_encode def check(file_id: str, expected_file_type: FileType): @@ -195,3 +198,32 @@ def test_stringify_file_unique_id(): string = "{'file_unique_type': , 'media_id': 5312458109417947140}" assert str(FileUniqueId.decode(file_unique_id)) == string + + +def test_file_unique_id_unknown_type_decode(): + data = struct.pack("" in s + assert "'url': 'https://example.com'" in s diff --git a/tests/test_layer_compatibility.py b/tests/test_layer_compatibility.py new file mode 100644 index 000000000..9f41524f6 --- /dev/null +++ b/tests/test_layer_compatibility.py @@ -0,0 +1,302 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +from io import BytesIO +from unittest.mock import AsyncMock + +import pytest +import pytest_asyncio + +from hydrogram import Client, enums, errors, raw, types +from hydrogram.methods.advanced.invoke import Invoke +from hydrogram.raw.core import TLObject + + +def wire(value): + return TLObject.read(BytesIO(value.write())) + + +def poll_media(): + return raw.types.MessageMediaPoll( + poll=raw.types.Poll( + id=123, + hash=456, + question=raw.types.TextWithEntities(text="Question?", entities=[]), + answers=[ + raw.types.PollAnswer( + text=raw.types.TextWithEntities(text="First", entities=[]), option=b"a" + ), + raw.types.PollAnswer( + text=raw.types.TextWithEntities(text="Second", entities=[]), option=b"b" + ), + ], + ), + results=raw.types.PollResults( + total_voters=3, + results=[ + raw.types.PollAnswerVoters(option=b"b", voters=3, recent_voters=[], chosen=True) + ], + ), + ) + + +@pytest_asyncio.fixture +async def client(): + client = Client( + "schema-tests", + api_id=1, + api_hash="test", + in_memory=True, + parse_mode=enums.ParseMode.DISABLED, + ) + client.invoke = AsyncMock( + return_value=raw.types.Updates(updates=[], users=[], chats=[], date=0, seq=0) + ) + client.resolve_peer = AsyncMock(return_value=raw.types.InputPeerUser(user_id=1, access_hash=2)) + await client.storage.open() + yield client + await client.storage.close() + + +def test_poll_parser_preserves_text_bytes_and_matches_partial_results_by_option(): + poll = types.Poll._parse(None, wire(poll_media())) + assert poll.question == "Question?" + assert [option.text for option in poll.options] == ["First", "Second"] + assert [option.voter_count for option in poll.options] == [0, 3] + assert [option.data for option in poll.options] == [b"a", b"b"] + assert poll.chosen_option_id == 1 + + +def test_minimal_poll_results_and_updates(): + media = poll_media() + media.results = raw.types.PollResults() + poll = types.Poll._parse(None, wire(media)) + assert poll.total_voter_count == 0 + assert [option.voter_count for option in poll.options] == [0, 0] + result = types.Poll._parse_update( + None, wire(raw.types.UpdateMessagePoll(poll_id=123, results=media.results)) + ) + assert result.options == [] + assert result.total_voter_count == 0 + + +@pytest.mark.asyncio +async def test_send_quiz_serializes_input_answers_integer_index_and_empty_entities(client): + await client.send_poll( + 1, + "Question?", + ["First", "Second"], + type=enums.PollType.QUIZ, + correct_option_id=0, + explanation="Explanation", + ) + request = client.invoke.call_args.args[0] + parsed = wire(request) + assert parsed.media.correct_answers == [0] + assert parsed.media.solution_entities == [] + assert parsed.media.poll.hash == 0 + assert all( + isinstance(answer, raw.types.InputPollAnswer) for answer in parsed.media.poll.answers + ) + assert parsed.media.poll.answers[0].text.text == "First" + + +@pytest.mark.asyncio +async def test_stop_poll_uses_new_required_hash(client): + media = poll_media() + client.get_messages = AsyncMock( + return_value=types.Message(id=1, poll=types.Poll._parse(client, media)) + ) + client.invoke.return_value = raw.types.Updates( + updates=[raw.types.UpdateMessagePoll(poll_id=123, poll=media.poll, results=media.results)], + users=[], + chats=[], + date=0, + seq=0, + ) + await client.stop_poll(1, 1) + request = wire(client.invoke.call_args.args[0]) + assert request.media.poll.closed + assert request.media.poll.hash == 0 + + +@pytest.mark.asyncio +async def test_vote_preserves_opaque_options(client): + media = poll_media() + client.get_messages = AsyncMock( + return_value=types.Message(id=1, poll=types.Poll._parse(client, media)) + ) + client.invoke.return_value = raw.types.Updates( + updates=[raw.types.UpdateMessagePoll(poll_id=123, poll=media.poll, results=media.results)], + users=[], + chats=[], + date=0, + seq=0, + ) + await client.vote_poll(1, 1, 1) + assert wire(client.invoke.call_args.args[0]).options == [b"b"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("payload", ["callback", b"\xff\x00"]) +async def test_inline_callback_markup_roundtrip(client, payload): + markup = types.InlineKeyboardMarkup([ + [types.InlineKeyboardButton("Button", callback_data=payload)] + ]) + request = wire(await markup.write(client)) + assert isinstance(request.rows[0], raw.types.KeyboardInlineButtonRow) + assert types.InlineKeyboardMarkup.read(request).inline_keyboard[0][0].callback_data == payload + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("kwargs", "raw_type"), + [ + ({"url": "https://example.com"}, raw.types.InlineButtonTypeUrl), + ( + {"web_app": types.WebAppInfo(url="https://example.com")}, + raw.types.InlineButtonTypeWebView, + ), + ({"switch_inline_query": ""}, raw.types.InlineButtonTypeSwitchInline), + ({"switch_inline_query_current_chat": ""}, raw.types.InlineButtonTypeSwitchInline), + ({"callback_game": types.CallbackGame()}, raw.types.InlineButtonTypeGame), + ({"user_id": 1}, raw.types.InputInlineButtonTypeUserProfile), + ( + {"login_url": types.LoginUrl(url="https://example.com")}, + raw.types.InputInlineButtonTypeUrlAuth, + ), + ], +) +async def test_inline_button_types_remain_serializable(client, kwargs, raw_type): + button = wire(await types.InlineKeyboardButton("Button", **kwargs).write(client)) + assert isinstance(button.type, raw_type) + if raw_type is raw.types.InputInlineButtonTypeUserProfile: + button.type = raw.types.InlineButtonTypeUserProfile(user_id=1) + elif raw_type is raw.types.InputInlineButtonTypeUrlAuth: + button.type = raw.types.InlineButtonTypeUrlAuth(url="https://example.com", button_id=7) + parsed = types.InlineKeyboardButton.read(button) + assert parsed.text == "Button" + if "switch_inline_query_current_chat" in kwargs: + assert parsed.switch_inline_query_current_chat is not None + assert not parsed.switch_inline_query_current_chat + + +@pytest.mark.asyncio +async def test_reply_keyboard_existing_options_roundtrip(client): + markup = types.ReplyKeyboardMarkup([ + [ + "Text", + types.KeyboardButton("Contact", request_contact=True), + types.KeyboardButton("Location", request_location=True), + types.KeyboardButton("App", web_app=types.WebAppInfo(url="https://example.com")), + ] + ]) + parsed = types.ReplyKeyboardMarkup.read(wire(await markup.write(client))) + assert parsed.keyboard[0][0] == "Text" + assert parsed.keyboard[0][1].request_contact + assert parsed.keyboard[0][2].request_location + assert parsed.keyboard[0][3].web_app.url == "https://example.com" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("chat_id", ["channel", "https://t.me/+test"]) +async def test_join_chat_unwraps_success_updates(client, chat_id): + client.invoke.return_value = wire( + raw.types.messages.ChatInviteJoinResultOk( + updates=raw.types.Updates( + updates=[], + users=[], + chats=[ + raw.types.Channel( + id=1, title="Channel", photo=raw.types.ChatPhotoEmpty(), date=1 + ) + ], + date=1, + seq=1, + ) + ) + ) + chat = await client.join_chat(chat_id) + assert chat.title == "Channel" + assert chat._client is client + + +@pytest.mark.asyncio +async def test_join_chat_webview_result_has_actionable_error(client): + client.invoke.return_value = raw.types.messages.ChatInviteJoinResultWebView( + bot_id=1, query_id=2, users=[] + ) + with pytest.raises(errors.NotAcceptable, match="verification"): + await client.join_chat("channel") + + +def test_user_chat_and_thumbnail_parsers_accept_absent_optional_vectors(): + user = wire(raw.types.User(id=1, first_name="Name")) + assert types.User._parse(None, user).usernames is None + assert types.Chat._parse_user_chat(None, user).first_name == "Name" + channel = wire( + raw.types.Channel(id=1, title="Channel", photo=raw.types.ChatPhotoEmpty(), date=1) + ) + assert types.Chat._parse_channel_chat(None, channel).usernames is None + document = wire( + raw.types.Document( + id=1, + access_hash=2, + file_reference=b"", + date=0, + mime_type="text/plain", + size=1, + dc_id=2, + attributes=[], + ) + ) + assert types.Thumbnail._parse(None, document) is None + + +@pytest.mark.asyncio +async def test_plain_message_parser_accepts_absent_entities(client): + user = wire(raw.types.User(id=1, first_name="Name")) + message = wire( + raw.types.Message(id=1, peer_id=raw.types.PeerUser(user_id=1), date=1, message="Hello") + ) + parsed = await types.Message._parse(client=client, message=message, users={1: user}, chats={}) + assert parsed.text == "Hello" + assert not parsed.entities + + +@pytest.mark.asyncio +async def test_invoke_caches_peers_inside_chat_join_result(client): + result = raw.types.messages.ChatInviteJoinResultOk( + updates=raw.types.Updates( + updates=[], users=[raw.types.User(id=1)], chats=[], date=1, seq=1 + ) + ) + client.is_connected = True + client.session = AsyncMock() + client.session.invoke.return_value = result + client.fetch_peers = AsyncMock() + returned = await Invoke.invoke( + client, + raw.functions.channels.JoinChannel( + channel=raw.types.InputChannel(channel_id=1, access_hash=2) + ), + ) + assert returned is result + assert client.fetch_peers.call_args_list[0].args[0] == result.updates.users diff --git a/tests/test_media_transmissions.py b/tests/test_media_transmissions.py new file mode 100644 index 000000000..ce97a82f4 --- /dev/null +++ b/tests/test_media_transmissions.py @@ -0,0 +1,527 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +import importlib +import io +import threading +from contextlib import aclosing +from hashlib import md5, sha256 +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest +import pytest_asyncio + +from hydrogram import Client, StopTransmission, raw +from hydrogram.errors import ConnectionLostError +from hydrogram.file_id import FileId, FileType +from hydrogram.transmission import run_sync + + +@pytest_asyncio.fixture +async def client(): + await asyncio.sleep(0) + app = Client("media-test", in_memory=True, max_concurrent_transmissions=2) + app.storage = SimpleNamespace( + dc_id=AsyncMock(return_value=1), + auth_key=AsyncMock(return_value=b"key"), + test_mode=AsyncMock(return_value=False), + ) + app.me = SimpleNamespace(is_premium=False) + yield app + app.executor.shutdown(wait=True) + + +@pytest.fixture +def file_id(): + return FileId( + file_type=FileType.DOCUMENT, + dc_id=1, + media_id=123, + access_hash=456, + file_reference=b"", + thumbnail_size="", + ) + + +@pytest.fixture +def sessions(monkeypatch): + instances = [] + + class MediaSession: + def __init__(self, *_args, **kwargs): + self.is_cdn = kwargs.get("is_cdn", False) + self.start = AsyncMock() + self.stop = AsyncMock() + self.invoke = AsyncMock(side_effect=self.reply) + instances.append(self) + + async def reply(self, query, **_kwargs): + assert not self.is_cdn + await asyncio.sleep(0) + if isinstance(query, raw.functions.upload.GetFile): + return raw.types.upload.File( + type=raw.types.storage.FileUnknown(), mtime=0, bytes=b"download" + ) + return True + + monkeypatch.setattr(importlib.import_module("hydrogram.client"), "Session", MediaSession) + monkeypatch.setattr( + importlib.import_module("hydrogram.methods.advanced.save_file"), "Session", MediaSession + ) + return SimpleNamespace(instances=instances, cls=MediaSession) + + +def packet(file_id, tmp_path, name="download.bin", in_memory=False, progress=None): + return (file_id, tmp_path, name, in_memory, 8, progress, ()) + + +@pytest.mark.parametrize("limit", [0, -1, 1.5, True, None]) +def test_invalid_limit(limit): + with pytest.raises(ValueError, match="positive integer"): + Client("invalid", max_concurrent_transmissions=limit) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("limit", [1, 2, 3]) +async def test_combined_limit_and_waiting_download_files( + client, file_id, sessions, tmp_path, limit +): + client.save_file_semaphore = client.get_file_semaphore = asyncio.Semaphore(limit) + gate = asyncio.Event() + full = asyncio.Event() + active = maximum = 0 + + async def reply(session, query, **kwargs): + nonlocal active, maximum + active += 1 + maximum = max(maximum, active) + if active == limit: + full.set() + try: + await gate.wait() + return await original(session, query, **kwargs) + finally: + active -= 1 + + original = sessions.cls.reply + sessions.cls.reply = reply + uploads = [asyncio.create_task(client.save_file(io.BytesIO(b"x"))) for _ in range(limit)] + try: + await asyncio.wait_for(full.wait(), 2) + except BaseException: + gate.set() + await asyncio.gather(*uploads, return_exceptions=True) + raise + downloads = [ + asyncio.create_task(client.handle_download(packet(file_id, tmp_path, f"{i}.bin"))) + for i in range(4) + ] + await asyncio.sleep(0) + assert not list(tmp_path.iterdir()) + # A cancelled waiter must not consume capacity or create a file. + downloads[0].cancel() + with pytest.raises(asyncio.CancelledError): + await downloads.pop(0) + gate.set() + await asyncio.wait_for(asyncio.gather(*uploads, *downloads), 3) + assert maximum == limit + assert len(list(tmp_path.iterdir())) == 3 + assert len(client.media_sessions) == 1 + + +@pytest.mark.asyncio +async def test_upload_chunks_progress_and_missing_part(client, sessions): + data = b"x" * (512 * 1024 + 7) + source = io.BytesIO(data) + progress = AsyncMock() + result = await client.save_file(source, progress=progress, progress_args=("extra",)) + assert result.parts == 2 + assert result.md5_checksum == md5(data).hexdigest() + assert not source.closed + session = sessions.instances[0] + assert b"".join(c.args[0].bytes for c in session.invoke.call_args_list) == data + assert [c.args for c in progress.call_args_list] == [ + (512 * 1024, len(data), "extra"), + (len(data), len(data), "extra"), + ] + session.stop.assert_awaited_once() + assert await client.save_file(source, file_id=123, file_part=1) is None + query = sessions.instances[1].invoke.call_args.args[0] + assert query.file_id == 123 + assert query.file_part == 1 + assert query.bytes == b"x" * 7 + + +@pytest.mark.asyncio +async def test_big_upload_bounded_reads(client, sessions): + class Source(io.BytesIO): + def read(self, size=-1): + assert size == 512 * 1024 + return super().read(size) + + result = await client.save_file(Source(b"x" * (11 * 1024 * 1024))) + assert isinstance(result, raw.types.InputFileBig) + assert result.parts == 22 + assert all( + isinstance(c.args[0], raw.functions.upload.SaveBigFilePart) + for c in sessions.instances[0].invoke.call_args_list + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("failure", [ConnectionLostError("uncertain"), OSError("disk"), False]) +async def test_upload_failure_is_not_retried(client, sessions, failure): + async def reply(_session, _query, **_kwargs): + await asyncio.sleep(0) + if isinstance(failure, Exception): + raise failure + return failure + + original = sessions.cls.reply + sessions.cls.reply = reply + progress = AsyncMock() + with pytest.raises(OSError): + await client.save_file(io.BytesIO(b"data"), progress=progress) + session = sessions.instances[0] + session.invoke.assert_awaited_once() + session.stop.assert_awaited_once() + progress.assert_not_called() + sessions.cls.reply = original + assert await asyncio.wait_for(client.save_file(io.BytesIO(b"next")), 2) + + +@pytest.mark.asyncio +async def test_upload_cancel_stops_session_closes_owned_file( + client, sessions, tmp_path, monkeypatch +): + path = tmp_path / "source" + path.write_bytes(b"data") + opened = [] + original_open = type(path).open + + def tracked_open(path, *args, **kwargs): + fp = original_open(path, *args, **kwargs) + opened.append(fp) + return fp + + monkeypatch.setattr(type(path), "open", tracked_open) + entered = asyncio.Event() + + async def reply(*_args, **_kwargs): + entered.set() + await asyncio.Event().wait() + + original = sessions.cls.reply + sessions.cls.reply = reply + task = asyncio.create_task(client.save_file(path)) + await asyncio.wait_for(entered.wait(), 2) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, 2) + assert opened[0].closed + sessions.instances[0].stop.assert_awaited_once() + sessions.cls.reply = original + assert await asyncio.wait_for(client.save_file(io.BytesIO(b"next")), 2) + + +@pytest.mark.asyncio +async def test_invalid_upload_closes_owned_file(client, tmp_path, monkeypatch): + path = tmp_path / "empty" + path.touch() + opened = [] + original_open = type(path).open + + def tracked_open(path, *args, **kwargs): + fp = original_open(path, *args, **kwargs) + opened.append(fp) + return fp + + monkeypatch.setattr(type(path), "open", tracked_open) + with pytest.raises(ValueError, match="0 B"): + await client.save_file(path) + assert opened[0].closed + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cancel", [False, True]) +async def test_session_start_failure_does_not_poison_cache( + client, file_id, sessions, cancel, monkeypatch +): + entered = asyncio.Event() + original_init = sessions.cls.__init__ + + async def start(): + entered.set() + if cancel: + await asyncio.Event().wait() + raise OSError("start failed") + + def init(self, *args, **kwargs): + original_init(self, *args, **kwargs) + self.start = AsyncMock(side_effect=start) + + monkeypatch.setattr(sessions.cls, "__init__", init) + stream = client.get_file(file_id) + task = asyncio.create_task(anext(stream)) + await asyncio.wait_for(entered.wait(), 2) + if cancel: + task.cancel() + with pytest.raises(asyncio.CancelledError if cancel else OSError): + await task + assert not client.media_sessions + sessions.instances[0].stop.assert_awaited_once() + monkeypatch.setattr(sessions.cls, "__init__", original_init) + async with aclosing(client.get_file(file_id)) as stream: + assert await asyncio.wait_for(anext(stream), 2) == b"download" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("in_memory", [False, True]) +@pytest.mark.parametrize( + "failure", [OSError("failed"), StopTransmission(), asyncio.CancelledError()] +) +async def test_download_cleanup(client, file_id, sessions, tmp_path, in_memory, failure): + async def progress(*_args): + await asyncio.sleep(0) + raise failure + + operation = client.handle_download( + packet(file_id, tmp_path, in_memory=in_memory, progress=progress) + ) + if isinstance(failure, asyncio.CancelledError): + with pytest.raises(asyncio.CancelledError): + await operation + else: + assert await operation is None + assert not list(tmp_path.iterdir()) + result = await asyncio.wait_for(client.handle_download(packet(file_id, tmp_path)), 2) + assert result.read_bytes() == b"download" + + +@pytest.mark.asyncio +async def test_stream_close_releases_slot(client, file_id, sessions): + client.save_file_semaphore = client.get_file_semaphore = asyncio.Semaphore(1) + stream = client.stream_media(file_id.encode()) + assert await anext(stream) == b"download" + upload = asyncio.create_task(client.save_file(io.BytesIO(b"x"))) + await asyncio.sleep(0) + assert not upload.done() + await stream.aclose() + assert await asyncio.wait_for(upload, 2) + + +@pytest.mark.asyncio +async def test_executor_cancellation_waits_for_io_without_blocking_loop(): + entered = threading.Event() + release = threading.Event() + + def slow_io(): + entered.set() + assert release.wait(3) + + task = asyncio.create_task(run_sync(slow_io)) + try: + async with asyncio.timeout(2): + while not entered.is_set(): + await asyncio.sleep(0.001) + task.cancel() + await asyncio.sleep(0) + task.cancel() + await asyncio.sleep(0) + assert not task.done() + finally: + release.set() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, 2) + + +@pytest.mark.asyncio +async def test_cdn_cleanup_and_completion(client, file_id, sessions, monkeypatch): + module = importlib.import_module("hydrogram.client") + monkeypatch.setattr( + module, "Auth", lambda *_args: SimpleNamespace(create=AsyncMock(return_value=b"key")) + ) + monkeypatch.setattr(module.aes, "ctr256_decrypt", lambda chunk, *_args: chunk) + content = b"cdn-data" + + async def reply(_session, query, **_kwargs): + await asyncio.sleep(0) + if isinstance(query, raw.functions.upload.GetFile): + return raw.types.upload.FileCdnRedirect( + dc_id=2, + file_token=b"token", + encryption_key=b"k" * 32, + encryption_iv=b"i" * 16, + file_hashes=[], + ) + if isinstance(query, raw.functions.upload.GetCdnFile): + return raw.types.upload.CdnFile(bytes=content) + return [raw.types.FileHash(offset=0, limit=len(content), hash=sha256(content).digest())] + + sessions.cls.reply = reply + assert [chunk async for chunk in client.get_file(file_id)] == [content] + cdn = sessions.instances[1] + assert cdn.is_cdn + cdn.stop.assert_awaited_once() + assert sessions.instances[0].invoke.await_count == 2 + + +@pytest.mark.asyncio +async def test_multiple_download_chunks_and_sync_progress(client, file_id, sessions, tmp_path): + chunks = [b"x" * (1024 * 1024), b"end"] + offsets = [] + progress_calls = [] + + async def reply(_session, query, **_kwargs): + await asyncio.sleep(0) + offsets.append(query.offset) + return raw.types.upload.File( + type=raw.types.storage.FileUnknown(), mtime=0, bytes=chunks[len(offsets) - 1] + ) + + def progress(current, total, tag): + progress_calls.append((current, total, tag, threading.get_ident())) + + sessions.cls.reply = reply + size = sum(map(len, chunks)) + result = await client.handle_download(( + file_id, + tmp_path, + "chunks", + False, + size, + progress, + ("tag",), + )) + assert result.read_bytes() == b"".join(chunks) + assert offsets == [0, 1024 * 1024] + assert [call[:3] for call in progress_calls] == [ + (1024 * 1024, size, "tag"), + (size, size, "tag"), + ] + assert all(call[3] != threading.get_ident() for call in progress_calls) + + +@pytest.mark.asyncio +async def test_concurrent_download_start_is_published_only_when_ready(client, file_id, sessions): + entered = asyncio.Event() + ready = asyncio.Event() + original_init = sessions.cls.__init__ + + async def start(): + entered.set() + await ready.wait() + + def init(self, *args, **kwargs): + original_init(self, *args, **kwargs) + self.start = AsyncMock(side_effect=start) + + sessions.cls.__init__ = init + first = client.get_file(file_id) + second = client.get_file(file_id) + tasks = [asyncio.create_task(anext(first)), asyncio.create_task(anext(second))] + try: + await asyncio.wait_for(entered.wait(), 2) + await asyncio.sleep(0) + assert len(sessions.instances) == 1 + assert not client.media_sessions + sessions.instances[0].invoke.assert_not_awaited() + ready.set() + assert await asyncio.wait_for(asyncio.gather(*tasks), 2) == [b"download"] * 2 + assert len(sessions.instances) == 1 + finally: + ready.set() + await first.aclose() + await second.aclose() + + +@pytest.mark.asyncio +async def test_repeated_cancellation_during_upload_cleanup(client, sessions): + client.save_file_semaphore = client.get_file_semaphore = asyncio.Semaphore(1) + entered = asyncio.Event() + stopping = asyncio.Event() + release = asyncio.Event() + original_init = sessions.cls.__init__ + + async def invoke(*_args, **_kwargs): + entered.set() + await asyncio.Event().wait() + + async def stop(): + stopping.set() + await release.wait() + + def init(self, *args, **kwargs): + original_init(self, *args, **kwargs) + self.invoke = AsyncMock(side_effect=invoke) + self.stop = AsyncMock(side_effect=stop) + + sessions.cls.__init__ = init + task = asyncio.create_task(client.save_file(io.BytesIO(b"data"))) + await asyncio.wait_for(entered.wait(), 2) + task.cancel() + await asyncio.wait_for(stopping.wait(), 2) + task.cancel() + await asyncio.sleep(0) + assert not task.done() + assert client.save_file_semaphore.locked() + release.set() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, 2) + assert not client.save_file_semaphore.locked() + sessions.cls.__init__ = original_init + assert await asyncio.wait_for(client.save_file(io.BytesIO(b"next")), 2) + + +@pytest.mark.asyncio +async def test_in_memory_write_failure_closes_stream( + client, file_id, sessions, tmp_path, monkeypatch +): + class BrokenFile(io.BytesIO): + def write(self, data): + assert not self.closed + raise OSError("write failed") + + stream = BrokenFile() + monkeypatch.setattr(importlib.import_module("hydrogram.client"), "BytesIO", lambda: stream) + assert await client.handle_download(packet(file_id, tmp_path, in_memory=True)) is None + assert stream.closed + assert not client.get_file_semaphore.locked() + + +@pytest.mark.asyncio +async def test_upload_start_and_stop_failures_release_slot(client, sessions): + client.save_file_semaphore = client.get_file_semaphore = asyncio.Semaphore(1) + original_init = sessions.cls.__init__ + + def init(self, *args, **kwargs): + original_init(self, *args, **kwargs) + self.start = AsyncMock(side_effect=OSError("startup")) + self.stop = AsyncMock(side_effect=OSError("cleanup")) + + sessions.cls.__init__ = init + with pytest.raises(OSError, match="cleanup"): + await client.save_file(io.BytesIO(b"data")) + assert not client.save_file_semaphore.locked() + sessions.cls.__init__ = original_init + assert await asyncio.wait_for(client.save_file(io.BytesIO(b"next")), 2) diff --git a/tests/test_mtproxy.py b/tests/test_mtproxy.py new file mode 100644 index 000000000..83886c874 --- /dev/null +++ b/tests/test_mtproxy.py @@ -0,0 +1,429 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +from contextlib import asynccontextmanager +from hashlib import sha256 +from unittest.mock import AsyncMock, Mock + +import pyaes +import pytest +import socks + +from hydrogram import Client +from hydrogram.connection import Connection +from hydrogram.connection.proxy import parse_mtproxy_secret +from hydrogram.connection.transport import TCP, TCPAbridged +from hydrogram.connection.transport.tcp.tcp_mtproxy import TCPMTProxy +from hydrogram.crypto import aes +from hydrogram.session.session import Session + +SECRET = "0123456789abcdef0123456789abcdef" + + +def proxy(**kwargs): + return {"scheme": "mtproxy", "hostname": "127.0.0.1", "port": 443, "secret": SECRET, **kwargs} + + +def cipher(key, iv): + # An independent CTR implementation checks stream position and key derivation. + return pyaes.AESModeOfOperationCTR(key, pyaes.Counter(int.from_bytes(iv, "big"))) + + +@asynccontextmanager +async def server(handler): + tasks = set() + writers = [] + + async def run(reader, writer): + tasks.add(asyncio.current_task()) + writers.append(writer) + try: + await handler(reader, writer) + finally: + writer.close() + await writer.wait_closed() + tasks.discard(asyncio.current_task()) + + listener = await asyncio.start_server(run, "127.0.0.1", 0) + try: + yield listener.sockets[0].getsockname()[1] + finally: + listener.close() + await listener.wait_closed() + for writer in writers: + writer.close() + pending = list(tasks) + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) + + +@pytest.mark.parametrize("secret", [SECRET, SECRET.upper(), "dd" + "00" * 15, "ee" + "00" * 15]) +def test_standard_secret(secret): + assert parse_mtproxy_secret(secret) == bytes.fromhex(secret) + + +@pytest.mark.parametrize( + ("secret", "message"), + [ + (None, "32-character"), + (bytes.fromhex(SECRET), "32-character"), + ("", "32-character"), + ("a" * 31, "32-character"), + ("a" * 33, "32-character"), + ("g" * 32, "32-character"), + (" " + "a" * 30 + " ", "32-character"), + ("dd" + SECRET, "padded intermediate"), + ("ee" + SECRET + b"example.com".hex(), "fake-TLS"), + ], +) +def test_invalid_secret(secret, message): + with pytest.raises(ValueError, match=message) as error: + parse_mtproxy_secret(secret) + if isinstance(secret, str) and len(secret) > 4: + assert secret not in str(error.value) + + +@pytest.mark.parametrize( + "settings", + [ + {"hostname": ""}, + {"hostname": None}, + {"hostname": " host "}, + {"port": 0}, + {"port": 65536}, + {"port": "443"}, + {"port": True}, + {"username": "user"}, + {"password": "password"}, + {"secret": None}, + ], +) +def test_client_validates_mtproxy_before_connect(settings): + with pytest.raises(ValueError, match="MTProxy"): + Client("invalid-proxy", proxy=proxy(**settings)) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("scheme", ["mtproxy", "MTPROXY", "mtproto"]) +async def test_client_accepts_mtproxy(scheme): # noqa: RUF029 - Client needs a running loop + settings = proxy(scheme=scheme) + client = Client("mtproxy", proxy=settings, in_memory=True) + try: + assert client.proxy == settings + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("dc_id", [2, -4, 10002, -10004, 203]) +async def test_handshake_keys_routing_and_stream_position(monkeypatch, dc_id): + nonce = bytes(range(64)) + monkeypatch.setattr( + "hydrogram.connection.transport.tcp.tcp_mtproxy.os.urandom", lambda _: nonce + ) + transport = TCPMTProxy(False, proxy(), dc_id) + header = transport._obfuscation_header() + assert header[:56] == nonce[:56] + outgoing = cipher(sha256(nonce[8:40] + bytes.fromhex(SECRET)).digest(), nonce[40:56]) + decoded = outgoing.decrypt(header) + assert decoded[56:60] == b"\xef" * 4 + assert int.from_bytes(decoded[60:62], "little", signed=True) == dc_id + assert decoded[62:64] == nonce[62:64] + # The client's outgoing stream has consumed all 64 handshake bytes. + assert aes.ctr256_encrypt(b"next packet", *transport.encrypt) == outgoing.encrypt( + b"next packet" + ) + await transport.close() + reverse = nonce[55:7:-1] + incoming = cipher(sha256(reverse[:32] + bytes.fromhex(SECRET)).digest(), reverse[32:48]) + assert aes.ctr256_decrypt(incoming.encrypt(b"reply"), *transport.decrypt) == b"reply" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "prefix", + [ + b"HEAD", + b"POST", + b"GET ", + b"OPTI", + b"\xee" * 4, + b"\xdd" * 4, + b"\x16\x03\x01\x02", + b"\xefabc", + b"abcd" + b"\x00" * 4, + ], +) +async def test_handshake_rejects_reserved_nonces(monkeypatch, prefix): + candidates = iter([prefix + b"x" * (64 - len(prefix)), bytes(range(64))]) + monkeypatch.setattr( + "hydrogram.connection.transport.tcp.tcp_mtproxy.os.urandom", lambda _: next(candidates) + ) + transport = TCPMTProxy(False, proxy(), 2) + assert transport._obfuscation_header()[:56] == bytes(range(56)) + await transport.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("test_mode", "media", "expected"), + [(False, False, 2), (False, True, -2), (True, False, 10002), (True, True, -10002)], +) +async def test_local_mtproxy_exchange(test_mode, media, expected): + completed = asyncio.get_running_loop().create_future() + payloads = [b"a" * 4, b"b" * 508, b"c" * 1024] + + async def relay(reader, writer): + try: + header = await reader.readexactly(64) + key = sha256(header[8:40] + bytes.fromhex(SECRET)).digest() + decoder = cipher(key, header[40:56]) + decoded = decoder.decrypt(header) + assert decoded[56:60] == b"\xef" * 4 + assert int.from_bytes(decoded[60:62], "little", signed=True) == expected + reverse = header[55:7:-1] + encoder = cipher(sha256(reverse[:32] + bytes.fromhex(SECRET)).digest(), reverse[32:48]) + for payload in payloads: + size = decoder.decrypt(await reader.readexactly(1))[0] + if size == 127: + size = int.from_bytes(decoder.decrypt(await reader.readexactly(3)), "little") + assert decoder.decrypt(await reader.readexactly(size * 4)) == payload + size = len(payload) // 4 + frame = ( + bytes([size]) if size < 127 else b"\x7f" + size.to_bytes(3, "little") + ) + payload + encrypted = encoder.encrypt(frame) + # Exercise partial reads across both the length and payload. + for chunk in (encrypted[:1], encrypted[1:3], encrypted[3:]): + writer.write(chunk) + await writer.drain() + completed.set_result(True) + except Exception as error: + completed.set_exception(error) + + async with server(relay) as port: + connection = Connection(2, test_mode, True, proxy(port=port), media=media) + try: + await asyncio.wait_for(connection.connect(), 2) + assert isinstance(connection.protocol, TCPMTProxy) + await asyncio.gather(*(connection.send(payload) for payload in payloads)) + for payload in payloads: + assert await asyncio.wait_for(connection.recv(), 2) == payload + assert await asyncio.wait_for(completed, 2) + assert await asyncio.wait_for(connection.recv(), 2) is None + assert connection.protocol.writer.is_closing() + finally: + await connection.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("failure", [OSError("offline"), TimeoutError(), asyncio.CancelledError()]) +async def test_failed_connect_closes_socket(monkeypatch, failure): + transport = TCPMTProxy(False, proxy(), 2) + transport.reader = Mock(at_eof=Mock(return_value=False)) + transport.writer = Mock(wait_closed=AsyncMock(), is_closing=Mock(return_value=False)) + monkeypatch.setattr(transport, "_connect", AsyncMock(side_effect=failure)) + with pytest.raises(type(failure)): + await transport.connect(("unused", 443)) + assert transport.closed + transport.writer.close.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cancel", [False, True]) +async def test_handshake_timeout_or_cancellation_closes_socket(monkeypatch, cancel): + started = asyncio.Event() + transport = TCPMTProxy(False, proxy(), 2) + monkeypatch.setattr(transport, "TIMEOUT", 0.02) + + async def drain(): + started.set() + await asyncio.Event().wait() + + async def connect(_): + await asyncio.sleep(0) + transport.reader = Mock(at_eof=Mock(return_value=False)) + transport.writer = Mock( + drain=drain, wait_closed=AsyncMock(), is_closing=Mock(return_value=False) + ) + + monkeypatch.setattr(transport, "_connect", connect) + task = asyncio.create_task(transport.connect(("unused", 443))) + await asyncio.wait_for(started.wait(), 1) + if cancel: + task.cancel() + with pytest.raises(asyncio.CancelledError if cancel else OSError): + await asyncio.wait_for(task, 1) + assert transport.closed + transport.writer.close.assert_called_once() + + +@pytest.mark.asyncio +async def test_receive_cancellation_closes_socket(): + ready = asyncio.Event() + + async def relay(reader, writer): + await reader.readexactly(64) + ready.set() + await reader.read() + + async with server(relay) as port: + transport = TCPMTProxy(False, proxy(port=port), 2) + await transport.connect(("unused", 443)) + await asyncio.wait_for(ready.wait(), 1) + task = asyncio.create_task(transport.recv()) + await asyncio.sleep(0) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + assert transport.closed + assert transport.writer.is_closing() + + +@pytest.mark.asyncio +async def test_direct_abridged_connection_is_unchanged(): + received = asyncio.get_running_loop().create_future() + + async def relay(reader, writer): + received.set_result(await reader.readexactly(6)) + + async with server(relay) as port: + transport = TCPAbridged(False, None) + try: + await transport.connect(("127.0.0.1", port)) + await transport.send(b"test") + assert await asyncio.wait_for(received, 1) == b"\xef\x01test" + finally: + await transport.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("scheme", "kind"), [("socks4", socks.SOCKS4), ("SOCKS5", socks.SOCKS5), ("http", socks.HTTP)] +) +async def test_legacy_proxy_configuration(monkeypatch, scheme, kind): + settings = { + "scheme": scheme, + "hostname": "localhost", + "port": 8080, + "username": "user", + "password": "pass", + } + sock = Mock() + monkeypatch.setattr( + "hydrogram.connection.transport.tcp.tcp.socks.socksocket", Mock(return_value=sock) + ) + monkeypatch.setattr( + "hydrogram.connection.transport.tcp.tcp.asyncio.open_connection", + AsyncMock(return_value=(Mock(), Mock())), + ) + transport = TCP(False, settings) + monkeypatch.setattr(transport.loop, "sock_connect", AsyncMock()) + await transport.connect(("149.154.167.51", 443)) + sock.set_proxy.assert_called_once_with( + proxy_type=kind, addr="localhost", port=8080, username="user", password="pass" + ) + transport.loop.sock_connect.assert_awaited_once_with( + sock=sock, address=("149.154.167.51", 443) + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cancel", [False, True]) +async def test_packet_send_failure_closes_socket(cancel): + transport = TCPMTProxy(False, proxy(), 2) + transport._obfuscation_header() + transport._closed = False + started = asyncio.Event() + + async def drain(): + started.set() + if cancel: + await asyncio.Event().wait() + raise OSError("reset") + + transport.reader = Mock(at_eof=Mock(return_value=False)) + transport.writer = Mock( + drain=drain, wait_closed=AsyncMock(), is_closing=Mock(return_value=False) + ) + task = asyncio.create_task(transport.send(b"test")) + await asyncio.wait_for(started.wait(), 1) + if cancel: + task.cancel() + with pytest.raises(asyncio.CancelledError if cancel else OSError): + await task + assert transport.closed + transport.writer.close.assert_called_once() + + +@pytest.mark.asyncio +async def test_connect_timeout_closes_socket(monkeypatch): + transport = TCPMTProxy(False, proxy(), 2) + monkeypatch.setattr(TCP, "TIMEOUT", 0.01) + + async def connect(_): + transport.writer = Mock(wait_closed=AsyncMock()) + await asyncio.Event().wait() + + monkeypatch.setattr(transport, "_connect", connect) + with pytest.raises(TimeoutError, match="Connection timed out"): + await transport.connect(("unused", 443)) + assert transport.closed + transport.writer.close.assert_called_once() + + +@pytest.mark.asyncio +async def test_truncated_proxy_frame_closes_socket(): + async def relay(reader, writer): + header = await reader.readexactly(64) + reverse = header[55:7:-1] + encoder = cipher(sha256(reverse[:32] + bytes.fromhex(SECRET)).digest(), reverse[32:48]) + writer.write(encoder.encrypt(b"\x7f\x01")) + await writer.drain() + + async with server(relay) as port: + transport = TCPMTProxy(False, proxy(port=port), 2) + try: + await transport.connect(("unused", 443)) + assert await asyncio.wait_for(transport.recv(), 1) is None + assert transport.writer.is_closing() + finally: + await transport.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("is_cdn", [False, True]) +async def test_session_routes_cdn_without_media_sign(is_cdn): + client = Client("route", in_memory=True, proxy=proxy()) + session = Session( + client, 203 if is_cdn else 2, b"a" * 256, False, is_media=True, is_cdn=is_cdn + ) + # Stop at the connection boundary, before creating receive workers or sending RPCs. + connection = Mock(connect=AsyncMock(side_effect=OSError("stop after routing"))) + client.connection_factory = Mock(return_value=connection) + try: + with pytest.raises(OSError, match="stop after routing"): + await session._connect() + assert client.connection_factory.call_args.kwargs["media"] is (not is_cdn) + finally: + client.executor.shutdown() diff --git a/tests/test_random_id.py b/tests/test_random_id.py new file mode 100644 index 000000000..8a4b96ca5 --- /dev/null +++ b/tests/test_random_id.py @@ -0,0 +1,205 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +import inspect +from io import BytesIO +from unittest.mock import AsyncMock, Mock + +import pytest + +from hydrogram import Client, raw +from hydrogram.errors import FilePartMissing +from hydrogram.file_id import FileId, FileType +from hydrogram.session.internals import MsgId + + +# Exercise real public methods and media construction, stopping at the network boundary. +class RequestCapturedError(Exception): + def __init__(self, request): + self.request = request + + +async def capture_request(request): + await asyncio.sleep(0) + raise RequestCapturedError(request) + + +def file_id(file_type): + return FileId(file_type=file_type, dc_id=2, media_id=42, access_hash=123).encode() + + +SEND_CASES = [ + ("send_message", ("text",), raw.functions.messages.SendMessage), + ("send_animation", (file_id(FileType.ANIMATION),), raw.functions.messages.SendMedia), + ("send_audio", (file_id(FileType.AUDIO),), raw.functions.messages.SendMedia), + ("send_cached_media", (file_id(FileType.DOCUMENT),), raw.functions.messages.SendMedia), + ("send_contact", ("123456789", "Alice", "Smith"), raw.functions.messages.SendMedia), + ("send_dice", ("🎲",), raw.functions.messages.SendMedia), + ("send_document", (file_id(FileType.DOCUMENT),), raw.functions.messages.SendMedia), + ("send_location", (1.0, 2.0), raw.functions.messages.SendMedia), + ("send_photo", ("https://example.com/photo.jpg", "caption"), raw.functions.messages.SendMedia), + ("send_poll", ("Question?", ["Yes", "No"]), raw.functions.messages.SendMedia), + ("send_sticker", (file_id(FileType.STICKER),), raw.functions.messages.SendMedia), + ("send_venue", (1.0, 2.0, "Title", "Address"), raw.functions.messages.SendMedia), + ("send_video", (file_id(FileType.VIDEO),), raw.functions.messages.SendMedia), + ("send_video_note", (file_id(FileType.VIDEO_NOTE),), raw.functions.messages.SendMedia), + ("send_voice", (file_id(FileType.VOICE),), raw.functions.messages.SendMedia), + ("send_game", ("game",), raw.functions.messages.SendMedia), + ("send_inline_bot_result", (42, "result"), raw.functions.messages.SendInlineBotResult), +] + + +@pytest.fixture +def client(): + app = Client("random-id-tests", in_memory=True) + app.resolve_peer = AsyncMock( + return_value=raw.types.InputPeerUser(user_id=123, access_hash=456) + ) + app.invoke = AsyncMock(side_effect=capture_request) + app.rnd_id = Mock(wraps=MsgId) + return app + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("method", "args", "request_type"), SEND_CASES) +@pytest.mark.parametrize("random_id", [-(1 << 63), -123, 0, 123, (1 << 63) - 1]) +async def test_supplied_id(client, method, args, request_type, random_id): + parameter = inspect.signature(getattr(Client, method)).parameters["random_id"] + assert parameter.kind == inspect.Parameter.KEYWORD_ONLY + assert parameter.default is None + generator = client.rnd_id + with pytest.raises(RequestCapturedError) as captured: + await getattr(client, method)("me", *args, random_id=random_id) + request = captured.value.request + assert isinstance(request, request_type) + assert request.random_id == random_id + # Poll IDs belong to the poll object, independently of the send ID. + if method == "send_poll": + assert generator.call_count == 1 + assert request.media.poll.id != random_id + else: + generator.assert_not_called() + assert client.rnd_id is generator + request.write() # The complete accepted range must serialize as a TL long. + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("method", "args", "request_type"), SEND_CASES) +@pytest.mark.parametrize("explicit_none", [False, True]) +async def test_automatic_id(client, method, args, request_type, explicit_none): + client.rnd_id = Mock(side_effect=[901, 902]) + kwargs = {"random_id": None} if explicit_none else {} + with pytest.raises(RequestCapturedError) as captured: + await getattr(client, method)("me", *args, **kwargs) + request = captured.value.request + assert isinstance(request, request_type) + if method == "send_poll": + assert request.media.poll.id == 901 + assert request.random_id == 902 + assert client.rnd_id.call_count == 2 + else: + assert request.random_id == 901 + client.rnd_id.assert_called_once_with() + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("method", "args", "request_type"), SEND_CASES) +@pytest.mark.parametrize( + ("value", "error"), + [ + (True, TypeError), + (False, TypeError), + (1.0, TypeError), + ("1", TypeError), + (b"1", TypeError), + ([], TypeError), + (-(1 << 63) - 1, ValueError), + (1 << 63, ValueError), + ], +) +async def test_invalid_id_before_network(client, method, args, request_type, value, error): + with pytest.raises(error, match="random_id"): + await getattr(client, method)("me", *args, random_id=value) + client.resolve_peer.assert_not_called() + client.invoke.assert_not_called() + client.rnd_id.assert_not_called() + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("method", "args", "request_type"), SEND_CASES) +async def test_concurrent_sends_keep_ids_local(client, method, args, request_type): + generator = client.rnd_id + send = getattr(client, method) + calls = [send("me", *args, random_id=i) for i in range(-20, 20)] + calls += [send("me", *args) for _ in range(40)] + results = await asyncio.gather(*calls, return_exceptions=True) + assert all(isinstance(result, RequestCapturedError) for result in results) + ids = [result.request.random_id for result in results] + assert ids[:40] == list(range(-20, 20)) + assert len(set(ids[40:])) == 40 + assert generator.call_count == (120 if method == "send_poll" else 40) + assert client.rnd_id is generator + + +@pytest.mark.asyncio +@pytest.mark.parametrize("random_id", [None, -123]) +async def test_upload_retry_keeps_supplied_id(client, random_id): + photo = BytesIO(b"photo") + photo.name = "photo.jpg" + client.save_file = AsyncMock( + return_value=raw.types.InputFile(id=1, parts=1, name=photo.name, md5_checksum="") + ) + client.rnd_id = Mock(side_effect=[901, 902]) + attempts = [] + + def retry(request): + attempts.append(request) + if len(attempts) == 1: + raise FilePartMissing(0) + raise RequestCapturedError(request) + + client.invoke.side_effect = retry + with pytest.raises(RequestCapturedError): + await client.send_photo("me", photo, random_id=random_id) + assert [request.random_id for request in attempts] == ( + [901, 902] if random_id is None else [-123, -123] + ) + client.save_file.assert_any_await(photo, file_id=1, file_part=0) + + +@pytest.mark.asyncio +async def test_job_retry_reuses_id_after_lost_response(client): + requests = [] + + def lost_response(request): + requests.append(request) + if len(requests) == 1: + raise TimeoutError + return raw.types.UpdateShortSentMessage(id=7, pts=1, pts_count=1, date=1, out=True) + + client.invoke.side_effect = lost_response + with pytest.raises(TimeoutError): + await client.send_message("me", "queued", random_id=123) + message = await client.send_message("me", "queued", random_id=123) + assert [request.random_id for request in requests] == [123, 123] + assert message.id == 7 + assert message.text == "queued" + client.rnd_id.assert_not_called() diff --git a/tests/test_raw_schema.py b/tests/test_raw_schema.py new file mode 100644 index 000000000..a5e4716b1 --- /dev/null +++ b/tests/test_raw_schema.py @@ -0,0 +1,252 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import ast +import importlib +import inspect +import re +import struct +from io import BytesIO +from pathlib import Path + +import pytest + +from compiler.api import compiler +from dev_tools import check_api_schema_updates as updater +from hydrogram import errors, raw +from hydrogram.raw.core import TLObject + +ROOT = Path(__file__).resolve().parents[1] + + +def schema_cases(): + cases = [] + section = None + for path in (ROOT / "compiler/api/source").glob("*.tl"): + for line in path.read_text().splitlines(): + if match := compiler.SECTION_RE.match(line): + section = match[1] + if match := compiler.COMBINATOR_RE.match(line): + name, constructor_id, _ = match.groups() + namespace, _, name = name.rpartition(".") + qualname = ".".join(filter(None, [namespace, compiler.camel(name)])) + cases.append((f"{section}.{qualname}", int(constructor_id, 16), line)) + return cases + + +def field_value(tl_type): + tl_type = tl_type.split("?")[-1] + if tl_type in {"int", "long", "int128", "int256"}: + return 42 + if tl_type == "double": + return 1.25 + if tl_type == "string": + return "Hydrogram 🌊" + if tl_type == "bytes": + return b"\x00\xffpayload" + if tl_type in {"Bool", "true"}: + return True + if "vector" in tl_type.lower(): + return [field_value(tl_type.split("<", 1)[1][:-1])] + # TLObject dispatch is polymorphic. Exercise a nested boxed value without + # recursively constructing potentially cyclic application object graphs. + return raw.types.InputPeerSelf() + + +@pytest.mark.parametrize(("qualname", "constructor_id", "line"), schema_cases()) +@pytest.mark.parametrize("with_optional", [False, True]) +def test_every_generated_declaration_roundtrips(qualname, constructor_id, line, with_optional): + namespace, _, name = qualname.rpartition(".") + cls = getattr(importlib.import_module("hydrogram.raw." + namespace), name) + kwargs = {} + for arg_name, tl_type in compiler.ARGS_RE.findall(line): + if tl_type == "#" or ("?" in tl_type and not with_optional): + continue + arg_name = {"self": "is_self", "from": "from_peer"}.get(arg_name, arg_name) + kwargs[arg_name] = field_value(tl_type) + value = cls(**kwargs) + payload = value.write() + assert int.from_bytes(payload[:4], "little") == constructor_id == cls.ID + stream = BytesIO(payload) + parsed = TLObject.read(stream) + assert type(parsed) is cls + assert stream.read() == b"" + assert parsed.write() == payload + + +def test_optional_empty_vector_keeps_its_flag_and_stream_alignment(): + value = raw.types.InputMediaPoll( + poll=raw.types.Poll( + id=1, + hash=0, + question=raw.types.TextWithEntities(text="Quiz?", entities=[]), + answers=[], + ), + correct_answers=[0], + solution="Explanation", + solution_entities=[], + ) + stream = BytesIO(value.write() + raw.types.PeerUser(user_id=123).write()) + parsed = TLObject.read(stream) + assert parsed.solution_entities == [] + assert parsed.correct_answers == [0] + assert TLObject.read(stream).user_id == 123 + assert stream.read() == b"" + + +def test_pinned_schema_provenance(): + updater.check_provenance(ROOT) + assert raw.all.layer == updater.get_layer((ROOT / updater.SCHEMA_PATH).read_text()) + + +def test_handwritten_raw_references_and_call_signatures(): + # These stale references predate this update (already absent in layer 223). + # Keep the baseline explicit so future updates cannot silently add regressions. + known_missing = { + "raw.types.Wallpaper": {"hydrogram/types/user_and_chats/chat_background.py"}, + "raw.functions.channels.EditForumTopic": { + f"hydrogram/methods/chats/{module}.py" + for module in ( + "close_forum_topic", + "close_general_topic", + "edit_forum_topic", + "edit_general_topic", + "hide_general_topic", + "reopen_forum_topic", + "reopen_general_topic", + "unhide_general_topic", + ) + }, + **{ + f"raw.functions.channels.{name}": {f"hydrogram/methods/chats/{module}.py"} + for name, module in ( + ("CreateForumTopic", "create_forum_topic"), + ("DeleteTopicHistory", "delete_forum_topic"), + ("GetForumTopics", "get_forum_topics"), + ("GetForumTopicsByID", "get_forum_topics_by_id"), + ("EditCreator", "transfer_chat_ownership"), + ) + }, + } + failures = [] + for path in (ROOT / "hydrogram").rglob("*.py"): + if "raw" in path.relative_to(ROOT / "hydrogram").parts: + continue + tree = ast.parse(path.read_text()) + for node in ast.walk(tree): + if not isinstance(node, ast.Attribute): + continue + # Inspect only the outermost raw.types/functions/base attribute. + dotted = ast.unparse(node) + if not re.fullmatch(r"raw\.(?:types|functions|base)\.[\w.]+", dotted): + continue + obj = raw + try: + for part in dotted.split(".")[1:]: + obj = getattr(obj, part) + except AttributeError: + if path.relative_to(ROOT).as_posix() not in known_missing.get(dotted, set()): + failures.append(f"{path.relative_to(ROOT)}:{node.lineno}: missing {dotted}") + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute): + continue + dotted = ast.unparse(node.func) + if not re.fullmatch(r"raw\.(?:types|functions)\.[\w.]+", dotted): + continue + obj = raw + for part in dotted.split(".")[1:]: + obj = getattr(obj, part, None) + if ( + obj is None + or not inspect.isclass(obj) + or any(k.arg is None for k in node.keywords) + ): + continue + try: + inspect.signature(obj).bind( + *[None for _ in node.args], **{k.arg: None for k in node.keywords} + ) + except TypeError as error: + failures.append(f"{path.relative_to(ROOT)}:{node.lineno}: {dotted}: {error}") + assert not failures, "\n".join(failures) + + +@pytest.mark.parametrize( + ("code", "name", "exception"), + [ + (400, "REVOTE_NOT_ALLOWED", "RevoteNotAllowed"), + (406, "POLL_COUNTRY_RESTRICTED", "PollCountryRestricted"), + (406, "POLL_MEMBER_RESTRICTED", "PollMemberRestricted"), + ], +) +def test_poll_rpc_errors(code, name, exception): + with pytest.raises(getattr(errors, exception)): + errors.RPCError.raise_it( + raw.types.RpcError(error_code=code, error_message=name), + raw.functions.messages.SendVote, + ) + + +def test_generated_registry_covers_every_schema_declaration(): + expected = {} + for path in (ROOT / "compiler/api/source").glob("*.tl"): + for name, declaration in updater.definitions(path.read_text()).items(): + constructor_id = int(declaration.split("#", 1)[1].split()[0], 16) + expected[constructor_id] = name + assert expected.keys() <= raw.all.objects.keys() + + +@pytest.mark.parametrize( + "declaration", + [ + "unsupported syntax;", + "example#1234 data:Vector> = Example;", + "example#1234 data:flags10.0?int = Example;", + "example#1234 data:unknown$syntax = Example;", + ], +) +def test_compiler_rejects_unsupported_syntax(declaration): + with pytest.raises(ValueError, match="Unsupported"): + compiler.validate_schema(["---types---", declaration]) + + +def test_transport_config_bare_vectors_match_wire_format(): + value = raw.types.help.ConfigSimple( + date=1, + expires=2, + rules=[ + raw.types.AccessPointRule( + phone_prefix_rules="", + dc_id=2, + ips=[raw.types.IpPort(ipv4=0x7F000001, port=443)], + ) + ], + ) + expected = ( + struct.pack(" +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import json +import subprocess +from unittest.mock import Mock + +import pytest +import requests + +from dev_tools import check_api_schema_updates as updater + +BASICS = """boolFalse#bc799737 = Bool; +boolTrue#997275b5 = Bool; +true#3fedd339 = True; +vector#1cb5c415 {t:Type} # [ t ] = Vector t; +error#c4b9f9bb code:int text:string = Error; +null#56730bcc = Null; +""" + + +def upstream(layer=100, declaration="example#1234 value:int = Example;"): + return ( + f"{BASICS}\n{declaration}\n---functions---\ngetExample#5678 = Example;\n// LAYER {layer}\n" + ) + + +@pytest.fixture +def checkout(tmp_path, monkeypatch): + monkeypatch.setattr(updater, "REPO_HOME", tmp_path) + path = tmp_path / updater.SCHEMA_PATH + path.parent.mkdir(parents=True) + path.write_text(updater.normalize_schema(upstream())) + (tmp_path / updater.PROVENANCE_PATH).write_text( + json.dumps({"layer": 100, "normalized_sha256": updater.sha256(path.read_bytes())}) + ) + monkeypatch.setattr(updater, "validate_candidate", Mock()) + return tmp_path + + +def mock_sources(monkeypatch, desktop, tdlib=None): + def fetch(repo, path, revision): + content = desktop if repo == updater.DESKTOP_REPO else (tdlib or desktop) + return content, { + "repository": repo, + "path": path, + "revision": "a" * 40, + "url": f"https://raw.githubusercontent.com/{repo}/{'a' * 40}/{path}", + "sha256": updater.sha256(content.encode()), + } + + monkeypatch.setattr(updater, "fetch_source", fetch) + + +def test_check_is_read_only_and_detects_same_layer_signature_drift(checkout, monkeypatch): + path = checkout / updater.SCHEMA_PATH + before = path.read_bytes() + mock_sources(monkeypatch, upstream(declaration="example#1234 value:long = Example;")) + report = checkout / "report.md" + assert updater.main(["--report", str(report)]) == 2 + assert path.read_bytes() == before + assert "Changed (1)" in report.read_text() + assert "value:long" in report.read_text() + updater.validate_candidate.assert_not_called() + + +def test_current_returns_success(checkout, monkeypatch): + mock_sources(monkeypatch, upstream()) + assert updater.main([]) == 0 + + +def test_metadata_only_revision_does_not_cause_drift(checkout, monkeypatch): + mock_sources(monkeypatch, upstream()) + assert updater.main(["--apply"]) == 0 + updater.validate_candidate.assert_called_once_with(checkout, {}) + + +def test_apply_validates_candidate_before_replacing_inputs(checkout, monkeypatch): + mock_sources(monkeypatch, upstream(101)) + path = checkout / updater.SCHEMA_PATH + + def validate(repo, replacements): + assert updater.get_layer(path.read_text()) == 100 + assert updater.get_layer(replacements[updater.SCHEMA_PATH]) == 101 + + monkeypatch.setattr(updater, "validate_candidate", validate) + assert updater.main(["--apply"]) == 0 + updater.check_provenance(checkout) + assert updater.get_layer(path.read_text()) == 101 + + +def test_validation_failure_preserves_both_inputs(checkout, monkeypatch): + mock_sources(monkeypatch, upstream(101)) + before = { + path: (checkout / path).read_bytes() + for path in (updater.SCHEMA_PATH, updater.PROVENANCE_PATH) + } + updater.validate_candidate.side_effect = subprocess.CalledProcessError(1, ["pytest"]) + assert updater.main(["--apply"]) == 1 + for path, content in before.items(): + assert (checkout / path).read_bytes() == content + + +@pytest.mark.parametrize( + "candidate", [upstream(99), upstream().replace("// LAYER 100", ""), "unavailable"] +) +def test_rejects_downgrades_and_malformed_responses(checkout, monkeypatch, candidate): + mock_sources(monkeypatch, candidate) + assert updater.main(["--apply"]) == 1 + updater.validate_candidate.assert_not_called() + + +def test_disagreement_blocks_adoption_and_is_reported(checkout, monkeypatch): + mock_sources(monkeypatch, upstream(101), upstream(101, "example#1234 value:bytes = Example;")) + report = checkout / "report.md" + assert updater.main(["--apply", "--report", str(report)]) == 1 + assert "disagreements: ['example']" in report.read_text() + updater.validate_candidate.assert_not_called() + + +def test_missing_tdlib_declaration_blocks_adoption(checkout, monkeypatch): + mock_sources(monkeypatch, upstream(101), upstream(101, "another#4567 = Example;")) + assert updater.main(["--apply"]) == 1 + updater.validate_candidate.assert_not_called() + + +def test_network_failure_preserves_inputs(checkout, monkeypatch): + fetch = Mock(side_effect=requests.Timeout("upstream timed out")) + monkeypatch.setattr(updater, "fetch_source", fetch) + assert updater.main(["--apply"]) == 1 + assert updater.get_layer((checkout / updater.SCHEMA_PATH).read_text()) == 100 + + +def test_offline_validation_never_fetches(checkout, monkeypatch): + fetch = Mock(side_effect=AssertionError("offline validation accessed the network")) + monkeypatch.setattr(updater, "fetch_source", fetch) + assert updater.main(["--validate"]) == 0 + fetch.assert_not_called() + updater.validate_candidate.assert_called_once_with(checkout, {}) + + +def test_changed_local_schema_fails_provenance_validation(checkout): + (checkout / updater.SCHEMA_PATH).write_text(updater.normalize_schema(upstream(101))) + assert updater.main(["--validate"]) == 1 + updater.validate_candidate.assert_not_called() + + +@pytest.mark.parametrize( + "suffix", ["example#1234 value:int = Example;", "another#1234 = Example;"] +) +def test_duplicate_names_and_ids_are_rejected(suffix): + with pytest.raises(ValueError, match="Duplicate"): + updater.normalize_schema(upstream(declaration=f"example#1234 = Example;\n{suffix}")) + + +def test_normalization_keeps_one_section_and_comments_only_manual_types(): + schema = updater.normalize_schema("---types---\n" + upstream()) + assert schema.count("---types---") == 1 + assert schema.endswith("// LAYER 100\n") + assert "boolTrue" not in updater.definitions(schema) + assert updater.normalize_schema("---types---\n" + upstream()) == schema + + +def test_fetch_uses_immutable_commit_and_timeout(monkeypatch): + commit = "b" * 40 + metadata = Mock() + metadata.json.return_value = {"sha": commit} + content = Mock(content=upstream().encode()) + get = Mock(side_effect=[metadata, content]) + monkeypatch.setattr(updater.requests, "get", get) + schema, source = updater.fetch_source(updater.DESKTOP_REPO, updater.DESKTOP_PATH, "dev") + assert schema == upstream() + assert source["revision"] == commit + assert f"/{commit}/" in get.call_args_list[1].args[0] + assert all(call.kwargs["timeout"] == 30 for call in get.call_args_list) + + +def test_changed_manual_primitive_is_rejected(): + with pytest.raises(ValueError, match="basic types changed"): + updater.normalize_schema(upstream().replace("boolTrue#997275b5", "boolTrue#997275b6")) + + +def test_validation_timeout_preserves_inputs(checkout, monkeypatch): + mock_sources(monkeypatch, upstream(101)) + updater.validate_candidate.side_effect = subprocess.TimeoutExpired("pytest", 300) + assert updater.main(["--apply"]) == 1 + assert updater.get_layer((checkout / updater.SCHEMA_PATH).read_text()) == 100 diff --git a/tests/test_update_recovery.py b/tests/test_update_recovery.py new file mode 100644 index 000000000..ba1b2f4fa --- /dev/null +++ b/tests/test_update_recovery.py @@ -0,0 +1,906 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2023 Dan +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hydrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hydrogram. If not, see . + +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest +import pytest_asyncio + +from hydrogram import Client, raw +from hydrogram.errors import ChannelPrivate, PersistentTimestampInvalid +from hydrogram.session.internals import MsgId +from hydrogram.session.session import Result, Session +from hydrogram.storage import SQLiteStorage +from hydrogram.update_manager import UpdateGapError, UpdateManager + + +def state(pts=10, qts=0, seq=1, date=100): + return raw.types.updates.State(pts=pts, qts=qts, seq=seq, date=date, unread_count=0) + + +def message(id=11, channel_id=None): + peer = ( + raw.types.PeerChannel(channel_id=channel_id) + if channel_id + else raw.types.PeerUser(user_id=1) + ) + return raw.types.Message(id=id, peer_id=peer, date=101, message=str(id)) + + +def update(pts=11, channel_id=None): + cls = raw.types.UpdateNewChannelMessage if channel_id else raw.types.UpdateNewMessage + return cls(message=message(pts, channel_id), pts=pts, pts_count=1) + + +def container(*updates, seq=0, date=101): + return raw.types.Updates(updates=list(updates), users=[], chats=[], date=date, seq=seq) + + +def difference(pts=11, *, messages=None, updates=None, sliced=False, seq=1, qts=0): + cls = raw.types.updates.DifferenceSlice if sliced else raw.types.updates.Difference + return cls( + new_messages=messages or [], + new_encrypted_messages=[], + other_updates=updates or [], + users=[], + chats=[], + **{("intermediate_state" if sliced else "state"): state(pts, qts=qts, seq=seq)}, + ) + + +def channel_difference(pts=11, *, messages=None, updates=None, final=True): + return raw.types.updates.ChannelDifference( + pts=pts, + new_messages=messages or [], + other_updates=updates or [], + users=[], + chats=[], + final=final, + ) + + +@pytest_asyncio.fixture +async def manager(): + storage = SQLiteStorage("test", use_memory=True) + await storage.open() + client = SimpleNamespace( + storage=storage, + dispatcher=SimpleNamespace(updates_queue=asyncio.Queue()), + invoke=AsyncMock(), + fetch_peers=AsyncMock(), + resolve_peer=AsyncMock( + return_value=raw.types.InputPeerChannel(channel_id=7, access_hash=8) + ), + ) + manager = UpdateManager(client) + await manager._set_common(state()) + yield manager + await manager.stop() + await storage.close() + + +def dispatched(manager): + queue = manager.client.dispatcher.updates_queue + items = [] + while not queue.empty(): + items.append(queue.get_nowait()[0]) + return items + + +@pytest.mark.asyncio +async def test_contiguous_updates_and_duplicates_make_no_requests(manager): + await manager.process(container(update(), seq=2)) + await manager.process(container(update(), seq=2)) + assert [u.message.id for u in dispatched(manager)] == [11] + manager.client.invoke.assert_not_awaited() + assert (await manager.client.storage.get_update_states())[0] == (11, 0, 101, 2) + + +@pytest.mark.asyncio +async def test_gap_replays_all_slices_before_live_update(manager): + manager.client.invoke.side_effect = [ + difference(11, messages=[message(11)], sliced=True), + difference(12, messages=[message(12)]), + ] + await manager.process(container(update(13))) + await manager.process(container(update(12))) + assert [u.message.id for u in dispatched(manager)] == [11, 12, 13] + assert [c.args[0].pts for c in manager.client.invoke.await_args_list] == [10, 11] + + +@pytest.mark.asyncio +async def test_recovery_contains_live_update_without_duplicate_dispatch(manager): + manager.client.invoke.return_value = difference(12, messages=[message(11), message(12)]) + await manager.process(container(update(12))) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + assert manager.states[0][0] == 12 + + +@pytest.mark.asyncio +async def test_updates_too_long_and_empty_difference(manager): + manager.client.invoke.side_effect = [ + difference(12, messages=[message(11), message(12)]), + raw.types.updates.DifferenceEmpty(date=102, seq=2), + ] + await manager.process(raw.types.UpdatesTooLong()) + await manager.process(raw.types.UpdatesTooLong()) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + assert manager.states[0] == (12, 0, 102, 2) + + +@pytest.mark.asyncio +async def test_seq_gap_reconciles_before_unordered_updates(manager): + missing = raw.types.UpdateUserStatus(user_id=1, status=raw.types.UserStatusEmpty()) + live = raw.types.UpdateUserStatus(user_id=2, status=raw.types.UserStatusEmpty()) + manager.client.invoke.return_value = difference(10, updates=[missing], seq=2) + await manager.process(container(live, seq=3)) + await manager.process(container(live, seq=3)) + assert [u.user_id for u in dispatched(manager)] == [1, 2] + assert manager.states[0][3] == 3 + + +@pytest.mark.asyncio +async def test_old_seq_container_can_contain_new_channel_event(manager): + await manager.process(container(update(20, 7), seq=1)) + assert [u.message.id for u in dispatched(manager)] == [20] + assert manager.states[0][0] == 10 + assert manager.states[7][0] == 20 + + +@pytest.mark.asyncio +async def test_qts_gap_and_duplicate(manager): + first = raw.types.UpdateBotStopped(user_id=1, date=101, stopped=True, qts=1) + second = raw.types.UpdateBotStopped(user_id=2, date=101, stopped=True, qts=2) + manager.client.invoke.return_value = difference(10, updates=[first], qts=1) + await manager.process(container(second)) + await manager.process(container(second)) + assert [u.qts for u in dispatched(manager)] == [1, 2] + assert manager.states[0][1] == 2 + + +@pytest.mark.asyncio +async def test_channel_gap_uses_independent_pts_and_paginates(manager): + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.side_effect = [ + channel_difference(21, messages=[message(21, 7)], final=False), + channel_difference(22, messages=[message(22, 7)]), + ] + await manager.process(container(update(23, 7))) + await manager.process(container(update(22, 7))) + events = dispatched(manager) + assert [u.message.id for u in events] == [21, 22, 23] + assert all(isinstance(u, raw.types.UpdateNewChannelMessage) for u in events) + assert manager.states[0][0] == 10 + assert manager.states[7][0] == 23 + queries = [c.args[0] for c in manager.client.invoke.await_args_list] + assert [q.pts for q in queries] == [20, 21] + assert isinstance(queries[0].channel, raw.types.InputChannel) + assert isinstance(queries[0].filter, raw.types.ChannelMessagesFilterEmpty) + assert queries[0].limit == 100 + + +@pytest.mark.asyncio +async def test_channel_notifications_from_common_difference(manager): + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.side_effect = [ + difference(11, updates=[raw.types.UpdateChannelTooLong(channel_id=7, pts=22)]), + channel_difference(22, messages=[message(21, 7), message(22, 7)]), + ] + await manager.process(raw.types.UpdatesTooLong()) + assert [u.message.id for u in dispatched(manager)] == [21, 22] + assert manager.states[0][0] == 11 + assert manager.states[7][0] == 22 + + +@pytest.mark.asyncio +async def test_short_message_recovers_every_message(manager): + manager.client.invoke.return_value = difference(12, messages=[message(11), message(12)]) + await manager.process( + raw.types.UpdateShortMessage( + id=12, + user_id=1, + message="12", + pts=12, + pts_count=1, + date=101, + ) + ) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + + +@pytest.mark.asyncio +async def test_short_sent_message_advances_without_dispatch(manager): + await manager.process(raw.types.UpdateShortSentMessage(id=11, pts=11, pts_count=1, date=101)) + assert manager.states[0][0] == 11 + assert dispatched(manager) == [] + + +@pytest.mark.asyncio +async def test_common_difference_too_long_warns_and_reconciles(manager, caplog): + manager.client.invoke.side_effect = [ + raw.types.updates.DifferenceTooLong(pts=30000), + state(pts=30000), + raw.types.updates.DifferenceEmpty(date=102, seq=2), + ] + await manager.process(raw.types.UpdatesTooLong()) + assert manager.states[0][0] == 30000 + assert "cannot replay" in caplog.text + assert dispatched(manager) == [] + + +@pytest.mark.asyncio +async def test_channel_too_long_snapshot_is_not_new_messages(manager, caplog): + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.return_value = raw.types.updates.ChannelDifferenceTooLong( + dialog=raw.types.Dialog( + peer=raw.types.PeerChannel(channel_id=7), + top_message=30, + read_inbox_max_id=0, + read_outbox_max_id=0, + unread_count=0, + unread_mentions_count=0, + unread_reactions_count=0, + unread_poll_votes_count=0, + notify_settings=raw.types.PeerNotifySettings(), + pts=30, + ), + messages=[message(30, 7)], + chats=[], + users=[], + final=True, + ) + await manager.process(container(raw.types.UpdateChannelTooLong(channel_id=7))) + assert manager.states[7][0] == 30 + assert dispatched(manager) == [] + assert "cannot replay" in caplog.text + + +@pytest.mark.asyncio +async def test_recovery_failure_retains_cursor_and_can_retry(manager): + manager.client.invoke.side_effect = OSError("disconnect") + with pytest.raises(OSError): + await manager.process(container(update(12))) + assert manager.states[0][0] == 10 + assert dispatched(manager) == [] + manager.client.invoke.side_effect = None + manager.client.invoke.return_value = difference(12, messages=[message(11), message(12)]) + await manager.process(container(update(12))) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + + +@pytest.mark.asyncio +async def test_channel_failure_does_not_dispatch_common_batch_twice(manager): + manager.states[7] = (20, 0, 0, 0) + diff = difference( + 11, messages=[message(11)], updates=[raw.types.UpdateChannelTooLong(channel_id=7)] + ) + manager.client.invoke.side_effect = [diff, OSError("disconnect"), diff, channel_difference(21)] + with pytest.raises(OSError): + await manager.process(raw.types.UpdatesTooLong()) + assert dispatched(manager) == [] + await manager.process(raw.types.UpdatesTooLong()) + assert [u.message.id for u in dispatched(manager)] == [11] + + +@pytest.mark.asyncio +async def test_no_progress_and_page_budget_are_bounded(manager): + manager.client.invoke.return_value = difference(10, sliced=True) + with pytest.raises(UpdateGapError, match="no progress"): + await manager.process(raw.types.UpdatesTooLong()) + manager.MAX_PAGES = 2 + manager.client.invoke.side_effect = [difference(11, sliced=True), difference(12, sliced=True)] + with pytest.raises(UpdateGapError, match="budget"): + await manager.process(raw.types.UpdatesTooLong()) + assert manager.states[0][0] == 12 + + +@pytest.mark.asyncio +async def test_worker_serializes_live_updates_during_disconnect_recovery(manager): + entered = asyncio.Event() + release = asyncio.Event() + + async def invoke(_query): + entered.set() + await release.wait() + return difference(12, messages=[message(11), message(12)]) + + manager.client.invoke.side_effect = invoke + await manager.start() + await asyncio.wait_for(entered.wait(), 1) + manager.submit(container(update(12))) + manager.submit(container(update(13))) + release.set() + await asyncio.wait_for(manager.queue.join(), 1) + assert [u.message.id for u in dispatched(manager)] == [11, 12, 13] + manager.client.invoke.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_cancel_inflight_recovery_preserves_restart_cursor(manager): + entered = asyncio.Event() + + async def invoke(_query): + entered.set() + await asyncio.Event().wait() + + manager.client.invoke.side_effect = invoke + await manager.start() + await asyncio.wait_for(entered.wait(), 1) + manager.submit(container(update(12))) + await asyncio.wait_for(manager.stop(), 1) + assert manager.task is None + assert manager.queue.empty() + assert (await manager.client.storage.get_update_states())[0][0] == 10 + assert dispatched(manager) == [] + + +@pytest.mark.asyncio +async def test_cancellation_waits_for_checkpoint(manager): + entered = asyncio.Event() + release = asyncio.Event() + original = manager.client.storage.set_update_state + + async def save(*args): + entered.set() + await release.wait() + await original(*args) + + manager.client.storage.set_update_state = save + task = asyncio.create_task(manager.process(container(update()))) + await entered.wait() + task.cancel() + release.set() + with pytest.raises(asyncio.CancelledError): + await task + assert (await manager.client.storage.get_update_states())[0][0] == 11 + assert [u.message.id for u in dispatched(manager)] == [11] + + +@pytest.mark.asyncio +async def test_file_session_restart_recovers_from_saved_cursor(tmp_path): + storage = SQLiteStorage("restart", tmp_path) + await storage.open() + await storage.set_update_state(0, 10, 0, 100, 1) + await storage.set_update_state(7, 20, 0, 0, 0) + await storage.save() + await storage.close() + reopened = SQLiteStorage("restart", tmp_path) + await reopened.open() + client = SimpleNamespace( + storage=reopened, + dispatcher=SimpleNamespace(updates_queue=asyncio.Queue()), + invoke=AsyncMock(return_value=difference(12, messages=[message(11), message(12)])), + fetch_peers=AsyncMock(), + ) + manager = UpdateManager(client) + try: + await manager.start() + await asyncio.wait_for(manager.queue.join(), 1) + # queue.join alone may return before the initial difference starts. + for _ in range(100): + if manager.states[0][0] == 12: + break + await asyncio.sleep(0.001) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + client.invoke.assert_awaited_once() + assert (await reopened.get_update_states())[7][0] == 20 + assert (await reopened.get_update_states())[0][2] == 100 + finally: + await manager.stop() + await reopened.close() + + +@pytest.mark.asyncio +async def test_legacy_session_migration_preserves_auth_and_date(tmp_path): + storage = SQLiteStorage("legacy", tmp_path) + await storage.open() + await storage.user_id(123) + await storage.date(42) + await storage.conn.execute("DROP TABLE update_state") + await storage.version(3) + await storage.close() + storage = SQLiteStorage("legacy", tmp_path) + await storage.open() + try: + assert await storage.version() == 4 + assert await storage.user_id() == 123 + assert await storage.date() == 42 + assert await storage.get_update_states() == {} + await storage.set_update_state(0, 10, 0, 100, 1) + await storage.save() + assert (await storage.get_update_states())[0] == (10, 0, 100, 1) + finally: + await storage.close() + + +@pytest.mark.asyncio +async def test_default_client_uses_legacy_handler_without_recovery(): + client = Client("legacy", in_memory=True) + try: + assert client.catch_up is False + assert client.update_manager is None + await client.handle_updates(raw.types.UpdatesTooLong()) + assert client.dispatcher.updates_queue.empty() + finally: + client.executor.shutdown() + + +@pytest.mark.parametrize("options", [{"no_updates": True}, {"takeout": True}]) +def test_no_updates_and_takeout_disable_catch_up(options): + client = Client("disabled", catch_up=True, in_memory=True, **options) + try: + assert client.update_manager is None + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_fresh_session_takes_baseline_without_replaying_history(manager): + await manager.client.storage.conn.execute("DELETE FROM update_state") + await manager.client.storage.conn.commit() + manager.client.invoke.return_value = state(50) + await manager.start() + assert manager.states[0][0] == 50 + assert isinstance(manager.client.invoke.await_args.args[0], raw.functions.updates.GetState) + manager.client.invoke.assert_awaited_once() + assert dispatched(manager) == [] + + +@pytest.mark.asyncio +async def test_unavailable_channel_does_not_block_common_updates(manager, caplog): + + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.side_effect = [ + difference( + 11, messages=[message(11)], updates=[raw.types.UpdateChannelTooLong(channel_id=7)] + ), + ChannelPrivate(), + ] + await manager.process(raw.types.UpdatesTooLong()) + assert [u.message.id for u in dispatched(manager)] == [11] + assert manager.states[0][0] == 11 + assert manager.states[7][0] == 20 + assert "inaccessible channel 7" in caplog.text + + +@pytest.mark.asyncio +async def test_common_reset_reconciles_known_channels(manager): + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.side_effect = [ + raw.types.updates.DifferenceTooLong(pts=30000), + state(30000), + channel_difference(22, messages=[message(21, 7), message(22, 7)]), + raw.types.updates.DifferenceEmpty(date=102, seq=2), + ] + await manager.process(raw.types.UpdatesTooLong()) + assert manager.states[0][0] == 30000 + assert manager.states[7][0] == 22 + assert [u.message.id for u in dispatched(manager)] == [21, 22] + + +@pytest.mark.asyncio +async def test_worker_retries_network_failure_without_losing_live_packet(manager, monkeypatch): + real_sleep = asyncio.sleep + + async def sleep(_delay): + await real_sleep(0) + + monkeypatch.setattr(asyncio, "sleep", sleep) + manager.client.invoke.side_effect = [ + OSError("offline"), + difference(12, messages=[message(11), message(12)]), + ] + manager.submit(container(update(12))) + await manager.start() + await asyncio.wait_for(manager.queue.join(), 1) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + assert manager.client.invoke.await_count == 2 + + +@pytest.mark.asyncio +async def test_checkpoint_failure_retries_write_without_replaying(manager, monkeypatch): + real_sleep = asyncio.sleep + original = manager.client.storage.set_update_state + save = AsyncMock(side_effect=[OSError("disk temporarily unavailable"), None, None]) + + async def checkpoint(*args): + await save(*args) + await original(*args) + + async def sleep(_delay): + await real_sleep(0) + + monkeypatch.setattr(asyncio, "sleep", sleep) + monkeypatch.setattr(manager.client.storage, "set_update_state", checkpoint) + await manager.process(container(update())) + assert [u.message.id for u in dispatched(manager)] == [11] + assert save.await_count == 3 + assert (await manager.client.storage.get_update_states())[0][0] == 11 + + +@pytest.mark.asyncio +async def test_short_message_not_yet_available_keeps_recovery_pending(manager): + manager.client.invoke.return_value = raw.types.updates.DifferenceEmpty(date=100, seq=1) + with pytest.raises(UpdateGapError, match="not yet present"): + await manager.process( + raw.types.UpdateShortChatMessage( + id=12, + from_id=1, + chat_id=1, + message="12", + pts=12, + pts_count=1, + date=101, + ) + ) + assert manager.states[0][0] == 10 + + +@pytest.mark.asyncio +async def test_recovery_requests_coalesce(manager): + manager.request_recovery() + manager.request_recovery() + manager.client.invoke.return_value = raw.types.updates.DifferenceEmpty(date=100, seq=1) + manager.task = asyncio.create_task(manager._run()) + await asyncio.wait_for(manager.queue.join(), 1) + manager.client.invoke.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_buffer_overflow_requests_reconciliation(manager, caplog): + manager.queue = asyncio.Queue(1) + manager.submit(container(update(11))) + manager.submit(container(update(12))) + assert manager.queue.qsize() == 1 + assert manager.recovery_requested + assert "buffer full" in caplog.text + manager.client.invoke.return_value = difference(12, messages=[message(11), message(12)]) + manager.task = asyncio.create_task(manager._run()) + await asyncio.wait_for(manager.queue.join(), 1) + assert [u.message.id for u in dispatched(manager)] == [11, 12] + + +@pytest.mark.asyncio +async def test_recovery_pauses_when_dispatcher_is_full(manager): + manager.DISPATCH_LIMIT = 1 + manager.client.invoke.side_effect = [ + difference(11, messages=[message(11)], sliced=True), + difference(12, messages=[message(12)]), + ] + task = asyncio.create_task(manager.process(raw.types.UpdatesTooLong())) + queue = manager.client.dispatcher.updates_queue + first = await asyncio.wait_for(queue.get(), 1) + assert first[0].message.id == 11 + manager.client.invoke.assert_awaited_once() + queue.task_done() + second = await asyncio.wait_for(queue.get(), 1) + assert second[0].message.id == 12 + queue.task_done() + await asyncio.wait_for(task, 1) + + +@pytest.mark.asyncio +async def test_combined_seq_range_advances_atomically(manager): + update_status = raw.types.UpdateUserStatus(user_id=1, status=raw.types.UserStatusEmpty()) + updates = raw.types.UpdatesCombined( + updates=[update_status], users=[], chats=[], date=101, seq_start=2, seq=4 + ) + await manager.process(updates) + await manager.process(updates) + assert len(dispatched(manager)) == 1 + assert manager.states[0][3] == 4 + manager.client.invoke.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_update_short_tracks_common_cursor(manager): + await manager.process(raw.types.UpdateShort(update=update(11), date=101)) + await manager.process(raw.types.UpdateShort(update=update(11), date=101)) + assert [u.message.id for u in dispatched(manager)] == [11] + assert manager.states[0][0] == 11 + + +@pytest.mark.asyncio +async def test_transport_submits_rpc_updates_without_waiting_on_recovery(): + + updates = container(update()) + body = raw.types.RpcResult(req_msg_id=123, result=updates) + loop = SimpleNamespace( + run_in_executor=AsyncMock( + return_value=SimpleNamespace( + body=body, + msg_id=MsgId(), + seq_no=0, + ) + ) + ) + manager = Mock() + client = SimpleNamespace(loop=loop, update_manager=manager) + session = Session(client, 2, b"x" * 256, False) + result = Result() + session.results[123] = result + await asyncio.wait_for(session.handle_packet(b""), 1) + assert result.event.is_set() + assert result.value is updates + manager.submit.assert_called_once_with(updates) + + +@pytest.mark.asyncio +async def test_transport_new_session_requests_recovery(): + + body = raw.types.NewSessionCreated(first_msg_id=1, unique_id=2, server_salt=3) + client = SimpleNamespace( + update_manager=Mock(), + loop=SimpleNamespace( + run_in_executor=AsyncMock( + return_value=SimpleNamespace(body=body, msg_id=MsgId(), seq_no=0) + ), + ), + ) + session = Session(client, 2, b"x" * 256, False) + await session.handle_packet(b"") + client.update_manager.request_recovery.assert_called_once() + + +@pytest.mark.asyncio +async def test_invalid_channel_cursor_reconciles(manager): + + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.side_effect = [ + PersistentTimestampInvalid(), + SimpleNamespace(full_chat=SimpleNamespace(pts=30)), + raw.types.updates.ChannelDifferenceEmpty(pts=30, final=True), + ] + await manager.process(container(raw.types.UpdateChannelTooLong(channel_id=7))) + assert manager.states[7][0] == 30 + assert dispatched(manager) == [] + + +@pytest.mark.asyncio +async def test_backwards_channel_response_is_not_dispatched(manager): + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.return_value = channel_difference(19, messages=[message(19, 7)]) + with pytest.raises(UpdateGapError, match="backwards"): + await manager.process(container(raw.types.UpdateChannelTooLong(channel_id=7))) + assert manager.states[7][0] == 20 + assert dispatched(manager) == [] + + +@pytest.mark.asyncio +async def test_bot_channel_participant_uses_common_qts(manager): + event = raw.types.UpdateChannelParticipant( + channel_id=7, + date=101, + actor_id=1, + user_id=2, + qts=2, + ) + missed = raw.types.UpdateBotStopped(user_id=3, date=100, stopped=True, qts=1) + manager.client.invoke.return_value = difference(10, updates=[missed], qts=1) + await manager.process(container(event)) + await manager.process(container(event)) + assert [u.qts for u in dispatched(manager)] == [1, 2] + assert manager.states[0][1] == 2 + assert 7 not in manager.states + assert isinstance( + manager.client.invoke.await_args.args[0], raw.functions.updates.GetDifference + ) + + +@pytest.mark.asyncio +async def test_business_channel_messages_in_difference_use_common_qts(manager): + event = raw.types.UpdateBotNewBusinessMessage( + connection_id="business", message=message(1, 7), qts=1 + ) + manager.client.invoke.return_value = difference(10, updates=[event], qts=1) + await manager.process(raw.types.UpdatesTooLong()) + await manager.process(container(event)) + assert dispatched(manager) == [event] + manager.client.invoke.assert_awaited_once() + assert manager.states[0][1] == 1 + assert 7 not in manager.states + + +@pytest.mark.asyncio +async def test_channel_notification_with_equal_pts_still_fetches_difference(manager): + manager.states[7] = (20, 0, 0, 0) + manager.client.invoke.return_value = channel_difference(21, messages=[message(21, 7)]) + await manager.process(container(raw.types.UpdateChannelTooLong(channel_id=7, pts=20))) + assert [u.message.id for u in dispatched(manager)] == [21] + assert manager.states[7][0] == 21 + + +@pytest.mark.asyncio +async def test_first_channel_notification_uses_supplied_starting_pts(manager): + manager.client.invoke.return_value = channel_difference(21, messages=[message(21, 7)]) + await manager.process(container(raw.types.UpdateChannelTooLong(channel_id=7, pts=20))) + assert manager.client.invoke.await_args.args[0].pts == 20 + assert [u.message.id for u in dispatched(manager)] == [21] + + +@pytest.mark.asyncio +async def test_first_channel_notification_without_pts_fetches_baseline(manager): + manager.client.invoke.side_effect = [ + SimpleNamespace(full_chat=SimpleNamespace(pts=20)), + channel_difference(21, messages=[message(21, 7)]), + ] + await manager.process(container(raw.types.UpdateChannelTooLong(channel_id=7))) + assert isinstance( + manager.client.invoke.await_args_list[0].args[0], raw.functions.channels.GetFullChannel + ) + assert [u.message.id for u in dispatched(manager)] == [21] + + +@pytest.mark.asyncio +async def test_stop_during_failure_backoff_releases_pending_packet(manager): + failed = asyncio.Event() + + async def invoke(_query): + failed.set() + await asyncio.sleep(0) + raise OSError("offline") + + manager.client.invoke.side_effect = invoke + manager.submit(container(update(12))) + manager.task = asyncio.create_task(manager._run()) + await asyncio.wait_for(failed.wait(), 1) + await manager.stop() + await asyncio.wait_for(manager.queue.join(), 1) + assert manager.states[0][0] == 10 + manager.submit(container(update(13))) + assert manager.queue.empty() + + +@pytest.mark.asyncio +async def test_client_lifecycle_starts_and_stops_recovery(): + client = Client("lifecycle", catch_up=True, in_memory=True) + client.is_connected = True + client.update_manager.start = AsyncMock() + client.update_manager.stop = AsyncMock() + client.dispatcher.start = AsyncMock() + client.dispatcher.stop = AsyncMock() + client.storage.save = AsyncMock() + client.updates_watchdog = AsyncMock() + order = Mock() + order.attach_mock(client.update_manager.start, "recovery_start") + order.attach_mock(client.dispatcher.start, "dispatcher_start") + order.attach_mock(client.update_manager.stop, "recovery_stop") + order.attach_mock(client.dispatcher.stop, "dispatcher_stop") + try: + await client.initialize() + assert client.is_initialized + await client.terminate() + assert not client.is_initialized + assert [call[0] for call in order.mock_calls] == [ + "recovery_start", + "dispatcher_start", + "recovery_stop", + "dispatcher_stop", + ] + finally: + client.executor.shutdown() + + +@pytest.mark.asyncio +async def test_transport_reconnect_requests_recovery_only_for_main_session(): + async def run(is_media): + manager = Mock() + connection = Mock(connect=AsyncMock()) + client = SimpleNamespace( + update_manager=manager, + is_initialized=True, + connection_factory=Mock(return_value=connection), + ipv6=False, + proxy=None, + protocol_factory=None, + loop=asyncio.get_running_loop(), + storage=SimpleNamespace(api_id=AsyncMock(return_value=1)), + app_version="test", + device_model="test", + system_version="test", + lang_code="en", + ) + session = Session(client, 2, b"x" * 256, False, is_media=is_media) + session.recv_worker = AsyncMock() + session.ping_worker = AsyncMock() + session.send = AsyncMock() + await session.start() + await asyncio.gather(session.recv_task, session.ping_task) + return manager + + manager = await run(False) + manager.request_recovery.assert_called_once() + media_manager = await run(True) + media_manager.request_recovery.assert_not_called() + + +@pytest.mark.asyncio +async def test_session_string_format_stays_compatible_and_excludes_cursors(): + storage = SQLiteStorage("export", use_memory=True) + await storage.open() + try: + await storage.api_id(123) + await storage.test_mode(False) + await storage.auth_key(b"x" * 256) + await storage.user_id(456) + await storage.is_bot(True) + before = await storage.export_session_string() + await storage.set_update_state(0, 10, 0, 100, 1) + assert await storage.export_session_string() == before + imported = SQLiteStorage("import", session_string=before, use_memory=True) + await imported.open() + try: + assert await imported.user_id() == 456 + assert await imported.is_bot() + assert await imported.get_update_states() == {} + finally: + await imported.close() + finally: + await storage.close() + + +@pytest.mark.asyncio +async def test_zero_count_events_deduplicate_exact_payloads_only(manager): + first = raw.types.UpdateReadMessagesContents(messages=[1], pts=10, pts_count=0) + second = raw.types.UpdateReadMessagesContents(messages=[2], pts=10, pts_count=0) + await manager.process(container(first, seq=2)) + await manager.process(container(first, seq=2)) + await manager.process(container(second, seq=3)) + assert dispatched(manager) == [first, second] + assert manager.states[0][0] == 10 + manager.client.invoke.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_zero_count_duplicate_cache_is_bounded(manager): + manager.ZERO_PTS_CACHE_LIMIT = 2 + for id in range(3): + await manager.process( + container( + raw.types.UpdateReadMessagesContents( + messages=[id], + pts=10, + pts_count=0, + ) + ) + ) + assert len(manager._zero_pts_seen) == 2 + assert len(dispatched(manager)) == 3 + + +@pytest.mark.asyncio +async def test_start_cleans_up_when_recovery_initialization_fails(): + client = Client("unsupported-storage", catch_up=True, in_memory=True) + client.connect = AsyncMock(return_value=True) + client.storage.is_bot = AsyncMock(return_value=True) + client.get_me = AsyncMock() + client.initialize = AsyncMock(side_effect=NotImplementedError("catch_up is unsupported")) + client.update_manager.stop = AsyncMock() + client.disconnect = AsyncMock() + try: + with pytest.raises(NotImplementedError, match="unsupported"): + await client.start() + client.update_manager.stop.assert_awaited_once() + client.disconnect.assert_awaited_once() + finally: + client.executor.shutdown() diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..3c9a38e64 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2089 @@ +version = 1 +revision = 5 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.11.5' and python_full_version < '3.12'", + "python_full_version < '3.11.5'", +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve", version = "2.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11.5'" }, + { name = "soupsieve", version = "3.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11.5'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "certifi" +version = "2026.10.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/11/b524cbd9bbdaca58608e4cf4053fd463417549e1959fa8d3aa18319f9555/certifi-2026.10.23.tar.gz", hash = "sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd", size = 136225, upload-time = "2026-10-23T11:36:16.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d1/a3652fe2c19134d0d15922177821d863cbd7e842d53a21bbe7e8e3a1b5ac/certifi-2026.10.23-py3-none-any.whl", hash = "sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1", size = 135082, upload-time = "2026-10-23T11:36:14.763Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/67/6a0b94a7960d5e1b5eacd2fb529f3fccc47db4644f7f0a7cfdcfc3be578a/charset_normalizer-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6", size = 370642, upload-time = "2026-09-30T04:35:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/01009e13b94041599004edf32e56e382c24e570f60f79bab8efe45cfe1eb/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5", size = 259280, upload-time = "2026-09-30T04:35:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/66/85/3b5358f60a13210f0b67d3755c168ef758701b021e655d88d4da28554467/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74", size = 245335, upload-time = "2026-09-30T04:35:10.104Z" }, + { url = "https://files.pythonhosted.org/packages/74/75/77c1c479b09ecd751d1e767b251ea5c14d4d50ff757bf404afab2692f600/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab", size = 291707, upload-time = "2026-09-30T04:35:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0d/363f78cacb70f58f15f4b083961bbd9d292f335d3f5c66fc4f1cfe69cb90/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c", size = 286747, upload-time = "2026-09-30T04:35:13.022Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ed/cf505d3011ffceb12c2067a7a5d3cfe92b875d4d44bb0ff0d69375e2c184/charset_normalizer-3.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f", size = 269972, upload-time = "2026-09-30T04:35:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/15/d8/f0a93a431d170e7ca681d4f6650fee3de934d18560e474e7267eb4b0f987/charset_normalizer-3.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288", size = 269339, upload-time = "2026-09-30T04:35:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/9b2bd1c5b7af02462c9752d33994834ff972a96b4c483eefde9e594488e2/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400", size = 261683, upload-time = "2026-09-30T04:35:17.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/a5/cac540ab0fd61f3fec88ad3dbb64509e71424593d73cfdfff5ab3e4db279/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd", size = 248143, upload-time = "2026-09-30T04:35:18.849Z" }, + { url = "https://files.pythonhosted.org/packages/71/7a/ff467301deef2089fad87f72df9e000a26a78fec7acbb18e1999371b8369/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37", size = 292358, upload-time = "2026-09-30T04:35:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/22d7e785d1e210afc2e2f58600dd1799d17a35665faf84383f002826c5f8/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac", size = 268766, upload-time = "2026-09-30T04:35:21.72Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/e8e946267f1c2d9e2bd651726e2fbd2addf02c4d36cea5069e32ca9d7bb5/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a", size = 287432, upload-time = "2026-09-30T04:35:23.273Z" }, + { url = "https://files.pythonhosted.org/packages/4e/88/7561d8a88d555e7df6623abe7c0070b4baf47549b9408783a2ae0a1a6cf7/charset_normalizer-3.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640", size = 272470, upload-time = "2026-09-30T04:35:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/578c702301ec036f01455f30744a08d2b42f6ab35b9b2d4bf8cae0ef2a80/charset_normalizer-3.5.2-cp311-cp311-win32.whl", hash = "sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d", size = 186688, upload-time = "2026-09-30T04:35:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fc/fdf8cf52ff21cd5bf158f20978991cf985325842f74283eb6df26c8a39d8/charset_normalizer-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96", size = 214932, upload-time = "2026-09-30T04:35:27.796Z" }, + { url = "https://files.pythonhosted.org/packages/97/66/3e45a506d8110b632541faf9a9470185aa9878f1ed44020f31346c1c5e5b/charset_normalizer-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1", size = 202828, upload-time = "2026-09-30T04:35:29.259Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" }, + { url = "https://files.pythonhosted.org/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" }, + { url = "https://files.pythonhosted.org/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" }, + { url = "https://files.pythonhosted.org/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" }, + { url = "https://files.pythonhosted.org/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" }, + { url = "https://files.pythonhosted.org/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, + { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, + { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/79/e78bd8f75a9b8e32233ce92d4607f0a21ce88a9156cace2884e7175a82f5/coverage-7.17.0.tar.gz", hash = "sha256:cc010a0a7fbdd9bb8bdfd0d3f04a5151275860213cbd7e2b876b29778489c712", size = 991021, upload-time = "2026-10-22T16:06:50.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/25/79a66272edd250031aeee6db87d500a0bf76390d0d5f8c2f431fe354715f/coverage-7.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99a7b390cbccc6bf029ca4da3f0f607c218bb4085e9cd6b58a90d14ca12993c3", size = 225837, upload-time = "2026-10-22T16:03:55.622Z" }, + { url = "https://files.pythonhosted.org/packages/52/a6/f5f6fefb2fc1305363927edad1d0a60ce6ef80d6c1a74a79e825f0f670eb/coverage-7.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f72640e02a470b57b15cd655820133f6892760209283a363a60e2df932d5f0fe", size = 226257, upload-time = "2026-10-22T16:03:57.164Z" }, + { url = "https://files.pythonhosted.org/packages/28/61/72889bac8eb44c51f8043edb227df551dfdca1fc4819a6c6d67b3f69427b/coverage-7.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2dc9233f39149b5a4f4aba17a4b6bc5cca22e52cfc4b06fdb07d014439b8b2ea", size = 258282, upload-time = "2026-10-22T16:03:58.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/03e4a5644df9b73de0d44d1b82acd18b06c42b5abc3f1433be8363fa983d/coverage-7.17.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e8113d3852e9377421a61de1917df27b3abb25404a4ecf69f9e64612c4345b5", size = 260506, upload-time = "2026-10-22T16:04:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4e/949022286df7e59e8633a3a7e6635e0e6b53af7d57074cb0bf8f500373fb/coverage-7.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86a39539d10d666bb819c16c94d7edfc1aa2e0531b937f7c549b83d15177c6e", size = 262600, upload-time = "2026-10-22T16:04:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/27/cf/7d67f60df081006315699b7edb9402c1b47142caf1383f4e56ae8ff1a501/coverage-7.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:559f748446ee799803686b5784e3deb54c70d655ac386a860e06b11672504ba7", size = 264609, upload-time = "2026-10-22T16:04:03.286Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/9293d94afbc099990a085e88229a51c79b00828aba0f2adda32dc4cc16d1/coverage-7.17.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d65c9e8e257f48a654908005046fbf1bc48d9540ae3776ad08c0acd010e10c", size = 258382, upload-time = "2026-10-22T16:04:04.804Z" }, + { url = "https://files.pythonhosted.org/packages/24/c8/bcd28efedf262c074dae76f61098739eb02b15f3f716f80cc86f9885b8a9/coverage-7.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b7fa0997b8eba7378ac66f31ec7f70164490353ab79b9667d37c0d365e50177", size = 260299, upload-time = "2026-10-22T16:04:06.291Z" }, + { url = "https://files.pythonhosted.org/packages/b8/dc/503f55e5e2200933403c90d619d9f779b5060ddd338b1bdc63cd235592cb/coverage-7.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6ae0453a3390e67f7bfbe34417bbdf5b4ebfc21db23fb446a0a41115eeeca6ca", size = 258314, upload-time = "2026-10-22T16:04:07.729Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b1/f1a57c36077cc516240636f7eff0d1427939ed795f096dd2c7769213a56c/coverage-7.17.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:96151262832f3c230ebae23b2d6911ad549e1738465946c97004913ff06112ba", size = 262239, upload-time = "2026-10-22T16:04:09.116Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/c2e59d4a132936c7c207b9b69ab99c0f27b14c3c93266e9922aa3b562b37/coverage-7.17.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:cd7397de6cc782f00fe5ac1d75e9b306bcfa49c8b6b8dd6ed1f7709393ad40b3", size = 257841, upload-time = "2026-10-22T16:04:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/05/04/50742c0ee8b6c85826ff11737c93ed957fea4c9acf4b4b99804bdac1dd84/coverage-7.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d1fdf82c6fea57b5e9dada509f9ce9638c8334664e2c98c7f6f1ccf3d143ad2", size = 259039, upload-time = "2026-10-22T16:04:11.916Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/83ad4bbbfcb6d045a33c14cdca313f6c9275737c67f3b7300222e3239a37/coverage-7.17.0-cp311-cp311-win32.whl", hash = "sha256:38c86e5d74bbfd88a393fa3f91257c1225c63aa6db6d5918de3e49ab04f91c6e", size = 227687, upload-time = "2026-10-22T16:04:13.537Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c8/dcc5a7effa79707eb8a61e699b141cff7c9b46036771274cfb48ea7b765f/coverage-7.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2ed3097c6817bc1a7fc8ae0a6131db74aa1150d863cccc308237ff362d3226b2", size = 228290, upload-time = "2026-10-22T16:04:14.899Z" }, + { url = "https://files.pythonhosted.org/packages/d1/58/a8553e0dcd38d8136df63964638d2b588e0a4a916bff1bca996244fda693/coverage-7.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:6052e878b628abcd38fa5f2b64abebc686eed4e2267da42434017a9c6d24f241", size = 228199, upload-time = "2026-10-22T16:04:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/0a/25/989fd3b495c4274ac0cf54cb6664326415e60183a7f8a73dd9bbb0b11e34/coverage-7.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0cfdc938ca3319d873d1f1252e60620a39aee4ebf9dbd7cc58238109ebda9c51", size = 225938, upload-time = "2026-10-22T16:04:18.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/cb/14726426bf3241657facbf0f7df7babd8f7d3fcf8e4609b7929f9dceecbc/coverage-7.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d523da3c2e4bca49417768bcdb1a443c5b359dc44b8b93a3102e118fce912c40", size = 226166, upload-time = "2026-10-22T16:04:19.524Z" }, + { url = "https://files.pythonhosted.org/packages/8e/97/f95ed32cd3cfbe261a3598e694cb770cefc7b2d13ff013418d6f70a44dde/coverage-7.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:adbf3450c0f3f2b2854e282a8758f2b3eddf787eeff24ec9c9bc2cbc38328cc1", size = 258462, upload-time = "2026-10-22T16:04:21.079Z" }, + { url = "https://files.pythonhosted.org/packages/91/ac/eb4c31f741509a9ef48cf6f28385e98e9de0785599282252eee252ff8af7/coverage-7.17.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85932227e9dd38e62cd29c17fefdfecfb919eead6af1e3259522d03cf85de983", size = 261789, upload-time = "2026-10-22T16:04:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3e/d9cd006d1a3cc5deab07beb29b86cb4a10dd84d66b330820363fb8d310d6/coverage-7.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f5477aac9c9e4a0a641c55f8f8fddd7f3bd91176c912c50f40ffced96b4034", size = 262322, upload-time = "2026-10-22T16:04:24.94Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3a/4c872c6ae0762b09cdb8e856891bb3f584848cb60b6dda2bacb85fe2f3f0/coverage-7.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b8bb651a4406fb6a983a905309fec1783443c65e1771b41e92740a0f4a1176b", size = 264600, upload-time = "2026-10-22T16:04:26.444Z" }, + { url = "https://files.pythonhosted.org/packages/df/98/1d41a2373fa98b423bcd70adb91d75e38eafb417955426dbb920b10016f3/coverage-7.17.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0edeb6594f080c8de1263cccb239cb316ef77aeb959e2e0f38f2953fcc72a49e", size = 258678, upload-time = "2026-10-22T16:04:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/fe04b5e63e76506daa63d90bdfb00a53767fe5df18546b437d6226918d6b/coverage-7.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da94ec0484e84fce5c7bf8d0f54b97767a90d992653a129b35634699d516e0a5", size = 260210, upload-time = "2026-10-22T16:04:29.275Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/5c5a78dc19dd4799be9c6ec022316fb25ababaeacfaf0f52f24c56a57b43/coverage-7.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a25cc5e8c23a590815c6a7aa43624234d25cb23b21b484adb8a0daed59860db5", size = 258375, upload-time = "2026-10-22T16:04:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e7/4c1d8eaff4d756fefd15d58c4953a78ab656782fd0e32c5a3465dba61aa0/coverage-7.17.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db645c81cf4d6bcb392ea1894aad65d55d1588a8472c834b25ca575da3029a56", size = 262422, upload-time = "2026-10-22T16:04:32.053Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2a/0b17c3e2e7b6ff1a5d92eb58915c0d1026668928f961d14dac73cf1b6096/coverage-7.17.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6a80a11b90c4a03df1502304cf24ff953255b6b5d076ef79066ecf88baaff31f", size = 257875, upload-time = "2026-10-22T16:04:33.824Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/eb0cbde0535342df0d17a667163030a3afb67e5eb9e3ad94af9540041584/coverage-7.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4eaf11d6b0f2147f46cfbf06fe7edcca6f095d0120da00ddc2458995ee869fc", size = 260361, upload-time = "2026-10-22T16:04:35.661Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/1be2a6db7eb655d0a1d899217c3f2469f9121cbbeddac35c1c8173b1482e/coverage-7.17.0-cp312-cp312-win32.whl", hash = "sha256:912a29f338798541d795e2e25903c6fe7a99dee70a40fe4706a9a124649b523e", size = 227747, upload-time = "2026-10-22T16:04:37.102Z" }, + { url = "https://files.pythonhosted.org/packages/11/c5/201c894c57de81875d8b8cc527ba222aa350e8cfb183eb3f6df45d06ad8e/coverage-7.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:88a1a9fb0075b83389dd9b403bb3fce976031b3c2aa0e05e3e57143cf639de69", size = 228325, upload-time = "2026-10-22T16:04:38.989Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/196095339b19758cf83f507fb598bc42820ae124745feb3084662729e17a/coverage-7.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:2c680e838ff9e68d491158625ca2c1ddb17d78d791bba96a31f8b839b4990438", size = 228131, upload-time = "2026-10-22T16:04:40.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/e6/1f01afbc9f4fa8ca5e1fa6fdf537a5c58b5765d1b4167ee284cddb9acc72/coverage-7.17.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:84538c040f4e6214f663459d89372ce4e6b2bbdd9d397ef0dca7b3ecd56b8eb2", size = 227080, upload-time = "2026-10-22T16:04:42.325Z" }, + { url = "https://files.pythonhosted.org/packages/92/7a/bdb7e6260c4a211389aeac9b89dc31662ebe122153ad0ecfd156897912df/coverage-7.17.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:6abfb6a4cf1cb1b67bc8fccdde177e2d2b781d0d34449e7078156547025140e5", size = 227152, upload-time = "2026-10-22T16:04:43.737Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/276e7d5b3cfb124871bdb544c27b004a8b4e270e6bf8e7414149c54b5e5b/coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef65e96f0fc1a7c8b7644c7011d47a8228555e7888ac4d6246f715a7f3d7c398", size = 225397, upload-time = "2026-10-22T16:04:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7d/dac073db12f3c131598a6693253aded581e4d7d622a89864f11bfb0ca2f5/coverage-7.17.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:55939ea4c097fda2edabf80fac5e49bef15597b950a9f29fc56292a895245feb", size = 225705, upload-time = "2026-10-22T16:04:46.603Z" }, + { url = "https://files.pythonhosted.org/packages/00/45/877b0a9248e921080ae91d115c13192d17e308f5aee86bcc7f7bc2a54ed1/coverage-7.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bfb6f868af552c0ceb2d19220b8cbcc2be096ca26163ac30e682a13041433429", size = 225828, upload-time = "2026-10-22T16:04:48.065Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f5/2c48d47abec0e4590b302920334be5ee3854f96bbd22524feabb8d92b220/coverage-7.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8d823d66afd59566030539c49c7a6acaf3083996682be2dda760c63555842094", size = 226088, upload-time = "2026-10-22T16:04:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a0/ec0442f3f372c7fd742f49b3022edd392f79e37a0048e1fa333f56d01ec3/coverage-7.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50678474e714c684b82c108e63fbb8f1bcf1266d8293a49980f1d08526f093b5", size = 257257, upload-time = "2026-10-22T16:04:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/eb0927ba93809f76eee69fa1e47999825fc7dbbabc3e0e1a2744f2677acb/coverage-7.17.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:045c1eeb19fb4eae949850a761102fffb3a5f638b97f977912f95843e54f6669", size = 260165, upload-time = "2026-10-22T16:04:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a2/8d4c6055efb32099b15c9cc33d007b8aabebc4832f372a3274de5ba0afc5/coverage-7.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb12891a773cbc2132349c77c690a045a882fe3f98b4bda50dfabb01893f9bd", size = 261098, upload-time = "2026-10-22T16:04:53.763Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/93412288688a7b03f152a734c8ef086e395108a0a6a746985d3967e220f2/coverage-7.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3067608b4429629f640c1b23d2f69b05d2fd45e00ebb6ee9e8fad220a6c11722", size = 263164, upload-time = "2026-10-22T16:04:55.386Z" }, + { url = "https://files.pythonhosted.org/packages/3c/20/7eb8d805c4f1a212245cef20181dfbf39535357985073d49730ee9cf08d4/coverage-7.17.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1762e85cbbe3b5ff9c6dcbcc1b89d5c72320094292b7b9572bf882c85e428b7", size = 257287, upload-time = "2026-10-22T16:04:56.976Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c1/1df29630b4ed17e135c76390afcc6bd273c3dc5ba2a21a76813a600fe98c/coverage-7.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62bb77987d3f72a0aaa77c567ee7f9806bc2e748c8f46fafbe1fdb90dd148398", size = 259256, upload-time = "2026-10-22T16:04:58.545Z" }, + { url = "https://files.pythonhosted.org/packages/a1/79/2365d1008d28aafe468a450a24c6c63363de794b5140223d67685cd0e205/coverage-7.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6cc31f27240d4e080437e3b8229d92f1d809c9a4f233607385373b73974e183e", size = 257178, upload-time = "2026-10-22T16:05:00.103Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9f/f002952d295d772688dffce0d991e3822795b1e3aad158073712a5a5e86e/coverage-7.17.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61cde5222f1933d476647c5a25ed1a8678b7703098cb5a15f4a0aa39967d8c28", size = 261275, upload-time = "2026-10-22T16:05:01.462Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/f9e15fbb426be97cd679ee5d359151e046f103bcd76b5092c318c26ed596/coverage-7.17.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11f5f7fb2d97a0fe865f5e7bad4eb0619b957637f676bc088d13844adf26ba95", size = 257029, upload-time = "2026-10-22T16:05:02.8Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4a/3c398f38694387334ff4c9f71afc8e15b4fa7db7bc0bea4d5b6032eb0df0/coverage-7.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e09612b1b82c9df1d2b6c85dee88d5aad3e73d77a8ec0b16ac6a7322cbdf5d9", size = 259109, upload-time = "2026-10-22T16:05:04.241Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a9/908a3cf1a2335aec1c788682a06c2f742a62a4f987033f080de1c5fd1997/coverage-7.17.0-cp313-cp313-win32.whl", hash = "sha256:bd3e9eb493631ff1ca34bf5efac25b5c66724f837bf6182c4fc771c55946423d", size = 227691, upload-time = "2026-10-22T16:05:05.636Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/8cdd7bfd8a63b3e6b133fc395ecfc7b1cb6f561926e7e2ad050de85b83e7/coverage-7.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1fc9a7cea899c51d8a53e66c2f082a3d9e461ece32418d3ef0a0b90c32bfd3d", size = 228418, upload-time = "2026-10-22T16:05:07.057Z" }, + { url = "https://files.pythonhosted.org/packages/09/cf/0b3fc57f735cd3831ca13d0be5312a5477d5934066683c5d34018cab7541/coverage-7.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:e65a1e61a68493fbfbe44d8bec71b3231d81b9917f62d3dafe0e105072147e83", size = 228142, upload-time = "2026-10-22T16:05:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/722462fe79912ebebc101497bd16c42413cde2dbae771017f1ec0acb84c2/coverage-7.17.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:dc5936491e306f2f6f095bd5fba593de11eb0375cee787ca1f245c3a333a523a", size = 227197, upload-time = "2026-10-22T16:05:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/74/826b9c09f2ebfea803bccb391d935688a25309bff8e819fc446bc629c108/coverage-7.17.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:968c5fb6e0c83cab752d79a70209648ce4d90cae815c60fa229b59e264601062", size = 227314, upload-time = "2026-10-22T16:05:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/6d/19/c8514d7f5e0659a96ff2bc7377119dca6d420e2761c5fc69ae37618734f8/coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:12cb8a20ad904a5811e6660d829a586b216e5254b9b34b76dde8a4d65d827dc6", size = 225477, upload-time = "2026-10-22T16:05:12.761Z" }, + { url = "https://files.pythonhosted.org/packages/81/74/ef9433ce599f07d8633b4ce6e6d6921505c4ef3f7f9006f8eb66e2c278cf/coverage-7.17.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bb5ca66937c55bc85d1a5e9c5900a3e4693a8edf2280a9b3672b7b2661a84b95", size = 225787, upload-time = "2026-10-22T16:05:14.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/26e58981940e8b3944c6a9941bec5c7f50840e7009a046712e3f3bcd9e6a/coverage-7.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5cc15faaf318ee5040d835a62c75bc6f122d837274f3b5efd47bf9fbd4ac5b85", size = 226019, upload-time = "2026-10-22T16:05:15.798Z" }, + { url = "https://files.pythonhosted.org/packages/e9/63/fe175310815cc8357b83b773c9b4c20943c269f217dd825a37c4b4bfe324/coverage-7.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d6d9d396c8b7bcaf76d774a10140b218111484e54c73a5f2a1b75655381748e", size = 226169, upload-time = "2026-10-22T16:05:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b0/ac84761f5b4bd226696b0ec4606903d2ef7208abda0aec06c14a4218def4/coverage-7.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:42e6f170d1ac04b7eb3363310a6c41cdfd82c1fe6bf9ffbc4a86f380bdf091c7", size = 258092, upload-time = "2026-10-22T16:05:18.691Z" }, + { url = "https://files.pythonhosted.org/packages/e9/58/d8bdd65a661913bc12eaaa3e59742b2cb8b94f2e7f78e62184f0a58ca493/coverage-7.17.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:acdb5f3c96046efff9dc6c572a6acb2b8d0da09372d7b9a9ed7249cf7d17434f", size = 261096, upload-time = "2026-10-22T16:05:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/5d82e2f0a75ea149e0a6412e137e1d610538772fdfd7199737db7fe234da/coverage-7.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3393209f6894713161ee82a9b0e673e58237e0f9733c6638e7a6c0c620ef0b33", size = 262080, upload-time = "2026-10-22T16:05:21.568Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cc/e3ebfd295d1dd47c7bc84a17d73f150465a37ab388dd68fe82b495dc79a0/coverage-7.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9494d9e99389d39e6a96227749af898d8924ff591cb63944f8a98e68582bf0e", size = 264558, upload-time = "2026-10-22T16:05:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/e2adff76d6544bbc6df7366d74967923c2de37ba889a006bfb875bf5857b/coverage-7.17.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:032d4c2db4c677eb71d43f2f8b80bf64e9346f7a3f7b6ebb13f1782f3cc6c274", size = 257855, upload-time = "2026-10-22T16:05:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fd/ecd1e9cddf1c36b5d03c438568b1f53e63db84bb3656539d71b454484c7d/coverage-7.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4343a9378b964eb3cb80fa90406385de91c7e982a769ca9314d848f93bc0d3f2", size = 259933, upload-time = "2026-10-22T16:05:25.79Z" }, + { url = "https://files.pythonhosted.org/packages/40/b6/e17a19dccdbcad494c64691f71c1dba3e55c30625bc29ffbb88ac5a0aa8b/coverage-7.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b609df3d6d900464207532c5170a65c6ede8d2e0b91f151f90008a866395cd76", size = 257972, upload-time = "2026-10-22T16:05:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0d/37dac534709a3fa43a937c8b0604839bb44b8670b83d143a4b78fec20d1e/coverage-7.17.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a01ef92caeedfa4f2ad171a3a8e6fece4565de871d8bca428151bf9929f41048", size = 262271, upload-time = "2026-10-22T16:05:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/7c05186925b4d64a46c271dfd944e3f0c1ad89db9095c31a6df157a75df2/coverage-7.17.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a3914e2c98c34b94c26d2add3d2a61ccfd3534fc4eaa879ee290bdbb5184ba6c", size = 257604, upload-time = "2026-10-22T16:05:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/95/65/c241cdee66a0ff86c68f6d37328e735d10e3d30cd6ff66f356039eb688d2/coverage-7.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8a72b26dc5c9a9422e45472b1d67ba5caf273c524c8668cc04b23062663755e", size = 259714, upload-time = "2026-10-22T16:05:31.402Z" }, + { url = "https://files.pythonhosted.org/packages/db/09/e93233fe2405574ce0a31ed201c0cba3a860e085db5d2828bf12b7dc0790/coverage-7.17.0-cp314-cp314-win32.whl", hash = "sha256:03baf0688e14a5a44ffd1eefa8928dfc92b14565e521174e072b31b971aa1657", size = 227914, upload-time = "2026-10-22T16:05:32.78Z" }, + { url = "https://files.pythonhosted.org/packages/d7/77/04979eb217edb94b94afabb4bcc179f60c746f68d3fd0b12688ba9195730/coverage-7.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3a4459b0f389dd1e51aac3dbfc4ca35b30cae7dc99caa3c0a086d1997a985709", size = 228543, upload-time = "2026-10-22T16:05:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/a0/38/155f2f2c6769f8c43eca8a5e581443faa0908828650090b97103b4a41376/coverage-7.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32ee5831b15ba239bef5d272ae586ca8acea4a661b5f2941f5a0d380e518b23", size = 228396, upload-time = "2026-10-22T16:05:35.951Z" }, + { url = "https://files.pythonhosted.org/packages/52/55/d6751572d9445485c4c5a51ec6ae11caa5c11b7ef2448cdccc884f7f54d9/coverage-7.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c083e253e96fdeeaf48133fe836cac86ace2db215d235700e7f7532420a200c", size = 226486, upload-time = "2026-10-22T16:05:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a8/82d37760e537bfdb9fdee285fa1e1b45bc153293d23e2331c7befedd1990/coverage-7.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b88fb4d4d7608cbcebdc7ff7dbffc2115e3b81d6c33a79147cd79d54fcb080f4", size = 226861, upload-time = "2026-10-22T16:05:38.777Z" }, + { url = "https://files.pythonhosted.org/packages/24/96/70ec00a9db6d7bdd3f914eba12057360769f5cb78a98456e71fe1937deb5/coverage-7.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2bf3ee38a40c0570c316b9e7507c472469586906a20f289705898528c31aaea8", size = 266849, upload-time = "2026-10-22T16:05:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/77/d8/d99be3bfbb208512f039c80b31e404138a86678fc929a17acd5b7135a6ae/coverage-7.17.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c570293f001ea91598b90e90cb9b98fddee833e90bbede432cbd218331ccd8e", size = 269402, upload-time = "2026-10-22T16:05:41.585Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/6cbe8b5a5060edbd21ce8109c4e900f8b7656e8b72ede6dc4d82dc0b8bfb/coverage-7.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:791f9ee09814dd30d9562b4f4eb037a371485ec1c830b53999d2320d20c4e764", size = 272025, upload-time = "2026-10-22T16:05:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/bd/05/d3633add01e5518dfdacfdbd1be41cb5c581412dd8bccd663edf6ef7fec8/coverage-7.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7c5672ecf1b497de7a0b37743bef40e3b58e3207e9fe2ed6682cb0420836a88", size = 273515, upload-time = "2026-10-22T16:05:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/57/d9/b83387780271bf9ea9f96a3aeb01c1c302c4494a927923c4fa70cd170a3f/coverage-7.17.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:337f87e62e1c5f43c8198d879f677c342d152f84d3f157add12558853cbb8eaf", size = 266725, upload-time = "2026-10-22T16:05:45.713Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/8e80b29764ae3f4d70d44952eee3f3927eb36897490fe799e400adb7f5da/coverage-7.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8a58baa8eb6154c6645eadcc19b6c089deab78c24a612a83206f2a8a5992a7c", size = 269312, upload-time = "2026-10-22T16:05:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/e702f588fe8d83ca844f133270ad0a49bf650c8cf260551ff6f11a6b6bbf/coverage-7.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb5745048ee20f796b0e3ab61831049e749acff188563e2af32f58ede957ac1b", size = 266740, upload-time = "2026-10-22T16:05:48.343Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/ee13ad14b714d75ef07c490b6e43969574a8b8b8c6d67bc741fcbacbf9c3/coverage-7.17.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:7b5abcae74750302ac4bc77351eaee87c450e4abd3b6225eacde3858e260f8b2", size = 271036, upload-time = "2026-10-22T16:05:49.727Z" }, + { url = "https://files.pythonhosted.org/packages/e2/bc/854e8912c11c54aaf416e74602000589c6efbbe1f251dc90202fdabddcb6/coverage-7.17.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4e153f0bd6509dbf837fd585f489b3328bfd98f2a718a0b15ae63032148b285c", size = 266158, upload-time = "2026-10-22T16:05:51.141Z" }, + { url = "https://files.pythonhosted.org/packages/a8/dd/96432830ddf63bc42654de54eee77a0cd6949cc65769bda06f353323f23a/coverage-7.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e42a835271922fe7bf98c7a05c78d4032f4e52bf5b07fafa1eadfdfc2f0ad22f", size = 268088, upload-time = "2026-10-22T16:05:52.92Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ee/230c6ec065a6629d41cc89323ceb54d37e2650c104f70fc108f4ee7c5051/coverage-7.17.0-cp314-cp314t-win32.whl", hash = "sha256:5900055351ec89fdffb34b27898815106529ded29a9496464c1ebd6313fd40fa", size = 228229, upload-time = "2026-10-22T16:05:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1d/23eb6aadcb3c00764ff2961c0ff55f5dfa2543856d932b949bbc04fa68e9/coverage-7.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5ee9c8bd13a59e301dc2aefe7aa8384633406808de051b1982775667094eb673", size = 229154, upload-time = "2026-10-22T16:05:55.682Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f1/7df117e8e016b7a3ee459e833b3456f3610505afefd071f8727702d98439/coverage-7.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a9cb9a0fb58e6fff5442969af36094ecd6976f4907f179e33b27e15355d8f74", size = 228726, upload-time = "2026-10-22T16:05:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/f9c24911755f91e6f451ca2e2c1804fbcbe9fecbc3310e341bbbbfa1d85b/coverage-7.17.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:adabc1079ba844b096799ba8638d8df434aca1613f95bb7d5791af320cf05559", size = 227196, upload-time = "2026-10-22T16:05:58.543Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a5/2b665b40c626e6bb40e5e8e23d91170b48b5f2f57186f46b07f2fac42386/coverage-7.17.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:6b4593d2871c4c801656dd5861e759f8f5156febd6b6061b02f6a72d64618b7b", size = 227315, upload-time = "2026-10-22T16:06:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8c/22b518d61b930b8a4d62905d046c76a03675ef19b466462011dfb16439a3/coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c2c48a36bb970b7265b196032bfd4237c0b373f2b25ef7012d7e905b5b35c19c", size = 225490, upload-time = "2026-10-22T16:06:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/08/5b/d3b141455944e030cd4987f055f8c84a44a1a0b28d81ae93389f44051691/coverage-7.17.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8bc2cd4c6aa58be1ca243fb8cddb6c3c42031fe4403991ad9cf2b3baed03d9f", size = 225797, upload-time = "2026-10-22T16:06:03.423Z" }, + { url = "https://files.pythonhosted.org/packages/03/24/aab2156983f7022361352640c583d08f0238e4cd7463eb921172001b4cfe/coverage-7.17.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:cbf0b83ce927de6053279ba61342d9f8892afde1385fcba9144cd05d4023d109", size = 226025, upload-time = "2026-10-22T16:06:04.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/db/94697b9ccf4ec94cdad07bf94e2a21b9702247cb212d2d5f18d2b9c3d0de/coverage-7.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:66efc0fb99cee34626fc17d3adac9c3cd816bfdad563bb1e983c3ef432f566d4", size = 226193, upload-time = "2026-10-22T16:06:06.167Z" }, + { url = "https://files.pythonhosted.org/packages/a0/fd/db1c4ade4219831d590fff5aa5b7d9ceaf84586e8e60da6fff71b6182251/coverage-7.17.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58a304efdf2253fb2ea03d45d0a04cc174a92a0cd5279c28c5bb893ce7410968", size = 258046, upload-time = "2026-10-22T16:06:07.51Z" }, + { url = "https://files.pythonhosted.org/packages/95/ee/84ed138990ed752abd73db4adffc1842d6152017d8e21809cc109a413fda/coverage-7.17.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aed06129a9f530a948928260d7d2d81c47261355b0127e3c782251b38958463c", size = 261735, upload-time = "2026-10-22T16:06:08.861Z" }, + { url = "https://files.pythonhosted.org/packages/64/ad/c1e4367bbd1e7d562d6bc979c82b542f2040544e95a8cd995b694caf0e1a/coverage-7.17.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6cdb47a9be2d3313829bed1d296086287682a3ee901ff987b3432b45705b771", size = 262819, upload-time = "2026-10-22T16:06:10.49Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3a/e8e8509b0940403eab15d86f70022790a01fce3e971a161919c544953f7a/coverage-7.17.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:804fbaf81d09b34e2f8b1159d01e211a50713d743d32fd9b2ca6cf0ae507a66f", size = 265194, upload-time = "2026-10-22T16:06:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/1f/77/d64bbdbb734b1059ebcde2b5df567f142a9948079e51af9b0abf8c9ec17b/coverage-7.17.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2afebb96da9a1bbca142f8cc12320c14e5c215e0a49cded4f191f5a31506a28", size = 258410, upload-time = "2026-10-22T16:06:13.234Z" }, + { url = "https://files.pythonhosted.org/packages/e2/29/7cf81f08a254532c074eb52cfe6b12bb753d08f2997269c9fac8de359541/coverage-7.17.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd04fd23206be9ad3de13b0b39313094fafa66a1f6baf70f5471c004a9fac47d", size = 260697, upload-time = "2026-10-22T16:06:14.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/56/bacea6dee2d5ccb31ab0cfffcee87ca7efa80ff048840209c8013aeaf02e/coverage-7.17.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:5ebfbc87836529a31d25c2dec1262c38eea852ec1c46c0f690a515a7339d9c5b", size = 258649, upload-time = "2026-10-22T16:06:16.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/97/fd8a6a63740f9ee177c53b59e2f4d0752369bb0fd154086a1e69aa1fb255/coverage-7.17.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:a59c3b470ac3b2432af3598a996affbeb10236cd899a005c1c4e517fcff8b592", size = 262989, upload-time = "2026-10-22T16:06:17.85Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/7cd3d3520d45db3788456452b65446182c08f2a7c276f40c9dd11966be02/coverage-7.17.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:7fef12cec4b96f59e09761cbd4fc9bdf4788062d1ae435b920241fc16c8a9549", size = 257955, upload-time = "2026-10-22T16:06:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b9/80343a55e07d854927a915cc21a52e350ae1db9ead33f0248507b960785f/coverage-7.17.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a14e8d69464da9cbe1dc29e7116dbd37296d1d51a695327c478e250349085089", size = 260512, upload-time = "2026-10-22T16:06:21.055Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/e4c816f0cb3efdc6392bb276ff88359729f9dec605bdd3c1c7a44776f759/coverage-7.17.0-cp315-cp315-win32.whl", hash = "sha256:9a66074231799d8884d267234d49f60c90b43a945dfe069646b72f3058cd4e67", size = 227913, upload-time = "2026-10-22T16:06:22.407Z" }, + { url = "https://files.pythonhosted.org/packages/43/14/283120142ac281834808584249b89957de997c28573ddd65ae106b622121/coverage-7.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:79cf71e5f9a48877ccfed5baa2c72f45af4feeaa596249e23eb15b473134830e", size = 228532, upload-time = "2026-10-22T16:06:23.756Z" }, + { url = "https://files.pythonhosted.org/packages/b9/bc/83dbc6c3bd58042afaefcb0cfd7d7a2e3076989a2f3d9bc6e6edac819784/coverage-7.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:09e563b49c81cd0a33734e926d7a4a6ef1778d74304cde2dcc3aff8b30ea1036", size = 228393, upload-time = "2026-10-22T16:06:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/00/ed/9463d1cb1766a2570343a372c2871c7fb4d561e5051f029399f4d57ce947/coverage-7.17.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a5e2e0625dfc789af0b2e10acfc106b352f314fffbd19e6392a7961c8cbe5d9a", size = 226481, upload-time = "2026-10-22T16:06:26.563Z" }, + { url = "https://files.pythonhosted.org/packages/ae/59/6536c7fa87be13236b4184fc395f14d18f622a5b8ef7448a6bfdf5f24ec0/coverage-7.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:194562ab1eff0d85aee8a17ce4c5c0bc1ebf70a629a7145257123156ad100fd6", size = 226876, upload-time = "2026-10-22T16:06:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bc/4ffae6038f78382c9ab6a4969cb5fc01d50b083d119851085807629abe88/coverage-7.17.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:85682ca89bc4601125dcdff23fa70e6274e13c9ad502fba845e6c274ab2c6569", size = 267241, upload-time = "2026-10-22T16:06:29.356Z" }, + { url = "https://files.pythonhosted.org/packages/c0/60/cae4b708b4217d33ebf66b9c57f8910a257346b3eaf054a78d73605ddc4c/coverage-7.17.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d0fac604a962ff546beb038cba9a5a09d88e6e3f01612fb46f7bd4fe5dc8dca", size = 270079, upload-time = "2026-10-22T16:06:30.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/84/1225ab04e55a6a2e483e903a5eb0f8f081cb5325df833594534aa288d91c/coverage-7.17.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72edd4f9483ae30cd1ba4f230687840ef7b58a16afb1c9e81131fd6d64655f36", size = 272775, upload-time = "2026-10-22T16:06:32.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/ad/567f8dd9246a553aa8753732c41f0e0654e98e9a617d6282e64eae0e6aa8/coverage-7.17.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91e487ad07c511da77b574820141a7c2da09bc2a4b4090b5be2424295d100697", size = 274110, upload-time = "2026-10-22T16:06:33.859Z" }, + { url = "https://files.pythonhosted.org/packages/66/5f/e0ee76895df40601e4aedffb6b8e71ea48e276a629fb4488b92cffffca14/coverage-7.17.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1cabb31531002fef5197de5df3c84730aef01e750e25154b9813a2583435aba2", size = 267775, upload-time = "2026-10-22T16:06:35.193Z" }, + { url = "https://files.pythonhosted.org/packages/19/6d/d19c6e45e940e5b9ba697b7be9a8e63cef73d90bdea45b670a59df466717/coverage-7.17.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c9ac886242bbd50fb35a2242ba79555a99891e657da33f98b7ca968e3488a350", size = 270268, upload-time = "2026-10-22T16:06:36.597Z" }, + { url = "https://files.pythonhosted.org/packages/27/a3/b2731bb7aef2db800b330603675c3fa58a6497a05d5b5f8625f3b6285f68/coverage-7.17.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d45cdc495671fc99268ff3653e0da4244972e9fc0e1fc3ea9de18781ad28963a", size = 267298, upload-time = "2026-10-22T16:06:37.973Z" }, + { url = "https://files.pythonhosted.org/packages/50/75/d0c90a81b6b6607933109914dc8b0ef0a5f4cacf9482ca868ae773ee5d0b/coverage-7.17.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:12961f738d717a046d4bf3f8d1727eb3ebe2ed5478ff96cd6233d4dfc39323d3", size = 271899, upload-time = "2026-10-22T16:06:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/64/1e/f038f3a2614e9ef839a40a184713aca40b8ed79e626ab5cdc388771045df/coverage-7.17.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1ee0a982b0e1230b3edf768a7dd022bd2377303763895327ac62deb5efacee03", size = 267380, upload-time = "2026-10-22T16:06:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/8d636e639b2d8b1934128573402a7caf66c8cfbc794fa02061d84f115c34/coverage-7.17.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:460679f53893081edb94eeed011d26be37228ba649b853fd2aed7e4394fb1f35", size = 268993, upload-time = "2026-10-22T16:06:42.361Z" }, + { url = "https://files.pythonhosted.org/packages/73/7a/219f6b391cb0636242d06c390a8add5af91afa3e92afd0380dbedd34f487/coverage-7.17.0-cp315-cp315t-win32.whl", hash = "sha256:600e121e412cd6bdb52ec105b95c6a04da0c9710d1534f6e64a42db667907802", size = 228216, upload-time = "2026-10-22T16:06:43.714Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3f/514d294319c5eac11494394545de209079f33993e14526c220eab29b2a96/coverage-7.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c3e11b81faafb1f063248735c3e2c5c4183fc1db2c7f46782170dd7760dffcbc", size = 229146, upload-time = "2026-10-22T16:06:45.262Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/c0d8bb0a67df838fc7bc6f4060ffc404fd1154fd18e624d83e6abc41aeef/coverage-7.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d472ee1c64868a917a47f02ffb4ac8c22b477f7589d57a3eb0c58b1ad4f0137c", size = 228716, upload-time = "2026-10-22T16:06:47.097Z" }, + { url = "https://files.pythonhosted.org/packages/c9/17/48032919343afa2906112d458aceda2043afdb24a2ee8eb4c60904d8ddf1/coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d", size = 217383, upload-time = "2026-10-22T16:06:48.471Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/f08d34ce09d60f89ebd391e2ebc6ba2b995e6dd7552f41820f8085f94e53/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67", size = 4716035, upload-time = "2026-09-30T15:29:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/e18fb65592451a2acb76e9f2fbe14e0f47a8318b4c5430f1633851d03daa/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a", size = 4726917, upload-time = "2026-09-30T15:29:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/38fdce17e60f6b825e69fc3b7f75e70a6612759980704697e1de4cbfaf6e/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48", size = 4715341, upload-time = "2026-09-30T15:29:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/d9121a717e0f893c64bd6ca7702614778d7df2a5c309128a002421788516/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42", size = 4726322, upload-time = "2026-09-30T15:29:54.263Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version >= '3.11.5' and python_full_version < '3.12'", + "python_full_version < '3.11.5'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "docutils" +version = "0.23" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, +] + +[[package]] +name = "filelock" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, +] + +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "hydrogram" +source = { editable = "." } +dependencies = [ + { name = "aiosqlite" }, + { name = "pyaes" }, + { name = "pysocks" }, +] + +[package.optional-dependencies] +docs = [ + { name = "furo" }, + { name = "pygments" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-copybutton" }, + { name = "sphinxcontrib-towncrier" }, + { name = "towncrier" }, +] +fast = [ + { name = "tgcrypto" }, + { name = "uvloop", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx", extra = ["http2"] }, + { name = "lxml" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "ruff" }, + { name = "twine" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiosqlite", specifier = ">=0.22.1" }, + { name = "furo", marker = "extra == 'docs'", specifier = ">=2025.12.19" }, + { name = "pyaes", specifier = "==1.6.1" }, + { name = "pygments", marker = "extra == 'docs'", specifier = ">=2.21.0" }, + { name = "pysocks", specifier = "==1.7.1" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=9.0.4" }, + { name = "sphinx-autobuild", marker = "extra == 'docs'", specifier = ">=2025.8.25" }, + { name = "sphinx-copybutton", marker = "extra == 'docs'", specifier = ">=0.5.2" }, + { name = "sphinxcontrib-towncrier", marker = "extra == 'docs'", specifier = ">=0.5.0a0" }, + { name = "tgcrypto", marker = "extra == 'fast'", specifier = ">=1.2.5" }, + { name = "towncrier", marker = "extra == 'docs'", specifier = ">=26.9.0" }, + { name = "uvloop", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin' and extra == 'fast') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux' and extra == 'fast')", specifier = ">=0.23.0" }, +] +provides-extras = ["docs", "fast"] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", extras = ["http2"], specifier = ">=0.28.1" }, + { name = "lxml", specifier = ">=6.1.3" }, + { name = "pre-commit", specifier = ">=4.7.0" }, + { name = "pytest", specifier = ">=9.2.0" }, + { name = "pytest-asyncio", specifier = ">=1.4.1" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "ruff", specifier = "==0.17.3" }, + { name = "twine", specifier = ">=7.0.0" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "id" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, +] + +[[package]] +name = "identify" +version = "2.6.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "10.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f6/4eda8458eb00ec03e715c376c6b23b3def69687924c68d36d1c4003d3bc4/importlib_metadata-10.1.0.tar.gz", hash = "sha256:82b04047fa09792c1a8cfeddfe08bec79e4b13c29c8469b0d3949860f61017e8", size = 67761, upload-time = "2026-10-24T00:01:44.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/b6/495438ad95e0102aff8f6786b5474202f38cd5d4700d03a72b77977dec10/importlib_metadata-10.1.0-py3-none-any.whl", hash = "sha256:cf45ce66a929c64e994185a809c04840d0501db17e663810d993d0f4829265c7", size = 32221, upload-time = "2026-10-24T00:01:43.507Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/ad/28ecd7cb894d172f3c9c80a075eeeb2017ac62e3632cee05a5f9493547eb/lxml-6.1.3.tar.gz", hash = "sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21", size = 4211198, upload-time = "2026-09-02T14:48:02.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/f1/95133bde7af7afb1f5ba6090b674d826b7a518318bba54bbbb633b27865a/lxml-6.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c66f858b82497173f73366795fc6ee8171620e75a338506d6b2e7bc16f5fca11", size = 8563141, upload-time = "2026-09-02T14:46:42.334Z" }, + { url = "https://files.pythonhosted.org/packages/80/54/5a79ee2181ac773ee13e48205411845feec69e1c3d097e985c1343171712/lxml-6.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:032a0a97eed428bd143c75a11118238546424ceb2fa311cca5f073aa44658dc4", size = 4613690, upload-time = "2026-09-02T14:46:45.253Z" }, + { url = "https://files.pythonhosted.org/packages/ab/29/8c24672f56807f119312f073f24204368574bd16b384ede861b5104b3a2b/lxml-6.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a579dfb9c835f8ab47f4b8ed33440cbc75b806b73297208e6ec2a33e903740b", size = 4935630, upload-time = "2026-09-02T14:46:48.071Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/ce2436d854c848c19fc9287143991f3fc76b8b4e9a0dbba8452e51dff264/lxml-6.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49fbc2682a9306135b7ec49e93f97f9c26689b9b7f96ed2742d8d6497e994d13", size = 5079033, upload-time = "2026-09-02T14:46:50.483Z" }, + { url = "https://files.pythonhosted.org/packages/91/ec/b66f66f6499ad800265d57540b51e6632e3232d3526f42f2f8fd4b14e0ea/lxml-6.1.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea2c01cdb16dc12156e455007c406dfaaece0c89aa4ba0e3b47586779f951d41", size = 5012298, upload-time = "2026-09-02T14:46:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/94/2a/25d128872f4d51753542bfc3feb482c2ea7c8a2d6d81a0bc5c6a00779ed4/lxml-6.1.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:527195c188d7d0af748cd48d220ab8cdc5cb99be3d49ac4d9be7324d8abf9bc0", size = 5211431, upload-time = "2026-09-02T14:46:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/0a41bbef074a556110f84fafb6d8c2998293c7d3bfbe1ce74515bc65393b/lxml-6.1.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:20384c2bbcbf87180c8c61eb60869699c1ec0cd09b62cfd13804022d860b0867", size = 5343417, upload-time = "2026-09-02T14:46:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cd/16116c3f91791aeeeab1cbe6e7eb6e646f127be7b0158b262eb526a21a0c/lxml-6.1.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:424aa5657141d306ba9ad1baab4b2c0a0719040075ee6c66aee9bb2dea2b5054", size = 4673219, upload-time = "2026-09-02T14:46:59.604Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bb/4dff849f443ef70221676aec938bc41e8bae6430aa2ca13b041319e14b98/lxml-6.1.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4736e6c87e603146d8949d8501da621ad20c31015060d3fcf95ace2859f3e3e6", size = 5281246, upload-time = "2026-09-02T14:47:02.375Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ac/4aa7dd059420bfd35278c7fe819e9d319ee36a0453b7bbde1907a7832d91/lxml-6.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6374e9e382e5a98c9c5e66d41b357b470da1c54bce30f17f9dc4bcc58436cc1c", size = 5055451, upload-time = "2026-09-02T14:47:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/44/20d90cf6f4234de9cd9eeb4f519419885fdb087fa80d073c7b57be342021/lxml-6.1.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:22eec57e26c418cde02c051ce9914a365e52a7f135a565c6f0480242aeebab48", size = 4722694, upload-time = "2026-09-02T14:47:08.461Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0e/6bee12325e53dd6613fe1e107def07583b6182ade03e94bfef8976622e44/lxml-6.1.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8753b8d51dbc86fd335ee31fcf7f3658e9f5c016d4edfb23f76ad295f4b8c9d0", size = 5269179, upload-time = "2026-09-02T14:47:10.647Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/54d269ce5cd0787c0424d9cef449ee794d4097725d13dd2acd6181c44e9c/lxml-6.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:207dfc3d47cf0e575e643bbc140dacc8863b39abaa1e5307cd64c7f2365b8a12", size = 5235559, upload-time = "2026-09-02T14:47:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f7/5a3095f187f1bec293591616a1677781acc265c5b313c009f8a19c471a09/lxml-6.1.3-cp311-cp311-win32.whl", hash = "sha256:18293f8a8d8b6a8e71ef37706b659e3846a4261232158167b1ddf35f6994f633", size = 3600377, upload-time = "2026-09-02T14:47:15.957Z" }, + { url = "https://files.pythonhosted.org/packages/45/5a/15531a0d307c96282fe8b639b3d74e8bd783e4ab4cb2b0781146ac4161b8/lxml-6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:7ae4949f212a53b007dbc355884fda122545c5764a54256c9217e419a62a6559", size = 4032700, upload-time = "2026-09-02T14:47:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/8de76314955545ceaaa7c0305017b8aaa217905dee59c62c0e2c1e44a68f/lxml-6.1.3-cp311-cp311-win_arm64.whl", hash = "sha256:2123e5aa075ac20d23c7af489255efd129cbfe190dbe88fd42598cc9df3199b6", size = 3674431, upload-time = "2026-09-02T14:47:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1f/a180b57d9eeabaab77f9d5aa30356898ea749c4795596a8f66d1eb6bef2e/lxml-6.1.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc", size = 8602094, upload-time = "2026-09-02T14:47:26.054Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/070c92013a1c029a602b03560d68772313d918268667fa993da7961759c9/lxml-6.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d", size = 4638308, upload-time = "2026-09-02T14:47:29.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1c/722e88883173097a1a375153e3c2447eba3060d0231522cf6596e99f4195/lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5", size = 4939696, upload-time = "2026-09-02T14:47:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11", size = 5105247, upload-time = "2026-09-02T14:47:36.734Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a0/a1f7f1313795bfec67b77f01ef3b1128d49f2d7f66a8413fa55d47f4e25f/lxml-6.1.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a", size = 5011915, upload-time = "2026-09-02T14:47:39.846Z" }, + { url = "https://files.pythonhosted.org/packages/b9/78/840e7e3f1d0cc7a5cfac5d8505b97e25b6427fd774ac4bae672aaebfb4b5/lxml-6.1.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32", size = 5638175, upload-time = "2026-09-02T14:47:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/0a/20/e022dbc6b4753a9bc9fc5fb28a27163430c1731b9913997f6544c1b2518c/lxml-6.1.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c", size = 5244675, upload-time = "2026-09-02T14:47:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/83/82cde81d2b5eb38d1539fdfdf318abdd014a7e604f4df01c9cd3deb18f2a/lxml-6.1.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56", size = 5358205, upload-time = "2026-09-02T14:47:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a1/f3b057371c8cb29f2a9c9c44ea320592446e40b74a4b0af68c3d8e65bc73/lxml-6.1.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f", size = 4704495, upload-time = "2026-09-02T14:47:53.251Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/230eb28be5d412152ffc3c679b51fe1aeede5a53f3a8eb6e9748f2f4754f/lxml-6.1.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5", size = 5255117, upload-time = "2026-09-02T14:47:55.963Z" }, + { url = "https://files.pythonhosted.org/packages/a3/18/1969f56763af24ce42ea156007b0b2d73fddea552e283b2010416394f0f4/lxml-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385", size = 5054424, upload-time = "2026-09-02T14:47:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/2a90acc1f6fabaa3a8db9340437822bd8d041b205d626a4b3e8621aaa390/lxml-6.1.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d", size = 4785572, upload-time = "2026-09-02T14:48:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1e/b90e845b1dcd0f2f3f26b98283d857f25909223aacd265eee032c34ab8b1/lxml-6.1.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9", size = 5656516, upload-time = "2026-09-02T14:48:03.419Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/0a1b802c57f3fba5c4efd77d5c6b78adaa8f7b681f0c90456b140fe8bf6c/lxml-6.1.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e", size = 5245982, upload-time = "2026-09-02T14:48:06.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/2c016fbceb3778137459292538d9dfa7e3ad9070fe409c15254ddd90d2cc/lxml-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5", size = 5267340, upload-time = "2026-09-02T14:48:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b1/736d18fd6f0835761923b7bac1f0c27d60c1200384e9093f05d8c5100525/lxml-6.1.3-cp312-cp312-win32.whl", hash = "sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c", size = 3602606, upload-time = "2026-09-02T14:48:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/6ed903e4e6278a020c8a6f0dbbe78030d041840a6b4a64ea441a1e414077/lxml-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c", size = 4005999, upload-time = "2026-09-02T14:48:12.51Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1b/7bcebb7b6332cb3ae85e9c13b139adb6f23f75c71d84041c56a5005d9a29/lxml-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa", size = 3666631, upload-time = "2026-09-02T14:48:14.567Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/3ef45db776baea068044c799bbba68f3ca00a440c0e930a17c572f3d9639/lxml-6.1.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd", size = 8590357, upload-time = "2026-09-02T14:48:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a5/eee2fc77eee5ea68e4a4334b1def1781a3beaeefd3d98e81b4a38dc447b7/lxml-6.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1", size = 4632616, upload-time = "2026-09-02T14:48:20.745Z" }, + { url = "https://files.pythonhosted.org/packages/35/42/df27b56848acd29d8a720acc28977911aab36f2a09df4208d5502e887415/lxml-6.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d", size = 4936186, upload-time = "2026-09-02T14:48:22.94Z" }, + { url = "https://files.pythonhosted.org/packages/ab/8d/8a7b91df0b54d09d25f5f44885d6b3e0a6d6643a8c070191580318d20c42/lxml-6.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed", size = 5093324, upload-time = "2026-09-02T14:48:25.132Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/8f340ddcd43790332fb0de8a26628d571a492da3300cd191821698407c96/lxml-6.1.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2", size = 4998850, upload-time = "2026-09-02T14:48:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/9c5bb572f1f09ec9e4322bd4a4e9f4ad48347fc56ef94cf4df58a5279dc8/lxml-6.1.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8", size = 5626813, upload-time = "2026-09-02T14:48:29.61Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/8bf1fd8bae8247743968bb76d027a1ac5bd2c4b44495fba6a71b30d10706/lxml-6.1.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e", size = 5232385, upload-time = "2026-09-02T14:48:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/6cef69ed81cb7df0d03b0dd09d08e6e2cf5061a743ff6f42f0b741548e9b/lxml-6.1.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245", size = 5347088, upload-time = "2026-09-02T14:48:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e1/8e5fd8ddc8c7d685badb0f2db149e3c9da84eefc2827c01c658df2c4e3cb/lxml-6.1.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0", size = 4707227, upload-time = "2026-09-02T14:48:36.62Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7e/00041382a11be40a88bf405ebff11c8efabd3de79f2691e1638b1c47a8a0/lxml-6.1.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e", size = 5240208, upload-time = "2026-09-02T14:48:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/316538b5cff0936fa63d45d421c655730fcbb5a28dcac728c175083002bc/lxml-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2", size = 5050271, upload-time = "2026-09-02T14:48:41.213Z" }, + { url = "https://files.pythonhosted.org/packages/c9/91/455bcccb3ac725373007344d351151810cd19762d1673b64b811f4359a42/lxml-6.1.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310", size = 4780433, upload-time = "2026-09-02T14:48:43.779Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/580440e2f52cf00bba5c5e1080bfa88cdfcde73be71a11d95170ddbb663f/lxml-6.1.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748", size = 5645928, upload-time = "2026-09-02T14:48:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/d123c1f244306543d545f62443f794959e4f1ea709fe100f8740d514e74a/lxml-6.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d", size = 5231184, upload-time = "2026-09-02T14:48:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3c/fe55b2bd5c6113c906511cd88f6a470195c5fbff1124f19970ab706c3477/lxml-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc", size = 5255814, upload-time = "2026-09-02T14:48:50.948Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a7/485df55acf55dc35e4ca89d2f48f03889e5a3241826b18b85102b32ce9d8/lxml-6.1.3-cp313-cp313-win32.whl", hash = "sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87", size = 3602214, upload-time = "2026-09-02T14:48:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/e46a7702bd95e9043291f7c3539b6184cba66f96cea9936f20939b284eeb/lxml-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477", size = 4004091, upload-time = "2026-09-02T14:48:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/154c78e20479a43916e63f19cb720d83f44f024b03228be44c92d9a97b24/lxml-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1", size = 3665468, upload-time = "2026-09-02T14:48:57.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/15/fc75a70b0af6021d0ea16811f1fc71cc42cd06ce90fe10f007a69b2eed84/lxml-6.1.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165", size = 8609725, upload-time = "2026-09-02T14:49:00.156Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/398fcf9018f881ec9aeaafae1ddd6586dfb13314a35d35e899de373dcae0/lxml-6.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d", size = 4639629, upload-time = "2026-09-02T14:49:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2d/49b6a6ad7ce8f64b07b9fe852ff0c6d3fcbb26db61bee4f63d4120180a1c/lxml-6.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e", size = 4965074, upload-time = "2026-09-02T14:49:05.133Z" }, + { url = "https://files.pythonhosted.org/packages/66/bc/6230cf80e4331c33383b0b6b73dc31a393dd76edd4cb73d761de5123034d/lxml-6.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8", size = 5099355, upload-time = "2026-09-02T14:49:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cf/d1143d9b7717e07a82f158a1fc9ce6e581fdad1226734950af869e3ffde4/lxml-6.1.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75", size = 5036795, upload-time = "2026-09-02T14:49:09.65Z" }, + { url = "https://files.pythonhosted.org/packages/31/6f/194bb00ffb89712c30f5a7e1b8e685590e140fad6c8261fec172c09a3dc0/lxml-6.1.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9", size = 5658740, upload-time = "2026-09-02T14:49:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/27e3cee3dcdb3b7bc09727b642bdbfcd098490ea77df04611db9060d7722/lxml-6.1.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0", size = 5245991, upload-time = "2026-09-02T14:49:14.154Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/8312560579fc980bbd2233a8a673cc46f7d613d3633f2bf08a21e8f4ad13/lxml-6.1.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6", size = 5354136, upload-time = "2026-09-02T14:49:16.459Z" }, + { url = "https://files.pythonhosted.org/packages/74/d8/eda60f4f73a9c780b5d6e1175484f66e6c81a2c93346e2906a1fec9c7a02/lxml-6.1.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023", size = 4704379, upload-time = "2026-09-02T14:49:19.032Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c8/c9cc60057be78ac34bd2b842e45e6e88edbfe5e532e82c3b82381b7aab49/lxml-6.1.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e", size = 5258676, upload-time = "2026-09-02T14:49:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/66894008fee8d1785b8db129747ae963fd427b68f456918df7f2f24a8b98/lxml-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92", size = 5090069, upload-time = "2026-09-02T14:49:23.562Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/c1b60404859f4c3cd1f41f29c65a24e25cea78fde822d9574a21f66810be/lxml-6.1.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48", size = 4741958, upload-time = "2026-09-02T14:49:26.037Z" }, + { url = "https://files.pythonhosted.org/packages/23/b8/6285f0cf546f14da2554cabdeaf7c2c2ff3190c74807f0de2e8810a786f9/lxml-6.1.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d", size = 5683245, upload-time = "2026-09-02T14:49:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f6/2168cab44336dcb15fed0f0b78577225b83297cdf0dee349c95420c3dcb0/lxml-6.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559", size = 5246087, upload-time = "2026-09-02T14:49:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/f5/89/32f5de69a0a31f30e6164981851f87b37ecb2c4ee838e504b88d49d4818e/lxml-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415", size = 5269352, upload-time = "2026-09-02T14:49:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/741d952ed3a7ef7a50055c6415aec3f067015e97f72f4389ce77b09657ba/lxml-6.1.3-cp314-cp314-win32.whl", hash = "sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d", size = 3662783, upload-time = "2026-09-02T14:50:23.751Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bc/5811cc73cac05e324e05ba9b0924e1a163a317a167ede8a9c748b11db30a/lxml-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861", size = 4073951, upload-time = "2026-09-02T14:50:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/92/18/3768c8b01ac3a9bed1914715e6011711b00e2a11628ffa6f7fa37f8e0269/lxml-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376", size = 3749279, upload-time = "2026-09-02T14:50:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/72/38/84684784738d9451db2b330de2483f496690c3a5c642071df24135739b37/lxml-6.1.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f", size = 8860296, upload-time = "2026-09-02T14:49:36.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/b7/fc4c50bb1b38e864010ea396046cabe85129bf9e65b11edcfbc37d356241/lxml-6.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55", size = 4755190, upload-time = "2026-09-02T14:49:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/e2/ee9aa6ed2b666b2db1f6f7fd48964ff9da39ebe827ef5eac0ab881f639d9/lxml-6.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2", size = 4979517, upload-time = "2026-09-02T14:49:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/29/e3/e7763d1661b283ddd4fa36f91b9a497db6b8d2aff55028b16c7f642e0755/lxml-6.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626", size = 5115270, upload-time = "2026-09-02T14:49:44.493Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cd/22205d5b4d177e3f4156f780412426ee7c7f8107809f119f0dcc40fa51e3/lxml-6.1.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414", size = 5032449, upload-time = "2026-09-02T14:49:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/06a4626c3bb79ef8c501b674afab8100d64e798665bb2a97d1c960636a49/lxml-6.1.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17", size = 5603325, upload-time = "2026-09-02T14:49:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/733682a0c2de9f5779ba207bbb3f3f6be8c6bda863fc01739b186b38783a/lxml-6.1.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473", size = 5229023, upload-time = "2026-09-02T14:49:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8a/e69cdaca3fd33a647942925664f01b20908d41a6968c182305be9c38fb11/lxml-6.1.3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37", size = 5317811, upload-time = "2026-09-02T14:49:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/0c397588174403c2ab68fc464abf97e03e7324f9c6cb6a99023104707195/lxml-6.1.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70", size = 4646516, upload-time = "2026-09-02T14:49:57.761Z" }, + { url = "https://files.pythonhosted.org/packages/56/7e/cfea25afafbe49db8b225764f7f74bb37c2a7f5e717d917d3d4a5e098ed4/lxml-6.1.3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7", size = 5240626, upload-time = "2026-09-02T14:50:00.279Z" }, + { url = "https://files.pythonhosted.org/packages/a1/75/7a587771bb52ebb0e2c57b6dbe9fd96a70fbb54d72ddd97d54c5f8ec18d5/lxml-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2", size = 5086619, upload-time = "2026-09-02T14:50:03.245Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/94c0ebe6d831861542d251e038052e52bf6d33f1d18f1cfffdc82851065a/lxml-6.1.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c", size = 4758828, upload-time = "2026-09-02T14:50:05.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/938d67bd0e5b1fdfa52be28aefdffbad57e1f6b8e921c2aab88542c75f40/lxml-6.1.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8", size = 5627083, upload-time = "2026-09-02T14:50:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/d8/65/4e51522f6c214650db0abb7b16ccd11b1238b8a05a8d59aa4ebed59c9f67/lxml-6.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb", size = 5235170, upload-time = "2026-09-02T14:50:11.255Z" }, + { url = "https://files.pythonhosted.org/packages/92/c2/e73d19365665f6b16ef84df21199befc3b06e4c539046ad2d9595f6fb9ea/lxml-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8", size = 5252273, upload-time = "2026-09-02T14:50:13.782Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/7f386c84c9fe2854e1ca6e231c285e1c8f392971ac353c6865e6ec49faff/lxml-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a", size = 3902712, upload-time = "2026-09-02T14:50:16.171Z" }, + { url = "https://files.pythonhosted.org/packages/82/a6/8a3eb793f7900ef01c7f99e6f5fcbcfbdff35251cfaef66b32a4c16352d6/lxml-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2", size = 4400979, upload-time = "2026-09-02T14:50:18.621Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/3807bea283b4fe9e9d9f5dde46a73df91178472b335d2778e10b2a37aa22/lxml-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026", size = 3823401, upload-time = "2026-09-02T14:50:21.119Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/4614fcd65496054cfb7172662f3576a59200278739506433b8c241ea422a/lxml-6.1.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0", size = 8609378, upload-time = "2026-09-02T14:50:31.772Z" }, + { url = "https://files.pythonhosted.org/packages/f2/51/2cdce3c65fa99a6195dd8fbd512d33407c1000ad99f63e0a285b63d7a8eb/lxml-6.1.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9", size = 4640022, upload-time = "2026-09-02T14:50:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/0b30084e9eb1c546a4be3d9c56df70058d116b1a320400a59b0f7da87bf0/lxml-6.1.3-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79", size = 5037928, upload-time = "2026-09-02T14:50:37.007Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/5c37275a3e361f6138dc06db748ea565c1fe8a5f4ee5e2ddd80047c81a89/lxml-6.1.3-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015", size = 5661932, upload-time = "2026-09-02T14:50:39.777Z" }, + { url = "https://files.pythonhosted.org/packages/70/c5/b71ffb289b15e2642e2a3cf6d468c44da39ea119061a99e5b05e3d10f217/lxml-6.1.3-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a", size = 5249209, upload-time = "2026-09-02T14:50:42.141Z" }, + { url = "https://files.pythonhosted.org/packages/81/ea/9910da149a23932f9301652e57661cd9e42b0df18f12be21159b7255f92b/lxml-6.1.3-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed", size = 4704543, upload-time = "2026-09-02T14:50:44.634Z" }, + { url = "https://files.pythonhosted.org/packages/76/07/9290329cd188c62e22021f79df04ee0cc33d9a93b0d38bd65ccd452ad9d0/lxml-6.1.3-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156", size = 5261298, upload-time = "2026-09-02T14:50:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0c/aba78bd3401cd99b73a0aed8e2b9b43e14be94fab3603d4bbc8a62365f2a/lxml-6.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d", size = 5090453, upload-time = "2026-09-02T14:50:49.952Z" }, + { url = "https://files.pythonhosted.org/packages/8d/dc/fa4426c3355aa0216cbeb3911495b5f65a26e0df85859a89928fe28f0396/lxml-6.1.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0", size = 4744709, upload-time = "2026-09-02T14:50:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/be/2b/224fe7918658ab7c532ac2412f3c1eb28f71e6364fb07566262d0cc6a7b6/lxml-6.1.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69", size = 5685802, upload-time = "2026-09-02T14:50:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/21/44/7d480819b9adcae5f84dd8ac529132c6b7a578544398225cd20321adcd91/lxml-6.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0", size = 5249019, upload-time = "2026-09-02T14:50:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/385a267ea1b6b283f2249dd827ef360a295e9db14e13ef4665a120c60d64/lxml-6.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4", size = 5271886, upload-time = "2026-09-02T14:51:01.667Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/f967b0eb172ae876855a402d6d9b11fa86e3e0c89ca9bbfeadf7ffbfa719/lxml-6.1.3-cp315-cp315-win32.whl", hash = "sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4", size = 3662894, upload-time = "2026-09-02T14:51:45.173Z" }, + { url = "https://files.pythonhosted.org/packages/f4/48/d8a8c4160a29e663109ad520bac2deb37fcd014756d024561e8bc3e611ec/lxml-6.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad", size = 4074626, upload-time = "2026-09-02T14:51:47.77Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/3e1395d34d19f9254625d0b567b81cf70d37d3417be074f4d63b94a2be3c/lxml-6.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758", size = 3749495, upload-time = "2026-09-02T14:51:50.663Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c6/7465ffd9c43883526a382df6fa4846c9d8d419214f7effbf65270e795471/lxml-6.1.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe", size = 8857677, upload-time = "2026-09-02T14:51:05.109Z" }, + { url = "https://files.pythonhosted.org/packages/ed/eb/1f3a917e299df43c8162c3e6f64fc2cea3bcf277910f35bff5b8e5d39901/lxml-6.1.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741", size = 4754522, upload-time = "2026-09-02T14:51:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f9/f81b4bdb6efb7a596be29603d8758154d00a5f545db9f3cef9d9041c8f64/lxml-6.1.3-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300", size = 5033744, upload-time = "2026-09-02T14:51:10.633Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/26d9bfaacb319c86e0eca8a1a0bf1130d36a7afbd318883e23caea63763d/lxml-6.1.3-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0", size = 5615269, upload-time = "2026-09-02T14:51:13.357Z" }, + { url = "https://files.pythonhosted.org/packages/5d/90/73675f3f4141350ed65d6fec533b107d4e802c5caa340cf111771edd86e0/lxml-6.1.3-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd", size = 5236280, upload-time = "2026-09-02T14:51:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/ed260767e7977de463a0f91f3f4fffcab85c0a2a024a21ffe1fa442c2c79/lxml-6.1.3-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e", size = 4650718, upload-time = "2026-09-02T14:51:19.102Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/e9839d03b1e767f2725cf7d7d81b80d5f3f9fdc10ad8827e2479311b046e/lxml-6.1.3-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2", size = 5243376, upload-time = "2026-09-02T14:51:21.606Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/4606e347e2788c301f677004aa83e28d24da9fe663a24380122af57be6fc/lxml-6.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a", size = 5092340, upload-time = "2026-09-02T14:51:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/3314a8661cdf30f493c55a87db283961dfaae08451976a2ca418958e1804/lxml-6.1.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011", size = 4758768, upload-time = "2026-09-02T14:51:26.813Z" }, + { url = "https://files.pythonhosted.org/packages/30/58/3bdc577f78ea8b7d72d39a84506f7001d5b28728f43e5b84891e3b7d9a4a/lxml-6.1.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5", size = 5649546, upload-time = "2026-09-02T14:51:29.453Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e4/652633de1a2395949ebb7a8fc7d089aba12a2b45f0fefbc9d29e3e3ab3cf/lxml-6.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a", size = 5234874, upload-time = "2026-09-02T14:51:32.262Z" }, + { url = "https://files.pythonhosted.org/packages/65/a6/c4581d171de30449304b4859bbd3607e9b40da13c0f88b68e6097c8d785e/lxml-6.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887", size = 5260043, upload-time = "2026-09-02T14:51:34.841Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/ed6ee6186a89e69ca4ea9658b2a278f46a5efe8b5d4db56c7197f18653fe/lxml-6.1.3-cp315-cp315t-win32.whl", hash = "sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e", size = 3901093, upload-time = "2026-09-02T14:51:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/67/9d/11d10257a4a048d04195d638bb61f0246ce2448eb05f682bcbab25a257a8/lxml-6.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6", size = 4395446, upload-time = "2026-09-02T14:51:39.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/44edd7de434181c582892e68d1ffe6775ca403ce14aea07cb5a218a936cf/lxml-6.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf", size = 3822836, upload-time = "2026-09-02T14:51:42.471Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c1/2433176de263cc3f51fd2c303f993d5bb7f1da3139a0f7d168116c0bfa7a/lxml-6.1.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2765c18ce303149ee804b1f3dad11232726dd0a702d73a15cf19179ac8cc962", size = 3942969, upload-time = "2026-09-02T14:46:36.55Z" }, + { url = "https://files.pythonhosted.org/packages/7c/71/de7759096f480180fd9e43ff7c017860e2d2a9a43741ab093cbdf1820f07/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d5a748d12dd9b535e0a130f60dae9ddf0adafbabe61e7864f55c7436c84547a", size = 4213008, upload-time = "2026-09-02T14:46:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/c2d09af47a34fa6c0c27473083812b449a411680bd04bbe609cde291ddc8/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41096ec0740a58dad03d3ae0c7486d306d20becefb13ceb1649835ab3eb64167", size = 4322012, upload-time = "2026-09-02T14:46:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/68/f3/bf56fee0403ebd995be8e78ec9aca566016487d1b3cbf755ebea8ccffbdb/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415e3a115c0d510e329020012834d1c0aa1c581ee53a218603e38abbc1dea70a", size = 4257402, upload-time = "2026-09-02T14:46:43.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1d/6da9cc086a20d9dd6bcbf7c5d9575f0331cca9a05e67dab02d15e828170b/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20428910dae17a1a93152a3ff2c0441d2f4932992c0797d65651dd0561f1792f", size = 4410889, upload-time = "2026-09-02T14:46:46.975Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/91fe48856f9f8089be3096fa4dbe4b3fb5526f3bf3e852ea9497f399cb9f/lxml-6.1.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae", size = 3511258, upload-time = "2026-09-02T14:46:49.046Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/55/18dbb4778b30ada5ce071608503cc3edc9e14e13d868c17a6d178fc30f7a/markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346", size = 11520, upload-time = "2026-10-02T23:04:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/6c/14/0b05f79b4733e264a18d08fe08fa1df7347630ff32a6cb82180d9dccec55/markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91", size = 12063, upload-time = "2026-10-02T23:04:37.291Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3a/63ba10b6c1463216b3e4df669a9f0e5a3b0c3071557d2e8229e3968c79fb/markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef", size = 24321, upload-time = "2026-10-02T23:04:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/5f015261b76ad633d187ef6f388b413aedd64a8773c4df59e530a0be5525/markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169", size = 28845, upload-time = "2026-10-02T23:04:39.295Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/26e594b26be40c2f1fec63ccf8a8b99d0335a5b2cbe84835c7d82a994375/markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb", size = 24701, upload-time = "2026-10-02T23:04:40.348Z" }, + { url = "https://files.pythonhosted.org/packages/81/a5/a513b76c139a3915b43404324e55c0b7979ae4f0d39eb6f075b0282e90a8/markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808", size = 22961, upload-time = "2026-10-02T23:04:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/4c66192c100b4542bcbe392ae06696b670f66927be3ac38a213234778ff9/markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692", size = 21920, upload-time = "2026-10-02T23:04:42.429Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/ac3662678bfbad649893117ada2ba44dc30bf56884e84f13154a792b10f1/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d", size = 23726, upload-time = "2026-10-02T23:04:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/fe47cee339180a69ebca3c57fb3483d0f5cbd1e8337d1871fb1d9c1aebee/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21", size = 22539, upload-time = "2026-10-02T23:04:44.618Z" }, + { url = "https://files.pythonhosted.org/packages/12/32/d55440ba140442800e02d799c9cb5ab597bf6ebdb1177b5ea39a11f797bd/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707", size = 23963, upload-time = "2026-10-02T23:04:45.656Z" }, + { url = "https://files.pythonhosted.org/packages/50/9d/9c86042cb364c2ad4c971e6d1247929effd25f714cd7ee11b05e6316445b/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e", size = 21506, upload-time = "2026-10-02T23:04:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/75/ef/5b824f03ba40c3b3652b6272d083d2fc4fcdd440de519a3a39ba2c3e7262/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7", size = 22955, upload-time = "2026-10-02T23:04:47.698Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/44cfcb5ea40e5e43cec7793ef90704ed0c475280839076c4345757eb8e59/markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5", size = 14094, upload-time = "2026-10-02T23:04:48.788Z" }, + { url = "https://files.pythonhosted.org/packages/91/89/f2b509f7bf79352e40117824c1070dbeafd4df67031d3fa98165a3134228/markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3", size = 14328, upload-time = "2026-10-02T23:04:49.801Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5a/ccf22672a0f64dc682306e288f0dabcb06c7a201f3bb6e6cbf86d9e8ad03/markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e", size = 14188, upload-time = "2026-10-02T23:04:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, + { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/7632a53360671a9b750cdbabaf9cdd89f18b42248b8e4cb42c0b0296e459/markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", size = 14108, upload-time = "2026-10-02T23:05:05.513Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/62495e180b7000aaf30000fff849e933f74264638057176cf46852500adc/markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", size = 14303, upload-time = "2026-10-02T23:05:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/4c24208776a65878d656996945aacfbfe010d3720d1a98fc0eb8491fc03b/markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", size = 14174, upload-time = "2026-10-02T23:05:07.617Z" }, + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz", hash = "sha256:cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63", size = 154133, upload-time = "2026-10-14T18:50:30.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl", hash = "sha256:35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0", size = 74405, upload-time = "2026-10-14T18:50:29.116Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" }, + { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" }, + { url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" }, + { url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729, upload-time = "2026-08-23T14:26:26.932Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462, upload-time = "2026-08-23T14:26:28.294Z" }, + { url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/28/1c058a9143d2304b3078f8dadcbb9dc27f7d3581b5d05157c84869c4cc0d/platformdirs-4.14.0.tar.gz", hash = "sha256:83f009c1e8c5fc36869b5171755b291887bd2cade7965233cdfabd23d754720b", size = 70387, upload-time = "2026-10-28T18:27:30.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/2b/887969ef4bb53e0cb271dc7c3f4a73d50c5d58bb88aeaae311e3340cd57e/platformdirs-4.14.0-py3-none-any.whl", hash = "sha256:1dadfc59a8a5d7565b3d9270ae8fa71c065595352c4277b41c11ee6c222c4b91", size = 35259, upload-time = "2026-10-28T18:27:29.659Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" }, +] + +[[package]] +name = "pyaes" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload-time = "2017-09-20T21:17:54.23Z" } + +[[package]] +name = "pycparser" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/9a/887535e477847f5159377ab35303923644840b13c0ca03c0a467f84d4680/pytest_asyncio-1.4.1.tar.gz", hash = "sha256:e8266eb094e5ee9ffdb2068c9836f33e209ea77a970eb846933d416b5602669b", size = 145237, upload-time = "2026-10-25T21:05:37.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/d7/cd9e2708afdbf059658adaf08bdb0792ca52489871051d6ce7d498b51277/pytest_asyncio-1.4.1-py3-none-any.whl", hash = "sha256:507afa78ec77c6661164ef1a2368bc7820383fd1a14b2a7f813de8b5a92949cc", size = 17061, upload-time = "2026-10-25T21:05:35.473Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/0b/908da9ef8a14b600fc188eee8d63ede071996114613721153f6fc9ea3cd5/python_discovery-1.6.7.tar.gz", hash = "sha256:7f7c28568c8ec3d988d8bddbdbec8e65f08e8767e52f734e18b8b498549b9b70", size = 117807, upload-time = "2026-10-28T06:24:21.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/e4/1c7c388e874222c28afb92fe3ebfeebe55555276523ba7aab0a82a10b2b5/python_discovery-1.6.7-py3-none-any.whl", hash = "sha256:5422f67e5f533bb02d7b73de1c422ed74037536fd869e05156c07565926ec68e", size = 39156, upload-time = "2026-10-28T06:24:20.462Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readme-renderer" +version = "46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "docutils", version = "0.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl", hash = "sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560", size = 14239, upload-time = "2026-08-28T15:18:31.132Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11.5'", +] +sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337, upload-time = "2026-09-24T02:36:35.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019, upload-time = "2026-09-24T02:36:33.709Z" }, +] + +[[package]] +name = "soupsieve" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.11.5' and python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/19/63/0b6bae54546aed98d72f12d09df57b19a287f01c84d22047fa2e23772ab9/soupsieve-3.0.4.tar.gz", hash = "sha256:924fab84ae21604c19e175e1f39815eb6fa83ba23836e09e9de180925e5bc4c0", size = 119017, upload-time = "2026-10-22T00:45:26.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/66/473f7ca0be0b06e38ee8c85ee2db184e3981bd769fac2e7c19c8a8865211/soupsieve-3.0.4-py3-none-any.whl", hash = "sha256:8412826614f9c632227067d0985aeb9f584bd15efc852955430e097a9fe7afbf", size = 42458, upload-time = "2026-10-22T00:45:24.843Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11.5' and python_full_version < '3.12'", + "python_full_version < '3.11.5'", +] +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/73/567727fc8c8d85bc3adc88a246e12937cfda8d565cf1d80e85bceaa61fe5/sphinx-9.1.1.tar.gz", hash = "sha256:48b627f02908f22c601b3ba9c7b17265b4f315dd05c8bc64cd4da41183f44fbc", size = 8725638, upload-time = "2026-10-20T00:41:13.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/18/3c66ea76da7de47e716b181c35bdbfafbd5e9af0bb247b14578e9de348e9/sphinx-9.1.1-py3-none-any.whl", hash = "sha256:f8b60013ce25017be41a509e5c2d265bc1918410061315c36b809c7f335646c0", size = 3923559, upload-time = "2026-10-20T00:41:11.288Z" }, +] + +[[package]] +name = "sphinx" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.23", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/d4/413466f0be8b1cb0e3e43893a148b96d7462d0923d82b4166b21afc5f30c/sphinx-9.2.0.tar.gz", hash = "sha256:90470459f3cab15d9dfed146c71cf77694b0445df17b2e0e69059cb9f7fc9ee4", size = 8782438, upload-time = "2026-10-21T00:05:15.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/28/a0c50761cd215b7042f4bc617a6bc5c0cd7be62f75bc908e169e28a4d21e/sphinx-9.2.0-py3-none-any.whl", hash = "sha256:4c3ec13b612712c0d3ce4cbdd9e3eb91e68577e07a8e31f32f21cf178453578c", size = 3925388, upload-time = "2026-10-21T00:05:12.241Z" }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/02/4f334c7fc3e54c51d2364575a41cd2fae6847e7f53001949f0be5f32e18b/sphinxcontrib_jsmath-2.0.0.tar.gz", hash = "sha256:551f780cf9687f9c3c74f0cf9f18aca06e0d6958853453f3803fe4d357c5b924", size = 14511, upload-time = "2026-10-20T18:22:05.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/c2/2965c35b39587d4b7a397f65a9c7d4b942b031061e7ac1ce91782f394a0d/sphinxcontrib_jsmath-2.0.0-py3-none-any.whl", hash = "sha256:15ca92fe0eec82f0baf91a7efcc51ffacdb605924be6327683a52ebdacb6744c", size = 78095, upload-time = "2026-10-20T18:22:04.42Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sphinxcontrib-towncrier" +version = "0.5.0a0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "sphinx", version = "9.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "towncrier" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/fe/72ed57093e28af10595c50839b183c5fdf0952482e9ef0ca6eb90eb85c5d/sphinxcontrib_towncrier-0.5.0a0.tar.gz", hash = "sha256:294e69df6e275e7a86df7ea6a927cc7c28c2c370a884cd5c45de6ec989858f27", size = 62453, upload-time = "2025-02-28T01:59:16.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/5c/f7e39f243636a5e1894f2f5a72579977bf3968922afdb75175ee45062066/sphinxcontrib_towncrier-0.5.0a0-py3-none-any.whl", hash = "sha256:11d130c3ad5e4649821d543c4ea7ab64bbe78df4d859ef94f4298e7845dc0f59", size = 12609, upload-time = "2025-02-28T01:59:15.178Z" }, +] + +[[package]] +name = "starlette" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/bb/3d8757f21e9a53d9035c9eb039a777047139a16bfdbedfc7a58241c3d914/starlette-1.10.0.tar.gz", hash = "sha256:b8399098d4c1e3522734f7235dc79b1fc967f96cf139c81e0fa30479e0685f9b", size = 2734356, upload-time = "2026-10-24T15:04:12.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/2e/947cfe02def9371c07cd5bf5b6b61c829e9aecb9904db02a66cfafbc1eec/starlette-1.10.0-py3-none-any.whl", hash = "sha256:478c0ab3b4ced6e82719849c118fa406f0278a3499c1a898cd15cd31bdc38ce4", size = 80571, upload-time = "2026-10-24T15:04:10.371Z" }, +] + +[[package]] +name = "tgcrypto" +version = "1.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/59/7cf5ced989e3139a791d5452d58cb8994de589576b80f9267ba76d794f6c/TgCrypto-1.2.5.tar.gz", hash = "sha256:9bc2cac6fb9a12ef5b08f3dd500174fe374d89b660cce981f57e3138559cb682", size = 37280, upload-time = "2022-11-11T19:55:11.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/e6/07dc413e57f3677e0b887975b174d9cbc8170e92651dd35a784da9571d5f/TgCrypto-1.2.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:59597cdb1c87eb1184088563d20b42a8f2e431e9334fed64926079044ad2a4af", size = 58988, upload-time = "2022-11-11T19:51:41.686Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fe/a418223ff6079184f50f5ad8c2dac37ec40adbb42b886c3ee44fdba2a540/TgCrypto-1.2.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1283337ae75b02406dd700377b8b783e70033b548492517df6e6c4156b0ed69c", size = 43347, upload-time = "2022-11-11T19:51:43.662Z" }, + { url = "https://files.pythonhosted.org/packages/39/0e/e861a9987c6d9d3ae04d156732311e018558f862490c466c369d73094807/TgCrypto-1.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1735437df0023a40e5fdd95e6b09ce806ec8f2cd2f8879023818840dfae60cab", size = 43123, upload-time = "2022-11-11T19:51:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ce/d60e249ac4d31d591f4c8ca76e7bd537f4c54f1947b400a14ac3a1678b09/TgCrypto-1.2.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfa17a20206532c6d2442c9d7a7f6434120bd75896ad9a3e9b9277477afa084f", size = 60713, upload-time = "2022-11-11T19:51:46.411Z" }, + { url = "https://files.pythonhosted.org/packages/27/55/50f81b1a557bd8f9b73f7fcda2381f8826665858750968394ceec0b01282/TgCrypto-1.2.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48da3674474839e5619e7430ff1f98aed9f55369f3cfaef7f65511852869572e", size = 60418, upload-time = "2022-11-11T19:51:47.872Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/7f5f5a024b71ef81b07c3fe15e79d03b634eb349b04bc79a62c8c3ed3376/TgCrypto-1.2.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b49e982e5b156be821a5235bd9102c00dc506a58607e2c8bd50ac872724a951f", size = 64397, upload-time = "2022-11-11T19:51:49.609Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/8f059edce82de8ebbc1e513076c0f65a86fac95887fae21ae087440ef23a/TgCrypto-1.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9d9f13586065a6d86d05c16409054033a84be208acee29b49f6f194e27b08642", size = 64436, upload-time = "2022-11-11T19:51:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/eb/87/fe894eb7c4cce4b6f710223c24e0ca22f4107fe46f3f56091ceabd482de7/TgCrypto-1.2.5-cp311-cp311-win32.whl", hash = "sha256:10dd3870aecb1a783c6eafd3b164b2149dbc93a9ee13feb7e6f5c58f87c24cd0", size = 44723, upload-time = "2022-11-11T19:51:52.982Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/b918235b19b70a45f4a596c773fad2c379d941d2ac5005293e2ee18bdc63/TgCrypto-1.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:a1beec47d6af8b509af7cf266e30f7703208076076594714005b42d2c25225b3", size = 45073, upload-time = "2022-11-11T19:51:54.724Z" }, +] + +[[package]] +name = "towncrier" +version = "26.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d4/e85ce614bf45b1a7fe13b82cc687819c2eaac5a1dd94b84834322095fd98/towncrier-26.9.0.tar.gz", hash = "sha256:ace9031631c718cc0709107b8755e6e31350eb8acdc877af43df3177addb9a55", size = 79338, upload-time = "2026-09-04T12:57:25.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/88/f9340b545dafa64de053798e760515a2736fbb60092cf9aa6b146ccf3ede/towncrier-26.9.0-py3-none-any.whl", hash = "sha256:ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660", size = 67030, upload-time = "2026-09-04T12:57:23.986Z" }, +] + +[[package]] +name = "twine" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz", hash = "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", size = 215032, upload-time = "2026-07-27T15:59:00.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size = 43204, upload-time = "2026-07-27T15:58:59.26Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, +] + +[[package]] +name = "uvloop" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/42/02c739ce85fb2ee8d99212c61417da8140c6b87e9d97c430bea520d76044/uvloop-0.23.0.tar.gz", hash = "sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27", size = 2559185, upload-time = "2026-10-01T03:17:04.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b1/948067eab45d5307f04b34e50eb7bd1f7352aee866fa5f0706b061ddacf0/uvloop-0.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:24c58ae4a83e93a04c504bcc678125e36a0bfc44af928ad69444880c60f187a5", size = 1415276, upload-time = "2026-10-01T03:15:32.634Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6f/ee3ee84c5d27f2f0a47ae8b67a6adeacf9841b193c0e07412a1403586ce2/uvloop-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0efdd55bddbd36bb2fcb842d64c0d5f6407c6958c68088cc25df8c09edc5b5fd", size = 779533, upload-time = "2026-10-01T03:15:34.062Z" }, + { url = "https://files.pythonhosted.org/packages/25/0d/b5f69dae3736d96a8753c6ecd32d676ecd212be7ba3252e9c379ad9cc05c/uvloop-0.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fcd721113260ffb5e38bf14a8725b17d431f34209f7d1c7005b667946e630b3", size = 3896377, upload-time = "2026-10-01T03:15:35.816Z" }, + { url = "https://files.pythonhosted.org/packages/16/fd/8cbf6124607863399008ae4b0d2bb50c22ed83526deec28dca08d635eb6d/uvloop-0.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab17b3a8aa754be0de0e397f7b95f13b14e56f077a4c6ae295e3d4afd199b325", size = 3956355, upload-time = "2026-10-01T03:15:37.688Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7a/b73007866e7198519067a1f1afc343b4973ae924d2b7afcea67c44320a98/uvloop-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:80cac5cb90ed7b9b72a217a1d6982b15b829cdbd0ee6bc19b93e3a9e47fb0ac9", size = 3755618, upload-time = "2026-10-01T03:15:39.27Z" }, + { url = "https://files.pythonhosted.org/packages/3c/28/e50816f1ce38b97b28d62bc4adf7c82c33b7c68fa902e41a39adc8a3d189/uvloop-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93087a845cdfb35753e539354ac9551bdd2ff528c202a98df0ae46e852bcf021", size = 3863192, upload-time = "2026-10-01T03:15:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/05/98/04e766a6de99e6f7f955ecb7829e8d5a557de3427cb85be2236de54dda0c/uvloop-0.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:93935ab27b6eaef4c3e5489aebc84284f0644592f7ab516df60ee1b27eaf5eb3", size = 1393055, upload-time = "2026-10-01T03:15:42.526Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/499e7b863a848ede009539bce39806b66205da5f8779354228e785601144/uvloop-0.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4448e9124537620f9c25d004c227bb5104440b58955c19bbd312d910af919a63", size = 768909, upload-time = "2026-10-01T03:15:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/3d/95/a880f8ce3b87ac5b307c354e8ee480be4658d24bf01f87921d57e3530b4a/uvloop-0.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7548ede3ee908cfabc0d068106e303a9a2d811af959cdf6ab85676344cedcda", size = 4419106, upload-time = "2026-10-01T03:15:45.551Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/c1d2f9fa977f8f42ea294604166df10e0027e6dc6cd17f85ede386c9bf36/uvloop-0.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:090865d8ce7a03986755a3ce711b7dd0d4b44eb14ab74368b717f3fad1180208", size = 4532597, upload-time = "2026-10-01T03:15:47.258Z" }, + { url = "https://files.pythonhosted.org/packages/42/dd/2cb6a2c8a30ca55c07a882dd4ae4ceae0fa7d8c15b25b3b7cb9a4b6cf4ca/uvloop-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd6f2f81c7b9da99d301c0b16b82044e76fe887086e42e1590ecf520b94dbdac", size = 4230048, upload-time = "2026-10-01T03:15:49.119Z" }, + { url = "https://files.pythonhosted.org/packages/f4/52/29989cbaa4022dc4ef35c1dd60a4ab989e4c2065f341ed483ae71d2bd950/uvloop-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a6ac96da66c35bf789bdcde78a88dc7d56b7907d8379648c54adc1c61594575d", size = 4394152, upload-time = "2026-10-01T03:15:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/5f/83/eb980d64e6dd5da46d4dc35755fa6afd6b5b47141437cf89615f1117c5a6/uvloop-0.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65", size = 1412726, upload-time = "2026-10-01T03:15:52.49Z" }, + { url = "https://files.pythonhosted.org/packages/04/c1/02a725e7698134c647904bdee6589e2be14a0e7fc9942c74f86e2b90d48b/uvloop-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb", size = 779071, upload-time = "2026-10-01T03:15:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1d/cde53c79e8c01884ad1cdca8e407e086d523362cfe4139e2c2a8dde27304/uvloop-0.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5", size = 4395323, upload-time = "2026-10-01T03:15:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/98/54/b12915bebbf99d7ae0796211e7f5977b95f069830dca45dc1a346d84125d/uvloop-0.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb", size = 4480449, upload-time = "2026-10-01T03:15:57.362Z" }, + { url = "https://files.pythonhosted.org/packages/f7/8e/da6de68c31549a052a105fc76f5a9a204f6df22cb0909440aa4dbb06f9a2/uvloop-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848", size = 4219177, upload-time = "2026-10-01T03:15:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/1b53c6a89dc9c9d5cb75eb9a0b891ad69b32e1421ad3aa01617a9cbdcc78/uvloop-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f", size = 4346132, upload-time = "2026-10-01T03:16:01.064Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a4/00e85345871c59c834a23c136c1771205856028ecc8ba940b3951178e59b/uvloop-0.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd", size = 1421363, upload-time = "2026-10-01T03:16:02.599Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a9/e5f0f3cfde30af3ec32eba8ec07bccdba2b5116afbd1ecc53edfeb0a0790/uvloop-0.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476", size = 785177, upload-time = "2026-10-01T03:16:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/79/9ddf78f8cd75a15c14a09a57f59c587b8cd9d82802c5c8368b9c3ebefa0b/uvloop-0.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e", size = 4381060, upload-time = "2026-10-01T03:16:05.642Z" }, + { url = "https://files.pythonhosted.org/packages/1e/20/57d63c44d32326878fcad5c63854afc9deb394ed95673c1b1a429178c79d/uvloop-0.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330", size = 4418891, upload-time = "2026-10-01T03:16:07.326Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/0795abecda2cc3dfe41033f880a32a9ff103be4e6b177ac736833c153a0e/uvloop-0.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f", size = 4214811, upload-time = "2026-10-01T03:16:09.13Z" }, + { url = "https://files.pythonhosted.org/packages/20/18/9010dacd5221eec1bd79a4a83ac68f3db6a42d7bb657f7b640c4838ca6b6/uvloop-0.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410", size = 4294876, upload-time = "2026-10-01T03:16:10.875Z" }, + { url = "https://files.pythonhosted.org/packages/b1/08/f6384a03c771d00067cba4f542a69b2fc1a982e9fd78b357c2f788678d72/uvloop-0.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208", size = 1494811, upload-time = "2026-10-01T03:16:12.399Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/756a4fb24a449f313cf4a153eb0c6210b49cfe5539255ec9fb1e17d2c4ef/uvloop-0.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d", size = 819396, upload-time = "2026-10-01T03:16:14.094Z" }, + { url = "https://files.pythonhosted.org/packages/3e/45/e314b0c600b14f53dad3a3c2d7a922a249a88225fd727652b53e1854b9dd/uvloop-0.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f", size = 4734966, upload-time = "2026-10-01T03:16:15.815Z" }, + { url = "https://files.pythonhosted.org/packages/66/0d/8686a7f0b1b2d55ebd770ba21f8e0e4ffa0cde5ab738f43ffb8264499052/uvloop-0.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49", size = 4584963, upload-time = "2026-10-01T03:16:18.198Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/034a2d47e435ac02357c42956246887167bdc0357bdd6ad31c5f6d94497b/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507", size = 4421388, upload-time = "2026-10-01T03:16:19.953Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/131f4b583e6b4b715c404a66b51c812d701db20f25c9018b188a2b00062c/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405", size = 4402414, upload-time = "2026-10-01T03:16:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/58/3d/ee11f4718ea1280595c67ed25c83d4c92115dc100bbdfd192d3ed9339168/uvloop-0.23.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d", size = 1418095, upload-time = "2026-10-01T03:16:23.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0c/7ca516a0671418517d79a09d3ff2ccbb44af94c75711afa6e4cf58aa6f65/uvloop-0.23.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5", size = 784837, upload-time = "2026-10-01T03:16:24.666Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/75d4e28e596d505b7ae11de517646b4ca3d369fb8537ba755410380da11a/uvloop-0.23.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2", size = 4380276, upload-time = "2026-10-01T03:16:26.389Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/68daf827ad62efaf4667d1f3fda127046d42161178396bdd93aab3684082/uvloop-0.23.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53", size = 4451496, upload-time = "2026-10-01T03:16:28.364Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/f67e696ee688f426a96f99099bae26fec14a1d0fa75dccdd6518ee267c0c/uvloop-0.23.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a", size = 4212541, upload-time = "2026-10-01T03:16:30.014Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6a/c8c436a9d7453297b4be70bdf6a9f9fc9400da45e0059ddf7b28ab63f4c7/uvloop-0.23.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027", size = 4319377, upload-time = "2026-10-01T03:16:31.705Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2c/8fc15a03489299aab8a6212dfe0f137dc39836f915c87f7fd9d9ddd814de/uvloop-0.23.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4", size = 1493428, upload-time = "2026-10-01T03:16:33.859Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7c/05e4a210790229607f71460fcb2ed4a2c7bc72668d8a928ce577c22e38f8/uvloop-0.23.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254", size = 818115, upload-time = "2026-10-01T03:16:35.45Z" }, + { url = "https://files.pythonhosted.org/packages/65/14/a40b11c6c024213803b13955664a15754c72f64c873a33d986b26ec9ff5b/uvloop-0.23.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8", size = 4734149, upload-time = "2026-10-01T03:16:37.025Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/f421a077712c1e87603bfec62744c3cd3a2f4b47378025db3d740df9af0d/uvloop-0.23.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc", size = 4661763, upload-time = "2026-10-01T03:16:38.719Z" }, + { url = "https://files.pythonhosted.org/packages/f5/62/25dcaa6b7e7b48f82ce633854ce96597ab768f9650931f4f86c572de392c/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55", size = 4421324, upload-time = "2026-10-01T03:16:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/05/46/04628239b43dcef703af314202a3307d6060918e2d76aa86c5b1188f5551/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f", size = 4462501, upload-time = "2026-10-01T03:16:42.359Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/24/20282bc8e69ff8dee680dc2b95e9dc81fa7e7894b14c1a8d06330e97fef7/virtualenv-21.15.1.tar.gz", hash = "sha256:e79c90b8f601701599c25905e02839924fede2c798d52325670d24fbdaab74ee", size = 5517048, upload-time = "2026-10-28T01:12:50.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/23/cdab19d3a9da42ca0938081e163e25f81e610141800ead743ba1ee881f0e/virtualenv-21.15.1-py3-none-any.whl", hash = "sha256:0d14a991689c67600316880eadc0a37b7690cb632e3b4ac67e2bc05b2b2e24a4", size = 5558848, upload-time = "2026-10-28T01:12:48.605Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/68/e6aa0b77d217b31f8f486ec0cdfe5e00e6e38dc0be657e7d85819b9faf0a/watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207", size = 108119, upload-time = "2026-09-21T09:08:55.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fa/c0b840d5d8bafe640925408ce11095948c8945bb61be1e67d0ab629b872a/watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441", size = 399703, upload-time = "2026-09-21T09:08:13.518Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/894799b485fe9473ad10422a0d9668e53fb78e3a2b5cc6061159572844ad/watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c", size = 397647, upload-time = "2026-09-21T09:08:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/5a/61/f277cd5ead05f5d37b98b41fbdd0c943ee0c04d49738f62e902cb291cf43/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5", size = 454994, upload-time = "2026-09-21T09:08:16.587Z" }, + { url = "https://files.pythonhosted.org/packages/46/66/ea4c01382975e53b1ac1b0a0ea1b353416b8127fb6e04f5c1c313119aab6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca", size = 460105, upload-time = "2026-09-21T09:08:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/1a501b597817096c9831c255f6270c124ce0e2faeb74124be24dcc93b86d/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30", size = 493761, upload-time = "2026-09-21T09:08:19.327Z" }, + { url = "https://files.pythonhosted.org/packages/5a/16/77352d05e152c7786a016e9f1decdb4507e8aa5bcf3343c72142a0413d14/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95", size = 577500, upload-time = "2026-09-21T09:08:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/81001c55b24466fe9911135e50269fd3a29f43e446627a92adfab54553b6/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29", size = 469209, upload-time = "2026-09-21T09:08:22.346Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cf/e3ad894ed5909a7c44b0707e89bc25d19a3d78b3a7aec846119443b6ae56/watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1", size = 458353, upload-time = "2026-09-21T09:08:23.7Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/2f106829e6e2bc91ad74614730f6df484202b9a4d20d927dbf2f08d37d44/watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935", size = 468707, upload-time = "2026-09-21T09:08:25.167Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/2741ab776b978669c1f3bba1e573f1a184f727ab8b4d3a6d9c61090800ed/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3", size = 632510, upload-time = "2026-09-21T09:08:26.532Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/db4a4684275fe9b696ee5118c194705c1423b3d42181c3f8ee70194d5c73/watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509", size = 661723, upload-time = "2026-09-21T09:08:28.004Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/95ddb24f6a2f595c031c24432ab7b8aa7d6d1ada0c6d63a552166e06e8d1/watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661", size = 277761, upload-time = "2026-09-21T09:08:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/48/a1/1c4a1b3c8030cd03206232612be8a6e8799093490c220b0fcb935d02b75a/watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61", size = 290643, upload-time = "2026-09-21T09:08:30.45Z" }, + { url = "https://files.pythonhosted.org/packages/82/e0/d2db577c632d26d08078c4390dd870b8555c4e558df25d22270f0166c71f/watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc", size = 285510, upload-time = "2026-09-21T09:08:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/a122743607939429ab6a7272672f49303aa02bbd34c3f06676765a3861de/watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6", size = 395978, upload-time = "2026-09-21T09:08:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9d/0ee1b0ea42acfe72afa2484f0222e2386dd09992e9034a81e63cc3cdab0a/watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80", size = 387997, upload-time = "2026-09-21T09:08:33.953Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/4319c6b683e8bd28465dfc53c750617a2a07f072eb2e3ab0182ccf812229/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef", size = 448067, upload-time = "2026-09-21T09:08:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/b7/71/9f37ac192a51a24de3a2d7fe9aac1e99b2b2668300674ff7a9f85d95beb1/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a", size = 452881, upload-time = "2026-09-21T09:08:36.238Z" }, + { url = "https://files.pythonhosted.org/packages/34/07/1224667c32a4a96876ddd642da69733cbfe6a15897db0865aec5a882cebc/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26", size = 484802, upload-time = "2026-09-21T09:08:37.717Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4d/7f29cd87e1f826df86fc0418d0b8f370ca0311c29c618406dc492aad9951/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3", size = 567522, upload-time = "2026-09-21T09:08:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ac/3aa1f84abd4dbbfbae99a817dd931f77b439261c911189cad4c30fdfd470/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977", size = 461997, upload-time = "2026-09-21T09:08:40.142Z" }, + { url = "https://files.pythonhosted.org/packages/82/1c/27c66764d5b0dbb70a36770a32eca217eba739c28b24e8583a035087a3d7/watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7", size = 451856, upload-time = "2026-09-21T09:08:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/a45827084d953c1d0dd3d75abf85dcac26a0f5d115ad2fe0b7e9e931573b/watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4", size = 459889, upload-time = "2026-09-21T09:08:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/07740d23ce2bb23419bb24055c435a9abe5fe03efdaea1ba0720b831aab6/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a", size = 624482, upload-time = "2026-09-21T09:08:44.064Z" }, + { url = "https://files.pythonhosted.org/packages/91/46/9540f8123eaec86df989f26d7704cad0448d9fb7271763a4f8f9fe23617d/watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694", size = 653724, upload-time = "2026-09-21T09:08:45.257Z" }, + { url = "https://files.pythonhosted.org/packages/25/9d/b9deb52f1e4528ffe44338c5ebab95c32ed79203082da661de50288e130a/watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e", size = 270863, upload-time = "2026-09-21T09:08:46.518Z" }, + { url = "https://files.pythonhosted.org/packages/59/59/8b1881afccbafda06248be4185e28034dd73c80c85e4da24f2cb0c1321eb/watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39", size = 286879, upload-time = "2026-09-21T09:08:47.874Z" }, + { url = "https://files.pythonhosted.org/packages/46/dd/41c12ae9745d5d72f709333287679d71cbba68ad7678fed872207bba94f4/watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0", size = 276924, upload-time = "2026-09-21T09:08:48.976Z" }, + { url = "https://files.pythonhosted.org/packages/cd/eb/289fec71eba51d6c3ef3a28c81f7213f4e860084ffae5abc866933c564f0/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1", size = 396932, upload-time = "2026-09-21T09:08:50.217Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/a3c3f557992a20a5bb24607f0da44d39cf6d6ba3f8f6ed7cb31f6c73371a/watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c", size = 394852, upload-time = "2026-09-21T09:08:51.412Z" }, + { url = "https://files.pythonhosted.org/packages/55/65/d45763a5e1108a65af85a217af2930ebf1081c73fd8169d3f75e586992bb/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef", size = 451789, upload-time = "2026-09-21T09:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/02/96/165c6e540bf765ca1115062017adf861d0816525f02464ba71313fe7cf92/watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f", size = 456919, upload-time = "2026-09-21T09:08:54.372Z" }, +] + +[[package]] +name = "websockets" +version = "17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/4692be6d5bf739ed049b3813901c42091f8c054df89d5c089343823dea0d/websockets-17.3.tar.gz", hash = "sha256:1c7dd93a1c9f89297471493674b1fc1d9467cafd45374f2172213046bdb3a329", size = 188878, upload-time = "2026-10-19T16:58:05.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b8/282f114a67886fccde02a77ff296501ad7fea25f4d696b2b1b337cbd27da/websockets-17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45cc1ccb12ced6d242335159e8221733c8696da5ad0b6ceef6debb0f5ce89110", size = 218223, upload-time = "2026-10-19T16:54:25.998Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ba/8043c303aa60af053e092c1d667165f46d6729217ce2af795a23a3cd700a/websockets-17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46ae05aa1aecd47d4d7de0f05ce2d1d2eb418f8de614964d754cc3b9a409c400", size = 215908, upload-time = "2026-10-19T16:54:27.785Z" }, + { url = "https://files.pythonhosted.org/packages/21/3d/5d0eaa9b29db6c999a2cf749605141796cff21a5e235b2488bd39dcdcac9/websockets-17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43f270a5f21e12db08c73d83849a877c02ab7b7d0fb6e2718b5101d9c810212f", size = 216181, upload-time = "2026-10-19T16:54:29.267Z" }, + { url = "https://files.pythonhosted.org/packages/43/fb/33080a7b2d8427b343056eab32ba78235a4bfbbdc79b5593688c7b7c62ef/websockets-17.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d875224bac1a05a20acade268a7a0e3498e7f8cf890acb7c2b23bacd2bd3659", size = 225246, upload-time = "2026-10-19T16:54:30.807Z" }, + { url = "https://files.pythonhosted.org/packages/61/8c/d4220455cee00adb553c602ba4f1f6a47cd882d86dd9e5124679c0c95c75/websockets-17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ad409b7ea102339041281117cf2dadfe71edbb653dc48037cc55d516fb7f448", size = 225517, upload-time = "2026-10-19T16:54:32.182Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/0ff8e4f0195884ae446844185e81bf7619673d816424b8a58bb5812dd4b8/websockets-17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d43c311f3519fb1057b6114dc24e9060bc3fb6aedf4aa11160484154b66b63", size = 226751, upload-time = "2026-10-19T16:54:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/06/cf/82c88e3fbb6a75a62b2997932664bccfe68256661c69144bd48020160cf1/websockets-17.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9eacb382f400bb5865e7e6d24bbe709a84510291e707f169dd4a24ee4a5ac6a", size = 230063, upload-time = "2026-10-19T16:54:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e4/9131d3b73504902a83d10b1e1244319377ca72755351998ac9c408ba6ddb/websockets-17.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f09f145b89ea292cf6c25fd7c8c0d90207fd6a9e54c7828499669996a1e5bdcc", size = 227310, upload-time = "2026-10-19T16:54:36.248Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b2/2208506afb45d181b0d6ccf377d9d79985fbcbf871af02ef4f7f3d405f5b/websockets-17.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:49fb5b4960ebe1bc557b435da8687deef1302a08f030a33c5606fa6fb11b773b", size = 226087, upload-time = "2026-10-19T16:54:37.583Z" }, + { url = "https://files.pythonhosted.org/packages/39/53/da73e3809f1d4e8c3346ca89a4bed7995de5708f2cb14c9359172a85f5a5/websockets-17.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:13cffaedde6e06183860fc92d88fe1d190abda73837fe4c14f5d4803c472f5dd", size = 223360, upload-time = "2026-10-19T16:54:38.91Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7b/eaf828fd414401672e615570b9f2bfd84342b49c86ffb2ae735e4aada32f/websockets-17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ef2950896673a9809e8e5ab857525924fbcacfc98b858b0509ccf50d341f80f4", size = 226240, upload-time = "2026-10-19T16:54:40.294Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/4b6aa6e761c8d89aa6defdd83e40a6b2d9aa21bd01520c8e8470fe8d8681/websockets-17.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2d08ca4bf5a9cdb6fd81b86b51bd5cfb10527d3a1199888f2b02cc6c6a3d5f80", size = 224833, upload-time = "2026-10-19T16:54:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/37/a8/0a6aea08cad7db2c1aa53dbd3ece3ebc09e420e8301cb02f2b812a728a5b/websockets-17.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5e0c0ed37472c4729f3001066510a656486f08f149dda1ebfd3dfe4450c9756e", size = 225599, upload-time = "2026-10-19T16:54:43.54Z" }, + { url = "https://files.pythonhosted.org/packages/1d/82/3998ea56beb079f43fc39dd5935ef8dc150f915bf2de8bd6ddcd50c0fae6/websockets-17.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62059d739af7fc035ed6e63c5390118a60a5e9aa5a5541cd0486a83e741842c7", size = 226643, upload-time = "2026-10-19T16:54:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a5/6568add8d0a2f37c5780c6de62b5b8ee191ca9d2d0b8ff2123b8277c41b7/websockets-17.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc6f87d03e3691154673aaa8f08991723d6e3394c83ef2875c7a9049eac9595c", size = 224223, upload-time = "2026-10-19T16:54:46.429Z" }, + { url = "https://files.pythonhosted.org/packages/72/79/b11bc05b0f64d8b14901443a4b01ea80d039cc68b260250b4cb488070aca/websockets-17.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3d78d93846eddc990551c1c31914d90d71957ca3af468969826f7ed0d7d1d51c", size = 225255, upload-time = "2026-10-19T16:54:47.82Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/9a8902074db7044b64a05feff349e4f44144a961310d3419bab612f00dd1/websockets-17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2a377c7132b1e69be7a50d7434d7f26f69e93c70166ebb6d8f8cabf802ac4506", size = 225494, upload-time = "2026-10-19T16:54:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/99/ba/7b873060ce6764207a24c10e81294739abffe2a90dc67c7f7db5042c596c/websockets-17.3-cp311-cp311-win32.whl", hash = "sha256:29b01520c39ef2f23c88b002f11e6ef4165675f6c92f5f3578fd587d69b5a5fc", size = 218720, upload-time = "2026-10-19T16:54:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/28/11/7469f3f28513ec0d43f10b1c88bf970b0b6f0aa8190c42731954297e824c/websockets-17.3-cp311-cp311-win_amd64.whl", hash = "sha256:d338813a440bf6d2f0df55140d3f3080284410b1ebfe9df50e3847bcdb1a17bb", size = 219017, upload-time = "2026-10-19T16:54:51.843Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/4724610baabbba94a44c5d1cf1b1a33ff77031e94a51addb0387c7f56b10/websockets-17.3-cp311-cp311-win_arm64.whl", hash = "sha256:6a8248b1ba8861e48e4c7ced3895a1264d2a3b617de72ec94172ce6e64bf16c7", size = 218950, upload-time = "2026-10-19T16:54:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/29/902ffe19c0d35603102f31490dea342f083bc77d8f8bcdc7d8b16f5810f9/websockets-17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:41665c7ea01d3ae497a0c61fd22d3424230d18782a5fceb79f04db9ccb2042c9", size = 218250, upload-time = "2026-10-19T16:54:54.718Z" }, + { url = "https://files.pythonhosted.org/packages/f0/66/f56a2637b1aac263205b0e3cd7b2ff2e2f8b404b8118c991f4d122fd381f/websockets-17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8f7469408740cf06a79b01605f2ae0b2acc8839c03fcbbf05da49baee12d3302", size = 215927, upload-time = "2026-10-19T16:54:56.124Z" }, + { url = "https://files.pythonhosted.org/packages/22/7d/a27f25451c4cc62141992b51005688481210f3d3e63de3dc66f9e882ba59/websockets-17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d68da281ac208b6236ffa8beef23fe7c7c956d8bf09b421d447232fd94441b35", size = 216196, upload-time = "2026-10-19T16:54:57.538Z" }, + { url = "https://files.pythonhosted.org/packages/9f/77/40bbc1a5c016cf9f598dea21d1250a93245c995eafaec6401d0e4eb4e647/websockets-17.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078d1d41f45ee34eed5a4f3bbe7c588caecbdd6e12dfca73e8c1b0e803773793", size = 225516, upload-time = "2026-10-19T16:54:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9c/91889c8c62fb7d16cd58e3074473a259303d6229bf0ce496aec87e66f3dc/websockets-17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7f391838e8adcf1f2fe71561d9e5a9f74ea7ffa5052eb2db329aac677383136", size = 225795, upload-time = "2026-10-19T16:55:00.438Z" }, + { url = "https://files.pythonhosted.org/packages/b8/29/2e5e2158e9920f806cd45f3cbc81cf5f3eef2b9e5a74b9ef049e2a3578e3/websockets-17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15a165673bb3fb57facbf040edf5c0556d20ccd2e0ccc5f23ebb69fb715fdc1", size = 227034, upload-time = "2026-10-19T16:55:02.065Z" }, + { url = "https://files.pythonhosted.org/packages/b6/af/fbab5c4098b26026b877bfc139b1ea0d7c6eaf7a86b84544933b4c445f82/websockets-17.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28d1935c16782a1e3650c9baa79069b016fff739e292e4cb14bbb830d6930491", size = 229863, upload-time = "2026-10-19T16:55:03.415Z" }, + { url = "https://files.pythonhosted.org/packages/ac/72/d6e460a991607794072fe3fdfcb3221a0342af61586f5bd7ea40bddcfd21/websockets-17.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c534c9524a2acd0b97b7022074bec3784ac273c683cf9535087e437672ea8531", size = 227669, upload-time = "2026-10-19T16:55:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/ec/8f/1c7da1cfb632e141e019cf2e0bbbd4e6dc8c198c8cc29544e4acf1a89f56/websockets-17.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b459d29c8b82a420a98fa0ce8e9cf787ce0062d98cc90604ef9574fb3cc26ff8", size = 226429, upload-time = "2026-10-19T16:55:06.112Z" }, + { url = "https://files.pythonhosted.org/packages/3e/63/c48ef8bbfbd982f2cfae1cfcf9e63f5bfbb8468fd37ca700507efc0b0007/websockets-17.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53209dffd91b0aefed9ea630fa6ad5c2944d6210ba3a8a3a8664dc1575289b14", size = 223653, upload-time = "2026-10-19T16:55:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/cb/37/7bedaad48ae8e89eaa4f3942c11be0c41de77ce2118a475f3ba05e9b5a0f/websockets-17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9662a9fe457820c60365b3209e03cbd8e4a817195d55de65a80fa680b4bea136", size = 226465, upload-time = "2026-10-19T16:55:08.791Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/ab9a8a0de4769ca1dce392021b6a922e21b4d3687fe9e1ab4db585a088f8/websockets-17.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5670a762c7ae388bf8aa04199fd436180a4d28318dca9c1eae7b38604676dbfd", size = 225071, upload-time = "2026-10-19T16:55:10.127Z" }, + { url = "https://files.pythonhosted.org/packages/89/96/64d8460cec95f873c61c6d13a09b435915150640525ae56d5871400aec09/websockets-17.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ed59cb4ee258674a6ed9b8515e42079db02cb3be94f133fdd3f200183a40c49d", size = 225841, upload-time = "2026-10-19T16:55:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/a6/98/2b83211ee3087db4ae66533782664c0e283cf4a08cbb14036018d0803e09/websockets-17.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f78bb2e82fe88912c9d7b25c98e57a9b08655579af89f164fd4c0657a075d62", size = 226945, upload-time = "2026-10-19T16:55:12.85Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fe/6b2a37d422d7f4dfc0c92c95a14649d9bd662243ed8b0774523ed2703604/websockets-17.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:71250e010eede33c1d638e2c7a98288520d67f782fd5eceecaeca3129233a09a", size = 224502, upload-time = "2026-10-19T16:55:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c5/364015c9311c89ea6085d54c70d654c5d27ba05014d44ef9fd9daa09462b/websockets-17.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:729989759feb81ff6493879a0d60a0bd7d2eee41001bbec0381a080318439382", size = 225483, upload-time = "2026-10-19T16:55:15.899Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b3/75d0cae8398816ed34a16a78252bfc8651f2f883cc5a568e1198395f15df/websockets-17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90d8c4fa5ba293cf92de351fbc292cdaadb1a1d58d031a1a97ff9b8fb6250ec7", size = 225761, upload-time = "2026-10-19T16:55:17.28Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/9a7c4b2e87f47a2a6a066acdc2157e9b7cb6222a224b95a432f54ae786ce/websockets-17.3-cp312-cp312-win32.whl", hash = "sha256:0824e6ecee838cc28e6481062574c7fde49f9a2cedfacd36f0e91cb4b37fd6fe", size = 218722, upload-time = "2026-10-19T16:55:18.658Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e8/e52953a8280a1e51cb9307218443f68c8f4dc4b9952db6021d5c12d03c0f/websockets-17.3-cp312-cp312-win_amd64.whl", hash = "sha256:6644eecd99c4e8cdf72e646e17b391bb70b054f44becde37dcdc48cbec7e1527", size = 219024, upload-time = "2026-10-19T16:55:19.987Z" }, + { url = "https://files.pythonhosted.org/packages/85/a4/005eb7c1f5c1a249c1231a6ed83a54ada0b6b8ffab33a608ca20e3034ea0/websockets-17.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a9133dbaedc6f1af358127275676171d9a1bb3e61f47ee62b2ff9acb78af3d0", size = 218950, upload-time = "2026-10-19T16:55:22.161Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1d/c1f4a1c45c5ca2c35ae9fd7cac0e491f8a014c9a86933a5da7b04ff825cf/websockets-17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:52a2ca6f401fb49a8d5d9e0e48ade5f58f1bdda33132907b0e4e3b702628fa4e", size = 218243, upload-time = "2026-10-19T16:55:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e8/f1b47d0ced4964ae748931a54347fd27b06b62e8ea7837a9de3aee0c5999/websockets-17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80578e91764bfca297ddca322c00a3136d993b829e9839e7fa378a19c1fcc25d", size = 215927, upload-time = "2026-10-19T16:55:25.317Z" }, + { url = "https://files.pythonhosted.org/packages/16/df/ab0ed0445d8d81a133c8fed760f3b4f743a2f563804fdbecb2c32fe05d94/websockets-17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe7a168d805c8a7bf1c2de55b7ab2bc958586bfd88d0e87235a92cac66b590a7", size = 216185, upload-time = "2026-10-19T16:55:26.678Z" }, + { url = "https://files.pythonhosted.org/packages/9f/51/b30fd6c5a33dac76a78083be125e3f9804f3d735da494ff03a46834f31f4/websockets-17.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:54fcd24944e6d127d198c28dc363967b7131ff0c97833962c2ef2048018de02d", size = 225574, upload-time = "2026-10-19T16:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/1c/8a/8ede659919c0805d1fcc79c2ed16588d3419f1687c5117ba46f0217750ec/websockets-17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de2c12555bf05546f3ee75733f2c81de70aab7f530e86d0d2162b0ddcb3c39ae", size = 225854, upload-time = "2026-10-19T16:55:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/e75d10e3896edc61320062890f71cdc0767dc4a54e2a26a943b703042d13/websockets-17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c95d25b5382ee0e6191ecafdeabefdc1f7bff8dfee29239b9b79e760ef3b04bd", size = 227091, upload-time = "2026-10-19T16:55:31.146Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d3/658f1e153566e5edae02812122d9f020c30785d8b53c4bda61b8e976f77f/websockets-17.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8f244dfee05ea29b8064701880865eb1684f3a704a5665bd10b64694444dbbd4", size = 228526, upload-time = "2026-10-19T16:55:32.524Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/002f95a02d721d8c6ec2676c5aee904a823838a9c205fc0b31eedd882a7b/websockets-17.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e99440ef446c7833690c1754e23b0188d440f3cb89812b0764c697a7880cb33", size = 227718, upload-time = "2026-10-19T16:55:34.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9c/d36ec9499a62ec4e2a4eb570f93c6fcedd80ad5685e0f9d6866e8cfbbb84/websockets-17.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:26838e2d95d7004c52e8c7b6b51676c872093a3b55a497824f899116a15a8a55", size = 226472, upload-time = "2026-10-19T16:55:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/36/5d/3a6d74e409f24ae4e8661a9d74d6645f71b3e91425a527c0e3f2e78bb15b/websockets-17.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:508775efa11d26aec186d10154359f52276e2baf9ca201b52356330883276dfd", size = 223710, upload-time = "2026-10-19T16:55:36.853Z" }, + { url = "https://files.pythonhosted.org/packages/e1/90/e08eb5d69b73cf5babf80922a8c964019b48339ce5c694a5d0c0f083c5a8/websockets-17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2789d203babb6ceb2770aadc2394c1f99263ec37fba57a93064f9a8ad899e8ff", size = 226532, upload-time = "2026-10-19T16:55:38.191Z" }, + { url = "https://files.pythonhosted.org/packages/c2/45/b1916efa1dad5af33beafe5c536810e1c80856b7e913f671c35031c57745/websockets-17.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:cfef7f7aa434a1e632e0ebda14b39f4f1d7975e0fce5c77ea7b43156fabf9bd8", size = 225129, upload-time = "2026-10-19T16:55:39.635Z" }, + { url = "https://files.pythonhosted.org/packages/04/c1/d427abb3f11e92f102f70f8e73addb767fc62c614565c0ee3f5657fa3551/websockets-17.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:39f532fa0d9026de4b9315a175998646112776baad54d55348435c11b05d034e", size = 225900, upload-time = "2026-10-19T16:55:41.033Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/462fab8e4b09eaa4eac2f40fcff8ee30f851aa40fb518efb0177262f6590/websockets-17.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:85d6daa3a7ca404570ae130babbeb347ef9d3faca5300fb13cfa4bb48c3d30d1", size = 227007, upload-time = "2026-10-19T16:55:42.314Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/7404a4376021a0cc7ae9fcbdb1196c0c8aa48df4fd370c185f4f5b677bff/websockets-17.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ce39283ef08b0f28478e920d4d89a1fc169176e568e8a6a821332673145522eb", size = 224565, upload-time = "2026-10-19T16:55:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5f/b8ada5787d11e6edbf58e2d680235fb3b2fb3298991cc310566f4d6c2fbf/websockets-17.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23046098ebf844753cfd211033624a8247520e5bf2194c24cb1a8c222917acff", size = 225516, upload-time = "2026-10-19T16:55:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/23/1c/0533f760237ac6604efd0bf98c18ea4c1381f2cadd8c8d0b05faecb07a80/websockets-17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:724224d4c457207271239114bf43afafc3eca4a1b4661ccdd428d11869a03bbd", size = 225797, upload-time = "2026-10-19T16:55:46.989Z" }, + { url = "https://files.pythonhosted.org/packages/97/b4/0e580e8f96197130ebdb32b727e9c891594c930be30d8a9de9aa0bbb7e2c/websockets-17.3-cp313-cp313-win32.whl", hash = "sha256:5af7070d80d278832cbca9c3c3dd44b30a26dcfa3ae1e0f6f19ff34edc27b689", size = 218712, upload-time = "2026-10-19T16:55:48.379Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6c/db5c907d1cbea84444bc759ff63db97fc586988d11a34d5565e8b50080fa/websockets-17.3-cp313-cp313-win_amd64.whl", hash = "sha256:66027e401c70d0cf55021b91b8a7ec2cd5810e20602f945b4fe6ae8b37ed232b", size = 219026, upload-time = "2026-10-19T16:55:50.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d3/30f04bec5b9cf0c00c98c038494b210ece923a0b05b6df38a162b759e5ed/websockets-17.3-cp313-cp313-win_arm64.whl", hash = "sha256:6b14914e67044724c31a9360c7b89e9d070117c630746eefdc1620d967545acd", size = 218958, upload-time = "2026-10-19T16:55:52.085Z" }, + { url = "https://files.pythonhosted.org/packages/27/ad/f65e365bbf38d61164b0c2f64776fc7714bfa59ac51e492b038a35f4ab1b/websockets-17.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bc8fd230ef013951900c1fda100eebe4a4b1b407dde2a44186995042dda2c628", size = 218288, upload-time = "2026-10-19T16:55:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/48/1c/33271eba53b4927a1b91d99d838a6d7b888e6f708425262c911450d73696/websockets-17.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cdb8e4b0f6a41b857c74c03116e8ffbef59ef8e533e934b532843178990d01d5", size = 215977, upload-time = "2026-10-19T16:55:55.063Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f3/6d982c60f6c7c95e5faed53d39263df273ecfda904c7d6920c37eee58dd1/websockets-17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:018fab26140ce221c3853454a608e4e7004f4f10641cb97819dcd5b917bb2e99", size = 216193, upload-time = "2026-10-19T16:55:56.424Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/4ff6b8d2a4b32719d4658bbd791ee9d19939a47a69b961c961fd9dc3a64c/websockets-17.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd0f3037d501178a761fd2814370aca547f79e99a200a61a1f6e7b79ec3d78b9", size = 225573, upload-time = "2026-10-19T16:55:58.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8f/46e4030947f5590cb61832901f44a3edb07ef8d1e27f6c06b42c5f1c4d33/websockets-17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8dbb3862aa649de95660b6337d97101977f160b5b354d07b82fe309874d2c8bc", size = 225927, upload-time = "2026-10-19T16:55:59.561Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1b/3db30fdf4fbefa4ac463060d3f87e2d444f960aa1234028468c09b88bffc/websockets-17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af37a1e6b6bd506b597d5fb4cf111fb527d1b2223608a5d441d4738827cfd166", size = 227169, upload-time = "2026-10-19T16:56:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/02/ed/849799cfc0e3e359b75ca1d50605376da52dc9ef0b2a53931e3f5991f96e/websockets-17.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:740bba4223fa5216d4d6c5dcd29e697f37229b8a61b69e1a50527b0ea993ab2c", size = 228542, upload-time = "2026-10-19T16:56:02.377Z" }, + { url = "https://files.pythonhosted.org/packages/19/53/dcc397e20ce792fffd9a8f79b6333275748ee7a23f807e576166a0b16edd/websockets-17.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4170556762c6fd2913d99dfdd8dbb4c927584899df7facb1034627e355263c45", size = 227772, upload-time = "2026-10-19T16:56:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/c9/36/e5935782f71e9e22247b2a2e562da05e18d8945c49f2692ca1bdaceec178/websockets-17.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d355b22270be8062360b9df2ee0b10277590789a1059257916fb9e256a0cff80", size = 226518, upload-time = "2026-10-19T16:56:05.332Z" }, + { url = "https://files.pythonhosted.org/packages/27/76/bd5978a1e403fc646230cf35cf7c06b40b3a232d440881b9e89ede229b18/websockets-17.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4aa85b34c21cf97b3470793d0bf0763d32bfd1e67b83067a0a3ba8dc3474dacf", size = 223766, upload-time = "2026-10-19T16:56:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/c66eb39bd851d5c14d9eb75aa64d03cfcf2ded77927ca067b5917291d19d/websockets-17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0e2e7c2c1a4f25db7b6596738d810c10732e77f852247300a1bc49e364d6dc9", size = 226644, upload-time = "2026-10-19T16:56:08.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/b9a31d5497fa0b73629a097b173a26123da2ed656b562114644bc623414b/websockets-17.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7fa69212ea60ea24e2d8aff23fe102b1f87f1b27a320f92d2e7b3468997a496a", size = 225107, upload-time = "2026-10-19T16:56:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/b773a102f84b60b2052b2cfde773d2f3cd3e8eaa93a145f54450460549cf/websockets-17.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:80a6e37e235780f189558ab804f3cbc01c18f5c578995739b49613d544d34ebf", size = 225891, upload-time = "2026-10-19T16:56:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/e7e538575dd4912dea9ea4c9ae86519aa1420969d9cb5cf02fd10fd20ba7/websockets-17.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:628efe13630ac3397e7d1278b6be6f22f37a86d638a324334b4cd43c1daf9928", size = 227064, upload-time = "2026-10-19T16:56:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/bf2b928c156deefd0c50285a48fc985e2ca85d284e7d9981cc5b79b0e696/websockets-17.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:443fa28652682d2a3ec3eca49af1b87cb2523561257683e62e03a98967b22138", size = 224618, upload-time = "2026-10-19T16:56:13.73Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/d59817ba3535f787c1cf8990a01b3d2896a7d9d8f3b5d55bed22fce595a6/websockets-17.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6705af9d1005127302c04d9f1533ba56e289336c465923ef878430e99295a0b9", size = 225571, upload-time = "2026-10-19T16:56:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/a6/96/8210379dac889e16c75099aefffa200dc55dc8cb030010f309a7eb9c75c1/websockets-17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9bde5d82d7ac4859e0cb3a35e148f537a8d4e56aa43f42582b5f263982f095ae", size = 225869, upload-time = "2026-10-19T16:56:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/d4741c977fec9e6482beffece572d544d42d230f56fc3ef504f96521063a/websockets-17.3-cp314-cp314-win32.whl", hash = "sha256:be7be57097a65a8fab3d0f0a10d12931deb626a7d8082eb732b1ae41f3a202e3", size = 218232, upload-time = "2026-10-19T16:56:18.028Z" }, + { url = "https://files.pythonhosted.org/packages/33/69/2f9ce899776d0c522f7f44080cad4228a1c14ec13b0125a2efc12ebe41c9/websockets-17.3-cp314-cp314-win_amd64.whl", hash = "sha256:2b06213c8e1ce93b18d0476d7a5e647436c8f23d099888c9065e930e80f4bed5", size = 218582, upload-time = "2026-10-19T16:56:19.399Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e9/11b885666d72add674440a3435c059bea325616959cec8be42605948714c/websockets-17.3-cp314-cp314-win_arm64.whl", hash = "sha256:6b7b540bb321f7f0a7b8f5d107f941bb361e3a154c0b594dae0394997b91b743", size = 218465, upload-time = "2026-10-19T16:56:21.005Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/0c0f40ab8eefe9d5a6c78b4e267d8e37d70d3db8dd195574e0ab867e4f81/websockets-17.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c9d06edfd974e918b40e0662a7e76dac13125529ee5ec9fbe42dc0d6faa209c3", size = 218614, upload-time = "2026-10-19T16:56:22.457Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/d22b3d63aa439bc01a9477880070d05e0b257b0548294786a33be3e842ec/websockets-17.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a0bcf66289b9d017c175562c35c5304b5cc3ed56aded994ae30b0a9654ba405", size = 216151, upload-time = "2026-10-19T16:56:23.95Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/f60a9672cdeb8cb4f7156e6e83c76d59fdc586737a97ac77129e9fc188a2/websockets-17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f3ed747b7651668131029c720fbc32c7ee2d129bdad200e6c8f4037b20f7913a", size = 216354, upload-time = "2026-10-19T16:56:25.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/da/2be8c5f3c6612d4523f95b074e2352bc11bd090cf121d32bc11af1d52e70/websockets-17.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5868a02c4f6d698ddd19b4c9742748c03dec045289915806845189d96bc3d314", size = 225937, upload-time = "2026-10-19T16:56:26.726Z" }, + { url = "https://files.pythonhosted.org/packages/85/1c/cb84aec871840acd66dfa84520bf7b8e48ce70bdb3f4baa4efd097fbe6b1/websockets-17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa87ea2ad64e59fe5622c1dc5716106b6d6e7e4b26b848cb5f5710a75c292ef5", size = 226215, upload-time = "2026-10-19T16:56:28.1Z" }, + { url = "https://files.pythonhosted.org/packages/9b/7b/cfb65557accec2eda2c80288e13eca6f1c797c579375906eb73f4053cfd1/websockets-17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65e937576141ac50fac32fa9736dbb3e0e4a7769f91be004011a09216c6ab05f", size = 227389, upload-time = "2026-10-19T16:56:29.518Z" }, + { url = "https://files.pythonhosted.org/packages/70/a3/7e08ec4b23e376ed228831aa44b2f2a3f2e7025cc66dda4b2e53dcc2619e/websockets-17.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:35381b575f70a08d4241e73c0cde4672bb9b2a9aae8767ca6b7fb61305a052c8", size = 229534, upload-time = "2026-10-19T16:56:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7d/8a813e3e53956f876cfaad4f6358625bbd94168aa8046c44e2f3561f12af/websockets-17.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5142798871a248b422f353e0684d51dfa119de3403a6fa0904b7cb806d9790ce", size = 227959, upload-time = "2026-10-19T16:56:32.656Z" }, + { url = "https://files.pythonhosted.org/packages/41/23/5f3dab6ab44f2f30042db83a658a57129d5e3c0425c798b3c3e727bc060a/websockets-17.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e164c4a6868d11988c7f2386e3278ccff21c665cd7d08279b62709dd37a3167c", size = 226691, upload-time = "2026-10-19T16:56:34.101Z" }, + { url = "https://files.pythonhosted.org/packages/5b/49/99a3fb5a2e94d677fbd0d014a6f9a56a3d2b8e8007b7175727bc4fc4ae94/websockets-17.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10b24e836ee0233ca078e1244f6ba40343a22127a9a736f46a7e7b18de2cc711", size = 224225, upload-time = "2026-10-19T16:56:35.452Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e5/f503a4d757858d083c90ea85dd1fd636ae9eb63c9552c75a4c38514ee7ed/websockets-17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:56a76556ab04ab54a675acce2cac8b4e74ea2cec558a7c9ed37e0367b04b5d50", size = 226984, upload-time = "2026-10-19T16:56:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/8586844ad4b21988837624e1f4b331bc71919c85a6eb1ee3c8bd29893f27/websockets-17.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:33221e49be79d7bfe6d12bc52ef9458b256ab74fa8ac5660acfa3065de1c7870", size = 225436, upload-time = "2026-10-19T16:56:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/f419506494d6ffd205a9c3258f56ff5aeae6976de26361057de74f4ab659/websockets-17.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2fa79b9a48ae59744316c71a34cb9f76f30c93e117338522c5135cb7eac10351", size = 226325, upload-time = "2026-10-19T16:56:39.606Z" }, + { url = "https://files.pythonhosted.org/packages/28/c5/55a58faa67149e84cb98b33f9d8fda95242677ea07b78f3ecaf2e77877d6/websockets-17.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f87f02afe05c1c246012e3de100a77cfbabd17db709fe93cf074271ab239b312", size = 227342, upload-time = "2026-10-19T16:56:41.041Z" }, + { url = "https://files.pythonhosted.org/packages/35/e0/fbe561dff24b6ef1b2034877f37b56eb9b81b3c103e2a8b03819bf2b3b6d/websockets-17.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8135a549e27e9941a70e2d2156676ef2ffeff4b06e9d564dbaaae963482afdef", size = 225036, upload-time = "2026-10-19T16:56:42.433Z" }, + { url = "https://files.pythonhosted.org/packages/0d/37/d80753064f8370a24d3bec74807c3928d7b0ebef5381956e2a0108b365c2/websockets-17.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ace8dd1efa5f44c482d091aa38322e20e3ffe5adcdba6b67ad9f4e7da58f3f68", size = 225807, upload-time = "2026-10-19T16:56:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9d/a11d3168522a2959c4a5661728744d62b9eee344abdba5f9fbc1d359742f/websockets-17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2129c9a5fc05a230ce088745a790af073e582b584badc7b05db067ee326f56d7", size = 226241, upload-time = "2026-10-19T16:56:45.088Z" }, + { url = "https://files.pythonhosted.org/packages/06/28/16107b3b83fd2b5024bfecf8d000140f555204a0191202f9c4c981137810/websockets-17.3-cp314-cp314t-win32.whl", hash = "sha256:af7c474f0d8f4d2a196370e85033873c7b063a0b5da9a4f143aa023e3bd25407", size = 218365, upload-time = "2026-10-19T16:56:46.41Z" }, + { url = "https://files.pythonhosted.org/packages/77/dd/8b00e1dd552788d8c7762d7ef61de8f2f238d7357b1a02d4c4650b112196/websockets-17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c63ad657695e8f1a48624754cb6b464118ac50d4c1d477c26461c98390f0a62", size = 218706, upload-time = "2026-10-19T16:56:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/39/eb/5d90d57a5cbe614b2f13433aaf1c0b068c804589379061ba06812a185a89/websockets-17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:3c4f631dc8d00e470cde3ec0a35d4b58a817bea592411956f1cf4099dbb306b3", size = 218622, upload-time = "2026-10-19T16:56:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/6e/41/1efe5ecae1f32495dc75641704f959bd2d8e56cdc825f3acc5e010a51960/websockets-17.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f300f28dff74ff4148c21e6971e4de6ca0c34e0086a3d95c017c5174ebf85ca5", size = 218211, upload-time = "2026-10-19T16:56:50.423Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/4edb653533241203db3f11522e122338d4fda50a41ce2a542029086f8eda/websockets-17.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:32029441af953e980b9940f042b337fe4ad9a97fa8b17a4ed878ec67b5b89ef4", size = 215944, upload-time = "2026-10-19T16:56:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b9/ec450b9650925dd5cf27c1f36d0464786fe3ac702ed656879c4c0dd5c4df/websockets-17.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09f007b2150eff015b2109f006e2ac2012d376f41486defa88ae78a776b00cff", size = 216164, upload-time = "2026-10-19T16:56:53.125Z" }, + { url = "https://files.pythonhosted.org/packages/15/1e/63f7fbff9f0540a0331f1a62424c9ff24faa6ba8aec264f55e9e31017eea/websockets-17.3-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bdc658c9474864893e0b6b94f1390bf6a912952df1870dd93bb7d1a902b5628", size = 225608, upload-time = "2026-10-19T16:56:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1b/f6679ad61cb8c110033a1b30d561d5c1052983e202157251ff1778f27932/websockets-17.3-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15decc0fe293d9e3f924bd483f3d76f3d0ff26845112a470e3f8842fec2c82a1", size = 226042, upload-time = "2026-10-19T16:56:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/28/c6/ed993aaef8046cbbb939ae367ee3dd5383dbd055f0500a5b8aed101168b6/websockets-17.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:997617dd7856c5dad742bf6893b872319156dd43f47587541a59e4a852d52bbc", size = 227203, upload-time = "2026-10-19T16:56:57.076Z" }, + { url = "https://files.pythonhosted.org/packages/aa/96/7057dc71affe6c8252df8648f5a225642228a6fdd8d046a6b9aa1fb0e9eb/websockets-17.3-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a004c1fe5a72c496f28c6c1d8c2dc5cfe3278d99478e18f8b838c70a912b366f", size = 228900, upload-time = "2026-10-19T16:56:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/8b3268bb7e72b465e55172b86d55f58aa277fb6d9bac92aee615345874df/websockets-17.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35fe35d20baa661cef4a12ee7bc42eaf2376d43ef9816ab29dd47ca957f7a73a", size = 227684, upload-time = "2026-10-19T16:56:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/fa31c0a12240b1225c3712ff080d2f7ede4f695ca90e59fdb58e8affabba/websockets-17.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8b3bede7d583bc9f47d8693a29984882e59077db1a5238ad977fc70ae8169741", size = 226580, upload-time = "2026-10-19T16:57:01.371Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/75d911c206a3035e2ecfad107878e62c06e574b2437fa2cfb4ecef436fa3/websockets-17.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dafbb58a149d1ebd5767b3f064f911c5ee0897608005e51c3904b0ada2a8d99c", size = 223967, upload-time = "2026-10-19T16:57:02.839Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f8/f98aa36f611acd5770819f6690125c20d55f221fd6db8d00d284b5f78e89/websockets-17.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80326bc571164bbbb6bead2e35d0c943b23b24fde629032197c58151529d24d8", size = 226689, upload-time = "2026-10-19T16:57:04.458Z" }, + { url = "https://files.pythonhosted.org/packages/1a/36/d52f5dff7597de6fbfc26c3d126abd668137915719aebf97abcc3be77eca/websockets-17.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:7d53fc4b90ef6e11de639dc18744d9be8cbd70ccb5ce80027aaffe7b1ab0215a", size = 225353, upload-time = "2026-10-19T16:57:05.857Z" }, + { url = "https://files.pythonhosted.org/packages/d4/10/dcd70ad9f9cd20e8af43284ac250af610e8bf9022da25f5b409153d033b4/websockets-17.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:fdd00937f15f5f93c8e53d8060a8c4c010431ab595a9b5b50ddd569a04603944", size = 225879, upload-time = "2026-10-19T16:57:07.249Z" }, + { url = "https://files.pythonhosted.org/packages/70/ab/f26c6bfa99d38ca3d1bf23145306d97c5d0c75e133a8d837373e4b3d4629/websockets-17.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1dae5cd6c92d4440b15f20aa2987baa73c47ea14baa0fec8536a542e82e0ab33", size = 227031, upload-time = "2026-10-19T16:57:08.817Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3f/c8fd74cdb526cc65295b7468f2f84ba16678646d8174288822f99f510274/websockets-17.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b225c656efa2dedecc81651d2290170af25887b7de4f38c5efc7c5ae2b9442ab", size = 224842, upload-time = "2026-10-19T16:57:10.298Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/b3a07ce1c54fc0f5441642f5352662b90d7e658c966730eaf6d78a5f20bc/websockets-17.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:bd645b5d2f810a181c44f4a71135e7c365b44f4be9ed3d1892635be13a857553", size = 225744, upload-time = "2026-10-19T16:57:11.615Z" }, + { url = "https://files.pythonhosted.org/packages/66/37/8321a8a6ab0215d4df0238e49ace5f539364cf337bb122b85c788e8c93ec/websockets-17.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d2605a3b2af8d66f8b75312d223642c065ec773446c04566d9030ad5e2db13c9", size = 226006, upload-time = "2026-10-19T16:57:13.049Z" }, + { url = "https://files.pythonhosted.org/packages/f8/04/8c457a7e6a260b673bf4e6f2f422eb4af5f11c48505748359706f24f1bef/websockets-17.3-cp315-cp315-win32.whl", hash = "sha256:82e60c00cfb8493a5ba27cdf9b3695fdd153f666f73e11f18968f78ead1d8705", size = 218196, upload-time = "2026-10-19T16:57:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7c/c2df129d9e344b99bb388bcac54fbbdfeec29e6c716c12dc9a3398757940/websockets-17.3-cp315-cp315-win_amd64.whl", hash = "sha256:4d5f2492cb2192aab2d42e57f2caa2159b4fd20fde06f859ec13354f524b176c", size = 218490, upload-time = "2026-10-19T16:57:16.234Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4f/721b778c85559b2949e46246838e31a346dd815b3cdd8460fe517235675a/websockets-17.3-cp315-cp315-win_arm64.whl", hash = "sha256:55494475206113dbaf731dc8c5ffd5e1094175b753b7bbaad09fe3d7df61165c", size = 218403, upload-time = "2026-10-19T16:57:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/241d9ebb4dad79346b5a6abf9f14d7685b12ca358d250eea4920cdb1caf3/websockets-17.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:23cb6f6d64e3a389bb8ba150f947f9a6c8c0aca383e814ca3c5cffab738d5646", size = 218523, upload-time = "2026-10-19T16:57:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bb/559fe539992102c66cbca38b488af6c7b184ab7736d819afc0f72268a2b4/websockets-17.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f27e7e8a85512b6a4b490d5756edde8e6772883a5800e1476172e44eda3f7d7c", size = 216103, upload-time = "2026-10-19T16:57:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/34/ee/e8ade4f13a3d0da68636fe44bb8ac1a3e36fc57070512788320dead42067/websockets-17.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6342f79d6f50e680c8aa59e0615d745e867e51ffd59c77093be60b7c433067af", size = 216316, upload-time = "2026-10-19T16:57:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/4812b2a7a5e6571d9be425e76cf54ee69aaa15720caa02307785aa5b19a7/websockets-17.3-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fc23e72bd7f8a8e540fed2244524d0a9aeb6ae3325f3d56f04a7a3debe5b04c2", size = 226467, upload-time = "2026-10-19T16:57:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e8/adfc05d7f9b6b0cce6cc554426c50b7058609119b6b84a39171fe5ed4e50/websockets-17.3-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:722ab747ff5dc28070b1a8c020dba6466f13f2ab56bc251ad301fefa2454aa1e", size = 226736, upload-time = "2026-10-19T16:57:24.419Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/d5b93dd933552554507edae9e3a4890269a8ea8b46019450ea9c8a30e7cc/websockets-17.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d20d6ea5aabbeba2e054d33e405192ef75dc12947bbd00ec97d23b14969d325", size = 228065, upload-time = "2026-10-19T16:57:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/10/37/642f0f1cf1c4814147a4f0586f796a0b38e42b69e5a296bfc270fb660051/websockets-17.3-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:90cc9973c51811918247a78d9ab2f54871296e4e37e69a98aba4d3c9acedbf62", size = 229692, upload-time = "2026-10-19T16:57:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/5b/33/eac81cf62ac394b61db6a27ccbe8fb9ae382016ef73ede1edf9242a6a43e/websockets-17.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63b87c93e28957e6dc2bb92d31727a863c9bf4a4daa080f0995fdcb0242c9166", size = 228503, upload-time = "2026-10-19T16:57:29.031Z" }, + { url = "https://files.pythonhosted.org/packages/0d/84/e057788a9bd056f4f7d3a06b59efaaf724bbc6f2d1e1d316f8a93c951abc/websockets-17.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f7431e71072962d3ed1768038200aa4f1e76d9c2d7bedd554d57a55faaf6644", size = 227379, upload-time = "2026-10-19T16:57:30.571Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/cca1f9f45b893bb5b7545f2b234f910e07fcd2f8065724ac1a62a0961be2/websockets-17.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:534ed583fd4323de45d196535a444cebd1af2105e6e4e0cdcbf8cea266a8747a", size = 224719, upload-time = "2026-10-19T16:57:31.895Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a0/5d07451027150c802ce4c9999e86994a3f0de5f4dbe3d1cce46995999269/websockets-17.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8879b6a4040ee925c638548238ab7fe612cf5fa8752fe847346da0da8be81c3f", size = 227564, upload-time = "2026-10-19T16:57:33.236Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/835f37e13fac0c615a700caae0d39deff1de2d2b9b341836a786f9b359e0/websockets-17.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58e5d1e935a90878a935c7d2737a25c71cf4f1c13b6e6bef454ab722c4f338bb", size = 226218, upload-time = "2026-10-19T16:57:34.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/8a/718639939b947fe715501e8073350fd3f8a42f8b5eba3c0c2389c5836513/websockets-17.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:0a657b51e4a5f4ac577c11e706ff9a884ea5aaf3e2666bae283a251c63e66acb", size = 226712, upload-time = "2026-10-19T16:57:36.248Z" }, + { url = "https://files.pythonhosted.org/packages/e8/45/6bef8572b9b0dee51369b69776af1e4c7cc3a807f199dc3c165bbcf1199a/websockets-17.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:411aa0fb234f42a1444dea1e177890ddfc577b36ffdfafec25cb0d2c818e1af8", size = 227819, upload-time = "2026-10-19T16:57:37.586Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/3b726d0b46a132e641d2d8c0243ea8a7d39464dbaf28c7a206e51fbae681/websockets-17.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:623d4b44f64efdf235b1e8dce266057073d93cde6197e7d7684f20c297f67f30", size = 225573, upload-time = "2026-10-19T16:57:39.285Z" }, + { url = "https://files.pythonhosted.org/packages/90/96/cd972a12e1412d9b42c42ef42efbd81aa396ca9408d982d524012f011a8d/websockets-17.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5d0c871efc696ef5eb8514df64b86ab28736db753ebb42a2101f3b562f4849c0", size = 226430, upload-time = "2026-10-19T16:57:40.915Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2d/e11524771588188a39f6da234779d4942496b96c5a10b92f3df30db48608/websockets-17.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1db2604e6aedb7a2cb2f6e0575d6c9a852ccac8239703a930ac83b63309177a5", size = 226728, upload-time = "2026-10-19T16:57:42.245Z" }, + { url = "https://files.pythonhosted.org/packages/17/9b/63722aed973f53ea579e1ce0c4563a2b8b536629e556feff26e4c6a2a60d/websockets-17.3-cp315-cp315t-win32.whl", hash = "sha256:07246d4f0293ef0b87eff3d7dd88c1f03dd9f678fa39f6de87b2af8504e7e0de", size = 218325, upload-time = "2026-10-19T16:57:43.585Z" }, + { url = "https://files.pythonhosted.org/packages/34/07/5e0c92e225546f1789c957c0383852797be7c899b07207e9b6eb4a93473f/websockets-17.3-cp315-cp315t-win_amd64.whl", hash = "sha256:856bbb24f2ee6436af7609267e73309e4bb0a0f9982f79f1c2e01019279c6671", size = 218641, upload-time = "2026-10-19T16:57:44.918Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e4/339663de65c44ab3eb5209890e86eed26e1a131d6afab3df6f1c3e5b4d8c/websockets-17.3-cp315-cp315t-win_arm64.whl", hash = "sha256:a22c4956a4d858200dba83f0691bb692d77b0eeddfca80157820d20b85dea4f1", size = 218564, upload-time = "2026-10-19T16:57:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f1/b462b1518779adf0b6fce305beee07357a79cf0724bbe61113ed6521882e/websockets-17.3-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:34c78052277053bcb09810ac8e9ba4029f9e3e277833fc18ec1c928a6246ac91", size = 215839, upload-time = "2026-10-19T16:57:47.742Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/7dcea408892967516cc00d491ee7bba48577059c6eb0811f436b720bcd98/websockets-17.3-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:ef07e0b6b879f699a16fc7a513b974246fdeaef44908a021130cf4afe6a1c89c", size = 216114, upload-time = "2026-10-19T16:57:49.111Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b7/2d447e461f1160dc8dcdd8b143e2adba54d402e5f828eeb1f8469970c937/websockets-17.3-pp311-pypy311_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99c5637b74e2805ee4de9e2a43c8b45f678e50abb1c2da9880f0372f6498ba2f", size = 217065, upload-time = "2026-10-19T16:57:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/51/35aa5edd12b23e0001b85517ad447342c07a29fcee62009b0859825b77a7/websockets-17.3-pp311-pypy311_pp80-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c460aeb12c2bd1c204944d9e9199595bf9e505eb542018d304bf9e63cb60cad", size = 216952, upload-time = "2026-10-19T16:57:51.72Z" }, + { url = "https://files.pythonhosted.org/packages/6c/db/537306b92236d761b99dc56a7316c0ad46f2854cd0756585b3a9d3fe5639/websockets-17.3-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1c174b366565e101f903f9d1cc96d5508f805759dd37d012743f1c73ec0d7b", size = 217844, upload-time = "2026-10-19T16:57:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c2/494bed87536f3822872e57b07ac9afa744af88ff64dda664b9caa32b1a72/websockets-17.3-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:c4b3f2c7d84f303da873b634d0beef83b5e1137294c3de4e23102841dac0cf4a", size = 306274, upload-time = "2026-10-19T16:57:54.525Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1a/5582103e0362f86c4725051ded89e585ceaf750f26beace530b7e42c81c0/websockets-17.3-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:0e65260a1b1d7b81651f47343d836de70b88b02358ae3658efa6382104ec37b1", size = 215872, upload-time = "2026-10-19T16:57:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a7/f69d65ed3ff9ae73d428a918300c23a992e4d50683090b8dc33cc3341d5a/websockets-17.3-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:94c02b0702656f083c098bdbd93bce22a7aa7a2b34ba56d2472efb67dab7cf58", size = 216136, upload-time = "2026-10-19T16:57:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/69/76/c64fda4ac18a3aeb32e66acec481011a766add480b6362fad80e2c3c1963/websockets-17.3-pp312-pypy312_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ade76f8c84ade325bbed96806357fffd353fcc29a5166d6dc6d39381f6d9f76c", size = 217080, upload-time = "2026-10-19T16:57:58.593Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6d/085a3f0e6152bbe79917973f3d33cd008437de6c04675e2f15253b921f21/websockets-17.3-pp312-pypy312_pp80-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a215749c9a88ef2842e880215193e56ae60b856773c99702b078236bc6d4adb", size = 216966, upload-time = "2026-10-19T16:57:59.924Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b8/7eb904583ea2090b329f50487e6f8dc58eb558712b9f3c16f289f0be668e/websockets-17.3-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7b5d23ef144b32645d97bd99f382c4394c443395f45fe1576786838d26e308", size = 217828, upload-time = "2026-10-19T16:58:01.822Z" }, + { url = "https://files.pythonhosted.org/packages/79/36/f54b4a1cd9456ca0a65a76d77d181621433793aca633dfeed214748ff4f0/websockets-17.3-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:13eace1b315345ebac5b27821f5dcce24e5cf9976ab18f79e00a59bdd67e5d19", size = 306301, upload-time = "2026-10-19T16:58:03.161Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/3f01fcb3406662f6deb1e3e72b34dd0979e993991c0b7435a9e12bc14f02/websockets-17.3-py3-none-any.whl", hash = "sha256:dc2b495a92b3971966bf102e73589f950db3ec1ca6713265355ce7771d97b199", size = 212375, upload-time = "2026-10-19T16:58:04.483Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/05/ba1458dee0ac485b2a01071c1b88f5199fe878a485df482e10e1ef2205cc/zipp-4.1.2.tar.gz", hash = "sha256:cf474b90b07ea932637e17c6b9479de2b2882900b0d311bdaf4f5c000a15a97f", size = 27864, upload-time = "2026-10-21T18:57:04.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/44/7bd2007260da11cccca29ba2d4c345f1287aa663db38ee7cefeabf6386a5/zipp-4.1.2-py3-none-any.whl", hash = "sha256:b30cb6cd253122169c47de245ca24a86bac2a5d3802ba15bf7ba6414eb9e144d", size = 10581, upload-time = "2026-10-21T18:57:03.79Z" }, +]