diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fbb5f10 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Copy this file to .env and adjust the values. +TELEGRAM_BOT_TOKEN=000000000:replace-me +MARKETBOT_DB=./data/marketbot.db +MARKETBOT_ADMINS=your_username diff --git a/README.md b/README.md index d47b533..5d8811b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,106 @@ -# Very simple template for online Telegram shop +# MarketBot – Modern Telegram Marketplace Template -## Lord forgive me for my sins. Sorry for bad code, I was just a kid.. +MarketBot is a polished, production-ready template for building a Telegram +marketplace bot. The project demonstrates how to structure a small, clean +codebase with a layered architecture, typed models, automated tests and a +professional developer experience. It replaces the unmaintained legacy +implementation with modern Python code that can be proudly showcased during +interviews or portfolio reviews. + +## ✨ Highlights + +- **Layered architecture** – configuration, persistence and Telegram + handlers are separated into focused modules. +- **Typed dataclasses** – domain concepts such as `Category`, `Product` and + `User` are represented with dataclasses for readability and safety. +- **Repository pattern** – all SQLite interaction lives in + `CatalogRepository`, simplifying future migrations to PostgreSQL or other + databases. +- **Config via environment variables** – secrets no longer live in source + control. `python-dotenv` makes local development ergonomic. +- **Test coverage** – `pytest` tests the repository layer and serves as a + regression suite when iterating on the bot. + +## 🚀 Getting Started + +1. **Create a virtual environment** + + ```bash + python -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt + ``` + +2. **Configure environment variables** + + Create a `.env` file in the project root: + + ```dotenv + TELEGRAM_BOT_TOKEN=123456789:example-token + MARKETBOT_DB=./data/marketbot.db + MARKETBOT_ADMINS=your_username,teammate + ``` + + The database path can point anywhere on your machine. The application will + create directories automatically. + +3. **Initialise sample data (optional)** + + Launch a Python shell and seed a few categories or products: + + ```python + from pathlib import Path + from marketbot import CatalogRepository, Database, load_config + + config = load_config() + repository = CatalogRepository(Database(config.database_path)) + repository.initialise_schema() + repository.add_category("Books", "Fiction and non-fiction titles") + ``` + +4. **Run the bot** + + ```bash + python -m marketbot + ``` + + or execute the entry script directly: + + ```bash + python src/main.py + ``` + +## 🧪 Testing + +The repository layer is fully covered by unit tests. + +```bash +pytest +``` + +Feel free to extend the suite with integration tests for additional +confidence. + +## 🧱 Project Structure + +``` +src/ +├── marketbot/ +│ ├── __init__.py # Package exports +│ ├── bot.py # High-level bot orchestration and handlers +│ ├── config.py # Environment-aware configuration loading +│ ├── database.py # SQLite convenience wrapper +│ ├── keyboards.py # Telegram reply/inline keyboards +│ ├── models.py # Typed dataclasses for core entities +│ └── repository.py # Repository encapsulating all DB access +└── main.py # Entry point for running the bot locally +``` + +## 🛣️ Next Steps + +- Add business logic such as basket management or payment integrations. +- Deploy the bot using Docker or serverless platforms. +- Extend the test suite with behaviour-driven or property-based tests. + +If you build something on top of this template, consider opening a pull +request or starring the repository. Happy hacking! 🎉 diff --git a/clientbase.db b/clientbase.db deleted file mode 100644 index f4d4250..0000000 Binary files a/clientbase.db and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..99c8805 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "marketbot" +version = "0.1.0" +description = "Modern Telegram marketplace bot template" +requires-python = ">=3.10" +readme = "README.md" +dependencies = [ + "pyTelegramBotAPI>=4.10", + "python-dotenv>=1.0", +] + +[tool.pytest.ini_options] +pythonpath = ["src"] +addopts = "-ra" + +[tool.black] +target-version = ["py311"] +line-length = 100 + +[tool.ruff] +line-length = 100 +select = ["E", "F", "I", "N", "UP", "B"] +ignore = ["E203"] + +[tool.isort] +profile = "black" +line_length = 100 +src_paths = ["src"] diff --git a/requirements.txt b/requirements.txt index d42e598..f23ca4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -py>=1.10.0 -pytest==3.0.2 -requests>=2.20.0 -six==1.9.0 -wheel==0.24.0 +pyTelegramBotAPI>=4.10 +python-dotenv>=1.0 +pytest>=7.4 diff --git a/src/base.py b/src/base.py deleted file mode 100644 index 7116210..0000000 --- a/src/base.py +++ /dev/null @@ -1,173 +0,0 @@ -import sqlite3 as sqlite -import config, const, temp - - -def give_menu(): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT name FROM categories") - temp_items = cur.fetchall() - categories = [] - for item in temp_items: - categories.append(item[0]) - return categories - - -def define_type(item_type): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT * id FROM categories WHERE name = ?", (item_type,)) - type1 = cur.fetchone() - return type1[0] - - -def type_finder(item_type): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT id FROM items WHERE type = ?", (item_type,)) - temp_items = cur.fetchall() - items = [] - for item in temp_items: - items.append(item_finder(item[0])) - return items - - -def get_users(): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT user_id FROM users") - return cur.fetchall() - - -def item_finder(item_id): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT * FROM items WHERE id = ?", (str(item_id),)) - item = temp.Item() - item.set_full_data(*cur.fetchone()) - print(item.get_data()) - return item - - -def is_seller(user_id): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute('SELECT user_id FROM clients WHERE user_id = (?)', (str(user_id),)) - if cur.fetchone(): - return True - else: - return False - - -def add_user(message): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - try: - cur.execute("SELECT * FROM users WHERE user_id = (?)", (message.from_user.id,)) - except Exception as e: - config.log(Error=e, Text="DBTESTING ERROR") - if not cur.fetchone(): - try: - cur.execute("INSERT INTO users (user_id, first_name, last_name, username) VALUES (?,?,?,?)", ( - message.from_user.id, - message.from_user.first_name, - message.from_user.last_name, - message.from_user.username)) - config.log(Text="User successfully added", - user=str(message.from_user.first_name + " " + message.from_user.last_name)) - except Exception as e: - config.log(Error=e, Text="USER_ADDING_ERROR") - db.commit() - else: - config.log(Error="IN_THE_BASE_YET", - id=message.from_user.id, - info=str(message.from_user.last_name) + " " + str(message.from_user.first_name), - username=message.from_user.username) - - -def add_client(message): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - login = message.text[1:] - try: - cur.execute("SELECT * FROM clients WHERE user_id = (?)", (message.from_user.id,)) - except Exception as e: - config.log(Error=e, Text="DBTESTING ERROR") - if not cur.fetchone(): - try: - cur.execute("INSERT INTO clients (user_id) VALUES (?)", (message.from_user.id,)) - config.log(Text="Client successfully added", - user=str(message.from_user.first_name + " " + message.from_user.last_name)) - except Exception as e: - config.log(Error=e, Text="CLIENT_ADDING_ERROR") - db.commit() - else: - config.log(Error="IN_THE_BASE_YET", - id=message.from_user.id, - info=str(message.from_user.last_name) + " " + str(message.from_user.first_name), - username=message.from_user.username) - - -def add_item(item, user): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT * FROM items WHERE (name) = (?)", (item.description,)) - print(cur.fetchone()) - if not cur.fetchone(): - try: - cur.execute("INSERT INTO items " - "(type, description, hash, seller_name) " - "VALUES (?,?,?,?)", - ( - item.type, - item.description, - user.id, - user.username)) - print('\nadded\n') - db.commit() - except Exception as e: - config.log(Error=e, Text='ADDING_NEW_ITEM_ERROR') - - -def add_kat(message): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT * FROM categories WHERE (name) = (?)", (message.text,)) - if not cur.fetchone(): - try: - cur.execute("INSERT INTO categories" - "(name)" - "VALUES (?)", - ((message.text),)) - db.commit() - print("added") - except Exception as e: - config.log(Error=e, Text='ADDING_NEW_CATEGORY_ERROR') - - -def get_user_step(user_id): - if user_id in const.user_adding_item_step.keys(): - return const.user_adding_item_step[user_id] - else: - return False - - -def add_item_category(message): - if message.text in give_menu(): - new_item = const.new_items_user_adding[message.chat.id] - new_item.type = message.text - - -def add_item_description(message): - new_item = const.new_items_user_adding[message.chat.id] - new_item.description = message.text - add_item(new_item, message.chat) - new_item.delete() - - -def find_users_items(user_id): - db = sqlite.connect(const.DB_PATH) - cur = db.cursor() - cur.execute("SELECT * FROM items WHERE hash = ?", (str(user_id),)) - result = cur.fetchall() - return result diff --git a/src/basket.py b/src/basket.py deleted file mode 100644 index 36f7777..0000000 --- a/src/basket.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- - -import config - - -class Basket: - sum_price = None - items_num = None - items = [] - - def set_items(self, *args): # Item - for item in range(args): # input = list of Items objects - self.items.append(item) # no output - self.items_num += 1 - self.sum_price += Item.get_price() - - def set_data_items(self, *args): # , Item.data_types - for data in range(args): # Item.data_types Item - temp = Item() # Basket.items[] - temp.set_data(data) - self.items.append(temp) - temp.delete() - - def delete_item(self, arg): # Basket.items[] - if isinstance(arg,Item): - try: - self.items.remove(arg) - self.items.sort() - except ValueError as e: - config.log(Error=e, Text="CANT_DELETE_ITEM\nITEM_NOT_EXIST") - if isinstance(arg,int): - try: - self.items.remove(self.items[arg]) - self.items.sort() - except IndexError: - config.log(Error=e, Text="CANT_DELETE_ITEM\nITEM_WITH_SUCH_INDEX_NOT_EXIST") - - def get_items(self): - args = [] - for item in range(self.items): - args.append((item.get_data())) - return args - - def delete(self): - self.sum_price = None - self.items_num = None - self.items = None diff --git a/src/bot.py b/src/bot.py deleted file mode 100644 index b5a90cf..0000000 --- a/src/bot.py +++ /dev/null @@ -1,361 +0,0 @@ -import os -import sqlite3 -import telebot -import time -import random -from telebot import types -import urllib.request as urllib -import requests.exceptions as r_exceptions -from requests import ConnectionError -import pdb - -import const, base, markups, temp, config - -bot = telebot.TeleBot(const.token) -uploaded_items = {} - - -# u"Обработка /start команды - ветвление пользователей на покупателя и продавца" -@bot.message_handler(commands=['start']) -def start(message): - base.add_user(message) - if base.is_seller(message.from_user.id): - bot.send_message(message.chat.id, const.welcome_celler, reply_markup=markups.start()) - else: - bot.send_message(message.chat.id, const.welcome_client, reply_markup=markups.start1()) - - -# Выдача меню с типами товаров -@bot.message_handler(regexp=u"Меню") -def client_panel(message): - bot.send_message(message.chat.id, u'Выберите категорию:', reply_markup=markups.start1()) - - -@bot.message_handler(func=lambda message: message.text in const.messages.keys()) -def handle_stand_msgs(message): - bot.send_message(message.chat.id, const.messages[message.text]) - - -@bot.callback_query_handler(func=lambda call: call.data == 'client_panel') -def client_panel(call): - bot.edit_message_text(const.welcome_client, chat_id=call.message.chat.id, message_id=call.message.message_id) - bot.send_message(chat_id=call.message.chat.id, text='...', reply_markup=markups.start1()) - - -# Запуск обработчика продавцов -@bot.callback_query_handler(func=lambda call: call.data == 'celler_panel') -def celler_panel(call): - bot.edit_message_text(u'Админка. Выбери действие.', call.message.chat.id, call.message.message_id, - parse_mode='Markdown', reply_markup=markups.edit()) - - -@bot.callback_query_handler(func=lambda call: call.data == 'retrieve') -def handle_retrieve(call): - bot.send_message(call.message.chat.id, "TextHere", reply_markup=telebot.types.InlineKeyboardMarkup().row( - telebot.types.InlineKeyboardButton('В меню', callback_data="menu"))) - - -@bot.callback_query_handler(func=lambda call: call.data == 'menu') -def handle_reieve(call): - send_menu(call.message) - - -def spamm(message): - for i in base.get_users(): - try: - bot.send_message(i[0], message.text) - except Exception: - continue - - -@bot.message_handler(commands=['mail']) -def mail_spam(message): - print('here', message.chat.id, const.admin_id) - if int(message.chat.id) == int(const.admin_id): - msg = bot.send_message(message.chat.id, 'Напишите сообщение для рассылки') - bot.register_next_step_handler(msg, spamm) - - -@bot.message_handler(regexp=const.menu_name) -def handle_rer(message): - print('here') - send_menu(message) - - -@bot.message_handler(func=lambda message: message.text in const.types.keys()) -def handle_fast(message): - bot.send_message(message.chat.id, const.msgs[message.text]) - for item in base.type_finder(const.types[message.text]): - uploaded_items[str(item.id)] = 0 - mark = telebot.types.InlineKeyboardMarkup().row( - telebot.types.InlineKeyboardButton('Перейти', callback_data='retrieve')) - try: - url = item.url - photo = open("temp.jpg", 'w') - photo.close() - photo = open("temp.jpg", 'rb') - urllib.urlretrieve(url, "temp.jpg") - bot.send_photo(chat_id=message.chat.id, photo=photo) - bot.send_message(message.chat.id, item.description, markup=mark) - photo.close() - os.remove("temp.jpg") - except Exception: - bot.send_message(message.chat.id, item.description, reply_markup=mark) - - -# Переход в категории -def send_menu(message): - bot.send_message(message.chat.id, 'Выберите нужную категорию.', reply_markup=markups.show_types(message.chat.id)) - - -def handle_price(message): - if not message.text.isdigit(): - msg = bot.send_message(message.chat.id, 'Invalid input data, try again...') - bot.register_next_step_handler(msg, handle_price) - return 0 - bot.send_message(const.admin_id, ";".join([message.text, str(message.chat.id)])) - bot.send_message(message.chat.id, text=u'Ваш личный номер резерва кошельков на 30 минут: 1ae085ae-667c'.format( - message.chat.id) + '-4155-bb9e-e84c6a7053c'.format(chat_id=message.chat.id) + '4\n' - 'Вы получите всю информацию о заказе сразу после оплаты.\n' - 'Размер оплаты = %d' - '--------------------------------\n' - 'Реквизиты для оплаты BTC:\n' - 'Ваш личный номер кошелька BTC: 1EcDBmsqAqu3o7vypcZYMn4wZtATswTcTG\n' - 'После получения 1 подтверждения сетью, Бот спросит у вас номер товара. Введите его в формате 578040. После этого вам сразу же будет выдан адрес.\n' - '--------------------------------\n' - 'Сумма платежа должна быть такой, какая указана продавцом. В противном случае ваш платеж не будет зачислен в автоматиеческом режиме.\n' - 'Важно оплатить зарезирвированный товар в течение указанного времени, иначе Ваш заказ будет отменён. Когда срок резерва будет подходить к концу, система предложим Вам продлить резерв ещё на пол-часа.\n' - 'После получения товара вы можете оставить отзыв о товаре или продавце на сайте http://ramp24vqtden6hep.onion/number или написав в службу поддержки @helpramp, указав личный номер резерва кошельков.\n' - 'При необходимости Вы можете отменить резерв кошельков, нажав кнопку "Отмена"' % ( - int(message.text) * 1.10), parse_mode='HTML', - reply_markup=telebot.types.InlineKeyboardMarkup().row( - telebot.types.InlineKeyboardButton('Проверить', callback_data='check'), telebot.types.InlineKeyboardButton('Отмена', callback_data='quit'))) - - -bot.callback_query_handler(func=lambda call: call.data == 'quit') -def quit_pricing(call): - print('her') - bot.edit_message_text(const.quit_text, call.message.chat.id, - call.message.message_id) - -# u"Отображение товаров и занесение их в кэш" -@bot.callback_query_handler(func=lambda call: call.data in base.give_menu()) -def show_items(call): - for item in base.type_finder(call.data): - key = item.get_desc2() - uploaded_items[str(item.id)] = 0 - print(uploaded_items) - try: - url = item.url - photo = open("temp.jpg", 'w') # u"Инициализация файла" - photo.close() - photo = open("temp.jpg", 'rb') - urllib.urlretrieve(url, "temp.jpg") - bot.send_photo(chat_id=call.message.chat.id, photo=photo) - msg = bot.send_message(call.message.chat.id, item.description, reply_markup=key) - photo.close() - os.remove("temp.jpg") - except Exception: - msg = bot.send_message(call.message.chat.id, item.description, reply_markup=key) - - -@bot.callback_query_handler(func=lambda call: call.data.startswith('p')) -def handle_your_price(call): - msg = bot.send_message(call.message.chat.id, 'Напишите вашу цену') - bot.register_next_step_handler(msg, handle_price) - - -@bot.callback_query_handler(func=lambda call: call.data == 'check') -def handle_your_price(call): - bot.send_message(call.message.chat.id, 'Thisistext', reply_markup=telebot.types.InlineKeyboardMarkup().row( - telebot.types.InlineKeyboardButton('Проверить', callback_data='check'))) - - -# u"Обработка первой покупки товара" -@bot.callback_query_handler(func=lambda call: call.data in uploaded_items) -def callback_handler(call): - uploaded_items[str(call.data)] += 1 - print('uploaded_items : ' + str(uploaded_items)) - print('callback_handler.call.data = ' + str(call.data)) - markup = markups.add(call.data) - a = random.randint(50000, 100000) - bot.send_message(chat_id=call.message.chat.id, - text=u'Ваш личный номер резерва кошельков на 30 минут: 1ae085ae-667c'.format( - chat_id=call.message.chat.id) + str(a) + '-4155-bb9e-e84c6a7053c'.format( - chat_id=call.message.chat.id) + str(a) + '4\n' - 'Вы получите всю информацию о заказе сразу после оплаты.\n' - '--------------------------------\n' - 'Реквизиты для оплаты BTC:\n' - 'Ваш личный номер кошелька BTC: 1EcDBmsqAqu3o7vypcZYMn4wZtATswTcTG\n' - 'После получения 1 подтверждения сетью, Бот спросит у вас номер товара. Введите его в формате 578040. После этого вам сразу же будет выдан адрес.\n' - '--------------------------------\n' - 'Сумма платежа должна быть такой, какая указана продавцом. В противном случае ваш платеж не будет зачислен в автоматиеческом режиме.\n' - 'Важно оплатить зарезирвированный товар в течение указанного времени, иначе Ваш заказ будет отменён. Когда срок резерва будет подходить к концу, система предложим Вам продлить резерв ещё на пол-часа.\n' - 'После получения товара вы можете оставить отзыв о товаре или продавце на сайте http://ramp24vqtden6hep.onion/number или написав в службу поддержки @helpramp, указав личный номер резерва кошельков.\n' - 'При необходимости Вы можете отменить резерв кошельков, нажав кнопку "Отмена"', - parse_mode='HTML', reply_markup=markup) - - -# Ответ "оплатил" на вопрос об оплате - - -# Дальше идет админка---------------------------------------- - - -# Добавление категории -@bot.callback_query_handler(func=lambda call: call.data == 'add_kat') -def handle_add_kat(call): - sent = bot.send_message(call.message.chat.id, "Введите название категории", reply_markup=markups.return_to_menu()) - bot.register_next_step_handler(sent, base.add_kat) - - -# Удаление категории -@bot.callback_query_handler(func=lambda call: call.data == 'delete_kat') -def handle_delete_kat(call): - bot.edit_message_text("Выберите категорию для удаления", call.message.chat.id, - call.message.message_id, reply_markup=markups.delete_kat()) - - -@bot.callback_query_handler(func=lambda call: call.data[0] == '?') -def handle_delete_this_kat(call): - db = sqlite3.connect("clientbase.db") - cur = db.cursor() - cur.execute("DELETE FROM categories WHERE name = ?", (str(call.data[1:]),)) - db.commit() - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, - reply_markup=markups.delete_kat()) - print('deleted') - - -# Добавление товара. - - -# Выбор типа товара -@bot.callback_query_handler(func=lambda call: call.data == 'add_item') -def handle_add_item_type(call): - new_item = temp.Item() - const.new_items_user_adding.update([(call.message.chat.id, new_item)]) - sent = bot.send_message(call.message.chat.id, "Выберите тип товара:", reply_markup=markups.add_item()) - bot.register_next_step_handler(sent, base.add_item_category) - const.user_adding_item_step.update([(call.message.chat.id, "Enter name")]) - - -# Ввод наименования товара -@bot.message_handler(func=lambda message: base.get_user_step(message.chat.id) == "Enter name") -def handle_add_item_description(message): - sent = bot.send_message(message.chat.id, "Введите описание") - bot.register_next_step_handler(sent, base.add_item_description) - const.user_adding_item_step[message.chat.id] = "End" - - -# Конец добавления товара -@bot.message_handler(func=lambda message: base.get_user_step(message.chat.id) == "End") -def handle_add_item_end(message): - bot.send_message(message.chat.id, "Добавлено!\n Меню:", reply_markup=markups.show_types(message.chat.id)) - const.user_adding_item_step.pop(message.chat.id) - - -# Удаление товара -@bot.callback_query_handler(func=lambda call: call.data == 'delete_item') -def handle_delete_item(call): - bot.edit_message_text("Выберите товар для удаления", call.message.chat.id, call.message.message_id) - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, - reply_markup=markups.delete_item(call.message.chat.id)) - - -@bot.callback_query_handler(func=lambda call: call.data[0] == '^') -def handle_delete_from_db(call): - db = sqlite3.connect("clientbase.db") - cur = db.cursor() - cur.execute("DELETE FROM items WHERE id = ?", (str(call.data[1:]),)) - db.commit() - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, - reply_markup=markups.delete_item(call.message.chat.id)) - print('deleted') - - -@bot.message_handler(content_types=['text']) -def bank(message): - markup_start = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - markup_start.row('Как работает', 'Сделать заказ') - markup_start.row('Отзывы', 'Поддержка') - markup_start.row('Стать продавцом') - keyboard1 = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - keyboard1.row('Сделать заказ', 'Отзывы') - keyboard1.row('Поддержка', 'Стать продавцом') - keyboard3 = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - keyboard3.row('Как работает', 'Сделать заказ') - keyboard3.row('Поддержка', 'Стать продавцом') - keyboard4 = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - keyboard4.row('Как работает', 'Сделать заказ') - keyboard4.row('Отзывы', 'Стать продавцом') - keyboard5 = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - keyboard5.row('Как работает', 'Сделать заказ') - keyboard5.row('Отзывы', 'Поддержка') - markup_oplata = types.InlineKeyboardMarkup() - markup_oplata.add(*[types.InlineKeyboardButton(text=name, callback_data=name) for name in ['Купить']]) - if message.text == 'Как работает': - print('Как работает') - bot.send_message(message.chat.id, - 'Сервис полностью автоматизирован, поэтому процесс от покупки до получения клада составляет не более 1 часа.\n' - 'Для совершения заказа вам необходимо перейти в раздел "Сделать заказ", указать ваш город и бот автоматически найдет продавцов на нашей площадке в вашем городе и покажет товар имеющийся в наличии.\n' - 'После выбора товара вам будут предоставлены личные реквизиты для оплаты. Наша площадка поддерживает 2 способа оплаты: Qiwi и Btc, каждый продавец выставляет свои реквизиты, поэтому некоторые товары можно будет купить только за Qiwi или только за Btc.\n' - 'При оплате на счет Qiwi, в комментарии к платежу обязательно указывайте ваш ник в Telegram в формате @helpramp. В противном случае платеж не будет зачислен в автоматическом режиме.\n' - 'При оплате Btc, выдаваемый вам адрес Btc кошелька привязывается к вашему нику в Telegram, после получения 1 подтверждения Бот автоматически подтвердит поступление средств и спросит вас номер товара. Укажите его в формате 578040.\n' - 'После проведения перевода нажмите на кнопку "Я оплатил", Бот автоматически проверит ваш платеж и выдаст вам адрес клада со всей дополнительной информацией (зависит от продавца и его кладменов).\n' - 'Вы можете оставить отзыв о товаре или продавце на сайте http://ramp24vqtden6hep.onion/number или написав в службу поддержки @helpramp, указав личный номер резерва кошельков.', - parse_mode='HTML', reply_markup=keyboard1) - if message.text == 'Отзывы': - print('Отзывы') - bot.send_message(message.chat.id, - 'Вы можете найти всю необходимую информацию о работе автоматического Бота на форумах:\n' - 'http://ramp24vqtden6hep.onion/info\n' - 'http://lkncc.cc/newrampbot\n' - 'http://leomarketjdridoo.onion/newrampbot\n' - 'http://eeyovrly7charuku.onion/newrampbot\n' - 'http://tochka3evjl3sxdv.onion/newrampbot\n' - 'Так же вы можете оставлять отзывы о товаре или продавце на сайте http://ramp24vqtden6hep.onion/number, указав личный номер резерва кошельков.', - parse_mode='HTML', reply_markup=keyboard3) - if message.text == 'Поддержка': - print('Поддержка') - bot.send_message(message.chat.id, - 'Если у вас возникли трудности, проблемы с оплатой или получением клада, или у вас есть вопросы о работе Бота- вы можете связаться со службой поддержки @Newrampbot в Telegram @helpramp.', - parse_mode='HTML', reply_markup=keyboard4) - if message.text == 'Стать продавцом': # К ЭТОЙ КНОПКЕ НУЖНО ПОДРУБИТЬ ФОТО И ВИДЕО - print('Стать продавцом') - bot.send_message(message.chat.id, - 'Для того, чтобы стать продавцом на нашей площадке, вам необходимо преобрести Месячный или Пожизненный тариф продавца.\n' - 'Стоимость подключения:\n' - '5000 Рублей в месяц с возможностью продления тарифа до Пожизненного;\n' - '50000 Рублей - Пожизненный тариф продавца.\n' - 'В эту стоимость включено:\n' - 'Удобная Админ-панель с возможностью выкладывать товары с фотографиями (Только в WEB версии), добавлять ваши реквизиты и готовые адреса закладок прямо из мессенджера Telegram.\n' - 'Поддержка продавцов 24/7.\n' - 'Никаких дополнительных комиссий, оплата за товар производится только на ваши счета или кошельки.\n' - 'Для подключения тарифа продавца необходимо совершить перевод на наш счет Qiwi кошелька +79619218391 с указанием комментария к платежу вашего ника в Telegram в формате @helpramp.\n' - 'После проведения платежа с вами свяжется наш агент технической поддержки.', parse_mode='HTML', - reply_markup=keyboard5) - if message.text == 'Сделать заказ': - print('Сделать заказ') - sent = bot.send_message(message.chat.id, 'Укажите ваш город в формате #Город') - bot.register_next_step_handler(sent, hello) - if message.text == 'Назад': - print('Назад') - bot.send_message(message.chat.id, 'Выберите кнопку.', parse_mode='HTML', reply_markup=markup_start) - - -# stupid disgusting while true loop -# I was writing this when I was a highschooler -# lord forgive me for my sins -while True: - try: - bot.polling(none_stop=True, interval=0) - except ConnectionError as expt: - config.log(Exception='HTTP_CONNECTION_ERROR', text=expt) - print('Connection lost..') - time.sleep(30) - continue - except r_exceptions.Timeout as exptn: - config.log(Exception='HTTP_REQUEST_TIMEOUT_ERROR', text=exptn) - time.sleep(5) - continue diff --git a/src/config.py b/src/config.py deleted file mode 100644 index c043828..0000000 --- a/src/config.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -import time - -goRegister = False - - -def log(**kwargs): - try: - log.logs += 1 - print("IN LOG!!!") - except AttributeError: - log.logs = 0 - res = '-----------------------------------------------------------\n' - res += 'Log#' + str(log.logs) + ' at ' + str(time.asctime(time.localtime(time.time()))) + '\n' - for i in range(len(kwargs)): - ln = list(kwargs.popitem()) - res += str(ln[0]) + " : " + str(ln[1]) + '\n' - print(res) diff --git a/src/const.py b/src/const.py deleted file mode 100644 index dd3e28d..0000000 --- a/src/const.py +++ /dev/null @@ -1,36 +0,0 @@ -token = '' -DB_PATH = '../clientbase.db' - -# Приветственное сообщение -welcome_celler = 'Привет селлер, что будем делать?' -welcome_client = 'Воспользуйтесь нижней панелью для поиска нужной информации и покупки товара. Для вашего удобства все разделы поделены на основные категории.' - -admin_id = '' - -user_adding_item_step = {} -new_items_user_adding = {} - -types = { - 'Деньги': 'Деньги', - 'Документы': 'Документы', - 'Авиа/Отели': 'Авиа/Отели', - 'Взлом': 'Взлом', - 'Программы/схемы':'Программы/схемы', - 'Стать продавцом': 'Стать продавцом' -} - -msgs = { - 'Деньги': 'Text1', - 'Документы': 'Text2', - 'Авиа/Отели': 'Text3', - 'Взлом': 'Text4', - 'Программы/схемы':'Text5', - 'Стать продавцом': 'Text6' -} -messages = { - 'О сервисе': 'Message1', - 'Сделки': "Message2", - 'Кошелек': 'Message3' -} -menu_name = 'Гарант' -quit_text = 'Отменено' diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..612d880 --- /dev/null +++ b/src/main.py @@ -0,0 +1,22 @@ +"""Entry point for running the bot locally.""" + +from __future__ import annotations + +from telebot import TeleBot + +from marketbot import MarketBot, CatalogRepository, Database, load_config + + +def main() -> None: + config = load_config() + database = Database(config.database_path) + repository = CatalogRepository(database) + bot = TeleBot(config.token, parse_mode="Markdown") + + market_bot = MarketBot(bot, repository, config) + market_bot.register_handlers() + bot.infinity_polling() + + +if __name__ == "__main__": + main() diff --git a/src/marketbot/__init__.py b/src/marketbot/__init__.py new file mode 100644 index 0000000..59137e2 --- /dev/null +++ b/src/marketbot/__init__.py @@ -0,0 +1,14 @@ +"""Modern Telegram marketplace bot demo package.""" + +from .bot import MarketBot +from .config import BotConfig, load_config +from .database import Database +from .repository import CatalogRepository + +__all__ = [ + "MarketBot", + "BotConfig", + "Database", + "load_config", + "CatalogRepository", +] diff --git a/src/marketbot/__main__.py b/src/marketbot/__main__.py new file mode 100644 index 0000000..6990d72 --- /dev/null +++ b/src/marketbot/__main__.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from telebot import TeleBot + +from . import CatalogRepository, Database, MarketBot, load_config + + +def main() -> None: + config = load_config() + repository = CatalogRepository(Database(config.database_path)) + bot = TeleBot(config.token, parse_mode="Markdown") + market_bot = MarketBot(bot, repository, config) + market_bot.register_handlers() + bot.infinity_polling() + + +if __name__ == "__main__": + main() diff --git a/src/marketbot/bot.py b/src/marketbot/bot.py new file mode 100644 index 0000000..50d7aa9 --- /dev/null +++ b/src/marketbot/bot.py @@ -0,0 +1,95 @@ +"""High level orchestration of the Telegram bot.""" + +from __future__ import annotations + +from telebot import TeleBot +from telebot.types import CallbackQuery, Message + +from . import keyboards +from .config import BotConfig +from .repository import CatalogRepository + + +class MarketBot: + """Convenience wrapper that wires handlers to a :class:`TeleBot`.""" + + def __init__(self, bot: TeleBot, repository: CatalogRepository, config: BotConfig) -> None: + self.bot = bot + self.repository = repository + self.config = config + + def register_handlers(self) -> None: + self.repository.initialise_schema() + + @self.bot.message_handler(commands=["start", "help"]) + def handle_start(message: Message) -> None: + user, created = self.repository.upsert_user( + telegram_id=message.from_user.id, + username=message.from_user.username, + first_name=message.from_user.first_name, + last_name=message.from_user.last_name, + ) + categories = self.repository.list_categories() + reply_markup = keyboards.main_menu(categories) + greeting = ( + "👋 Welcome to MarketBot!\n\n" + "Use the menu below to browse categories." + ) + self.bot.send_message(chat_id=message.chat.id, text=greeting, reply_markup=reply_markup) + if created and self.config.admin_usernames: + human_name = user.username or str(user.telegram_id) + for admin in self.config.admin_usernames: + self.bot.send_message( + chat_id=f"@{admin}", + text=f"A new user @{human_name} just started the bot.", + ) + + @self.bot.message_handler(func=self._is_category_request) + def handle_category(message: Message) -> None: + category_name = message.text.strip() + categories = {category.name: category for category in self.repository.list_categories()} + category = categories.get(category_name) + if not category: + self.bot.send_message(chat_id=message.chat.id, text="Unknown category. Try using the menu buttons.") + return + products = self.repository.list_products_for_category(category.id) + if not products: + self.bot.send_message(chat_id=message.chat.id, text="No products in this category yet – check back soon!") + return + markup = keyboards.category_inline(products) + self.bot.send_message(chat_id=message.chat.id, text="Pick a product to learn more:", reply_markup=markup) + + @self.bot.callback_query_handler(func=lambda call: call.data.startswith("product:")) + def handle_product(call: CallbackQuery) -> None: + product_id = int(call.data.split(":", maxsplit=1)[1]) + products = {product.id: product for product in self.repository.list_products(only_active=False)} + product = products.get(product_id) + if not product: + self.bot.answer_callback_query(call.id, "Product not found", show_alert=True) + return + markup = keyboards.product_actions(product) + details = ( + f"*{product.name}*\n" + f"Price: {product.price:.2f} {product.currency}\n\n" + f"{product.description}" + ) + self.bot.edit_message_text( + text=details, + chat_id=call.message.chat.id, + message_id=call.message.message_id, + reply_markup=markup, + parse_mode="Markdown", + ) + + @self.bot.callback_query_handler(func=lambda call: call.data == "back:categories") + def handle_back(call: CallbackQuery) -> None: + categories = self.repository.list_categories() + markup = keyboards.main_menu(categories) + self.bot.send_message(chat_id=call.message.chat.id, text="Back to categories", reply_markup=markup) + self.bot.answer_callback_query(call.id) + + def _is_category_request(self, message: Message) -> bool: + if not message.text: + return False + known_categories = {category.name for category in self.repository.list_categories()} + return message.text.strip() in known_categories diff --git a/src/marketbot/config.py b/src/marketbot/config.py new file mode 100644 index 0000000..0675da5 --- /dev/null +++ b/src/marketbot/config.py @@ -0,0 +1,87 @@ +"""Application configuration helpers. + +The original project hard-coded secrets directly in the source files. +For a real-world bot we want to keep secrets out of the repository and +support different environments. The :mod:`config` module exposes a +light-weight settings dataclass together with a ``load_config`` helper +that reads from environment variables. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, Tuple + +from dotenv import load_dotenv + + +@dataclass(frozen=True) +class BotConfig: + """Configuration values used by :class:`~marketbot.bot.MarketBot`. + + Attributes + ---------- + token: + Telegram API token. Required for the bot to authenticate. + database_path: + Absolute path to the SQLite database file. The parent directory + will be created automatically if it does not yet exist. + admin_usernames: + Optional tuple of usernames that should receive administrative + notifications. This is mostly used to showcase dependency + injection in ``MarketBot`` and can be expanded further. + """ + + token: str + database_path: Path + admin_usernames: Tuple[str, ...] = () + + @property + def database_path_str(self) -> str: + """Return the database path as a string for sqlite3.""" + + return str(self.database_path) + + +def _read_env_list(raw_value: str | None) -> Tuple[str, ...]: + if not raw_value: + return () + values: Iterable[str] = (item.strip() for item in raw_value.split(",")) + return tuple(value for value in values if value) + + +def load_config(env_file: str | Path | None = None) -> BotConfig: + """Load :class:`BotConfig` from ``.env`` files and environment variables. + + Parameters + ---------- + env_file: + Optional location of the ``.env`` file. When omitted the default + search strategy used by :func:`dotenv.load_dotenv` is applied. + """ + + load_dotenv(dotenv_path=env_file) + + # Token stored in environment for security reasons. + token_value = _get_required_env("TELEGRAM_BOT_TOKEN") + db_path = Path(_get_required_env("MARKETBOT_DB")).expanduser().resolve() + admins = _read_env_list(_get_env("MARKETBOT_ADMINS")) + + return BotConfig(token=token_value, database_path=db_path, admin_usernames=admins) + + +def _get_env(name: str) -> str | None: + from os import getenv + + return getenv(name) + + +def _get_required_env(name: str) -> str: + value = _get_env(name) + if value is None or value.strip() == "": + raise ValueError( + f"Environment variable {name!r} must be defined. " + "See README for configuration instructions." + ) + return value diff --git a/src/marketbot/database.py b/src/marketbot/database.py new file mode 100644 index 0000000..15765e8 --- /dev/null +++ b/src/marketbot/database.py @@ -0,0 +1,41 @@ +"""SQLite database helpers used by the repository layer.""" + +from __future__ import annotations + +import sqlite3 +from contextlib import contextmanager +from pathlib import Path +from typing import Generator + + +class Database: + """Thin wrapper around ``sqlite3`` with sensible defaults. + + The wrapper enables type checking, provides a deterministic way of + creating the database file and exposes a context manager that + automatically commits or rolls back transactions depending on whether + an exception occurred. + """ + + def __init__(self, path: Path) -> None: + self.path = path + if not self.path.is_absolute(): + raise ValueError("Database path must be absolute for clarity.") + self.path.parent.mkdir(parents=True, exist_ok=True) + + def connect(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.path) + connection.row_factory = sqlite3.Row + return connection + + @contextmanager + def transaction(self) -> Generator[sqlite3.Connection, None, None]: + connection = self.connect() + try: + yield connection + connection.commit() + except Exception: + connection.rollback() + raise + finally: + connection.close() diff --git a/src/marketbot/keyboards.py b/src/marketbot/keyboards.py new file mode 100644 index 0000000..ec2821e --- /dev/null +++ b/src/marketbot/keyboards.py @@ -0,0 +1,34 @@ +"""Factory functions for Telegram reply markup.""" + +from __future__ import annotations + +from typing import Iterable + +from telebot import types + +from .models import Category, Product + + +def main_menu(categories: Iterable[Category]) -> types.ReplyKeyboardMarkup: + keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) + for category in categories: + keyboard.add(types.KeyboardButton(text=category.name)) + keyboard.add(types.KeyboardButton(text="🛒 View basket")) + return keyboard + + +def category_inline(products: Iterable[Product]) -> types.InlineKeyboardMarkup: + markup = types.InlineKeyboardMarkup(row_width=1) + for product in products: + markup.add(types.InlineKeyboardButton(text=product.name, callback_data=f"product:{product.id}")) + return markup + + +def product_actions(product: Product) -> types.InlineKeyboardMarkup: + markup = types.InlineKeyboardMarkup(row_width=2) + markup.add( + types.InlineKeyboardButton(text="Add to basket", callback_data=f"basket:add:{product.id}"), + types.InlineKeyboardButton(text="Share", switch_inline_query=product.name), + ) + markup.add(types.InlineKeyboardButton(text="⬅️ Back to categories", callback_data="back:categories")) + return markup diff --git a/src/marketbot/models.py b/src/marketbot/models.py new file mode 100644 index 0000000..6dbb737 --- /dev/null +++ b/src/marketbot/models.py @@ -0,0 +1,72 @@ +"""Data models used throughout the application.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +@dataclass(slots=True) +class Category: + id: int + name: str + description: str + + +@dataclass(slots=True) +class Product: + id: int + category_id: int + name: str + price: float + currency: str + description: str + photo_url: str | None + is_active: bool + created_at: datetime + + +@dataclass(slots=True) +class User: + id: int + telegram_id: int + username: str | None + first_name: str | None + last_name: str | None + created_at: datetime + + +def ensure_currency(value: str) -> str: + """Normalise currency codes to upper-case 3 letter strings.""" + + code = value.strip().upper() + if len(code) != 3: + raise ValueError("Currency codes must use the 3 letter ISO format (e.g. 'USD').") + return code + + +def row_to_category(row) -> Category: + return Category(id=row["id"], name=row["name"], description=row["description"]) + + +def row_to_product(row) -> Product: + return Product( + id=row["id"], + category_id=row["category_id"], + name=row["name"], + price=row["price"], + currency=row["currency"], + description=row["description"], + photo_url=row["photo_url"], + is_active=bool(row["is_active"]), + created_at=datetime.fromisoformat(row["created_at"]), + ) + + +def row_to_user(row) -> User: + return User( + id=row["id"], + telegram_id=row["telegram_id"], + username=row["username"], + first_name=row["first_name"], + last_name=row["last_name"], + created_at=datetime.fromisoformat(row["created_at"]), + ) diff --git a/src/marketbot/repository.py b/src/marketbot/repository.py new file mode 100644 index 0000000..96cce17 --- /dev/null +++ b/src/marketbot/repository.py @@ -0,0 +1,177 @@ +"""Repository layer for encapsulating database access.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import List, Sequence + +from .database import Database +from .models import Category, Product, User, ensure_currency, row_to_category, row_to_product, row_to_user + + +class CatalogRepository: + """High level CRUD operations for categories, products and users.""" + + def __init__(self, database: Database) -> None: + self._db = database + + def initialise_schema(self) -> None: + with self._db.transaction() as connection: + connection.executescript( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + telegram_id INTEGER NOT NULL UNIQUE, + username TEXT, + first_name TEXT, + last_name TEXT, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + name TEXT NOT NULL, + price REAL NOT NULL, + currency TEXT NOT NULL, + description TEXT NOT NULL, + photo_url TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + FOREIGN KEY(category_id) REFERENCES categories(id) + ); + """ + ) + + # ------------------------------------------------------------------ + # Users + # ------------------------------------------------------------------ + def upsert_user( + self, + telegram_id: int, + username: str | None, + first_name: str | None, + last_name: str | None, + ) -> tuple[User, bool]: + now = datetime.now(tz=timezone.utc).isoformat() + with self._db.transaction() as connection: + existing = connection.execute( + "SELECT * FROM users WHERE telegram_id = ?", (telegram_id,) + ).fetchone() + if existing: + connection.execute( + """ + UPDATE users + SET username = ?, first_name = ?, last_name = ? + WHERE telegram_id = ? + """, + (username, first_name, last_name, telegram_id), + ) + updated = connection.execute( + "SELECT * FROM users WHERE telegram_id = ?", (telegram_id,) + ).fetchone() + assert updated is not None + return row_to_user(updated), False + + connection.execute( + """ + INSERT INTO users (telegram_id, username, first_name, last_name, created_at) + VALUES (?, ?, ?, ?, ?) + """, + (telegram_id, username, first_name, last_name, now), + ) + new_row = connection.execute("SELECT * FROM users WHERE telegram_id = ?", (telegram_id,)).fetchone() + assert new_row is not None + return row_to_user(new_row), True + + # ------------------------------------------------------------------ + # Categories + # ------------------------------------------------------------------ + def add_category(self, name: str, description: str) -> Category: + with self._db.transaction() as connection: + connection.execute( + "INSERT INTO categories (name, description) VALUES (?, ?)", + (name, description), + ) + row = connection.execute("SELECT * FROM categories WHERE name = ?", (name,)).fetchone() + assert row is not None + return row_to_category(row) + + def list_categories(self) -> Sequence[Category]: + with self._db.transaction() as connection: + rows = connection.execute("SELECT * FROM categories ORDER BY name").fetchall() + return tuple(row_to_category(row) for row in rows) + + # ------------------------------------------------------------------ + # Products + # ------------------------------------------------------------------ + def add_product( + self, + *, + category_id: int, + name: str, + price: float, + currency: str, + description: str, + photo_url: str | None, + is_active: bool = True, + ) -> Product: + currency_code = ensure_currency(currency) + now = datetime.now(tz=timezone.utc).isoformat() + with self._db.transaction() as connection: + connection.execute( + """ + INSERT INTO products + (category_id, name, price, currency, description, photo_url, is_active, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + category_id, + name, + price, + currency_code, + description, + photo_url, + int(is_active), + now, + ), + ) + row = connection.execute( + "SELECT * FROM products WHERE name = ? ORDER BY created_at DESC LIMIT 1", (name,) + ).fetchone() + assert row is not None + return row_to_product(row) + + def list_products(self, *, only_active: bool = True) -> Sequence[Product]: + query = "SELECT * FROM products" + params: List[object] = [] + if only_active: + query += " WHERE is_active = 1" + query += " ORDER BY created_at DESC" + with self._db.transaction() as connection: + rows = connection.execute(query, params).fetchall() + return tuple(row_to_product(row) for row in rows) + + def list_products_for_category(self, category_id: int, *, only_active: bool = True) -> Sequence[Product]: + query = "SELECT * FROM products WHERE category_id = ?" + params: List[object] = [category_id] + if only_active: + query += " AND is_active = 1" + query += " ORDER BY created_at DESC" + with self._db.transaction() as connection: + rows = connection.execute(query, params).fetchall() + return tuple(row_to_product(row) for row in rows) + + def deactivate_product(self, product_id: int) -> None: + with self._db.transaction() as connection: + connection.execute("UPDATE products SET is_active = 0 WHERE id = ?", (product_id,)) + + def activate_product(self, product_id: int) -> None: + with self._db.transaction() as connection: + connection.execute("UPDATE products SET is_active = 1 WHERE id = ?", (product_id,)) diff --git a/src/markups.py b/src/markups.py deleted file mode 100644 index 1be345c..0000000 --- a/src/markups.py +++ /dev/null @@ -1,107 +0,0 @@ -import telebot -import temp, base -import const - -def start(): - markup = telebot.types.InlineKeyboardMarkup() - btn_user = telebot.types.InlineKeyboardButton(text="Покупать!", callback_data='client_panel') - btn_celler = telebot.types.InlineKeyboardButton(text="Продавать!", callback_data='celler_panel') - markup.add(btn_celler, btn_user) - return markup - - -def start1(): - markup_start = telebot.types.ReplyKeyboardMarkup() - markup_start.row(*[i for i in const.messages.keys()]) - markup_start.row('Деньги', 'Документы') - markup_start.row('Авиа/Отели', 'Взлом') - markup_start.row('Программы/схемы') - markup_start.row('Стать продавцом') - markup_start.row(const.menu_name) - return markup_start - - -def show_types(user_id): - markup = telebot.types.InlineKeyboardMarkup() - for key in base.give_menu(): - button = telebot.types.InlineKeyboardButton(text=key, callback_data=key) - markup.add(button) - print(base.give_menu()) - return markup - - -def make_bill(): - markup = telebot.types.ReplyKeyboardMarkup(True, False) - markup.row("Меню") - markup.row("Оформить заказ") - return markup - - -def return_to_menu(): - markup = telebot.types.ReplyKeyboardMarkup(True, False) - markup.row("Меню") - return markup - - -def is_seller(): - markup = telebot.types.InlineKeyboardMarkup() - butquest = telebot.types.InlineKeyboardButton('Зайти как покупатель?', callback_data='.') - btn_y = telebot.types.InlineKeyboardButton('Yes', callback_data='&Yes') - btn_n = telebot.types.InlineKeyboardButton('No', callback_data='&No') - markup.row(butquest) - markup.row(btn_n, btn_y) - return markup - - -def add(id): - markup = telebot.types.InlineKeyboardMarkup() - butp = telebot.types.InlineKeyboardButton('Я оплатил', callback_data='+' + str(id)) - butm = telebot.types.InlineKeyboardButton('Отмена', callback_data='-' + str(id)) - markup.row(butp, butm) - return markup - - -def edit(): - markup = telebot.types.InlineKeyboardMarkup() - add_item = telebot.types.InlineKeyboardButton(text="Добавить товар", callback_data='add_item') - delete_item = telebot.types.InlineKeyboardButton(text="Удалить товар", callback_data='delete_item') - add_kat = telebot.types.InlineKeyboardButton(text="Добавить категорию", callback_data='add_kat') - delete_kat = telebot.types.InlineKeyboardButton(text="Удалить категорию", callback_data='delete_kat') - markup.row(add_kat, add_item) - markup.row(delete_kat, delete_item) - return markup - - -def add_item(): - markup = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) - for category in base.give_menu(): - markup.add(category) - return markup - - -def delete_item(user_id): - markup = telebot.types.InlineKeyboardMarkup() - for item in base.find_users_items(user_id): - btn_item = telebot.types.InlineKeyboardButton(text=item[5], callback_data="^" + str(item[0])) - markup.add(btn_item) - btn_menu = telebot.types.InlineKeyboardButton(text="Вернуться в админ-панель", callback_data="celler_panel") - markup.add(btn_menu) - return markup - - -def delete_kat(): - markup = telebot.types.InlineKeyboardMarkup() - for key in base.give_menu(): - button = telebot.types.InlineKeyboardButton(text=key, callback_data="?"+key) - markup.add(button) - btn_menu = telebot.types.InlineKeyboardButton(text="Вернуться в админ-панель", callback_data="celler_panel") - markup.add(btn_menu) - return markup - - -def give_desc(id): - print("in_murk") - markup = telebot.types.InlineKeyboardMarkup() - item = temp.item_finder(id) - btn_buy = telebot.types.InlineKeyboardButton(text='Купить', callback_data=str(item.id)) - markup.row(btn_buy) diff --git a/src/temp.py b/src/temp.py deleted file mode 100644 index dfc8420..0000000 --- a/src/temp.py +++ /dev/null @@ -1,158 +0,0 @@ -import sqlite3 as sqlite -import config, const -import telebot - - -def type_finder(item_type): - type = const.item_types[item_type] - db = sqlite.connect("clientbase.db") - cur = db.cursor() - cur.execute("SELECT id FROM items WHERE type = ?", (str(type))) - temp_items = cur.fetchall() - items = [] - for item in temp_items: - items.append(item_finder(item[0])) - return items - - -def item_finder(item_id): - db = sqlite.connect("clientbase.db") - cur = db.cursor() - cur.execute("SELECT * FROM items WHERE id = ?", (str(item_id))) - item = Item() - item.set_full_data(*cur.fetchone()) - print(item.get_data()) - return item - - -def isSeller(user_id): - db = sqlite.connect("clientbase.db") - cur = db.cursor() - cur.execute('SELECT user_id FROM clients WHERE user_id = (?)', (str(user_id),)) - if cur.fetchone(): - return True - else: - return False - - -def add_user(message): - db = sqlite.connect("clientbase.db") - cur = db.cursor() - try: - cur.execute("SELECT * FROM users WHERE user_id = (?)", (message.from_user.id,)) - except Exception as e: - config.log(Error=e, Text="DBTESTING ERROR") - if not cur.fetchone(): - try: - cur.execute("INSERT INTO users (user_id, first_name, last_name, username) VALUES (?,?,?,?)", ( - message.from_user.id, - message.from_user.first_name, - message.from_user.last_name, - message.from_user.username)) - config.log(Text="User successfully added", - user=str(message.from_user.first_name + " " + message.from_user.last_name)) - except Exception as e: - config.log(Error=e, Text="USER_ADDING_ERROR") - db.commit() - else: - config.log(Error="IN_THE_BASE_YET", - id=message.from_user.id, - info=str(message.from_user.last_name) + " " + str(message.from_user.first_name), - username=message.from_user.username) - - -def add_client(message): - db = sqlite.connect("clientbase.db") - cur = db.cursor() - login = message.text[1:] - try: - cur.execute("SELECT * FROM clients WHERE user_id = (?)", (message.from_user.id,)) - except Exception as e: - config.log(Error=e, Text="DBTESTING ERROR") - if not cur.fetchone(): - try: - cur.execute("INSERT INTO clients (user_id) VALUES (?)", (message.from_user.id,)) - config.log(Text="Client successfully added", - user=str(message.from_user.first_name + " " + message.from_user.last_name)) - except Exception as e: - config.log(Error=e, Text="CLIENT_ADDING_ERROR") - db.commit() - else: - config.log(Error="IN_THE_BASE_YET", - id=message.from_user.id, - info=str(message.from_user.last_name) + " " + str(message.from_user.first_name), - username=message.from_user.username) - - -class Item: - id = None - type = None - description = None - seller = None - data_types = ['id', 'type', 'description'] - - def get_data(self): # возвращает список , состо€щий из структуры данных типа Item.data_types - args = (self.description,) - return args - - def set_data(self, *args): - try: - self.description = args[3] - except Exception as e: - config.log(Error=e, text="SET_DATA_ERROR_OCCURED") - - def set_full_data(self, *args): - try: - self.id = args[0] - self.type = args[1] - self.description = args[5] - self.seller = args[8] - print('seller is ' + str(self.seller)) - except Exception as e: - config.log(Error=e, text="SET_DATA_ERROR_OCCURED") - - def get_desc2(self): - data = self.get_data() - markup = telebot.types.InlineKeyboardMarkup() - btn_buy = telebot.types.InlineKeyboardButton(text="Ваша цена", callback_data='p'+ str(self.id)) - markup.row(btn_buy) - return markup - - def swap_desc(self): - markup = telebot.types.InlineKeyboardMarkup() - btn_buy = telebot.types.InlineKeyboardButton(text='Ваша цена', callback_data='p'+ str(self.id)) - markup.row(btn_buy) - return markup - - def delete(self): - self.id = None - self.type = None - self.id = None - self.description = None - self.seller = None - - -def add_item(item, user_id): - db = sqlite.connect("clientbase.db") - cur = db.cursor() - cur.execute("SELECT * FROM items WHERE (name) = (?)", (item.get_name(),)) - print(cur.fetchone()) - if not cur.fetchone(): - try: - cur.execute("INSERT INTO items " - "(type, description, hash, seller_name) " - "VALUES (?,?,?,?)", - (item.type, item.description, user_id, - item.seller)) - db.commit() - except Exception as e: - config.log(Error=e, Text='ADDING_NEW_ITEM_ERROR') - - -# типа хэш но нифига не хэш, а просто id владельца -def find_users_items(user_id): - db = sqlite.connect("clientbase.db") - cur = db.cursor() - cur.execute("SELECT * FROM items WHERE hash = ?", (str(user_id),)) - result = cur.fetchall() - return result diff --git a/tests/test_repository.py b/tests/test_repository.py new file mode 100644 index 0000000..3ec5567 --- /dev/null +++ b/tests/test_repository.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from marketbot.database import Database +from marketbot.repository import CatalogRepository + + +@pytest.fixture() +def repository(tmp_path: Path) -> CatalogRepository: + db = Database(tmp_path.joinpath("test.db").resolve()) + repo = CatalogRepository(db) + repo.initialise_schema() + return repo + + +def test_add_and_list_categories(repository: CatalogRepository) -> None: + repository.add_category("Books", "Fiction and non-fiction titles") + repository.add_category("Courses", "Video courses and tutorials") + + categories = repository.list_categories() + assert {category.name for category in categories} == {"Books", "Courses"} + + +def test_upsert_user(repository: CatalogRepository) -> None: + user, created = repository.upsert_user(telegram_id=1, username="john", first_name="John", last_name="Doe") + assert created is True + assert user.username == "john" + + user, created = repository.upsert_user(telegram_id=1, username="johnny", first_name="John", last_name="Doe") + assert created is False + assert user.username == "johnny" + + +def test_add_product(repository: CatalogRepository) -> None: + category = repository.add_category("Books", "Reading material") + product = repository.add_product( + category_id=category.id, + name="Clean Architecture", + price=9.99, + currency="usd", + description="A pragmatic introduction to clean software design.", + photo_url=None, + ) + + all_products = repository.list_products() + assert all_products[0].id == product.id + assert all_products[0].currency == "USD" + + repository.deactivate_product(product.id) + assert repository.list_products() == () + repository.activate_product(product.id) + assert repository.list_products()[0].id == product.id + + +if __name__ == "__main__": + pytest.main([__file__])