feat: TenChat auto-posting via refresh token (TCRF) auth
- 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
This commit is contained in:
1 parent
09b22446d4
commit
849e5c472a
6 files changed
+441
-2
No files matched your search
@@ -0,0 +1,176 @@
|
|||||||
|
"""Публикация постов в 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
|
||||||
+81
-1
@@ -20,6 +20,7 @@ from ai_core.telegram_publisher import (
|
|||||||
publish_post,
|
publish_post,
|
||||||
resolve_chat,
|
resolve_chat,
|
||||||
)
|
)
|
||||||
|
from ai_core.tenchat_publisher import publish_post as publish_post_tenchat
|
||||||
from cabinet_service.brand_doc_generator import generate_brand_docx
|
from cabinet_service.brand_doc_generator import generate_brand_docx
|
||||||
from config import SECRET_KEY
|
from config import SECRET_KEY
|
||||||
from database import (
|
from database import (
|
||||||
@@ -63,9 +64,13 @@ from database import (
|
|||||||
save_post_for_user,
|
save_post_for_user,
|
||||||
save_post_image,
|
save_post_image,
|
||||||
save_provider_settings,
|
save_provider_settings,
|
||||||
|
clear_tenchat_credentials,
|
||||||
|
get_tenchat_credentials,
|
||||||
|
save_tenchat_credentials,
|
||||||
update_plan_status,
|
update_plan_status,
|
||||||
update_post_content,
|
update_post_content,
|
||||||
update_task_status,
|
update_task_status,
|
||||||
|
update_tenchat_tokens,
|
||||||
update_user_field_by_id,
|
update_user_field_by_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -352,6 +357,48 @@ def provider_submit(
|
|||||||
return RedirectResponse("/provider", status_code=303)
|
return RedirectResponse("/provider", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/tenchat")
|
||||||
|
def tenchat_form(request: Request, user: dict = Depends(current_user)):
|
||||||
|
creds = get_tenchat_credentials(user["id"])
|
||||||
|
return render(request, "tenchat.html", user=user, creds=creds)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tenchat")
|
||||||
|
def tenchat_submit(
|
||||||
|
request: Request,
|
||||||
|
tenchat_username: str = Form(...),
|
||||||
|
tenchat_tcrf: str = Form(...),
|
||||||
|
user: dict = Depends(current_user),
|
||||||
|
):
|
||||||
|
errors = []
|
||||||
|
username = tenchat_username.strip().lstrip("@")
|
||||||
|
tcrf = tenchat_tcrf.strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
errors.append("Укажите username в TenChat.")
|
||||||
|
if not tcrf:
|
||||||
|
errors.append("Укажите ключ TCRF (refresh token).")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return render(
|
||||||
|
request, "tenchat.html", status_code=400,
|
||||||
|
user=user, creds=get_tenchat_credentials(user["id"]),
|
||||||
|
errors=errors,
|
||||||
|
form={"tenchat_username": username, "tenchat_tcrf": tcrf},
|
||||||
|
)
|
||||||
|
|
||||||
|
save_tenchat_credentials(user["id"], username, tcrf)
|
||||||
|
flash(request, "TenChat подключён: @%s. Refresh token сохранён." % username)
|
||||||
|
return RedirectResponse("/tenchat", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tenchat/disconnect")
|
||||||
|
def tenchat_disconnect(request: Request, user: dict = Depends(current_user)):
|
||||||
|
clear_tenchat_credentials(user["id"])
|
||||||
|
flash(request, "TenChat отключён. Ключи сессии удалены.")
|
||||||
|
return RedirectResponse("/tenchat", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/generate")
|
@app.get("/generate")
|
||||||
def generate_form(request: Request, user: dict = Depends(current_user),
|
def generate_form(request: Request, user: dict = Depends(current_user),
|
||||||
plan_id: str = Query(""), platform: str = Query(""),
|
plan_id: str = Query(""), platform: str = Query(""),
|
||||||
@@ -969,8 +1016,11 @@ def publish_form(request: Request, post_id: int, user: dict = Depends(current_us
|
|||||||
if not post:
|
if not post:
|
||||||
raise HTTPException(status_code=404, detail="Пост не найден")
|
raise HTTPException(status_code=404, detail="Пост не найден")
|
||||||
channels = get_channels_for_user(user["id"])
|
channels = get_channels_for_user(user["id"])
|
||||||
|
tenchat = get_tenchat_credentials(user["id"])
|
||||||
return render(request, "publish.html", user=user, post=post, channels=channels,
|
return render(request, "publish.html", user=user, post=post, channels=channels,
|
||||||
platform_label=platform_label)
|
platform_label=platform_label,
|
||||||
|
tenchat_connected=bool(tenchat),
|
||||||
|
tenchat_username=tenchat["tenchat_username"] if tenchat else None)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/posts/{post_id}/publish")
|
@app.post("/posts/{post_id}/publish")
|
||||||
@@ -1018,6 +1068,36 @@ def publish_submit(
|
|||||||
return RedirectResponse(f"/posts/{post_id}", status_code=303)
|
return RedirectResponse(f"/posts/{post_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/posts/{post_id}/publish/tenchat")
|
||||||
|
def publish_tenchat_submit(request: Request, post_id: int, user: dict = Depends(current_user)):
|
||||||
|
post = get_post_by_id_for_user(post_id, user["id"])
|
||||||
|
if not post:
|
||||||
|
raise HTTPException(status_code=404, detail="Пост не найден")
|
||||||
|
|
||||||
|
creds = get_tenchat_credentials(user["id"])
|
||||||
|
if not creds:
|
||||||
|
flash(request, "❌ TenChat не подключён. Настройте ключи сессии на странице /tenchat.")
|
||||||
|
return RedirectResponse(f"/posts/{post_id}/publish", status_code=303)
|
||||||
|
|
||||||
|
ok, result, new_tcrf = publish_post_tenchat(
|
||||||
|
creds["tenchat_tcrf"],
|
||||||
|
post["content"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Обновляем TCRF (одноразовый — после каждого refresh он меняется)
|
||||||
|
if new_tcrf:
|
||||||
|
update_tenchat_tokens(user["id"], new_tcrf)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
mark_post_published(post_id, user["id"], "TenChat")
|
||||||
|
url = result.get("url") if isinstance(result, dict) else None
|
||||||
|
flash(request, f"✅ Пост опубликован в TenChat" + (f": {url}" if url else ""))
|
||||||
|
else:
|
||||||
|
flash(request, f"❌ Ошибка публикации в TenChat: {result}")
|
||||||
|
|
||||||
|
return RedirectResponse(f"/posts/{post_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
# === ФОНОВАЯ ГЕНЕРАЦИЯ (AJAX) ===
|
# === ФОНОВАЯ ГЕНЕРАЦИЯ (AJAX) ===
|
||||||
|
|
||||||
def _run_post_generation(task_id, user_id, platform, style, wishes, survey_id, plan_id=None):
|
def _run_post_generation(task_id, user_id, platform, style, wishes, survey_id, plan_id=None):
|
||||||
|
|||||||
@@ -22,5 +22,11 @@
|
|||||||
<a href="/provider" class="copy-btn">⚙ Настроить провайдера</a>
|
<a href="/provider" class="copy-btn">⚙ Настроить провайдера</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>TenChat</h2>
|
||||||
|
<p>Подключите аккаунт TenChat, чтобы публиковать посты автоматически.</p>
|
||||||
|
<a href="/tenchat" class="copy-btn">🔑 Настроить TenChat</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">📤 Опубликовать</button>
|
<button type="submit">📤 Опубликовать в Telegram</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -36,6 +36,22 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tenchat_connected %}
|
||||||
|
<div class="card" style="margin-top:15px;">
|
||||||
|
<h2>TenChat</h2>
|
||||||
|
<p>Опубликовать этот пост в ваш профиль TenChat (@{{ tenchat_username }}).</p>
|
||||||
|
<form method="post" action="/posts/{{ post.id }}/publish/tenchat" class="form" style="margin:0;">
|
||||||
|
<button type="submit">📤 Опубликовать в TenChat</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card" style="margin-top:15px;">
|
||||||
|
<h2>TenChat</h2>
|
||||||
|
<p>Подключите TenChat, чтобы публиковать посты в профиль.</p>
|
||||||
|
<a href="/tenchat" class="copy-btn">🔑 Настроить TenChat</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="post-actions">
|
<div class="post-actions">
|
||||||
<a href="/posts/{{ post.id }}" class="copy-btn">← Назад к посту</a>
|
<a href="/posts/{{ post.id }}" class="copy-btn">← Назад к посту</a>
|
||||||
<a href="/channels" class="copy-btn">⚙ Управление каналами</a>
|
<a href="/channels" class="copy-btn">⚙ Управление каналами</a>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}TenChat — Личный кабинет{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Интеграция с TenChat</h1>
|
||||||
|
<p class="form-hint">Подключите свой аккаунт TenChat, чтобы публиковать посты автоматически. Refresh token хранится только у вас и используется для публикации от вашего имени.</p>
|
||||||
|
|
||||||
|
{% if creds %}
|
||||||
|
<div class="card">
|
||||||
|
<p style="margin:0">
|
||||||
|
<strong>Подключено:</strong> @{{ creds.tenchat_username }}
|
||||||
|
| <span class="form-hint">подключено {{ creds.tenchat_connected_at }}</span>
|
||||||
|
{% if creds.tenchat_last_refresh %}
|
||||||
|
| <span class="form-hint">обновлено {{ creds.tenchat_last_refresh }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<p style="margin:0">⚠️ TenChat не подключён. Введите refresh token ниже, чтобы публиковать посты.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/tenchat" class="form">
|
||||||
|
<h2>🔑 Refresh token TenChat</h2>
|
||||||
|
|
||||||
|
<label>Username в TenChat
|
||||||
|
<input type="text" name="tenchat_username"
|
||||||
|
value="{{ form.tenchat_username if form else (creds.tenchat_username if creds else '') }}"
|
||||||
|
placeholder="например, kavalar" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>Ключ TCRF (refresh token)
|
||||||
|
<input type="password" name="tenchat_tcrf"
|
||||||
|
value="{{ form.tenchat_tcrf if form else '' }}"
|
||||||
|
placeholder="вставьте значение куки TCRF" required>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="submit">💾 Сохранить ключи</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if creds %}
|
||||||
|
<form method="post" action="/tenchat/disconnect" class="form" style="margin-top:15px"
|
||||||
|
onsubmit="return confirm('Отключить TenChat? Ключи будут удалены.');">
|
||||||
|
<button type="submit" style="background: var(--danger, #dc3545);">🔌 Отключить TenChat</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card" style="margin-top:20px; background: var(--bg-secondary);">
|
||||||
|
<h2>📖 Как получить refresh token</h2>
|
||||||
|
<ol style="margin:5px 0 0 18px; font-size: 14px; line-height: 1.7;">
|
||||||
|
<li>Откройте <a href="https://tenchat.ru" target="_blank" rel="noopener">tenchat.ru</a> и войдите в свой аккаунт.</li>
|
||||||
|
<li>Нажмите <strong>F12</strong> (или <strong>Ctrl+Shift+I</strong>), чтобы открыть инструменты разработчика.</li>
|
||||||
|
<li>Перейдите на вкладку <strong>Application</strong> (Приложение).</li>
|
||||||
|
<li>В левом меню выберите <strong>Cookies</strong> → <strong>https://tenchat.ru</strong>.</li>
|
||||||
|
<li>Найдите куку <strong>TCRF</strong> и скопируйте её значение.</li>
|
||||||
|
<li>Вставьте значение в поле выше и нажмите «Сохранить ключи».</li>
|
||||||
|
</ol>
|
||||||
|
<p class="form-hint" style="margin-top:10px">
|
||||||
|
⚠️ Refresh token живёт ~14 дней и автоматически обновляется при каждой публикации.
|
||||||
|
Если публикация перестала работать — обновите ключ заново.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:20px"><a href="/profile/edit">← Назад в профиль</a> · <a href="/dashboard">← На главную</a></p>
|
||||||
|
{% endblock %}
|
||||||
+96
@@ -37,6 +37,25 @@ def create_db():
|
|||||||
if 'password_hash' not in columns:
|
if 'password_hash' not in columns:
|
||||||
cursor.execute('ALTER TABLE users ADD COLUMN password_hash TEXT')
|
cursor.execute('ALTER TABLE users ADD COLUMN password_hash TEXT')
|
||||||
print("✅ Колонка 'password_hash' добавлена в таблицу users")
|
print("✅ Колонка 'password_hash' добавлена в таблицу users")
|
||||||
|
# === TenChat интеграция ===
|
||||||
|
if 'tenchat_username' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE users ADD COLUMN tenchat_username TEXT')
|
||||||
|
print("✅ Колонка 'tenchat_username' добавлена в таблицу users")
|
||||||
|
if 'tenchat_tcaf' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE users ADD COLUMN tenchat_tcaf TEXT')
|
||||||
|
print("✅ Колонка 'tenchat_tcaf' добавлена в таблицу users")
|
||||||
|
if 'tenchat_tcrf' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE users ADD COLUMN tenchat_tcrf TEXT')
|
||||||
|
print("✅ Колонка 'tenchat_tcrf' добавлена в таблицу users")
|
||||||
|
if 'tenchat_session' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE users ADD COLUMN tenchat_session TEXT')
|
||||||
|
print("✅ Колонка 'tenchat_session' добавлена в таблицу users")
|
||||||
|
if 'tenchat_connected_at' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE users ADD COLUMN tenchat_connected_at TIMESTAMP')
|
||||||
|
print("✅ Колонка 'tenchat_connected_at' добавлена в таблицу users")
|
||||||
|
if 'tenchat_last_refresh' not in columns:
|
||||||
|
cursor.execute('ALTER TABLE users ADD COLUMN tenchat_last_refresh TIMESTAMP')
|
||||||
|
print("✅ Колонка 'tenchat_last_refresh' добавлена в таблицу users")
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS posts (
|
CREATE TABLE IF NOT EXISTS posts (
|
||||||
@@ -963,5 +982,82 @@ def get_task(task_id):
|
|||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
# === TenChat интеграция ===
|
||||||
|
|
||||||
|
def save_tenchat_credentials(user_id, username, tcrf):
|
||||||
|
"""Сохраняет ключи сессии TenChat для пользователя.
|
||||||
|
|
||||||
|
Хранит только TCRF (refresh token) — он используется для получения
|
||||||
|
access_token через refresh. TCRF одноразовый, обновляется после каждого refresh.
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect('users.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
'''UPDATE users SET
|
||||||
|
tenchat_username = ?,
|
||||||
|
tenchat_tcrf = ?,
|
||||||
|
tenchat_connected_at = COALESCE(tenchat_connected_at, CURRENT_TIMESTAMP),
|
||||||
|
tenchat_last_refresh = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?''',
|
||||||
|
(username, tcrf, user_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_tenchat_credentials(user_id):
|
||||||
|
"""Возвращает ключи сессии TenChat пользователя (или None).
|
||||||
|
|
||||||
|
Возвращает dict с tenchat_username и tenchat_tcrf.
|
||||||
|
"""
|
||||||
|
conn = _row_conn()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
'''SELECT tenchat_username, tenchat_tcrf,
|
||||||
|
tenchat_connected_at, tenchat_last_refresh
|
||||||
|
FROM users WHERE id = ?''',
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
creds = dict(row)
|
||||||
|
if not creds.get('tenchat_tcrf'):
|
||||||
|
return None
|
||||||
|
return creds
|
||||||
|
|
||||||
|
|
||||||
|
def update_tenchat_tokens(user_id, new_tcrf):
|
||||||
|
"""Обновляет TCRF после refresh (TCRF одноразовый!)."""
|
||||||
|
conn = sqlite3.connect('users.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
'UPDATE users SET tenchat_tcrf = ?, tenchat_last_refresh = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
|
(new_tcrf, user_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_tenchat_credentials(user_id):
|
||||||
|
"""Удаляет ключи сессии TenChat пользователя."""
|
||||||
|
conn = sqlite3.connect('users.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
'''UPDATE users SET
|
||||||
|
tenchat_username = NULL,
|
||||||
|
tenchat_tcaf = NULL,
|
||||||
|
tenchat_tcrf = NULL,
|
||||||
|
tenchat_session = NULL,
|
||||||
|
tenchat_connected_at = NULL,
|
||||||
|
tenchat_last_refresh = NULL
|
||||||
|
WHERE id = ?''',
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# Создаем базу при первом импорте
|
# Создаем базу при первом импорте
|
||||||
create_db()
|
create_db()
|
||||||
Reference in new issue
Block a user