From 81625b2e6f9ae5bfc5f76381899052691df2a380 Mon Sep 17 00:00:00 2001 From: Kavalar Date: Fri, 7 Aug 2026 09:35:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B2=D0=B5=D0=B1-=D0=BB=D0=B8=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BA=D0=B0=D0=B1=D0=B8=D0=BD=D0=B5=D1=82:=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8B,=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D1=82-=D0=BF=D0=BB=D0=B0=D0=BD,=20=D0=BC=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=B0-=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + __pycache__/config.cpython-314.pyc | Bin 657 -> 0 bytes __pycache__/database.cpython-314.pyc | Bin 2578 -> 0 bytes .../neural_network.cpython-314.pyc | Bin 13761 -> 0 bytes ai_core/image_generator.py | 89 +++ ai_core/neural_network.py | 217 +++++-- cabinet_service/app.py | 435 ++++++++++++++ cabinet_service/runner.py | 13 + cabinet_service/static/style.css | 555 ++++++++++++++++++ cabinet_service/templates/base.html | 114 ++++ cabinet_service/templates/dashboard.html | 20 + cabinet_service/templates/delete_confirm.html | 11 + cabinet_service/templates/generate.html | 26 + cabinet_service/templates/history.html | 30 + cabinet_service/templates/image_generate.html | 27 + cabinet_service/templates/images.html | 34 ++ cabinet_service/templates/login.html | 15 + cabinet_service/templates/plan.html | 58 ++ cabinet_service/templates/post_detail.html | 24 + cabinet_service/templates/post_edit.html | 18 + cabinet_service/templates/profile_edit.html | 25 + cabinet_service/templates/register.html | 35 ++ cabinet_service/templates/stats.html | 20 + config.py | 54 +- database.py | 392 ++++++++++++- handlers/__pycache__/content.cpython-314.pyc | Bin 3151 -> 0 bytes handlers/__pycache__/menu.cpython-314.pyc | Bin 5536 -> 0 bytes .../__pycache__/registration.cpython-314.pyc | Bin 4066 -> 0 bytes handlers/menu.py | 18 +- .../reply_keyboards.cpython-314.pyc | Bin 4037 -> 0 bytes keyboards/reply_keyboards.py | 11 +- main.py | 23 +- opencode.jsonc | 3 + requirements.txt | 10 +- users.db | Bin 16384 -> 0 bytes 35 files changed, 2220 insertions(+), 64 deletions(-) create mode 100644 .gitignore delete mode 100644 __pycache__/config.cpython-314.pyc delete mode 100644 __pycache__/database.cpython-314.pyc delete mode 100644 ai_core/__pycache__/neural_network.cpython-314.pyc create mode 100644 ai_core/image_generator.py create mode 100644 cabinet_service/app.py create mode 100644 cabinet_service/runner.py create mode 100644 cabinet_service/static/style.css create mode 100644 cabinet_service/templates/base.html create mode 100644 cabinet_service/templates/dashboard.html create mode 100644 cabinet_service/templates/delete_confirm.html create mode 100644 cabinet_service/templates/generate.html create mode 100644 cabinet_service/templates/history.html create mode 100644 cabinet_service/templates/image_generate.html create mode 100644 cabinet_service/templates/images.html create mode 100644 cabinet_service/templates/login.html create mode 100644 cabinet_service/templates/plan.html create mode 100644 cabinet_service/templates/post_detail.html create mode 100644 cabinet_service/templates/post_edit.html create mode 100644 cabinet_service/templates/profile_edit.html create mode 100644 cabinet_service/templates/register.html create mode 100644 cabinet_service/templates/stats.html delete mode 100644 handlers/__pycache__/content.cpython-314.pyc delete mode 100644 handlers/__pycache__/menu.cpython-314.pyc delete mode 100644 handlers/__pycache__/registration.cpython-314.pyc delete mode 100644 keyboards/__pycache__/reply_keyboards.cpython-314.pyc create mode 100644 opencode.jsonc delete mode 100644 users.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..767e7bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.env +__pycache__/ +*.pyc +users.db +venv/ +cabinet_service/media/ +.idea/ diff --git a/__pycache__/config.cpython-314.pyc b/__pycache__/config.cpython-314.pyc deleted file mode 100644 index ee8b1fa26a6684aecd25592a34b02141421333c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 657 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 diff --git a/ai_core/image_generator.py b/ai_core/image_generator.py new file mode 100644 index 0000000..77ee336 --- /dev/null +++ b/ai_core/image_generator.py @@ -0,0 +1,89 @@ +import base64 +import struct + +import requests + +from config import PIAPI_API_KEY, PIAPI_IMAGE_MODEL, get_proxies + +PIAPI_IMAGE_URL = "https://api.piapi.ai/v1/images/generations" + +# Доступные размеры (gpt-image-1) +IMAGE_SIZES = { + "square": ("1024x1024", "Квадрат 1024x1024"), + "wide": ("1536x1024", "Горизонтальный 1536x1024"), + "tall": ("1024x1536", "Вертикальный 1024x1536"), +} + +# Пресеты стилей - дописываются к промпту +STYLE_PRESETS = { + "none": ("Без стиля", ""), + "photo": ("Фотореализм", ", ultra realistic photo, professional photography, natural lighting"), + "illustration": ("Иллюстрация", ", digital illustration, vibrant colors, detailed"), + "minimal": ("Минимализм", ", minimalist style, clean background, simple composition"), + "3d": ("3D-рендер", ", 3D render, soft shadows, modern design"), + "flat": ("Плоская графика", ", flat vector illustration, bold shapes, social media style"), +} + + +def detect_ext(data: bytes) -> str: + """Определяет расширение файла по magic-байтам.""" + if data[:8] == b"\x89PNG\r\n\x1a\n": + return ".png" + if data[:2] == b"\xff\xd8": + return ".jpg" + if data[:4] == b"RIFF" and data[8:12] == b"WEBP": + return ".webp" + if data[:4] == b"GIF8": + return ".gif" + return ".png" + + +class ImageGenerator: + """Генерация изображений через PiAPI (OpenAI-совместимый /images/generations).""" + + def __init__(self, model: str | None = None): + self.model = model or PIAPI_IMAGE_MODEL + self.proxies = get_proxies() + + def generate(self, prompt: str, size: str = "square") -> bytes: + """Генерирует картинку и возвращает её байты. Поднимает исключение при ошибке.""" + if size not in IMAGE_SIZES: + size = "square" + size_value = IMAGE_SIZES[size][0] + + response = requests.post( + PIAPI_IMAGE_URL, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {PIAPI_API_KEY}", + }, + json={ + "model": self.model, + "prompt": prompt, + "n": 1, + "size": size_value, + }, + proxies=self.proxies, + timeout=240, + ) + response.raise_for_status() + + data = response.json().get("data") or [] + if not data: + raise RuntimeError("Пустой ответ от API картинок") + + item = data[0] + if item.get("b64_json"): + return base64.b64decode(item["b64_json"]) + if item.get("url"): + img_response = requests.get(item["url"], proxies=self.proxies, timeout=120) + img_response.raise_for_status() + return img_response.content + + raise RuntimeError("Неожиданный формат ответа от API картинок") + + def build_prompt(self, prompt: str, style: str = "none") -> str: + """Дополняет промпт пресетом стиля.""" + if style in STYLE_PRESETS: + return prompt.strip() + STYLE_PRESETS[style][1] + return prompt.strip() diff --git a/ai_core/neural_network.py b/ai_core/neural_network.py index b4e8b96..ef437e2 100644 --- a/ai_core/neural_network.py +++ b/ai_core/neural_network.py @@ -1,28 +1,26 @@ import random import os -from groq import Groq -from config import GROQ_API_KEY +import json +import requests +from config import PIAPI_API_KEY, PIAPI_MODEL, get_proxies + +PIAPI_URL = "https://api.piapi.ai/v1/chat/completions" class ContentGenerator: def __init__(self): """Инициализация генератора контента""" print("🔄 Инициализация ContentGenerator...") - - # Проверяем, есть ли ключ Groq - if GROQ_API_KEY: - try: - self.client = Groq(api_key=GROQ_API_KEY) - self.model = "llama-3.3-70b-versatile" - self.use_groq = True - print("✅ Groq API подключен!") - except Exception as e: - print(f"⚠️ Ошибка подключения Groq: {e}") - self.use_groq = False + + # Проверяем, есть ли ключ PiAPI + if PIAPI_API_KEY: + self.model = PIAPI_MODEL + self.use_piapi = True + print("✅ PiAPI подключен!") else: - print("⚠️ Groq API ключ не найден, использую шаблоны") - self.use_groq = False + print("⚠️ PiAPI ключ не найден, использую шаблоны") + self.use_piapi = False - # Шаблоны для запасного варианта (если Groq не работает) + # Шаблоны для запасного варианта (если PiAPI не работает) self.templates = { 'instagram': [ "🔥 {business_name} — это не просто бизнес, это миссия!\n\n" @@ -116,22 +114,22 @@ class ContentGenerator: """ print(f"🔄 Генерация поста для {platform}...") - # Пробуем сгенерировать через Groq - if self.use_groq: + # Пробуем сгенерировать через PiAPI + if self.use_piapi: try: - post = self._generate_with_groq(business_type, business_description, platform) + post = self._generate_with_piapi(business_type, business_description, platform) if post and len(post) > 20: - print("✅ Пост сгенерирован через Groq!") + print("✅ Пост сгенерирован через PiAPI!") return post except Exception as e: - print(f"⚠️ Ошибка Groq: {e}, использую шаблоны") - - # Если Groq не работает - используем шаблоны + print(f"⚠️ Ошибка PiAPI: {e}, использую шаблоны") + + # Если PiAPI не работает - используем шаблоны print("🔄 Использую шаблоны...") return self._generate_with_templates(business_type, business_description, platform) - - def _generate_with_groq(self, business_type, business_description, platform): - """Генерация через Groq API""" + + def _generate_with_piapi(self, business_type, business_description, platform): + """Генерация через PiAPI (OpenAI-совместимый chat/completions)""" platform_names = { 'instagram': 'Instagram', @@ -160,18 +158,163 @@ class ContentGenerator: Пост должен быть уникальным и интересным. """ - response = self.client.chat.completions.create( - model=self.model, - messages=[ - {"role": "system", "content": "Ты профессиональный копирайтер. Пиши ярко, полезно и вовлекающе."}, - {"role": "user", "content": prompt} - ], - temperature=0.9, - max_tokens=500 + response = requests.post( + PIAPI_URL, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {PIAPI_API_KEY}" + }, + json={ + "model": self.model, + "messages": [ + {"role": "system", "content": "Ты профессиональный копирайтер. Пиши ярко, полезно и вовлекающе."}, + {"role": "user", "content": prompt} + ], + "temperature": 0.9, + "max_tokens": 500 + }, + proxies=get_proxies(), + timeout=30 ) - - return response.choices[0].message.content.strip() - + response.raise_for_status() + data = response.json() + + return data["choices"][0]["message"]["content"].strip() + + def generate_post_with_style(self, business_type, business_description, platform, style, wishes): + """Генерирует пост с учётом стиля и пожеланий пользователя""" + print(f"🔄 Генерация поста в своём стиле для {platform}...") + + if self.use_piapi: + try: + platform_names = { + 'instagram': 'Instagram', + 'linkedin': 'LinkedIn', + 'telegram': 'Telegram', + 'twitter': 'Twitter/X', + 'ad': 'рекламный пост' + } + + prompt = f""" +Ты — профессиональный SMM-специалист и копирайтер. + +Напиши пост для {platform_names.get(platform, 'соцсетей')} о бизнесе. + +Тип бизнеса: {business_type} +Описание бизнеса: {business_description} +Стиль поста: {style} +Пожелания к посту: {wishes} + +Требования к посту: +- Длина: 100-200 слов +- Строго придерживайся указанного стиля и пожеланий +- Призыв к действию в конце +- Используй 2-3 эмодзи + +Напиши только текст поста, без лишних пояснений. +""" + + response = requests.post( + PIAPI_URL, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {PIAPI_API_KEY}" + }, + json={ + "model": self.model, + "messages": [ + {"role": "system", "content": "Ты профессиональный копирайтер. Точно следуешь стилю и пожеланиям клиента."}, + {"role": "user", "content": prompt} + ], + "temperature": 0.9, + "max_tokens": 500 + }, + proxies=get_proxies(), + timeout=30 + ) + response.raise_for_status() + post = response.json()["choices"][0]["message"]["content"].strip() + if post and len(post) > 20: + print("✅ Пост в стиле сгенерирован через PiAPI!") + return post + except Exception as e: + print(f"⚠️ Ошибка PiAPI: {e}, использую шаблоны") + + print("🔄 Использую шаблоны...") + post = self._generate_with_templates(business_type, business_description, platform) + return f"{post}\n\n💭 Пожелания учтены частично (без ИИ): {wishes}" + + def generate_content_plan(self, business_type, business_description, days): + """Генерирует контент-план на несколько дней вперёд""" + print(f"🔄 Генерация контент-плана на {days} дн...") + + if self.use_piapi: + try: + prompt = f""" +Ты — SMM-стратег. Составь контент-план на {days} дней для бизнеса. + +Тип бизнеса: {business_type} +Описание бизнеса: {business_description} + +Для каждого дня укажи платформу (instagram, linkedin, telegram, twitter или ad) и короткую идею поста (1 предложение). + +Верни ТОЛЬКО валидный JSON без пояснений в формате: +{{"plan": [{{"day": 1, "platform": "instagram", "idea": "..."}}, ...]}} +""" + + response = requests.post( + PIAPI_URL, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {PIAPI_API_KEY}" + }, + json={ + "model": self.model, + "messages": [ + {"role": "system", "content": "Ты отвечаешь только валидным JSON, без markdown и пояснений."}, + {"role": "user", "content": prompt} + ], + "temperature": 0.7, + "max_tokens": 1000, + "response_format": {"type": "json_object"} + }, + proxies=get_proxies(), + timeout=30 + ) + response.raise_for_status() + content = response.json()["choices"][0]["message"]["content"].strip() + parsed = json.loads(content) + plan = parsed.get("plan") + if plan: + print("✅ Контент-план сгенерирован через PiAPI!") + return plan + except Exception as e: + print(f"⚠️ Ошибка PiAPI: {e}, использую шаблонный план") + + print("🔄 Использую шаблонный план...") + return self._generate_template_plan(business_type, days) + + def _generate_template_plan(self, business_type, days): + """Простой контент-план без обращения к ИИ (запасной вариант)""" + platforms = ['instagram', 'linkedin', 'telegram', 'twitter', 'ad'] + ideas = [ + f"Расскажи историю успеха {business_type}", + f"Покажи закулисье работы {business_type}", + "Поделись отзывом клиента", + "Развенчай миф из твоей ниши", + "Дай практический совет подписчикам", + "Проведи опрос среди аудитории", + "Расскажи о новинке или акции" + ] + plan = [] + for day in range(1, days + 1): + plan.append({ + 'day': day, + 'platform': platforms[(day - 1) % len(platforms)], + 'idea': ideas[(day - 1) % len(ideas)] + }) + return plan + def _generate_with_templates(self, business_type, business_description, platform): """Генерация через шаблоны (запасной вариант)""" diff --git a/cabinet_service/app.py b/cabinet_service/app.py new file mode 100644 index 0000000..4dd1691 --- /dev/null +++ b/cabinet_service/app.py @@ -0,0 +1,435 @@ +from pathlib import Path +import os +import uuid + +import bcrypt +from fastapi import Depends, FastAPI, Form, HTTPException, Request +from fastapi.responses import RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from starlette.middleware.sessions import SessionMiddleware + +from ai_core.image_generator import IMAGE_SIZES, STYLE_PRESETS, ImageGenerator, detect_ext +from ai_core.neural_network import ContentGenerator +from config import SECRET_KEY +from database import ( + create_web_user, + delete_media_by_id, + delete_plan_item, + delete_post_by_id, + delete_user_by_id, + get_media_by_id_for_user, + get_media_for_user, + get_plan_for_user, + get_plan_stats_for_user, + get_post_by_id_for_user, + get_posts_for_user, + get_stats_for_user, + get_user_by_email, + get_user_by_id, + replace_plan_for_user, + save_media, + save_post_for_user, + update_plan_status, + update_post_content, + update_user_field_by_id, +) +from keyboards.reply_keyboards import BUSINESS_TYPES + +BASE_DIR = Path(__file__).resolve().parent +MEDIA_DIR = BASE_DIR / "media" +MEDIA_DIR.mkdir(exist_ok=True) + +app = FastAPI(title="Личный кабинет") +app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY) +app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") +app.mount("/media", StaticFiles(directory=str(MEDIA_DIR)), name="media") +templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) + +content_gen = ContentGenerator() +image_gen = ImageGenerator() + +PLATFORMS = [ + ("instagram", "Instagram"), + ("linkedin", "LinkedIn"), + ("telegram", "Telegram"), + ("twitter", "Twitter/X"), + ("ad", "Рекламный пост"), +] + +PLATFORM_LABELS = dict(PLATFORMS) + +def platform_label(platform: str) -> str: + return PLATFORM_LABELS.get(platform, platform) + + +class NotAuthenticated(Exception): + pass + + +@app.exception_handler(NotAuthenticated) +async def not_authenticated_handler(request: Request, exc: NotAuthenticated): + return RedirectResponse(url="/login", status_code=303) + + +def current_user(request: Request) -> dict: + user_id = request.session.get("user_id") + if not user_id: + raise NotAuthenticated() + user = get_user_by_id(user_id) + if not user: + request.session.clear() + raise NotAuthenticated() + return user + + +def flash(request: Request, message: str): + request.session["flash"] = message + + +def render(request: Request, template: str, status_code: int = 200, **context): + context.setdefault("user", None) + context["flash_message"] = request.session.pop("flash", None) + return templates.TemplateResponse(request, template, context, status_code=status_code) + + +def business_name_of(user: dict) -> str: + return BUSINESS_TYPES.get(str(user["business_type"]), {}).get("name", "Не указан") + + +@app.get("/health") +def health(): + return {"status": "ok"} + + +@app.get("/") +def root(request: Request): + if request.session.get("user_id"): + return RedirectResponse("/dashboard", status_code=303) + return RedirectResponse("/login", status_code=303) + + +# === РЕГИСТРАЦИЯ === + +@app.get("/register") +def register_form(request: Request): + if request.session.get("user_id"): + return RedirectResponse("/dashboard", status_code=303) + return render(request, "register.html", business_types=BUSINESS_TYPES) + + +@app.post("/register") +def register_submit( + request: Request, + name: str = Form(...), + phone: str = Form(...), + business_type: str = Form(...), + description: str = Form(...), + email: str = Form(...), + password: str = Form(...), + password_confirm: str = Form(...), +): + email = email.strip().lower() + errors = [] + if business_type not in BUSINESS_TYPES: + errors.append("Выбери вид бизнеса из списка.") + if len(password) < 8: + errors.append("Пароль должен быть не короче 8 символов.") + if password != password_confirm: + errors.append("Пароли не совпадают.") + if "@" not in email or "." not in email.split("@")[-1]: + errors.append("Введи корректный email.") + if not errors and get_user_by_email(email): + errors.append("Пользователь с таким email уже зарегистрирован.") + + if errors: + return render( + request, "register.html", status_code=400, + business_types=BUSINESS_TYPES, errors=errors, + form={ + "name": name, "phone": phone, "business_type": business_type, + "description": description, "email": email, + }, + ) + + password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + user_id = create_web_user(email, password_hash, name, phone, int(business_type), description) + request.session["user_id"] = user_id + flash(request, "Регистрация завершена! Добро пожаловать.") + return RedirectResponse("/dashboard", status_code=303) + + +# === ВХОД / ВЫХОД === + +@app.get("/login") +def login_form(request: Request): + if request.session.get("user_id"): + return RedirectResponse("/dashboard", status_code=303) + return render(request, "login.html") + + +@app.post("/login") +def login_submit(request: Request, email: str = Form(...), password: str = Form(...)): + email = email.strip().lower() + user = get_user_by_email(email) + valid = ( + user is not None + and user["password_hash"] + and bcrypt.checkpw(password.encode("utf-8"), user["password_hash"].encode("utf-8")) + ) + if not valid: + return render( + request, "login.html", status_code=400, + errors=["Неверный email или пароль."], form={"email": email}, + ) + + request.session["user_id"] = user["id"] + return RedirectResponse("/dashboard", status_code=303) + + +@app.post("/logout") +def logout(request: Request): + request.session.clear() + return RedirectResponse("/login", status_code=303) + + +# === КАБИНЕТ === + +@app.get("/dashboard") +def dashboard(request: Request, user: dict = Depends(current_user)): + return render(request, "dashboard.html", user=user, business_name=business_name_of(user)) + + +@app.get("/profile/edit") +def profile_edit_form(request: Request, user: dict = Depends(current_user)): + return render(request, "profile_edit.html", user=user, business_types=BUSINESS_TYPES) + + +@app.post("/profile/edit") +def profile_edit_submit( + request: Request, + name: str = Form(...), + phone: str = Form(...), + business_type: str = Form(...), + description: str = Form(...), + user: dict = Depends(current_user), +): + if business_type not in BUSINESS_TYPES: + return render( + request, "profile_edit.html", status_code=400, + user=user, business_types=BUSINESS_TYPES, + errors=["Выбери вид бизнеса из списка."], + ) + + update_user_field_by_id(user["id"], "name", name) + update_user_field_by_id(user["id"], "phone", phone) + update_user_field_by_id(user["id"], "business_type", int(business_type)) + update_user_field_by_id(user["id"], "description", description) + flash(request, "Профиль обновлён.") + return RedirectResponse("/dashboard", status_code=303) + + +@app.get("/generate") +def generate_form(request: Request, user: dict = Depends(current_user)): + return render(request, "generate.html", user=user, platforms=PLATFORMS) + + +@app.post("/generate") +def generate_submit( + request: Request, + platform: str = Form(...), + style: str = Form(...), + wishes: str = Form(""), + user: dict = Depends(current_user), +): + description = user["description"] or "не указано" + content = content_gen.generate_post_with_style( + business_name_of(user), description, platform, style, wishes + ) + save_post_for_user(user["id"], platform, content, style, wishes) + return render( + request, "generate.html", user=user, platforms=PLATFORMS, result=content, + form={"platform": platform, "style": style, "wishes": wishes}, + ) + + +@app.get("/history") +def history(request: Request, user: dict = Depends(current_user)): + posts = get_posts_for_user(user["id"], limit=50) + return render(request, "history.html", user=user, posts=posts, platform_label=platform_label) + + +@app.get("/posts/{post_id}") +def post_detail(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="Пост не найден") + return render(request, "post_detail.html", user=user, post=post, platform_label=platform_label) + + +@app.get("/posts/{post_id}/edit") +def post_edit_form(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="Пост не найден") + return render(request, "post_edit.html", user=user, post=post, platform_label=platform_label) + + +@app.post("/posts/{post_id}/edit") +def post_edit_submit( + request: Request, + post_id: int, + content: str = Form(...), + 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="Пост не найден") + content = content.strip() + if not content: + return render( + request, "post_edit.html", status_code=400, + user=user, post=post, platform_label=platform_label, + errors=["Текст поста не может быть пустым."], + ) + update_post_content(user["id"], post_id, content) + flash(request, "Пост обновлён.") + return RedirectResponse(f"/posts/{post_id}", status_code=303) + + +@app.post("/posts/{post_id}/delete") +def post_delete(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="Пост не найден") + delete_post_by_id(user["id"], post_id) + flash(request, "Пост удалён.") + return RedirectResponse("/history", status_code=303) + + +@app.get("/plan") +def plan_form(request: Request, user: dict = Depends(current_user)): + items = get_plan_for_user(user["id"]) + status_counts = get_plan_stats_for_user(user["id"]) + return render( + request, "plan.html", + user=user, plan=items, status_counts=status_counts, platform_label=platform_label, + ) + + +@app.post("/plan") +def plan_submit(request: Request, days: int = Form(7), user: dict = Depends(current_user)): + days = max(1, min(days, 14)) + description = user["description"] or "не указано" + plan = content_gen.generate_content_plan(business_name_of(user), description, days) + replace_plan_for_user(user["id"], plan) + flash(request, f"Контент-план на {days} дн. сохранён.") + return RedirectResponse("/plan", status_code=303) + + +@app.post("/plan/{item_id}/status") +def plan_status_submit( + request: Request, + item_id: int, + status: str = Form(...), + user: dict = Depends(current_user), +): + try: + update_plan_status(user["id"], item_id, status) + flash(request, "Статус пункта обновлён.") + except ValueError: + flash(request, "Недопустимый статус.") + return RedirectResponse("/plan", status_code=303) + + +@app.post("/plan/{item_id}/delete") +def plan_item_delete(request: Request, item_id: int, user: dict = Depends(current_user)): + delete_plan_item(user["id"], item_id) + flash(request, "Пункт плана удалён.") + return RedirectResponse("/plan", status_code=303) + + +@app.get("/stats") +def stats(request: Request, user: dict = Depends(current_user)): + return render(request, "stats.html", user=user, stats=get_stats_for_user(user["id"])) + + +@app.get("/delete-account") +def delete_confirm_form(request: Request, user: dict = Depends(current_user)): + return render(request, "delete_confirm.html", user=user) + + +@app.post("/delete-account") +def delete_account_submit( + request: Request, confirm: str = Form(""), user: dict = Depends(current_user) +): + if confirm != "yes": + return RedirectResponse("/dashboard", status_code=303) + delete_user_by_id(user["id"]) + request.session.clear() + return RedirectResponse("/login", status_code=303) + + +# === ГЕНЕРАЦИЯ КАРТИНОК === + +@app.get("/images") +def images_gallery(request: Request, user: dict = Depends(current_user)): + items = get_media_for_user(user["id"]) + return render(request, "images.html", user=user, images=items) + + +@app.get("/images/generate") +def image_generate_form(request: Request, user: dict = Depends(current_user)): + return render( + request, "image_generate.html", + user=user, sizes=IMAGE_SIZES, styles=STYLE_PRESETS, + ) + + +@app.post("/images/generate") +def image_generate_submit( + request: Request, + prompt: str = Form(...), + size: str = Form("square"), + style: str = Form("none"), + user: dict = Depends(current_user), +): + prompt = prompt.strip() + if not prompt: + return render( + request, "image_generate.html", status_code=400, + user=user, sizes=IMAGE_SIZES, styles=STYLE_PRESETS, + form={"prompt": prompt, "size": size, "style": style}, + errors=["Опиши, что нужно нарисовать."], + ) + + full_prompt = image_gen.build_prompt(prompt, style) + try: + image_bytes = image_gen.generate(full_prompt, size) + except Exception as e: + return render( + request, "image_generate.html", status_code=502, + user=user, sizes=IMAGE_SIZES, styles=STYLE_PRESETS, + form={"prompt": prompt, "size": size, "style": style}, + errors=[f"Ошибка генерации картинки: {e}"], + ) + + filename = f"{uuid.uuid4().hex}{detect_ext(image_bytes)}" + (MEDIA_DIR / filename).write_bytes(image_bytes) + save_media(user["id"], prompt, style, filename) + flash(request, "Картинка сгенерирована и сохранена в библиотеку.") + return RedirectResponse("/images", status_code=303) + + +@app.post("/images/{media_id}/delete") +def image_delete(request: Request, media_id: int, user: dict = Depends(current_user)): + item = get_media_by_id_for_user(media_id, user["id"]) + if not item: + raise HTTPException(status_code=404, detail="Картинка не найдена") + filename = delete_media_by_id(user["id"], media_id) + if filename: + file_path = MEDIA_DIR / filename + if file_path.exists(): + file_path.unlink() + flash(request, "Картинка удалена.") + return RedirectResponse("/images", status_code=303) diff --git a/cabinet_service/runner.py b/cabinet_service/runner.py new file mode 100644 index 0000000..0550bab --- /dev/null +++ b/cabinet_service/runner.py @@ -0,0 +1,13 @@ +import uvicorn + +from config import CABINET_SERVICE_HOST, CABINET_SERVICE_PORT +from cabinet_service.app import app + + +def run(): + uvicorn.run( + app, + host=CABINET_SERVICE_HOST, + port=CABINET_SERVICE_PORT, + log_level="warning" + ) diff --git a/cabinet_service/static/style.css b/cabinet_service/static/style.css new file mode 100644 index 0000000..b704d74 --- /dev/null +++ b/cabinet_service/static/style.css @@ -0,0 +1,555 @@ +:root { + --bg: #f4f5f7; + --surface: #ffffff; + --border: #e2e5ea; + --border-strong: #d1d5db; + --text: #1f2328; + --text-muted: #4b5563; + --text-faint: #6b7280; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --accent-soft: #eef2ff; + --accent-soft-border: #c7d2fe; + --accent-soft-hover: #e0e7ff; + --danger: #dc2626; + --danger-hover: #b91c1c; + --danger-text: #b91c1c; + --success-bg: #ecfdf3; + --success-border: #a7e3bb; + --success-text: #14532d; + --warn-bg: #fffbeb; + --warn-border: #fde68a; + --warn-text: #78350f; + --error-bg: #fef2f2; + --error-border: #f3b4b4; + --error-text: #7f1d1d; + --table-head: #f9fafb; + --badge-bg: #f3f4f6; + --badge-text: #374151; + --input-bg: #ffffff; + --published-surface: #fafefb; + color-scheme: light; +} + +:root[data-theme="dark"] { + --bg: #0f172a; + --surface: #1e293b; + --border: #334155; + --border-strong: #475569; + --text: #e2e8f0; + --text-muted: #94a3b8; + --text-faint: #94a3b8; + --accent: #60a5fa; + --accent-hover: #93c5fd; + --accent-soft: #172554; + --accent-soft-border: #1e40af; + --accent-soft-hover: #1e3a8a; + --danger: #ef4444; + --danger-hover: #f87171; + --danger-text: #f87171; + --success-bg: #064e3b; + --success-border: #059669; + --success-text: #a7f3d0; + --warn-bg: #451a03; + --warn-border: #b45309; + --warn-text: #fde68a; + --error-bg: #450a0a; + --error-border: #991b1b; + --error-text: #fecaca; + --table-head: #243244; + --badge-bg: #334155; + --badge-text: #e2e8f0; + --input-bg: #0f172a; + --published-surface: #0b2e22; + color-scheme: dark; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --bg: #0f172a; + --surface: #1e293b; + --border: #334155; + --border-strong: #475569; + --text: #e2e8f0; + --text-muted: #94a3b8; + --text-faint: #94a3b8; + --accent: #60a5fa; + --accent-hover: #93c5fd; + --accent-soft: #172554; + --accent-soft-border: #1e40af; + --accent-soft-hover: #1e3a8a; + --danger: #ef4444; + --danger-hover: #f87171; + --danger-text: #f87171; + --success-bg: #064e3b; + --success-border: #059669; + --success-text: #a7f3d0; + --warn-bg: #451a03; + --warn-border: #b45309; + --warn-text: #fde68a; + --error-bg: #450a0a; + --error-border: #991b1b; + --error-text: #fecaca; + --table-head: #243244; + --badge-bg: #334155; + --badge-text: #e2e8f0; + --input-bg: #0f172a; + --published-surface: #0b2e22; + color-scheme: dark; + } +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + transition: background-color 0.2s ease, color 0.2s ease; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.topbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 20px; + background: var(--surface); + border-bottom: 1px solid var(--border); +} + +.topbar .brand { + font-weight: 600; + font-size: 1.1rem; + color: var(--text); +} + +.topbar nav { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 14px; +} + +.theme-toggle { + background: none; + border: 1px solid var(--border); + border-radius: 8px; + padding: 4px 10px; + font-size: 1rem; + line-height: 1.4; + cursor: pointer; +} + +.theme-toggle:hover { + background: var(--accent-soft); + border-color: var(--accent-soft-border); +} + +.inline-form { + display: inline; + margin: 0; +} + +.link-button { + background: none; + border: none; + padding: 0; + font: inherit; + color: var(--accent); + cursor: pointer; +} + +.link-button:hover { + text-decoration: underline; +} + +.container { + max-width: 720px; + margin: 0 auto; + padding: 24px 20px 60px; +} + +h1 { + font-size: 1.5rem; + margin-bottom: 16px; +} + +h2 { + font-size: 1.15rem; + margin-top: 28px; +} + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 16px 18px; + margin-bottom: 16px; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.card p { + margin: 6px 0; +} + +.result { + white-space: pre-wrap; +} + +.flash { + background: var(--success-bg); + border: 1px solid var(--success-border); + color: var(--success-text); + padding: 10px 14px; + border-radius: 8px; + margin-bottom: 16px; +} + +.errors { + background: var(--error-bg); + border: 1px solid var(--error-border); + color: var(--error-text); + padding: 10px 14px; + border-radius: 8px; + margin-bottom: 16px; +} + +.errors ul { + margin: 0; + padding-left: 18px; +} + +.form { + display: flex; + flex-direction: column; + gap: 14px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 20px; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.form label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.9rem; + color: var(--text-muted); +} + +.form input, +.form select, +.form textarea { + font: inherit; + padding: 9px 11px; + border: 1px solid var(--border-strong); + border-radius: 8px; + background: var(--input-bg); + color: var(--text); + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.form input:focus, +.form select:focus, +.form textarea:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +.form button, +.button-danger { + align-self: flex-start; + background: var(--accent); + color: #fff; + border: none; + padding: 10px 18px; + border-radius: 8px; + font: inherit; + cursor: pointer; +} + +.form button:hover { + background: var(--accent-hover); +} + +.button-danger { + background: var(--danger); +} + +.button-danger:hover { + background: var(--danger-hover); +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; +} + +.tile { + display: block; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 18px; + font-weight: 500; + color: var(--text); + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.tile:hover { + border-color: var(--accent); + text-decoration: none; +} + +.tile-danger { + color: var(--danger-text); +} + +.post-list { + list-style: none; + margin: 0; + padding: 0; +} + +.post-meta { + font-size: 0.85rem; + color: var(--text-faint); + margin-bottom: 6px; +} + +.post-content { + white-space: pre-wrap; +} + +.table { + width: 100%; + border-collapse: collapse; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.table th, +.table td { + text-align: left; + padding: 10px 12px; + border-bottom: 1px solid var(--border); +} + +.table th { + background: var(--table-head); +} + +/* === Действия с постами / планом === */ + +.post-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: 12px; +} + +.copy-btn { + background: var(--accent-soft); + color: var(--accent); + border: 1px solid var(--accent-soft-border); + border-radius: 8px; + padding: 7px 12px; + font: inherit; + font-size: 0.9rem; + cursor: pointer; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.copy-btn:hover { + background: var(--accent-soft-hover); +} + +.button-link { + background: none; + border: none; + padding: 0; + font: inherit; + font-size: 0.9rem; + color: var(--accent); + cursor: pointer; + text-decoration: none; +} + +.button-link:hover { + text-decoration: underline; +} + +.button-link-danger { + color: var(--danger-text); +} + +.post-notes { + margin-top: 10px; + padding-top: 10px; + border-top: 1px dashed var(--border); + font-size: 0.9rem; + color: var(--text-muted); +} + +.form-actions { + display: flex; + align-items: center; + gap: 16px; +} + +/* === Контент-план === */ + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; +} + +.chip { + padding: 6px 12px; + border-radius: 999px; + font-size: 0.85rem; + background: var(--surface); + border: 1px solid var(--border); + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.chip-published { + background: var(--success-bg); + border-color: var(--success-border); + color: var(--success-text); +} + +.chip-postponed { + background: var(--warn-bg); + border-color: var(--warn-border); + color: var(--warn-text); +} + +.plan-item-head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.plan-day { + font-weight: 600; +} + +.plan-platform { + font-size: 0.9rem; + color: var(--text-faint); +} + +.plan-idea { + white-space: pre-wrap; +} + +.status-badge { + font-size: 0.8rem; + padding: 3px 10px; + border-radius: 999px; + background: var(--badge-bg); + color: var(--badge-text); +} + +.status-badge.status-published { + background: var(--success-bg); + color: var(--success-text); +} + +.status-badge.status-postponed { + background: var(--warn-bg); + color: var(--warn-text); +} + +.plan-item.status-published { + background: var(--published-surface); +} + +.status-select { + font: inherit; + font-size: 0.9rem; + padding: 6px 8px; + border: 1px solid var(--border-strong); + border-radius: 8px; + background: var(--input-bg); + color: var(--text); +} + +/* === Медиа-библиотека === */ + +.media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.media-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + display: flex; + flex-direction: column; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.media-card a.media-link { + display: block; + line-height: 0; +} + +.media-thumb { + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + background: var(--table-head); + display: block; +} + +.media-body { + padding: 10px 12px 12px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.media-prompt { + font-size: 0.9rem; + line-height: 1.35; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.form-hint { + font-size: 0.85rem; + color: var(--text-faint); + margin: 0; +} diff --git a/cabinet_service/templates/base.html b/cabinet_service/templates/base.html new file mode 100644 index 0000000..09fb4da --- /dev/null +++ b/cabinet_service/templates/base.html @@ -0,0 +1,114 @@ + + + + + + {% block title %}Личный кабинет{% endblock %} + + + + +
+ 🗂 Личный кабинет + + {% else %} + Вход + Регистрация + + {% endif %} +
+ +
+ {% if flash_message %} +
{{ flash_message }}
+ {% endif %} + {% if errors %} +
+
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+
+ {% endif %} + {% block content %}{% endblock %} +
+ + + diff --git a/cabinet_service/templates/dashboard.html b/cabinet_service/templates/dashboard.html new file mode 100644 index 0000000..4a25230 --- /dev/null +++ b/cabinet_service/templates/dashboard.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}Главная — Личный кабинет{% endblock %} +{% block content %} +

Привет, {{ user.name }}!

+
+

Телефон: {{ user.phone }}

+

Вид бизнеса: {{ business_name }}

+

Описание: {{ user.description }}

+

В кабинете с: {{ user.created_at }}

+
+ +{% endblock %} diff --git a/cabinet_service/templates/delete_confirm.html b/cabinet_service/templates/delete_confirm.html new file mode 100644 index 0000000..2fbd0cf --- /dev/null +++ b/cabinet_service/templates/delete_confirm.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% block title %}Удаление аккаунта — Личный кабинет{% endblock %} +{% block content %} +

Удалить аккаунт?

+

Это безвозвратно удалит твой профиль и всю историю постов.

+
+ + +
+

Отмена, вернуться в кабинет

+{% endblock %} diff --git a/cabinet_service/templates/generate.html b/cabinet_service/templates/generate.html new file mode 100644 index 0000000..025690f --- /dev/null +++ b/cabinet_service/templates/generate.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}Сгенерировать пост — Личный кабинет{% endblock %} +{% block content %} +

Пост в своём стиле

+
+ + + + +
+{% if result %} +

Готовый пост

+
{{ result }}
+{% endif %} +

← Назад в кабинет

+{% endblock %} diff --git a/cabinet_service/templates/history.html b/cabinet_service/templates/history.html new file mode 100644 index 0000000..cfa766b --- /dev/null +++ b/cabinet_service/templates/history.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}История постов — Личный кабинет{% endblock %} +{% block content %} +

История постов

+{% if posts %} +
    + {% for post in posts %} +
  • + +
    {{ post.content }}
    +
    + + 👁 Открыть + ✏️ Изменить +
    + +
    +
    +
  • + {% endfor %} +
+{% else %} +
+

Постов пока нет.

+

💡 Сгенерировать первый пост

+
+{% endif %} +

← Назад в кабинет

+{% endblock %} diff --git a/cabinet_service/templates/image_generate.html b/cabinet_service/templates/image_generate.html new file mode 100644 index 0000000..a7b8a88 --- /dev/null +++ b/cabinet_service/templates/image_generate.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block title %}Сгенерировать картинку — Личный кабинет{% endblock %} +{% block content %} +

Генерация картинки

+
+ + + + +

Генерация занимает 10–60 секунд. Картинка сохранится в медиа-библиотеку.

+
+

← Назад в библиотеку

+{% endblock %} diff --git a/cabinet_service/templates/images.html b/cabinet_service/templates/images.html new file mode 100644 index 0000000..da13152 --- /dev/null +++ b/cabinet_service/templates/images.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block title %}Картинки — Личный кабинет{% endblock %} +{% block content %} +

Медиа-библиотека

+

🎨 Сгенерировать картинку

+{% if images %} +
+ {% for img in images %} +
+ + {{ img.prompt }} + +
+
{{ img.prompt }}
+ +
+ ⬇ Скачать +
+ +
+
+
+
+ {% endfor %} +
+{% else %} +
+

Картинок пока нет.

+

🎨 Сгенерировать первую картинку

+
+{% endif %} +

← Назад в кабинет

+{% endblock %} diff --git a/cabinet_service/templates/login.html b/cabinet_service/templates/login.html new file mode 100644 index 0000000..f5551f5 --- /dev/null +++ b/cabinet_service/templates/login.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block title %}Вход — Личный кабинет{% endblock %} +{% block content %} +

Вход

+
+ + + +
+

Ещё нет аккаунта? Зарегистрироваться

+{% endblock %} diff --git a/cabinet_service/templates/plan.html b/cabinet_service/templates/plan.html new file mode 100644 index 0000000..8019c6a --- /dev/null +++ b/cabinet_service/templates/plan.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block title %}Контент-план — Личный кабинет{% endblock %} +{% block content %} +

Контент-план

+ +{% if plan %} +
+ 📝 В плане: {{ status_counts.get('planned', 0) }} + ✅ Опубликовано: {{ status_counts.get('published', 0) }} + 🕓 Отложено: {{ status_counts.get('postponed', 0) }} +
+ +
+ {% for item in plan %} +
+
+ День {{ item.day }} + {{ platform_label(item.platform) }} + + {% if item.status == 'published' %}✅ Опубликовано + {% elif item.status == 'postponed' %}🕓 Отложено + {% else %}📝 В плане{% endif %} + +
+
{{ item.idea }}
+
+
+ + +
+
+ +
+
+
+ {% endfor %} +
+{% else %} +
+

Контент-плана пока нет. Составь его на 1–14 дней — он будет сохранён, и ты сможешь отмечать опубликованные и отложенные посты.

+
+{% endif %} + +

{% if plan %}Пересоставить план{% else %}Составить план{% endif %}

+
+ + +
+

← Назад в кабинет

+{% endblock %} diff --git a/cabinet_service/templates/post_detail.html b/cabinet_service/templates/post_detail.html new file mode 100644 index 0000000..b4c7b3d --- /dev/null +++ b/cabinet_service/templates/post_detail.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}Пост — Личный кабинет{% endblock %} +{% block content %} +

Пост

+
+ +
{{ post.content }}
+ {% if post.wishes %} +
Пожелания: {{ post.wishes }}
+ {% endif %} +
+
+ + ✏️ Изменить +
+ +
+
+

← Назад к истории

+{% endblock %} diff --git a/cabinet_service/templates/post_edit.html b/cabinet_service/templates/post_edit.html new file mode 100644 index 0000000..7860cce --- /dev/null +++ b/cabinet_service/templates/post_edit.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Изменить пост — Личный кабинет{% endblock %} +{% block content %} +

Изменить пост

+
+ +
+ +
+ + Отмена +
+
+
+

← Назад к истории

+{% endblock %} diff --git a/cabinet_service/templates/profile_edit.html b/cabinet_service/templates/profile_edit.html new file mode 100644 index 0000000..47cd14c --- /dev/null +++ b/cabinet_service/templates/profile_edit.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}Профиль — Личный кабинет{% endblock %} +{% block content %} +

Редактировать профиль

+
+ + + + + +
+

← Назад в кабинет

+{% endblock %} diff --git a/cabinet_service/templates/register.html b/cabinet_service/templates/register.html new file mode 100644 index 0000000..58029a4 --- /dev/null +++ b/cabinet_service/templates/register.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %}Регистрация — Личный кабинет{% endblock %} +{% block content %} +

Регистрация

+
+ + + + + + + + +
+

Уже есть аккаунт? Войти

+{% endblock %} diff --git a/cabinet_service/templates/stats.html b/cabinet_service/templates/stats.html new file mode 100644 index 0000000..9202506 --- /dev/null +++ b/cabinet_service/templates/stats.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% block title %}Статистика — Личный кабинет{% endblock %} +{% block content %} +

Статистика использования

+
+

В кабинете с: {{ stats.created_at }}

+

Всего постов: {{ stats.total_posts }}

+
+

По платформам

+{% if stats.by_platform %} +
    + {% for platform, count in stats.by_platform.items() %} +
  • {{ platform }}: {{ count }}
  • + {% endfor %} +
+{% else %} +

Пока нет постов.

+{% endif %} +

← Назад в кабинет

+{% endblock %} diff --git a/config.py b/config.py index 9e73f1a..999037a 100644 --- a/config.py +++ b/config.py @@ -1,19 +1,59 @@ import os +import secrets from dotenv import load_dotenv # Загружаем переменные из .env load_dotenv() # Токен бота -TOKEN = "8876908400:AAGb4_ayjfhWuUI_evP8vvc5YUUiG8jxepE" +TOKEN = os.getenv("BOT_TOKEN") -# Ключ Groq -GROQ_API_KEY = os.getenv("GROQ_API_KEY") +if not TOKEN: + print("❌ BOT_TOKEN не найден в .env файле!") + print("👉 Создай файл .env и добавь: BOT_TOKEN=твой_токен") + +# Ключ PiAPI +PIAPI_API_KEY = os.getenv("PIAPI_API_KEY") +PIAPI_MODEL = os.getenv("PIAPI_MODEL", "gpt-4o") + +# Модель генерации картинок через PiAPI (список доступных: /v1/models) +PIAPI_IMAGE_MODEL = os.getenv("PIAPI_IMAGE_MODEL", "gpt-image-1") + +# Прокси для доступа к внешним API (PiAPI и др.). +# Формат в .env: PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS +PROXY_HOST = os.getenv("PROXY_HOST") +PROXY_PORT = os.getenv("PROXY_PORT") +PROXY_USER = os.getenv("PROXY_USER") +PROXY_PASS = os.getenv("PROXY_PASS") + +def get_proxies(): + """Возвращает dict прокси для requests, или None, если прокси не настроен.""" + if not (PROXY_HOST and PROXY_PORT): + return None + auth = f"{PROXY_USER}:{PROXY_PASS}@" if (PROXY_USER and PROXY_PASS) else "" + proxy = f"http://{auth}{PROXY_HOST}:{PROXY_PORT}" + return {"http": proxy, "https": proxy} + +# Сервис "Личный кабинет" (отдельный FastAPI-процесс, веб-интерфейс на браузере) +CABINET_SERVICE_HOST = os.getenv("CABINET_SERVICE_HOST", "127.0.0.1") +CABINET_SERVICE_PORT = int(os.getenv("CABINET_SERVICE_PORT", "8001")) + +# Публичный адрес кабинета - именно эту ссылку бот присылает пользователю. +# Для реального использования (не localhost) нужно задать в .env настоящий +# домен/IP, доступный из браузера пользователя. +CABINET_WEB_URL = os.getenv("CABINET_WEB_URL", f"http://{CABINET_SERVICE_HOST}:{CABINET_SERVICE_PORT}") + +# Ключ подписи cookie-сессий веб-кабинета +SECRET_KEY = os.getenv("SECRET_KEY") +if not SECRET_KEY: + SECRET_KEY = secrets.token_hex(32) + print("⚠️ SECRET_KEY не найден в .env - сгенерирован временный ключ.") + print("👉 Добавь SECRET_KEY в .env, иначе все сессии слетают при каждом перезапуске.") # Проверяем, что ключ загрузился -if not GROQ_API_KEY: - print("❌ GROQ_API_KEY не найден в .env файле!") - print("👉 Создай файл .env и добавь: GROQ_API_KEY=твой_ключ") +if not PIAPI_API_KEY: + print("❌ PIAPI_API_KEY не найден в .env файле!") + print("👉 Создай файл .env и добавь: PIAPI_API_KEY=твой_ключ") else: - print("✅ GROQ_API_KEY загружен!") + print("✅ PIAPI_API_KEY загружен!") diff --git a/database.py b/database.py index 24d3611..b425bde 100644 --- a/database.py +++ b/database.py @@ -1,9 +1,12 @@ 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, @@ -15,7 +18,68 @@ def create_db(): 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") + + 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 + ) + ''') + conn.commit() conn.close() print("✅ База данных создана!") @@ -50,5 +114,329 @@ def update_user_description(telegram_id, description): 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, description): + """Создаёт пользователя веб-кабинета (без telegram_id). Возвращает 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() + conn.close() + + +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() + + +# === КОНТЕНТ-ПЛАН (веб-кабинет) === + +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 + + # Создаем базу при первом импорте create_db() \ No newline at end of file diff --git a/handlers/__pycache__/content.cpython-314.pyc b/handlers/__pycache__/content.cpython-314.pyc deleted file mode 100644 index 6e04c73c639bbf40e68e0e0c5167594febd163e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3151 zcmb_eU2GKB6~41GJ3Bl3=h$mwgUybOVpzK}s&SzOR77JJ8?a@Koh3=PtOoDc+1R@~ zogWd@J}jn99XU!|plYfL45Uq6MN!rSh-0vc)J965dZ&xD+bK~a`cU<4BT!qlFFkj5 z#!eIZ)Q;Epo_p{4Ki`?T-Qf_A;Q7hdhF^FyfY6`G!hJjr@$e`R1#}XL)DRk^bSg$@ z&yZ)3)@hrjhnPXH?j2-x)?PD1KHc|ix*hd7Ds)ctZb7DQi zt3cO1mCxyNPCp{&WL47hYTRQ*$@xTWKQ%3%IhB{xv=#ow(_=#;17l;U@n=T|#zrP+ z#|W87G_YnKegxhZPy&s9=FlZa-N@Vxm=!wdb(aackE+oMlVEl_+f$7QwW0(y>T~%v z=L9WMeU8=`Pv0iL&&lL`Ict5M- z0JV_`$Z#uyIHQUm@)6mz<5G~1Mpsh|Z9~aA_hhuer8egiFDKYOr-}+MHYI$I^De=P zTU?z%BsM45QK-o09~W`J(URa{+}i6?0*|lSi38#`v2~wgIriUc8%wshc3l0Uuqj9C zEudtR%fq5{JIRm|9DgiY#%Zx_JP5vR@1@;{VyF#pqXZ8HkN4N+43$9#=r)w0;_U{3 zF}{Fr;x&BJEZ`NhV4eeNR=}%x9j}?Qv7zXvP@(Dg2uI19|SDUiQTH1!Dh63lnuY!0so zyER=>^$}w`sHrYqxFq03Pz_S97ehJVm{O&z<zw-s|A|S zat`l>d!{@vhKb_HaS>64N<5uGjeAuoHZXaGr&~d0<*K z&k<-BVF$QLBLGpv@0+tc51<9`{{n3O(DnruH|)lGx7INLdfNf0mpOY#fQU#d!LF?l z-vZ362px93n7DeAbRTel1JF94?LT_6gBP5~;)W%4I-NhAgAX6CNUA2MviY=ZJPCER z8}c$~Fh~{PMSxy(+rDVN1lue71w#Z*o+nVpcfm5G70_ij`8ojX7HMTCMI_@jw;^A^ zO&Z%q;0j?{1~vz%xY%P1gIdT6G~C77dxQX%L}90G-FyjTp`s-9YhV!U;5Cxa+az}L zocXHxGAOM1LEZ$1Ai$t1D>G+OSqRxs;vvgFsphlxXW#N<(kglTT0u?DrBhj1)1)cc zqT#!*vSgpubk(Y}4=0|N^prHAXYx5K;9d%*QCi$E2PXw`ea_srgZs5-SUxZNGg-I7(!UVjnk~8Se(1E!Y-APv-uY? zR_K(WWpdzl3YuBA0%=*BP&2k!i?Q7asj{Zzb5LR14kc^kBi-5A>4HHJtgk$^KlwD6 z)sn+wL&<(wo7VG6(tWd~z}@8YQZ7A1IGo6PlrvUKZFA#>b+zmF!HA7p3&Mu%& z!j0u{%nZlM;SMw0Q8;ox(6Df%6lg8<|0NKqaDg({Y;w)7Z^0e%J#O+3T)4_1e|)LC z!tGqz>I}P<;uWrIslOUvqio@LwGP#_mLoAU61x&w`fe$5pfLCe*RYVNaPtFp+;m6>Lf0XMJgy2o@^*==6_eq?JovdfI@T59{?nNnnA zj;XR#8&#nqWvaoX8Wsj$pDa;NV$_K{2R~~5-S*#ZFAY752cN4V=35j6VS3mCTSdIj zUkyZBf^(s22l5Bqe5@SBT=PBdNVVR!9yOV0g@c5hxZfBrHFj5h$lLfg#CX|%Jc^)T z<0ByXjdWIm4VCbo;^E?+Dnk2?d0-43_x#;U`}t214F9Uq!2VB-+%10XJ>1a07yV{$ zV4$7;kZKxer9W)-0v+cqE-mTODG2~#_eztLfs(6oPfj*eX(pAE_0xHES|#5qtHr%0 zYdy8L+gRIeQ0pp2&tzqln6-iOEA$XZq;?Sn+(CE~88hSNUQ fG1~Jdbo7rX@F|NTu~Jwl1RgO16ve7v2Cjbt6YX)R diff --git a/handlers/__pycache__/menu.cpython-314.pyc b/handlers/__pycache__/menu.cpython-314.pyc deleted file mode 100644 index eca2503c4be0ba5bff2cc8601d675cdbe91e72ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5536 zcmc&&|8o@O8Q;6x%k6#11(Jj05M&cDct8`1G-6~xEokCE8aE+e=W6EQmXHFMb9b*u z$LSne#VSKmz?o=`1jMP%*l7TxQ6T;cyXK~eJLy2FosK{CD%hX!8Qq^qB@fC4)jl~T zD+%(s*BL)WS%H5K$MH9ZP|0#}7d6t;!)>tZX?Kr%mDR{4mvh`wuE*7hKBZ3&<}vPd zS~u7g$^EdcOS87(&V4-+U#c7PX4b-teny|vKi9|fPmMS9i})QkUJ~_7`ejrv>Q^Bg z7xgRpxPH|*k=cX}`7?u~qJFBZLLi`@gqaI4lGP_f{bTg|%s6Vi1XC0GcjC|KWDrl!W(>js*j=bI6|19o@5CBlPtEYO`<-6z`j7+YjDu4 zY6(T_hERUf?Kr_{9#XGV(d^pwL?gOl?^Ze%=6-wJl1~yO|Gd3wSq#kcT;=GO z@f>7&&gm%cArxQeSSXy1+dWp^gQaksmXtkru~fw@EwWE+`Ir{mHirxEp;z^7y=qvm z+PnISl%3&4tf#fMh}C*_)!*i!_7RAZSOe?<__6g|J)RA7_<}W)76XCnGMgdS-_=nW%q3hururVB)fIygTzC zwNS+uF0Yl&kU@^%6;3%A5C0GF<``IB!7D$-9s>x|{ft>8u<|l*9)0W4P zVg#`bGn6YaD;h@uI*=-c>66jZ2(FeKw$%l6B(NHJ4#zwI>?_neOlB{LjAIF!Z>D!3V#<_ z{F6upm&r2(gRH}icU}?o_sWT}`~@>K#5d|QO>B&(--HKjl6&&eTymsSS$^&gbY zMNVpLQslmVRkH|?vKS5&AP#2+m^#L%sR|_)h4>K5mx>>?nAh|(vrixIiK`v>~>%8Cd9!J4XpPG!04;YHKouKO$Xg^%X^kN!h=Xy*QvdgDfY*(0y{4Pi-f z+TQJCq?aK6!FWi#zZz-q`*Xj+OIpG<{@#8uF<3+-*oY3tEH|PAD3qnRkp1i}D*5&-` zW`)H=hjYS8^k$y(oV&W^7UvGM+^XRG4Wp~Z_?++)NdFdGphPhHMI41z7CD2zsjwya zJ{)C^-}bl*{1^tB`>VF=UWMJQU%&PC2hVQ9uff(Z;8ae-kT1Y@33dnm*TyMoPc#gy;k$C~W#krb{EVUyZA+2|d`2)-ZGVhbZ z4+`1J@b3dUk0G$vEP`@(#F?#ZbADO{d17iz~6`D_0#8FW%qbLKvvxU3RKZ8{T03zs;bHcu_gz(pqhUrM-myyQv z>IWOfTz`7_y%)!%smR9Z$R;DQX`)BpwtFh_Y<5SU=j*ET0#{Qz^!%HvhN5}Sb>EVi zCHD_MKeB3=-d5Dl)HjV*0jk!yuDiV1#4EK~f1cw5-u%K)M96N>FX6nxG#@c|;Q!dK zbv}~gpP1!=-hbi6+fj_pAVab4|1*k7p8q+H0lOR=voWg(nIvBcP6_LA@4Ev|t(5#y z;GS?Qn2BMI%iAHqqt5XAbRaMy1M zHp8~A9z@Ow9w&&};#d-u2hteF$s2Ad=g|5^tEe;}00nnoE>mZR0+v~dCt_>IH|rtp z$V{MYYHFgO=o)aAAuU*Zt3|&q0abyx@QV*Ebgyw>1C7MT5Dg(6QRg7=r zYXinHM2pV&hC&yIE2v*0f=OR+=yG;kWTU}n8LALY5#PfLT}tQ%!I&y_ARqcfJp&qL zC4yHTpn@`R%v%;U0*v2TAW_WN6e7K>IE;!qisFZ01kV2A@GXI>S~eXPzYL4#Yeu_2 zsDFRgdrLo1ro!u|!!1U*W$ah_7HKNnrB`+X4R*TfX6lv>KQrPR^^AmueghzA^yY*9 zY9ae{J~AH&e$Ig4u_7Soh=omou9ABa_%uZKUxi0uMxC#RDhMxq!z#S9PfZrPCE&0y zcR_(3+aDAe>9y#uNLma+X^uYR7ywx`SJ(tF&m5FDD-S^g^x;&R{k+HJa$VzEu5m}E hxFdh%TE6AOuE4kAHkWJRw;k@NtNvC?#8uA>{TK3ZWXb>l diff --git a/handlers/__pycache__/registration.cpython-314.pyc b/handlers/__pycache__/registration.cpython-314.pyc deleted file mode 100644 index 9d0fd50392419f66da85e51517db6866de87f9e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4066 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 diff --git a/keyboards/reply_keyboards.py b/keyboards/reply_keyboards.py index c5905a5..d1bd6e6 100644 --- a/keyboards/reply_keyboards.py +++ b/keyboards/reply_keyboards.py @@ -1,4 +1,4 @@ -from telebot.types import ReplyKeyboardMarkup, KeyboardButton +from telebot.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton # === 9 ВИДОВ ПРЕДПРИНИМАТЕЛЕЙ === BUSINESS_TYPES = { @@ -21,10 +21,17 @@ def get_main_menu_keyboard(): KeyboardButton("✏️ Обновить описание"), KeyboardButton("📊 Статистика"), KeyboardButton("💡 Предложить контент"), - KeyboardButton("⚙️ Настройки") + KeyboardButton("⚙️ Настройки"), + KeyboardButton("🗂 Личный кабинет") ) return keyboard +def get_cabinet_link_keyboard(cabinet_web_url): + """Инлайн-кнопка со ссылкой на веб-версию личного кабинета""" + keyboard = InlineKeyboardMarkup() + keyboard.add(InlineKeyboardButton("🌐 Открыть личный кабинет", url=cabinet_web_url)) + return keyboard + def get_business_types_keyboard(): """Клавиатура с 9 видами предпринимателей""" keyboard = ReplyKeyboardMarkup(row_width=3, resize_keyboard=True) diff --git a/main.py b/main.py index ea05d8d..4e0d759 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import telebot import requests import sys import os +import threading from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from telebot import apihelper @@ -87,13 +88,13 @@ def status_bot(message): import datetime now = datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S") - # Проверяем, есть ли GROQ_API_KEY + # Проверяем, есть ли PIAPI_API_KEY try: - from config import GROQ_API_KEY - groq_status = "подключен ✅" if GROQ_API_KEY else "не подключен ⚠️" + from config import PIAPI_API_KEY + piapi_status = "подключен ✅" if PIAPI_API_KEY else "не подключен ⚠️" except: - groq_status = "не подключен ⚠️" - + piapi_status = "не подключен ⚠️" + status_text = f""" 📊 **СТАТУС БОТА** @@ -103,7 +104,7 @@ def status_bot(message): 📋 **Информация:** • Бот запущен и работает • База данных: подключена ✅ -• Groq API: {groq_status} +• PiAPI: {piapi_status} 💡 **Команды управления:** /stop - остановить бота @@ -133,6 +134,7 @@ def help_command(message): 📊 Статистика - статистика 💡 Предложить контент - сгенерировать пост для соцсетей ⚙️ Настройки - настройки бота +🗂 Личный кабинет - расширенный профиль, пост в своём стиле, контент-план, история постов и статистика 💡 **Как использовать:** 1. Напиши /start @@ -170,7 +172,7 @@ def handle_messages(message): reply_markup=get_main_menu_keyboard() ) return - + # Обрабатываем выбор в меню if handle_menu_choice(message, bot): return @@ -194,6 +196,11 @@ def handle_messages(message): # === ЗАПУСК БОТА === if __name__ == '__main__': + from cabinet_service.runner import run as run_cabinet_service + cabinet_thread = threading.Thread(target=run_cabinet_service, daemon=True) + cabinet_thread.start() + print("🗂 Сервис личного кабинета запущен в фоне") + print("🤖 БОТ ЗАПУЩЕН!") print("📋 Команды:") print(" /start - начать работу") @@ -202,7 +209,7 @@ if __name__ == '__main__': print(" /stop - остановить бота") print(" /restart - перезапустить бота") print("✅ Используй кнопки для навигации") - + try: bot.infinity_polling(timeout=30) except KeyboardInterrupt: diff --git a/opencode.jsonc b/opencode.jsonc new file mode 100644 index 0000000..720ece5 --- /dev/null +++ b/opencode.jsonc @@ -0,0 +1,3 @@ +{ + "$schema": "https://opencode.ai/config.json" +} diff --git a/requirements.txt b/requirements.txt index 32af77c..e677e03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ pytelegrambotapi>=4.16.0 -sqlalchemy>=2.0.23 python-dotenv>=1.0.0 requests>=2.31.0 urllib3>=2.1.0 -groq>=0.5.0 -transformers>=4.36.0 # (опционально, для локальных моделей) -torch>=2.1.0 # (опционально, для локальных моделей) \ No newline at end of file +fastapi>=0.110.0 +uvicorn>=0.29.0 +bcrypt>=4.1.0 +itsdangerous>=2.1.0 +jinja2>=3.1.0 +python-multipart>=0.0.9 \ No newline at end of file diff --git a/users.db b/users.db deleted file mode 100644 index a8b665c5bb02f37a4fcebe56b704a9940c92db62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI3O-~a+7{_-h#X zJ#h8n#ZRylRu=lg7tmzp%NURTXSP+^=*<&8lWu2co|pMQzh^IP)B6jvS;u0J^M&<{ z!zQ(hnyza%8PhZ^jH?${d+507Ubk(3o5R}pyGw(^KeW)$3vKA@@Hb2}hyW2F0z`la z5CI}U1c(3;AOb|-zalUn>NCd1^d%>=yk^;(Yxo$Nwyn*gm0Rgv_TEmKv6RVDv0Jky z>+&7RW!9}IB1cOv^$Uh!=-ZD}&Z2Dq#NAo<0-0n20Ydnw@@99ZQ)HyTUh~=5v4AUoP6& zoMqc-=lO=!*3_AC)v{L#*$pR~&#A<}WQ~sY^cgn{UCZWHt!FJvnWB?d>$IvneN9av zJZcg7xDDdc2a6hBG>8BZAOb{y2oM1xKm>>Y5g-CY;9nD{MD&OjzI;W0`#{rIhOU0( zpSaH(+#Q>qnVOlJo|w2kJsILnvBeL#3xm&(xGP?9U+hI0KY^vJtghH&(xhDKm_1Pz zC2WckZ^CfcGI_inWyt38BlW(oG9V8>M!qKBhks3M^Dj85`a6bkpJCnL9zR5!r;b4` z?DK}$Q4(;f6ow=X?7bHi25p|GVA$Pq%IfV(Z!strLB zSb~+kYz!tT*Tr_N9j2HgEo|t4Y>ceZ z6I)F6S<3Rq2k>m`!{0T$Xb=G+Km>>Y5g-CYfCvx)B0vO)01+Sp=ZZjoDBK@>^Q3