160 lines
6.3 KiB
Python
160 lines
6.3 KiB
Python
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)
|