diff --git a/config.example.yaml b/config.example.yaml index ef181f2..1831fe9 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -34,6 +34,15 @@ notifier: enabled: false # Set to true to enable Lark notifications webhook_url: "${LARK_WEBHOOK_URL}" # Your Lark webhook URL (recommended: store in .env) + # OneBot-11 notifier - sends messages to OneBot v11 servers (supports QQ, Discord, Lark, etc.) + # See: https://github.com/botuniverse/onebot-11 + onebot-11: + enabled: false # Set to true to enable OneBot-11 notifications + url: "http://127.0.0.1:5700" # OneBot HTTP API server URL + access_token: "${ONEBOT_ACCESS_TOKEN}" # Access token for authentication (recommended: store in .env) + to_group_ids: [] # List of group IDs to send notifications to, e.g., ["1016792818", "1234567890"] + to_friend_ids: [] # List of friend/user IDs to send notifications to, e.g., ["2014709936", "9876543210"] + # Watchers configuration watchers: # IMAP Email Watcher diff --git a/docs/development.md b/docs/development.md index 9387a01..d3eba10 100644 --- a/docs/development.md +++ b/docs/development.md @@ -75,6 +75,8 @@ AWA supports multiple notification backends: **Console Notifier** - Rich markdown rendering in your terminal **Ntfy Notifier** - Push notifications to [ntfy.sh](https://ntfy.sh) (self-hosted or public) +**Lark Notifier** - Send messages to Lark (Feishu) via webhook +**OneBot-11 Notifier** - Send messages to OneBot-11 servers (supports QQ, Discord, Lark via adapters) More notifiers can be added by extending `BaseNotifier`. @@ -144,6 +146,47 @@ notifier: Authorization: "Bearer your-token" ``` +### OneBot-11 Notifier + +The OneBot-11 notifier allows AWA to send messages to OneBot-11 compatible servers, which provides indirect support for multiple platforms including QQ, Discord, Lark, and more through protocol adapters. + +**Features:** +- Send messages to multiple groups simultaneously +- Send private messages to multiple users +- Authentication via access token +- Configurable server URL +- Automatic error handling and retry + +**Configuration in `config.yaml`:** + +```yaml +notifier: + onebot-11: + enabled: true + url: "http://127.0.0.1:5700" # OneBot HTTP API server URL + access_token: "${ONEBOT_ACCESS_TOKEN}" # Use environment variable for security + to_group_ids: # List of group IDs to send notifications to + - "1016792818" + - "1234567890" + to_friend_ids: # List of friend/user IDs to send private messages to + - "2014709936" + - "9876543210" +``` + +**Setup Steps:** + +1. Deploy an OneBot-11 compatible server (e.g., [go-cqhttp](https://github.com/Mrs4s/go-cqhttp), [OpenShamrock](https://github.com/whitechi73/OpenShamrock)) +2. Configure the server to listen on HTTP (default: `http://127.0.0.1:5700`) +3. (Optional) Set an access token in the OneBot server for authentication +4. Add the OneBot-11 configuration to your `config.yaml` +5. Store your access token in `.env` file: `ONEBOT_ACCESS_TOKEN=your-secret-token` + +**Relevant Documentation:** +- [OneBot-11 Protocol](https://github.com/botuniverse/onebot-11) +- [OneBot-11 HTTP Communication](https://github.com/botuniverse/onebot-11/blob/master/communication/http.md) +- [OneBot-11 API Reference](https://github.com/botuniverse/onebot-11/blob/master/api/public.md) + + ## Built-in Watchers ### IMAP Email Watcher diff --git a/notifier.py b/notifier.py index 0425967..cfcd3c3 100644 --- a/notifier.py +++ b/notifier.py @@ -170,6 +170,103 @@ async def close(self) -> None: self.session = None +class OneBotNotifier(BaseNotifier): + """OneBot-11 notifier that sends messages to OneBot-11 servers via HTTP""" + + def __init__(self, config: dict): + super().__init__(config) + self.url = config.get("url", "http://127.0.0.1:5700") + self.access_token = config.get("access_token", "") + self.to_group_ids = config.get("to_group_ids", []) + self.to_friend_ids = config.get("to_friend_ids", []) + + if not self.to_group_ids and not self.to_friend_ids: + logger.warning( + "OneBotNotifier: No group or friend IDs configured, notifications will not be sent" + ) + + # Prepare headers once during initialization + self.headers = {} + if self.access_token: + self.headers["Authorization"] = f"Bearer {self.access_token}" + + self.session = None + logger.info( + f"OneBot-11 notifier initialized: {self.url} " + f"(groups: {len(self.to_group_ids)}, friends: {len(self.to_friend_ids)})" + ) + + async def send(self, markdown_content: str) -> None: + """ + Send notification to OneBot-11 server. + + Args: + markdown_content: The notification content in markdown format + """ + if not self.enabled: + return + + if not self.to_group_ids and not self.to_friend_ids: + return + + # Check if session is initialized + if self.session is None: + # Import here to avoid dependency if not using OneBotNotifier + import aiohttp + + # Create session with timeout configuration + timeout = aiohttp.ClientTimeout(total=30) + self.session = aiohttp.ClientSession(timeout=timeout) + + # Send to all configured groups + for group_id in self.to_group_ids: + payload = {"group_id": group_id, "message": markdown_content} + try: + async with self.session.post( + f"{self.url}/send_group_msg", json=payload, headers=self.headers + ) as response: + if response.status != 200: + text = await response.text() + logger.error( + f"Failed to send OneBot group message to {group_id}: {response.status}, {text}" + ) + else: + logger.debug(f"OneBot group notification sent to {group_id}") + except Exception as e: + logger.error( + f"Error sending OneBot group notification to {group_id}: {e}", + exc_info=True, + ) + + # Send to all configured friends + for friend_id in self.to_friend_ids: + payload = {"user_id": friend_id, "message": markdown_content} + try: + async with self.session.post( + f"{self.url}/send_private_msg", json=payload, headers=self.headers + ) as response: + if response.status != 200: + text = await response.text() + logger.error( + f"Failed to send OneBot private message to {friend_id}: {response.status}, {text}" + ) + else: + logger.debug( + f"OneBot private notification sent to {friend_id}" + ) + except Exception as e: + logger.error( + f"Error sending OneBot private notification to {friend_id}: {e}", + exc_info=True, + ) + + async def close(self) -> None: + """Close the aiohttp session""" + if self.session is not None: + await self.session.close() + self.session = None + + class Notifier: """Main notifier class that manages multiple notification providers. This class handles the configuration file and initializes the appropriate notifier instances based on the provided settings.""" @@ -197,6 +294,11 @@ def __init__(self, config: dict): if lark_config.get("enabled", False): self.notifiers.append(LarkNotifier(lark_config)) + # Initialize onebot-11 notifier if configured + onebot_config = config.get("onebot-11", {}) + if onebot_config.get("enabled", False): + self.notifiers.append(OneBotNotifier(onebot_config)) + logger.info( f"Notifier system initialized with {len(self.notifiers)} provider(s)" )