Compare commits
33 Commits
1415c0209e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| e970eecb24 | |||
| 3a19bdfdd5 | |||
| 4b261543ed | |||
| 243b9e8c9c | |||
| 9990e18982 | |||
| 120b23a885 | |||
| 1cc4e9ee2d | |||
| c51d151bdc | |||
| a042a033a6 | |||
| 2d7a04bc7f | |||
| c759cacf9c | |||
| e131826519 | |||
| 8161954db9 | |||
| f49413b7d7 | |||
| 3241b25d25 | |||
| ef3cee477c | |||
| 696f1cb2ed | |||
| ebeef5c15f | |||
| 93af06d985 | |||
| e55898663d | |||
| fb33edec7d | |||
| f74b605572 | |||
| 76201e6847 | |||
| dd2bcff3d7 | |||
| 942efb8036 | |||
| 8d68450710 | |||
| e495a7c48a | |||
| 14d87bed02 | |||
| 0f16bff9ac | |||
| 7dc6df0182 | |||
| c6f5c44d00 | |||
| 7250e3438a | |||
| 5b8c334424 |
@@ -5,13 +5,12 @@ services:
|
|||||||
|
|
||||||
bot:
|
bot:
|
||||||
image: mathwave/sprint-repo:roulette-bot
|
image: mathwave/sprint-repo:roulette-bot
|
||||||
command: bot
|
|
||||||
environment:
|
environment:
|
||||||
TELEGRAM_TOKEN: $TELEGRAM_TOKEN_DEV
|
|
||||||
MONGO_HOST: "mongo.develop.sprinthub.ru"
|
MONGO_HOST: "mongo.develop.sprinthub.ru"
|
||||||
MONGO_PASSWORD: $MONGO_PASSWORD_DEV
|
MONGO_PASSWORD: $MONGO_PASSWORD_DEV
|
||||||
|
STAGE: "development"
|
||||||
networks:
|
networks:
|
||||||
- net
|
- queues-development
|
||||||
deploy:
|
deploy:
|
||||||
mode: replicated
|
mode: replicated
|
||||||
restart_policy:
|
restart_policy:
|
||||||
@@ -21,7 +20,5 @@ services:
|
|||||||
order: start-first
|
order: start-first
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
net:
|
queues-development:
|
||||||
driver: overlay
|
|
||||||
common-infra-nginx:
|
|
||||||
external: true
|
external: true
|
||||||
@@ -5,14 +5,12 @@ services:
|
|||||||
|
|
||||||
bot:
|
bot:
|
||||||
image: mathwave/sprint-repo:roulette-bot
|
image: mathwave/sprint-repo:roulette-bot
|
||||||
command: bot
|
|
||||||
networks:
|
|
||||||
- net
|
|
||||||
- common-infra-nginx
|
|
||||||
environment:
|
environment:
|
||||||
TELEGRAM_TOKEN: $TELEGRAM_TOKEN_PROD
|
|
||||||
MONGO_HOST: "mongo.sprinthub.ru"
|
MONGO_HOST: "mongo.sprinthub.ru"
|
||||||
MONGO_PASSWORD: $MONGO_PASSWORD_PROD
|
MONGO_PASSWORD: $MONGO_PASSWORD_PROD
|
||||||
|
STAGE: "production"
|
||||||
|
networks:
|
||||||
|
- queues
|
||||||
deploy:
|
deploy:
|
||||||
mode: replicated
|
mode: replicated
|
||||||
restart_policy:
|
restart_policy:
|
||||||
@@ -22,7 +20,5 @@ services:
|
|||||||
order: start-first
|
order: start-first
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
net:
|
queues:
|
||||||
driver: overlay
|
|
||||||
common-infra-nginx:
|
|
||||||
external: true
|
external: true
|
||||||
43
.gitea/workflows/deploy-dev.yaml
Normal file
43
.gitea/workflows/deploy-dev.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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: [prod]
|
||||||
|
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:
|
||||||
|
MONGO_PASSWORD_DEV: ${{ secrets.MONGO_PASSWORD_DEV }}
|
||||||
|
run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-dev.yaml roulette-bot-development
|
||||||
43
.gitea/workflows/deploy-prod.yaml
Normal file
43
.gitea/workflows/deploy-prod.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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:
|
||||||
|
MONGO_PASSWORD_PROD: ${{ secrets.MONGO_PASSWORD_PROD }}
|
||||||
|
run: docker stack deploy --with-registry-auth -c ./.deploy/deploy-prod.yaml roulette-bot
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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 == "main"'
|
|
||||||
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:
|
|
||||||
- main
|
|
||||||
when: manual
|
|
||||||
script:
|
|
||||||
- docker stack deploy --with-registry-auth -c ./.deploy/deploy-prod.yaml roulette-bot
|
|
||||||
@@ -5,4 +5,4 @@ COPY requirements.txt requirements.txt
|
|||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENTRYPOINT ["python", "main.py"]
|
ENTRYPOINT ["python", "bot.py"]
|
||||||
87
bot.py
87
bot.py
@@ -1,18 +1,20 @@
|
|||||||
import os
|
import json
|
||||||
|
|
||||||
import telebot
|
|
||||||
from telebot.types import Message, ReplyKeyboardRemove
|
from telebot.types import Message, ReplyKeyboardRemove
|
||||||
|
|
||||||
from mongo import mongo
|
from tools.mongo import mongo
|
||||||
|
from tools.queues import TasksHandlerMixin, set_task
|
||||||
bot = telebot.TeleBot(os.getenv("TELEGRAM_TOKEN"))
|
|
||||||
|
|
||||||
|
|
||||||
class Core:
|
class Core(TasksHandlerMixin):
|
||||||
def __init__(self, message: Message):
|
@property
|
||||||
|
def queue_name(self):
|
||||||
|
return 'roulette_bot_worker'
|
||||||
|
|
||||||
|
def process(self, payload):
|
||||||
|
message: Message = Message.de_json(json.dumps(payload))
|
||||||
self.message = message
|
self.message = message
|
||||||
self.chat_id = message.chat.id
|
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}')
|
print(f'Handled message from {self.chat_id}: {self.message_text}')
|
||||||
user = mongo.chats_collection.find_one({"chat_id": message.chat.id})
|
user = mongo.chats_collection.find_one({"chat_id": message.chat.id})
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -25,8 +27,6 @@ class Core:
|
|||||||
doc = user
|
doc = user
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
self.state = doc['state']
|
self.state = doc['state']
|
||||||
|
|
||||||
def process(self):
|
|
||||||
if self.message_text.startswith('/'):
|
if self.message_text.startswith('/'):
|
||||||
self.exec_command()
|
self.exec_command()
|
||||||
return
|
return
|
||||||
@@ -63,10 +63,42 @@ class Core:
|
|||||||
def handle_state_search(self):
|
def handle_state_search(self):
|
||||||
self.send_message('🤖 Поиски собеседника продолжаются')
|
self.send_message('🤖 Поиски собеседника продолжаются')
|
||||||
|
|
||||||
def send_message(self, text, chat_id=None, reply_markup=None, remove_keyboard=True, **kwargs):
|
def send_message(self, text, chat_id=None, reply_markup=None, remove_keyboard=True):
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
if reply_markup is None and remove_keyboard:
|
if reply_markup is None and remove_keyboard:
|
||||||
reply_markup = ReplyKeyboardRemove()
|
reply_markup = ReplyKeyboardRemove()
|
||||||
bot.send_message(chat_id or self.chat_id, text, reply_markup=reply_markup, **kwargs)
|
body = {
|
||||||
|
'chat_id': chat_id or self.chat_id,
|
||||||
|
'text': text,
|
||||||
|
}
|
||||||
|
if reply_markup:
|
||||||
|
body['reply_markup'] = reply_markup.to_json()
|
||||||
|
set_task(
|
||||||
|
'botalka_mailbox',
|
||||||
|
{
|
||||||
|
'project': 'roulette-bot',
|
||||||
|
'name': 'telegram-bot',
|
||||||
|
'method': 'send_message',
|
||||||
|
'body': body,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(self, chat_id, method, **kwargs):
|
||||||
|
set_task(
|
||||||
|
'botalka_mailbox',
|
||||||
|
{
|
||||||
|
'project': 'roulette-bot',
|
||||||
|
'name': 'telegram-bot',
|
||||||
|
'method': method,
|
||||||
|
'body': {
|
||||||
|
'chat_id': chat_id,
|
||||||
|
**kwargs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
def set_state(self, state, chat_ids=None):
|
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}})
|
mongo.chats_collection.update_many({"chat_id": {"$in": chat_ids or [self.chat_id]}}, {"$set": {"state": state}})
|
||||||
@@ -79,13 +111,24 @@ class Core:
|
|||||||
|
|
||||||
def handle_state_dialog(self):
|
def handle_state_dialog(self):
|
||||||
current_dialog = mongo.get_current_dialog(self.chat_id)
|
current_dialog = mongo.get_current_dialog(self.chat_id)
|
||||||
mongo.create_message(self.message_text, current_dialog['_id'], self.chat_id)
|
chat_to_send = current_dialog['chat_id_2'] if current_dialog['chat_id_1'] == self.chat_id else current_dialog['chat_id_1']
|
||||||
if current_dialog['chat_id_1'] == self.chat_id:
|
if self.message.photo:
|
||||||
self.send_message(self.message_text, current_dialog['chat_id_2'])
|
self.send(chat_to_send, 'send_photo', photo=self.message.photo[-1].file_id)
|
||||||
else:
|
if self.message.sticker:
|
||||||
self.send_message(self.message_text, current_dialog['chat_id_1'])
|
self.send(chat_to_send, 'send_data', data=self.message.sticker.file_id, data_type='sticker')
|
||||||
|
if self.message.voice:
|
||||||
|
self.send(chat_to_send, 'send_voice', voice=self.message.voice.file_id)
|
||||||
|
if self.message.video_note:
|
||||||
|
self.send(chat_to_send, 'send_video_note', data=self.message.video_note.file_id)
|
||||||
|
if self.message.animation:
|
||||||
|
self.send(chat_to_send, 'send_animation', data=self.message.animation.file_id)
|
||||||
|
self.send_message(self.message_text, chat_to_send)
|
||||||
|
|
||||||
def start_new_dialog(self, chat_ids):
|
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)
|
self.set_state('search', chat_ids)
|
||||||
for chat in chat_ids:
|
for chat in chat_ids:
|
||||||
self.send_message("🤖 Начинаю искать собеседника. Сообщу тебе, когда найду его.", chat)
|
self.send_message("🤖 Начинаю искать собеседника. Сообщу тебе, когда найду его.", chat)
|
||||||
@@ -98,9 +141,5 @@ class Core:
|
|||||||
self.set_state('dialog', [chat, next_chat['chat_id']])
|
self.set_state('dialog', [chat, next_chat['chat_id']])
|
||||||
|
|
||||||
|
|
||||||
def run_bot():
|
if __name__ == '__main__':
|
||||||
@bot.message_handler()
|
Core().poll()
|
||||||
def do_action(message: Message):
|
|
||||||
Core(message).process()
|
|
||||||
|
|
||||||
bot.polling()
|
|
||||||
|
|||||||
8
main.py
8
main.py
@@ -1,8 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
if sys.argv[-1] == "bot":
|
|
||||||
from bot import run_bot
|
|
||||||
run_bot()
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
|
async-timeout==4.0.3
|
||||||
certifi==2022.12.7
|
certifi==2022.12.7
|
||||||
charset-normalizer==3.0.1
|
charset-normalizer==3.0.1
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
dnspython==2.3.0
|
dnspython==2.3.0
|
||||||
Flask==2.2.3
|
Flask==2.2.3
|
||||||
idna==3.4
|
idna==3.4
|
||||||
|
importlib-metadata==6.7.0
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
MarkupSafe==2.1.2
|
MarkupSafe==2.1.2
|
||||||
minio==7.1.13
|
minio==7.1.13
|
||||||
pymongo==4.3.3
|
pymongo==4.3.3
|
||||||
pyTelegramBotAPI==4.1.1
|
pyTelegramBotAPI==4.1.1
|
||||||
|
redis==5.0.4
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
|
typing_extensions==4.7.1
|
||||||
urllib3==1.26.14
|
urllib3==1.26.14
|
||||||
Werkzeug==2.2.3
|
Werkzeug==2.2.3
|
||||||
|
zipp==3.15.0
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ import os
|
|||||||
MONGO_USER = os.getenv("MONGO_USER", "mongo")
|
MONGO_USER = os.getenv("MONGO_USER", "mongo")
|
||||||
MONGO_PASSWORD = os.getenv("MONGO_PASSWORD", "password")
|
MONGO_PASSWORD = os.getenv("MONGO_PASSWORD", "password")
|
||||||
MONGO_HOST = os.getenv("MONGO_HOST", "localhost")
|
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
0
tools/__init__.py
Normal file
@@ -16,9 +16,16 @@ class Mongo:
|
|||||||
('state', 1)
|
('state', 1)
|
||||||
])
|
])
|
||||||
self.dialogs_collection.create_index([
|
self.dialogs_collection.create_index([
|
||||||
("chat_id", 1),
|
("chat_id_1", 1),
|
||||||
('finished_at', 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):
|
def __getitem__(self, item):
|
||||||
return self.database.get_collection(item)
|
return self.database.get_collection(item)
|
||||||
@@ -44,8 +51,9 @@ class Mongo:
|
|||||||
def get_current_dialog(self, chat_id):
|
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})
|
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):
|
def create_message(self, message_type, text, dialog_id, sender):
|
||||||
self.messages_collection.insert_one({
|
return self.messages_collection.insert_one({
|
||||||
|
'message_type': message_type,
|
||||||
'dialog_id': dialog_id,
|
'dialog_id': dialog_id,
|
||||||
'text': text,
|
'text': text,
|
||||||
'sender': sender,
|
'sender': sender,
|
||||||
52
tools/queues.py
Normal file
52
tools/queues.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
stage = os.getenv("STAGE", 'local')
|
||||||
|
if stage == 'local':
|
||||||
|
QUEUES_URL = 'http://localhost:1239'
|
||||||
|
else:
|
||||||
|
QUEUES_URL = 'http://queues:1239'
|
||||||
|
|
||||||
|
|
||||||
|
class QueuesException(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class TasksHandlerMixin:
|
||||||
|
def poll(self):
|
||||||
|
while True:
|
||||||
|
response = requests.get(f'{QUEUES_URL}/api/v1/take', headers={'queue': self.queue_name}).json()
|
||||||
|
task = response.get('task')
|
||||||
|
if not task:
|
||||||
|
time.sleep(0.2)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
self.process(task['payload'])
|
||||||
|
except Exception as exc:
|
||||||
|
print(f'Error processing message id={task["id"]}, payload={task["payload"]}, exc={exc}')
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
resp = requests.post(f'{QUEUES_URL}/api/v1/finish', json={'id': task['id']})
|
||||||
|
if resp.status_code != 202:
|
||||||
|
raise QueuesException
|
||||||
|
except:
|
||||||
|
print(f'Failed to finish task id={task["id"]}')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def queue_name(self):
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
def process(self, payload):
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
def set_task(queue_name: str, payload: dict, seconds_to_execute: int, delay: int|None = None):
|
||||||
|
resp = requests.post(f'{QUEUES_URL}/api/v1/put', headers={'queue': queue_name}, json={
|
||||||
|
'payload': payload,
|
||||||
|
'seconds_to_execute': seconds_to_execute,
|
||||||
|
'delay': delay,
|
||||||
|
})
|
||||||
|
if resp.status_code != 202:
|
||||||
|
raise QueuesException
|
||||||
Reference in New Issue
Block a user