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(
|
public async Task NotifyUsersNewsPublishedAsync(
|
||||||
string title,
|
string title,
|
||||||
|
string body,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -194,6 +195,9 @@ internal sealed class TelegramNotifier(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var text = $"📰 Новая новость: <b>{Escape(title)}</b>";
|
var text = $"📰 Новая новость: <b>{Escape(title)}</b>";
|
||||||
|
var preview = NewsPreview.FromMarkdown(body);
|
||||||
|
if (preview.Length > 0)
|
||||||
|
text += $"\n\n{Escape(preview)}";
|
||||||
|
|
||||||
InlineKeyboardMarkup? keyboard = null;
|
InlineKeyboardMarkup? keyboard = null;
|
||||||
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
|
if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl))
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ public sealed class CreatePostCommandHandler(
|
|||||||
post.CreatedAt,
|
post.CreatedAt,
|
||||||
cancellationToken
|
cancellationToken
|
||||||
);
|
);
|
||||||
await telegramNotifier.NotifyUsersNewsPublishedAsync(post.Title, cancellationToken);
|
await telegramNotifier.NotifyUsersNewsPublishedAsync(
|
||||||
|
post.Title,
|
||||||
|
post.Body,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
return Result.Success(NewsPostDto.FromDomain(post));
|
return Result.Success(NewsPostDto.FromDomain(post));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,13 @@ public interface ITelegramNotifier
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>Рассылка о публикации новости всем активированным пользователям с привязанным Telegram
|
/// <summary>Рассылка о публикации новости всем активированным пользователям с привязанным Telegram
|
||||||
/// (кнопка-ссылка на сайт, где новость показана целиком).</summary>
|
/// (заголовок + текстовое превью markdown-тела, длинное обрезается с «…»; кнопка-ссылка на сайт,
|
||||||
Task NotifyUsersNewsPublishedAsync(string title, CancellationToken cancellationToken);
|
/// где новость показана целиком).</summary>
|
||||||
|
Task NotifyUsersNewsPublishedAsync(
|
||||||
|
string title,
|
||||||
|
string body,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без
|
/// <summary>Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без
|
||||||
/// инлайн-действий (аналогично баг-репортам).</summary>
|
/// инлайн-действий (аналогично баг-репортам).</summary>
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ AppUser
|
|||||||
Нет статуса черновик/запланировано — публикация мгновенная. Нет видимости по ролям — доступно
|
Нет статуса черновик/запланировано — публикация мгновенная. Нет видимости по ролям — доступно
|
||||||
всем аутентифицированным пользователям. Realtime-оповещение о новом посте — `newsPublished`
|
всем аутентифицированным пользователям. Realtime-оповещение о новом посте — `newsPublished`
|
||||||
(SignalR, широковещательно всем подключенным клиентам), см. [architecture.md](architecture.md#realtime-signalr).
|
(SignalR, широковещательно всем подключенным клиентам), см. [architecture.md](architecture.md#realtime-signalr).
|
||||||
|
Плюс Telegram-рассылка активированным пользователям с привязкой: заголовок + текстовое превью тела
|
||||||
|
(markdown снят, картинки выкинуты, длиннее ~600 символов — обрезка по слову с «…») и кнопка на сайт.
|
||||||
|
|
||||||
### AuditLog — журнал действий
|
### AuditLog — журнал действий
|
||||||
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.
|
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.
|
||||||
|
|||||||
Reference in new issue
Block a user