aiowapy 0.1.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.
- aiowapy-0.1.0/.github/workflows/ci.yml +24 -0
- aiowapy-0.1.0/.github/workflows/docs.yml +19 -0
- aiowapy-0.1.0/.github/workflows/workflow.yml +34 -0
- aiowapy-0.1.0/.gitignore +50 -0
- aiowapy-0.1.0/AGENTS.md +22 -0
- aiowapy-0.1.0/CHANGELOG.md +27 -0
- aiowapy-0.1.0/LICENSE +21 -0
- aiowapy-0.1.0/PKG-INFO +204 -0
- aiowapy-0.1.0/README.md +172 -0
- aiowapy-0.1.0/docs/api/client.md +11 -0
- aiowapy-0.1.0/docs/api/events.md +3 -0
- aiowapy-0.1.0/docs/api/models.md +3 -0
- aiowapy-0.1.0/docs/api/router.md +3 -0
- aiowapy-0.1.0/docs/api/store.md +3 -0
- aiowapy-0.1.0/docs/appstate.md +61 -0
- aiowapy-0.1.0/docs/groups.md +70 -0
- aiowapy-0.1.0/docs/index.md +76 -0
- aiowapy-0.1.0/docs/media.md +80 -0
- aiowapy-0.1.0/docs/messages.md +67 -0
- aiowapy-0.1.0/docs/presence.md +37 -0
- aiowapy-0.1.0/docs/protocol.md +68 -0
- aiowapy-0.1.0/docs/quickstart.md +98 -0
- aiowapy-0.1.0/docs/receiving.md +102 -0
- aiowapy-0.1.0/docs/storage.md +103 -0
- aiowapy-0.1.0/examples/pair.py +69 -0
- aiowapy-0.1.0/examples/pair_code.py +62 -0
- aiowapy-0.1.0/examples/pairing_qr_demo.py +60 -0
- aiowapy-0.1.0/mkdocs.yml +80 -0
- aiowapy-0.1.0/pyproject.toml +80 -0
- aiowapy-0.1.0/scripts/fixtures/gen_signal_fixtures.mjs +79 -0
- aiowapy-0.1.0/scripts/fixtures/gen_wabinary_fixtures.mjs +104 -0
- aiowapy-0.1.0/scripts/fixtures/package-lock.json +1428 -0
- aiowapy-0.1.0/scripts/fixtures/package.json +12 -0
- aiowapy-0.1.0/scripts/fixtures/tmp_login.mjs +12 -0
- aiowapy-0.1.0/scripts/fixtures/tmp_trace.mjs +38 -0
- aiowapy-0.1.0/scripts/review.lua +381 -0
- aiowapy-0.1.0/scripts/review.sh +126 -0
- aiowapy-0.1.0/scripts/sync_qr_tables.py +57 -0
- aiowapy-0.1.0/scripts/sync_wabinary_tokens.py +74 -0
- aiowapy-0.1.0/src/aiowapy/__init__.py +39 -0
- aiowapy-0.1.0/src/aiowapy/client.py +1285 -0
- aiowapy-0.1.0/src/aiowapy/creds.py +205 -0
- aiowapy-0.1.0/src/aiowapy/crypto/__init__.py +72 -0
- aiowapy-0.1.0/src/aiowapy/crypto/media.py +107 -0
- aiowapy-0.1.0/src/aiowapy/crypto/primitives.py +173 -0
- aiowapy-0.1.0/src/aiowapy/crypto/sender_key.py +268 -0
- aiowapy-0.1.0/src/aiowapy/crypto/session.py +389 -0
- aiowapy-0.1.0/src/aiowapy/crypto/signal_proto.py +203 -0
- aiowapy-0.1.0/src/aiowapy/crypto/xeddsa.py +167 -0
- aiowapy-0.1.0/src/aiowapy/errors.py +15 -0
- aiowapy-0.1.0/src/aiowapy/events.py +128 -0
- aiowapy-0.1.0/src/aiowapy/messaging/__init__.py +33 -0
- aiowapy-0.1.0/src/aiowapy/messaging/appstate.py +279 -0
- aiowapy-0.1.0/src/aiowapy/messaging/devices.py +89 -0
- aiowapy-0.1.0/src/aiowapy/messaging/groups.py +330 -0
- aiowapy-0.1.0/src/aiowapy/messaging/history.py +123 -0
- aiowapy-0.1.0/src/aiowapy/messaging/media.py +162 -0
- aiowapy-0.1.0/src/aiowapy/messaging/messenger.py +169 -0
- aiowapy-0.1.0/src/aiowapy/messaging/real.py +177 -0
- aiowapy-0.1.0/src/aiowapy/messaging/receiving.py +123 -0
- aiowapy-0.1.0/src/aiowapy/messaging/signals.py +24 -0
- aiowapy-0.1.0/src/aiowapy/models.py +147 -0
- aiowapy-0.1.0/src/aiowapy/pairing.py +158 -0
- aiowapy-0.1.0/src/aiowapy/pb.py +141 -0
- aiowapy-0.1.0/src/aiowapy/protocol/appstate.py +543 -0
- aiowapy-0.1.0/src/aiowapy/protocol/bundles.py +130 -0
- aiowapy-0.1.0/src/aiowapy/protocol/client_payload.py +181 -0
- aiowapy-0.1.0/src/aiowapy/protocol/groups.py +251 -0
- aiowapy-0.1.0/src/aiowapy/protocol/handshake.py +119 -0
- aiowapy-0.1.0/src/aiowapy/protocol/history.py +282 -0
- aiowapy-0.1.0/src/aiowapy/protocol/media.py +114 -0
- aiowapy-0.1.0/src/aiowapy/protocol/messages.py +619 -0
- aiowapy-0.1.0/src/aiowapy/protocol/noise.py +220 -0
- aiowapy-0.1.0/src/aiowapy/protocol/pairing.py +216 -0
- aiowapy-0.1.0/src/aiowapy/protocol/pairing_code.py +267 -0
- aiowapy-0.1.0/src/aiowapy/protocol/prekeys.py +135 -0
- aiowapy-0.1.0/src/aiowapy/protocol/presence.py +95 -0
- aiowapy-0.1.0/src/aiowapy/protocol/receipts.py +164 -0
- aiowapy-0.1.0/src/aiowapy/protocol/stream.py +145 -0
- aiowapy-0.1.0/src/aiowapy/protocol/transport.py +182 -0
- aiowapy-0.1.0/src/aiowapy/protocol/usync.py +94 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/__init__.py +20 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/decode.py +204 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/encode.py +184 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/jid.py +79 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/node.py +14 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/tokens.json +1294 -0
- aiowapy-0.1.0/src/aiowapy/protocol/wabinary/tokens.py +63 -0
- aiowapy-0.1.0/src/aiowapy/qr/__init__.py +8 -0
- aiowapy-0.1.0/src/aiowapy/qr/encoder.py +402 -0
- aiowapy-0.1.0/src/aiowapy/qr/render.py +67 -0
- aiowapy-0.1.0/src/aiowapy/qr/tables.json +1 -0
- aiowapy-0.1.0/src/aiowapy/router.py +182 -0
- aiowapy-0.1.0/src/aiowapy/session.py +619 -0
- aiowapy-0.1.0/src/aiowapy/store/__init__.py +7 -0
- aiowapy-0.1.0/src/aiowapy/store/migrations/0001_init.sql +37 -0
- aiowapy-0.1.0/src/aiowapy/store/migrations/0002_groups.sql +12 -0
- aiowapy-0.1.0/src/aiowapy/store/migrations/0003_app_state_data.sql +8 -0
- aiowapy-0.1.0/src/aiowapy/store/migrations/0004_peers_messages.sql +25 -0
- aiowapy-0.1.0/src/aiowapy/store/migrations/0005_reactions.sql +14 -0
- aiowapy-0.1.0/src/aiowapy/store/migrations/0006_lid.sql +6 -0
- aiowapy-0.1.0/src/aiowapy/store/sqlite.py +630 -0
- aiowapy-0.1.0/tests/fixtures/protocol/client_payload_login.hex +1 -0
- aiowapy-0.1.0/tests/fixtures/protocol/client_payload_registration.hex +1 -0
- aiowapy-0.1.0/tests/fixtures/signal/protobuf.json +22 -0
- aiowapy-0.1.0/tests/fixtures/signal/xeddsa.json +38 -0
- aiowapy-0.1.0/tests/fixtures/wabinary/nodes.json +3862 -0
- aiowapy-0.1.0/tests/test_import_graph.py +75 -0
- aiowapy-0.1.0/tests/unit/test_appstate.py +379 -0
- aiowapy-0.1.0/tests/unit/test_client.py +1028 -0
- aiowapy-0.1.0/tests/unit/test_creds.py +109 -0
- aiowapy-0.1.0/tests/unit/test_crypto_media.py +103 -0
- aiowapy-0.1.0/tests/unit/test_crypto_primitives.py +150 -0
- aiowapy-0.1.0/tests/unit/test_crypto_sender_key.py +141 -0
- aiowapy-0.1.0/tests/unit/test_crypto_session.py +155 -0
- aiowapy-0.1.0/tests/unit/test_crypto_signal_proto.py +126 -0
- aiowapy-0.1.0/tests/unit/test_crypto_xeddsa.py +92 -0
- aiowapy-0.1.0/tests/unit/test_groups.py +415 -0
- aiowapy-0.1.0/tests/unit/test_history.py +175 -0
- aiowapy-0.1.0/tests/unit/test_media.py +252 -0
- aiowapy-0.1.0/tests/unit/test_messages_extras.py +84 -0
- aiowapy-0.1.0/tests/unit/test_messaging.py +210 -0
- aiowapy-0.1.0/tests/unit/test_messaging_real.py +457 -0
- aiowapy-0.1.0/tests/unit/test_pairing_source.py +66 -0
- aiowapy-0.1.0/tests/unit/test_protocol_client_payload.py +106 -0
- aiowapy-0.1.0/tests/unit/test_protocol_handshake.py +74 -0
- aiowapy-0.1.0/tests/unit/test_protocol_messages.py +161 -0
- aiowapy-0.1.0/tests/unit/test_protocol_noise.py +233 -0
- aiowapy-0.1.0/tests/unit/test_protocol_pairing.py +202 -0
- aiowapy-0.1.0/tests/unit/test_protocol_pairing_code.py +255 -0
- aiowapy-0.1.0/tests/unit/test_protocol_prekeys.py +113 -0
- aiowapy-0.1.0/tests/unit/test_protocol_presence.py +120 -0
- aiowapy-0.1.0/tests/unit/test_protocol_receipts.py +95 -0
- aiowapy-0.1.0/tests/unit/test_protocol_stream.py +98 -0
- aiowapy-0.1.0/tests/unit/test_protocol_transport.py +72 -0
- aiowapy-0.1.0/tests/unit/test_qr_encoder.py +122 -0
- aiowapy-0.1.0/tests/unit/test_qr_render.py +120 -0
- aiowapy-0.1.0/tests/unit/test_router.py +179 -0
- aiowapy-0.1.0/tests/unit/test_session.py +490 -0
- aiowapy-0.1.0/tests/unit/test_store_creds.py +71 -0
- aiowapy-0.1.0/tests/unit/test_store_durability.py +93 -0
- aiowapy-0.1.0/tests/unit/test_store_messages.py +160 -0
- aiowapy-0.1.0/tests/unit/test_store_prekeys.py +54 -0
- aiowapy-0.1.0/tests/unit/test_store_schema.py +83 -0
- aiowapy-0.1.0/tests/unit/test_store_sessions.py +77 -0
- aiowapy-0.1.0/tests/unit/test_wabinary_decode.py +76 -0
- aiowapy-0.1.0/tests/unit/test_wabinary_encode.py +60 -0
- aiowapy-0.1.0/tests/unit/test_wabinary_jid.py +86 -0
- aiowapy-0.1.0/tests/unit/test_wabinary_roundtrip.py +104 -0
- aiowapy-0.1.0/tests/unit/test_wabinary_tokens.py +39 -0
- aiowapy-0.1.0/uv.lock +1580 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync --group dev
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run mypy src
|
|
24
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: ["docs/**", "mkdocs.yml", "src/**"]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
deploy:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
- run: uv sync --group docs
|
|
19
|
+
- run: uv run mkdocs gh-deploy --force
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
- run: uv build
|
|
15
|
+
- run: uvx twine check dist/*
|
|
16
|
+
- uses: actions/upload-artifact@v4
|
|
17
|
+
with:
|
|
18
|
+
name: dist
|
|
19
|
+
path: dist/
|
|
20
|
+
|
|
21
|
+
pypi:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
environment:
|
|
25
|
+
name: pypi
|
|
26
|
+
url: https://pypi.org/p/aiowapy
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/download-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
aiowapy-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
|
|
12
|
+
# Tooling caches
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.hypothesis/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
node_modules/
|
|
20
|
+
|
|
21
|
+
# Secrets / runtime artifacts
|
|
22
|
+
*.session
|
|
23
|
+
*.session-journal
|
|
24
|
+
*.session-wal
|
|
25
|
+
*.session-shm
|
|
26
|
+
.env
|
|
27
|
+
.env.*
|
|
28
|
+
!.env.example
|
|
29
|
+
|
|
30
|
+
# Captured traffic (never commit)
|
|
31
|
+
*.har
|
|
32
|
+
*.har.gz
|
|
33
|
+
*.pcap
|
|
34
|
+
*.pcapng
|
|
35
|
+
*.mitm
|
|
36
|
+
keys.log
|
|
37
|
+
/flows*
|
|
38
|
+
|
|
39
|
+
# Local-only debugging tools (kept on disk, never committed)
|
|
40
|
+
/tools/capture/
|
|
41
|
+
|
|
42
|
+
# Local notes and design docs (never published)
|
|
43
|
+
/notes/
|
|
44
|
+
|
|
45
|
+
# Local scratch
|
|
46
|
+
/tmp/
|
|
47
|
+
scratch/
|
|
48
|
+
|
|
49
|
+
# Built docs
|
|
50
|
+
/site/
|
aiowapy-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Agent notes
|
|
2
|
+
|
|
3
|
+
## Code review
|
|
4
|
+
|
|
5
|
+
When the user asks to review the changes, do not paste diffs into the chat.
|
|
6
|
+
Run the review window from the repository root and wait for it to exit:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
scripts/review.sh # working tree against HEAD
|
|
10
|
+
scripts/review.sh HEAD~3 # or any range git diff understands
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The command opens neovim with the diff (old | new) and a notes buffer, blocks
|
|
14
|
+
until the window closes, and prints the notes the user wrote, or
|
|
15
|
+
`no review comments`. Comments are grouped by file and hunk anchor.
|
|
16
|
+
|
|
17
|
+
Treat the printed notes as review feedback: fix the code, then re-run the
|
|
18
|
+
review if the user asks for another round. Never invent notes the user did not
|
|
19
|
+
write, and do not edit `REVIEW.md` yourself.
|
|
20
|
+
|
|
21
|
+
The window needs kitty or a tmux session; the workspace lives in
|
|
22
|
+
`/tmp/opencode/review/`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — unreleased
|
|
8
|
+
|
|
9
|
+
First public release. The protocol is implemented natively; live traffic is not yet
|
|
10
|
+
validated against production WhatsApp.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Noise XX handshake, WABinary codec, WebSocket transport, keepalive and reconnect.
|
|
15
|
+
- QR pairing and phone-number pairing, prekey publication.
|
|
16
|
+
- Signal: X3DH, double ratchet, session serialization, prekey and one-time prekey
|
|
17
|
+
sessions.
|
|
18
|
+
- Sending: text, replies, reactions, edits, revokes; images, video, audio, voice notes,
|
|
19
|
+
documents, stickers.
|
|
20
|
+
- Receiving: pairwise and sender-key decryption, automatic delivery receipts, retry
|
|
21
|
+
receipts in both directions.
|
|
22
|
+
- Groups: info, create, participants, leave, sender keys with out-of-order handling.
|
|
23
|
+
- Media: encrypted upload and download with hash verification.
|
|
24
|
+
- App state: patch sync, MAC verification, LTHash, mute/pin/mark-read/push-name.
|
|
25
|
+
- History sync: encrypted chunk download, unpacking, storage.
|
|
26
|
+
- Storage in one `.session` file: chats, contacts, messages, reactions, LID mappings.
|
|
27
|
+
- Filter router with `F` helpers, events, reconnect lifecycle.
|
aiowapy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yarfer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
aiowapy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aiowapy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async WhatsApp Multi-Device client: Noise, Signal and WABinary implemented natively in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/Yarfer800/aiowapy
|
|
6
|
+
Project-URL: Documentation, https://yarfer800.github.io/aiowapy/
|
|
7
|
+
Project-URL: Repository, https://github.com/Yarfer800/aiowapy
|
|
8
|
+
Project-URL: Issues, https://github.com/Yarfer800/aiowapy/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Yarfer800/aiowapy/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Yarfer <yarfer@rs.me>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: async,asyncio,bot,messaging,noise,signal,whatsapp,whatsapp-web
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Natural Language :: English
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Communications :: Chat
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: aiosqlite>=0.20
|
|
28
|
+
Requires-Dist: cryptography>=42
|
|
29
|
+
Requires-Dist: protobuf>=5
|
|
30
|
+
Requires-Dist: websockets>=15
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# aiowapy
|
|
34
|
+
|
|
35
|
+
[](https://github.com/Yarfer800/aiowapy/actions/workflows/ci.yml)
|
|
36
|
+
[](https://yarfer800.github.io/aiowapy/)
|
|
37
|
+
[](https://pypi.org/project/aiowapy/)
|
|
38
|
+
[](https://pypi.org/project/aiowapy/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
An asynchronous WhatsApp Multi-Device client for Python: a native implementation of the
|
|
42
|
+
protocol (Noise + Signal + WABinary), no official Cloud API involved.
|
|
43
|
+
|
|
44
|
+
> ⚠️ **Unofficial client.** Using it violates WhatsApp's Terms of Service and the number
|
|
45
|
+
> may be banned. Use a test number and proceed at your own risk.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install aiowapy
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The distribution is called **aiowapy**, the import is **`aiowapy`**:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from aiowapy import Client
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Full documentation: **https://yarfer800.github.io/aiowapy/**
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
|
|
66
|
+
from aiowapy import Client
|
|
67
|
+
from aiowapy.events import MessageEvent
|
|
68
|
+
from aiowapy.router import F
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def main() -> None:
|
|
72
|
+
async with Client("bot.session") as client:
|
|
73
|
+
print("connected as", client.info.jid)
|
|
74
|
+
|
|
75
|
+
@client.router.message(F.text.startswith("/ping"))
|
|
76
|
+
async def ping(event: MessageEvent) -> None:
|
|
77
|
+
await client.send_text(event.chat_jid, "pong", quote=event.message_id)
|
|
78
|
+
|
|
79
|
+
await client.run()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The first run needs linking: scan the QR code (`qr_callback`) or use
|
|
86
|
+
`ClientSession.pair_with_phone_number` (see `examples/pair_code.py`). After that the
|
|
87
|
+
session file keeps the keys and later runs only log in.
|
|
88
|
+
|
|
89
|
+
## One session file, like Pyrogram
|
|
90
|
+
|
|
91
|
+
`.session` is SQLite and holds everything: credentials, Signal sessions, prekeys,
|
|
92
|
+
app state, **chats, contacts and messages**.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
history = await client.get_chat_history(chat_jid, limit=50) # newest first
|
|
96
|
+
older = await client.get_chat_history(chat_jid, limit=50, before_timestamp=history[-1].timestamp)
|
|
97
|
+
|
|
98
|
+
chats = await client.get_chats() # conversations, newest first
|
|
99
|
+
users = await client.get_users() # contacts we have seen
|
|
100
|
+
chat = await client.get_chat(jid) # Peer: .name, .push_name, .contact_name
|
|
101
|
+
await client.set_contact_name(jid, "Mom")
|
|
102
|
+
await client.download_stored(history[0]) # attachment of a stored message
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Messages arrive from three places: live traffic, `history sync` chunks sent by the
|
|
106
|
+
phone, and everything the bot sends. All of it lands in the same table, so
|
|
107
|
+
`get_chat_history` returns a real conversation, not just what the process saw.
|
|
108
|
+
|
|
109
|
+
## Sending
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
await client.send_text(jid, "hello")
|
|
113
|
+
await client.send_text(jid, "with a reply", quote=message_id)
|
|
114
|
+
await client.send_image(jid, picture_bytes, caption="look")
|
|
115
|
+
await client.send_document(jid, pdf_bytes, file_name="report.pdf", mime_type="application/pdf")
|
|
116
|
+
await client.send_audio(jid, voice_bytes, voice_note=True)
|
|
117
|
+
await client.send_reaction(jid, message_id, "\U0001f44d")
|
|
118
|
+
await client.edit_message(jid, message_id, "fixed text")
|
|
119
|
+
await client.revoke_message(jid, message_id)
|
|
120
|
+
await client.send_text("120363000000000001@g.us", "hello group") # groups just work
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`send_*` returns `SendResult(message_id, sent, failed, acked)`: a message counts as
|
|
124
|
+
delivered when at least one device accepted it; per-device failures are listed, not
|
|
125
|
+
raised.
|
|
126
|
+
|
|
127
|
+
## Receiving
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from aiowapy.router import F
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@client.router.message(F.text.contains("invoice") & F.chat("1203630@g.us"))
|
|
134
|
+
async def handle(event: MessageEvent) -> None:
|
|
135
|
+
await client.mark_read(event)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@client.router.message(F.media("image"))
|
|
139
|
+
async def on_image(event: MessageEvent) -> None:
|
|
140
|
+
data = await client.download_media(event.media)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Events: `MessageEvent`, `ReactionEvent`, `EditEvent`, `RevokeEvent`, `HistorySyncEvent`,
|
|
144
|
+
`ReceiptEvent`, `PresenceEvent`, `ChatStateEvent`, `ConnectedEvent`,
|
|
145
|
+
`DisconnectedEvent`, `NotificationEvent`.
|
|
146
|
+
|
|
147
|
+
Presence and typing:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
await client.send_presence(True)
|
|
151
|
+
await client.subscribe_presence(jid)
|
|
152
|
+
await client.send_typing(jid, composing=True)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Groups
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
info = await client.group_info(group_jid)
|
|
159
|
+
group = await client.create_group("Team", [jid1, jid2])
|
|
160
|
+
await client.update_group_participants(group, "add", [jid3])
|
|
161
|
+
await client.leave_group(group)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A group message is encrypted once with a sender key; the key distribution message
|
|
165
|
+
travels to every member device, exactly as the reference client does it.
|
|
166
|
+
|
|
167
|
+
## App state
|
|
168
|
+
|
|
169
|
+
Contacts, mutes, pins and read markers live in WhatsApp's app state. Keys arrive from
|
|
170
|
+
the phone; after that:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
await client.sync_app_state()
|
|
174
|
+
await client.mute_chat(jid, True)
|
|
175
|
+
await client.pin_chat(jid, True)
|
|
176
|
+
await client.mark_chat_read(jid, True)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Coverage
|
|
180
|
+
|
|
181
|
+
| Layer | Module |
|
|
182
|
+
|---|---|
|
|
183
|
+
| Noise handshake, framing | `aiowapy.protocol.noise`, `aiowapy.protocol.transport` |
|
|
184
|
+
| Pairing (QR and code) | `aiowapy.protocol.pairing`, `aiowapy.protocol.pairing_code` |
|
|
185
|
+
| WABinary codec | `aiowapy.protocol.wabinary` |
|
|
186
|
+
| Signal (X3DH, double ratchet) | `aiowapy.crypto.session` |
|
|
187
|
+
| Sending: text, media, groups | `aiowapy.messaging` |
|
|
188
|
+
| Receiving and decryption | `aiowapy.messaging.receiving` |
|
|
189
|
+
| Receipts, retry requests | `aiowapy.protocol.receipts`, `aiowapy.client` |
|
|
190
|
+
| Presence and typing | `aiowapy.protocol.presence` |
|
|
191
|
+
| Groups (sender keys) | `aiowapy.messaging.groups` |
|
|
192
|
+
| Media upload and download | `aiowapy.messaging.media` |
|
|
193
|
+
| App state sync | `aiowapy.messaging.appstate` |
|
|
194
|
+
| History sync | `aiowapy.messaging.history` |
|
|
195
|
+
| Chats, contacts, messages | `aiowapy.store` |
|
|
196
|
+
| Reactions, replies, edits, revokes | `aiowapy.protocol.messages` |
|
|
197
|
+
|
|
198
|
+
Not implemented: calls, channels (newsletters), statuses, polls and buttons,
|
|
199
|
+
LID-based encryption (LID mappings are stored and exposed, but sessions still start
|
|
200
|
+
on phone-number devices).
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT.
|
aiowapy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# aiowapy
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Yarfer800/aiowapy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://yarfer800.github.io/aiowapy/)
|
|
5
|
+
[](https://pypi.org/project/aiowapy/)
|
|
6
|
+
[](https://pypi.org/project/aiowapy/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
An asynchronous WhatsApp Multi-Device client for Python: a native implementation of the
|
|
10
|
+
protocol (Noise + Signal + WABinary), no official Cloud API involved.
|
|
11
|
+
|
|
12
|
+
> ⚠️ **Unofficial client.** Using it violates WhatsApp's Terms of Service and the number
|
|
13
|
+
> may be banned. Use a test number and proceed at your own risk.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install aiowapy
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The distribution is called **aiowapy**, the import is **`aiowapy`**:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from aiowapy import Client
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Full documentation: **https://yarfer800.github.io/aiowapy/**
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
|
|
34
|
+
from aiowapy import Client
|
|
35
|
+
from aiowapy.events import MessageEvent
|
|
36
|
+
from aiowapy.router import F
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def main() -> None:
|
|
40
|
+
async with Client("bot.session") as client:
|
|
41
|
+
print("connected as", client.info.jid)
|
|
42
|
+
|
|
43
|
+
@client.router.message(F.text.startswith("/ping"))
|
|
44
|
+
async def ping(event: MessageEvent) -> None:
|
|
45
|
+
await client.send_text(event.chat_jid, "pong", quote=event.message_id)
|
|
46
|
+
|
|
47
|
+
await client.run()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The first run needs linking: scan the QR code (`qr_callback`) or use
|
|
54
|
+
`ClientSession.pair_with_phone_number` (see `examples/pair_code.py`). After that the
|
|
55
|
+
session file keeps the keys and later runs only log in.
|
|
56
|
+
|
|
57
|
+
## One session file, like Pyrogram
|
|
58
|
+
|
|
59
|
+
`.session` is SQLite and holds everything: credentials, Signal sessions, prekeys,
|
|
60
|
+
app state, **chats, contacts and messages**.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
history = await client.get_chat_history(chat_jid, limit=50) # newest first
|
|
64
|
+
older = await client.get_chat_history(chat_jid, limit=50, before_timestamp=history[-1].timestamp)
|
|
65
|
+
|
|
66
|
+
chats = await client.get_chats() # conversations, newest first
|
|
67
|
+
users = await client.get_users() # contacts we have seen
|
|
68
|
+
chat = await client.get_chat(jid) # Peer: .name, .push_name, .contact_name
|
|
69
|
+
await client.set_contact_name(jid, "Mom")
|
|
70
|
+
await client.download_stored(history[0]) # attachment of a stored message
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Messages arrive from three places: live traffic, `history sync` chunks sent by the
|
|
74
|
+
phone, and everything the bot sends. All of it lands in the same table, so
|
|
75
|
+
`get_chat_history` returns a real conversation, not just what the process saw.
|
|
76
|
+
|
|
77
|
+
## Sending
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
await client.send_text(jid, "hello")
|
|
81
|
+
await client.send_text(jid, "with a reply", quote=message_id)
|
|
82
|
+
await client.send_image(jid, picture_bytes, caption="look")
|
|
83
|
+
await client.send_document(jid, pdf_bytes, file_name="report.pdf", mime_type="application/pdf")
|
|
84
|
+
await client.send_audio(jid, voice_bytes, voice_note=True)
|
|
85
|
+
await client.send_reaction(jid, message_id, "\U0001f44d")
|
|
86
|
+
await client.edit_message(jid, message_id, "fixed text")
|
|
87
|
+
await client.revoke_message(jid, message_id)
|
|
88
|
+
await client.send_text("120363000000000001@g.us", "hello group") # groups just work
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`send_*` returns `SendResult(message_id, sent, failed, acked)`: a message counts as
|
|
92
|
+
delivered when at least one device accepted it; per-device failures are listed, not
|
|
93
|
+
raised.
|
|
94
|
+
|
|
95
|
+
## Receiving
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from aiowapy.router import F
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@client.router.message(F.text.contains("invoice") & F.chat("1203630@g.us"))
|
|
102
|
+
async def handle(event: MessageEvent) -> None:
|
|
103
|
+
await client.mark_read(event)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@client.router.message(F.media("image"))
|
|
107
|
+
async def on_image(event: MessageEvent) -> None:
|
|
108
|
+
data = await client.download_media(event.media)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Events: `MessageEvent`, `ReactionEvent`, `EditEvent`, `RevokeEvent`, `HistorySyncEvent`,
|
|
112
|
+
`ReceiptEvent`, `PresenceEvent`, `ChatStateEvent`, `ConnectedEvent`,
|
|
113
|
+
`DisconnectedEvent`, `NotificationEvent`.
|
|
114
|
+
|
|
115
|
+
Presence and typing:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
await client.send_presence(True)
|
|
119
|
+
await client.subscribe_presence(jid)
|
|
120
|
+
await client.send_typing(jid, composing=True)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Groups
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
info = await client.group_info(group_jid)
|
|
127
|
+
group = await client.create_group("Team", [jid1, jid2])
|
|
128
|
+
await client.update_group_participants(group, "add", [jid3])
|
|
129
|
+
await client.leave_group(group)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A group message is encrypted once with a sender key; the key distribution message
|
|
133
|
+
travels to every member device, exactly as the reference client does it.
|
|
134
|
+
|
|
135
|
+
## App state
|
|
136
|
+
|
|
137
|
+
Contacts, mutes, pins and read markers live in WhatsApp's app state. Keys arrive from
|
|
138
|
+
the phone; after that:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
await client.sync_app_state()
|
|
142
|
+
await client.mute_chat(jid, True)
|
|
143
|
+
await client.pin_chat(jid, True)
|
|
144
|
+
await client.mark_chat_read(jid, True)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Coverage
|
|
148
|
+
|
|
149
|
+
| Layer | Module |
|
|
150
|
+
|---|---|
|
|
151
|
+
| Noise handshake, framing | `aiowapy.protocol.noise`, `aiowapy.protocol.transport` |
|
|
152
|
+
| Pairing (QR and code) | `aiowapy.protocol.pairing`, `aiowapy.protocol.pairing_code` |
|
|
153
|
+
| WABinary codec | `aiowapy.protocol.wabinary` |
|
|
154
|
+
| Signal (X3DH, double ratchet) | `aiowapy.crypto.session` |
|
|
155
|
+
| Sending: text, media, groups | `aiowapy.messaging` |
|
|
156
|
+
| Receiving and decryption | `aiowapy.messaging.receiving` |
|
|
157
|
+
| Receipts, retry requests | `aiowapy.protocol.receipts`, `aiowapy.client` |
|
|
158
|
+
| Presence and typing | `aiowapy.protocol.presence` |
|
|
159
|
+
| Groups (sender keys) | `aiowapy.messaging.groups` |
|
|
160
|
+
| Media upload and download | `aiowapy.messaging.media` |
|
|
161
|
+
| App state sync | `aiowapy.messaging.appstate` |
|
|
162
|
+
| History sync | `aiowapy.messaging.history` |
|
|
163
|
+
| Chats, contacts, messages | `aiowapy.store` |
|
|
164
|
+
| Reactions, replies, edits, revokes | `aiowapy.protocol.messages` |
|
|
165
|
+
|
|
166
|
+
Not implemented: calls, channels (newsletters), statuses, polls and buttons,
|
|
167
|
+
LID-based encryption (LID mappings are stored and exposed, but sessions still start
|
|
168
|
+
on phone-number devices).
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# App state
|
|
2
|
+
|
|
3
|
+
Contacts, mutes, pins, archives and read markers are not messages: they live in a
|
|
4
|
+
key-value store synchronized through patches. The phone owns the keys and shares them
|
|
5
|
+
with linked devices; patches are encrypted with those keys, chained by an additive
|
|
6
|
+
integrity hash.
|
|
7
|
+
|
|
8
|
+
## Sync
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
results = await client.sync_app_state()
|
|
12
|
+
for result in results:
|
|
13
|
+
print(result.name, result.version, result.applied, result.skipped)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Each collection (`critical_block`, `critical_unblock_low`, `regular`, `regular_high`,
|
|
17
|
+
`regular_low`) is fetched from the stored version, verified and applied. Unknown
|
|
18
|
+
mutations are counted as `skipped` instead of failing the whole patch.
|
|
19
|
+
|
|
20
|
+
What is verified:
|
|
21
|
+
|
|
22
|
+
| Check | Detail |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Value MAC | HMAC-SHA512 over operation, key id and ciphertext, truncated |
|
|
25
|
+
| Index MAC | HMAC-SHA256 of the JSON index |
|
|
26
|
+
| Patch MAC | snapshot MAC, value MACs, version and collection name |
|
|
27
|
+
| Integrity | LTHash: 128-byte additive hash over `WhatsApp Patch Integrity` |
|
|
28
|
+
|
|
29
|
+
Local copy:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
entries = await client.app_state_entries("regular_high")
|
|
33
|
+
for index_json, value, timestamp in entries:
|
|
34
|
+
print(index_json, timestamp)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Writing
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
await client.mute_chat(jid, True) # mute forever
|
|
41
|
+
await client.pin_chat(jid, True)
|
|
42
|
+
await client.mark_chat_read(jid, True)
|
|
43
|
+
await client.set_push_name("bot")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each call builds one patch, updates the integrity hash and pushes it. The reply is
|
|
47
|
+
checked for a `type="error"` collection; a conflict raises `AppStateSyncError`.
|
|
48
|
+
|
|
49
|
+
## Keys
|
|
50
|
+
|
|
51
|
+
Keys arrive from the phone in a protocol message (`AppStateSyncKeyShare`). The library
|
|
52
|
+
stores them, then runs a sync in the background:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
@client.router.on(NotificationEvent)
|
|
56
|
+
async def notifications(event: NotificationEvent) -> None:
|
|
57
|
+
print(event.kind)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If a key is missing when a patch arrives, syncing that collection fails with
|
|
61
|
+
`AppStateSyncError`; the local copy stays untouched.
|