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