From f82f6e821c2f29a56a47e0292a240831fb4a36b1 Mon Sep 17 00:00:00 2001 From: emmatveev Date: Mon, 22 Apr 2024 23:21:19 +0300 Subject: [PATCH] initial --- .deploy/deploy-dev.yaml | 27 +++++++++ .deploy/deploy-prod.yaml | 28 +++++++++ .gitignore | 119 +++++++++++++++++++++++++++++++++++++++ .gitlab-ci.yml | 43 ++++++++++++++ Dockerfile | 8 +++ bot.py | 105 ++++++++++++++++++++++++++++++++++ main.py | 8 +++ mongo.py | 67 ++++++++++++++++++++++ requirements.txt | 15 +++++ settings.py | 5 ++ 10 files changed, 425 insertions(+) create mode 100644 .deploy/deploy-dev.yaml create mode 100644 .deploy/deploy-prod.yaml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100644 bot.py create mode 100644 main.py create mode 100644 mongo.py create mode 100644 requirements.txt create mode 100644 settings.py diff --git a/.deploy/deploy-dev.yaml b/.deploy/deploy-dev.yaml new file mode 100644 index 0000000..94c6e29 --- /dev/null +++ b/.deploy/deploy-dev.yaml @@ -0,0 +1,27 @@ +version: "3.4" + + +services: + + bot: + image: mathwave/sprint-repo:roulette-bot + command: bot + environment: + TELEGRAM_TOKEN: $TELEGRAM_TOKEN_DEV + MONGO_HOST: "mongo.develop.sprinthub.ru" + MONGO_PASSWORD: $MONGO_PASSWORD_DEV + networks: + - net + deploy: + mode: replicated + restart_policy: + condition: any + update_config: + parallelism: 1 + order: start-first + +networks: + net: + driver: overlay + common-infra-nginx: + external: true \ No newline at end of file diff --git a/.deploy/deploy-prod.yaml b/.deploy/deploy-prod.yaml new file mode 100644 index 0000000..66924a5 --- /dev/null +++ b/.deploy/deploy-prod.yaml @@ -0,0 +1,28 @@ +version: "3.4" + + +services: + + bot: + image: mathwave/sprint-repo:roulette-bot + command: bot + networks: + - net + - common-infra-nginx + environment: + TELEGRAM_TOKEN: $TELEGRAM_TOKEN_PROD + MONGO_HOST: "mongo.sprinthub.ru" + MONGO_PASSWORD: $MONGO_PASSWORD_PROD + deploy: + mode: replicated + restart_policy: + condition: any + update_config: + parallelism: 1 + order: start-first + +networks: + net: + driver: overlay + common-infra-nginx: + external: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92785dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# Django # +*.log +*.pot +*.pyc +__pycache__ +db.sqlite3 +media +data +*/__pycache__ + +# Backup files # +*.bak + +# If you are using PyCharm # +.idea +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/gradle.xml +.idea/**/libraries +*.iws /out/ + +# Python # +*.py[cod] +*$py.class + +# Distribution / packaging +.Python build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +postgres-data + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +ENV/ +venv/ +env.bak/ +venv.bak/ + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Sublime Text # +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project + +# sftp configuration file +sftp-config.json + +# Package control specific files Package +Control.last-run +Control.ca-list +Control.ca-bundle +Control.system-ca-bundle +GitHub.sublime-settings + +# Visual Studio Code # +.vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..b979777 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,43 @@ +stages: + - build + - deploy-dev + - deploy-prod + +build: + stage: build + tags: + - dev + before_script: + - docker login -u mathwave -p $DOCKERHUB_PASSWORD + script: + - docker build -t mathwave/sprint-repo:roulette-bot . + - docker push mathwave/sprint-repo:roulette-bot + +.deploy: + before_script: + - docker login -u mathwave -p $DOCKERHUB_PASSWORD + +deploy-dev: + extends: + - .deploy + stage: deploy-dev + tags: + - dev + rules: + - if: '$CI_COMMIT_BRANCH == "master"' + when: on_success + - when: manual + script: + - docker stack deploy --with-registry-auth -c ./.deploy/deploy-dev.yaml roulette-bot + +deploy-prod: + extends: + - .deploy + stage: deploy-prod + tags: + - prod + only: + - master + when: manual + script: + - docker stack deploy --with-registry-auth -c ./.deploy/deploy-prod.yaml roulette-bot diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3c5568e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.10 +RUN mkdir /usr/src/app +WORKDIR /usr/src/app +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt +COPY . . +ENV PYTHONUNBUFFERED 1 +ENTRYPOINT ["python", "main.py"] \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..8af217e --- /dev/null +++ b/bot.py @@ -0,0 +1,105 @@ +import os + +import telebot +from telebot.types import Message, ReplyKeyboardRemove + +from mongo import mongo + +bot = telebot.TeleBot(os.getenv("TELEGRAM_TOKEN")) + + +class Core: + def __init__(self, message: Message): + self.message = message + self.chat_id = message.chat.id + self.message_text = message.text + user = mongo.chats_collection.find_one({"chat_id": message.chat.id}) + if user is None: + doc = { + "state": "new", + "chat_id": message.chat.id, + } + mongo.chats_collection.insert_one(doc) + else: + doc = user + self.doc = doc + self.state = doc['state'] + + def process(self): + if self.message_text.startswith('/'): + self.exec_command() + return + getattr(self, "handle_state_" + self.state, self.handle_default)() + + def exec_command(self): + if self.message_text == '/pause': + self.set_state('pause') + if self.state == 'dialog': + current_dialog = mongo.get_current_dialog(self.chat_id) + mongo.finish_dialog(current_dialog['_id']) + if self.chat_id == current_dialog['chat_id_1']: + another_chat_id = current_dialog['chat_id_2'] + else: + another_chat_id = current_dialog['chat_id_1'] + self.send_message('πŸ€– Π”ΠΈΠ°Π»ΠΎΠ³ ΠΎΠΊΠΎΠ½Ρ‡Π΅Π½, ΠΆΠ΄Ρƒ тСбя снова!') + self.start_new_dialog([another_chat_id]) + return + if self.state == 'pause': + self.send_message('πŸ€– БСйчас Ρ‚Π²ΠΎΠΉ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ Π½Π΅ Π°ΠΊΡ‚ΠΈΠ²Π΅Π½. Активируй Π΅Π³ΠΎ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ /start') + return + if self.state == 'search': + self.send_message('πŸ€– Поиск собСсСдника ΠΎΠΊΠΎΠ½Ρ‡Π΅Π½, ΠΆΠ΄Ρƒ тСбя снова!') + return + if self.message_text == '/next' or self.message_text == '/start': + if self.state == 'dialog': + dialog = mongo.get_current_dialog(self.chat_id) + self.start_new_dialog([dialog['chat_id_1'], dialog['chat_id_2']]) + return + else: + self.start_new_dialog([self.chat_id]) + return + + def handle_state_search(self): + self.send_message('πŸ€– Поиски собСсСдника ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°ΡŽΡ‚ΡΡ') + + def send_message(self, text, chat_id=None, reply_markup=None, remove_keyboard=True, **kwargs): + if reply_markup is None and remove_keyboard: + reply_markup = ReplyKeyboardRemove() + bot.send_message(chat_id or self.chat_id, text, reply_markup=reply_markup, **kwargs) + + def set_state(self, state, chat_ids=None): + mongo.chats_collection.update_many({"chat_id": {"$in": chat_ids or [self.chat_id]}}, {"$set": {"state": state}}) + + def handle_default(self): + raise NotImplementedError(f"handler for {self.state} is not implemented") + + def handle_state_new(self): + self.start_new_dialog([self.chat_id]) + + def handle_state_dialog(self): + current_dialog = mongo.get_current_dialog(self.chat_id) + mongo.create_message(self.message_text, current_dialog['_id'], self.chat_id) + if current_dialog['chat_id_1'] == self.chat_id: + self.send_message(self.message_text, current_dialog['chat_id_2']) + else: + self.send_message(self.message_text, current_dialog['chat_id_1']) + + def start_new_dialog(self, chat_ids): + self.set_state('search', chat_ids) + for chat in chat_ids: + self.send_message("πŸ€– ΠΠ°Ρ‡ΠΈΠ½Π°ΡŽ ΠΈΡΠΊΠ°Ρ‚ΡŒ собСсСдника. Π‘ΠΎΠΎΠ±Ρ‰Ρƒ Ρ‚Π΅Π±Π΅, ΠΊΠΎΠ³Π΄Π° Π½Π°ΠΉΠ΄Ρƒ Π΅Π³ΠΎ.", chat) + next_chat = mongo.find_searching(chat_ids) + if not next_chat: + continue + self.send_message('πŸ€– БобСсСдник Π½Π°ΠΉΠ΄Π΅Π½! МоТСшь Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒ ΠΎΠ±Ρ‰Π°Ρ‚ΡŒΡΡ', chat) + self.send_message('πŸ€– БобСсСдник Π½Π°ΠΉΠ΄Π΅Π½! МоТСшь Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒ ΠΎΠ±Ρ‰Π°Ρ‚ΡŒΡΡ', next_chat['chat_id']) + mongo.create_dialog(chat, next_chat['chat_id']) + self.set_state('dialog', [chat, next_chat['chat_id']]) + + +def run_bot(): + @bot.message_handler() + def do_action(message: Message): + Core(message).process() + + bot.polling() diff --git a/main.py b/main.py new file mode 100644 index 0000000..7ec5195 --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +import sys + + +if sys.argv[-1] == "bot": + from bot import run_bot + run_bot() +else: + raise NotImplementedError diff --git a/mongo.py b/mongo.py new file mode 100644 index 0000000..c129a4d --- /dev/null +++ b/mongo.py @@ -0,0 +1,67 @@ +import datetime +import random + +import pymongo + +import settings + + +class Mongo: + def __init__(self): + url = f"mongodb://{settings.MONGO_USER}:{settings.MONGO_PASSWORD}@{settings.MONGO_HOST}:27017/" + self.client = pymongo.MongoClient(url) + self.database = self.client.get_database("roulette-bot") + self.chats_collection.create_index([ + ("chat_id", 1), + ('state', 1) + ]) + self.dialogs_collection.create_index([ + ("chat_id", 1), + ('finished_at', 1) + ]) + + def __getitem__(self, item): + return self.database.get_collection(item) + + @property + def chats_collection(self): + return self["chats"] + + @property + def dialogs_collection(self): + return self["dialogs"] + + @property + def messages_collection(self): + return self["messages"] + + def find_searching(self, except_ids): + chats = list(self.chats_collection.find({"state": 'search', 'chat_id': {'$nin': except_ids}})) + if not chats: + return None + return random.choice(chats) + + def get_current_dialog(self, chat_id): + return self.dialogs_collection.find_one({'$or': [{'chat_id_1': chat_id}, {'chat_id_2': chat_id}], 'finished_at': None}) + + def create_message(self, text, dialog_id, sender): + self.messages_collection.insert_one({ + 'dialog_id': dialog_id, + 'text': text, + 'sender': sender, + 'sent_at': datetime.datetime.now(), + }) + + def create_dialog(self, chat_id_1, chat_id_2): + self.dialogs_collection.insert_one({ + 'chat_id_1': chat_id_1, + 'chat_id_2': chat_id_2, + 'started_at': datetime.datetime.now(), + 'finished_at': None, + }) + + def finish_dialog(self, dialog_id): + self.dialogs_collection.update_one({'_id': dialog_id}, {'$set': {'finished_at': datetime.datetime.now()}}) + + +mongo = Mongo() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1850856 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +certifi==2022.12.7 +charset-normalizer==3.0.1 +click==8.1.3 +dnspython==2.3.0 +Flask==2.2.3 +idna==3.4 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.2 +minio==7.1.13 +pymongo==4.3.3 +pyTelegramBotAPI==4.1.1 +requests==2.28.2 +urllib3==1.26.14 +Werkzeug==2.2.3 diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..6eef607 --- /dev/null +++ b/settings.py @@ -0,0 +1,5 @@ +import os + +MONGO_USER = os.getenv("MONGO_USER", "mongo") +MONGO_PASSWORD = os.getenv("MONGO_PASSWORD", "password") +MONGO_HOST = os.getenv("MONGO_HOST", "localhost")