Skip to content

Commit 0402f39

Browse files
gambletanclaude
andcommitted
feat: add ServiceBridge for remote service management + YAML config support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ebcfabb commit 0402f39

6 files changed

Lines changed: 799 additions & 2 deletions

File tree

README.md

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ That's it. Your bot is live, responding to `/status` and `/deploy staging`.
110110
- [Multi-Channel Setup](#multi-channel-setup)
111111
- [Message Types](#message-types)
112112
- [Writing a Custom Adapter](#writing-a-custom-adapter)
113+
- [ServiceBridge](#servicebridge)
114+
- [YAML Config](#yaml-config)
113115
- [Real-World Example](#real-world-example)
114116
- [API Reference](#api-reference)
115117

@@ -823,6 +825,99 @@ manager.add_channel(MyAdapter(...))
823825

824826
---
825827

828+
## ServiceBridge
829+
830+
`ServiceBridge` is the fastest way to expose any service as a chat-controllable interface. Instead of wiring up `CommandMiddleware` by hand, you call `expose()` and get automatic `/help`, argument parsing, error handling, and sync-function support for free.
831+
832+
```python
833+
import asyncio
834+
from unified_channel import ChannelManager, TelegramAdapter, ServiceBridge
835+
836+
manager = ChannelManager()
837+
manager.add_channel(TelegramAdapter(token="BOT_TOKEN"))
838+
839+
bridge = ServiceBridge(manager)
840+
841+
# Expose any function as a chat command
842+
bridge.expose("deploy", lambda args: f"Deploying to {args[0] if args else 'staging'}...",
843+
description="Deploy the app", params=["env"])
844+
845+
# Sync or async — both work
846+
def disk_usage(args):
847+
import shutil
848+
total, used, free = shutil.disk_usage("/")
849+
return f"Disk: {used // (1 << 30)}G / {total // (1 << 30)}G"
850+
851+
bridge.expose("disk", disk_usage, description="Check disk usage")
852+
853+
# Built-in /status and /logs shortcuts
854+
bridge.expose_status(lambda args: "All systems operational")
855+
bridge.expose_logs(lambda args: open("app.log").readlines()[-10:])
856+
857+
# Handlers can receive the full UnifiedMessage
858+
async def whoami(args, msg):
859+
return f"You are {msg.sender.username} on {msg.channel}"
860+
861+
bridge.expose("whoami", whoami, description="Show caller info")
862+
863+
asyncio.run(bridge.run())
864+
```
865+
866+
This gives you `/help`, `/deploy`, `/disk`, `/status`, `/logs`, and `/whoami` — all with automatic error handling. If a command throws, the user gets a friendly error message instead of silence.
867+
868+
### Flag parsing
869+
870+
Arguments like `--force` and `--count 3` are automatically parsed:
871+
872+
```python
873+
async def restart(args, msg):
874+
flags = msg.metadata.get("_flags", {})
875+
force = flags.get("force") == "true"
876+
service = args[0] if args else "all"
877+
return f"Restarting {service} (force={force})"
878+
879+
bridge.expose("restart", restart, description="Restart services", params=["service"])
880+
# /restart nginx --force → "Restarting nginx (force=True)"
881+
```
882+
883+
---
884+
885+
## YAML Config
886+
887+
Load channels and middleware from a config file instead of writing Python:
888+
889+
```yaml
890+
# unified-channel.yaml
891+
channels:
892+
telegram:
893+
token: "${UC_TELEGRAM_TOKEN}"
894+
discord:
895+
token: "${UC_DISCORD_TOKEN}"
896+
slack:
897+
bot_token: "${UC_SLACK_BOT_TOKEN}"
898+
app_token: "${UC_SLACK_APP_TOKEN}"
899+
900+
middleware:
901+
access:
902+
allowed_users: ["admin_id_1", "admin_id_2"]
903+
904+
settings:
905+
command_prefix: "/"
906+
```
907+
908+
```python
909+
from unified_channel import load_config, ServiceBridge
910+
911+
manager = load_config("unified-channel.yaml")
912+
bridge = ServiceBridge(manager)
913+
bridge.expose("status", lambda args: "OK")
914+
asyncio.run(bridge.run())
915+
```
916+
917+
Environment variables are interpolated with `${VAR}` syntax. Adapters are auto-detected by name. Returns a fully configured `ChannelManager` ready to use.
918+
919+
---
920+
826921
## Real-World Example
827922

828923
A complete remote management bot for a job queue system:
@@ -933,6 +1028,22 @@ if __name__ == "__main__":
9331028
|-----------|-------------|
9341029
| `allowed_user_ids` | `set[str]` of allowed sender IDs. `None` = allow all |
9351030

1031+
### ServiceBridge
1032+
1033+
| Method | Description |
1034+
|--------|-------------|
1035+
| `ServiceBridge(manager, prefix="/")` | Create a bridge wrapping a `ChannelManager` |
1036+
| `expose(name, handler, description, params)` | Expose a function as a chat command |
1037+
| `expose_status(handler)` | Register `/status` command |
1038+
| `expose_logs(handler)` | Register `/logs` command |
1039+
| `await run()` | Start the bridge (delegates to `manager.run()`) |
1040+
1041+
### load_config
1042+
1043+
| Function | Description |
1044+
|----------|-------------|
1045+
| `load_config(path)` | Load a YAML config file, return a configured `ChannelManager` |
1046+
9361047
### Adapters
9371048

9381049
| Adapter | Install Extra | Mode | Needs Public URL |
@@ -960,7 +1071,7 @@ if __name__ == "__main__":
9601071

9611072
## Testing
9621073

963-
76 tests covering every layer of the stack. Run with:
1074+
96 tests covering every layer of the stack. Run with:
9641075

9651076
```bash
9661077
pip install -e ".[dev]"
@@ -977,6 +1088,8 @@ pytest -v
9771088
| `test_manager.py` | 4 | Core `ChannelManager` pipeline — command end-to-end, access control blocking, fallback handler, `get_status()`. |
9781089
| `test_manager_advanced.py` | 14 | Multi-channel routing, `OutboundMessage` return, `send()` direct push, unknown channel error, `broadcast()`, middleware chain order verification, short-circuit, no-reply/null-reply cases, auth+commands combo, fluent API chaining, no-channels guard. |
9791090
| `test_adapters_unit.py` | 32 | Per-adapter unit tests with mocked SDKs: **IRC** (PRIVMSG parsing, commands, self-ignore, DM routing), **iMessage** (macOS-only), **WhatsApp** (text/command/image/reaction/reply-context), **Mattermost** (text/command/self-ignore/threads), **Twitch** (text/commands/self-ignore/IRC tags), **Zalo** (text/commands), **BlueBubbles/Synology/Nextcloud** (channel_id, status). Lazy import verification for all 18 adapter names. |
1091+
| `test_bridge.py` | 12 | `ServiceBridge` — expose commands, sync/async handlers, args/flag parsing, `/help` generation, `/status` + `/logs` shortcuts, error handling, handler signature detection. |
1092+
| `test_config.py` | 8 | YAML config loading — env var interpolation (basic, embedded, missing, non-string), nested dict interpolation, full config parse with mocked adapter, empty file error, missing PyYAML error. |
9801093

9811094
### What's tested per adapter
9821095

0 commit comments

Comments
 (0)