from __future__ import annotations import logging import time import httpx from .errors import MAX_RETRIES, MAX_RETRY_DELAY_MS, MSTeamsError, classify_http_error, is_retryable from .format import chunk_text from .sdk import BotFrameworkAdapter, build_message_activity, build_typing_activity from .types import MSTeamsErrorCode, StoredConversationReference logger = logging.getLogger(__name__) async def _retry_send( adapter: BotFrameworkAdapter, ref: StoredConversationReference, activity: dict, max_retries: int = MAX_RETRIES, ) -> dict: last_error: Exception | None = None for attempt in range(max_retries + 1): try: return await adapter.send_activity(ref, activity) except httpx.HTTPStatusError as e: status = e.response.status_code error_code = classify_http_error(status) if is_retryable(error_code) and attempt < max_retries: delay = min(1000 * (2**attempt), MAX_RETRY_DELAY_MS) / 1000.0 logger.warning( "Send attempt %d/%d failed (status=%d), retrying in %.1fs", attempt + 1, max_retries, status, delay, ) time.sleep(delay) continue raise MSTeamsError(error_code, f"HTTP {status}: {e.response.text[:200]}", status) from e except httpx.NetworkError as e: if attempt < max_retries: delay = min(1000 * (2**attempt), MAX_RETRY_DELAY_MS) / 1000.0 logger.warning("Send attempt %d/%d network error, retrying in %.1fs", attempt + 1, max_retries, delay) time.sleep(delay) continue raise MSTeamsError(MSTeamsErrorCode.NETWORK, str(e)) from e raise last_error # type: ignore[misc] async def send_text( adapter: BotFrameworkAdapter, ref: StoredConversationReference, text: str, *, reply_to_id: str | None = None, importance: str | None = None, ) -> list[str]: chunks = chunk_text(text) message_ids = [] for i, chunk in enumerate(chunks): reply = reply_to_id if i == 0 else None activity = build_message_activity( chunk, reply_to_id=reply, tenant_id=ref.tenant_id, ai_generated=True, importance=importance, ) result = await _retry_send(adapter, ref, activity) message_ids.append(result.get("id", "")) return message_ids async def send_media( adapter: BotFrameworkAdapter, ref: StoredConversationReference, media_url: str, media_type: str, *, name: str = "", reply_to_id: str | None = None, ) -> str: activity = { "type": "message", "text": name or "Media", "attachments": [ { "contentType": media_type, "contentUrl": media_url, "name": name or "file", } ], "channelData": {}, } if reply_to_id: activity["replyToId"] = reply_to_id if ref.tenant_id: activity["channelData"]["tenant"] = {"id": ref.tenant_id} result = await _retry_send(adapter, ref, activity) return result.get("id", "") async def send_typing_indicator( adapter: BotFrameworkAdapter, ref: StoredConversationReference, ) -> None: try: activity = build_typing_activity() await adapter.send_activity(ref, activity) except Exception as e: logger.debug("Failed to send typing indicator: %s", e) async def edit_message( adapter: BotFrameworkAdapter, ref: StoredConversationReference, message_id: str, text: str, ) -> None: activity = build_message_activity( text, tenant_id=ref.tenant_id, ai_generated=True, ) await adapter.update_activity(ref.service_url, ref.conversation_id, message_id, activity) async def delete_message( adapter: BotFrameworkAdapter, ref: StoredConversationReference, message_id: str, ) -> None: await adapter.delete_activity(ref.service_url, ref.conversation_id, message_id) async def send_quoted_reply( adapter: BotFrameworkAdapter, ref: StoredConversationReference, text: str, *, reply_to_id: str | None = None, importance: str | None = None, ) -> list[str]: return await send_text(adapter, ref, text, reply_to_id=reply_to_id, importance=importance) async def send_forwarded_message( adapter: BotFrameworkAdapter, ref: StoredConversationReference, text: str, *, reply_to_id: str | None = None, importance: str | None = None, ) -> list[str]: return await send_text(adapter, ref, text, reply_to_id=reply_to_id, importance=importance)