diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7a41be8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Используем официальный образ Python в качестве базового +FROM python:3.11-slim + +# Установим рабочую директорию внутри контейнера +WORKDIR /app + +# Копируем файл requirements.txt в контейнер +COPY requirements.txt . + +# Устанавливаем зависимости из requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем весь код проекта в контейнер +COPY . . + +# Указываем команду для запуска вашего приложения +CMD ["python", "app.py"] diff --git a/app.py b/app.py index 9373215..252acde 100644 --- a/app.py +++ b/app.py @@ -3,16 +3,16 @@ from aiogram import Dispatcher, Bot from aiogram.client.default import DefaultBotProperties from config import * from bot.handlers.user.user_handlers import user -from bot.handlers.admin.admin_handlers import admin +from db.db import * dp = Dispatcher() dp.include_router(user) -dp.include_router(admin) bot = Bot(token=bot_token, default=DefaultBotProperties(parse_mode='HTML')) async def main(): + await init_db() await dp.start_polling(bot) diff --git a/bot/handlers/admin/admin_handlers.py b/bot/handlers/admin/admin_handlers.py deleted file mode 100644 index 7ea1453..0000000 --- a/bot/handlers/admin/admin_handlers.py +++ /dev/null @@ -1,4 +0,0 @@ -from aiogram import Router - - -admin = Router() \ No newline at end of file diff --git a/bot/handlers/user/model_processing.py b/bot/handlers/user/model_processing.py index 9326182..b2f1ad4 100644 --- a/bot/handlers/user/model_processing.py +++ b/bot/handlers/user/model_processing.py @@ -1,25 +1,26 @@ from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +from db.db import * models_description = { "1o": "Это базовая модель ИИ, которая \"умеет думать\". Она хороша для выполнения повседневных задач, \ таких как анализ данных, обработка текста и создание отчетов. Она подходит для малого и среднего бизнеса, \ где важно автоматизировать рутинные задачи и получать быстрые результаты.\n\n\ -Выберите тариф к модели 1о.", +Выберите тариф к модели 1о.\nОставшиеся токены: ", "1o mini": "Уменьшенная версия 1о, которая также \"умеет думать\". Эта модель менее мощная, \ но зато работает быстрее на устройствах с ограниченными ресурсами, таких как смартфоны или небольшие компьютеры. \ Прекрасно подойдет для мобильных приложений и стартапов, где ресурсы ограничены, но нужны умные решения.\n\n\ -Выберите тариф к модели 1о mini.", +Выберите тариф к модели 1о mini.\nОставшиеся токены: ", "4o": "Это мощная модель ИИ, предназначенная для сложных и объемных задач. Она хорошо справляется \ с глубоким анализом больших данных, сложными прогнозами и инновационными бизнес-проектами. \ Идеальна для крупных компаний, требующих высокую производительность для динамичного принятия решений и стратегического анализа.\n\n\ -Выберите тариф к модели 4о.", +Выберите тариф к модели 4о.\nОставшиеся токены: ", "4o mini": "Уменьшенная версия модели 4о. Она сохраняет высокую мощность при меньшем использовании ресурсов. \ Подходит для сложных бизнес-задач в условиях ограниченных ресурсов, таких как аналитика в режиме реального времени \ на портативных и мобильных устройствах. Отличный выбор для бизнеса, который хочет получить максимум от своих технологий \ без больших затрат на оборудование.\n\n\ -Выберите тариф к модели 4о." +Выберите тариф к модели 4о.\nОставшиеся токены: " } model_prices = { @@ -87,23 +88,35 @@ name_of_model = { "pro" : "Профессиональный" } +class BuyCallBack(CallbackData, prefix="buy"): + name: str + option_of_model: str + tokens: str + amount: str + class PayCallBack(CallbackData, prefix="pay"): name: str option_of_model: str tokens: str amount: str -def get_model_page(*, page: int, name: str): - text: str = models_description[name] +class ChooseCallBack(CallbackData, prefix="choose"): + name: str + +async def get_model_page(*, page: int, name: str, user_id): + tokens = await get_current_model_tokens(user_id, name) + status = await get_current_model(user_id) == name + + text: str = models_description[name] + str(tokens) temp_kbds: InlineKeyboardBuilder = InlineKeyboardBuilder() for option_name, item in model_prices[name].items(): option_name_rus, tokens, amount = name_of_model[option_name], item["tokens"], item["amount"] - temp_text = f"{name_of_model[option_name]} | {tokens} запросов за {amount} RUB" + temp_text = f"{name_of_model[option_name]} | {tokens} запросов" temp_kbds.add(InlineKeyboardButton( text=temp_text, - callback_data=PayCallBack( + callback_data=BuyCallBack( name=name, option_of_model=option_name_rus, tokens=tokens, @@ -111,6 +124,13 @@ def get_model_page(*, page: int, name: str): ).pack() )) + temp_kbds.add(InlineKeyboardButton( + text="🟢 Выбрано" if status else "🔴 Не выбрано", + callback_data=ChooseCallBack( + name=name, + ).pack() + )) + return text, temp_kbds diff --git a/bot/handlers/user/page_processing.py b/bot/handlers/user/page_processing.py index f0d1ccb..ab4f1be 100644 --- a/bot/handlers/user/page_processing.py +++ b/bot/handlers/user/page_processing.py @@ -97,8 +97,8 @@ async def tarif_page(page: int, name: str): return text, kbds -async def models_description_page(page: int, name: str): - text, kbds = get_user_model_description_page(page=page, name=name) +async def models_description_page(page: int, name: str, user_id): + text, kbds = await get_user_model_description_page(page=page, name=name, user_id=user_id) return text, kbds @@ -114,7 +114,9 @@ async def price_list_page(page: int, name: str): return text, kbds -async def get_page_content(page: int, name: str): + + +async def get_page_content(page: int, name: str, user_id): if page == 0: return await main_page(page, name) @@ -125,7 +127,7 @@ async def get_page_content(page: int, name: str): elif page == 3: return await more_about_models_page(page, name) elif 4 <= page <= 7: - return await models_description_page(page, name) + return await models_description_page(page, name, user_id) elif page == 8: #TODO ... diff --git a/bot/handlers/user/states.py b/bot/handlers/user/states.py new file mode 100644 index 0000000..e923833 --- /dev/null +++ b/bot/handlers/user/states.py @@ -0,0 +1,7 @@ +from aiogram.fsm.state import State, StatesGroup + +class ChooseModelState(StatesGroup): + choosing_model = State() + +class SendMessageState(StatesGroup): + waiting_for_message = State() \ No newline at end of file diff --git a/bot/handlers/user/user_handlers.py b/bot/handlers/user/user_handlers.py index 4ca160c..ff2ed7a 100644 --- a/bot/handlers/user/user_handlers.py +++ b/bot/handlers/user/user_handlers.py @@ -1,53 +1,348 @@ -from aiogram import Router, types +import csv +from aiogram.fsm.context import FSMContext +from bot.handlers.user.states import ChooseModelState, SendMessageState +import time +from db.db import * +from aiogram import Router, types, F from aiogram.filters import CommandStart, Command -from aiogram.types import LabeledPrice, PreCheckoutQuery - -from bot.handlers.user.model_processing import PayCallBack +from aiogram.types import LabeledPrice, PreCheckoutQuery, InlineKeyboardButton, FSInputFile +from aiogram.utils.keyboard import InlineKeyboardBuilder +from db.db import user_exists, add_user_to_db, set_curren_model, get_temp_prompts +from bot.handlers.user.model_processing import PayCallBack, BuyCallBack, ChooseCallBack from bot.handlers.user.page_processing import get_page_content from bot.kbs.inline import PageCallBack -from config import shop_api_token +from config import shop_api_token, admins +from bot.utils.openai_tasks import get_model_suggestion_from_openai, get_answer_to_question user = Router() @user.message(CommandStart()) -async def start(message: types.Message): - text, reply_markup = await get_page_content(page=0,name="main") +async def start(message: types.Message, state: FSMContext): + await state.clear() + # TODO + isAdded = await user_exists(message.from_user.id) + if not isAdded: + # Извлекаем информацию о пользователе + first_name = message.from_user.first_name or "" + last_name = message.from_user.last_name or "" + user_name = message.from_user.username or "" + + # Добавляем пользователя в базу данных + await add_user_to_db( + user_id=message.from_user.id, + first_name=first_name, + last_name=last_name, + user_name=user_name + ) + + + text, reply_markup = await get_page_content(page=0,name="main", user_id=message.from_user.id) await message.answer(text, reply_markup=reply_markup) @user.callback_query(PageCallBack.filter()) -async def user_pages(callback: types.CallbackQuery, callback_data: PageCallBack): +async def user_pages(callback: types.CallbackQuery, callback_data: PageCallBack, state: FSMContext): + await state.clear() text, reply_markup = await get_page_content( page=callback_data.page, name=callback_data.page_name, + user_id=callback.from_user.id, ) await callback.message.edit_text(text, reply_markup=reply_markup) await callback.answer() -@user.callback_query(PayCallBack.filter()) -async def buy(callback: types.CallbackQuery, callback_data: PayCallBack): - await callback.message.bot.send_invoice( +@user.callback_query(ChooseCallBack.filter()) +async def choose(callback: types.CallbackQuery, callback_data: ChooseCallBack): + await set_curren_model(user_id=callback.from_user.id, name_of_model=callback_data.name) + + text, reply_markup = await get_page_content( + page=2, + user_id=callback.from_user.id, + name='tarif', + ) + + await callback.message.edit_text(text, reply_markup=reply_markup) + await callback.answer() + +@user.message(Command(commands=['tarif'])) +async def tarif(message: types.Message): + # Получаем текст и клавиатуру для страницы тарифа + text, reply_markup = await get_page_content( + page=2, + user_id=message.from_user.id, # Используем message.from_user.id вместо callback + name='tarif', + ) + + # Отправляем новое сообщение с текстом и кнопками + await message.answer(text, reply_markup=reply_markup) + +@user.callback_query(BuyCallBack.filter()) +async def buy(callback: types.CallbackQuery, callback_data: BuyCallBack): + kbds = InlineKeyboardBuilder() + buttons = [ + InlineKeyboardButton(text="Купить", callback_data=PayCallBack( + name=callback_data.name, + option_of_model=callback_data.option_of_model, + tokens=callback_data.tokens, + amount=callback_data.amount + ).pack()), + InlineKeyboardButton(text="⬅️Назад", callback_data=PageCallBack(page=2, page_name="tarif").pack()), + ] + + for button in buttons: + kbds.add(button) + + + await callback.message.edit_text( + text = f"Модель: {callback_data.name}\nТариф: {callback_data.option_of_model}\nКол-ов запросов: {callback_data.tokens}\nСтоимость: {callback_data.amount}RUB", + reply_markup=kbds.adjust(*(1,)).as_markup() + ) + + await callback.answer() + + +@user.callback_query(PayCallBack.filter()) +async def pay(callback: types.CallbackQuery, callback_data: PayCallBack): + temp = await callback.message.bot.send_invoice( chat_id=callback.from_user.id, description=f'{callback_data.tokens} токенов за {callback_data.amount} RUB для модели {callback_data.name}', title=f'{callback_data.option_of_model} подписка на {callback_data.name}', - payload=f'sub_{callback_data.name}', + payload=f'sub_{callback_data.name}_{callback.from_user.id}_{callback_data.tokens}', + # Добавляем id пользователя и количество токенов в payload provider_token=shop_api_token, start_parameter='test', currency="RUB", prices=[LabeledPrice( label=f'Оплата токенов для {callback_data.option_of_model}', - amount=int(callback_data.amount)*100, + amount=int(callback_data.amount) * 100, )], ) + # if temp.successful_payment is None: + # await callback.bot.send_message(chat_id=callback.from_user.id,text="Something went wrong") + + @user.pre_checkout_query() -async def procces_pre_checkout_query(pre_checkout_query: PreCheckoutQuery): +async def process_pre_checkout_query(pre_checkout_query: PreCheckoutQuery): await pre_checkout_query.answer(ok=True) + # time.sleep(2) + # + # await pre_checkout_query.bot.send_message(chat_id=pre_checkout_query.from_user.id, text="Спасибо, пользутесь нашим сервисом!") + # + # text, reply_markup = await get_page_content(page=0, name="main", user_id=pre_checkout_query.from_user.id) + # + # await pre_checkout_query.bot.send_message( + # chat_id=pre_checkout_query.from_user.id, + # text=text, + # reply_markup=reply_markup, + # ) + +@user.message(F.successful_payment) +async def process_payment(message: types.Message): + + if message.successful_payment is None: + await message.bot.send_message(chat_id=message.from_user.id,text="Что то пошло не так") + return + + payload_data = message.successful_payment.invoice_payload.split('_') + if len(payload_data) != 4: + return # Не валидный payload + + model_name = payload_data[1] # Имя модели + user_id = int(payload_data[2]) # ID пользователя + tokens = int(payload_data[3]) # Количество токенов + + await add_tokens_to_user(user_id, model_name, tokens) + + time.sleep(2) + + await message.bot.send_message(chat_id=message.from_user.id, + text="Спасибо, что пользуетесь нашим сервисом!") + + await message.bot.send_message(chat_id=message.from_user.id, + text="Для работы с chatGPT отправьте текст с вашим вопросом и получите на него ответ)") + +@user.message(Command(commands=["failed"])) +async def process_payment(message: types.Message): + await message.bot.send_message(chat_id=message.from_user.id, + text="Что-то пошло не так( Попробуйте еще раз!") + + text, reply_markup = await get_page_content(page=0, name="main", user_id=message.from_user.id) + + await message.bot.send_message( + chat_id=message.from_user.id, + text=text, + reply_markup=reply_markup, + ) + + +@user.callback_query(lambda c: c.data == 'choose') +async def chat_test_gpt(callback: types.CallbackQuery, state: FSMContext): + kbds = InlineKeyboardBuilder() + + kbds.add(InlineKeyboardButton( + text="⬅️Назад", + callback_data=PageCallBack( + page=3, + page_name='more_about_models', + ).pack() + )) + + await callback.message.edit_text("Вы находитесь в разделе подбора модели. Опишите ваши задачи, и я помогу выбрать модель.", reply_markup=kbds.adjust(*(1,)).as_markup()) + await callback.answer() + await state.set_state(ChooseModelState.choosing_model) + +@user.message(ChooseModelState.choosing_model) +async def process_model_question(message: types.Message): + user_id = message.from_user.id + + remaining_queries = await get_temp_prompts(user_id=user_id) + + kbds = InlineKeyboardBuilder() + + kbds.add(InlineKeyboardButton( + text="⬅️Назад", + callback_data=PageCallBack( + page=3, + page_name='more_about_models' + ).pack() + )) + + if remaining_queries > 0: + # Обрабатываем вопрос с помощью OpenAI GPT + openai_response = await get_model_suggestion_from_openai(message.text) + + # Отправляем ответ пользователю + # await message.answer(text=f"Ответ OpenAI: {openai_response}" + f"\nОсталось {remaining_queries-1} запросов.", reply_markup=kbds.adjust(*(1,)).as_markup()) + + # Уменьшаем количество запросов + if "Ошибка при обращении к OpenAI" not in openai_response: + await message.answer( + text=f"Ответ OpenAI: {openai_response}" + f"\nОсталось {remaining_queries - 1} запросов.", + reply_markup=kbds.adjust(*(1,)).as_markup()) + + # await decrease_test_queries(user_id) + ... + else: + await message.answer( + text=f"Попробуйте еще раз или чуть подождите!") + else: + await message.answer(text="У вас закончились доступные запросы для подбора модели.", reply_markup=kbds.adjust(*(1,)).as_markup()) + + +@user.message(Command(commands=['export_users'])) +async def send_csv_to_admin(message: types.Message): + # Генерируем CSV файл + if message.from_user.id in admins: + users = await get_all_users() + + # Генерируем CSV файл + with open('data/users.csv', 'w', newline='', encoding='utf-8') as csvfile: + csvwriter = csv.writer(csvfile, delimiter=';', quoting=csv.QUOTE_MINIMAL) + csvwriter.writerow(['ID', 'First Name', 'Last Name', 'Username']) + + for user in users: + csvwriter.writerow([user.user_id, user.first_name, user.last_name, user.user_name]) + + # Отправляем CSV файл администратору + csv_file = FSInputFile('data/users.csv') # Обернуть путь к файлу как InputFile + await message.answer_document(csv_file) + + +@user.message(Command(commands=['send_message_to_all'])) +async def send_messages_to_users(message: types.Message, state: FSMContext): + if message.from_user.id in admins: + # Устанавливаем состояние, что бот ждет текст сообщения для рассылки + await state.set_state(SendMessageState.waiting_for_message) + + await message.answer("Введите сообщение для рассылки:") + + +@user.message(SendMessageState.waiting_for_message) +async def get_message_for_users(msg: types.Message, state: FSMContext): + # Получаем текст сообщения для рассылки + text_to_send = msg.text + + users = await get_all_users() + + # Рассылаем сообщение всем пользователям + for user in users: + if user.user_id == msg.from_user.id: + continue + try: + await msg.bot.send_message(chat_id=user.user_id, text=f'Сообщение от админа:\n{text_to_send}') + except Exception as e: + # Логируем ошибки для недоступных пользователей + print(f"Не удалось отправить сообщение пользователю {user.user_id}: {e}") + + await msg.answer("Сообщение успешно отправлено всем пользователям.") + + # Очистить состояние после завершения рассылки + await state.clear() + + + +@user.message(~Command(commands=["export_users", 'send_message_to_all'])) # Игнорируем команды +async def chatgpt_chatting(message: types.Message): + user_id = message.from_user.id + + db_model = await get_current_model(user_id) + + kbds = InlineKeyboardBuilder() + + kbds.add(InlineKeyboardButton( + text="Выбрать тариф", + callback_data=PageCallBack( + page=2, + page_name='tarif' + ).pack())) + + kbds.add(InlineKeyboardButton( + text="⬅️На главную", + callback_data=PageCallBack( + page=0, + page_name='main' + ).pack() + )) + + if db_model is None: + await message.bot.send_message(chat_id=message.from_user.id, text="У вас еще не выбрана модель", reply_markup=kbds.adjust(*(1,)).as_markup()) + + models = { + '1o' : "o1-preview", + '1o mini': "o1-mini", + '4o': "gpt-4o", + '4o mini': "gpt-4o-mini" + } + + current_model = models[db_model] + + remaining_queries = await get_current_model_tokens(user_id=user_id, name_of_model=db_model) + + if remaining_queries > 0: + openai_response = await get_answer_to_question(message.text, current_model) + + # Уменьшаем количество запросов + if "Ошибка при обращении к OpenAI" not in openai_response: + await decrease_current_model_tokens(user_id, name_of_model=db_model) + await message.answer(text=f"Ответ OpenAI: {openai_response}") + await message.answer(text=f"Модель: {current_model}\nОсталось {remaining_queries - 1} запросов.", + reply_markup=kbds.adjust(*(1,)).as_markup()) + else: + await message.answer( + text=f"{openai_response}") + await message.answer( + text=f"Попробуйте еще раз или вы можете выбрать другую доступную для вас модель!") + + else: + await message.answer(text=f"У вас закончились доступные запросы для модели {current_model}. Купите новые токены или выберете другую модель", + reply_markup=kbds.adjust(*(1,)).as_markup()) + diff --git a/bot/kbs/inline.py b/bot/kbs/inline.py index 33e9124..94932ae 100644 --- a/bot/kbs/inline.py +++ b/bot/kbs/inline.py @@ -2,7 +2,7 @@ from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from bot.handlers.user.model_processing import get_model_page +from bot.handlers.user.model_processing import get_model_page, ChooseCallBack class PageCallBack(CallbackData, prefix="page"): @@ -38,8 +38,8 @@ def get_user_tarif_page(*, page): # page 2 return get_kb_buttons(btns=btns) -def get_user_model_description_page(*, page: int, name: str): - text, kbd = get_model_page(page=page, name=name) +async def get_user_model_description_page(*, page: int, name: str, user_id): + text, kbd = await get_model_page(page=page, name=name, user_id=user_id) kbd.add(InlineKeyboardButton(text="⬅️Назад", callback_data=PageCallBack(page=2, page_name="tarif").pack()),) @@ -47,7 +47,8 @@ def get_user_model_description_page(*, page: int, name: str): def get_more_about_modules_page(*, page: int): # page 3 btns = [ - InlineKeyboardButton(text="Как узнать, какая модель мне подходит", callback_data=PageCallBack(page=8, page_name='match_model').pack()), + # InlineKeyboardButton(text="Какая модель мне подходит", callback_data=PageCallBack(page=8, page_name='match_model').pack()), + InlineKeyboardButton(text="Как узнать, какая модель мне подходит", callback_data='choose'), InlineKeyboardButton(text="Узнать стоимость моделей", callback_data=PageCallBack(page=9, page_name='price_list').pack()), InlineKeyboardButton(text="Выбрать модель", callback_data=PageCallBack(page=2, page_name="tarif").pack()), # 2 InlineKeyboardButton(text="⬅️Назад", callback_data=PageCallBack(page=0, page_name="main").pack()), diff --git a/bot/utils/openai_tasks.py b/bot/utils/openai_tasks.py new file mode 100644 index 0000000..03c0b99 --- /dev/null +++ b/bot/utils/openai_tasks.py @@ -0,0 +1,71 @@ +from openai import AsyncOpenAI +from config import gpt_api_key + +aclient = AsyncOpenAI(api_key=gpt_api_key) + +# Инициализация клиента OpenAI + +async def get_model_suggestion_from_openai(user_question: str) -> str: + # Формирование сообщения для чата + messages = [ + { + "role": "system", + "content": ( + "Ты работаешь как консультант по выбору тарифов для ChatGPT. " + "Тарифы называются 1o, 1o mini, 4o, 4o mini, и они представляют собой количество запросов или мощность обработки модели. " + "Твоя задача — помочь пользователям выбрать подходящий тариф." + "1o: Для автоматизации рутинных задач (анализ данных, обработка текста). Подходит малым и средним бизнесам." + "1o mini: Оптимизирован для мобильных устройств, подходит для стартапов с ограниченными ресурсами." + "4o: Для глубокого анализа и сложных прогнозов. Идеальна для крупных компаний с высокими требованиями." + "4o mini: Высокая мощность при низком использовании ресурсов. Хороша для аналитики в реальном времени на мобильных устройствах." + "Помоги ему выбрать подходящую модель на основе описаний. Ответ должен быть до 300 символов и касаться только выбора модели. На все другие вопросы скажи, что они не по теме." + ) + }, + { + "role": "user", + "content": ( + f"Пользователь задал вопрос: {user_question}. " + "Он хочет выбрать подходящий тариф для ChatGPT среди 1o, 1o mini, 4o, 4o mini. " + "Опиши, какой тариф лучше всего подходит для его задач." + ) + } + ] + + try: + # Используем эндпоинт для чата + response = await aclient.chat.completions.create( + model="gpt-4", + messages=messages, + n=1, + stop=None, + temperature=0.7 + ) + response_message = response.choices[0].message.content + return response_message + except Exception as e: + return f"Ошибка при обращении к OpenAI: {str(e)}" + + +async def get_answer_to_question(question: str, name_of_model: str): + messages = [ + { + "role": "system", + "content": "Ты — помощник, который отвечает на вопросы пользователя. Ты даешь развернутые и ценные ответы, которые должны отвечать на вопрос пользователя" + }, + { + "role": "user", + "content": question + } + ] + + try: + response = await aclient.chat.completions.create( + model=name_of_model, + messages=messages, + n=1, + stop=None, + temperature=0.7 + ) + return response.choices[0].message.content + except Exception as e: + return f"Ошибка при обращении к OpenAI: {str(e)}" \ No newline at end of file diff --git a/config.py b/config.py index 31c4fb4..1d3efe5 100644 --- a/config.py +++ b/config.py @@ -4,7 +4,18 @@ from dotenv import load_dotenv load_dotenv() bot_token = os.getenv("BOT_TOKEN") -admins = [367757357] +admins = [367757357, 1617340397, 5899041406] + + +# Теперь вы можете получить доступ к переменным окружения +postgres_user = os.getenv('POSTGRES_USER') +postgres_password = os.getenv('POSTGRES_PASSWORD') +postgres_db = os.getenv('POSTGRES_DB') + +# Формируем строку подключения +DATABASE_URL = f"postgresql+asyncpg://{postgres_user}:{postgres_password}@db:5432/{postgres_db}" shop_id = os.getenv("SHOP_ID") -shop_api_token = os.getenv("SHOP_API_TOKEN") \ No newline at end of file +shop_api_token = os.getenv("SHOP_API_TOKEN") + +gpt_api_key = os.getenv("GPT_API_KEY") \ No newline at end of file diff --git a/data/users.csv b/data/users.csv new file mode 100644 index 0000000..a1cbd9d --- /dev/null +++ b/data/users.csv @@ -0,0 +1,3 @@ +ID;First Name;Last Name;Username +5411565044;Notimy;;notimy_official +367757357;4hellboy4;;pushkin404 diff --git a/db/db.py b/db/db.py new file mode 100644 index 0000000..9ae3fb0 --- /dev/null +++ b/db/db.py @@ -0,0 +1,201 @@ + +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, BigInteger +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.future import select +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker, relationship +from config import DATABASE_URL + +Base = declarative_base() +class User(Base): + __tablename__ = 'users' + + user_id = Column(BigInteger, primary_key=True, unique=True, index=True) # уникальный ID пользователя из aiogram + first_name = Column(String) + last_name = Column(String) + user_name = Column(String) + test_queries = Column(Integer) # временные промпты + current_model = Column(String) + + subscriptions = relationship("Subscription", back_populates="user", cascade="all, delete-orphan") + +class Subscription(Base): + __tablename__ = 'subscriptions' + + id = Column(Integer, primary_key=True) + user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) + purchased_model = Column(String, nullable=False) + purchased_tokens = Column(Integer) + + user = relationship("User", back_populates="subscriptions") + +engine = create_async_engine(DATABASE_URL) +async_session = sessionmaker(bind=engine, class_=AsyncSession) + +async def init_db() -> None: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + +async def get_all_users(): + async with engine.connect() as connection: + result = await connection.execute(select(User)) + users = result.fetchall() # Получаем всех пользователей + return users + + +async def add_user_to_db(user_id: int, first_name: str, last_name: str, user_name: str) -> None: + async with async_session() as session: + async with session.begin(): + user = User( + user_id=user_id, + first_name=first_name, + last_name=last_name, + user_name=user_name, + test_queries=10, # количество временных промптов + current_model="" + ) + session.add(user) + await session.commit() + return + +async def user_exists(user_id: int) -> bool: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + return user is not None + +async def decrease_test_queries(user_id: int) -> None: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + user.test_queries -= 1 if user.test_queries > 0 else 0 + await session.commit() + else: + return None + + +async def get_temp_prompts(user_id: int): + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + return user.test_queries + else: + return 0 + + +async def set_curren_model(user_id: int, name_of_model: str) -> None: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + user.current_model = name_of_model + await session.commit() + else: + return None + + +async def add_tokens_to_user(user_id: int, name_of_model: str, queries: int) -> None: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + # Проверяем, есть ли подписка на модель + subscription_result = await session.execute( + select(Subscription).where( + Subscription.user_id == user_id, + Subscription.purchased_model == name_of_model + ) + ) + subscription = subscription_result.scalar() + + if subscription: + # Подписка существует, обновляем количество токенов + subscription.purchased_tokens += queries # Предполагается, что поле tokens существует в модели Subscription + await session.commit() # Сохраняем изменения в базе данных + else: + # Подписка не существует, создаем новую + new_subscription = Subscription( + user_id=user_id, + purchased_model=name_of_model, + purchased_tokens=queries + ) + session.add(new_subscription) + await session.commit() # Сохраняем изменения в базе данных + else: + # Пользователь не найден, можно обработать эту ситуацию + print(f"Пользователь с ID {user_id} не найден.") + + +async def get_current_model(user_id: int) -> str: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + return user.current_model if user else "" + +async def get_current_model_tokens(user_id: int, name_of_model: str) -> int: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + subscription_result = await session.execute( + select(Subscription).where(Subscription.user_id == user_id, + Subscription.purchased_model == name_of_model) + ) + subscription = subscription_result.scalar() + + if subscription: + return subscription.purchased_tokens + else: + return 0 + else: + return 0 + +async def decrease_current_model_tokens(user_id: int, name_of_model: str) -> None: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + subscription_result = await session.execute( + select(Subscription).where(Subscription.user_id == user_id, + Subscription.purchased_model == name_of_model) + ) + subscription = subscription_result.scalar() + + if subscription: + subscription.purchased_tokens -= 1 if subscription.purchased_tokens > 0 else 0 + await session.commit() + return + else: + return None + else: + print('No user found') + return None + +async def remoove_current_model(user_id: int) -> None: + async with async_session() as session: + async with session.begin(): + result = await session.execute(select(User).where(User.user_id == user_id)) + user = result.scalar() + + if user: + user.current_model = "" + await session.commit() + return + return diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e10d691 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,35 @@ +version: '3.8' + +services: + db: + image: postgres:latest + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + app: + build: . + depends_on: + db: + condition: service_healthy # Ждем, пока база данных станет доступной + environment: + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + volumes: + - .:/app + ports: + - "8000:8000" + restart: unless-stopped + +volumes: + pgdata: diff --git a/requirements.txt b/requirements.txt index e69de29..88ada78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,40 @@ +aiofiles==24.1.0 +aiogram==3.13.1 +aiohappyeyeballs==2.4.3 +aiohttp==3.10.8 +aiosignal==1.3.1 +alembic==1.13.3 +annotated-types==0.7.0 +anyio==4.6.0 +asyncpg==0.29.0 +attrs==24.2.0 +certifi==2024.8.30 +charset-normalizer==3.3.2 +Deprecated==1.2.14 +distro==1.9.0 +frozenlist==1.4.1 +greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.6 +httpx==0.27.2 +idna==3.10 +jiter==0.5.0 +magic-filter==1.0.12 +Mako==1.3.5 +MarkupSafe==3.0.0 +multidict==6.1.0 +netaddr==1.3.0 +openai==1.51.0 +psycopg2-binary==2.9.9 +pydantic==2.9.2 +pydantic_core==2.23.4 +python-dotenv==1.0.1 +requests==2.32.3 +sniffio==1.3.1 +SQLAlchemy==2.0.35 +tqdm==4.66.5 +typing_extensions==4.12.2 +urllib3==2.2.3 +wrapt==1.16.0 +yarl==1.13.1 +yookassa==3.3.0