This commit is contained in:
emmatveev 2024-04-22 23:21:19 +03:00
commit f82f6e821c
10 changed files with 425 additions and 0 deletions

27
.deploy/deploy-dev.yaml Normal file
View File

@ -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

28
.deploy/deploy-prod.yaml Normal file
View File

@ -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

119
.gitignore vendored Normal file
View File

@ -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

43
.gitlab-ci.yml Normal file
View File

@ -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

8
Dockerfile Normal file
View File

@ -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"]

105
bot.py Normal file
View File

@ -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()

8
main.py Normal file
View File

@ -0,0 +1,8 @@
import sys
if sys.argv[-1] == "bot":
from bot import run_bot
run_bot()
else:
raise NotImplementedError

67
mongo.py Normal file
View File

@ -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()

15
requirements.txt Normal file
View File

@ -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

5
settings.py Normal file
View File

@ -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")