Compare commits

...

10 Commits

Author SHA1 Message Date
dd2bcff3d7 fix 2024-10-22 00:19:48 +03:00
942efb8036 fix 2024-05-01 16:21:51 +03:00
8d68450710 fix 2024-05-01 16:19:28 +03:00
e495a7c48a deploy 2024-05-01 16:15:10 +03:00
14d87bed02 add admin 2024-05-01 16:11:11 +03:00
0f16bff9ac remove lock 2024-04-30 11:28:13 +03:00
7dc6df0182 redis 2024-04-29 15:05:05 +03:00
c6f5c44d00 gifs 2024-04-29 14:51:48 +03:00
7250e3438a platform 2024-04-29 14:12:07 +03:00
5b8c334424 add new features 2024-04-29 01:19:48 +03:00
16 changed files with 453 additions and 11 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -10,6 +10,12 @@ services:
TELEGRAM_TOKEN: $TELEGRAM_TOKEN_DEV
MONGO_HOST: "mongo.develop.sprinthub.ru"
MONGO_PASSWORD: $MONGO_PASSWORD_DEV
MINIO_HOST: "minio.develop.sprinthub.ru"
MINIO_SECRET_KEY: $MINIO_SECRET_KEY_DEV
PLATFORM_SECURITY_TOKEN: $PLATFORM_SECURITY_TOKEN
STAGE: "development"
REDIS_HOST: "redis.develop.sprinthub.ru"
REDIS_PASSWORD: $REDIS_PASSWORD_DEV
networks:
- net
deploy:
@ -20,6 +26,30 @@ services:
parallelism: 1
order: start-first
roulette-nginx:
image: mathwave/sprint-repo:roulette-bot
command: api
environment:
TELEGRAM_TOKEN: $TELEGRAM_TOKEN_DEV
MONGO_HOST: "mongo.develop.sprinthub.ru"
MONGO_PASSWORD: $MONGO_PASSWORD_DEV
MINIO_HOST: "minio.develop.sprinthub.ru"
MINIO_SECRET_KEY: $MINIO_SECRET_KEY_DEV
PLATFORM_SECURITY_TOKEN: $PLATFORM_SECURITY_TOKEN
STAGE: "development"
REDIS_HOST: "redis.develop.sprinthub.ru"
REDIS_PASSWORD: $REDIS_PASSWORD_DEV
networks:
- net
- common-infra-nginx
deploy:
mode: replicated
restart_policy:
condition: any
update_config:
parallelism: 1
order: start-first
networks:
net:
driver: overlay

View File

@ -6,6 +6,29 @@ services:
bot:
image: mathwave/sprint-repo:roulette-bot
command: bot
networks:
- net
environment:
TELEGRAM_TOKEN: $TELEGRAM_TOKEN_PROD
MONGO_HOST: "mongo.sprinthub.ru"
MONGO_PASSWORD: $MONGO_PASSWORD_PROD
MINIO_HOST: "minio.sprinthub.ru"
MINIO_SECRET_KEY: $MINIO_SECRET_KEY_PROD
PLATFORM_SECURITY_TOKEN: $PLATFORM_SECURITY_TOKEN
STAGE: "production"
REDIS_HOST: "redis.sprinthub.ru"
REDIS_PASSWORD: $REDIS_PASSWORD_PROD
deploy:
mode: replicated
restart_policy:
condition: any
update_config:
parallelism: 1
order: start-first
roulette-nginx:
image: mathwave/sprint-repo:roulette-bot
command: api
networks:
- net
- common-infra-nginx
@ -13,6 +36,12 @@ services:
TELEGRAM_TOKEN: $TELEGRAM_TOKEN_PROD
MONGO_HOST: "mongo.sprinthub.ru"
MONGO_PASSWORD: $MONGO_PASSWORD_PROD
MINIO_HOST: "minio.sprinthub.ru"
MINIO_SECRET_KEY: $MINIO_SECRET_KEY_PROD
PLATFORM_SECURITY_TOKEN: $PLATFORM_SECURITY_TOKEN
STAGE: "production"
REDIS_HOST: "redis.sprinthub.ru"
REDIS_PASSWORD: $REDIS_PASSWORD_PROD
deploy:
mode: replicated
restart_policy:

View File

@ -0,0 +1,47 @@
name: Deploy Dev
on:
pull_request:
branches:
- dev
types: [closed]
jobs:
build:
name: Build
runs-on: [ dev ]
steps:
- name: login
run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }}
- name: checkout
uses: actions/checkout@v4
with:
ref: dev
- name: build
run: docker build -t mathwave/sprint-repo:roulette-bot .
push:
name: Push
runs-on: [ dev ]
needs: build
steps:
- name: push
run: docker push mathwave/sprint-repo:roulette-bot
deploy-dev:
name: Deploy dev
runs-on: [dev]
needs: push
steps:
- name: login
run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }}
- name: checkout
uses: actions/checkout@v4
with:
ref: dev
- name: deploy
env:
TELEGRAM_TOKEN_DEV: ${{ secrets.TELEGRAM_TOKEN_DEV }}
MONGO_PASSWORD_DEV: ${{ secrets.MONGO_PASSWORD_DEV }}
PLATFORM_SECURITY_TOKEN: ${{ secrets.PLATFORM_SECURITY_TOKEN }}
MINIO_SECRET_KEY_DEV: ${{ secrets.MINIO_SECRET_KEY_DEV }}
REDIS_PASSWORD_DEV: ${{ secrets.REDIS_PASSWORD_DEV }}
run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-dev.yaml roulette-bot

View File

@ -0,0 +1,47 @@
name: Deploy Prod
on:
pull_request:
branches:
- prod
types: [closed]
jobs:
build:
name: Build
runs-on: [ dev ]
steps:
- name: login
run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }}
- name: checkout
uses: actions/checkout@v4
with:
ref: prod
- name: build
run: docker build -t mathwave/sprint-repo:roulette-bot .
push:
name: Push
runs-on: [ dev ]
needs: build
steps:
- name: push
run: docker push mathwave/sprint-repo:roulette-bot
deploy-prod:
name: Deploy prod
runs-on: [prod]
needs: push
steps:
- name: login
run: docker login -u mathwave -p ${{ secrets.DOCKERHUB_PASSWORD }}
- name: checkout
uses: actions/checkout@v4
with:
ref: prod
- name: deploy
env:
TELEGRAM_TOKEN_PROD: ${{ secrets.TELEGRAM_TOKEN_PROD }}
MONGO_PASSWORD_PROD: ${{ secrets.MONGO_PASSWORD_PROD }}
PLATFORM_SECURITY_TOKEN: ${{ secrets.PLATFORM_SECURITY_TOKEN }}
MINIO_SECRET_KEY_PROD: ${{ secrets.MINIO_SECRET_KEY_PROD }}
REDIS_PASSWORD_PROD: ${{ secrets.REDIS_PASSWORD_PROD }}
run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-prod.yaml roulette-bot

24
api.py Normal file
View File

@ -0,0 +1,24 @@
from bson import ObjectId
from flask import Flask, request
from tools.mongo import mongo
app = Flask("roulette")
@app.route('/dialogs')
def main():
html = "<html><head></head><body>"
for d in mongo.dialogs_collection.find({}).sort([('started_at', -1)]):
html += f'<a href="/dialog?id={d["_id"]}">{d["_id"]}</a><br>'
html += "</body></html>"
return html
@app.route('/dialog')
def dialog():
html = "<html><head></head><body>"
for message in mongo.messages_collection.find({"dialog_id": ObjectId(request.args.get('id'))}).sort([('sent_at', 1)]):
html += f'{message["sender"]}: {message["text"]}<br>'
html += "</body></html>"
return html

70
bot.py
View File

@ -1,9 +1,14 @@
import os
import uuid
import requests
import telebot
from telebot.types import Message, ReplyKeyboardRemove
from mongo import mongo
from tools.minio import minio_client as minio
from tools.mongo import mongo
from tools.sprint_platform import platform
from tools.redis import redis_client as redis
bot = telebot.TeleBot(os.getenv("TELEGRAM_TOKEN"))
@ -12,7 +17,7 @@ class Core:
def __init__(self, message: Message):
self.message = message
self.chat_id = message.chat.id
self.message_text = message.text
self.message_text = message.text or message.caption or ""
print(f'Handled message from {self.chat_id}: {self.message_text}')
user = mongo.chats_collection.find_one({"chat_id": message.chat.id})
if user is None:
@ -64,6 +69,8 @@ class Core:
self.send_message('🤖 Поиски собеседника продолжаются')
def send_message(self, text, chat_id=None, reply_markup=None, remove_keyboard=True, **kwargs):
if not text:
return
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)
@ -79,13 +86,44 @@ class Core:
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'])
chat_to_send = current_dialog['chat_id_2'] if current_dialog['chat_id_1'] == self.chat_id else current_dialog['chat_id_1']
saves = platform.get_config('save')
if saves['messages']:
res = mongo.create_message(self.message.content_type, self.message_text, current_dialog['_id'], self.chat_id).inserted_id
else:
self.send_message(self.message_text, current_dialog['chat_id_1'])
res = uuid.uuid4()
if self.message.photo:
if saves['photos']:
photo = requests.get(bot.get_file_url(self.message.photo[-1].file_id)).content
minio.put_object(f"photos/{res}", photo)
bot.send_photo(chat_to_send, self.message.photo[-1].file_id)
if self.message.sticker:
if saves['stickers']:
sticker = requests.get(bot.get_file_url(self.message.sticker.file_id)).content
minio.put_object(f"stickers/{res}", sticker)
bot.send_sticker(chat_to_send, self.message.sticker.file_id)
if self.message.voice:
if saves['voices']:
voice = requests.get(bot.get_file_url(self.message.voice.file_id)).content
minio.put_object(f"voices/{res}", voice)
bot.send_voice(chat_to_send, self.message.voice.file_id)
if self.message.video_note:
if saves['video_notes']:
video_note = requests.get(bot.get_file_url(self.message.video_note.file_id)).content
minio.put_object(f"video_notes/{res}", video_note)
bot.send_video_note(chat_to_send, self.message.video_note.file_id)
if self.message.animation:
if saves['gifs']:
video_note = requests.get(bot.get_file_url(self.message.animation.file_id)).content
minio.put_object(f"gifs/{res}", video_note)
bot.send_animation(chat_to_send, self.message.animation.file_id)
self.send_message(self.message_text, chat_to_send)
def start_new_dialog(self, chat_ids):
for chat in chat_ids:
current_dialog = mongo.get_current_dialog(chat)
if current_dialog:
mongo.finish_dialog(current_dialog['_id'])
self.set_state('search', chat_ids)
for chat in chat_ids:
self.send_message("🤖 Начинаю искать собеседника. Сообщу тебе, когда найду его.", chat)
@ -99,8 +137,24 @@ class Core:
def run_bot():
@bot.message_handler()
@bot.message_handler(content_types=[
# 'audio',
'photo',
'voice',
'video_note',
# 'document',
'text',
'animation',
# 'location',
# 'contact',
'sticker'
]
)
def do_action(message: Message):
Core(message).process()
try:
Core(message).process()
except Exception as e:
print(e)
print('bot is starting')
bot.polling()

14
local_platform.json Normal file
View File

@ -0,0 +1,14 @@
{
"configs": {
"save": {
"photos": false,
"voices": false,
"messages": false,
"stickers": false,
"video_notes": false,
"gifs": false
}
},
"experiments": {},
"platform_staff": {}
}

View File

@ -4,5 +4,8 @@ import sys
if sys.argv[-1] == "bot":
from bot import run_bot
run_bot()
elif sys.argv[-1] == "api":
from api import app
app.run(host="0.0.0.0", port=1238)
else:
raise NotImplementedError

View File

@ -1,15 +1,20 @@
async-timeout==4.0.3
certifi==2022.12.7
charset-normalizer==3.0.1
click==8.1.3
dnspython==2.3.0
Flask==2.2.3
idna==3.4
importlib-metadata==6.7.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
minio==7.1.13
pymongo==4.3.3
pyTelegramBotAPI==4.1.1
redis==5.0.4
requests==2.28.2
typing_extensions==4.7.1
urllib3==1.26.14
Werkzeug==2.2.3
zipp==3.15.0

View File

@ -3,3 +3,11 @@ import os
MONGO_USER = os.getenv("MONGO_USER", "mongo")
MONGO_PASSWORD = os.getenv("MONGO_PASSWORD", "password")
MONGO_HOST = os.getenv("MONGO_HOST", "localhost")
MINIO_HOST = os.getenv("MINIO_HOST", "localhost") + ":9000"
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "serviceminioadmin")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin")
MINIO_BUCKET_NAME = 'ruletka'
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None)

0
tools/__init__.py Normal file
View File

45
tools/minio.py Normal file
View File

@ -0,0 +1,45 @@
import io
from minio import Minio
from minio.error import MinioException
import settings
class Client:
def __init__(self, host: str, access_key: str, secret_key: str, bucket_name: str):
self.bucket_name = bucket_name
self.cli = Minio(
host,
access_key=access_key,
secret_key=secret_key,
secure=False
)
try:
self.cli.make_bucket(bucket_name)
except MinioException:
pass
def put_object(self, name: str, data: bytes):
self.cli.put_object(self.bucket_name, name, io.BytesIO(data), len(data))
def get_object(self, name: str) -> bytes:
try:
return self.cli.get_object(self.bucket_name, name).data
except MinioException:
return b""
def delete_object(self, name: str):
try:
self.cli.remove_object(self.bucket_name, name)
except MinioException:
pass
minio_client = Client(
settings.MINIO_HOST,
settings.MINIO_ACCESS_KEY,
settings.MINIO_SECRET_KEY,
settings.MINIO_BUCKET_NAME
)

View File

@ -16,9 +16,16 @@ class Mongo:
('state', 1)
])
self.dialogs_collection.create_index([
("chat_id", 1),
("chat_id_1", 1),
('finished_at', 1)
])
self.dialogs_collection.create_index([
("chat_id_2", 1),
('finished_at', 1)
])
self.messages_collection.create_index([
('dialog_id', 1)
])
def __getitem__(self, item):
return self.database.get_collection(item)
@ -44,8 +51,9 @@ class Mongo:
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({
def create_message(self, message_type, text, dialog_id, sender):
return self.messages_collection.insert_one({
'message_type': message_type,
'dialog_id': dialog_id,
'text': text,
'sender': sender,

30
tools/redis.py Normal file
View File

@ -0,0 +1,30 @@
import contextlib
import redis
import settings
class RedisClient:
def __init__(self, host, password=None):
kwargs = {
"host": host,
}
if password:
kwargs['password'] = password
self.cli = redis.Redis(**kwargs)
def get(self, key):
with self.cli as cli:
return cli.get(f"ruletka_{key}")
def set(self, key, value):
with self.cli as cli:
cli.set(f"ruletka_{key}", value)
redis_client = RedisClient(
settings.REDIS_HOST,
settings.REDIS_PASSWORD
)

98
tools/sprint_platform.py Normal file
View File

@ -0,0 +1,98 @@
import json
import os
import typing
import urllib.parse
from threading import Thread
from time import sleep
from requests import get
class PlatformClient:
def __init__(self, platform_security_token: str, app_name: str, stage: str, need_poll: bool = True):
self.platform_security_token = platform_security_token
self.app_name = app_name
self.stage = stage
self.endpoint = 'https://platform.sprinthub.ru/'
self.configs_url = urllib.parse.urljoin(self.endpoint, 'configs/get')
self.experiments_url = urllib.parse.urljoin(self.endpoint, 'experiments/get')
self.staff_url = urllib.parse.urljoin(self.endpoint, 'is_staff')
self.fetch_url = urllib.parse.urljoin(self.endpoint, 'fetch')
self.config_storage = {}
self.experiment_storage = {}
self.staff_storage = {}
self.poll_data()
if need_poll:
self.poll_data_in_thread()
def poll_data_in_thread(self):
def inner():
while True:
sleep(30)
self.fetch()
Thread(target=inner, daemon=True).start()
def poll_data(self):
self.fetch(with_exception=True)
def request_with_retries(self, url, params, with_exception=False, retries_count=3):
exception_to_throw = None
for _ in range(retries_count):
try:
response = get(
url,
headers={'X-Security-Token': self.platform_security_token},
params=params
)
if response.status_code == 200:
return response.json()
print(f'Failed to request {url}, status_code={response.status_code}')
exception_to_throw = Exception('Not 200 status')
except Exception as exc:
print(exc)
exception_to_throw = exc
sleep(1)
print(f'Failed fetching with retries: {url}, {params}')
if with_exception:
raise exception_to_throw
def fetch(self, with_exception=False):
if self.stage == 'local':
local_platform = json.loads(open('local_platform.json', 'r').read())
self.config_storage = local_platform['configs']
self.experiment_storage = local_platform['experiments']
self.staff_storage = {
key: set(value)
for key, value in local_platform['platform_staff'].items()
}
return
response_data = self.request_with_retries(self.fetch_url, {
'project': self.app_name,
'stage': self.stage,
}, with_exception)
self.config_storage = response_data['configs']
self.experiment_storage = response_data['experiments']
self.staff_storage = {
key: set(value)
for key, value in response_data['platform_staff'].items()
}
def is_staff(self, **kwargs):
for key, value in kwargs.items():
if value in self.staff_storage[key]:
return True
return False
def get_config(self, name):
return self.config_storage[name]
def get_experiment(self, name):
return self.experiment_storage[name]
platform = PlatformClient(
os.getenv('PLATFORM_SECURITY_TOKEN'),
'Ruletka',
os.getenv('STAGE', 'local')
)