Добавить планировщик утреннего дайджеста
- utils/morning_runner.py: Scheduler + run_morning() - bot.py: автоматический запуск планировщика при старте - commands/morning.py: использовать run_morning() вместо дублирования - .env.example: добавить MORNING_TIME=07:00 - AGENTS.md: обновить документацию - tests/test_morning_runner.py: 10 тестов для Scheduler
This commit is contained in:
parent
a3e11b1e9c
commit
e387f330a0
@ -1 +1,2 @@
|
|||||||
DISCORD_TOKEN=your_bot_token_here
|
DISCORD_TOKEN=your_bot_token_here
|
||||||
|
MORNING_TIME=07:00
|
||||||
|
|||||||
@ -31,6 +31,14 @@ python bot.py
|
|||||||
| `!cat` | Случайный котик | Embed с изображением |
|
| `!cat` | Случайный котик | Embed с изображением |
|
||||||
| `!msg <текст>` | Повторяет текст | Текст |
|
| `!msg <текст>` | Повторяет текст | Текст |
|
||||||
|
|
||||||
|
## Планировщик
|
||||||
|
|
||||||
|
- Ежедневный утренний дайджест (`!morning`) запускается автоматически в 07:00
|
||||||
|
- Время задаётся в `.env` переменной `MORNING_TIME` (формат `ЧЧ:ММ`)
|
||||||
|
- Реализован через `discord.ext.tasks.loop`
|
||||||
|
- Отправляет дайджест в первый канал, где бот имеет права
|
||||||
|
- Код в `utils/morning_runner.py`
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
|
|
||||||
| Каталог | Назначение |
|
| Каталог | Назначение |
|
||||||
@ -71,6 +79,7 @@ python bot.py
|
|||||||
| Переменная | Описание | Где взять |
|
| Переменная | Описание | Где взять |
|
||||||
|------------|----------|-----------|
|
|------------|----------|-----------|
|
||||||
| `DISCORD_TOKEN` | Токен бота | Discord Developer Portal |
|
| `DISCORD_TOKEN` | Токен бота | Discord Developer Portal |
|
||||||
|
| `MORNING_TIME` | Время запуска утреннего дайджеста | `.env` (формат `ЧЧ:ММ`, по умолчанию `07:00`) |
|
||||||
|
|
||||||
## Зависимости
|
## Зависимости
|
||||||
```txt
|
```txt
|
||||||
|
|||||||
11
bot.py
11
bot.py
@ -12,6 +12,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
from commands import ALL_COMMANDS
|
from commands import ALL_COMMANDS
|
||||||
from console_commands import ALL_CONSOLE_COMMANDS
|
from console_commands import ALL_CONSOLE_COMMANDS
|
||||||
|
from utils.morning_runner import Scheduler
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -23,16 +24,24 @@ intents.message_content = True
|
|||||||
bot = commands.Bot(command_prefix="!", intents=intents)
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
bot_ready = threading.Event()
|
bot_ready = threading.Event()
|
||||||
|
scheduler: Scheduler | None = None
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
|
global scheduler
|
||||||
print(f"Бот вошёл как {bot.user}")
|
print(f"Бот вошёл как {bot.user}")
|
||||||
for cog_class in ALL_COMMANDS:
|
for cog_class in ALL_COMMANDS:
|
||||||
cog = cog_class()
|
cog = cog_class()
|
||||||
await bot.add_cog(cog)
|
await bot.add_cog(cog)
|
||||||
for cog in bot.cogs:
|
for cog in bot.cogs:
|
||||||
print(f" Загружен: {cog}")
|
print(f" Загружен: {cog}")
|
||||||
|
|
||||||
|
# Запуск планировщика
|
||||||
|
morning_time = os.getenv("MORNING_TIME", "07:00")
|
||||||
|
scheduler = Scheduler(bot, morning_time)
|
||||||
|
print(f" Планировщик запущен (время: {morning_time})")
|
||||||
|
|
||||||
bot_ready.set()
|
bot_ready.set()
|
||||||
|
|
||||||
|
|
||||||
@ -123,5 +132,7 @@ if __name__ == "__main__":
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nОстановка бота...")
|
print("\nОстановка бота...")
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
if scheduler:
|
||||||
|
scheduler.stop()
|
||||||
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result()
|
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@ -1,95 +1,16 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from utils.pogoda import fetch_weather, pressure_to_mmhg, translate_weather
|
|
||||||
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
from utils.morning_runner import run_morning
|
||||||
from utils.cat import fetch_cat
|
|
||||||
|
|
||||||
|
|
||||||
class Morning(commands.Cog):
|
class Morning(commands.Cog):
|
||||||
"""Команда !morning — погода и новости утром"""
|
"""Команда !morning — погода и новости утром"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
|
pass
|
||||||
|
|
||||||
@commands.command(name="morning")
|
@commands.command(name="morning")
|
||||||
async def morning(self, ctx):
|
async def morning(self, ctx):
|
||||||
"""Погода, лучшие статьи за сутки и котик"""
|
"""Погода, лучшие статьи за сутки и котик"""
|
||||||
# Параллельный запрос погоды, новостей и котика
|
await run_morning(ctx.bot, ctx.channel)
|
||||||
weather_data, articles, posts, cat_url = await asyncio.gather(
|
|
||||||
fetch_weather(self.api_url),
|
|
||||||
fetch_rss(RSS_URL_ARTICLES),
|
|
||||||
fetch_rss(RSS_URL_POSTS),
|
|
||||||
fetch_cat(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Формируем embed ---
|
|
||||||
embed = discord.Embed(title="Доброе утро!", color=0xF4A460)
|
|
||||||
|
|
||||||
# Котик как thumbnail (маленький в углу)
|
|
||||||
if cat_url:
|
|
||||||
embed.set_thumbnail(url=cat_url)
|
|
||||||
|
|
||||||
description_lines = []
|
|
||||||
|
|
||||||
# --- Погода ---
|
|
||||||
if weather_data is not None:
|
|
||||||
current = weather_data.get("current_condition", [{}])[0]
|
|
||||||
if current:
|
|
||||||
temp = current.get("temp_C", "\u2014")
|
|
||||||
feels_like = current.get("FeelsLikeC", "\u2014")
|
|
||||||
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "\u2014"))
|
|
||||||
humidity = current.get("humidity", "\u2014")
|
|
||||||
wind_kmh = current.get("windspeedKmph", "\u2014")
|
|
||||||
try:
|
|
||||||
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "\u2014" else "\u2014"
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
wind = "\u2014"
|
|
||||||
pressure_mb = current.get("pressure", "\u2014")
|
|
||||||
pressure_mm = pressure_to_mmhg(pressure_mb)
|
|
||||||
|
|
||||||
description_lines.append(
|
|
||||||
f"**Погода в Магнитогорске:**\n"
|
|
||||||
f"Температура: {temp}\u00b0C (ощущается как {feels_like}\u00b0C)\n"
|
|
||||||
f"Описание: {description}\n"
|
|
||||||
f"Влажность: {humidity}%\n"
|
|
||||||
f"Ветер: {wind} м/с\n"
|
|
||||||
f"Давление: {pressure_mm} мм рт. ст."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
description_lines.append("Не удалось получить данные о погоде.")
|
|
||||||
else:
|
|
||||||
description_lines.append("Не удалось получить данные о погоде.")
|
|
||||||
|
|
||||||
description_lines.append("") # пустая строка-разделитель
|
|
||||||
|
|
||||||
# --- Новости: статьи ---
|
|
||||||
if articles is not None:
|
|
||||||
if articles:
|
|
||||||
lines = format_articles(articles,
|
|
||||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
|
||||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
|
||||||
description_lines.append("\n".join(lines))
|
|
||||||
else:
|
|
||||||
description_lines.append("Новостей пока нет.")
|
|
||||||
else:
|
|
||||||
description_lines.append("Не удалось получить новости.")
|
|
||||||
|
|
||||||
description_lines.append("") # пустая строка-разделитель
|
|
||||||
|
|
||||||
# --- Новости: посты ---
|
|
||||||
if posts is not None:
|
|
||||||
if posts:
|
|
||||||
lines = format_articles(posts,
|
|
||||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
|
||||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
|
||||||
description_lines.append("\n".join(lines))
|
|
||||||
else:
|
|
||||||
description_lines.append("Новостей пока нет.")
|
|
||||||
else:
|
|
||||||
description_lines.append("Не удалось получить новости.")
|
|
||||||
|
|
||||||
embed.description = "\n".join(description_lines)
|
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|||||||
134
tests/test_morning_runner.py
Normal file
134
tests/test_morning_runner.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"""Тесты для utils/morning_runner.py — Scheduler и run_morning."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import discord
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from utils.morning_runner import Scheduler, run_morning
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchedulerInit:
|
||||||
|
"""Тесты инициализации Scheduler."""
|
||||||
|
|
||||||
|
def test_init_sets_morning_time(self):
|
||||||
|
"""Инициализация должна устанавливать время."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot, "08:30")
|
||||||
|
assert scheduler.morning_time == "08:30"
|
||||||
|
|
||||||
|
def test_init_default_morning_time(self):
|
||||||
|
"""Инициализация с дефолтным временем."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot)
|
||||||
|
assert scheduler.morning_time == "07:00"
|
||||||
|
|
||||||
|
def test_init_creates_loop(self):
|
||||||
|
"""Инициализация должна создавать loop."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot)
|
||||||
|
assert scheduler.morning_loop is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchedulerCalculateNextRun:
|
||||||
|
"""Тесты расчёта следующего запуска."""
|
||||||
|
|
||||||
|
def test_next_run_today_before_time(self):
|
||||||
|
"""Если сейчас раньше времени — вернуть сегодня."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot, "14:00")
|
||||||
|
|
||||||
|
with patch("utils.morning_runner.datetime") as mock_dt:
|
||||||
|
mock_dt.now.return_value = datetime(2026, 5, 29, 10, 0, 0)
|
||||||
|
next_run = scheduler._calculate_next_run()
|
||||||
|
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
|
||||||
|
|
||||||
|
def test_next_run_tomorrow_after_time(self):
|
||||||
|
"""Если сейчас позже времени — вернуть завтра."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot, "14:00")
|
||||||
|
|
||||||
|
with patch("utils.morning_runner.datetime") as mock_dt:
|
||||||
|
mock_dt.now.return_value = datetime(2026, 5, 29, 15, 0, 0)
|
||||||
|
next_run = scheduler._calculate_next_run()
|
||||||
|
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
||||||
|
|
||||||
|
def test_next_run_exact_time(self):
|
||||||
|
"""Если сейчас ровно время — вернуть завтра."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot, "14:00")
|
||||||
|
|
||||||
|
with patch("utils.morning_runner.datetime") as mock_dt:
|
||||||
|
mock_dt.now.return_value = datetime(2026, 5, 29, 14, 0, 0)
|
||||||
|
next_run = scheduler._calculate_next_run()
|
||||||
|
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchedulerStartStop:
|
||||||
|
"""Тесты запуска/остановки планировщика."""
|
||||||
|
|
||||||
|
def test_start_starts_loop(self):
|
||||||
|
"""start() должен вызывать start() на loop."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot)
|
||||||
|
loop_mock = MagicMock()
|
||||||
|
scheduler.morning_loop = loop_mock
|
||||||
|
|
||||||
|
scheduler.start()
|
||||||
|
loop_mock.start.assert_called_once()
|
||||||
|
|
||||||
|
def test_stop_stops_loop(self):
|
||||||
|
"""stop() должен вызывать stop() на loop."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot)
|
||||||
|
loop_mock = MagicMock()
|
||||||
|
scheduler.morning_loop = loop_mock
|
||||||
|
|
||||||
|
scheduler.stop()
|
||||||
|
loop_mock.stop.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchedulerCheckAndRun:
|
||||||
|
"""Тесты проверки и запуска morning."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_and_run_same_day_no_duplicate(self):
|
||||||
|
"""Не должен запускать дважды в один день."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
scheduler = Scheduler(bot, "07:00")
|
||||||
|
scheduler._last_run_date = datetime.now().day
|
||||||
|
|
||||||
|
with patch("utils.morning_runner.datetime") as mock_dt:
|
||||||
|
mock_dt.now.return_value = datetime(2026, 5, 29, 7, 0, 0)
|
||||||
|
await scheduler._check_and_run_morning()
|
||||||
|
|
||||||
|
# run_morning не должен вызываться
|
||||||
|
assert scheduler._last_run_date == datetime.now().day
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunMorning:
|
||||||
|
"""Тесты run_morning."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_morning_sends_embed(self):
|
||||||
|
"""run_morning должен отправлять embed в канал."""
|
||||||
|
bot = AsyncMock()
|
||||||
|
channel = AsyncMock()
|
||||||
|
channel.name = "test-channel"
|
||||||
|
channel.guild.me = MagicMock()
|
||||||
|
channel.permissions_for.return_value.send_messages = True
|
||||||
|
|
||||||
|
with patch("utils.morning_runner.asyncio.gather", new=AsyncMock(return_value=(
|
||||||
|
{"current_condition": [{"temp_C": "20", "FeelsLikeC": "22", "weatherDesc": [{"value": "Clear"}], "humidity": "50", "windspeedKmph": "10", "pressure": "1013"}]},
|
||||||
|
[{"title": "Test", "link": "http://test.com", "pub_date": "Mon, 01 Jan 2026 00:00:00 GMT", "creator": "", "tags": []}],
|
||||||
|
[{"title": "Test", "link": "http://test.com", "pub_date": "Mon, 01 Jan 2026 00:00:00 GMT", "creator": "", "tags": []}],
|
||||||
|
"http://cat.jpg",
|
||||||
|
))), patch("utils.morning_runner.discord.Embed") as mock_embed:
|
||||||
|
await run_morning(bot, channel)
|
||||||
|
|
||||||
|
channel.send.assert_called_once()
|
||||||
|
call_args = channel.send.call_args[1]
|
||||||
|
assert "embed" in call_args
|
||||||
|
assert call_args["embed"] is not None
|
||||||
167
utils/morning_runner.py
Normal file
167
utils/morning_runner.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
"""Утилита для запуска утреннего дайджеста."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import tasks
|
||||||
|
|
||||||
|
from utils.pogoda import fetch_weather, pressure_to_mmhg, translate_weather
|
||||||
|
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||||
|
from utils.cat import fetch_cat
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
|
||||||
|
"""Выполнить утренний дайджест и отправить в канал."""
|
||||||
|
try:
|
||||||
|
api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
|
||||||
|
|
||||||
|
# Параллельный запрос погоды, новостей и котика
|
||||||
|
weather_data, articles, posts, cat_url = await asyncio.gather(
|
||||||
|
fetch_weather(api_url),
|
||||||
|
fetch_rss(RSS_URL_ARTICLES),
|
||||||
|
fetch_rss(RSS_URL_POSTS),
|
||||||
|
fetch_cat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Формируем embed ---
|
||||||
|
embed = discord.Embed(title="🌅 Утренний дайджест!", color=0xF4A460)
|
||||||
|
|
||||||
|
# Котик как thumbnail
|
||||||
|
if cat_url:
|
||||||
|
embed.set_thumbnail(url=cat_url)
|
||||||
|
|
||||||
|
description_lines = []
|
||||||
|
|
||||||
|
# --- Погода ---
|
||||||
|
if weather_data is not None:
|
||||||
|
current = weather_data.get("current_condition", [{}])[0]
|
||||||
|
if current:
|
||||||
|
temp = current.get("temp_C", "—")
|
||||||
|
feels_like = current.get("FeelsLikeC", "—")
|
||||||
|
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "—"))
|
||||||
|
humidity = current.get("humidity", "—")
|
||||||
|
wind_kmh = current.get("windspeedKmph", "—")
|
||||||
|
try:
|
||||||
|
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "—" else "—"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
wind = "—"
|
||||||
|
pressure_mb = current.get("pressure", "—")
|
||||||
|
pressure_mm = pressure_to_mmhg(pressure_mb)
|
||||||
|
|
||||||
|
description_lines.append(
|
||||||
|
f"**Погода в Магнитогорске:**\n"
|
||||||
|
f"Температура: {temp}°C (ощущается как {feels_like}°C)\n"
|
||||||
|
f"Описание: {description}\n"
|
||||||
|
f"Влажность: {humidity}%\n"
|
||||||
|
f"Ветер: {wind} м/с\n"
|
||||||
|
f"Давление: {pressure_mm} мм рт. ст."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
description_lines.append("Не удалось получить данные о погоде.")
|
||||||
|
else:
|
||||||
|
description_lines.append("Не удалось получить данные о погоде.")
|
||||||
|
|
||||||
|
description_lines.append("")
|
||||||
|
|
||||||
|
# --- Новости: статьи ---
|
||||||
|
if articles is not None:
|
||||||
|
if articles:
|
||||||
|
lines = format_articles(articles,
|
||||||
|
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||||
|
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||||
|
description_lines.append("\n".join(lines))
|
||||||
|
else:
|
||||||
|
description_lines.append("Новостей пока нет.")
|
||||||
|
else:
|
||||||
|
description_lines.append("Не удалось получить новости.")
|
||||||
|
|
||||||
|
description_lines.append("")
|
||||||
|
|
||||||
|
# --- Новости: посты ---
|
||||||
|
if posts is not None:
|
||||||
|
if posts:
|
||||||
|
lines = format_articles(posts,
|
||||||
|
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||||
|
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
||||||
|
description_lines.append("\n".join(lines))
|
||||||
|
else:
|
||||||
|
description_lines.append("Новостей пока нет.")
|
||||||
|
else:
|
||||||
|
description_lines.append("Не удалось получить новости.")
|
||||||
|
|
||||||
|
embed.description = "\n".join(description_lines)
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
logger.info("✅ Утренний дайджест отправлен в #%s", channel.name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Ошибка при выполнении утреннего дайджеста: %s", e, exc_info=True)
|
||||||
|
try:
|
||||||
|
await channel.send("❌ Не удалось выполнить утренний дайджест.")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler:
|
||||||
|
"""Планировщик ежедневных задач."""
|
||||||
|
|
||||||
|
def __init__(self, bot: commands.Bot, morning_time: str = "07:00"):
|
||||||
|
self.bot = bot
|
||||||
|
self.morning_time = morning_time
|
||||||
|
self._last_run_date = None
|
||||||
|
self.morning_loop = tasks.loop(seconds=1.0)(self._check_and_run_morning)
|
||||||
|
self._start_scheduler()
|
||||||
|
|
||||||
|
def _start_scheduler(self):
|
||||||
|
try:
|
||||||
|
self.morning_loop.start()
|
||||||
|
logger.info("Планировщик запущен (время: %s)", self.morning_time)
|
||||||
|
except RuntimeError:
|
||||||
|
logger.warning("Планировщик уже запущен")
|
||||||
|
|
||||||
|
def _stop_scheduler(self):
|
||||||
|
try:
|
||||||
|
self.morning_loop.stop()
|
||||||
|
logger.info("Планировщик остановлен")
|
||||||
|
except RuntimeError:
|
||||||
|
logger.warning("Планировщик уже остановлен")
|
||||||
|
|
||||||
|
def _calculate_next_run(self) -> datetime:
|
||||||
|
now = datetime.now()
|
||||||
|
hour, minute = map(int, self.morning_time.split(":"))
|
||||||
|
today_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||||
|
|
||||||
|
if now >= today_run:
|
||||||
|
return today_run + timedelta(days=1)
|
||||||
|
return today_run
|
||||||
|
|
||||||
|
async def _check_and_run_morning(self):
|
||||||
|
now = datetime.now()
|
||||||
|
target = self._calculate_next_run()
|
||||||
|
|
||||||
|
if now >= target and now.day != self._last_run_date:
|
||||||
|
self._last_run_date = now.day
|
||||||
|
await self._run_morning()
|
||||||
|
|
||||||
|
async def _run_morning(self):
|
||||||
|
logger.info(f"Выполняю morning в {self.morning_time}")
|
||||||
|
|
||||||
|
for channel in self.bot.get_all_channels():
|
||||||
|
if isinstance(channel, discord.TextChannel):
|
||||||
|
if channel.permissions_for(channel.guild.me).send_messages:
|
||||||
|
try:
|
||||||
|
await channel.send("🌅 Утренний дайджест!")
|
||||||
|
await run_morning(self.bot, channel)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Ошибка отправки в #%s: %s", channel.name, e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._start_scheduler()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_scheduler()
|
||||||
Loading…
x
Reference in New Issue
Block a user