init-data-py 0.2.7__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- init_data_py-1.0.0/.gitattributes +1 -0
- init_data_py-1.0.0/.github/workflows/ci.yml +97 -0
- init_data_py-1.0.0/.github/workflows/publish.yml +31 -0
- init_data_py-1.0.0/CHANGELOG.md +75 -0
- init_data_py-1.0.0/PKG-INFO +279 -0
- init_data_py-1.0.0/README.md +247 -0
- init_data_py-1.0.0/pyproject.toml +118 -0
- init_data_py-1.0.0/src/init_data_py/__init__.py +41 -0
- init_data_py-1.0.0/src/init_data_py/_crypto.py +87 -0
- init_data_py-1.0.0/src/init_data_py/_ed25519.py +63 -0
- init_data_py-1.0.0/src/init_data_py/errors.py +217 -0
- init_data_py-1.0.0/src/init_data_py/models.py +434 -0
- init_data_py-1.0.0/src/init_data_py/parsing.py +221 -0
- init_data_py-1.0.0/src/init_data_py/signing.py +85 -0
- init_data_py-1.0.0/src/init_data_py/validation.py +284 -0
- init_data_py-1.0.0/tests/__init__.py +0 -0
- init_data_py-1.0.0/tests/test_forward_compat.py +65 -0
- init_data_py-1.0.0/tests/test_parse.py +58 -0
- init_data_py-1.0.0/tests/test_sign.py +80 -0
- init_data_py-1.0.0/tests/test_signature.py +219 -0
- init_data_py-1.0.0/tests/test_validate.py +140 -0
- init_data_py-1.0.0/tests/vectors.py +86 -0
- init_data_py-1.0.0/uv.lock +502 -0
- init_data_py-0.2.7/.github/workflows/publish.yml +0 -41
- init_data_py-0.2.7/PKG-INFO +0 -86
- init_data_py-0.2.7/README.md +0 -76
- init_data_py-0.2.7/pyproject.toml +0 -77
- init_data_py-0.2.7/requirements-dev.lock +0 -42
- init_data_py-0.2.7/requirements.lock +0 -12
- init_data_py-0.2.7/src/init_data_py/__init__.py +0 -5
- init_data_py-0.2.7/src/init_data_py/errors/__init__.py +0 -15
- init_data_py-0.2.7/src/init_data_py/errors/errors.py +0 -27
- init_data_py-0.2.7/src/init_data_py/init_data.py +0 -304
- init_data_py-0.2.7/src/init_data_py/types/__init__.py +0 -4
- init_data_py-0.2.7/src/init_data_py/types/chat.py +0 -41
- init_data_py-0.2.7/src/init_data_py/types/object.py +0 -34
- init_data_py-0.2.7/src/init_data_py/types/user.py +0 -65
- init_data_py-0.2.7/tests/test_parse.py +0 -29
- init_data_py-0.2.7/tests/test_sign.py +0 -33
- init_data_py-0.2.7/tests/test_to_query_string.py +0 -11
- init_data_py-0.2.7/tests/test_validate.py +0 -60
- {init_data_py-0.2.7 → init_data_py-1.0.0}/.gitignore +0 -0
- {init_data_py-0.2.7 → init_data_py-1.0.0}/.python-version +0 -0
- {init_data_py-0.2.7 → init_data_py-1.0.0}/LICENCE +0 -0
- /init_data_py-0.2.7/tests/__init__.py → /init_data_py-1.0.0/src/init_data_py/py.typed +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test on Python ${{ matrix.python-version }}
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install project
|
|
27
|
+
run: uv sync --locked
|
|
28
|
+
|
|
29
|
+
# Run twice: the core must work with no dependencies at all, and the
|
|
30
|
+
# signature tests must run when the extra is present.
|
|
31
|
+
- name: Test without the ed25519 extra
|
|
32
|
+
run: uv run python -m unittest discover --verbose
|
|
33
|
+
|
|
34
|
+
- name: Test with the ed25519 extra
|
|
35
|
+
run: uv run --extra ed25519 python -m unittest discover --verbose
|
|
36
|
+
|
|
37
|
+
lint:
|
|
38
|
+
name: Lint
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
45
|
+
with:
|
|
46
|
+
enable-cache: true
|
|
47
|
+
|
|
48
|
+
- name: Install project
|
|
49
|
+
run: uv sync --locked
|
|
50
|
+
|
|
51
|
+
- name: Ruff check
|
|
52
|
+
run: uv run ruff check .
|
|
53
|
+
|
|
54
|
+
- name: Ruff format check
|
|
55
|
+
run: uv run ruff format --check .
|
|
56
|
+
|
|
57
|
+
types:
|
|
58
|
+
name: Type check
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
- name: Install uv
|
|
64
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
65
|
+
with:
|
|
66
|
+
enable-cache: true
|
|
67
|
+
|
|
68
|
+
- name: Install project with the ed25519 extra
|
|
69
|
+
run: uv sync --locked --extra ed25519
|
|
70
|
+
|
|
71
|
+
- name: Mypy
|
|
72
|
+
run: uv run --no-sync mypy
|
|
73
|
+
|
|
74
|
+
build:
|
|
75
|
+
name: Build
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
|
|
80
|
+
- name: Install uv
|
|
81
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
82
|
+
with:
|
|
83
|
+
enable-cache: true
|
|
84
|
+
|
|
85
|
+
- name: Build distributions
|
|
86
|
+
run: uv build
|
|
87
|
+
|
|
88
|
+
- name: Check the wheel ships py.typed
|
|
89
|
+
run: |
|
|
90
|
+
python - <<'EOF'
|
|
91
|
+
import glob, zipfile
|
|
92
|
+
|
|
93
|
+
wheel = glob.glob("dist/*.whl")[0]
|
|
94
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
95
|
+
assert "init_data_py/py.typed" in names, names
|
|
96
|
+
print("py.typed present in", wheel)
|
|
97
|
+
EOF
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish Python Package to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types:
|
|
6
|
+
- published
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
environment:
|
|
14
|
+
name: pypi
|
|
15
|
+
url: https://pypi.org/p/init-data-py
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: uv build --no-sources
|
|
29
|
+
|
|
30
|
+
- name: Publish to PyPI
|
|
31
|
+
run: uv publish
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [1.0.0] - 2026-09-06
|
|
7
|
+
|
|
8
|
+
A rewrite. See the migration table in the README for the old to new mapping.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Hashes are computed from the query string as received.** Earlier versions
|
|
13
|
+
rebuilt the signed string from parsed fields, then patched over the
|
|
14
|
+
differences by escaping every slash in it. That produced hashes Telegram
|
|
15
|
+
would reject whenever a value held a slash, such as `start_param=ref/abc`,
|
|
16
|
+
and made the result depend on the order of keys inside the `user` JSON.
|
|
17
|
+
- **Unknown fields no longer raise.** A field this library does not recognise
|
|
18
|
+
used to raise `UnexpectedFormatError`, and an unrecognised field inside
|
|
19
|
+
`user` raised a bare `TypeError`. Both are now kept and readable through
|
|
20
|
+
`extra`, so a field Telegram adds later cannot break parsing.
|
|
21
|
+
- Hashes are compared with `hmac.compare_digest` instead of `!=`.
|
|
22
|
+
- A field sent more than once is rejected instead of silently taking the last
|
|
23
|
+
value, which could hash one value and hand another to the caller.
|
|
24
|
+
- Timestamps are timezone aware UTC rather than naive local time.
|
|
25
|
+
- A malformed `auth_date` raises `MalformedFieldError` instead of letting a
|
|
26
|
+
`ValueError` escape.
|
|
27
|
+
- Objects are hashable again. Defining `__eq__` without `__hash__` had made
|
|
28
|
+
them unusable in sets and as dict keys.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `validate_by_signature` and `is_valid_by_signature`, which verify Telegram's
|
|
33
|
+
Ed25519 `signature` field. For third parties that need to trust init data for
|
|
34
|
+
a bot whose token they do not have. Needs the new `ed25519` extra:
|
|
35
|
+
`pip install "init-data-py[ed25519]"`.
|
|
36
|
+
- `hash_token` and `token_hashed`, so a service can validate without ever
|
|
37
|
+
holding the bot token itself.
|
|
38
|
+
- `strip_auth_header`, for the `Authorization: tma <init data>` convention.
|
|
39
|
+
- `InitData.extra`, `InitData.get`, `User.extra` and `Chat.extra` for reading
|
|
40
|
+
fields this version does not know about.
|
|
41
|
+
- `InitData.issued_at`, `auth_date` as a timezone aware datetime.
|
|
42
|
+
- `is_ed25519_available` and `TELEGRAM_PUBLIC_KEYS`.
|
|
43
|
+
- A `py.typed` marker. Type checkers previously treated the package as
|
|
44
|
+
untyped despite it being fully annotated.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Init data now expires after one day by default.** It previously never
|
|
49
|
+
expired unless a lifetime was passed, so replay protection was something you
|
|
50
|
+
had to remember to switch on. Pass `expires_in=0` for the old behaviour.
|
|
51
|
+
- `validate_by_hash` returns the parsed init data, so there is no step where
|
|
52
|
+
parsed but unchecked data sits in a variable.
|
|
53
|
+
- `validate(raise_error=False)` is replaced by `is_valid_by_hash`. The return
|
|
54
|
+
type no longer depends on an argument. It reports `False` only for
|
|
55
|
+
unacceptable init data, and still raises on a mistake in the calling code.
|
|
56
|
+
- `sign` returns a new query string instead of mutating and returning `self`.
|
|
57
|
+
- `chat_type` and `Chat.type` are plain strings rather than a fixed set, since
|
|
58
|
+
Telegram can add values.
|
|
59
|
+
- `User` and `Chat` are frozen dataclasses.
|
|
60
|
+
- Errors carry structured data. `ExpiredError` has `issued_at`, `expires_at`
|
|
61
|
+
and `expired_for`; `SignatureInvalidError` has `bot_id` and `environment`.
|
|
62
|
+
- `MissingDependencyError` sits outside `InvalidInitDataError`, so a missing
|
|
63
|
+
optional dependency raises rather than being reported as invalid data.
|
|
64
|
+
- Requires Python 3.10 or newer.
|
|
65
|
+
|
|
66
|
+
### Removed
|
|
67
|
+
|
|
68
|
+
- The `InitData` class API: `parse`, `from_query_string`, `validate`, `sign`,
|
|
69
|
+
`calculate_hash`, `to_dict`, `to_json` as methods.
|
|
70
|
+
- The `init_data_py.types` package. `User` and `Chat` come from the top level.
|
|
71
|
+
- `init_data_py.errors.errors`. Import from `init_data_py.errors`.
|
|
72
|
+
|
|
73
|
+
The old error names `InitDataPyError`, `UnexpectedFormatError`,
|
|
74
|
+
`SignMissingError` and `SignInvalidError` are kept as aliases of their
|
|
75
|
+
replacements, so existing `except` clauses keep working.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: init-data-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python library that provides tools for using and validating Telegram Mini App init data.
|
|
5
|
+
Project-URL: Homepage, https://github.com/nimaxin/init-data-py
|
|
6
|
+
Project-URL: Repository, https://github.com/nimaxin/init-data-py
|
|
7
|
+
Project-URL: Issues, https://github.com/nimaxin/init-data-py/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/nimaxin/init-data-py/blob/master/CHANGELOG.md
|
|
9
|
+
Author-email: nimaxin <nimaxin@proton.me>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENCE
|
|
12
|
+
Keywords: authentication,init-data,telegram,telegram-mini-app,telegram-web-app,validation,webappinitdata
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
|
+
Classifier: Topic :: Internet
|
|
25
|
+
Classifier: Topic :: Security
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Provides-Extra: ed25519
|
|
30
|
+
Requires-Dist: cryptography>=41.0; extra == 'ed25519'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# init-data-py
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
Parse, validate and sign [Telegram Mini App init data](https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app).
|
|
41
|
+
|
|
42
|
+
Init data is the signed payload a Mini App receives in
|
|
43
|
+
`window.Telegram.WebApp.initData` and sends to your backend. Anyone can post a
|
|
44
|
+
made up query string to that backend, so the signature has to be checked before
|
|
45
|
+
anything inside it is trusted.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install init-data-py
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The core has no dependencies. Validating with Telegram's public key instead of
|
|
54
|
+
your bot token needs one extra:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install "init-data-py[ed25519]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Requires Python 3.10 or newer.
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from init_data_py import validate_by_hash
|
|
66
|
+
|
|
67
|
+
init_data = validate_by_hash(query_string, bot_token)
|
|
68
|
+
print(init_data.user.id)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`validate_by_hash` returns the parsed init data and raises if anything is
|
|
72
|
+
wrong, so there is never a moment where you are holding data you have not
|
|
73
|
+
checked yet.
|
|
74
|
+
|
|
75
|
+
## Validating
|
|
76
|
+
|
|
77
|
+
### With your bot token
|
|
78
|
+
|
|
79
|
+
The usual case, for a backend that owns the bot.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from init_data_py import errors, validate_by_hash
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
init_data = validate_by_hash(query_string, bot_token)
|
|
86
|
+
except errors.ExpiredError as error:
|
|
87
|
+
... # error.issued_at, error.expires_at, error.expired_for
|
|
88
|
+
except errors.InvalidInitDataError:
|
|
89
|
+
... # anything else wrong with it
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Init data expires after one day by default. Pass `expires_in` to change it, or
|
|
93
|
+
`0` to turn the check off. Turning it off lets an old query string be replayed
|
|
94
|
+
forever, so prefer a short lifetime.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from datetime import timedelta
|
|
98
|
+
|
|
99
|
+
validate_by_hash(query_string, bot_token, expires_in=3600)
|
|
100
|
+
validate_by_hash(query_string, bot_token, expires_in=timedelta(hours=1))
|
|
101
|
+
validate_by_hash(query_string, bot_token, expires_in=0) # no expiry check
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If you only want a boolean, use `is_valid_by_hash`. It returns `False` when the
|
|
105
|
+
init data is unacceptable, but still raises if your own call was wrong, so a
|
|
106
|
+
mistake in your code never looks like a rejected user.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from init_data_py import is_valid_by_hash
|
|
110
|
+
|
|
111
|
+
if not is_valid_by_hash(query_string, bot_token):
|
|
112
|
+
raise PermissionError
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Keeping the bot token out of the service
|
|
116
|
+
|
|
117
|
+
`hash_token` turns a bot token into the digest that validation actually uses.
|
|
118
|
+
Store that instead of the token and pass `token_hashed=True`.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from init_data_py import hash_token, validate_by_hash
|
|
122
|
+
|
|
123
|
+
digest = hash_token(bot_token) # do this once, somewhere safe
|
|
124
|
+
validate_by_hash(query_string, digest, token_hashed=True)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Without the bot token
|
|
128
|
+
|
|
129
|
+
A third party that needs to trust init data for a bot it does not own can
|
|
130
|
+
verify Telegram's Ed25519 signature instead. This needs the `ed25519` extra,
|
|
131
|
+
and only clients from Bot API 8.0 onwards send the `signature` field.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from init_data_py import validate_by_signature
|
|
135
|
+
|
|
136
|
+
init_data = validate_by_signature(query_string, bot_id)
|
|
137
|
+
init_data = validate_by_signature(query_string, bot_id, environment="test")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`is_valid_by_signature` is the boolean form. If the extra is not installed,
|
|
141
|
+
both raise `MissingDependencyError` rather than reporting a bad signature, so a
|
|
142
|
+
packaging mistake can never be mistaken for a forged request. Use
|
|
143
|
+
`is_ed25519_available()` to check for the extra at startup instead of finding
|
|
144
|
+
out on the first request. The keys themselves are in `TELEGRAM_PUBLIC_KEYS`.
|
|
145
|
+
|
|
146
|
+
## Parsing without validating
|
|
147
|
+
|
|
148
|
+
`parse` only reads the query string. It proves nothing about where the data
|
|
149
|
+
came from, so do not trust anything it returns until a validator has passed.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from init_data_py import parse
|
|
153
|
+
|
|
154
|
+
init_data = parse(query_string)
|
|
155
|
+
init_data.user # a User, or None
|
|
156
|
+
init_data.chat # a Chat, or None
|
|
157
|
+
init_data.issued_at # timezone aware UTC datetime
|
|
158
|
+
init_data.to_query_string() # exactly the string that was parsed
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Fields this version does not know about are kept rather than rejected, so a
|
|
162
|
+
field Telegram adds later cannot break parsing:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
init_data.get("some_new_field") # raw value from the query string
|
|
166
|
+
init_data.extra # every field with no attribute of its own
|
|
167
|
+
init_data.user.extra # the same, inside user and chat
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Reading the Authorization header
|
|
171
|
+
|
|
172
|
+
Mini App backends usually receive init data as `Authorization: tma <init data>`.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from init_data_py import strip_auth_header, validate_by_hash
|
|
176
|
+
|
|
177
|
+
raw = strip_auth_header(request.headers["Authorization"])
|
|
178
|
+
init_data = validate_by_hash(raw, bot_token)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Signing
|
|
182
|
+
|
|
183
|
+
Useful for tests and fixtures. Real init data comes from Telegram.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from init_data_py import UnsignedInitData, User, sign
|
|
187
|
+
|
|
188
|
+
query_string = sign(
|
|
189
|
+
UnsignedInitData(user=User(id=5167898484, first_name="xin")),
|
|
190
|
+
bot_token,
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`sign` returns a new query string and changes nothing in place. It produces
|
|
195
|
+
`auth_date` and `hash` itself, so passing either one in is an error. A mapping
|
|
196
|
+
or a list of pairs works too, when you want to control the exact strings.
|
|
197
|
+
|
|
198
|
+
## Errors
|
|
199
|
+
|
|
200
|
+
Everything raised by this library inherits from `InitDataError`. Everything
|
|
201
|
+
caused by bad init data inherits from `InvalidInitDataError`, which is exactly
|
|
202
|
+
what the `is_valid_*` functions turn into `False`.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
InitDataError
|
|
206
|
+
├── InvalidInitDataError
|
|
207
|
+
│ ├── ParseError
|
|
208
|
+
│ │ ├── MalformedQueryStringError
|
|
209
|
+
│ │ ├── MalformedAuthHeaderError
|
|
210
|
+
│ │ ├── DuplicateFieldError
|
|
211
|
+
│ │ ├── MissingFieldError
|
|
212
|
+
│ │ │ ├── AuthDateMissingError
|
|
213
|
+
│ │ │ └── HashMissingError
|
|
214
|
+
│ │ └── MalformedFieldError
|
|
215
|
+
│ ├── AuthenticityError
|
|
216
|
+
│ │ ├── HashInvalidError
|
|
217
|
+
│ │ ├── SignatureMissingError
|
|
218
|
+
│ │ └── SignatureInvalidError
|
|
219
|
+
│ └── ExpiredError
|
|
220
|
+
└── MissingDependencyError
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`MissingDependencyError` sits outside `InvalidInitDataError` on purpose. A
|
|
224
|
+
missing optional dependency raises instead of quietly reporting invalid data.
|
|
225
|
+
|
|
226
|
+
## Migrating from 0.2.x
|
|
227
|
+
|
|
228
|
+
Version 1.0 replaces the `InitData` class with functions. The old class
|
|
229
|
+
rebuilt the signed string from parsed fields, which produced hashes Telegram
|
|
230
|
+
would reject whenever a value held a slash or the JSON key order differed.
|
|
231
|
+
|
|
232
|
+
| 0.2.x | 1.0 |
|
|
233
|
+
| --- | --- |
|
|
234
|
+
| `InitData.parse(qs)` | `parse(qs)` |
|
|
235
|
+
| `InitData.from_query_string(qs)` | `parse(qs)` |
|
|
236
|
+
| `init_data.validate(token)` | `validate_by_hash(qs, token)` |
|
|
237
|
+
| `init_data.validate(token, raise_error=False)` | `is_valid_by_hash(qs, token)` |
|
|
238
|
+
| `init_data.validate(token, lifetime=3600)` | `validate_by_hash(qs, token, expires_in=3600)` |
|
|
239
|
+
| `InitData(user=user).sign(token)` | `sign(UnsignedInitData(user=user), token)` |
|
|
240
|
+
| `init_data.calculate_hash(token)` | not public, use `validate_by_hash` |
|
|
241
|
+
| `from init_data_py.types import User` | `from init_data_py import User` |
|
|
242
|
+
| `errors.InitDataPyError` | `errors.InitDataError` |
|
|
243
|
+
| `errors.UnexpectedFormatError` | `errors.ParseError` |
|
|
244
|
+
| `errors.SignMissingError` | `errors.HashMissingError` |
|
|
245
|
+
| `errors.SignInvalidError` | `errors.HashInvalidError` |
|
|
246
|
+
|
|
247
|
+
The old error names are kept as aliases of the new classes, so existing
|
|
248
|
+
`except` clauses keep working.
|
|
249
|
+
|
|
250
|
+
Two behaviour changes worth knowing:
|
|
251
|
+
|
|
252
|
+
- **Init data now expires by default.** 0.2.x accepted init data of any age
|
|
253
|
+
unless you passed `lifetime`. Pass `expires_in=0` for the old behaviour.
|
|
254
|
+
- **Unknown fields no longer raise.** 0.2.x rejected any field it did not
|
|
255
|
+
recognise, which broke every time Telegram added one.
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
This project uses [uv](https://docs.astral.sh/uv/).
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
uv sync --extra ed25519 # set up the environment
|
|
263
|
+
uv run python -m unittest discover --verbose # run the tests
|
|
264
|
+
uv run ruff check . # lint
|
|
265
|
+
uv run ruff format . # format
|
|
266
|
+
uv run mypy # type check
|
|
267
|
+
uv build # build the distributions
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The test suite must also pass without the extra, since the core is meant to
|
|
271
|
+
have no dependencies:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
uv sync && uv run python -m unittest discover
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
This library is licensed under the [MIT License](LICENCE).
|