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.
Files changed (151) hide show
  1. aiowapy-0.1.0/.github/workflows/ci.yml +24 -0
  2. aiowapy-0.1.0/.github/workflows/docs.yml +19 -0
  3. aiowapy-0.1.0/.github/workflows/workflow.yml +34 -0
  4. aiowapy-0.1.0/.gitignore +50 -0
  5. aiowapy-0.1.0/AGENTS.md +22 -0
  6. aiowapy-0.1.0/CHANGELOG.md +27 -0
  7. aiowapy-0.1.0/LICENSE +21 -0
  8. aiowapy-0.1.0/PKG-INFO +204 -0
  9. aiowapy-0.1.0/README.md +172 -0
  10. aiowapy-0.1.0/docs/api/client.md +11 -0
  11. aiowapy-0.1.0/docs/api/events.md +3 -0
  12. aiowapy-0.1.0/docs/api/models.md +3 -0
  13. aiowapy-0.1.0/docs/api/router.md +3 -0
  14. aiowapy-0.1.0/docs/api/store.md +3 -0
  15. aiowapy-0.1.0/docs/appstate.md +61 -0
  16. aiowapy-0.1.0/docs/groups.md +70 -0
  17. aiowapy-0.1.0/docs/index.md +76 -0
  18. aiowapy-0.1.0/docs/media.md +80 -0
  19. aiowapy-0.1.0/docs/messages.md +67 -0
  20. aiowapy-0.1.0/docs/presence.md +37 -0
  21. aiowapy-0.1.0/docs/protocol.md +68 -0
  22. aiowapy-0.1.0/docs/quickstart.md +98 -0
  23. aiowapy-0.1.0/docs/receiving.md +102 -0
  24. aiowapy-0.1.0/docs/storage.md +103 -0
  25. aiowapy-0.1.0/examples/pair.py +69 -0
  26. aiowapy-0.1.0/examples/pair_code.py +62 -0
  27. aiowapy-0.1.0/examples/pairing_qr_demo.py +60 -0
  28. aiowapy-0.1.0/mkdocs.yml +80 -0
  29. aiowapy-0.1.0/pyproject.toml +80 -0
  30. aiowapy-0.1.0/scripts/fixtures/gen_signal_fixtures.mjs +79 -0
  31. aiowapy-0.1.0/scripts/fixtures/gen_wabinary_fixtures.mjs +104 -0
  32. aiowapy-0.1.0/scripts/fixtures/package-lock.json +1428 -0
  33. aiowapy-0.1.0/scripts/fixtures/package.json +12 -0
  34. aiowapy-0.1.0/scripts/fixtures/tmp_login.mjs +12 -0
  35. aiowapy-0.1.0/scripts/fixtures/tmp_trace.mjs +38 -0
  36. aiowapy-0.1.0/scripts/review.lua +381 -0
  37. aiowapy-0.1.0/scripts/review.sh +126 -0
  38. aiowapy-0.1.0/scripts/sync_qr_tables.py +57 -0
  39. aiowapy-0.1.0/scripts/sync_wabinary_tokens.py +74 -0
  40. aiowapy-0.1.0/src/aiowapy/__init__.py +39 -0
  41. aiowapy-0.1.0/src/aiowapy/client.py +1285 -0
  42. aiowapy-0.1.0/src/aiowapy/creds.py +205 -0
  43. aiowapy-0.1.0/src/aiowapy/crypto/__init__.py +72 -0
  44. aiowapy-0.1.0/src/aiowapy/crypto/media.py +107 -0
  45. aiowapy-0.1.0/src/aiowapy/crypto/primitives.py +173 -0
  46. aiowapy-0.1.0/src/aiowapy/crypto/sender_key.py +268 -0
  47. aiowapy-0.1.0/src/aiowapy/crypto/session.py +389 -0
  48. aiowapy-0.1.0/src/aiowapy/crypto/signal_proto.py +203 -0
  49. aiowapy-0.1.0/src/aiowapy/crypto/xeddsa.py +167 -0
  50. aiowapy-0.1.0/src/aiowapy/errors.py +15 -0
  51. aiowapy-0.1.0/src/aiowapy/events.py +128 -0
  52. aiowapy-0.1.0/src/aiowapy/messaging/__init__.py +33 -0
  53. aiowapy-0.1.0/src/aiowapy/messaging/appstate.py +279 -0
  54. aiowapy-0.1.0/src/aiowapy/messaging/devices.py +89 -0
  55. aiowapy-0.1.0/src/aiowapy/messaging/groups.py +330 -0
  56. aiowapy-0.1.0/src/aiowapy/messaging/history.py +123 -0
  57. aiowapy-0.1.0/src/aiowapy/messaging/media.py +162 -0
  58. aiowapy-0.1.0/src/aiowapy/messaging/messenger.py +169 -0
  59. aiowapy-0.1.0/src/aiowapy/messaging/real.py +177 -0
  60. aiowapy-0.1.0/src/aiowapy/messaging/receiving.py +123 -0
  61. aiowapy-0.1.0/src/aiowapy/messaging/signals.py +24 -0
  62. aiowapy-0.1.0/src/aiowapy/models.py +147 -0
  63. aiowapy-0.1.0/src/aiowapy/pairing.py +158 -0
  64. aiowapy-0.1.0/src/aiowapy/pb.py +141 -0
  65. aiowapy-0.1.0/src/aiowapy/protocol/appstate.py +543 -0
  66. aiowapy-0.1.0/src/aiowapy/protocol/bundles.py +130 -0
  67. aiowapy-0.1.0/src/aiowapy/protocol/client_payload.py +181 -0
  68. aiowapy-0.1.0/src/aiowapy/protocol/groups.py +251 -0
  69. aiowapy-0.1.0/src/aiowapy/protocol/handshake.py +119 -0
  70. aiowapy-0.1.0/src/aiowapy/protocol/history.py +282 -0
  71. aiowapy-0.1.0/src/aiowapy/protocol/media.py +114 -0
  72. aiowapy-0.1.0/src/aiowapy/protocol/messages.py +619 -0
  73. aiowapy-0.1.0/src/aiowapy/protocol/noise.py +220 -0
  74. aiowapy-0.1.0/src/aiowapy/protocol/pairing.py +216 -0
  75. aiowapy-0.1.0/src/aiowapy/protocol/pairing_code.py +267 -0
  76. aiowapy-0.1.0/src/aiowapy/protocol/prekeys.py +135 -0
  77. aiowapy-0.1.0/src/aiowapy/protocol/presence.py +95 -0
  78. aiowapy-0.1.0/src/aiowapy/protocol/receipts.py +164 -0
  79. aiowapy-0.1.0/src/aiowapy/protocol/stream.py +145 -0
  80. aiowapy-0.1.0/src/aiowapy/protocol/transport.py +182 -0
  81. aiowapy-0.1.0/src/aiowapy/protocol/usync.py +94 -0
  82. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/__init__.py +20 -0
  83. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/decode.py +204 -0
  84. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/encode.py +184 -0
  85. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/jid.py +79 -0
  86. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/node.py +14 -0
  87. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/tokens.json +1294 -0
  88. aiowapy-0.1.0/src/aiowapy/protocol/wabinary/tokens.py +63 -0
  89. aiowapy-0.1.0/src/aiowapy/qr/__init__.py +8 -0
  90. aiowapy-0.1.0/src/aiowapy/qr/encoder.py +402 -0
  91. aiowapy-0.1.0/src/aiowapy/qr/render.py +67 -0
  92. aiowapy-0.1.0/src/aiowapy/qr/tables.json +1 -0
  93. aiowapy-0.1.0/src/aiowapy/router.py +182 -0
  94. aiowapy-0.1.0/src/aiowapy/session.py +619 -0
  95. aiowapy-0.1.0/src/aiowapy/store/__init__.py +7 -0
  96. aiowapy-0.1.0/src/aiowapy/store/migrations/0001_init.sql +37 -0
  97. aiowapy-0.1.0/src/aiowapy/store/migrations/0002_groups.sql +12 -0
  98. aiowapy-0.1.0/src/aiowapy/store/migrations/0003_app_state_data.sql +8 -0
  99. aiowapy-0.1.0/src/aiowapy/store/migrations/0004_peers_messages.sql +25 -0
  100. aiowapy-0.1.0/src/aiowapy/store/migrations/0005_reactions.sql +14 -0
  101. aiowapy-0.1.0/src/aiowapy/store/migrations/0006_lid.sql +6 -0
  102. aiowapy-0.1.0/src/aiowapy/store/sqlite.py +630 -0
  103. aiowapy-0.1.0/tests/fixtures/protocol/client_payload_login.hex +1 -0
  104. aiowapy-0.1.0/tests/fixtures/protocol/client_payload_registration.hex +1 -0
  105. aiowapy-0.1.0/tests/fixtures/signal/protobuf.json +22 -0
  106. aiowapy-0.1.0/tests/fixtures/signal/xeddsa.json +38 -0
  107. aiowapy-0.1.0/tests/fixtures/wabinary/nodes.json +3862 -0
  108. aiowapy-0.1.0/tests/test_import_graph.py +75 -0
  109. aiowapy-0.1.0/tests/unit/test_appstate.py +379 -0
  110. aiowapy-0.1.0/tests/unit/test_client.py +1028 -0
  111. aiowapy-0.1.0/tests/unit/test_creds.py +109 -0
  112. aiowapy-0.1.0/tests/unit/test_crypto_media.py +103 -0
  113. aiowapy-0.1.0/tests/unit/test_crypto_primitives.py +150 -0
  114. aiowapy-0.1.0/tests/unit/test_crypto_sender_key.py +141 -0
  115. aiowapy-0.1.0/tests/unit/test_crypto_session.py +155 -0
  116. aiowapy-0.1.0/tests/unit/test_crypto_signal_proto.py +126 -0
  117. aiowapy-0.1.0/tests/unit/test_crypto_xeddsa.py +92 -0
  118. aiowapy-0.1.0/tests/unit/test_groups.py +415 -0
  119. aiowapy-0.1.0/tests/unit/test_history.py +175 -0
  120. aiowapy-0.1.0/tests/unit/test_media.py +252 -0
  121. aiowapy-0.1.0/tests/unit/test_messages_extras.py +84 -0
  122. aiowapy-0.1.0/tests/unit/test_messaging.py +210 -0
  123. aiowapy-0.1.0/tests/unit/test_messaging_real.py +457 -0
  124. aiowapy-0.1.0/tests/unit/test_pairing_source.py +66 -0
  125. aiowapy-0.1.0/tests/unit/test_protocol_client_payload.py +106 -0
  126. aiowapy-0.1.0/tests/unit/test_protocol_handshake.py +74 -0
  127. aiowapy-0.1.0/tests/unit/test_protocol_messages.py +161 -0
  128. aiowapy-0.1.0/tests/unit/test_protocol_noise.py +233 -0
  129. aiowapy-0.1.0/tests/unit/test_protocol_pairing.py +202 -0
  130. aiowapy-0.1.0/tests/unit/test_protocol_pairing_code.py +255 -0
  131. aiowapy-0.1.0/tests/unit/test_protocol_prekeys.py +113 -0
  132. aiowapy-0.1.0/tests/unit/test_protocol_presence.py +120 -0
  133. aiowapy-0.1.0/tests/unit/test_protocol_receipts.py +95 -0
  134. aiowapy-0.1.0/tests/unit/test_protocol_stream.py +98 -0
  135. aiowapy-0.1.0/tests/unit/test_protocol_transport.py +72 -0
  136. aiowapy-0.1.0/tests/unit/test_qr_encoder.py +122 -0
  137. aiowapy-0.1.0/tests/unit/test_qr_render.py +120 -0
  138. aiowapy-0.1.0/tests/unit/test_router.py +179 -0
  139. aiowapy-0.1.0/tests/unit/test_session.py +490 -0
  140. aiowapy-0.1.0/tests/unit/test_store_creds.py +71 -0
  141. aiowapy-0.1.0/tests/unit/test_store_durability.py +93 -0
  142. aiowapy-0.1.0/tests/unit/test_store_messages.py +160 -0
  143. aiowapy-0.1.0/tests/unit/test_store_prekeys.py +54 -0
  144. aiowapy-0.1.0/tests/unit/test_store_schema.py +83 -0
  145. aiowapy-0.1.0/tests/unit/test_store_sessions.py +77 -0
  146. aiowapy-0.1.0/tests/unit/test_wabinary_decode.py +76 -0
  147. aiowapy-0.1.0/tests/unit/test_wabinary_encode.py +60 -0
  148. aiowapy-0.1.0/tests/unit/test_wabinary_jid.py +86 -0
  149. aiowapy-0.1.0/tests/unit/test_wabinary_roundtrip.py +104 -0
  150. aiowapy-0.1.0/tests/unit/test_wabinary_tokens.py +39 -0
  151. 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
@@ -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/
@@ -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
+ [![CI](https://github.com/Yarfer800/aiowapy/actions/workflows/ci.yml/badge.svg)](https://github.com/Yarfer800/aiowapy/actions/workflows/ci.yml)
36
+ [![Docs](https://img.shields.io/badge/docs-yarfer800.github.io%2Faiowapy-green)](https://yarfer800.github.io/aiowapy/)
37
+ [![PyPI](https://img.shields.io/pypi/v/aiowapy.svg)](https://pypi.org/project/aiowapy/)
38
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/aiowapy/)
39
+ [![License](https://img.shields.io/badge/license-MIT-green)](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.
@@ -0,0 +1,172 @@
1
+ # aiowapy
2
+
3
+ [![CI](https://github.com/Yarfer800/aiowapy/actions/workflows/ci.yml/badge.svg)](https://github.com/Yarfer800/aiowapy/actions/workflows/ci.yml)
4
+ [![Docs](https://img.shields.io/badge/docs-yarfer800.github.io%2Faiowapy-green)](https://yarfer800.github.io/aiowapy/)
5
+ [![PyPI](https://img.shields.io/pypi/v/aiowapy.svg)](https://pypi.org/project/aiowapy/)
6
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/aiowapy/)
7
+ [![License](https://img.shields.io/badge/license-MIT-green)](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,11 @@
1
+ # Client
2
+
3
+ ::: aiowapy.client
4
+ options:
5
+ members:
6
+ - Client
7
+ - ClientInfo
8
+ - ClientError
9
+ - SendResult
10
+ - OutgoingMessage
11
+ - default_mime_type
@@ -0,0 +1,3 @@
1
+ # Events
2
+
3
+ ::: aiowapy.events
@@ -0,0 +1,3 @@
1
+ # Models
2
+
3
+ ::: aiowapy.models
@@ -0,0 +1,3 @@
1
+ # Router and filters
2
+
3
+ ::: aiowapy.router
@@ -0,0 +1,3 @@
1
+ # Store
2
+
3
+ ::: aiowapy.store
@@ -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.