feat(plugins): generalize native platform handler registration to every gateway platform
ctx.register_platform_handler(platform, factory) — the generic surface for plugins to wire native handlers into any platform adapter at connect() time. Factories receive (native, adapter): the platform's client/app object (PTB Application, discord.py Bot, slack_bolt AsyncApp, Teams App, DingTalkStreamClient, aiohttp web.Application) or None for adapters with no separate native object. - BasePlatformAdapter._wire_plugin_handlers(native): shared, isolated invocation helper — a raising plugin cannot block a platform connect. - All 27 connectable adapters call it: telegram/slack/teams/line/ api_server/msgraph_webhook wire before their dispatch tables freeze; the rest hook at connect success. - register_telegram_handler and get_telegram_handler_factories retained as thin back-compat aliases over the telegram bucket. - Source-invariant test guarantees every adapter with connect() keeps calling the hook.
This commit is contained in:
@@ -1279,18 +1279,59 @@ def register(ctx):
|
||||
- Standard slack_bolt rules apply — `await ack()` within 3 seconds, then do longer work.
|
||||
- For multi-workspace deployments the handler fires for clicks from any connected workspace; use `body["team"]["id"]` if you need to scope behaviour.
|
||||
|
||||
This is the public way for plugins to participate in Slack interactivity. Older plugins may patch `SlackAdapter.connect`; prefer this API instead.
|
||||
This is the public way for plugins to participate in Slack interactivity. Older plugins may patch `SlackAdapter.connect`; prefer this API instead. For the full slack_bolt surface (events, shortcuts, commands — not just Block Kit actions), use the generic `register_platform_handler("slack", ...)` below.
|
||||
|
||||
### Register Telegram (PTB) handlers
|
||||
### Register native platform handlers (any platform)
|
||||
|
||||
Plugins that need to receive Telegram updates the core adapter doesn't route — inline button callbacks with their own prefix, Business API updates, chat-member events, etc. — can register a handler factory that the Telegram adapter invokes at connect time.
|
||||
Plugins that need to receive platform events the core adapter doesn't route — extra update types, native button callbacks, reaction/member events, webhook routes — can register a handler factory that the platform's adapter invokes at connect time. This works on **every** gateway platform.
|
||||
|
||||
```python
|
||||
def register(ctx):
|
||||
def _wire(native, adapter):
|
||||
# native: the platform's client/app object (see table below)
|
||||
# adapter: the platform adapter instance (treat as read-only)
|
||||
# Import platform SDKs HERE so register() works without them.
|
||||
...
|
||||
|
||||
ctx.register_platform_handler("discord", _wire)
|
||||
```
|
||||
|
||||
**Signature:** `ctx.register_platform_handler(platform, factory) -> None`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `platform` | `str` | Gateway platform name, lowercase (`"telegram"`, `"discord"`, `"slack"`, `"matrix"`, ...) |
|
||||
| `factory` | callable | Receives `(native, adapter)` at connect time |
|
||||
|
||||
**What `native` is, per platform:**
|
||||
|
||||
| Platform | `native` object | Typical hooks |
|
||||
|----------|-----------------|---------------|
|
||||
| `telegram` | PTB `Application` | `add_handler` — any update type, pattern-scoped callbacks |
|
||||
| `discord` | `discord.ext.commands.Bot` | `add_listener` — reactions, member events, threads, voice |
|
||||
| `slack` | `slack_bolt.AsyncApp` | `app.event()` / `app.action()` / `app.command()` |
|
||||
| `matrix` | Matrix client | event callbacks |
|
||||
| `teams` | Teams `App` | `on_message` / `on_card_action` decorators |
|
||||
| `dingtalk` | `DingTalkStreamClient` | `register_callback_handler` for other stream topics |
|
||||
| `feishu` | lark_oapi client | API calls; event routing |
|
||||
| `line`, `api_server`, `msgraph_webhook` | aiohttp `web.Application` | `router.add_get/post` — custom routes (wired before the router freezes) |
|
||||
| everything else (whatsapp, signal, irc, email, sms, ntfy, wecom, weixin, bluebubbles, yuanbao, ...) | `None` | connect-time hook; work through the `adapter` handle |
|
||||
|
||||
**Runtime behavior:**
|
||||
|
||||
- Factories are queued at plugin-load time and invoked when the platform connects — for platforms where dispatch order matters (Telegram, Slack, Teams, aiohttp routers) they run **before** the core handlers register, so scoped plugin handlers take precedence and everything else falls through.
|
||||
- **Always scope handlers you add to first-match dispatch tables.** On Telegram, use `CallbackQueryHandler(..., pattern=r"^myplugin:")` — an unscoped handler would swallow the core button flows (exec approvals, model picker, clarify prompts).
|
||||
- Each factory is isolated: if it raises, the error is logged and the platform still connects.
|
||||
- Import platform SDKs inside the factory body, not at module level — `register()` must work when the SDK isn't installed.
|
||||
- One plugin can register factories for several platforms; each fires only when its platform connects.
|
||||
|
||||
**Telegram alias:** `ctx.register_telegram_handler(factory)` is a back-compat alias for `ctx.register_platform_handler("telegram", factory)`.
|
||||
|
||||
Example — Telegram, pattern-scoped inline buttons:
|
||||
|
||||
```python
|
||||
def register(ctx):
|
||||
def _wire(application, adapter):
|
||||
# Called with the PTB Application right after it is built,
|
||||
# BEFORE the core handlers are added. Import telegram here so
|
||||
# register() works even when PTB isn't installed.
|
||||
from telegram.ext import CallbackQueryHandler
|
||||
|
||||
async def _on_button(update, context):
|
||||
@@ -1302,21 +1343,21 @@ def register(ctx):
|
||||
CallbackQueryHandler(_on_button, pattern=r"^myplugin:")
|
||||
)
|
||||
|
||||
ctx.register_telegram_handler(_wire)
|
||||
ctx.register_platform_handler("telegram", _wire)
|
||||
```
|
||||
|
||||
**Signature:** `ctx.register_telegram_handler(factory) -> None`
|
||||
Example — Discord, reaction events:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `factory` | callable | Receives `(application, adapter)` — the PTB `Application` and the `TelegramAdapter` instance (`adapter.bot`, `adapter.config`; treat it as read-only) |
|
||||
```python
|
||||
def register(ctx):
|
||||
def _wire(bot, adapter):
|
||||
async def on_raw_reaction_add(payload):
|
||||
... # e.g. reaction-based voting / moderation
|
||||
|
||||
**Runtime behavior:**
|
||||
bot.add_listener(on_raw_reaction_add, "on_raw_reaction_add")
|
||||
|
||||
- The factory is queued at plugin-load time and invoked when the Telegram platform connects, before the core handlers register. PTB dispatches only the first matching handler per group, so plugin handlers take precedence for the updates they scope to; everything else falls through to core.
|
||||
- **Always scope `CallbackQueryHandler` with a `pattern=` prefix** (e.g. `r"^myplugin:"`). An unscoped handler would swallow the core button flows (exec approvals, model picker, clarify prompts).
|
||||
- The factory is isolated: if it raises, the error is logged and Telegram still connects.
|
||||
- The adapter polls with `allowed_updates=Update.ALL_TYPES`, so non-message update types (e.g. `business_connection`, `chat_member`) already arrive without extra configuration.
|
||||
ctx.register_platform_handler("discord", _wire)
|
||||
```
|
||||
|
||||
:::tip
|
||||
This guide covers **general plugins** (tools, hooks, slash commands, CLI commands). The sections below sketch the authoring pattern for each specialized plugin type; each links to its full guide for field reference and examples.
|
||||
|
||||
Reference in New Issue
Block a user