import os import re import hmac import hashlib import logging from datetime import datetime from typing import Optional import aiohttp logger = logging.getLogger(__name__) MAX_API_URL = "https://platform-api.max.ru" class MaxAPIClient: def __init__(self, token: str, test_mode: bool = False): self.token = token self.test_mode = test_mode self._session: Optional[aiohttp.ClientSession] = None self.test_messages: list = [] async def _get_session(self) -> aiohttp.ClientSession: if self._session is None or self._session.closed: self._session = aiohttp.ClientSession() return self._session async def close(self): if self._session and not self._session.closed: await self._session.close() def _headers(self) -> dict: return {"Authorization": self.token, "Content-Type": "application/json"} async def _request(self, method: str, path: str, **kwargs) -> dict: if self.test_mode: logger.info(f"[TEST_MODE] {method} {path} kwargs={kwargs}") return {"ok": True, "test_mode": True} session = await self._get_session() url = f"{MAX_API_URL}{path}" async with session.request(method, url, headers=self._headers(), **kwargs) as resp: if resp.status >= 400: body = await resp.text() logger.error(f"Max API error {resp.status}: {body}") resp.raise_for_status() return await resp.json() async def get_me(self) -> dict: return await self._request("GET", "/me") async def send_message(self, user_id: int = None, chat_id: int = None, text: str = "", attachments: list = None, format: str = None, disable_link_preview: bool = False, notify: bool = True) -> dict: body = {"text": text} if attachments: body["attachments"] = attachments if format: body["format"] = format if not disable_link_preview: body["disable_link_preview"] = False if not notify: body["notify"] = False params = {} if user_id: params["user_id"] = user_id if chat_id: params["chat_id"] = chat_id if self.test_mode: msg = {"role": "bot", "text": text, "format": format} if attachments: msg["attachments"] = attachments self.test_messages.append(msg) return await self._request("POST", "/messages", json=body, params=params) async def send_message_with_keyboard(self, user_id: int, text: str, buttons: list, format: str = None) -> dict: attachments = [{ "type": "inline_keyboard", "payload": {"buttons": buttons} }] return await self.send_message(user_id=user_id, text=text, attachments=attachments, format=format) async def edit_message(self, message_id: int, text: str, attachments: list = None) -> dict: body = {"text": text} if attachments: body["attachments"] = attachments return await self._request("PUT", f"/messages/{message_id}", json=body) async def get_message(self, message_id: int) -> dict: return await self._request("GET", f"/messages/{message_id}") async def get_messages(self, user_id: int = None, chat_id: int = None, limit: int = 10) -> dict: params = {"limit": limit} if user_id: params["user_id"] = user_id if chat_id: params["chat_id"] = chat_id return await self._request("GET", "/messages", params=params) async def answer_callback(self, callback_id: str, text: str = "", show_alert: bool = False) -> dict: body = {"callback_id": callback_id} if text: body["text"] = text if show_alert: body["show_alert"] = True return await self._request("POST", "/answers", json=body) async def subscribe_webhook(self, url: str, update_types: list = None, secret: str = None) -> dict: body = {"url": url} if update_types: body["update_types"] = update_types if secret: body["secret"] = secret return await self._request("POST", "/subscriptions", json=body) async def get_subscriptions(self) -> dict: return await self._request("GET", "/subscriptions") async def unsubscribe_webhook(self) -> dict: return await self._request("DELETE", "/subscriptions") async def upload_file(self, file_path: str) -> dict: session = await self._get_session() url = f"{MAX_API_URL}/uploads" with open(file_path, "rb") as f: data = aiohttp.FormData() data.add_field("file", f, filename=os.path.basename(file_path)) async with session.post(url, headers={"Authorization": self.token}, data=data) as resp: resp.raise_for_status() return await resp.json() def verify_webhook_secret(self, header_value: str, expected_secret: str) -> bool: if not expected_secret: return True return hmac.compare_digest(header_value, expected_secret) async def send_file(self, user_id: int, file_path: str, caption: str = "") -> dict: upload = await self.upload_file(file_path) file_id = upload.get("file_id", "") body = {"text": caption, "attachments": [{"type": "file", "payload": {"file_id": file_id}}]} return await self._request("POST", "/messages", json=body, params={"user_id": user_id}) async def get_file_url(self, file_id: str) -> str: resp = await self._request("GET", f"/uploads/{file_id}") return resp.get("url", "") async def send_contact_request(self, user_id: int, text: str = "Пожалуйста, отправьте ваш контакт") -> dict: attachments = [{ "type": "contact_request", "payload": {"text": text} }] return await self.send_message(user_id=user_id, text=text, attachments=attachments) def verify_contact_hash(self, vcf_info: str, access_token: str, expected_hash: str) -> bool: vcf_normalized = vcf_info.replace("\\r\\n", "\r\n") computed = hmac.new(access_token.encode(), vcf_normalized.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(computed, expected_hash)