Include news text preview in Telegram notification
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
94b02242ed
commit
0dcbd4e2b8
5 files changed
+100
-3
No files matched your search
@@ -0,0 +1,82 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PnvPanel.Api.Telegram;
|
||||
|
||||
/// <summary>Короткое текстовое превью markdown-тела новости для Telegram-оповещения: разметка
|
||||
/// снимается (картинки выкидываются, ссылки → текст ссылки), длинный текст обрезается по границе
|
||||
/// слова с «…». Полная новость — на сайте по кнопке.</summary>
|
||||
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();
|
||||
}
|
||||
@@ -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 = $"📰 Новая новость: <b>{Escape(title)}</b>";
|
||||
var preview = NewsPreview.FromMarkdown(body);
|
||||
if (preview.Length > 0)
|
||||
text += $"\n\n{Escape(preview)}";
|
||||
|
||||
InlineKeyboardMarkup? keyboard = null;
|
||||
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -33,8 +33,13 @@ public interface ITelegramNotifier
|
||||
);
|
||||
|
||||
/// <summary>Рассылка о публикации новости всем активированным пользователям с привязанным Telegram
|
||||
/// (кнопка-ссылка на сайт, где новость показана целиком).</summary>
|
||||
Task NotifyUsersNewsPublishedAsync(string title, CancellationToken cancellationToken);
|
||||
/// (заголовок + текстовое превью markdown-тела, длинное обрезается с «…»; кнопка-ссылка на сайт,
|
||||
/// где новость показана целиком).</summary>
|
||||
Task NotifyUsersNewsPublishedAsync(
|
||||
string title,
|
||||
string body,
|
||||
CancellationToken cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без
|
||||
/// инлайн-действий (аналогично баг-репортам).</summary>
|
||||
|
||||
@@ -281,6 +281,8 @@ AppUser
|
||||
Нет статуса черновик/запланировано — публикация мгновенная. Нет видимости по ролям — доступно
|
||||
всем аутентифицированным пользователям. Realtime-оповещение о новом посте — `newsPublished`
|
||||
(SignalR, широковещательно всем подключенным клиентам), см. [architecture.md](architecture.md#realtime-signalr).
|
||||
Плюс Telegram-рассылка активированным пользователям с привязкой: заголовок + текстовое превью тела
|
||||
(markdown снят, картинки выкинуты, длиннее ~600 символов — обрезка по слову с «…») и кнопка на сайт.
|
||||
|
||||
### AuditLog — журнал действий
|
||||
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.
|
||||
|
||||
Reference in new issue
Block a user