- Rewrite tenchat_publisher to TCRF-only flow: refresh -> Bearer access_token -> publish - Store only TCRF (one-time refresh token, rotates on each publish) - Add /tenchat page (GET/POST/disconnect) with username + TCRF fields - Add publish-to-TenChat button on post publish page - Update DB helpers: save/get/update/clear tenchat credentials
177 lines
6.3 KiB
Python
177 lines
6.3 KiB
Python
"""Публикация постов в TenChat через внутренний API.
|
||
|
||
TenChat — SPA на Nuxt, API-префикс /gostinder/api/web/auth/...
|
||
Авторизация через Bearer-токен (access_token), получаемый через refresh.
|
||
|
||
Механизм аутентификации:
|
||
1. POST /tenchat-oauth2/oauth/token?refresh_token={TCRF}&grant_type=refresh_token
|
||
→ возвращает access_token + новый refresh_token (TCRF одноразовый!)
|
||
2. API-запросы с заголовком Authorization: Bearer {access_token}
|
||
|
||
API-поток публикации (изучен через редактор tenchat.ru/editor):
|
||
1. POST /gostinder/api/web/auth/user/post/draft
|
||
— создать черновик, получаем числовой draft_id
|
||
2. PUT /gostinder/api/web/auth/user/post/draft/publish/id/{draft_id}
|
||
— опубликовать
|
||
3. GET /gostinder/api/web/auth/post/id/{draft_id}
|
||
— получить данные поста (draft_id == post_id после публикации)
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
|
||
import requests
|
||
|
||
TENCHAT_BASE = "https://tenchat.ru"
|
||
REFRESH_ENDPOINT = "/tenchat-oauth2/oauth/token"
|
||
DRAFT_ENDPOINT = "/gostinder/api/web/auth/user/post/draft"
|
||
DRAFT_PUBLISH_ENDPOINT = "/gostinder/api/web/auth/user/post/draft/publish/id/{draft_id}"
|
||
POST_GET_ENDPOINT = "/gostinder/api/web/auth/post/id/{post_id}"
|
||
|
||
# Заголовки, имитирующие браузер
|
||
DEFAULT_HEADERS = {
|
||
"User-Agent": (
|
||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
|
||
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||
),
|
||
"Accept": "application/json, text/plain, */*",
|
||
"Accept-Language": "ru-RU,ru;q=0.9,en;q=0.8",
|
||
"Content-Type": "application/json",
|
||
"Origin": TENCHAT_BASE,
|
||
"Referer": TENCHAT_BASE + "/",
|
||
}
|
||
|
||
|
||
def refresh_access_token(tcrf: str) -> dict:
|
||
"""Обновляет access_token через refresh_token (TCRF).
|
||
|
||
TCRF — одноразовый! После использования он заменяется новым.
|
||
|
||
Возвращает dict с access_token, refresh_token (новый TCRF), expires_in.
|
||
При ошибке — dict с error.
|
||
"""
|
||
url = f"{TENCHAT_BASE}{REFRESH_ENDPOINT}?refresh_token={tcrf}&grant_type=refresh_token"
|
||
try:
|
||
resp = requests.post(url, headers=DEFAULT_HEADERS, timeout=30)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
return {
|
||
"access_token": data.get("access_token"),
|
||
"refresh_token": data.get("refresh_token"),
|
||
"expires_in": data.get("expires_in"),
|
||
}
|
||
return {"error": f"HTTP {resp.status_code}: {resp.text[:200]}"}
|
||
except Exception as e:
|
||
return {"error": str(e)}
|
||
|
||
|
||
def _request(method, url, access_token, json_body=None, timeout=30):
|
||
"""Выполняет запрос к TenChat API с Bearer-токеном.
|
||
|
||
Возвращает (ok, data_or_error).
|
||
"""
|
||
headers = {**DEFAULT_HEADERS, "Authorization": f"Bearer {access_token}"}
|
||
try:
|
||
resp = requests.request(
|
||
method, url, json=json_body, headers=headers, timeout=timeout
|
||
)
|
||
try:
|
||
data = resp.json()
|
||
except ValueError:
|
||
data = resp.text
|
||
|
||
if resp.status_code in (200, 201):
|
||
return True, data
|
||
return False, {"status": resp.status_code, "body": data}
|
||
except Exception as e:
|
||
return False, {"error": str(e)}
|
||
|
||
|
||
def _build_draft_body(text: str, tags=None):
|
||
"""Собирает тело запроса для создания черновика."""
|
||
return {
|
||
"adminTagTechNames": [],
|
||
"attachments": [],
|
||
"metadata": "",
|
||
"pictures": [],
|
||
"picturesDto": [],
|
||
"tagCodes": tags or [],
|
||
"text": text,
|
||
"title": "",
|
||
"videos": [],
|
||
"videosDto": [],
|
||
}
|
||
|
||
|
||
def create_draft(access_token: str, text: str, tags=None):
|
||
"""Создаёт черновик поста в TenChat."""
|
||
body = _build_draft_body(text, tags)
|
||
url = TENCHAT_BASE + DRAFT_ENDPOINT
|
||
ok, data = _request("POST", url, access_token, json_body=body)
|
||
if not ok:
|
||
return False, data
|
||
|
||
draft_id = None
|
||
if isinstance(data, dict):
|
||
draft_id = data.get("id") or data.get("draftId")
|
||
elif isinstance(data, (int, float)):
|
||
draft_id = int(data)
|
||
elif isinstance(data, str):
|
||
m = re.search(r"(\d+)", data)
|
||
if m:
|
||
draft_id = m.group(1)
|
||
|
||
if not draft_id:
|
||
return False, {"error": "Не удалось получить draft_id", "body": data}
|
||
return True, draft_id
|
||
|
||
|
||
def publish_draft(access_token: str, draft_id):
|
||
"""Публикует черновик поста в TenChat."""
|
||
url = TENCHAT_BASE + DRAFT_PUBLISH_ENDPOINT.format(draft_id=draft_id)
|
||
return _request("PUT", url, access_token)
|
||
|
||
|
||
def get_post(access_token: str, post_id):
|
||
"""Получает данные поста по ID."""
|
||
url = TENCHAT_BASE + POST_GET_ENDPOINT.format(post_id=post_id)
|
||
return _request("GET", url, access_token)
|
||
|
||
|
||
def publish_post(tcrf: str, text: str, tags=None):
|
||
"""Полный цикл публикации поста в TenChat.
|
||
|
||
Принимает TCRF (refresh token).
|
||
Возвращает (ok, result_or_error, new_tcrf).
|
||
new_tcrf — новый refresh_token для сохранения в БД.
|
||
"""
|
||
# 1. Получаем access_token через refresh
|
||
refresh_result = refresh_access_token(tcrf)
|
||
if "error" in refresh_result:
|
||
return False, refresh_result, None
|
||
|
||
access_token = refresh_result["access_token"]
|
||
new_tcrf = refresh_result["refresh_token"]
|
||
|
||
# 2. Создаём черновик
|
||
ok, draft_id = create_draft(access_token, text, tags)
|
||
if not ok:
|
||
return False, draft_id, new_tcrf
|
||
|
||
# 3. Публикуем
|
||
ok, result = publish_draft(access_token, draft_id)
|
||
if not ok:
|
||
return False, result, new_tcrf
|
||
|
||
# 4. Получаем post_id
|
||
post_id = draft_id
|
||
ok, post_data = get_post(access_token, draft_id)
|
||
if ok and isinstance(post_data, dict):
|
||
post_id = post_data.get("id") or post_id
|
||
|
||
return True, {
|
||
"post_id": post_id,
|
||
"url": f"{TENCHAT_BASE}/post/{post_id}" if post_id else None,
|
||
"draft_id": draft_id,
|
||
}, new_tcrf
|