Исправление планировщика morning: tasks.loop → asyncio.sleep
- Scheduler больше не опрашивает каждую секунду (tasks.loop мог пропустить момент срабатывания из-за неточности тайминга). Теперь спит ровно до целевого времени через asyncio.sleep, затем запускает morning и повторяет. - Добавлено логирование ожидания и срабатывания расписания. - Перенес инициализацию планировщика из on_ready в on_guild_available — гарантирует, что кэш гильдии загружен до запуска Scheduler. - Обновлены тесты под новый Scheduler (asyncio.create_task вместо tasks.loop). - README.md: исправлены пути admin.py → console_commands/admin.py для docker exec. - docker-compose.yml: добавлены переменные LOG_LEVEL и rate-limit конфиги. - console_commands/__init__.py: переименованы help → console_help, reload → reload_cogs (избежал конфликта с built-in и уточнил имена).
This commit is contained in:
parent
4f48471564
commit
8ee5ed669f
14
README.md
14
README.md
@ -187,12 +187,12 @@ DISCORD_TOKEN=ваш_токен docker-compose up
|
|||||||
Для управления ботом из терминала (без Discord-чата) используйте `admin.py`:
|
Для управления ботом из терминала (без Discord-чата) используйте `admin.py`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec discord-bot python admin.py pogoda
|
docker exec discord-bot python console_commands/admin.py pogoda
|
||||||
docker exec discord-bot python admin.py news
|
docker exec discord-bot python console_commands/admin.py news
|
||||||
docker exec discord-bot python admin.py cat
|
docker exec discord-bot python console_commands/admin.py cat
|
||||||
docker exec discord-bot python admin.py morning
|
docker exec discord-bot python console_commands/admin.py morning
|
||||||
docker exec discord-bot python admin.py logs # последние строки лога
|
docker exec discord-bot python console_commands/admin.py logs # последние строки лога
|
||||||
docker exec discord-bot python admin.py help
|
docker exec discord-bot python console_commands/admin.py help
|
||||||
docker stop discord-bot # остановка бота
|
docker stop discord-bot # остановка бота
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -336,7 +336,7 @@ pytest-asyncio>=0.21.0
|
|||||||
| `MorningData` | dataclass с полями weather, articles, posts, cat_url |
|
| `MorningData` | dataclass с полями weather, articles, posts, cat_url |
|
||||||
| `gather_morning()` | Параллельный сбор всех данных для дайджеста |
|
| `gather_morning()` | Параллельный сбор всех данных для дайджеста |
|
||||||
| `run_morning()` | Формирование и отправка embed в канал Discord |
|
| `run_morning()` | Формирование и отправка embed в канал Discord |
|
||||||
| `Scheduler` | Планировщик ежедневных задач (discord.ext.tasks.loop) |
|
| `Scheduler` | Планировщик ежедневных задач (asyncio.sleep до целевого времени) |
|
||||||
|
|
||||||
### utils/rate_limiter.py
|
### utils/rate_limiter.py
|
||||||
|
|
||||||
|
|||||||
18
bot.py
18
bot.py
@ -51,14 +51,24 @@ class BotRunner:
|
|||||||
await self.bot.add_cog(cog)
|
await self.bot.add_cog(cog)
|
||||||
for cog in self.bot.cogs:
|
for cog in self.bot.cogs:
|
||||||
logger.info(" Загружен: %s", cog)
|
logger.info(" Загружен: %s", cog)
|
||||||
|
self.bot_ready.set()
|
||||||
|
|
||||||
|
@self.bot.event
|
||||||
|
async def on_guild_available(guild: discord.Guild) -> None:
|
||||||
|
"""Запуск планировщика после загрузки кэша сервера.
|
||||||
|
|
||||||
|
on_ready срабатывает до полной загрузки guild-кэша,
|
||||||
|
из-за чего get_channel() возвращает None.
|
||||||
|
on_guild_available гарантирует, что данные сервера в кэше.
|
||||||
|
"""
|
||||||
|
# Запускаем планировщик только один раз
|
||||||
|
if self.scheduler is not None:
|
||||||
|
return
|
||||||
|
|
||||||
# Запуск планировщика
|
|
||||||
morning_time = os.getenv("MORNING_TIME", "07:00")
|
morning_time = os.getenv("MORNING_TIME", "07:00")
|
||||||
self.scheduler = Scheduler(self.bot, morning_time)
|
self.scheduler = Scheduler(self.bot, morning_time)
|
||||||
self.bot._scheduler = self.scheduler
|
self.bot._scheduler = self.scheduler
|
||||||
logger.info(" Планировщик запущен (время: %s)", morning_time)
|
logger.info("Планировщик запущен (время: %s, сервер: %s)", morning_time, guild.name)
|
||||||
|
|
||||||
self.bot_ready.set()
|
|
||||||
|
|
||||||
@self.bot.event
|
@self.bot.event
|
||||||
async def on_command_error(ctx: commands.Context, error: Exception) -> None:
|
async def on_command_error(ctx: commands.Context, error: Exception) -> None:
|
||||||
|
|||||||
@ -3,11 +3,11 @@ from .news import news
|
|||||||
from .cat import cat
|
from .cat import cat
|
||||||
from .pogoda import pogoda
|
from .pogoda import pogoda
|
||||||
from .morning import morning
|
from .morning import morning
|
||||||
from .help import help
|
from .help import help as console_help
|
||||||
from .status import status
|
from .status import status
|
||||||
from .stats import stats
|
from .stats import stats
|
||||||
from .logs import logs
|
from .logs import logs
|
||||||
from .reload import reload
|
from .reload import reload as reload_cogs
|
||||||
from .trigger_morning import trigger_morning
|
from .trigger_morning import trigger_morning
|
||||||
|
|
||||||
ALL_CONSOLE_COMMANDS = {
|
ALL_CONSOLE_COMMANDS = {
|
||||||
@ -16,10 +16,10 @@ ALL_CONSOLE_COMMANDS = {
|
|||||||
"cat": cat,
|
"cat": cat,
|
||||||
"pogoda": pogoda,
|
"pogoda": pogoda,
|
||||||
"morning": morning,
|
"morning": morning,
|
||||||
"help": help,
|
"help": console_help,
|
||||||
"status": status,
|
"status": status,
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
"logs": logs,
|
"logs": logs,
|
||||||
"reload": reload,
|
"reload": reload_cogs,
|
||||||
"trigger morning": trigger_morning,
|
"trigger morning": trigger_morning,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,5 +7,14 @@ services:
|
|||||||
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
||||||
- MORNING_TIME=${MORNING_TIME:-07:00}
|
- MORNING_TIME=${MORNING_TIME:-07:00}
|
||||||
- MORNING_CHANNEL_ID=${MORNING_CHANNEL_ID}
|
- MORNING_CHANNEL_ID=${MORNING_CHANNEL_ID}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
|
- CAT_API_RATE=${CAT_API_RATE:-1}
|
||||||
|
- CAT_API_BURST=${CAT_API_BURST:-3}
|
||||||
|
- WEATHER_API_RATE=${WEATHER_API_RATE:-1}
|
||||||
|
- WEATHER_API_BURST=${WEATHER_API_BURST:-3}
|
||||||
|
- OPEN_METEO_API_RATE=${OPEN_METEO_API_RATE:-2}
|
||||||
|
- OPEN_METEO_API_BURST=${OPEN_METEO_API_BURST:-5}
|
||||||
|
- HABR_RSS_RATE=${HABR_RSS_RATE:-1}
|
||||||
|
- HABR_RSS_BURST=${HABR_RSS_BURST:-2}
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- TZ=UTC5
|
- TZ=UTC5
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"""Тесты для utils/morning_runner.py — Scheduler и run_morning."""
|
"""Тесты для utils/morning_runner.py — Scheduler и run_morning."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@ -16,114 +16,86 @@ class TestSchedulerInit:
|
|||||||
def test_init_sets_morning_time(self):
|
def test_init_sets_morning_time(self):
|
||||||
"""Инициализация должна устанавливать время."""
|
"""Инициализация должна устанавливать время."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
mock_loop = MagicMock()
|
with patch("asyncio.create_task") as mock_task:
|
||||||
with patch("utils.morning_runner.tasks.loop", return_value=mock_loop):
|
|
||||||
scheduler = Scheduler(bot, "08:30")
|
scheduler = Scheduler(bot, "08:30")
|
||||||
assert scheduler.morning_time == "08:30"
|
assert scheduler.morning_time == "08:30"
|
||||||
|
|
||||||
def test_init_default_morning_time(self):
|
def test_init_default_morning_time(self):
|
||||||
"""Инициализация с дефолтным временем."""
|
"""Инициализация с дефолтным временем."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
mock_loop = MagicMock()
|
with patch("asyncio.create_task") as mock_task:
|
||||||
with patch("utils.morning_runner.tasks.loop", return_value=mock_loop):
|
|
||||||
scheduler = Scheduler(bot)
|
scheduler = Scheduler(bot)
|
||||||
assert scheduler.morning_time == "07:00"
|
assert scheduler.morning_time == "07:00"
|
||||||
|
|
||||||
def test_init_creates_loop(self):
|
def test_init_creates_task(self):
|
||||||
"""Инициализация должна создавать loop."""
|
"""Инициализация должна создавать asyncio.Task."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
mock_loop = MagicMock()
|
with patch("asyncio.create_task") as mock_task:
|
||||||
with patch("utils.morning_runner.tasks.loop", return_value=mock_loop):
|
|
||||||
scheduler = Scheduler(bot)
|
scheduler = Scheduler(bot)
|
||||||
assert scheduler.morning_loop is not None
|
mock_task.assert_called_once()
|
||||||
|
assert scheduler._task is not None
|
||||||
|
|
||||||
|
|
||||||
class TestSchedulerCalculateNextRun:
|
class TestSchedulerCalculateNextRun:
|
||||||
"""Тесты расчёта следующего запуска."""
|
"""Тесты расчёта следующего запуска."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def _mock_loop(self):
|
|
||||||
"""Замокать tasks.loop, чтобы не создавать реальный coroutine."""
|
|
||||||
with patch("utils.morning_runner.tasks.loop", return_value=MagicMock()):
|
|
||||||
yield
|
|
||||||
|
|
||||||
def test_next_run_today_before_time(self):
|
def test_next_run_today_before_time(self):
|
||||||
"""Если сейчас раньше времени — вернуть сегодня."""
|
"""Если сейчас раньше времени — вернуть сегодня."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
|
with patch("asyncio.create_task"):
|
||||||
scheduler = Scheduler(bot, "14:00")
|
scheduler = Scheduler(bot, "14:00")
|
||||||
|
|
||||||
with patch("utils.morning_runner.datetime") as mock_dt:
|
now = datetime(2026, 5, 29, 10, 0, 0)
|
||||||
mock_dt.now.return_value = datetime(2026, 5, 29, 10, 0, 0)
|
next_run = scheduler._calculate_next_run(now)
|
||||||
next_run = scheduler._calculate_next_run()
|
|
||||||
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
|
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
|
||||||
|
|
||||||
def test_next_run_tomorning_after_time(self):
|
def test_next_run_tomorrow_after_time(self):
|
||||||
"""Если сейчас позже времени — вернуть завтра."""
|
"""Если сейчас позже времени — вернуть завтра."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
|
with patch("asyncio.create_task"):
|
||||||
scheduler = Scheduler(bot, "14:00")
|
scheduler = Scheduler(bot, "14:00")
|
||||||
|
|
||||||
with patch("utils.morning_runner.datetime") as mock_dt:
|
now = datetime(2026, 5, 29, 15, 0, 0)
|
||||||
mock_dt.now.return_value = datetime(2026, 5, 29, 15, 0, 0)
|
next_run = scheduler._calculate_next_run(now)
|
||||||
next_run = scheduler._calculate_next_run()
|
|
||||||
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
||||||
|
|
||||||
def test_next_run_exact_time(self):
|
def test_next_run_exact_time(self):
|
||||||
"""Если сейчас ровно время — вернуть завтра."""
|
"""Если сейчас ровно время — вернуть завтра."""
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
|
with patch("asyncio.create_task"):
|
||||||
scheduler = Scheduler(bot, "14:00")
|
scheduler = Scheduler(bot, "14:00")
|
||||||
|
|
||||||
with patch("utils.morning_runner.datetime") as mock_dt:
|
now = datetime(2026, 5, 29, 14, 0, 0)
|
||||||
mock_dt.now.return_value = datetime(2026, 5, 29, 14, 0, 0)
|
next_run = scheduler._calculate_next_run(now)
|
||||||
next_run = scheduler._calculate_next_run()
|
|
||||||
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
class TestSchedulerStartStop:
|
class TestSchedulerStartStop:
|
||||||
"""Тесты запуска/остановки планировщика."""
|
"""Тесты запуска/остановки планировщика."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
def test_start_starts_task(self):
|
||||||
def _mock_loop(self):
|
"""start() должен запустить task."""
|
||||||
"""Замокать tasks.loop, чтобы не создавать реальный coroutine."""
|
|
||||||
with patch("utils.morning_runner.tasks.loop", return_value=MagicMock()):
|
|
||||||
yield
|
|
||||||
|
|
||||||
def test_start_starts_loop(self):
|
|
||||||
"""start() должен вызывать start() на loop."""
|
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
|
with patch("asyncio.create_task"):
|
||||||
scheduler = Scheduler(bot)
|
scheduler = Scheduler(bot)
|
||||||
loop_mock = MagicMock()
|
assert scheduler._running is True
|
||||||
scheduler.morning_loop = loop_mock
|
|
||||||
|
|
||||||
scheduler.start()
|
def test_stop_stops_task(self):
|
||||||
loop_mock.start.assert_called_once()
|
"""stop() должен остановить task."""
|
||||||
|
|
||||||
def test_stop_stops_loop(self):
|
|
||||||
"""stop() должен вызывать stop() на loop."""
|
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
|
with patch("asyncio.create_task"):
|
||||||
scheduler = Scheduler(bot)
|
scheduler = Scheduler(bot)
|
||||||
loop_mock = MagicMock()
|
|
||||||
scheduler.morning_loop = loop_mock
|
|
||||||
|
|
||||||
scheduler.stop()
|
scheduler.stop()
|
||||||
loop_mock.stop.assert_called_once()
|
assert scheduler._running is False
|
||||||
|
|
||||||
|
def test_double_start_no_duplicate(self):
|
||||||
class TestSchedulerCheckAndRun:
|
"""Повторный start не должен создавать второй task."""
|
||||||
"""Тесты проверки и запуска morning."""
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_check_and_run_same_day_no_duplicate(self):
|
|
||||||
"""Не должен запускать дважды в один день."""
|
|
||||||
bot = AsyncMock()
|
bot = AsyncMock()
|
||||||
scheduler = Scheduler(bot, "07:00")
|
with patch("asyncio.create_task") as mock_task:
|
||||||
scheduler._last_run_date = datetime.now().day
|
scheduler = Scheduler(bot)
|
||||||
|
scheduler.start() # второй вызов
|
||||||
with patch("utils.morning_runner.datetime") as mock_dt:
|
# create_task вызван только при инициализации
|
||||||
mock_dt.now.return_value = datetime(2026, 5, 29, 7, 0, 0)
|
assert mock_task.call_count == 1
|
||||||
await scheduler._check_and_run_morning()
|
|
||||||
|
|
||||||
# run_morning не должен вызываться
|
|
||||||
assert scheduler._last_run_date == datetime.now().day
|
|
||||||
|
|
||||||
|
|
||||||
class TestRunMorning:
|
class TestRunMorning:
|
||||||
|
|||||||
@ -4,12 +4,11 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext import tasks
|
|
||||||
|
|
||||||
from utils.pogoda import (
|
from utils.pogoda import (
|
||||||
API_URL_WEATHER,
|
API_URL_WEATHER,
|
||||||
@ -120,13 +119,18 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel):
|
|||||||
|
|
||||||
|
|
||||||
class Scheduler:
|
class Scheduler:
|
||||||
"""Планировщик ежедневных задач."""
|
"""Планировщик ежедневных задач.
|
||||||
|
|
||||||
|
Использует asyncio.Task вместо tasks.loop —
|
||||||
|
спит до целевого времени, затем выполняет задачу и повторяет.
|
||||||
|
Это надёжнее, чем опрос каждую секунду (tasks.loop может пропустить
|
||||||
|
момент срабатывания из-за неточности asyncio тайминга).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot, morning_time: str = "07:00"):
|
def __init__(self, bot: commands.Bot, morning_time: str = "07:00"):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.morning_time = morning_time
|
self.morning_time = morning_time
|
||||||
self._last_run_date = None
|
self._last_run_date: date | None = None # полная дата, не день месяца
|
||||||
# Канал для утреннего дайджеста (по умолчанию None — первый попавшийся)
|
|
||||||
self._target_channel_id: int | None = None
|
self._target_channel_id: int | None = None
|
||||||
channel_id_str = os.getenv("MORNING_CHANNEL_ID")
|
channel_id_str = os.getenv("MORNING_CHANNEL_ID")
|
||||||
if channel_id_str:
|
if channel_id_str:
|
||||||
@ -134,25 +138,26 @@ class Scheduler:
|
|||||||
self._target_channel_id = int(channel_id_str)
|
self._target_channel_id = int(channel_id_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warning("Неверное значение MORNING_CHANNEL_ID: %s", channel_id_str)
|
logger.warning("Неверное значение MORNING_CHANNEL_ID: %s", channel_id_str)
|
||||||
self.morning_loop = tasks.loop(seconds=1.0)(self._check_and_run_morning)
|
self._task: asyncio.Task | None = None
|
||||||
|
self._running = False
|
||||||
self._start_scheduler()
|
self._start_scheduler()
|
||||||
|
|
||||||
def _start_scheduler(self):
|
def _start_scheduler(self):
|
||||||
try:
|
if self._running:
|
||||||
self.morning_loop.start()
|
|
||||||
logger.info("Планировщик запущен (время: %s)", self.morning_time)
|
|
||||||
except RuntimeError:
|
|
||||||
logger.warning("Планировщик уже запущен")
|
logger.warning("Планировщик уже запущен")
|
||||||
|
return
|
||||||
|
self._running = True
|
||||||
|
self._task = asyncio.create_task(self._scheduler_loop())
|
||||||
|
logger.info("Планировщик запущен (время: %s)", self.morning_time)
|
||||||
|
|
||||||
def _stop_scheduler(self):
|
def _stop_scheduler(self):
|
||||||
try:
|
if self._task and not self._task.done():
|
||||||
self.morning_loop.stop()
|
self._task.cancel()
|
||||||
|
self._running = False
|
||||||
logger.info("Планировщик остановлен")
|
logger.info("Планировщик остановлен")
|
||||||
except RuntimeError:
|
|
||||||
logger.warning("Планировщик уже остановлен")
|
|
||||||
|
|
||||||
def _calculate_next_run(self) -> datetime:
|
def _calculate_next_run(self, now: datetime) -> datetime:
|
||||||
now = datetime.now()
|
"""Рассчитать время следующего запуска."""
|
||||||
hour, minute = map(int, self.morning_time.split(":"))
|
hour, minute = map(int, self.morning_time.split(":"))
|
||||||
today_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
today_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||||
|
|
||||||
@ -160,43 +165,65 @@ class Scheduler:
|
|||||||
return today_run + timedelta(days=1)
|
return today_run + timedelta(days=1)
|
||||||
return today_run
|
return today_run
|
||||||
|
|
||||||
async def _check_and_run_morning(self):
|
async def _scheduler_loop(self):
|
||||||
|
"""Бесконечный цикл: ждём целевое время -> запускаем morning -> повторяем."""
|
||||||
|
while self._running:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
target = self._calculate_next_run()
|
target = self._calculate_next_run(now)
|
||||||
|
sleep_seconds = (target - now).total_seconds()
|
||||||
|
|
||||||
if now >= target and now.day != self._last_run_date:
|
logger.info(
|
||||||
self._last_run_date = now.day
|
"Ожидание: target=%s, sleep=%.0f сек (%.0f мин)",
|
||||||
|
target.strftime("%Y-%m-%d %H:%M"),
|
||||||
|
sleep_seconds,
|
||||||
|
sleep_seconds / 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Спим до целевого времени
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(sleep_seconds)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверяем, что нужно запускать (не запускалось сегодня и бот ещё работает)
|
||||||
|
if self._running and datetime.now().date() != self._last_run_date:
|
||||||
|
logger.info("Срабатывание расписания: %s", self.morning_time)
|
||||||
await self._run_morning()
|
await self._run_morning()
|
||||||
|
else:
|
||||||
|
logger.info("Morning уже запускался сегодня, пропуск")
|
||||||
|
|
||||||
async def _run_morning(self):
|
async def _run_morning(self):
|
||||||
logger.info(f"Выполняю morning в {self.morning_time}")
|
logger.info("Выполняю morning в %s", self.morning_time)
|
||||||
|
|
||||||
# Если задан конкретный канал — отправляем туда
|
# Если задан конкретный канал — отправляем туда
|
||||||
if self._target_channel_id:
|
if self._target_channel_id:
|
||||||
channel = self.bot.get_channel(self._target_channel_id)
|
# fetch_channel — API-запрос, не зависит от кэша
|
||||||
|
channel = await self.bot.fetch_channel(self._target_channel_id)
|
||||||
if isinstance(channel, discord.TextChannel):
|
if isinstance(channel, discord.TextChannel):
|
||||||
if channel.permissions_for(channel.guild.me).send_messages:
|
if channel.permissions_for(channel.guild.me).send_messages:
|
||||||
try:
|
try:
|
||||||
await channel.send("🌅 Утренний дайджест!")
|
|
||||||
await run_morning(self.bot, channel)
|
await run_morning(self.bot, channel)
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Ошибка отправки в канал %s: %s", self._target_channel_id, e)
|
logger.error("Ошибка отправки в канал %s: %s", self._target_channel_id, e)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
logger.warning("Канал с ID %s не найден или не текстовый — fallback", self._target_channel_id)
|
logger.warning("Канал с ID %s не текстовый — fallback", self._target_channel_id)
|
||||||
|
|
||||||
# Fallback: первый канал с правами send_messages
|
# Fallback: первый канал с правами send_messages
|
||||||
|
sent = False
|
||||||
for channel in self.bot.get_all_channels():
|
for channel in self.bot.get_all_channels():
|
||||||
if isinstance(channel, discord.TextChannel):
|
if isinstance(channel, discord.TextChannel):
|
||||||
if channel.permissions_for(channel.guild.me).send_messages:
|
if channel.permissions_for(channel.guild.me).send_messages:
|
||||||
try:
|
try:
|
||||||
await channel.send("🌅 Утренний дайджест!")
|
|
||||||
await run_morning(self.bot, channel)
|
await run_morning(self.bot, channel)
|
||||||
|
sent = True
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Ошибка отправки в #%s: %s", channel.name, e)
|
logger.error("Ошибка отправки в #%s: %s", channel.name, e)
|
||||||
continue
|
continue
|
||||||
|
if not sent:
|
||||||
|
logger.error("Не удалось найти канал для отправки morning-дайджеста")
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self._start_scheduler()
|
self._start_scheduler()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user