feat: !morning — заменить embed на plain text, кот отдельным сообщением
- Переименован format_weather_for_embed -> format_weather_for_message - Убран markdown жирный (**text**) из заголовков погоды и статей - Утренний дайджест отправляется как обычное текстовое сообщение - Котик отправляется отдельным сообщением перед дайджестом - Truncation через truncate_message (лимит 2000 символов) - Обновлены тесты: проверка 2 send() вместо embed
This commit is contained in:
parent
2197a0052d
commit
cef53197b1
@ -99,7 +99,7 @@ class TestFormatArticles:
|
||||
]
|
||||
result = format_articles(articles, "Заголовок", "https://habr.com/feed")
|
||||
assert len(result) == 3 # заголовок + 2 статьи
|
||||
assert result[0] == "**Заголовок**\n<https://habr.com/feed>"
|
||||
assert result[0] == "Заголовок\n<https://habr.com/feed>"
|
||||
assert result[1] == "Статья 1\n28.05.2026 <https://habr.com/1>"
|
||||
assert result[2] == "Статья 2\n29.05.2026 <https://habr.com/2>"
|
||||
|
||||
@ -122,7 +122,7 @@ class TestFormatArticles:
|
||||
def test_format_articles_empty_list(self) -> None:
|
||||
"""Пустой список должен вернуть только заголовок."""
|
||||
result = format_articles([], "Заголовок", "https://habr.com/feed")
|
||||
assert result == ["**Заголовок**\n<https://habr.com/feed>"]
|
||||
assert result == ["Заголовок\n<https://habr.com/feed>"]
|
||||
assert len(result) == 1
|
||||
|
||||
def test_format_articles_none(self) -> None:
|
||||
@ -143,7 +143,7 @@ class TestFormatArticles:
|
||||
]
|
||||
result = format_articles(articles, "Новости AI", "https://habr.com/ai")
|
||||
assert len(result) == 2
|
||||
assert result[0] == "**Новости AI**\n<https://habr.com/ai>"
|
||||
assert result[0] == "Новости AI\n<https://habr.com/ai>"
|
||||
assert result[1] == "Единственная статья\n28.05.2026 <https://habr.com/1>"
|
||||
|
||||
def test_format_articles_long_title_truncated(self) -> None:
|
||||
|
||||
@ -101,8 +101,8 @@ class TestRunMorning:
|
||||
"""Тесты run_morning."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_morning_sends_embed(self) -> None:
|
||||
"""run_morning должен отправлять embed в канал."""
|
||||
async def test_run_morning_sends_plain_message(self) -> None:
|
||||
"""run_morning должен отправлять plain text в канал."""
|
||||
bot = AsyncMock()
|
||||
channel = AsyncMock()
|
||||
channel.name = "test-channel"
|
||||
@ -153,22 +153,29 @@ class TestRunMorning:
|
||||
"utils.morning_runner.fetch_cat",
|
||||
new=AsyncMock(return_value="http://cat.jpg"),
|
||||
),
|
||||
patch("utils.morning_runner.discord.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
|
||||
# Два сообщения: кот + дайджест
|
||||
assert channel.send.call_count == 2
|
||||
|
||||
# Первое сообщение — URL кота
|
||||
first_call = channel.send.call_args_list[0]
|
||||
assert first_call[0][0] == "http://cat.jpg"
|
||||
|
||||
# Второе сообщение — текстовый дайджест
|
||||
second_call = channel.send.call_args_list[1]
|
||||
message_text = second_call[0][0]
|
||||
assert "Утренний дайджест" in message_text
|
||||
assert "Погода в Магнитогорске" in message_text
|
||||
|
||||
|
||||
class TestRunMorningWithFallback:
|
||||
"""Тесты fallback в пустом embed."""
|
||||
"""Тесты fallback в plain text."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_morning_empty_embed_fallback(self) -> None:
|
||||
"""run_morning должен добавлять fallback сообщение при пустых данных."""
|
||||
async def test_run_morning_empty_fallback(self) -> None:
|
||||
"""run_morning должен отправлять fallback сообщение при пустых данных."""
|
||||
bot = AsyncMock()
|
||||
channel = AsyncMock()
|
||||
channel.name = "test-channel"
|
||||
@ -182,21 +189,14 @@ class TestRunMorningWithFallback:
|
||||
),
|
||||
patch("utils.morning_runner.fetch_rss", new=AsyncMock(return_value=None)),
|
||||
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)),
|
||||
patch("utils.morning_runner.discord.Embed") as mock_embed_class,
|
||||
):
|
||||
embed_mock = AsyncMock()
|
||||
mock_embed_class.return_value = embed_mock
|
||||
|
||||
await run_morning(bot, channel)
|
||||
|
||||
# Убедимся, что send был вызван
|
||||
# Только одно сообщение (без кота) с fallback текстом
|
||||
channel.send.assert_called_once()
|
||||
call_args = channel.send.call_args[1]
|
||||
assert "embed" in call_args
|
||||
|
||||
# Проверяем, что description содержит fallback сообщение
|
||||
embed_description = call_args["embed"].description
|
||||
assert "Не удалось получить данные из внешних источников" in embed_description
|
||||
call_args = channel.send.call_args
|
||||
message_text = call_args[0][0]
|
||||
assert "Не удалось получить данные из внешних источников" in message_text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_morning_only_weather_data(self) -> None:
|
||||
@ -233,13 +233,13 @@ class TestRunMorningWithFallback:
|
||||
):
|
||||
await run_morning(bot, channel)
|
||||
|
||||
# Одно сообщение (погода + отсутствие новостей, без кота)
|
||||
channel.send.assert_called_once()
|
||||
call_args = channel.send.call_args[1]
|
||||
assert "embed" in call_args
|
||||
call_args = channel.send.call_args
|
||||
message_text = call_args[0][0]
|
||||
|
||||
# Проверяем, что в embed есть только погода и нет fallback сообщения
|
||||
embed_description = call_args["embed"].description
|
||||
assert "Погода в Магнитогорске" in embed_description
|
||||
# Проверяем, что в тексте есть погода и нет fallback сообщения
|
||||
assert "Погода в Магнитогорске" in message_text
|
||||
assert (
|
||||
"Не удалось получить данные из внешних источников" not in embed_description
|
||||
"Не удалось получить данные из внешних источников" not in message_text
|
||||
)
|
||||
|
||||
@ -3,7 +3,7 @@ from .pogoda import (
|
||||
fetch_weather,
|
||||
fetch_open_meteo,
|
||||
format_weather_data_for_console,
|
||||
format_weather_for_embed,
|
||||
format_weather_for_message,
|
||||
pressure_to_mmhg,
|
||||
translate_weather,
|
||||
wmo_to_russian,
|
||||
@ -23,7 +23,7 @@ __all__ = [
|
||||
"fetch_weather",
|
||||
"fetch_open_meteo",
|
||||
"format_weather_data_for_console",
|
||||
"format_weather_for_embed",
|
||||
"format_weather_for_message",
|
||||
"pressure_to_mmhg",
|
||||
"translate_weather",
|
||||
"wmo_to_russian",
|
||||
|
||||
@ -13,14 +13,14 @@ from discord.ext import commands
|
||||
from utils.pogoda import (
|
||||
API_URL_WEATHER,
|
||||
fetch_weather,
|
||||
format_weather_for_embed,
|
||||
format_weather_for_message,
|
||||
)
|
||||
from utils.news import (
|
||||
fetch_rss,
|
||||
format_articles,
|
||||
RSS_URL_ARTICLES,
|
||||
RSS_URL_POSTS,
|
||||
truncate_embed_text,
|
||||
truncate_message,
|
||||
)
|
||||
from utils.cat import fetch_cat
|
||||
|
||||
@ -54,29 +54,27 @@ async def gather_morning() -> MorningData:
|
||||
|
||||
|
||||
async def run_morning(bot: "commands.Bot", channel: discord.TextChannel) -> None:
|
||||
"""Выполнить утренний дайджест и отправить в канал."""
|
||||
"""Выполнить утренний дайджест и отправить в канал как plain text."""
|
||||
try:
|
||||
data = await gather_morning()
|
||||
|
||||
# --- Формируем embed ---
|
||||
embed = discord.Embed(title="Утренний дайджест", color=0xF4A460)
|
||||
|
||||
# Котик как thumbnail
|
||||
# --- Котик отдельным сообщением ---
|
||||
if data.cat_url:
|
||||
embed.set_thumbnail(url=data.cat_url)
|
||||
await channel.send(data.cat_url)
|
||||
|
||||
description_lines = []
|
||||
# --- Формируем plain text ---
|
||||
message_lines = ["Утренний дайджест", ""]
|
||||
has_real_data = False
|
||||
|
||||
# --- Погода ---
|
||||
weather_text = format_weather_for_embed(data.weather)
|
||||
weather_text = format_weather_for_message(data.weather)
|
||||
if weather_text:
|
||||
has_real_data = True
|
||||
description_lines.append(weather_text)
|
||||
message_lines.append(weather_text)
|
||||
else:
|
||||
description_lines.append("Не удалось получить данные о погоде.")
|
||||
message_lines.append("Не удалось получить данные о погоде.")
|
||||
|
||||
description_lines.append("")
|
||||
message_lines.append("")
|
||||
|
||||
# --- Новости: статьи ---
|
||||
if data.articles is not None:
|
||||
@ -87,13 +85,13 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel) -> None
|
||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/",
|
||||
)
|
||||
description_lines.append("\n".join(lines))
|
||||
message_lines.extend(lines)
|
||||
else:
|
||||
description_lines.append("Новостей пока нет.")
|
||||
message_lines.append("Новостей пока нет.")
|
||||
else:
|
||||
description_lines.append("Не удалось получить новости.")
|
||||
message_lines.append("Не удалось получить новости.")
|
||||
|
||||
description_lines.append("")
|
||||
message_lines.append("")
|
||||
|
||||
# --- Новости: посты ---
|
||||
if data.posts is not None:
|
||||
@ -104,22 +102,21 @@ async def run_morning(bot: "commands.Bot", channel: discord.TextChannel) -> None
|
||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/",
|
||||
)
|
||||
description_lines.append("\n".join(lines))
|
||||
message_lines.extend(lines)
|
||||
else:
|
||||
description_lines.append("Новостей пока нет.")
|
||||
message_lines.append("Новостей пока нет.")
|
||||
else:
|
||||
description_lines.append("Не удалось получить новости.")
|
||||
message_lines.append("Не удалось получить новости.")
|
||||
|
||||
# Fallback для пустых данных
|
||||
if not has_real_data:
|
||||
description_lines = [
|
||||
message_lines = [
|
||||
"Не удалось получить данные из внешних источников.",
|
||||
"Проверьте доступность API и повторите попытку позже.",
|
||||
]
|
||||
|
||||
description = "\n".join(description_lines)
|
||||
embed.description = truncate_embed_text(description)
|
||||
await channel.send(embed=embed)
|
||||
message = truncate_message("\n".join(message_lines))
|
||||
await channel.send(message)
|
||||
logger.info("Утренний дайджест отправлен в #%s", channel.name)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@ -123,7 +123,7 @@ def truncate_message(text: str, max_len: int = 2000) -> str:
|
||||
|
||||
def format_articles(articles: list[dict], title: str, link: str) -> list[str]:
|
||||
"""Сформировать список строк для вывода статей/постов."""
|
||||
lines = [f"**{title}**\n<{link}>"]
|
||||
lines = [f"{title}\n<{link}>"]
|
||||
for i, article in enumerate(articles[:5], 1):
|
||||
date_str = _parse_date(article["pub_date"])
|
||||
short_title = truncate_title(article["title"])
|
||||
|
||||
@ -215,14 +215,14 @@ def format_weather_data_for_console(data: Optional[dict]) -> Optional[list[str]]
|
||||
]
|
||||
|
||||
|
||||
def format_weather_for_embed(data: Optional[dict]) -> Optional[str]:
|
||||
"""Форматировать погоду для Discord embed (с заголовком)."""
|
||||
def format_weather_for_message(data: Optional[dict]) -> Optional[str]:
|
||||
"""Форматировать погоду для plain text сообщения (с заголовком)."""
|
||||
if data is None:
|
||||
return None
|
||||
lines = format_weather_data_for_console(data)
|
||||
if not lines:
|
||||
return None
|
||||
return "**Погода в Магнитогорске:**\n" + "\n".join(lines)
|
||||
return "Погода в Магнитогорске:\n" + "\n".join(lines)
|
||||
|
||||
|
||||
def pressure_to_mmhg(mb: Any) -> float | str:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user