initial
This commit is contained in:
commit
f82f6e821c
27
.deploy/deploy-dev.yaml
Normal file
27
.deploy/deploy-dev.yaml
Normal 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
28
.deploy/deploy-prod.yaml
Normal 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
119
.gitignore
vendored
Normal 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
43
.gitlab-ci.yml
Normal 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
8
Dockerfile
Normal 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
105
bot.py
Normal 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
8
main.py
Normal 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
67
mongo.py
Normal 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
15
requirements.txt
Normal 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
5
settings.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user