| | | 1 | | using System.Net; |
| | | 2 | | using System.Text; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices.TelegramBotApi; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Собирает HTML-сообщение построчно. Весь пользовательский текст экранируется, длина режется по границе строки, |
| | | 8 | | /// чтобы не разорвать открытый тег. |
| | | 9 | | /// </summary> |
| | | 10 | | public sealed class TelegramMessageBuilder |
| | | 11 | | { |
| | | 12 | | /// <summary>Лимит Bot API на текст сообщения — 4096; оставляем запас под маркер обрезки.</summary> |
| | | 13 | | public const int MaxLength = 4000; |
| | | 14 | | public const int MaxCaptionLength = 1000; |
| | | 15 | | |
| | | 16 | | private const string TruncatedMark = "… <i>(обрезано)</i>"; |
| | 74 | 17 | | private readonly List<string> _lines = []; |
| | | 18 | | |
| | 2531 | 19 | | public static string Encode(string? text) => WebUtility.HtmlEncode(text ?? ""); |
| | | 20 | | |
| | | 21 | | /// <summary>Жирный заголовок.</summary> |
| | | 22 | | public TelegramMessageBuilder Title(string text, string? icon = null) |
| | | 23 | | { |
| | 84 | 24 | | _lines.Add($"{(icon == null ? "" : icon + " ")}<b>{Encode(text)}</b>"); |
| | 84 | 25 | | return this; |
| | | 26 | | } |
| | | 27 | | |
| | | 28 | | public TelegramMessageBuilder Line(string? text) |
| | | 29 | | { |
| | 1038 | 30 | | if (!string.IsNullOrWhiteSpace(text)) _lines.Add(Encode(text)); |
| | 519 | 31 | | return this; |
| | | 32 | | } |
| | | 33 | | |
| | | 34 | | public TelegramMessageBuilder Lines(IEnumerable<string>? lines, string bullet = "• ") |
| | | 35 | | { |
| | 18 | 36 | | foreach (var line in lines ?? []) |
| | 4 | 37 | | if (!string.IsNullOrWhiteSpace(line)) _lines.Add(bullet + Encode(line)); |
| | 7 | 38 | | return this; |
| | | 39 | | } |
| | | 40 | | |
| | | 41 | | /// <summary>Строка «Метка: значение»; пустые значения пропускаются.</summary> |
| | | 42 | | public TelegramMessageBuilder Field(string label, string? value, bool code = false) |
| | | 43 | | { |
| | 132 | 44 | | if (string.IsNullOrWhiteSpace(value)) return this; |
| | 130 | 45 | | _lines.Add(code ? $"{Encode(label)}: <code>{Encode(value)}</code>" : $"{Encode(label)}: {Encode(value)}"); |
| | 130 | 46 | | return this; |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | public TelegramMessageBuilder Link(string text, string url) |
| | | 50 | | { |
| | 53 | 51 | | _lines.Add($"<a href=\"{WebUtility.HtmlEncode(url)}\">{Encode(text)}</a>"); |
| | 53 | 52 | | return this; |
| | | 53 | | } |
| | | 54 | | |
| | | 55 | | /// <summary>Моноширинный блок (стектрейсы, JSON).</summary> |
| | | 56 | | public TelegramMessageBuilder Pre(string? text) |
| | | 57 | | { |
| | 53 | 58 | | if (!string.IsNullOrWhiteSpace(text)) _lines.Add($"<pre>{Encode(text)}</pre>"); |
| | 27 | 59 | | return this; |
| | | 60 | | } |
| | | 61 | | |
| | | 62 | | /// <summary>Готовый HTML-блок (может быть многострочным); экранирование — на вызывающем.</summary> |
| | | 63 | | public TelegramMessageBuilder Raw(string html) |
| | | 64 | | { |
| | 282 | 65 | | _lines.Add(html); |
| | 282 | 66 | | return this; |
| | | 67 | | } |
| | | 68 | | |
| | | 69 | | /// <summary> |
| | | 70 | | /// Разбивает содержимое на сообщения не длиннее <paramref name="maxLength"/> по границам блоков. |
| | | 71 | | /// Блок, который сам не влезает, обрезается. |
| | | 72 | | /// </summary> |
| | | 73 | | public IReadOnlyList<string> BuildChunks(int maxLength = MaxLength) |
| | | 74 | | { |
| | 38 | 75 | | var chunks = new List<string>(); |
| | 38 | 76 | | var sb = new StringBuilder(); |
| | 854 | 77 | | foreach (var raw in _lines) |
| | | 78 | | { |
| | 389 | 79 | | var block = raw.Length > maxLength ? raw[..(maxLength - TruncatedMark.Length - 1)] + TruncatedMark : raw; |
| | 389 | 80 | | if (sb.Length > 0 && sb.Length + 1 + block.Length > maxLength) |
| | | 81 | | { |
| | 10 | 82 | | chunks.Add(sb.ToString()); |
| | 10 | 83 | | sb.Clear(); |
| | | 84 | | } |
| | | 85 | | |
| | 730 | 86 | | if (sb.Length > 0) sb.Append('\n'); |
| | 389 | 87 | | sb.Append(block); |
| | | 88 | | } |
| | | 89 | | |
| | 76 | 90 | | if (sb.Length > 0) chunks.Add(sb.ToString()); |
| | 38 | 91 | | return chunks; |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | public TelegramMessageBuilder Blank() |
| | | 95 | | { |
| | 94 | 96 | | _lines.Add(""); |
| | 94 | 97 | | return this; |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | public string Build(int maxLength = MaxLength) |
| | | 101 | | { |
| | 36 | 102 | | var sb = new StringBuilder(); |
| | 830 | 103 | | for (var i = 0; i < _lines.Count; i++) |
| | | 104 | | { |
| | 380 | 105 | | var line = _lines[i]; |
| | 380 | 106 | | var extra = (sb.Length > 0 ? 1 : 0) + line.Length; |
| | 380 | 107 | | if (sb.Length + extra > maxLength - TruncatedMark.Length - 1) |
| | | 108 | | { |
| | | 109 | | // Единственная слишком длинная строка (например <pre>) целиком не помещается — режем нечем, оставляем м |
| | 2 | 110 | | if (sb.Length > 0) sb.Append('\n'); |
| | 1 | 111 | | sb.Append(TruncatedMark); |
| | 1 | 112 | | return sb.ToString(); |
| | | 113 | | } |
| | | 114 | | |
| | 722 | 115 | | if (sb.Length > 0) sb.Append('\n'); |
| | 379 | 116 | | sb.Append(line); |
| | | 117 | | } |
| | | 118 | | |
| | 35 | 119 | | return sb.ToString(); |
| | | 120 | | } |
| | | 121 | | } |