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()