diff --git a/backend/src/PnvPanel.Api/Telegram/NewsPreview.cs b/backend/src/PnvPanel.Api/Telegram/NewsPreview.cs
new file mode 100644
index 0000000..97bf735
--- /dev/null
+++ b/backend/src/PnvPanel.Api/Telegram/NewsPreview.cs
@@ -0,0 +1,82 @@
+using System.Text.RegularExpressions;
+
+namespace PnvPanel.Api.Telegram;
+
+/// Короткое текстовое превью markdown-тела новости для Telegram-оповещения: разметка
+/// снимается (картинки выкидываются, ссылки → текст ссылки), длинный текст обрезается по границе
+/// слова с «…». Полная новость — на сайте по кнопке.
+internal static partial class NewsPreview
+{
+ public const int MaxLength = 600;
+
+ public static string FromMarkdown(string? markdown, int maxLength = MaxLength)
+ {
+ if (string.IsNullOrWhiteSpace(markdown))
+ return string.Empty;
+
+ var text = markdown.Replace("\r\n", "\n");
+ text = CodeFenceRegex().Replace(text, "");
+ text = ImageRegex().Replace(text, "");
+ text = LinkRegex().Replace(text, "$1");
+ text = HtmlTagRegex().Replace(text, "");
+ text = HeadingRegex().Replace(text, "");
+ text = QuoteRegex().Replace(text, "");
+ text = HorizontalRuleRegex().Replace(text, "");
+ text = ListBulletRegex().Replace(text, "• ");
+ text = EmphasisRegex().Replace(text, "$2");
+ text = InlineCodeRegex().Replace(text, "$1");
+ text = TrailingSpacesRegex().Replace(text, "\n");
+ text = ExtraBlankLinesRegex().Replace(text, "\n\n").Trim();
+
+ return Truncate(text, maxLength);
+ }
+
+ private static string Truncate(string text, int maxLength)
+ {
+ if (text.Length <= maxLength)
+ return text;
+
+ var cut = text[..maxLength];
+ var lastSpace = cut.LastIndexOfAny([' ', '\n']);
+ if (lastSpace > maxLength / 2)
+ cut = cut[..lastSpace];
+
+ return cut.TrimEnd(' ', '\n', ',', '.', ';', ':', '-', '—') + "…";
+ }
+
+ [GeneratedRegex(@"```.*?```", RegexOptions.Singleline)]
+ private static partial Regex CodeFenceRegex();
+
+ [GeneratedRegex(@"!\[[^\]]*\]\([^)]*\)")]
+ private static partial Regex ImageRegex();
+
+ [GeneratedRegex(@"\[([^\]]*)\]\([^)]*\)")]
+ private static partial Regex LinkRegex();
+
+ [GeneratedRegex(@"<[^>]+>")]
+ private static partial Regex HtmlTagRegex();
+
+ [GeneratedRegex(@"^\s{0,3}#{1,6}\s*", RegexOptions.Multiline)]
+ private static partial Regex HeadingRegex();
+
+ [GeneratedRegex(@"^\s{0,3}>\s?", RegexOptions.Multiline)]
+ private static partial Regex QuoteRegex();
+
+ [GeneratedRegex(@"^\s{0,3}([-*_])(\s*\1){2,}\s*$", RegexOptions.Multiline)]
+ private static partial Regex HorizontalRuleRegex();
+
+ [GeneratedRegex(@"^\s{0,3}[-*+]\s+", RegexOptions.Multiline)]
+ private static partial Regex ListBulletRegex();
+
+ [GeneratedRegex(@"(\*\*|__|\*|_|~~)(?=\S)(.+?)(?<=\S)\1")]
+ private static partial Regex EmphasisRegex();
+
+ [GeneratedRegex(@"`([^`]*)`")]
+ private static partial Regex InlineCodeRegex();
+
+ [GeneratedRegex(@"[ \t]+\n")]
+ private static partial Regex TrailingSpacesRegex();
+
+ [GeneratedRegex(@"\n{3,}")]
+ private static partial Regex ExtraBlankLinesRegex();
+}
diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs
index f776ad3..94b4790 100644
--- a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs
+++ b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs
@@ -187,6 +187,7 @@ internal sealed class TelegramNotifier(
public async Task NotifyUsersNewsPublishedAsync(
string title,
+ string body,
CancellationToken cancellationToken
)
{
@@ -194,6 +195,9 @@ internal sealed class TelegramNotifier(
return;
var text = $"📰 Новая новость: {Escape(title)}";
+ var preview = NewsPreview.FromMarkdown(body);
+ if (preview.Length > 0)
+ text += $"\n\n{Escape(preview)}";
InlineKeyboardMarkup? keyboard = null;
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
diff --git a/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs
index f428d0f..3fb0eb6 100644
--- a/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs
+++ b/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs
@@ -26,7 +26,11 @@ public sealed class CreatePostCommandHandler(
post.CreatedAt,
cancellationToken
);
- await telegramNotifier.NotifyUsersNewsPublishedAsync(post.Title, cancellationToken);
+ await telegramNotifier.NotifyUsersNewsPublishedAsync(
+ post.Title,
+ post.Body,
+ cancellationToken
+ );
return Result.Success(NewsPostDto.FromDomain(post));
}
diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs
index 9a06712..2e3b183 100644
--- a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs
+++ b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs
@@ -33,8 +33,13 @@ public interface ITelegramNotifier
);
/// Рассылка о публикации новости всем активированным пользователям с привязанным Telegram
- /// (кнопка-ссылка на сайт, где новость показана целиком).
- Task NotifyUsersNewsPublishedAsync(string title, CancellationToken cancellationToken);
+ /// (заголовок + текстовое превью markdown-тела, длинное обрезается с «…»; кнопка-ссылка на сайт,
+ /// где новость показана целиком).
+ Task NotifyUsersNewsPublishedAsync(
+ string title,
+ string body,
+ CancellationToken cancellationToken
+ );
/// Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без
/// инлайн-действий (аналогично баг-репортам).
diff --git a/docs/domain-model.md b/docs/domain-model.md
index 4adc9b6..7a49fb8 100644
--- a/docs/domain-model.md
+++ b/docs/domain-model.md
@@ -281,6 +281,8 @@ AppUser
Нет статуса черновик/запланировано — публикация мгновенная. Нет видимости по ролям — доступно
всем аутентифицированным пользователям. Realtime-оповещение о новом посте — `newsPublished`
(SignalR, широковещательно всем подключенным клиентам), см. [architecture.md](architecture.md#realtime-signalr).
+Плюс Telegram-рассылка активированным пользователям с привязкой: заголовок + текстовое превью тела
+(markdown снят, картинки выкинуты, длиннее ~600 символов — обрезка по слову с «…») и кнопка на сайт.
### AuditLog — журнал действий
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.