Compare commits

..

5 Commits

Author SHA1 Message Date
e55e330c50 chore: remove unused placeholder file for settings functions
All checks were successful
Build and Push Docker Images / build (push) Successful in 44s
Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-19 23:54:44 +05:00
8b34d79f4e fix(config): update environment variable retrieval for MAILING_MAX_WORKERS, MAILING_RATE_LIMIT, and MAILING_MAX_RETRIES
Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-19 23:54:04 +05:00
60f77b39eb feat(subscription): add "subscribe all" and "unsubscribe all" buttons
feat(subscription): add check on unsubscribe to notify user if no active subscriptions

Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-19 23:52:48 +05:00
55510a4379 chore(notification mode switch): update icon in final message for notification importance change
Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-19 23:50:43 +05:00
604957f1a7 feat(notification): improve processing, formatting, and sending of user notifications
Signed-off-by: UdoChudo <stream@udochudo.ru>
2025-06-19 23:49:22 +05:00
12 changed files with 267 additions and 99 deletions

View File

@ -1,13 +1,13 @@
from . import subscribe, active_triggers
from . import unsubscribe
from . import my_subscriptions
from . import cancel_input
from . import notification_switch_mode
from . import debug
from . import help
from . import my_subscriptions
from . import notification_switch_mode
from . import registration
from . import settings
from . import start
from . import debug
from . import subscribe, active_triggers
from . import unsubscribe
from ..states import UserStateManager
state_manager = UserStateManager()
@ -32,3 +32,5 @@ def register_callbacks(bot, app):
notification_switch_mode.register_callback_notification(bot, app, state_manager)
active_triggers.register_callbacks_active_triggers(bot, app, state_manager)
cancel_input.register_callback_cancel_input(bot,state_manager)
subscribe.register_callback_subscribe(bot, app, state_manager)
unsubscribe.register_callback_unsubscribe(bot, app, state_manager)

View File

@ -1,8 +1,9 @@
import telebot
from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from app.bot.keyboards.main_menu import get_main_menu
from app import Subscriptions, db
from app.bot.constants import UserStates
from app.bot.keyboards.main_menu import get_main_menu
from app.bot.keyboards.settings_menu import get_settings_menu
from app.bot.utils.auth import auth
from app.bot.utils.tg_audit import log_user_event
@ -60,7 +61,7 @@ def register_callback_notification(bot, app, state_manager):
mode_text_emoji = "⛔️ Критические события" if disaster_only else "⚠️ Все события"
mode_text = "Критические события" if disaster_only else "Все события"
bot.send_message(chat_id, f" Режим уведомлений успешно изменён на:\n {mode_text_emoji}",reply_markup=get_settings_menu())
bot.send_message(chat_id, f"🔄 Режим уведомлений успешно изменён на:\n {mode_text_emoji}",reply_markup=get_settings_menu())
log_user_event(chat_id, app, username, f"Режим уведомлений изменился на: {mode_text}")
state_manager.set_state(chat_id, UserStates.SETTINGS_MENU)

View File

@ -1,13 +1,13 @@
from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from telebot import TeleBot, logger
from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from app import Subscriptions
from app.bot.constants import UserStates
from app.bot.processors.subscribe_processor import process_subscription_button
from app.bot.processors.subscribe_processor import process_subscription_button, process_subscribe_all_regions
from app.bot.states import UserStateManager
from app.bot.utils.auth import auth
from app.bot.utils.regions import get_sorted_regions, format_regions_list, format_regions_list_marked
from telebot import TeleBot, logger
from app.bot.utils.regions import get_sorted_regions, format_regions_list_marked
def register_handlers(bot: TeleBot, app, state_manager: UserStateManager):
@ -36,6 +36,7 @@ def register_handlers(bot: TeleBot, app, state_manager: UserStateManager):
regions_text = format_regions_list_marked(regions, subscribed)
markup = InlineKeyboardMarkup()
markup.add(InlineKeyboardButton("Подписаться на все регионы", callback_data="subscribe_all"))
markup.add(InlineKeyboardButton(text="Отмена", callback_data="cancel_input"))
bot_message = bot.send_message(chat_id,
@ -44,3 +45,17 @@ def register_handlers(bot: TeleBot, app, state_manager: UserStateManager):
bot.register_next_step_handler(message, process_subscription_button, app, bot, chat_id, state_manager, bot_message.message_id)
def register_callback_subscribe(bot: TeleBot, app, state_manager):
@bot.callback_query_handler(func=lambda call: call.data == "subscribe_all")
def handle_subscribe_all_button(call: CallbackQuery):
chat_id = call.message.chat.id
username = f"{call.from_user.username}" if call.from_user.username else "N/A"
with app.app_context():
if not auth(chat_id, app):
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
logger.warning(f"Неавторизованный пользователь {chat_id} @{username}")
state_manager.set_state(chat_id, UserStates.REGISTRATION)
return
process_subscribe_all_regions(call, app, bot, chat_id, state_manager)

View File

@ -1,15 +0,0 @@
# app/bot/handlers/settings.py
from telebot.types import Message
from app.bot.keyboards.main_menu import get_main_menu
from app.bot.keyboards.settings_menu import get_settings_menu
def register_handlers(bot,app):
@bot.message_handler(func=lambda msg: msg.text == "Режим уведомлений")
def handle_notify_mode(message: Message):
bot.send_message(message.chat.id, "⚙️ Настройка режима уведомлений пока не реализована.")
@bot.message_handler(func=lambda msg: msg.text == "Назад")
def handle_back(message: Message):
bot.send_message(message.chat.id, "Возврат в главное меню", reply_markup=get_main_menu())

View File

@ -1,10 +1,12 @@
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
from telebot import logger
from telebot import logger, TeleBot
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from app.bot.constants import UserStates
from app.bot.keyboards.settings_menu import get_settings_menu
from app.bot.processors.unsubscribe_processor import process_unsubscribe_button, process_unsubscribe_all_regions
from app.bot.utils.auth import auth
from app.bot.utils.helpers import get_user_subscribed_regions
from app.bot.utils.regions import format_regions_list
from app.bot.processors.unsubscribe_processor import process_unsubscribe_button
def register_handlers(bot, app, state_manager):
@ -14,22 +16,48 @@ def register_handlers(bot, app, state_manager):
with app.app_context():
chat_id = message.chat.id
username = f"{message.from_user.username}" if message.from_user.username else "N/A"
if not auth(chat_id, app):
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
logger.warning(f"Неавторизованный пользователь {chat_id} @{username}")
state_manager.set_state(chat_id, UserStates.REGISTRATION)
return
else:
state_manager.set_state(chat_id, UserStates.WAITING_INPUT)
user_subscriptions = get_user_subscribed_regions(chat_id)
# ✅ Предварительная проверка: есть ли подписки
if not user_subscriptions:
bot.send_message(chat_id, " У вас нет активных подписок для отписки.",reply_markup=get_settings_menu())
state_manager.set_state(chat_id, UserStates.SETTINGS_MENU)
return
# Есть подписки — предлагаем меню отписки
state_manager.set_state(chat_id, UserStates.WAITING_INPUT)
formated_user_subscriptions = format_regions_list(user_subscriptions)
markup = InlineKeyboardMarkup()
markup.add(InlineKeyboardButton("Отписаться от всех регионов", callback_data="unsubscribe_all"))
markup.add(InlineKeyboardButton("Отмена", callback_data="cancel_input"))
bot.send_message(chat_id,
f"Введите номер(а) региона(ов) через запятую подписки которых вы хотите удалить:\n\n{formated_user_subscriptions}",
f"Введите номер(а) региона(ов) через запятую, от которых вы хотите отписаться:\n\n{formated_user_subscriptions}",
reply_markup=markup)
bot.register_next_step_handler(message, process_unsubscribe_button, app, bot, chat_id, state_manager)
def register_callback_unsubscribe(bot: TeleBot, app, state_manager):
@bot.callback_query_handler(func=lambda call: call.data == "unsubscribe_all")
def handle_subscribe_all_button(call: CallbackQuery):
chat_id = call.message.chat.id
username = f"{call.from_user.username}" if call.from_user.username else "N/A"
with app.app_context():
if not auth(chat_id, app):
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
logger.warning(f"Неавторизованный пользователь {chat_id} @{username}")
state_manager.set_state(chat_id, UserStates.REGISTRATION)
return
process_unsubscribe_all_regions(call, app, bot, state_manager)

View File

@ -75,3 +75,50 @@ def process_subscription_button(message: Message, app, bot, chat_id: int, state_
# Показываем меню
bot.send_message(chat_id, f"✅ Подписка на регионы: {', '.join(subbed_regions)} оформлена.", reply_markup=get_settings_menu())
def process_subscribe_all_regions(call, app, bot, chat_id: int, state_manager):
try:
with app.app_context():
username = f"{call.from_user.username}" if call.from_user.username else "N/A"
# Получаем все активные регионы
active_regions = Regions.query.filter_by(active=True).all()
active_region_ids = {r.region_id for r in active_regions}
# Получаем уже подписанные регионы
existing_subs = Subscriptions.query.filter_by(chat_id=chat_id).all()
existing_region_ids = {sub.region_id for sub in existing_subs if sub.active}
newly_added = []
for region in active_regions:
if region.region_id in existing_region_ids:
continue
existing = next((sub for sub in existing_subs if sub.region_id == region.region_id), None)
if existing:
existing.active = True
db.session.add(existing)
else:
new_sub = Subscriptions(chat_id=chat_id, region_id=region.region_id, active=True)
db.session.add(new_sub)
newly_added.append(str(region.region_id))
db.session.commit()
# Логирование действия
if newly_added:
log_user_event(chat_id, app, username, f"Подписался на все регионы: {', '.join(newly_added)}")
bot.answer_callback_query(call.id)
bot.clear_step_handler_by_chat_id(chat_id)
bot.delete_message(chat_id, call.message.message_id)
bot.send_message(chat_id,
"✅ Вы подписались на все доступные регионы.",
reply_markup=get_settings_menu())
state_manager.set_state(chat_id, UserStates.SETTINGS_MENU)
except Exception as e:
bot.send_message(chat_id, "⚠️ Не удалось выполнить подписку. Попробуйте позже.")

View File

@ -1,13 +1,15 @@
from flask import Flask
from telebot import TeleBot, logger
from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from app.bot.keyboards.settings_menu import get_settings_menu
from app.bot.utils.helpers import get_user_subscribed_regions
from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from app import Subscriptions
from app.bot.constants import UserStates
from app.bot.keyboards.settings_menu import get_settings_menu
from app.bot.states import UserStateManager
from app.bot.utils.helpers import get_user_subscribed_regions
from app.bot.utils.tg_audit import log_user_event
from app.extensions.db import db
from app.bot.states import UserStateManager
from app.bot.constants import UserStates
def process_unsubscribe_button(message: Message, app: Flask, bot: TeleBot, chat_id: int, state_manager: UserStateManager):
unsubbed_regions = []
@ -63,3 +65,38 @@ def process_unsubscribe_button(message: Message, app: Flask, bot: TeleBot, chat_
state_manager.set_state(chat_id, UserStates.SETTINGS_MENU)
bot.send_message(chat_id, "⚙ Вернулись в меню настроек.", reply_markup=get_settings_menu())
def process_unsubscribe_all_regions(call: CallbackQuery, app: Flask, bot: TeleBot, state_manager: UserStateManager):
chat_id = call.message.chat.id
username = f"{call.from_user.username}" if call.from_user.username else "N/A"
try:
with app.app_context():
subscriptions = Subscriptions.query.filter_by(chat_id=chat_id, active=True).all()
if not subscriptions:
bot.answer_callback_query(call.id, text="У вас нет активных подписок.")
return
for sub in subscriptions:
sub.active = False
db.session.add(sub)
db.session.commit()
log_user_event(chat_id, app, username, "Отписался от всех регионов")
bot.answer_callback_query(call.id)
bot.clear_step_handler_by_chat_id(chat_id)
bot.delete_message(chat_id, call.message.message_id)
bot.send_message(chat_id,
"✅ Вы успешно отписались от всех регионов.",
reply_markup=get_settings_menu())
state_manager.set_state(chat_id, UserStates.SETTINGS_MENU)
except Exception as e:
state_manager.set_state(chat_id, UserStates.SETTINGS_MENU)
bot.send_message(chat_id, "⚠ Произошла ошибка при отписке. Попробуйте позже.",reply_markup=get_settings_menu())
return

View File

@ -1,17 +1,21 @@
import asyncio
import json
import logging
from typing import Optional
from telebot import apihelper
from aio_pika import connect_robust, Message, DeliveryMode
from aio_pika.abc import AbstractIncomingMessage
from app.bot.services.mailing_service.parser import parse_region_id
from telebot import apihelper
from app.bot.services.mailing_service.composer import compose_telegram_message
from app.bot.services.mailing_service.db_utils import get_recipients_by_region
from config import RABBITMQ_URL_FULL, RABBITMQ_QUEUE, RABBITMQ_NOTIFICATIONS_QUEUE
from app.bot.services.mailing_service.parser import parse_region_id
from app.bot.services.mailing_service.utils.link_button import create_link_button
from app.bot.services.mailing_service.utils.tg_get_user import get_telegram_id_by_chat_id
from config import RABBITMQ_URL_FULL, RABBITMQ_QUEUE, RABBITMQ_NOTIFICATIONS_QUEUE, MAILING_RATE_LIMIT
logger = logging.getLogger("TeleBot")
rate_limit_semaphore = asyncio.Semaphore(25)
rate_limit_semaphore = asyncio.Semaphore(MAILING_RATE_LIMIT)
class AsyncMailingService:
def __init__(self, flask_app, bot):
@ -28,7 +32,7 @@ class AsyncMailingService:
async def consume_raw_messages(self):
while True:
try:
logger.info("[MailingService] Подключение к RabbitMQ (сырые сообщения)...")
logger.info("[MailingService][raw] Подключение к RabbitMQ (сырые сообщения)...")
connection = await connect_robust(RABBITMQ_URL_FULL, loop=self.loop)
async with connection:
channel = await connection.channel()
@ -36,14 +40,14 @@ class AsyncMailingService:
raw_queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True)
notifications_queue = await channel.declare_queue(RABBITMQ_NOTIFICATIONS_QUEUE, durable=True)
logger.info("[MailingService] Ожидание сообщений из очереди...")
logger.info("[MailingService][raw] Ожидание сообщений из очереди...")
async with raw_queue.iterator() as queue_iter:
async for message in queue_iter:
await self.handle_raw_message(message, channel, notifications_queue)
except Exception as e:
logger.error(f"[MailingService] Ошибка подключения или обработки: {e}")
logger.info("[MailingService] Повторное подключение через 5 секунд...")
logger.error(f"[MailingService][raw] Ошибка подключения или обработки: {e}")
logger.info("[MailingService][raw] Повторное подключение через 5 секунд...")
await asyncio.sleep(5)
async def handle_raw_message(self, message: AbstractIncomingMessage, channel, notifications_queue):
@ -51,9 +55,9 @@ class AsyncMailingService:
with self.flask_app.app_context():
try:
data = json.loads(message.body.decode("utf-8"))
logger.info(f"[MailingService] Получено сообщение: {json.dumps(data, ensure_ascii=False)}")
logger.info(f"[MailingService][raw] Получены данные: {json.dumps(data, ensure_ascii=False)}")
region_id = parse_region_id(data.get("host"))
region_id = int(parse_region_id(data.get("hostgroups")))
severity = data.get("severity", "")
# Получатели и сообщение параллельно
@ -61,12 +65,8 @@ class AsyncMailingService:
message_task = asyncio.to_thread(compose_telegram_message, data)
recipients, (final_message, link) = await asyncio.gather(recipients_task, message_task)
logger.info(f"[MailingService] Получатели: {recipients}")
if link:
logger.info(f"[MailingService] Сообщение для Telegram: {final_message} {link}")
else:
logger.info(f"[MailingService] Сообщение для Telegram: {final_message}")
logger.info(f"[MailingService][raw] Формирование списка получателей ({len(recipients)})")
logger.debug(f"[MailingService][raw] Список получателей: {', '.join(map(str, recipients))}")
# Формируем и публикуем индивидуальные уведомления в очередь отправки
for chat_id in recipients:
@ -79,29 +79,29 @@ class AsyncMailingService:
msg = Message(body, delivery_mode=DeliveryMode.PERSISTENT)
await channel.default_exchange.publish(msg, routing_key=notifications_queue.name)
logger.info(f"[MailingService] Поставлено в очередь уведомлений: {len(recipients)} сообщений")
logger.info(f"[MailingService][raw] Поставлено в очередь уведомлений: {len(recipients)} сообщений")
except Exception as e:
logger.exception(f"[MailingService] Ошибка обработки сообщения: {e}")
logger.exception(f"[MailingService][raw] Ошибка обработки сообщения: {e}")
async def consume_notifications(self):
while True:
try:
logger.info("[MailingService] Подключение к RabbitMQ (уведомления для отправки)...")
logger.info("[MailingService][formated] Подключение к RabbitMQ (уведомления для отправки)...")
connection = await connect_robust(RABBITMQ_URL_FULL, loop=self.loop)
async with connection:
channel = await connection.channel()
await channel.set_qos(prefetch_count=5)
notif_queue = await channel.declare_queue(RABBITMQ_NOTIFICATIONS_QUEUE, durable=True)
logger.info("[MailingService] Ожидание сообщений из очереди уведомлений...")
logger.info("[MailingService][formated] Ожидание сообщений из очереди уведомлений...")
async with notif_queue.iterator() as queue_iter:
async for message in queue_iter:
await self.handle_notification_message(message)
except Exception as e:
logger.error(f"[MailingService] Ошибка подключения или обработки уведомлений: {e}")
logger.info("[MailingService] Повторное подключение через 5 секунд...")
logger.error(f"[MailingService][formated] Ошибка подключения или обработки уведомлений: {e}")
logger.info("[MailingService][formated] Повторное подключение через 5 секунд...")
await asyncio.sleep(5)
async def handle_notification_message(self, message: AbstractIncomingMessage):
@ -111,41 +111,60 @@ class AsyncMailingService:
chat_id = data.get("chat_id")
message_text = data.get("message")
link = data.get("link")
if link:
message_text = f"{message_text} {link}"
# TODO: расширить логику отправки с кнопкой, если нужно
await self.send_message(chat_id, message_text)
await self.send_message(chat_id, message_text, link)
except Exception as e:
logger.error(f"[MailingService] Ошибка отправки уведомления: {e}")
logger.error(f"[MailingService][tg message] Ошибка отправки уведомления: {e}")
# Можно реализовать message.nack() для повторной попытки
async def send_message(self, chat_id, message):
async def send_message(self, chat_id, message, link: Optional[str] = None):
telegram_id = "unknown"
try:
await rate_limit_semaphore.acquire()
def get_telegram_id():
with self.flask_app.app_context():
from app.models.users import Users
user = Users.query.filter_by(chat_id=chat_id).first()
return user.telegram_id if user else "unknown"
# Получаем telegram_id в отдельном потоке
telegram_id = await asyncio.to_thread(get_telegram_id_by_chat_id, self.flask_app, chat_id)
telegram_id = await asyncio.to_thread(get_telegram_id)
# Формируем клавиатуру с кнопкой
link_button = create_link_button(link)
await asyncio.to_thread(self.bot.send_message, chat_id, message, parse_mode="HTML")
await asyncio.to_thread(
self.bot.send_message,
chat_id,
message,
parse_mode="HTML",
reply_markup=link_button
)
formatted_message = message.replace("\n", " ").replace("\r", "")
logger.info(f"[MailingService] Отправлено уведомление {telegram_id} ({chat_id}): {formatted_message}")
logger.info(f"[MailingService][tg message] Отправлено уведомление {telegram_id} ({chat_id}): {formatted_message}")
except apihelper.ApiTelegramException as e:
if "429" in str(e):
logger.warning(f"[MailingService] Rate limit для {telegram_id} ({chat_id}), ждем и повторяем...")
# Попытка получить error_code и описание из исключения
error_code = None
description = None
try:
if hasattr(e, "result") and e.result:
error_code = e.result.get("error_code")
description = e.result.get("description")
except Exception:
pass
if error_code == 429:
logger.warning(f"[MailingService][tg message] Rate limit для {telegram_id} ({chat_id}), ждем и повторяем...")
await asyncio.sleep(1)
await self.send_message(chat_id, message)
await self.send_message(chat_id, message, link)
elif error_code in (400, 403) and description and (
"user is deactivated" in description.lower() or
"bot was blocked by the user" in description.lower() or
"user not found" in description.lower()
):
logger.warning(f"[MailingService][tg message] Ошибка {error_code}: {description}. Пользователь {telegram_id} ({chat_id}) заблокировал бота или не существует. Отправка пропущена.")
# Не повторяем отправку
else:
logger.error(f"[MailingService] Ошибка отправки сообщения {telegram_id} ({chat_id}): {e}")
logger.error(f"[MailingService][tg message] Ошибка отправки сообщения {telegram_id} ({chat_id}): {e}")
except Exception as e:
logger.error(f"[MailingService] Неожиданная ошибка отправки сообщения {telegram_id} ({chat_id}): {e}")
logger.error(f"[MailingService][tg message] Неожиданная ошибка отправки сообщения {telegram_id} ({chat_id}): {e}")
finally:
rate_limit_semaphore.release()

View File

@ -1,19 +1,31 @@
# parser.py
import re
def parse_region_id(host: str) -> int | None:
"""
Извлекает region_id из строки host.
Формат: p<region><...>, например p18ecpapp01 region_id = 18
# def parse_region_id(host: str) -> int | None:
# """
# Извлекает region_id из строки host.
# Формат: p<region><...>, например p18ecpapp01 → region_id = 18
#
# Returns:
# int | None: номер региона или None
# """
# if not host or not host.startswith("p"):
# return None
#
# match = re.match(r"^p(\d+)", host)
# if match:
# return int(match.group(1))
# return None
Returns:
int | None: номер региона или None
"""
if not host or not host.startswith("p"):
return None
def parse_region_id(hostgroups: str | None) -> str:
if not hostgroups:
return "0"
match = re.match(r"^p(\d+)", host)
if match:
return int(match.group(1))
return None
if hostgroups == "p00rtmis":
return "0"
parts = hostgroups.split("_")
if len(parts) >= 2:
return parts[1]
return "0"

View File

@ -0,0 +1,17 @@
from typing import Optional
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
def create_link_button(link: Optional[str]) -> Optional[InlineKeyboardMarkup]:
"""
Создаёт InlineKeyboardMarkup с кнопкой-ссылкой.
Если ссылка не передана, возвращает None.
"""
if not link:
return None
markup = InlineKeyboardMarkup()
button = InlineKeyboardButton(text="График", url=link)
markup.add(button)
return markup

View File

@ -0,0 +1,6 @@
def get_telegram_id_by_chat_id(flask_app, chat_id: int) -> str:
from app.models.users import Users # импорт здесь, чтобы избежать циклических зависимостей
with flask_app.app_context():
user = Users.query.filter_by(chat_id=chat_id).first()
return user.telegram_id if user else "unknown"

View File

@ -27,14 +27,13 @@ RABBITMQ_QUEUE = os.environ.get("RABBITMQ_QUEUE", "telegram_notifications")
RABBITMQ_NOTIFICATIONS_QUEUE = os.environ.get("RABBITMQ_NOTIFICATIONS_QUEUE", "notifications_queue")
RABBITMQ_VHOST = os.getenv("RABBITMQ_VHOST", "/")
RABBITMQ_VHOST_ENCODED = quote_plus(RABBITMQ_VHOST)
RABBITMQ_URL_FULL = (
f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST_ENCODED}"
)
# Mailing settings
MAILING_MAX_WORKERS = int(os.environ.get("MAILING_MAX_WORKERS", "16"))
MAILING_RATE_LIMIT = int(os.environ.get("MAILING_RATE_LIMIT", "25"))
MAILING_MAX_WORKERS = int(os.getenv("MAILING_MAX_WORKERS", "16"))
MAILING_RATE_LIMIT = int(os.getenv("MAILING_RATE_LIMIT", "25"))
MAILING_MAX_RETRIES = int(os.getenv("MAILING_MAX_RETRIES", "5"))
# Настройки Flask-LDAP3-login
LDAP_HOST = os.getenv('LDAP_HOST', 'localhost')
LDAP_PORT = int(os.getenv('LDAP_PORT', 389))