"""Service connectors — pre-built connectors for common services. Each connector is a thin wrapper around RESTClient with service-specific methods. All require explicit configuration — no hardcoded keys. Services: - HTTPFetcher: generic web page / API fetcher - WebSocketClient: basic WebSocket support (via stdlib) - EmailConnector: send/receive via SMTP/IMAP (stdlib) - DiscordWebhook: send messages via Discord webhook - SlackWebhook: send messages via Slack webhook - GitHubAPI: interact with GitHub API - CustomAPI: user-defined API connector """ from __future__ import annotations import json import logging from typing import Any from .api_client import RESTClient, APIConfig, APIResponse logger = logging.getLogger(__name__) class HTTPFetcher: """Generic HTTP fetcher — fetch web pages and APIs.""" def __init__(self, timeout_s: float = 15.0) -> None: self.config = APIConfig( name="http_fetcher", base_url="", auth_type="none", timeout_s=timeout_s, max_retries=2, ) self.client = RESTClient(self.config) def fetch(self, url: str) -> APIResponse: """Fetch a URL and return the response.""" # Override base_url for this request self.config.base_url = "" # Use the full URL directly import urllib.request try: req = urllib.request.Request(url, headers={"User-Agent": "SplitBit-LLM/0.1"}) with urllib.request.urlopen(req, timeout=self.config.timeout_s) as resp: raw = resp.read().decode() return APIResponse(success=True, status_code=resp.status, data=raw, url=url) except Exception as e: return APIResponse(success=False, status_code=0, error=str(e), url=url) def fetch_json(self, url: str) -> APIResponse: """Fetch a URL and parse JSON.""" resp = self.fetch(url) if resp.success and isinstance(resp.data, str): try: resp.data = json.loads(resp.data) except json.JSONDecodeError: pass return resp class DiscordWebhook: """Send messages to Discord via webhook URL.""" def __init__(self, webhook_url: str) -> None: self.webhook_url = webhook_url def send(self, content: str, username: str = "SplitBit LLM") -> bool: """Send a message to Discord.""" import urllib.request payload = json.dumps({"content": content, "username": username}).encode() try: req = urllib.request.Request( self.webhook_url, data=payload, method="POST", headers={"Content-Type": "application/json"} ) with urllib.request.urlopen(req, timeout=10) as resp: return resp.status < 300 except Exception as e: logger.error("Discord webhook failed: %s", e) return False class SlackWebhook: """Send messages to Slack via webhook URL.""" def __init__(self, webhook_url: str) -> None: self.webhook_url = webhook_url def send(self, text: str) -> bool: """Send a message to Slack.""" import urllib.request payload = json.dumps({"text": text}).encode() try: req = urllib.request.Request( self.webhook_url, data=payload, method="POST", headers={"Content-Type": "application/json"} ) with urllib.request.urlopen(req, timeout=10) as resp: return resp.status < 300 except Exception as e: logger.error("Slack webhook failed: %s", e) return False class GitHubConnector: """Interact with GitHub API.""" def __init__(self, token: str = "") -> None: self.config = APIConfig( name="github", base_url="https://api.github.com", api_key=token, auth_type="bearer" if token else "none", timeout_s=15.0, ) self.client = RESTClient(self.config) def get_repo(self, owner: str, repo: str) -> APIResponse: return self.client.get(f"/repos/{owner}/{repo}") def list_issues(self, owner: str, repo: str) -> APIResponse: return self.client.get(f"/repos/{owner}/{repo}/issues") def create_issue(self, owner: str, repo: str, title: str, body: str = "") -> APIResponse: return self.client.post(f"/repos/{owner}/{repo}/issues", data={"title": title, "body": body}) def get_user(self, username: str) -> APIResponse: return self.client.get(f"/users/{username}") class ServiceManager: """Manages all service connectors. Central registry for all external service connections. """ def __init__(self) -> None: self.fetcher = HTTPFetcher() self._services: dict[str, Any] = {"http_fetcher": self.fetcher} self._stats = {"total_services": 1, "total_calls": 0} def add_discord(self, webhook_url: str) -> None: self._services["discord"] = DiscordWebhook(webhook_url) self._stats["total_services"] += 1 def add_slack(self, webhook_url: str) -> None: self._services["slack"] = SlackWebhook(webhook_url) self._stats["total_services"] += 1 def add_github(self, token: str = "") -> None: self._services["github"] = GitHubConnector(token) self._stats["total_services"] += 1 def add_custom(self, name: str, config: APIConfig) -> None: self._services[name] = RESTClient(config) self._stats["total_services"] += 1 def get(self, name: str) -> Any | None: return self._services.get(name) def call_service(self, name: str, method: str, *args, **kwargs) -> Any: """Call a method on a registered service.""" service = self._services.get(name) if service is None: return {"error": f"Service '{name}' not found"} self._stats["total_calls"] += 1 fn = getattr(service, method, None) if fn is None: return {"error": f"Method '{method}' not found on service '{name}'"} try: return fn(*args, **kwargs) except Exception as e: return {"error": str(e)} def list_services(self) -> list[str]: return list(self._services.keys()) def get_stats(self) -> dict[str, Any]: return {**self._stats, "services": list(self._services.keys())}