from __future__ import annotations import logging from yuxi.channel.extensions.helpscout.auth import HelpScoutAuth from yuxi.channel.extensions.helpscout.client import HelpScoutClient from yuxi.channel.extensions.helpscout.config import resolve_account from yuxi.channel.extensions.helpscout.template import text_to_html_body from yuxi.channel.extensions.helpscout.types import OutboundResult logger = logging.getLogger(__name__) class HelpScoutOutbound: delivery_mode = "direct" chunker_mode = "length" text_chunk_limit: int = 10000 async def send_text( self, target_id: str, content: str, *, reply_to_id: str | None = None, thread_id: str | None = None, account_id: str | None = None, ) -> OutboundResult: account = resolve_account(account_id or "default") if not account.get("app_id") or not account.get("app_secret") or int(account.get("mailbox_id", 0)) <= 0: logger.error("Help Scout outbound: account not configured") return OutboundResult(success=False, error="account_not_configured") auto_reply_mode = account.get("auto_reply_mode", "draft") if auto_reply_mode == "off": logger.debug("Help Scout outbound: auto_reply_mode=off, skipping send") return OutboundResult(success=False, error="auto_reply_disabled") conversation_id = int(target_id.split(":")[-1]) client = self._build_client(account) is_draft = auto_reply_mode == "draft" html_content = text_to_html_body(content) try: result = await client.reply_conversation( conversation_id=conversation_id, text=content, html=html_content, draft=is_draft, ) reply_id = result.get("id", "") logger.info( "Help Scout reply %s: conv=%d reply_id=%s", "drafted" if is_draft else "sent", conversation_id, reply_id, ) return OutboundResult(success=True, message_id=str(reply_id)) except Exception as e: logger.exception("Help Scout send_text failed: conv=%d", conversation_id) return OutboundResult(success=False, error="exception", detail=str(e)) finally: await client.close() async def send_draft( self, conversation_id: int, content: str, account_id: str | None = None, ) -> OutboundResult: account = resolve_account(account_id or "default") client = self._build_client(account) try: result = await client.reply_conversation( conversation_id=conversation_id, text=content, draft=True, ) return OutboundResult(success=True, message_id=str(result.get("id", ""))) except Exception as e: logger.exception("Help Scout send_draft failed: conv=%d", conversation_id) return OutboundResult(success=False, error="exception", detail=str(e)) finally: await client.close() async def add_note( self, conversation_id: int, text: str, account_id: str | None = None, ) -> OutboundResult: account = resolve_account(account_id or "default") client = self._build_client(account) try: result = await client.create_note( conversation_id=conversation_id, text=text, ) return OutboundResult(success=True, message_id=str(result.get("id", ""))) except Exception as e: logger.exception("Help Scout add_note failed: conv=%d", conversation_id) return OutboundResult(success=False, error="exception", detail=str(e)) finally: await client.close() def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]: if len(text) <= limit: return [text] chunks = [] while len(text) > limit: break_pos = text.rfind("\n", 0, limit) if break_pos == -1: break_pos = limit chunks.append(text[:break_pos]) text = text[break_pos:].lstrip("\n") if text: chunks.append(text) return chunks async def send_media( self, target_id: str, media_url: str, media_type: str, reply_to_id: str | None = None, thread_id: str | None = None, ) -> OutboundResult: account = resolve_account("default") conversation_id = int(target_id.split(":")[-1]) client = self._build_client(account) try: msg = f"[{media_type.upper()}] {media_url}" result = await client.reply_conversation( conversation_id=conversation_id, text=msg, draft=True, ) return OutboundResult(success=True, message_id=str(result.get("id", ""))) except Exception as e: logger.exception("Help Scout send_media failed: conv=%d", conversation_id) return OutboundResult(success=False, error="exception", detail=str(e)) finally: await client.close() def sanitize_text(self, text: str, payload: object) -> str: return text @staticmethod def _build_client(account: dict) -> HelpScoutClient: auth = HelpScoutAuth(account["app_id"], account["app_secret"]) return HelpScoutClient(auth)