"""GRAM SDK для ботов-площадок (MAX). Зависимость только aiohttp. Две модели подключения (выберите одну — какую скажет менеджер сети): ── PUSH (сеть шлёт рекламу сама, вы дали токен бота) — 1 строка: from gram_sdk import send_ad await send_ad("gram_ВАШ_КЛЮЧ", user_id) # в удачный момент для юзера ── PULL / self-serve (токен НЕ передаёте, бот шлёт рекламу САМ) — 3 строки: from gram_sdk import fetch_ad, confirm_ad ad = await fetch_ad("gram_ВАШ_КЛЮЧ", user_id) if ad: # отправьте объявление юзеру СВОИМ ботом: # текст: ad["html"] с format="html", если ad["format"]=="html", иначе ad["text"] # кнопки: ad["buttons"] — список [{"text","url"}], КАЖДАЯ своим рядом клавиатуры text = ad["html"] if ad.get("format") == "html" else ad["text"] fmt = "html" if ad.get("format") == "html" else None rows = [[{"type": "link", "text": b["text"], "url": b["url"]}] for b in (ad.get("buttons") or [])] await my_bot_send(user_id, text, buttons=rows, fmt=fmt) await confirm_ad("gram_ВАШ_КЛЮЧ", ad["imp_token"]) # только ПОСЛЕ успешной отправки Рекомендуемый ритм: раз в 3–5 действий юзера + свой кулдаун ~5 сек на человека. ── СТАТИСТИКА (показы + заработок площадки по дням): from gram_sdk import stats s = await stats("gram_ВАШ_КЛЮЧ", days=30) # days ∈ 7|30|90 if s: print(s["totals"]) # {"impressions": N, "earnings_rub": X} for row in s["series"]: # [{"date","impressions","earnings_rub"}, ...] по дням ... Ключ выдаётся при подключении к сети GRAM. Ответы можно игнорировать: если рекламы нет / сработал частотный лимит / юзер недоступен — вернётся None или {"sent": false}. Ошибки сети подавляются, ваш бот не упадёт и не замедлится. Наглядная страница статистики (без кабинета, открыть в браузере): https://hubsmaxbot.ru/pub/stats?key=gram_ВАШ_КЛЮЧ ВАЖНО: дёргайте это только в ответ на действие живого юзера, не рассылками — иначе площадку отключат. confirm_ad вызывайте ТОЛЬКО если реально отправили рекламу (иначе показ засчитан не будет — и это правильно). """ from __future__ import annotations import aiohttp GRAM_BASE_URL = "https://hubsmaxbot.ru" # базовый URL сети GRAM async def _post(path: str, api_key: str, payload: dict, base_url: str | None, timeout_sec: float) -> dict | None: url = f"{(base_url or GRAM_BASE_URL).rstrip('/')}{path}" try: tmo = aiohttp.ClientTimeout(total=timeout_sec) async with aiohttp.ClientSession(timeout=tmo) as s: async with s.post(url, json=payload, headers={"Authorization": f"Bearer {api_key}"}) as r: if r.status != 200: return {"_http": r.status} return await r.json() except Exception: return None async def _get(path: str, api_key: str, base_url: str | None, timeout_sec: float) -> dict | None: url = f"{(base_url or GRAM_BASE_URL).rstrip('/')}{path}" try: tmo = aiohttp.ClientTimeout(total=timeout_sec) async with aiohttp.ClientSession(timeout=tmo) as s: async with s.get(url, headers={"Authorization": f"Bearer {api_key}"}) as r: if r.status != 200: return {"_http": r.status} return await r.json() except Exception: return None # ── PUSH ──────────────────────────────────────────────────────────────────── async def send_ad(api_key: str, user_id: int, *, base_url: str | None = None, timeout_sec: float = 12.0) -> dict: """Показать юзеру рекламу (сеть отправит сама). Никогда не бросает исключений.""" r = await _post("/pub/v1/sendpost", api_key, {"user_id": user_id}, base_url, timeout_sec) if r is None: return {"sent": False, "reason": "network"} if "_http" in r: return {"sent": False, "reason": f"http_{r['_http']}"} return r # ── PULL / self-serve ─────────────────────────────────────────────────────── async def fetch_ad(api_key: str, user_id: int, *, base_url: str | None = None, timeout_sec: float = 12.0) -> dict | None: """Запросить объявление для юзера, либо None — рекламы нет (это штатно). Возвращает dict: text (плоский), html + format=="html" (текст с форматированием — шлите его), buttons — [{"text","url"}] до 10 шт (каждая своим рядом клавиатуры), button_text/url (legacy, первая кнопка), imp_token — для confirm_ad после отправки.""" r = await _post("/pub/v1/ad/next", api_key, {"user_id": user_id}, base_url, timeout_sec) if not r or "_http" in r or not r.get("ad"): return None return {**r["ad"], "imp_token": r["imp_token"]} async def confirm_ad(api_key: str, imp_token: str, *, base_url: str | None = None, timeout_sec: float = 12.0) -> bool: """Подтвердить, что реклама отправлена (засчитывает показ). True — засчитано.""" r = await _post("/pub/v1/ad/confirm", api_key, {"imp_token": imp_token}, base_url, timeout_sec) return bool(r) and "_http" not in r and bool(r.get("ok")) # ── СТАТИСТИКА ─────────────────────────────────────────────────────────────── async def stats(api_key: str, days: int = 7, *, base_url: str | None = None, timeout_sec: float = 12.0) -> dict | None: """Статистика площадки за period. days ∈ {7, 30, 90} (иначе сервер вернёт за 7). Возвращает dict или None (сеть/ключ недоступны): { "platform": str, "days": int, "share_percent": int, "series": [{"date": "YYYY-MM-DD", "impressions": int, "earnings_rub": float}, ...], "totals": {"impressions": int, "earnings_rub": float}, } earnings_rub — ваш заработок в рублях (доля от расхода рекламодателя).""" r = await _get(f"/pub/v1/stats?days={int(days)}", api_key, base_url, timeout_sec) if not r or "_http" in r: return None return r