Webhook¶
You get a WebhookApp from Bot.create_webhook_app(). The guide is
Webhooks.
- class WebhookApp(
- bot,
- *,
- secret,
- path=None,
- url=None,
- max_body=DEFAULT_MAX_BODY,
- ack_timeout=8.0,
- allow_no_secret=False,
The ASGI application the server sends updates to.
Get one from
Bot.create_webhook_app. It is a complete ASGI 3 application: it answers HTTP requests and takes the lifespan events, so that, served on its own, it starts the bot when the web server starts and stops it at shutdown. WebSocket connections are closed.Each request is checked in this order, and the status code tells the server what to do:
404: a different path, whenpathwas given;405, withAllow: POST: not aPOST;401, withWWW-Authenticate: the secret header is missing or wrong. It is compared in constant time, before the body is read;413: the body is larger thanmax_body;408: the body did not arrive within 10 seconds;200: the update is safe (stored, or handled withMemoryStorage), or it is a repeat. Also a request with the right secret whose body is not an update: it is logged as an error, because a4xxwould make the server send it again for days, and every later update would wait behind it;503: the update is not safe yet (it could not be stored, or the handlers took longer thanack_timeoutwithMemoryStorage), or the bot is stopping. The server tries again later;500: an unexpected error, logged. The server tries again later.
Inside another application, the app gets no lifespan events of its own: pass
lifespan=bot.lifespanto that application. Without it, the bot starts at the first delivery and logs a warning. For a web framework that is not ASGI,handledoes the checks and the processing.Examples
On its own, served with
uvicorn bot:app:app = bot.create_webhook_app( secret=os.environ["WEBHOOK_SECRET"], url="https://bot.example.org/pgpchat", )
In FastAPI, next to your own routes:
from fastapi import FastAPI webhook = bot.create_webhook_app( secret=os.environ["WEBHOOK_SECRET"], url="https://example.org/pgpchat" ) app = FastAPI(lifespan=bot.lifespan) app.add_route("/pgpchat", webhook, methods=["POST"])
In Starlette, with the same
webhook:from starlette.applications import Starlette from starlette.routing import Route app = Starlette( routes=[Route("/pgpchat", webhook, methods=["POST"])], lifespan=bot.lifespan, )
Warning
Give the app a route, not a mount. With the app mounted at
/pgpchat, Starlette answers aPOSTto/pgpchatwith a redirect to/pgpchat/, and the server counts a redirect as a failed delivery: the queue stops. If you do mount it, register the address with the trailing slash.Sets up the endpoint. Nothing is sent to the server here.
Use
Bot.create_webhook_apprather than this: it passes the bot, andBot.lifespanthen knows whichurlto register.- Parameters:
secret (str | None) – The secret the server sends in the
X-PGPChat-Bot-Api-Secret-Tokenheader, checked on every delivery: 1 to 256 characters ofA-Z,a-z,0-9,_and-.generate_secretmakes a good one.Noneonly withallow_no_secret=True.path (str | None) – Answer only on this path, and
404elsewhere. A trailing/makes no difference. Leave it out when another application routes to the app.url (str | None) – The app’s public
httpsaddress. When given, the bot registers it with the server, with the secret, every time it starts.max_body (int) – The largest body accepted, in bytes: 1 MiB by default. A larger one gets
413.ack_timeout (float) – With
MemoryStorage, how many seconds to wait for the handlers before answering503. Above 0 and under 10, the server’s own time limit.allow_no_secret (bool) – Accept
secret=None.
- Raises:
ConfigurationError –
secretisNonewithoutallow_no_secret=True, orack_timeoutis not above 0 and under 10.ValidationError –
secretorurlis malformed.
Warning
With
allow_no_secret=Trueand no secret, anyone who finds the address can send the bot fake updates. Use it only when something in front of the app already checks that requests come from the server.- property secret: str | None¶
The secret checked on every delivery, or
Noneif the app was made without one.
- async handle(headers, body, *, peer=None)¶
Checks and processes one delivery, for a web framework that is not ASGI.
It checks the secret, the size and the shape, then hands the update to the bot and answers as the ASGI app does. The path, the method and the 10-second limit for reading the body are left to your framework. Start the bot before the first delivery (
await bot.start()) and stop it at the end (await bot.stop()).- Parameters:
- Returns:
The status, body and headers to answer with. Anything but a
2xxmakes the server send the update again later.- Return type:
Example
A route in aiohttp:
from aiohttp import web async def pgpchat(request: web.Request) -> web.Response: body = await request.read() result = await webhook.handle(request.headers, body, peer=request.remote) return web.json_response( result.body, status=result.status, headers=dict(result.headers) )
- class WebhookResult(status, body, headers=())[source]¶
The answer
handlechose, to send back to the server.A
2xxstatus tells the server the update was delivered. Anything else makes it send the same update again later, and the updates after it wait.- body¶
The body, to send as JSON:
{"ok": True}with a2xx,{"ok": False}otherwise.
- generate_secret(nbytes=32)¶
Makes a random secret for a webhook, in the alphabet the server accepts.
Pass it to
Bot.create_webhook_app. Make it once and keep it with your other secrets: the server sends the secret it was given with each delivery, so every worker process must use that same one.- Parameters:
nbytes (int) – How many random bytes. The default, 32, gives 43 characters. The server takes 1 to 256 characters, so keep it between 1 and 192.
- Returns:
The secret, made of letters, digits,
_and-bysecrets.token_urlsafe.- Return type:
Examples
The default length:
>>> len(generate_secret()) 43
To make one in a terminal:
python -c "from pgpcbot import generate_secret; print(generate_secret())"
- SECRET_HEADER = 'x-pgpchat-bot-api-secret-token'¶
The HTTP header that carries the webhook secret, in lower case.
The server sends it as
X-PGPChat-Bot-Api-Secret-Tokenwith every delivery, when the webhook has a secret.WebhookAppcompares header names without regard to case.