PGPCbot 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 (137) hide show
  1. pgpcbot-0.1.0/.gitignore +39 -0
  2. pgpcbot-0.1.0/CHANGELOG.md +31 -0
  3. pgpcbot-0.1.0/CONTRIBUTING.md +82 -0
  4. pgpcbot-0.1.0/LICENSE +21 -0
  5. pgpcbot-0.1.0/PKG-INFO +270 -0
  6. pgpcbot-0.1.0/README.md +234 -0
  7. pgpcbot-0.1.0/SECURITY.md +115 -0
  8. pgpcbot-0.1.0/docs/API_COVERAGE.md +72 -0
  9. pgpcbot-0.1.0/docs/CRYPTOGRAPHY.md +383 -0
  10. pgpcbot-0.1.0/docs/RESEARCH.md +423 -0
  11. pgpcbot-0.1.0/docs/api/bot.rst +26 -0
  12. pgpcbot-0.1.0/docs/api/client.rst +92 -0
  13. pgpcbot-0.1.0/docs/api/crypto.rst +25 -0
  14. pgpcbot-0.1.0/docs/api/errors.rst +254 -0
  15. pgpcbot-0.1.0/docs/api/index.rst +50 -0
  16. pgpcbot-0.1.0/docs/api/keyboard.rst +20 -0
  17. pgpcbot-0.1.0/docs/api/models.rst +159 -0
  18. pgpcbot-0.1.0/docs/api/routing.rst +52 -0
  19. pgpcbot-0.1.0/docs/api/storage.rst +19 -0
  20. pgpcbot-0.1.0/docs/api/webhook.rst +17 -0
  21. pgpcbot-0.1.0/docs/architecture/index.md +162 -0
  22. pgpcbot-0.1.0/docs/conf.py +96 -0
  23. pgpcbot-0.1.0/docs/deployment/index.md +153 -0
  24. pgpcbot-0.1.0/docs/examples.md +46 -0
  25. pgpcbot-0.1.0/docs/getting-started/configuration.md +135 -0
  26. pgpcbot-0.1.0/docs/getting-started/installation.md +81 -0
  27. pgpcbot-0.1.0/docs/getting-started/quickstart.md +120 -0
  28. pgpcbot-0.1.0/docs/guides/async-and-sync.md +124 -0
  29. pgpcbot-0.1.0/docs/guides/command-line.md +100 -0
  30. pgpcbot-0.1.0/docs/guides/commands-and-buttons.md +192 -0
  31. pgpcbot-0.1.0/docs/guides/errors.md +151 -0
  32. pgpcbot-0.1.0/docs/guides/events.md +326 -0
  33. pgpcbot-0.1.0/docs/guides/long-polling.md +101 -0
  34. pgpcbot-0.1.0/docs/guides/messages.md +232 -0
  35. pgpcbot-0.1.0/docs/guides/openpgp.md +195 -0
  36. pgpcbot-0.1.0/docs/guides/rate-limiting.md +116 -0
  37. pgpcbot-0.1.0/docs/guides/storage.md +115 -0
  38. pgpcbot-0.1.0/docs/guides/webhook.md +203 -0
  39. pgpcbot-0.1.0/docs/index.rst +129 -0
  40. pgpcbot-0.1.0/docs/openapi-reference/index.md +18 -0
  41. pgpcbot-0.1.0/docs/openapi-reference/operations.md +608 -0
  42. pgpcbot-0.1.0/docs/openapi.json +991 -0
  43. pgpcbot-0.1.0/docs/project/changelog.md +2 -0
  44. pgpcbot-0.1.0/docs/project/contributing.md +2 -0
  45. pgpcbot-0.1.0/docs/project/testing.md +76 -0
  46. pgpcbot-0.1.0/docs/project/versioning.md +49 -0
  47. pgpcbot-0.1.0/docs/security/index.md +2 -0
  48. pgpcbot-0.1.0/docs/troubleshooting/faq.md +85 -0
  49. pgpcbot-0.1.0/docs/troubleshooting/index.md +94 -0
  50. pgpcbot-0.1.0/examples/01_hello_world.py +20 -0
  51. pgpcbot-0.1.0/examples/02_echo_bot.py +33 -0
  52. pgpcbot-0.1.0/examples/03_commands.py +61 -0
  53. pgpcbot-0.1.0/examples/04_private_messages.py +54 -0
  54. pgpcbot-0.1.0/examples/05_group_bot.py +92 -0
  55. pgpcbot-0.1.0/examples/06_inline_buttons.py +50 -0
  56. pgpcbot-0.1.0/examples/07_callbacks.py +80 -0
  57. pgpcbot-0.1.0/examples/08_long_polling.py +45 -0
  58. pgpcbot-0.1.0/examples/09_fastapi_webhook.py +40 -0
  59. pgpcbot-0.1.0/examples/10_encrypted_bot.py +49 -0
  60. pgpcbot-0.1.0/examples/11_generate_pgp_keys.py +43 -0
  61. pgpcbot-0.1.0/examples/12_send_encrypted_message.py +46 -0
  62. pgpcbot-0.1.0/examples/13_custom_handlers.py +76 -0
  63. pgpcbot-0.1.0/examples/14_error_handling.py +78 -0
  64. pgpcbot-0.1.0/examples/15_rate_limiting.py +66 -0
  65. pgpcbot-0.1.0/examples/16_async_bot.py +52 -0
  66. pgpcbot-0.1.0/examples/17_sync_client.py +49 -0
  67. pgpcbot-0.1.0/examples/18_persistent_updates.py +45 -0
  68. pgpcbot-0.1.0/examples/19_advanced_configuration.py +58 -0
  69. pgpcbot-0.1.0/examples/20_complete_bot.py +133 -0
  70. pgpcbot-0.1.0/pyproject.toml +177 -0
  71. pgpcbot-0.1.0/src/pgpcbot/__init__.py +304 -0
  72. pgpcbot-0.1.0/src/pgpcbot/__main__.py +319 -0
  73. pgpcbot-0.1.0/src/pgpcbot/_http.py +311 -0
  74. pgpcbot-0.1.0/src/pgpcbot/_secrets.py +192 -0
  75. pgpcbot-0.1.0/src/pgpcbot/_spec.py +385 -0
  76. pgpcbot-0.1.0/src/pgpcbot/_validate.py +276 -0
  77. pgpcbot-0.1.0/src/pgpcbot/_version.py +1 -0
  78. pgpcbot-0.1.0/src/pgpcbot/bot.py +3681 -0
  79. pgpcbot-0.1.0/src/pgpcbot/client.py +2450 -0
  80. pgpcbot-0.1.0/src/pgpcbot/crypto/__init__.py +33 -0
  81. pgpcbot-0.1.0/src/pgpcbot/crypto/_lib.py +326 -0
  82. pgpcbot-0.1.0/src/pgpcbot/crypto/keys.py +1093 -0
  83. pgpcbot-0.1.0/src/pgpcbot/engine.py +547 -0
  84. pgpcbot-0.1.0/src/pgpcbot/errors.py +888 -0
  85. pgpcbot-0.1.0/src/pgpcbot/filters.py +661 -0
  86. pgpcbot-0.1.0/src/pgpcbot/keyboard.py +457 -0
  87. pgpcbot-0.1.0/src/pgpcbot/models.py +2638 -0
  88. pgpcbot-0.1.0/src/pgpcbot/py.typed +0 -0
  89. pgpcbot-0.1.0/src/pgpcbot/ratelimit.py +246 -0
  90. pgpcbot-0.1.0/src/pgpcbot/router.py +776 -0
  91. pgpcbot-0.1.0/src/pgpcbot/storage.py +1257 -0
  92. pgpcbot-0.1.0/src/pgpcbot/webhook.py +440 -0
  93. pgpcbot-0.1.0/src/pgpcbot/wire.py +157 -0
  94. pgpcbot-0.1.0/tests/__init__.py +0 -0
  95. pgpcbot-0.1.0/tests/conftest.py +103 -0
  96. pgpcbot-0.1.0/tests/fakeapi.py +538 -0
  97. pgpcbot-0.1.0/tests/fixtures/attachment.pgp +0 -0
  98. pgpcbot-0.1.0/tests/fixtures/bot.pub.asc +15 -0
  99. pgpcbot-0.1.0/tests/fixtures/bot.sec.asc +14 -0
  100. pgpcbot-0.1.0/tests/fixtures/bot_locked.sec.asc +16 -0
  101. pgpcbot-0.1.0/tests/fixtures/bot_old.pub.asc +15 -0
  102. pgpcbot-0.1.0/tests/fixtures/bot_old.sec.asc +14 -0
  103. pgpcbot-0.1.0/tests/fixtures/msg_for_old_key.asc +14 -0
  104. pgpcbot-0.1.0/tests/fixtures/msg_media.asc +16 -0
  105. pgpcbot-0.1.0/tests/fixtures/msg_not_for_bot.asc +12 -0
  106. pgpcbot-0.1.0/tests/fixtures/msg_rsa_signed.asc +27 -0
  107. pgpcbot-0.1.0/tests/fixtures/msg_text_signed.asc +14 -0
  108. pgpcbot-0.1.0/tests/fixtures/msg_text_unsigned.asc +12 -0
  109. pgpcbot-0.1.0/tests/fixtures/phone_ed.pub.asc +22 -0
  110. pgpcbot-0.1.0/tests/fixtures/phone_ed.sec.asc +24 -0
  111. pgpcbot-0.1.0/tests/fixtures/phone_rsa.pub.asc +70 -0
  112. pgpcbot-0.1.0/tests/fixtures/phone_rsa.sec.asc +130 -0
  113. pgpcbot-0.1.0/tests/interop/Interop.java +372 -0
  114. pgpcbot-0.1.0/tests/interop/__init__.py +0 -0
  115. pgpcbot-0.1.0/tests/interop/test_pgpainless.py +170 -0
  116. pgpcbot-0.1.0/tests/test_bot.py +2265 -0
  117. pgpcbot-0.1.0/tests/test_cli.py +469 -0
  118. pgpcbot-0.1.0/tests/test_client.py +2035 -0
  119. pgpcbot-0.1.0/tests/test_crypto.py +1115 -0
  120. pgpcbot-0.1.0/tests/test_docs.py +235 -0
  121. pgpcbot-0.1.0/tests/test_engine.py +642 -0
  122. pgpcbot-0.1.0/tests/test_errors.py +302 -0
  123. pgpcbot-0.1.0/tests/test_filters.py +487 -0
  124. pgpcbot-0.1.0/tests/test_http.py +479 -0
  125. pgpcbot-0.1.0/tests/test_keyboard.py +393 -0
  126. pgpcbot-0.1.0/tests/test_lifecycle.py +393 -0
  127. pgpcbot-0.1.0/tests/test_models.py +1229 -0
  128. pgpcbot-0.1.0/tests/test_parity.py +233 -0
  129. pgpcbot-0.1.0/tests/test_polling.py +585 -0
  130. pgpcbot-0.1.0/tests/test_ratelimit.py +312 -0
  131. pgpcbot-0.1.0/tests/test_router.py +508 -0
  132. pgpcbot-0.1.0/tests/test_secrets.py +283 -0
  133. pgpcbot-0.1.0/tests/test_storage.py +709 -0
  134. pgpcbot-0.1.0/tests/test_syncbot.py +899 -0
  135. pgpcbot-0.1.0/tests/test_validate.py +528 -0
  136. pgpcbot-0.1.0/tests/test_webhook.py +900 -0
  137. pgpcbot-0.1.0/tests/test_wire.py +269 -0
@@ -0,0 +1,39 @@
1
+ # environments
2
+ .venv/
3
+ venv/
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+
8
+ # python
9
+ __pycache__/
10
+ *.py[cod]
11
+ *.egg-info/
12
+ build/
13
+ dist/
14
+
15
+ # tools
16
+ .cache/
17
+ .pytest_cache/
18
+ .mypy_cache/
19
+ .ruff_cache/
20
+ .coverage
21
+ .coverage.*
22
+ htmlcov/
23
+ coverage.xml
24
+ *.log
25
+
26
+ # docs output
27
+ docs/_build/
28
+
29
+ # the tests that drive a real server live with the server, not here
30
+ /tests/integration/
31
+
32
+ # never commit these: bot keys, tokens, local storage
33
+ *.asc
34
+ !tests/fixtures/*.asc
35
+ *.key
36
+ *.db
37
+ *.db-wal
38
+ *.db-shm
39
+ *.lock
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the version
4
+ numbers follow [Semantic Versioning](https://semver.org/), with the rules in
5
+ `docs/project/versioning.md`.
6
+
7
+ ## [0.1.0] - 2026-10-11
8
+
9
+ First release.
10
+
11
+ ### Added
12
+
13
+ - `AsyncClient` and `Client`: all 22 operations of Bot API 1.0.0 (`openapi.json`), plus media
14
+ downloads, with typed models, client-side checks that match the server's, retries with
15
+ backoff and a stable `sendId`, waiting on `429`, and a client-side rate limiter.
16
+ - `Bot` and `SyncBot`: handlers for messages, commands, buttons, edited and deleted messages,
17
+ and membership changes; filters, routers, middleware, error handlers, automatic answers to
18
+ button presses; startup and shutdown hooks; `request_stop()` from a handler or another thread.
19
+ - `resolve()`, for `GET /resolve/{username}`: the handle somebody writes at a bot, turned into
20
+ an id. One pool of names covers people, groups and channels, so the answer says which it was
21
+ - `Resolved`, with `HandleKind`, a `ChatPeer` for a person or a `ChatCard` for a public group
22
+ or channel. A leading `@` and the case are taken off before the request goes out, and
23
+ `UsernameNotFoundError` (404 `username_not_found`) is what a free handle, a group with no
24
+ public name and a closed account all answer.
25
+ - OpenPGP: making, loading, protecting and rotating the bot's key; automatic encryption and
26
+ signing in protected chats; decryption and signature checks on incoming messages, including
27
+ the compressed messages phones send; reading the app's internal message format; downloading
28
+ and decrypting attachments; pinning keys the first time they are seen.
29
+ - Long polling and an ASGI webhook (FastAPI, Starlette, uvicorn) with "at least once"
30
+ delivery, a durable `SQLiteStorage` queue, and failed updates kept so they can be retried.
31
+ - The `pgpcbot` command line: `keygen`, `fingerprint`, `whoami`, `publish-key`, `webhook`.
@@ -0,0 +1,82 @@
1
+ # Contributing
2
+
3
+ ## Setting up
4
+
5
+ ```bash
6
+ python -m venv .venv
7
+ . .venv/bin/activate # Windows: .venv\Scripts\activate
8
+ pip install -e . --group dev # needs pip 25.1 or newer
9
+ ```
10
+
11
+ ## Before you open a pull request
12
+
13
+ ```bash
14
+ ruff check src tests examples scripts
15
+ ruff format --check src tests examples scripts
16
+ mypy
17
+ pytest
18
+ ```
19
+
20
+ `mypy` runs in `strict` mode on `src/pgpcbot`. Any warning during the tests is an error
21
+ (`filterwarnings = error`): if a test expects a warning, it has to say so.
22
+
23
+ ### Tests that need more
24
+
25
+ - **Integration** (`-m integration`): the tests that drive a real PGP Chat server need a copy
26
+ of the server to start one, so they live with it and not in this repository. The marker stays
27
+ declared here, for a working copy that has both.
28
+ - **Interoperability** (`-m interop`): they check the SDK against PGPainless 1.7.6, the phones'
29
+ library. You need Java 21 or newer and `PGPCBOT_PGPAINLESS_CP` set to a classpath with
30
+ pgpainless-core 1.7.6, Bouncy Castle 1.80 (bcprov, bcpg, bcutil), slf4j-api, jsr305 and
31
+ kotlin-stdlib. The CI job in `.github/workflows/ci.yml` shows how to download them.
32
+
33
+ Without those variables these tests are skipped, they don't fail.
34
+
35
+ ## How the code is organised
36
+
37
+ - `src/pgpcbot/_spec.py` describes each API operation once. `client.py` has the two shells,
38
+ async and blocking, with the same methods. A new method goes in both:
39
+ `tests/test_parity.py` checks that the signatures match.
40
+ - The client-side checks mirror the server's own rules, so a request that passes them is not
41
+ refused for that reason. When the server changes a rule, change it here too, with a
42
+ test.
43
+ - Everything that touches OpenPGP goes through `src/pgpcbot/crypto/_lib.py`. A change there
44
+ needs the interoperability tests with PGPainless, not only the unit tests.
45
+ - Comments explain why, not what. Code, comments and documentation are in English; keep the
46
+ documentation simple, for readers whose first language is not English.
47
+
48
+ ## Documentation
49
+
50
+ The API reference is generated from the docstrings, so a docstring is documentation. Write it
51
+ in Google style with reStructuredText markup, like the ones already there: a summary line,
52
+ then `Args:`, `Returns:`, `Raises:`, `Attributes:` and `Example:` sections as needed. Names in
53
+ single backticks become links (`Bot.send_message`); code and values go in double backticks.
54
+ `tests/test_docs.py` checks that everything public has a docstring, that every parameter is
55
+ described and that the `>>>` examples still run.
56
+
57
+ Build the site with Sphinx:
58
+
59
+ ```bash
60
+ pip install -e . --group docs
61
+ sphinx-build -W --keep-going -n -b html docs docs/_build/html
62
+ ```
63
+
64
+ then open `docs/_build/html/index.html`. `-n` reports every link that does not resolve and
65
+ `-W` turns warnings into errors, as in the CI. To read one page without a browser:
66
+ `python -m pydoc pgpcbot.Bot`.
67
+
68
+ The guides are Markdown pages in `docs/`. The examples in the documentation must work with the
69
+ real code: if you change an API, update the docstrings, the pages and the scripts in
70
+ `examples/` that use it.
71
+
72
+ `docs/openapi.json` is an exact copy of the document the server publishes for its Bot API. When
73
+ the server's spec changes, copy it again and run `python scripts/openapi_pages.py`: it rewrites
74
+ `docs/openapi-reference/operations.md`, and stops if an operation has no SDK method. The CI checks
75
+ that the page is up to date.
76
+
77
+ ## Releases
78
+
79
+ 1. update `src/pgpcbot/_version.py` and `CHANGELOG.md`;
80
+ 2. create a `vX.Y.Z` tag;
81
+ 3. the `release.yml` workflow builds and checks the packages. Publishing to PyPI needs a manual
82
+ approval of the `pypi` environment.
pgpcbot-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PGP Chat
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.
pgpcbot-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: PGPCbot
3
+ Version: 0.1.0
4
+ Summary: Bots for PGP Chat: the whole Bot API, end-to-end encryption included.
5
+ Project-URL: Homepage, https://pgpchat.app
6
+ Project-URL: Source, https://github.com/vntcore/PGPCbot
7
+ Project-URL: Documentation, https://github.com/vntcore/PGPCbot/tree/main/docs
8
+ Project-URL: Issues, https://github.com/vntcore/PGPCbot/issues
9
+ Project-URL: Changelog, https://github.com/vntcore/PGPCbot/blob/main/CHANGELOG.md
10
+ Author: PGP Chat
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: asyncio,bot,bot-api,chatbot,end-to-end-encryption,openpgp,pgp-chat,webhook
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Communications :: Chat
25
+ Classifier: Topic :: Security :: Cryptography
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Requires-Dist: httpx2<3,>=2.13.1
29
+ Requires-Dist: pysequoia<0.2,>=0.1.35
30
+ Requires-Dist: rpgp-py<0.21,>=0.20
31
+ Provides-Extra: http2
32
+ Requires-Dist: httpx2[http2]<3,>=2.13.1; extra == 'http2'
33
+ Provides-Extra: webhook
34
+ Requires-Dist: uvicorn>=0.30; extra == 'webhook'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # PGPCbot
38
+
39
+ [![ci](https://github.com/vntcore/PGPCbot/actions/workflows/ci.yml/badge.svg)](https://github.com/vntcore/PGPCbot/actions/workflows/ci.yml)
40
+
41
+ A Python library for writing bots for [PGP Chat](https://pgpchat.app), a messenger whose
42
+ private chats are end-to-end encrypted with OpenPGP. It covers the whole Bot API, and it
43
+ handles the encryption for you.
44
+
45
+ ```python
46
+ from pgpcbot import Bot
47
+
48
+ bot = Bot() # reads the token from PGPCHAT_BOT_TOKEN
49
+
50
+
51
+ @bot.command("start", description="Say hello")
52
+ async def start(message):
53
+ await message.reply("Hello! Send me anything and I will repeat it.")
54
+
55
+
56
+ @bot.on_message()
57
+ async def echo(message):
58
+ await message.reply(f"You said: {message.text}")
59
+
60
+
61
+ bot.run()
62
+ ```
63
+
64
+ If the bot has a published OpenPGP key, its private chats are end-to-end encrypted. The same
65
+ code still works: messages reach your handlers already decrypted, and replies are encrypted
66
+ for the people who should read them. You don't change anything in the handlers.
67
+
68
+ ## What it does
69
+
70
+ - **Every operation of the Bot API**, with typed models, as async calls (`AsyncClient`)
71
+ and as blocking calls (`Client`).
72
+ - **A high-level bot** with handlers for messages, commands (`/start` and `/start@yourbot`),
73
+ button presses, edited and deleted messages, and changes to the bot's membership. Filters you
74
+ can combine, routers to split a bot into modules, middleware, and one place to handle errors.
75
+ - **Automatic OpenPGP**: incoming messages are decrypted and their signatures checked;
76
+ replies are encrypted for every member of the chat and signed. A message that should be
77
+ encrypted is never sent in clear. Tested with real keys and messages from the phones'
78
+ library (PGPainless 1.7.6).
79
+ - **Long polling and webhooks** with "at least once" delivery done right: no update is lost
80
+ when a handler fails, no update is confirmed to the server before it is safe, and a SQLite
81
+ queue survives restarts.
82
+ - **Safe retries**: every send has a `sendId`, so a retry after a timeout never writes the
83
+ message twice. After a `429` the client waits as long as the server asks. A client-side rate
84
+ limiter uses the same algorithm as the server.
85
+ - **Security**: the token never shows up in logs, exceptions or `repr`. Secrets can come from
86
+ environment variables or from files (Docker and Kubernetes secrets too). The webhook checks
87
+ its secret in constant time.
88
+
89
+ ## Install
90
+
91
+ You need Python 3.11 or newer (tested up to 3.14). The OpenPGP engines come as prebuilt
92
+ wheels for Linux and macOS (x86-64 and arm64) and for Windows (x86-64), so there is nothing
93
+ to compile; anywhere else pip builds them and you need a Rust toolchain.
94
+
95
+ ```bash
96
+ pip install PGPCbot
97
+ ```
98
+
99
+ From a clone of the repository, `pip install .`, or `pip install -e .` to work on the code.
100
+
101
+ Optional extras: `PGPCbot[webhook]` adds uvicorn to serve the webhook, `PGPCbot[http2]` adds
102
+ HTTP/2 support. Check that it works:
103
+
104
+ ```bash
105
+ python -c "import pgpcbot; print(pgpcbot.__version__)"
106
+ pgpcbot --version
107
+ ```
108
+
109
+ ## Your first bot
110
+
111
+ 1. Create the bot in the app: Settings → Account → My bots, or write to `@bot`. Copy the
112
+ token when you see it: it is shown only once.
113
+ 2. Put it in an environment variable, not in your code:
114
+
115
+ ```bash
116
+ export PGPCHAT_BOT_TOKEN="pgpbot_..." # PowerShell: $env:PGPCHAT_BOT_TOKEN="pgpbot_..."
117
+ export PGPCHAT_API="https://api.pgpchat.app" # only if your server is a different one
118
+ ```
119
+
120
+ 3. Check the token: `pgpcbot whoami`.
121
+ 4. Save the example at the top of this page as `bot.py`, run `python bot.py`, and write to your
122
+ bot from the app. Stop it with Ctrl+C: it finishes the messages it is working on first.
123
+
124
+ ## Commands and buttons
125
+
126
+ ```python
127
+ from pgpcbot import Bot, Button, InlineKeyboard
128
+
129
+ bot = Bot()
130
+
131
+
132
+ @bot.command("weather", description="Forecast for a town")
133
+ async def weather(message, command):
134
+ town = command.args or "Rome"
135
+ keyboard = InlineKeyboard.build(
136
+ [Button.callback("Hourly", f"hourly:{town}"), Button.callback("Another town", "ask")],
137
+ [Button.link("On the web", "https://example.org/weather")],
138
+ )
139
+ await message.reply(f"{town}: sun, sun, rain.", markup=keyboard)
140
+
141
+
142
+ @bot.on_callback_query(prefix="hourly:")
143
+ async def hourly(query):
144
+ await query.answer("Here is the hourly view")
145
+ await query.edit_message(f"{query.data[7:]}: 14° at 10, 18° at 14.", markup=None)
146
+
147
+
148
+ bot.run()
149
+ ```
150
+
151
+ Commands registered with a `description` go into the app's command menu with
152
+ `await bot.sync_commands()`.
153
+
154
+ ## An end-to-end encrypted bot
155
+
156
+ ```bash
157
+ pgpcbot keygen --name Weather --username weatherbot --out bot-key.asc --passphrase-env BOT_KEY_PASSPHRASE
158
+ ```
159
+
160
+ ```python
161
+ import os
162
+ from pgpcbot import Bot
163
+
164
+ bot = Bot(
165
+ pgp_private_key="bot-key.asc",
166
+ pgp_passphrase=os.environ["BOT_KEY_PASSPHRASE"],
167
+ publish_key=True, # on the first run, publish the key: private chats become encrypted
168
+ )
169
+
170
+
171
+ @bot.on_message()
172
+ async def echo(message):
173
+ # already decrypted; message.verification says if the sender's signature is valid
174
+ await message.reply(f"Got it, privately: {message.text}")
175
+
176
+
177
+ bot.run()
178
+ ```
179
+
180
+ Publishing the key cannot be undone through the API: only the bot's owner can switch the bot
181
+ back to readable chats, from the console in the app. Read [the cryptography page][crypto]
182
+ before you go to production.
183
+
184
+ ## Webhook with FastAPI
185
+
186
+ ```python
187
+ import os
188
+ from fastapi import FastAPI
189
+ from pgpcbot import Bot, SQLiteStorage
190
+
191
+ bot = Bot(storage=SQLiteStorage("bot.db"))
192
+
193
+
194
+ @bot.on_message()
195
+ async def echo(message):
196
+ await message.reply(message.text or "...")
197
+
198
+
199
+ webhook = bot.create_webhook_app(
200
+ secret=os.environ["WEBHOOK_SECRET"],
201
+ url="https://example.org/pgpchat", # registered with the server at start-up
202
+ )
203
+ app = FastAPI(lifespan=bot.lifespan)
204
+ app.add_route("/pgpchat", webhook, methods=["POST"])
205
+ ```
206
+
207
+ Run it with `uvicorn app:app`. With `SQLiteStorage` every update is saved before the server
208
+ gets its answer, and handled right after: a crash loses nothing.
209
+
210
+ ## Without asyncio
211
+
212
+ ```python
213
+ from pgpcbot import SyncBot
214
+
215
+ bot = SyncBot()
216
+
217
+
218
+ @bot.command("start")
219
+ def start(message):
220
+ message.reply("Hello!")
221
+
222
+
223
+ bot.run()
224
+ ```
225
+
226
+ `SyncBot` has the same methods as `Bot`, without `await`. Handlers run in a pool of threads.
227
+ For a script that only needs to send one message:
228
+
229
+ ```python
230
+ from pgpcbot import Client
231
+
232
+ with Client() as api:
233
+ api.send_message(7781, "Last night's backup finished fine.")
234
+ ```
235
+
236
+ ## Documentation
237
+
238
+ The guides are in the [`docs/`][docs] folder; the API reference is generated from the
239
+ docstrings. To build the whole documentation as a website with Sphinx:
240
+
241
+ ```bash
242
+ pip install -e . --group docs
243
+ sphinx-build -b html docs docs/_build/html
244
+ ```
245
+
246
+ then open `docs/_build/html/index.html`. The same reference is available without a browser:
247
+ `help(pgpcbot.Bot)` in Python, or `python -m pydoc pgpcbot.Bot` in a terminal.
248
+
249
+ Good places to start: [quick start][quickstart], [cryptography][crypto],
250
+ [API coverage][coverage], [research and decisions][research]. Ready-to-run examples are in
251
+ [`examples/`][examples].
252
+
253
+ ## Security
254
+
255
+ See [SECURITY.md][security] for the threat model and for how to report a vulnerability. In
256
+ short: keep the token and the private key out of your code and out of repositories. If the
257
+ token leaks, `await bot.revoke_token()` burns it at once.
258
+
259
+ ## License
260
+
261
+ MIT. The dependencies have their own licenses: httpx2 (BSD-3-Clause), pysequoia (Apache-2.0,
262
+ with Sequoia PGP under LGPL-2.0-or-later inside its wheels), rpgp-py (MIT).
263
+
264
+ [docs]: https://github.com/vntcore/PGPCbot/tree/main/docs
265
+ [examples]: https://github.com/vntcore/PGPCbot/tree/main/examples
266
+ [quickstart]: https://github.com/vntcore/PGPCbot/blob/main/docs/getting-started/quickstart.md
267
+ [crypto]: https://github.com/vntcore/PGPCbot/blob/main/docs/CRYPTOGRAPHY.md
268
+ [coverage]: https://github.com/vntcore/PGPCbot/blob/main/docs/API_COVERAGE.md
269
+ [research]: https://github.com/vntcore/PGPCbot/blob/main/docs/RESEARCH.md
270
+ [security]: https://github.com/vntcore/PGPCbot/blob/main/SECURITY.md
@@ -0,0 +1,234 @@
1
+ # PGPCbot
2
+
3
+ [![ci](https://github.com/vntcore/PGPCbot/actions/workflows/ci.yml/badge.svg)](https://github.com/vntcore/PGPCbot/actions/workflows/ci.yml)
4
+
5
+ A Python library for writing bots for [PGP Chat](https://pgpchat.app), a messenger whose
6
+ private chats are end-to-end encrypted with OpenPGP. It covers the whole Bot API, and it
7
+ handles the encryption for you.
8
+
9
+ ```python
10
+ from pgpcbot import Bot
11
+
12
+ bot = Bot() # reads the token from PGPCHAT_BOT_TOKEN
13
+
14
+
15
+ @bot.command("start", description="Say hello")
16
+ async def start(message):
17
+ await message.reply("Hello! Send me anything and I will repeat it.")
18
+
19
+
20
+ @bot.on_message()
21
+ async def echo(message):
22
+ await message.reply(f"You said: {message.text}")
23
+
24
+
25
+ bot.run()
26
+ ```
27
+
28
+ If the bot has a published OpenPGP key, its private chats are end-to-end encrypted. The same
29
+ code still works: messages reach your handlers already decrypted, and replies are encrypted
30
+ for the people who should read them. You don't change anything in the handlers.
31
+
32
+ ## What it does
33
+
34
+ - **Every operation of the Bot API**, with typed models, as async calls (`AsyncClient`)
35
+ and as blocking calls (`Client`).
36
+ - **A high-level bot** with handlers for messages, commands (`/start` and `/start@yourbot`),
37
+ button presses, edited and deleted messages, and changes to the bot's membership. Filters you
38
+ can combine, routers to split a bot into modules, middleware, and one place to handle errors.
39
+ - **Automatic OpenPGP**: incoming messages are decrypted and their signatures checked;
40
+ replies are encrypted for every member of the chat and signed. A message that should be
41
+ encrypted is never sent in clear. Tested with real keys and messages from the phones'
42
+ library (PGPainless 1.7.6).
43
+ - **Long polling and webhooks** with "at least once" delivery done right: no update is lost
44
+ when a handler fails, no update is confirmed to the server before it is safe, and a SQLite
45
+ queue survives restarts.
46
+ - **Safe retries**: every send has a `sendId`, so a retry after a timeout never writes the
47
+ message twice. After a `429` the client waits as long as the server asks. A client-side rate
48
+ limiter uses the same algorithm as the server.
49
+ - **Security**: the token never shows up in logs, exceptions or `repr`. Secrets can come from
50
+ environment variables or from files (Docker and Kubernetes secrets too). The webhook checks
51
+ its secret in constant time.
52
+
53
+ ## Install
54
+
55
+ You need Python 3.11 or newer (tested up to 3.14). The OpenPGP engines come as prebuilt
56
+ wheels for Linux and macOS (x86-64 and arm64) and for Windows (x86-64), so there is nothing
57
+ to compile; anywhere else pip builds them and you need a Rust toolchain.
58
+
59
+ ```bash
60
+ pip install PGPCbot
61
+ ```
62
+
63
+ From a clone of the repository, `pip install .`, or `pip install -e .` to work on the code.
64
+
65
+ Optional extras: `PGPCbot[webhook]` adds uvicorn to serve the webhook, `PGPCbot[http2]` adds
66
+ HTTP/2 support. Check that it works:
67
+
68
+ ```bash
69
+ python -c "import pgpcbot; print(pgpcbot.__version__)"
70
+ pgpcbot --version
71
+ ```
72
+
73
+ ## Your first bot
74
+
75
+ 1. Create the bot in the app: Settings → Account → My bots, or write to `@bot`. Copy the
76
+ token when you see it: it is shown only once.
77
+ 2. Put it in an environment variable, not in your code:
78
+
79
+ ```bash
80
+ export PGPCHAT_BOT_TOKEN="pgpbot_..." # PowerShell: $env:PGPCHAT_BOT_TOKEN="pgpbot_..."
81
+ export PGPCHAT_API="https://api.pgpchat.app" # only if your server is a different one
82
+ ```
83
+
84
+ 3. Check the token: `pgpcbot whoami`.
85
+ 4. Save the example at the top of this page as `bot.py`, run `python bot.py`, and write to your
86
+ bot from the app. Stop it with Ctrl+C: it finishes the messages it is working on first.
87
+
88
+ ## Commands and buttons
89
+
90
+ ```python
91
+ from pgpcbot import Bot, Button, InlineKeyboard
92
+
93
+ bot = Bot()
94
+
95
+
96
+ @bot.command("weather", description="Forecast for a town")
97
+ async def weather(message, command):
98
+ town = command.args or "Rome"
99
+ keyboard = InlineKeyboard.build(
100
+ [Button.callback("Hourly", f"hourly:{town}"), Button.callback("Another town", "ask")],
101
+ [Button.link("On the web", "https://example.org/weather")],
102
+ )
103
+ await message.reply(f"{town}: sun, sun, rain.", markup=keyboard)
104
+
105
+
106
+ @bot.on_callback_query(prefix="hourly:")
107
+ async def hourly(query):
108
+ await query.answer("Here is the hourly view")
109
+ await query.edit_message(f"{query.data[7:]}: 14° at 10, 18° at 14.", markup=None)
110
+
111
+
112
+ bot.run()
113
+ ```
114
+
115
+ Commands registered with a `description` go into the app's command menu with
116
+ `await bot.sync_commands()`.
117
+
118
+ ## An end-to-end encrypted bot
119
+
120
+ ```bash
121
+ pgpcbot keygen --name Weather --username weatherbot --out bot-key.asc --passphrase-env BOT_KEY_PASSPHRASE
122
+ ```
123
+
124
+ ```python
125
+ import os
126
+ from pgpcbot import Bot
127
+
128
+ bot = Bot(
129
+ pgp_private_key="bot-key.asc",
130
+ pgp_passphrase=os.environ["BOT_KEY_PASSPHRASE"],
131
+ publish_key=True, # on the first run, publish the key: private chats become encrypted
132
+ )
133
+
134
+
135
+ @bot.on_message()
136
+ async def echo(message):
137
+ # already decrypted; message.verification says if the sender's signature is valid
138
+ await message.reply(f"Got it, privately: {message.text}")
139
+
140
+
141
+ bot.run()
142
+ ```
143
+
144
+ Publishing the key cannot be undone through the API: only the bot's owner can switch the bot
145
+ back to readable chats, from the console in the app. Read [the cryptography page][crypto]
146
+ before you go to production.
147
+
148
+ ## Webhook with FastAPI
149
+
150
+ ```python
151
+ import os
152
+ from fastapi import FastAPI
153
+ from pgpcbot import Bot, SQLiteStorage
154
+
155
+ bot = Bot(storage=SQLiteStorage("bot.db"))
156
+
157
+
158
+ @bot.on_message()
159
+ async def echo(message):
160
+ await message.reply(message.text or "...")
161
+
162
+
163
+ webhook = bot.create_webhook_app(
164
+ secret=os.environ["WEBHOOK_SECRET"],
165
+ url="https://example.org/pgpchat", # registered with the server at start-up
166
+ )
167
+ app = FastAPI(lifespan=bot.lifespan)
168
+ app.add_route("/pgpchat", webhook, methods=["POST"])
169
+ ```
170
+
171
+ Run it with `uvicorn app:app`. With `SQLiteStorage` every update is saved before the server
172
+ gets its answer, and handled right after: a crash loses nothing.
173
+
174
+ ## Without asyncio
175
+
176
+ ```python
177
+ from pgpcbot import SyncBot
178
+
179
+ bot = SyncBot()
180
+
181
+
182
+ @bot.command("start")
183
+ def start(message):
184
+ message.reply("Hello!")
185
+
186
+
187
+ bot.run()
188
+ ```
189
+
190
+ `SyncBot` has the same methods as `Bot`, without `await`. Handlers run in a pool of threads.
191
+ For a script that only needs to send one message:
192
+
193
+ ```python
194
+ from pgpcbot import Client
195
+
196
+ with Client() as api:
197
+ api.send_message(7781, "Last night's backup finished fine.")
198
+ ```
199
+
200
+ ## Documentation
201
+
202
+ The guides are in the [`docs/`][docs] folder; the API reference is generated from the
203
+ docstrings. To build the whole documentation as a website with Sphinx:
204
+
205
+ ```bash
206
+ pip install -e . --group docs
207
+ sphinx-build -b html docs docs/_build/html
208
+ ```
209
+
210
+ then open `docs/_build/html/index.html`. The same reference is available without a browser:
211
+ `help(pgpcbot.Bot)` in Python, or `python -m pydoc pgpcbot.Bot` in a terminal.
212
+
213
+ Good places to start: [quick start][quickstart], [cryptography][crypto],
214
+ [API coverage][coverage], [research and decisions][research]. Ready-to-run examples are in
215
+ [`examples/`][examples].
216
+
217
+ ## Security
218
+
219
+ See [SECURITY.md][security] for the threat model and for how to report a vulnerability. In
220
+ short: keep the token and the private key out of your code and out of repositories. If the
221
+ token leaks, `await bot.revoke_token()` burns it at once.
222
+
223
+ ## License
224
+
225
+ MIT. The dependencies have their own licenses: httpx2 (BSD-3-Clause), pysequoia (Apache-2.0,
226
+ with Sequoia PGP under LGPL-2.0-or-later inside its wheels), rpgp-py (MIT).
227
+
228
+ [docs]: https://github.com/vntcore/PGPCbot/tree/main/docs
229
+ [examples]: https://github.com/vntcore/PGPCbot/tree/main/examples
230
+ [quickstart]: https://github.com/vntcore/PGPCbot/blob/main/docs/getting-started/quickstart.md
231
+ [crypto]: https://github.com/vntcore/PGPCbot/blob/main/docs/CRYPTOGRAPHY.md
232
+ [coverage]: https://github.com/vntcore/PGPCbot/blob/main/docs/API_COVERAGE.md
233
+ [research]: https://github.com/vntcore/PGPCbot/blob/main/docs/RESEARCH.md
234
+ [security]: https://github.com/vntcore/PGPCbot/blob/main/SECURITY.md