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.
- pgpcbot-0.1.0/.gitignore +39 -0
- pgpcbot-0.1.0/CHANGELOG.md +31 -0
- pgpcbot-0.1.0/CONTRIBUTING.md +82 -0
- pgpcbot-0.1.0/LICENSE +21 -0
- pgpcbot-0.1.0/PKG-INFO +270 -0
- pgpcbot-0.1.0/README.md +234 -0
- pgpcbot-0.1.0/SECURITY.md +115 -0
- pgpcbot-0.1.0/docs/API_COVERAGE.md +72 -0
- pgpcbot-0.1.0/docs/CRYPTOGRAPHY.md +383 -0
- pgpcbot-0.1.0/docs/RESEARCH.md +423 -0
- pgpcbot-0.1.0/docs/api/bot.rst +26 -0
- pgpcbot-0.1.0/docs/api/client.rst +92 -0
- pgpcbot-0.1.0/docs/api/crypto.rst +25 -0
- pgpcbot-0.1.0/docs/api/errors.rst +254 -0
- pgpcbot-0.1.0/docs/api/index.rst +50 -0
- pgpcbot-0.1.0/docs/api/keyboard.rst +20 -0
- pgpcbot-0.1.0/docs/api/models.rst +159 -0
- pgpcbot-0.1.0/docs/api/routing.rst +52 -0
- pgpcbot-0.1.0/docs/api/storage.rst +19 -0
- pgpcbot-0.1.0/docs/api/webhook.rst +17 -0
- pgpcbot-0.1.0/docs/architecture/index.md +162 -0
- pgpcbot-0.1.0/docs/conf.py +96 -0
- pgpcbot-0.1.0/docs/deployment/index.md +153 -0
- pgpcbot-0.1.0/docs/examples.md +46 -0
- pgpcbot-0.1.0/docs/getting-started/configuration.md +135 -0
- pgpcbot-0.1.0/docs/getting-started/installation.md +81 -0
- pgpcbot-0.1.0/docs/getting-started/quickstart.md +120 -0
- pgpcbot-0.1.0/docs/guides/async-and-sync.md +124 -0
- pgpcbot-0.1.0/docs/guides/command-line.md +100 -0
- pgpcbot-0.1.0/docs/guides/commands-and-buttons.md +192 -0
- pgpcbot-0.1.0/docs/guides/errors.md +151 -0
- pgpcbot-0.1.0/docs/guides/events.md +326 -0
- pgpcbot-0.1.0/docs/guides/long-polling.md +101 -0
- pgpcbot-0.1.0/docs/guides/messages.md +232 -0
- pgpcbot-0.1.0/docs/guides/openpgp.md +195 -0
- pgpcbot-0.1.0/docs/guides/rate-limiting.md +116 -0
- pgpcbot-0.1.0/docs/guides/storage.md +115 -0
- pgpcbot-0.1.0/docs/guides/webhook.md +203 -0
- pgpcbot-0.1.0/docs/index.rst +129 -0
- pgpcbot-0.1.0/docs/openapi-reference/index.md +18 -0
- pgpcbot-0.1.0/docs/openapi-reference/operations.md +608 -0
- pgpcbot-0.1.0/docs/openapi.json +991 -0
- pgpcbot-0.1.0/docs/project/changelog.md +2 -0
- pgpcbot-0.1.0/docs/project/contributing.md +2 -0
- pgpcbot-0.1.0/docs/project/testing.md +76 -0
- pgpcbot-0.1.0/docs/project/versioning.md +49 -0
- pgpcbot-0.1.0/docs/security/index.md +2 -0
- pgpcbot-0.1.0/docs/troubleshooting/faq.md +85 -0
- pgpcbot-0.1.0/docs/troubleshooting/index.md +94 -0
- pgpcbot-0.1.0/examples/01_hello_world.py +20 -0
- pgpcbot-0.1.0/examples/02_echo_bot.py +33 -0
- pgpcbot-0.1.0/examples/03_commands.py +61 -0
- pgpcbot-0.1.0/examples/04_private_messages.py +54 -0
- pgpcbot-0.1.0/examples/05_group_bot.py +92 -0
- pgpcbot-0.1.0/examples/06_inline_buttons.py +50 -0
- pgpcbot-0.1.0/examples/07_callbacks.py +80 -0
- pgpcbot-0.1.0/examples/08_long_polling.py +45 -0
- pgpcbot-0.1.0/examples/09_fastapi_webhook.py +40 -0
- pgpcbot-0.1.0/examples/10_encrypted_bot.py +49 -0
- pgpcbot-0.1.0/examples/11_generate_pgp_keys.py +43 -0
- pgpcbot-0.1.0/examples/12_send_encrypted_message.py +46 -0
- pgpcbot-0.1.0/examples/13_custom_handlers.py +76 -0
- pgpcbot-0.1.0/examples/14_error_handling.py +78 -0
- pgpcbot-0.1.0/examples/15_rate_limiting.py +66 -0
- pgpcbot-0.1.0/examples/16_async_bot.py +52 -0
- pgpcbot-0.1.0/examples/17_sync_client.py +49 -0
- pgpcbot-0.1.0/examples/18_persistent_updates.py +45 -0
- pgpcbot-0.1.0/examples/19_advanced_configuration.py +58 -0
- pgpcbot-0.1.0/examples/20_complete_bot.py +133 -0
- pgpcbot-0.1.0/pyproject.toml +177 -0
- pgpcbot-0.1.0/src/pgpcbot/__init__.py +304 -0
- pgpcbot-0.1.0/src/pgpcbot/__main__.py +319 -0
- pgpcbot-0.1.0/src/pgpcbot/_http.py +311 -0
- pgpcbot-0.1.0/src/pgpcbot/_secrets.py +192 -0
- pgpcbot-0.1.0/src/pgpcbot/_spec.py +385 -0
- pgpcbot-0.1.0/src/pgpcbot/_validate.py +276 -0
- pgpcbot-0.1.0/src/pgpcbot/_version.py +1 -0
- pgpcbot-0.1.0/src/pgpcbot/bot.py +3681 -0
- pgpcbot-0.1.0/src/pgpcbot/client.py +2450 -0
- pgpcbot-0.1.0/src/pgpcbot/crypto/__init__.py +33 -0
- pgpcbot-0.1.0/src/pgpcbot/crypto/_lib.py +326 -0
- pgpcbot-0.1.0/src/pgpcbot/crypto/keys.py +1093 -0
- pgpcbot-0.1.0/src/pgpcbot/engine.py +547 -0
- pgpcbot-0.1.0/src/pgpcbot/errors.py +888 -0
- pgpcbot-0.1.0/src/pgpcbot/filters.py +661 -0
- pgpcbot-0.1.0/src/pgpcbot/keyboard.py +457 -0
- pgpcbot-0.1.0/src/pgpcbot/models.py +2638 -0
- pgpcbot-0.1.0/src/pgpcbot/py.typed +0 -0
- pgpcbot-0.1.0/src/pgpcbot/ratelimit.py +246 -0
- pgpcbot-0.1.0/src/pgpcbot/router.py +776 -0
- pgpcbot-0.1.0/src/pgpcbot/storage.py +1257 -0
- pgpcbot-0.1.0/src/pgpcbot/webhook.py +440 -0
- pgpcbot-0.1.0/src/pgpcbot/wire.py +157 -0
- pgpcbot-0.1.0/tests/__init__.py +0 -0
- pgpcbot-0.1.0/tests/conftest.py +103 -0
- pgpcbot-0.1.0/tests/fakeapi.py +538 -0
- pgpcbot-0.1.0/tests/fixtures/attachment.pgp +0 -0
- pgpcbot-0.1.0/tests/fixtures/bot.pub.asc +15 -0
- pgpcbot-0.1.0/tests/fixtures/bot.sec.asc +14 -0
- pgpcbot-0.1.0/tests/fixtures/bot_locked.sec.asc +16 -0
- pgpcbot-0.1.0/tests/fixtures/bot_old.pub.asc +15 -0
- pgpcbot-0.1.0/tests/fixtures/bot_old.sec.asc +14 -0
- pgpcbot-0.1.0/tests/fixtures/msg_for_old_key.asc +14 -0
- pgpcbot-0.1.0/tests/fixtures/msg_media.asc +16 -0
- pgpcbot-0.1.0/tests/fixtures/msg_not_for_bot.asc +12 -0
- pgpcbot-0.1.0/tests/fixtures/msg_rsa_signed.asc +27 -0
- pgpcbot-0.1.0/tests/fixtures/msg_text_signed.asc +14 -0
- pgpcbot-0.1.0/tests/fixtures/msg_text_unsigned.asc +12 -0
- pgpcbot-0.1.0/tests/fixtures/phone_ed.pub.asc +22 -0
- pgpcbot-0.1.0/tests/fixtures/phone_ed.sec.asc +24 -0
- pgpcbot-0.1.0/tests/fixtures/phone_rsa.pub.asc +70 -0
- pgpcbot-0.1.0/tests/fixtures/phone_rsa.sec.asc +130 -0
- pgpcbot-0.1.0/tests/interop/Interop.java +372 -0
- pgpcbot-0.1.0/tests/interop/__init__.py +0 -0
- pgpcbot-0.1.0/tests/interop/test_pgpainless.py +170 -0
- pgpcbot-0.1.0/tests/test_bot.py +2265 -0
- pgpcbot-0.1.0/tests/test_cli.py +469 -0
- pgpcbot-0.1.0/tests/test_client.py +2035 -0
- pgpcbot-0.1.0/tests/test_crypto.py +1115 -0
- pgpcbot-0.1.0/tests/test_docs.py +235 -0
- pgpcbot-0.1.0/tests/test_engine.py +642 -0
- pgpcbot-0.1.0/tests/test_errors.py +302 -0
- pgpcbot-0.1.0/tests/test_filters.py +487 -0
- pgpcbot-0.1.0/tests/test_http.py +479 -0
- pgpcbot-0.1.0/tests/test_keyboard.py +393 -0
- pgpcbot-0.1.0/tests/test_lifecycle.py +393 -0
- pgpcbot-0.1.0/tests/test_models.py +1229 -0
- pgpcbot-0.1.0/tests/test_parity.py +233 -0
- pgpcbot-0.1.0/tests/test_polling.py +585 -0
- pgpcbot-0.1.0/tests/test_ratelimit.py +312 -0
- pgpcbot-0.1.0/tests/test_router.py +508 -0
- pgpcbot-0.1.0/tests/test_secrets.py +283 -0
- pgpcbot-0.1.0/tests/test_storage.py +709 -0
- pgpcbot-0.1.0/tests/test_syncbot.py +899 -0
- pgpcbot-0.1.0/tests/test_validate.py +528 -0
- pgpcbot-0.1.0/tests/test_webhook.py +900 -0
- pgpcbot-0.1.0/tests/test_wire.py +269 -0
pgpcbot-0.1.0/.gitignore
ADDED
|
@@ -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
|
+
[](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
|
pgpcbot-0.1.0/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# PGPCbot
|
|
2
|
+
|
|
3
|
+
[](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
|