commit d13d6e7916eaa243233dc6c18c830e1a5081c3bd Author: Beesquit Date: Mon Sep 15 16:25:26 2025 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69169fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8312ec4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +aiofiles==24.1.0 +aiogram==3.22.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anyio==4.10.0 +attrs==25.3.0 +certifi==2025.8.3 +frozenlist==1.7.0 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.10 +loguru==0.7.3 +magic-filter==1.0.12 +multidict==6.6.4 +propcache==0.3.2 +pydantic==2.11.7 +pydantic-settings==2.10.1 +pydantic_core==2.33.2 +python-dotenv==1.1.1 +sniffio==1.3.1 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +yarl==1.20.1 diff --git a/src/internal/internal.py b/src/internal/internal.py new file mode 100644 index 0000000..fc2839c --- /dev/null +++ b/src/internal/internal.py @@ -0,0 +1,8 @@ +import asyncio + +from loguru import logger + + +async def internal(): + logger.info("Internal") + await asyncio.sleep(4) diff --git a/src/jsonData/__init__.py b/src/jsonData/__init__.py new file mode 100644 index 0000000..43405fc --- /dev/null +++ b/src/jsonData/__init__.py @@ -0,0 +1,11 @@ +from connect import load, start +from utils import add, check, removeById, removeByParam + +__all__ = [ + "load", + "start", + "add", + "check", + "removeById", + "removeByParam", +] diff --git a/src/jsonData/connect.py b/src/jsonData/connect.py new file mode 100644 index 0000000..b7ad657 --- /dev/null +++ b/src/jsonData/connect.py @@ -0,0 +1,39 @@ +import json +import os +import shutil +from datetime import datetime + +from loguru import logger + +from ..settings import settings + + +def start(): + # Inits JSON file + filePath = settings.jsonPath + + if not(os.path.exists('data')): + os.mkdir('data') + if not(os.path.exists('data/backup')): + os.mkdir('data/backup') + + if os.path.exists(filePath): + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backupPath = (f'data/backup/data_backup_{timestamp}.json') + shutil.copy(filePath, backupPath) + logger.info(f'JSON backup was created: {backupPath}') + + with open(filePath, 'w') as f: + json.dump({}, f, ensure_ascii = False, indent=4) + logger.info(f'New {filePath} created with empty JSON.') + + +def load(): + # Returnes the contents of the JSON file as a dictionary + data = {} + try: + with open(settings.jsonPath, 'r') as f: + data = json.load(f) + except json.decoder.JSONDecodeError: + logger.warning('JSON file is empty!') + return data diff --git a/src/jsonData/utils.py b/src/jsonData/utils.py new file mode 100644 index 0000000..42a6ff8 --- /dev/null +++ b/src/jsonData/utils.py @@ -0,0 +1,69 @@ +import json + +from connect import load +from loguru import logger + +from ..settings import settings + + +def getParamId(param): + # Returnes -1 if param not found and id if found + currentData = load() + + id = -1 + for (i, j) in [(i[0], i[1]) for i in currentData]: + if j == param: + id = i + break + return id + + +def check(param): + # Returnes 1 if param exists and 0 if not + currentData = load() + + if param in [i[1] for i in currentData]: + return 1 + else: + return 0 + + +def removeById(id: int): + # Returnes 0 if deleted successfully and -1 if not + currentData = load() + + if id in currentData: + del currentData[id] + with open(settings.jsonPath, 'w', encoding = 'utf-8') as f: + json.dump(currentData, f, ensure_ascii = False, indent = 4) + logger.info(f'Id {id} was deleted successfully!') + return 0 + else: + logger.info(f'Id {id} was not found in the data file when trying to delete it.') + return -1 + +def removeByParam(param): + # Returnes 0 if deleted successfully and -1 if not + id = getParamId(param) + + if id != -1: + return removeById(id) + else: + logger.info(f'Param {param} was not found in the data file when trying to delete it.') + return -1 + + +def add(param): + # Saves or updates data in JSON + currentData = load() + if currentData == {}: + id = 0 + else: + id = currentData[-1][0] + 1 + newData = {id: param} + + with open(settings.jsonPath, 'w', encoding = 'utf-8') as f: + currentData.update(newData) + json.dump(currentData, f, ensure_ascii = False, indent = 4) + logger.info(f"Param {param} was added with id {id}") + return id diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..be5bb26 --- /dev/null +++ b/src/main.py @@ -0,0 +1,5 @@ +import asyncio + +from telegram.telegram import start_bot + +asyncio.run(start_bot()) diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 0000000..e3c66ab --- /dev/null +++ b/src/settings.py @@ -0,0 +1,13 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + botToken: str + whitelist: list + + jsonPath: str + + modelConfig = SettingsConfigDict(env_file = ".env", env_file_encoding="utf-8") + + +settings = Settings() # type: ignore diff --git a/src/telegram/strings.py b/src/telegram/strings.py new file mode 100644 index 0000000..a98f8e8 --- /dev/null +++ b/src/telegram/strings.py @@ -0,0 +1,32 @@ +# Bot Statuses + +startBot = "Bot started" + +stopBot = "Bot stopped" + +unexpectedError = "Unexpected error" + + +# Commands + +startCommand = "Hi" + +helpCommand = "This is help" + + +# Route status + +askParam = "Enter param:" + +successAdd = "Added successfully" + +successRemove = "Removed successfully" + +alreadyExists = "Key already exists" + + +# Data status + +noData = "No data" + +foundData = "Founde data:" diff --git a/src/telegram/telegram.py b/src/telegram/telegram.py new file mode 100644 index 0000000..369c6bd --- /dev/null +++ b/src/telegram/telegram.py @@ -0,0 +1,130 @@ +import asyncio + +import strings +from aiogram import Bot, Dispatcher, F, Router +from aiogram.client.default import DefaultBotProperties +from aiogram.enums import ParseMode +from aiogram.filters import Command +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.fsm.storage.memory import MemoryStorage +from aiogram.types import BotCommand, BotCommandScopeDefault, Message +from loguru import logger + +import jsonData +from internal.internal import internal +from settings import settings + + +async def setCommands(): + commands = [ + BotCommand(command='start', description='Start'), + BotCommand(command='help', description='Help'), + BotCommand(command='info', description='Info'), + BotCommand(command='add', description='Add'), + BotCommand(command='remove', description='Remove') + ] + await bot.set_my_commands(commands, BotCommandScopeDefault()) + + +class addForm(StatesGroup): + param = State() + +class removeForm(StatesGroup): + param = State() + + +dp = Dispatcher(storage=MemoryStorage()) +bot = Bot( + token=settings.botToken, + default=DefaultBotProperties(parse_mode=ParseMode.HTML), +) + +strategyRouter = Router() +removeRouter = Router() + + +@dp.message(Command("start")) +async def commandStart(message: Message) -> None: + await message.answer(strings.startCommand) + +@dp.message(Command("help"), F.chat.id.in_(settings.whitelist)) +async def commandHelp(message: Message) -> None: + await message.answer(strings.helpCommand) + +@dp.message(Command("info"), F.chat.id.in_(settings.whitelist)) +async def commandInfo(message: Message) -> None: + data = jsonData.load() + msgText = '' + if data == {}: + msgText = strings.noData + else: + msgText = strings.foundData + for i in data: + msgText += (f"{str(i[0])}: {str(i[1])}\n") + await message.answer(msgText) + + +@strategyRouter.message(Command("add"), F.chat.id.in_(settings.whitelist)) +async def commandAdd(message: Message, state: FSMContext): + await message.answer(strings.askParam) + await state.set_state(addForm.param) + +@strategyRouter.message(F.text, addForm.param) +async def captureStartPair(message: Message, state: FSMContext): + await state.update_data(pair=message.text) + data = await state.get_data() + param = data.get("param") + + t = jsonData.add(param) + msgText = strings.successAdd + str(t) + await asyncio.to_thread(internal) + + await message.answer(msgText) + await state.clear() + + +@removeRouter.message(Command("remove"), F.chat.id.in_(settings.whitelist)) +async def commandRemove(message: Message, state: FSMContext): + await message.answer(strings.askParam) + await state.set_state(removeForm.param) + +@removeRouter.message(F.text, removeForm.param) +async def captureRemoveParam(message: Message, state: FSMContext): + await state.update_data(pair=message.text) + data = await state.get_data() + param = data.get("param") + + t = jsonData.removeByParam(param) + if t == 0: + msgText = strings.successRemove + else: + msgText = strings.noData + + await message.answer(msgText) + await state.clear() + + +async def startBot(): + await setCommands() + try: + for i in settings.whitelist: + await bot.send_message(chat_id=i, text=strings.startBot) + except Exception as e: + logger.error(e) + +async def stopBot(): + try: + for i in settings.whitelist: + await bot.send_message(chat_id=i, text=strings.stopBot) + except Exception as e: + logger.error(e) + + +async def start_bot() -> None: + logger.info("Started bot!") + dp.startup.register(startBot) + dp.include_router(strategyRouter) + dp.include_router(removeRouter) + dp.shutdown.register(stopBot) + await dp.start_polling(bot)