Files
apuc 849e5c472a 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
2026-09-11 13:35:42 +03:00

1063 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sqlite3
# Поля профиля, которые разрешено менять через "личный кабинет"
EDITABLE_FIELDS = {'name', 'phone', 'business_type', 'description'}
def create_db():
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id INTEGER UNIQUE,
name TEXT,
phone TEXT,
business_type INTEGER,
description TEXT,
registered BOOLEAN DEFAULT 0
)
''')
cursor.execute("PRAGMA table_info(users)")
columns = [row[1] for row in cursor.fetchall()]
if 'registered' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN registered BOOLEAN DEFAULT 0')
print("✅ Колонка 'registered' добавлена в таблицу users")
if 'created_at' not in columns:
# SQLite не разрешает non-constant DEFAULT в ALTER TABLE ADD COLUMN
cursor.execute('ALTER TABLE users ADD COLUMN created_at TIMESTAMP')
cursor.execute('UPDATE users SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL')
print("✅ Колонка 'created_at' добавлена в таблицу users")
if 'email' not in columns:
# Без UNIQUE - SQLite не позволяет добавить UNIQUE через ALTER TABLE,
# уникальность email проверяется на уровне приложения (get_user_by_email)
cursor.execute('ALTER TABLE users ADD COLUMN email TEXT')
print("✅ Колонка 'email' добавлена в таблицу users")
if 'password_hash' not in columns:
cursor.execute('ALTER TABLE users ADD COLUMN password_hash TEXT')
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('''
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id INTEGER,
platform TEXT,
content TEXT,
style TEXT,
wishes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute("PRAGMA table_info(posts)")
posts_columns = [row[1] for row in cursor.fetchall()]
if 'user_id' not in posts_columns:
# Посты веб-кабинета привязаны к users.id, посты из бота - к telegram_id
cursor.execute('ALTER TABLE posts ADD COLUMN user_id INTEGER')
print("✅ Колонка 'user_id' добавлена в таблицу posts")
cursor.execute('''
CREATE TABLE IF NOT EXISTS content_plan (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
day INTEGER,
platform TEXT,
idea TEXT,
status TEXT DEFAULT 'planned',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS media (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
prompt TEXT,
style TEXT,
filename TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS brand_surveys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
title TEXT,
data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
chat_id TEXT,
title TEXT,
username TEXT,
chat_type TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS provider_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER UNIQUE,
text_provider_type TEXT DEFAULT 'piapi',
text_api_key TEXT,
text_base_url TEXT,
text_model TEXT,
image_provider_type TEXT DEFAULT 'piapi',
image_api_key TEXT,
image_base_url TEXT,
image_model TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS post_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER,
user_id INTEGER,
filename TEXT,
source TEXT DEFAULT 'upload',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
task_type TEXT NOT NULL,
status TEXT DEFAULT 'pending',
result_json TEXT,
error TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS post_revisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
)
''')
# Миграция со старой схемы (provider_type/api_key/base_url/text_model/image_model)
# на новую (text_* + image_*). Если старых колонок нет — пропускаем.
cursor.execute("PRAGMA table_info(provider_settings)")
ps_columns = [row[1] for row in cursor.fetchall()]
if 'provider_type' in ps_columns and 'text_provider_type' in ps_columns:
# Промежуточное состояние миграции — копируем данные из старых колонок в новые
try:
cursor.execute('''
UPDATE provider_settings SET
text_provider_type = COALESCE(text_provider_type, provider_type),
text_api_key = COALESCE(text_api_key, api_key),
text_base_url = COALESCE(text_base_url, base_url),
image_provider_type = COALESCE(image_provider_type, provider_type),
image_api_key = COALESCE(image_api_key, api_key),
image_base_url = COALESCE(image_base_url, base_url),
image_model = COALESCE(image_model, image_model)
WHERE api_key IS NOT NULL
''')
except Exception:
pass
# Колонка published в posts
cursor.execute("PRAGMA table_info(posts)")
posts_columns = [row[1] for row in cursor.fetchall()]
if 'published' not in posts_columns:
cursor.execute('ALTER TABLE posts ADD COLUMN published BOOLEAN DEFAULT 0')
print("✅ Колонка 'published' добавлена в таблицу posts")
if 'published_at' not in posts_columns:
cursor.execute('ALTER TABLE posts ADD COLUMN published_at TIMESTAMP')
print("✅ Колонка 'published_at' добавлена в таблицу posts")
if 'published_to' not in posts_columns:
cursor.execute('ALTER TABLE posts ADD COLUMN published_to TEXT')
print("✅ Колонка 'published_to' добавлена в таблицу posts")
conn.commit()
conn.close()
print("✅ База данных создана!")
def save_user(telegram_id, name, phone, business_type, description):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO users (telegram_id, name, phone, business_type, description, registered)
VALUES (?, ?, ?, ?, ?, 1)
''', (telegram_id, name, phone, business_type, description))
conn.commit()
conn.close()
print(f"✅ Пользователь {name} сохранен!")
def get_user(telegram_id):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE telegram_id = ?', (telegram_id,))
user = cursor.fetchone()
conn.close()
return user
def update_user_description(telegram_id, description):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('UPDATE users SET description = ? WHERE telegram_id = ?', (description, telegram_id))
conn.commit()
conn.close()
def update_user_field(telegram_id, field, value):
"""Обновляет одно поле профиля. field обязан быть из EDITABLE_FIELDS."""
if field not in EDITABLE_FIELDS:
raise ValueError(f"Поле '{field}' нельзя редактировать")
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(f'UPDATE users SET {field} = ? WHERE telegram_id = ?', (value, telegram_id))
conn.commit()
conn.close()
def delete_user(telegram_id):
"""Удаляет пользователя и всю его историю постов."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('DELETE FROM users WHERE telegram_id = ?', (telegram_id,))
cursor.execute('DELETE FROM posts WHERE telegram_id = ?', (telegram_id,))
conn.commit()
conn.close()
def save_post(telegram_id, platform, content, style=None, wishes=None):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
INSERT INTO posts (telegram_id, platform, content, style, wishes)
VALUES (?, ?, ?, ?, ?)
''', (telegram_id, platform, content, style, wishes))
conn.commit()
conn.close()
def get_posts(telegram_id, limit=10):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
SELECT platform, content, style, wishes, created_at
FROM posts
WHERE telegram_id = ?
ORDER BY created_at DESC
LIMIT ?
''', (telegram_id, limit))
posts = cursor.fetchall()
conn.close()
return posts
def get_stats(telegram_id):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT created_at FROM users WHERE telegram_id = ?', (telegram_id,))
row = cursor.fetchone()
created_at = row[0] if row else None
cursor.execute('SELECT COUNT(*) FROM posts WHERE telegram_id = ?', (telegram_id,))
total_posts = cursor.fetchone()[0]
cursor.execute('''
SELECT platform, COUNT(*) FROM posts
WHERE telegram_id = ?
GROUP BY platform
''', (telegram_id,))
by_platform = dict(cursor.fetchall())
conn.close()
return {
'created_at': created_at,
'total_posts': total_posts,
'by_platform': by_platform
}
# === ФУНКЦИИ ДЛЯ ВЕБ-КАБИНЕТА (ключ - внутренний users.id, не telegram_id) ===
def _row_conn():
conn = sqlite3.connect('users.db')
conn.row_factory = sqlite3.Row
return conn
def get_user_by_email(email):
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE email = ?', (email,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_user_by_id(user_id):
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def create_web_user(email, password_hash, name, phone, business_type=None, description=None):
"""Создаёт пользователя веб-кабинета (без telegram_id).
business_type и description опциональны — посты генерируются на основе медиа-образа.
Возвращает id нового пользователя."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
INSERT INTO users (name, phone, business_type, description, registered, created_at, email, password_hash)
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, ?, ?)
''', (name, phone, business_type, description, email, password_hash))
conn.commit()
user_id = cursor.lastrowid
conn.close()
return user_id
def update_user_field_by_id(user_id, field, value):
"""Обновляет одно поле профиля по внутреннему id. field обязан быть из EDITABLE_FIELDS."""
if field not in EDITABLE_FIELDS:
raise ValueError(f"Поле '{field}' нельзя редактировать")
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(f'UPDATE users SET {field} = ? WHERE id = ?', (value, user_id))
conn.commit()
conn.close()
def delete_user_by_id(user_id):
"""Удаляет пользователя веб-кабинета и всю его историю постов."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
cursor.execute('DELETE FROM posts WHERE user_id = ?', (user_id,))
conn.commit()
conn.close()
def save_post_for_user(user_id, platform, content, style=None, wishes=None):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
INSERT INTO posts (user_id, platform, content, style, wishes)
VALUES (?, ?, ?, ?, ?)
''', (user_id, platform, content, style, wishes))
conn.commit()
post_id = cursor.lastrowid
# Сохраняем начальную ревизию
cursor.execute(
'INSERT INTO post_revisions (post_id, user_id, content) VALUES (?, ?, ?)',
(post_id, user_id, content),
)
conn.commit()
conn.close()
return post_id
def get_posts_for_user(user_id, limit=10):
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT id, platform, content, style, wishes, created_at
FROM posts
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?
''', (user_id, limit))
posts = [dict(row) for row in cursor.fetchall()]
conn.close()
return posts
def get_stats_for_user(user_id):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT created_at FROM users WHERE id = ?', (user_id,))
row = cursor.fetchone()
created_at = row[0] if row else None
cursor.execute('SELECT COUNT(*) FROM posts WHERE user_id = ?', (user_id,))
total_posts = cursor.fetchone()[0]
cursor.execute('''
SELECT platform, COUNT(*) FROM posts
WHERE user_id = ?
GROUP BY platform
''', (user_id,))
by_platform = dict(cursor.fetchall())
conn.close()
return {
'created_at': created_at,
'total_posts': total_posts,
'by_platform': by_platform
}
# === УПРАВЛЕНИЕ ПОСТАМИ (веб-кабинет) ===
def get_post_by_id_for_user(post_id, user_id):
"""Возвращает один пост веб-пользователя (только если он принадлежит user_id)."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM posts WHERE id = ? AND user_id = ?', (post_id, user_id))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def update_post_content(user_id, post_id, content):
"""Редактирует текст поста. Меняет только посты, принадлежащие user_id."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('UPDATE posts SET content = ? WHERE id = ? AND user_id = ?', (content, post_id, user_id))
conn.commit()
conn.close()
def delete_post_by_id(user_id, post_id):
"""Удаляет пост. Удаляет только посты, принадлежащие user_id."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('DELETE FROM posts WHERE id = ? AND user_id = ?', (post_id, user_id))
conn.commit()
conn.close()
# === РЕВИЗИИ ПОСТОВ ===
def add_post_revision(post_id, user_id, content):
"""Сохраняет ревизию (каждое изменение текста поста)."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'INSERT INTO post_revisions (post_id, user_id, content) VALUES (?, ?, ?)',
(post_id, user_id, content),
)
conn.commit()
conn.close()
def get_post_revisions(post_id, user_id, limit=50):
"""Возвращает последние N ревизий поста (от новых к старым)."""
conn = sqlite3.connect('users.db')
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(
'SELECT id, content, created_at FROM post_revisions '
'WHERE post_id = ? AND user_id = ? ORDER BY id DESC LIMIT ?',
(post_id, user_id, limit),
)
rows = [dict(r) for r in cursor.fetchall()]
conn.close()
return rows
# === КОНТЕНТ-ПЛАН (веб-кабинет) ===
PLAN_STATUSES = ('planned', 'published', 'postponed')
def replace_plan_for_user(user_id, plan_items):
"""Полностью заменяет контент-план пользователя новыми пунктами.
plan_items: список словарей {'day': int, 'platform': str, 'idea': str}"""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('DELETE FROM content_plan WHERE user_id = ?', (user_id,))
cursor.executemany(
'INSERT INTO content_plan (user_id, day, platform, idea) VALUES (?, ?, ?, ?)',
[(user_id, item['day'], item['platform'], item['idea']) for item in plan_items]
)
conn.commit()
conn.close()
def get_plan_for_user(user_id):
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM content_plan WHERE user_id = ? ORDER BY day, id', (user_id,))
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rows
def update_plan_status(user_id, item_id, status):
if status not in PLAN_STATUSES:
raise ValueError(f"Недопустимый статус '{status}'")
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'UPDATE content_plan SET status = ? WHERE id = ? AND user_id = ?',
(status, item_id, user_id)
)
conn.commit()
conn.close()
def delete_plan_item(user_id, item_id):
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('DELETE FROM content_plan WHERE id = ? AND user_id = ?', (item_id, user_id))
conn.commit()
conn.close()
def get_plan_stats_for_user(user_id):
"""Считает пункты плана по статусам: {'planned': n, 'published': n, ...}"""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT status, COUNT(*) FROM content_plan WHERE user_id = ? GROUP BY status', (user_id,))
result = dict(cursor.fetchall())
conn.close()
return result
# === МЕДИА-БИБЛИОТЕКА (веб-кабинет) ===
def save_media(user_id, prompt, style, filename):
"""Сохраняет запись о сгенерированной картинке. Возвращает id записи."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'INSERT INTO media (user_id, prompt, style, filename) VALUES (?, ?, ?, ?)',
(user_id, prompt, style, filename)
)
conn.commit()
media_id = cursor.lastrowid
conn.close()
return media_id
def get_media_for_user(user_id, limit=50):
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT id, prompt, style, filename, created_at FROM media '
'WHERE user_id = ? ORDER BY created_at DESC, id DESC LIMIT ?',
(user_id, limit)
)
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rows
def get_media_by_id_for_user(media_id, user_id):
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM media WHERE id = ? AND user_id = ?', (media_id, user_id))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def delete_media_by_id(user_id, media_id):
"""Удаляет запись о картинке. Возвращает filename, чтобы удалить и сам файл."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('SELECT filename FROM media WHERE id = ? AND user_id = ?', (media_id, user_id))
row = cursor.fetchone()
if row:
cursor.execute('DELETE FROM media WHERE id = ? AND user_id = ?', (media_id, user_id))
conn.commit()
conn.close()
return row[0]
conn.close()
return None
# === ОПРОСЫ МЕДИА-ОБРАЗА (веб-кабинет) ===
import json as _json
def save_brand_survey(user_id, title, data):
"""Сохраняет опрос медиа-образа. data — dict (сериализуется в JSON).
Возвращает id новой записи."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'INSERT INTO brand_surveys (user_id, title, data) VALUES (?, ?, ?)',
(user_id, title, _json.dumps(data, ensure_ascii=False))
)
conn.commit()
survey_id = cursor.lastrowid
conn.close()
return survey_id
def get_brand_surveys_for_user(user_id, limit=50):
"""Возвращает список опросов пользователя (без поля data)."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT id, title, created_at FROM brand_surveys '
'WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
(user_id, limit)
)
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rows
def get_latest_brand_survey_for_user(user_id):
"""Возвращает последний опрос медиа-образа пользователя (с data) или None."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM brand_surveys WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
(user_id,)
)
row = cursor.fetchone()
conn.close()
if not row:
return None
result = dict(row)
result['data'] = _json.loads(result['data']) if result['data'] else {}
return result
def get_brand_survey_by_id(survey_id, user_id):
"""Возвращает один опрос (с data) только если он принадлежит user_id."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM brand_surveys WHERE id = ? AND user_id = ?',
(survey_id, user_id)
)
row = cursor.fetchone()
conn.close()
if not row:
return None
result = dict(row)
result['data'] = _json.loads(result['data']) if result['data'] else {}
return result
def delete_brand_survey(user_id, survey_id):
"""Удаляет опрос. Только если он принадлежит user_id."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'DELETE FROM brand_surveys WHERE id = ? AND user_id = ?',
(survey_id, user_id)
)
conn.commit()
conn.close()
# === КАНАЛЫ ДЛЯ ПУБЛИКАЦИИ (веб-кабинет) ===
def save_channel(user_id, chat_id, title, username, chat_type):
"""Сохраняет канал/чат для публикации. Возвращает id записи."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'INSERT INTO channels (user_id, chat_id, title, username, chat_type) VALUES (?, ?, ?, ?, ?)',
(user_id, str(chat_id), title, username, chat_type)
)
conn.commit()
channel_id = cursor.lastrowid
conn.close()
return channel_id
def get_channels_for_user(user_id):
"""Возвращает список каналов пользователя."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT id, chat_id, title, username, chat_type, created_at '
'FROM channels WHERE user_id = ? ORDER BY created_at DESC',
(user_id,)
)
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rows
def get_channel_by_id(channel_id, user_id):
"""Возвращает один канал только если он принадлежит user_id."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM channels WHERE id = ? AND user_id = ?',
(channel_id, user_id)
)
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def delete_channel(user_id, channel_id):
"""Удаляет канал. Только если он принадлежит user_id."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'DELETE FROM channels WHERE id = ? AND user_id = ?',
(channel_id, user_id)
)
conn.commit()
conn.close()
def is_channel_already_added(user_id, chat_id):
"""Проверяет, добавлен ли уже канал с таким chat_id."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'SELECT id FROM channels WHERE user_id = ? AND chat_id = ?',
(user_id, str(chat_id))
)
row = cursor.fetchone()
conn.close()
return row is not None
# === ОТМЕТКА О ПУБЛИКАЦИИ ПОСТА ===
def mark_post_published(post_id, user_id, channel_title):
"""Отмечает пост как опубликованный."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'UPDATE posts SET published = 1, published_at = CURRENT_TIMESTAMP, '
'published_to = ? WHERE id = ? AND user_id = ?',
(channel_title, post_id, user_id)
)
conn.commit()
conn.close()
# === НАСТРОЙКИ ПРОВАЙДЕРА ИИ (веб-кабинет) ===
# Пресеты провайдеров
PROVIDER_PRESETS = {
'piapi': {
'name': 'PiAPI',
'base_url': 'https://api.piapi.ai/v1',
'text_model': 'gpt-4o',
'image_model': 'gpt-image-1',
'needs_key': True,
'supports_images': True,
},
'openrouter': {
'name': 'OpenRouter',
'base_url': 'https://openrouter.ai/api/v1',
'text_model': 'openai/gpt-4o',
'image_model': 'openai/dall-e-3',
'needs_key': True,
'supports_images': True,
},
'opencode': {
'name': 'OpenCode Zen',
'base_url': 'https://opencode.ai/zen/v1',
'text_model': 'nemotron-3-ultra-free',
'image_model': '',
'needs_key': True,
'supports_images': False,
},
'openai': {
'name': 'OpenAI',
'base_url': 'https://api.openai.com/v1',
'text_model': 'gpt-4o',
'image_model': 'gpt-image-1',
'needs_key': True,
'supports_images': True,
},
'custom': {
'name': 'Кастомный (OpenAI-совместимый)',
'base_url': '',
'text_model': '',
'image_model': '',
'needs_key': True,
'supports_images': True,
},
}
def get_provider_settings(user_id):
"""Возвращает настройки провайдеров пользователя (text + image) или None."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('''
SELECT text_provider_type, text_api_key, text_base_url, text_model,
image_provider_type, image_api_key, image_base_url, image_model,
updated_at
FROM provider_settings WHERE user_id = ?
''', (user_id,))
row = cursor.fetchone()
conn.close()
if not row:
return None
d = dict(row)
return {
# Text provider
'text_provider_type': d['text_provider_type'],
'text_api_key': d['text_api_key'],
'text_base_url': d['text_base_url'],
'text_model': d['text_model'],
# Image provider
'image_provider_type': d['image_provider_type'],
'image_api_key': d['image_api_key'],
'image_base_url': d['image_base_url'],
'image_model': d['image_model'],
# Meta
'updated_at': d['updated_at'],
}
def get_text_provider_settings(user_id):
"""Возвращает настройки только text-провайдера в формате для ContentGenerator."""
s = get_provider_settings(user_id)
if not s or not s.get('text_api_key'):
return None
return {
'provider_type': s['text_provider_type'],
'api_key': s['text_api_key'],
'base_url': s['text_base_url'],
'text_model': s['text_model'],
}
def get_image_provider_settings(user_id):
"""Возвращает настройки только image-провайдера в формате для ImageGenerator."""
s = get_provider_settings(user_id)
if not s or not s.get('image_api_key'):
return None
return {
'provider_type': s['image_provider_type'],
'api_key': s['image_api_key'],
'base_url': s['image_base_url'],
'image_model': s['image_model'],
}
def save_provider_settings(user_id, text_provider_type, text_api_key, text_base_url, text_model,
image_provider_type, image_api_key, image_base_url, image_model):
"""Сохраняет или обновляет настройки обоих провайдеров."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute('''
INSERT INTO provider_settings (
user_id, text_provider_type, text_api_key, text_base_url, text_model,
image_provider_type, image_api_key, image_base_url, image_model, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET
text_provider_type = excluded.text_provider_type,
text_api_key = excluded.text_api_key,
text_base_url = excluded.text_base_url,
text_model = excluded.text_model,
image_provider_type = excluded.image_provider_type,
image_api_key = excluded.image_api_key,
image_base_url = excluded.image_base_url,
image_model = excluded.image_model,
updated_at = CURRENT_TIMESTAMP
''', (user_id, text_provider_type, text_api_key, text_base_url, text_model,
image_provider_type, image_api_key, image_base_url, image_model))
conn.commit()
conn.close()
# === ИЗОБРАЖЕНИЯ ПОСТОВ (веб-кабинет) ===
def save_post_image(post_id, user_id, filename, source='upload'):
"""Сохраняет запись об изображении, прикреплённом к посту."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'INSERT INTO post_images (post_id, user_id, filename, source) VALUES (?, ?, ?, ?)',
(post_id, user_id, filename, source)
)
conn.commit()
img_id = cursor.lastrowid
conn.close()
return img_id
def get_post_images(post_id, user_id):
"""Возвращает все изображения поста (только если пост принадлежит user_id)."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute(
'SELECT id, filename, source, created_at FROM post_images '
'WHERE post_id = ? AND user_id = ? ORDER BY created_at ASC',
(post_id, user_id)
)
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rows
def delete_post_image(image_id, user_id):
"""Удаляет изображение. Возвращает filename для удаления файла, или None."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'SELECT filename FROM post_images WHERE id = ? AND user_id = ?',
(image_id, user_id)
)
row = cursor.fetchone()
if row:
cursor.execute('DELETE FROM post_images WHERE id = ? AND user_id = ?', (image_id, user_id))
conn.commit()
conn.close()
return row[0]
conn.close()
return None
# === Задачи (background tasks) ===
def create_task(user_id, task_type):
"""Создаёт задачу. Возвращает task_id."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'INSERT INTO tasks (user_id, task_type, status) VALUES (?, ?, ?)',
(user_id, task_type, 'pending')
)
conn.commit()
task_id = cursor.lastrowid
conn.close()
return task_id
def update_task_status(task_id, status, result_json=None, error=None):
"""Обновляет статус задачи."""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute(
'UPDATE tasks SET status = ?, result_json = ?, error = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
(status, result_json, error, task_id)
)
conn.commit()
conn.close()
def get_task(task_id):
"""Возвращает задачу по ID."""
conn = _row_conn()
cursor = conn.cursor()
cursor.execute('SELECT * FROM tasks WHERE id = ?', (task_id,))
row = cursor.fetchone()
conn.close()
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()