From 972a80815202e7dfdd50548dd9c9b221877cfe84 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Wed, 23 Sep 2026 19:51:42 +0800 Subject: [PATCH] feat(telegram): post calendar, repair and Minecraft messages --- .../ClubAffairsReminder.cs | 30 +++- .../ReminderTask.cs | 70 +++++++-- .../HuaJiBot.NET.Plugin.MessageBridge.csproj | 4 + .../MinecraftPosts.cs | 28 ++++ .../PluginMain.cs | 17 ++- .../PluginMain.cs | 47 +++++- .../TicketDigest.cs | 33 ++++- .../TicketReminder.cs | 8 +- .../ClubAffairsReminderTest.cs | 15 ++ src/HuaJiBot.NET.UnitTest/Doubles.cs | 12 +- src/HuaJiBot.NET.UnitTest/PluginPostTest.cs | 136 ++++++++++++++++++ src/HuaJiBot.NET/Bot/BotService.cs | 3 + 12 files changed, 368 insertions(+), 35 deletions(-) create mode 100644 src/HuaJiBot.NET.Plugin.MessageBridge/MinecraftPosts.cs create mode 100644 src/HuaJiBot.NET.UnitTest/PluginPostTest.cs diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs b/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs index f98f407..081587b 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs @@ -157,7 +157,13 @@ internal bool SendWeeklySummary(DateTimeOffset now) continue; _ = Service.TrySendGroupMessageAsync( group.GroupId, - BuildWeeklySummaryMessage(now, weekEnd, groupEvents) + new Post( + $"📢 **社团事务一周预告**({now:MM月dd日}~{weekEnd:MM月dd日})\n\n{Items(groupEvents, true)}", + BuildWeeklySummaryMessage(now, weekEnd, groupEvents) + ) + { + Tags = ["日程", "周预告"], + } ); Service.Log($"[社团事务] 已向群组 {group.GroupId} 发送每周汇总"); } @@ -187,13 +193,33 @@ internal bool SendDailyReminder(DateTimeOffset now) continue; _ = Service.TrySendGroupMessageAsync( group.GroupId, - BuildDailyReminderMessage(tomorrow, groupEvents) + new Post( + $"⏰ **明天截止**({tomorrow:MM月dd日})\n\n{Items(groupEvents, false)}", + BuildDailyReminderMessage(tomorrow, groupEvents) + ) + { + Tags = ["日程", "临期"], + } ); Service.Log($"[社团事务] 已向群组 {group.GroupId} 发送每日提醒"); } return true; } + private string Items( + List<(CalendarExtensions.Period period, CalendarEvent e)> events, + bool withDate + ) => + string.Join( + "\n", + from x in events + let responsible = ExtractResponsible(x.e) + select "- " + + (withDate ? $"{x.period.StartTime:MM月dd日} " : "") + + $"**{Post.Escape(x.e.Summary ?? "未命名事务")}**" + + (string.IsNullOrEmpty(responsible) ? "" : $"(负责人:{Post.Escape(responsible)})") + ); + private string BuildWeeklySummaryMessage( DateTimeOffset weekStart, DateTimeOffset weekEnd, diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs index b3ad93a..694c60c 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs @@ -43,10 +43,10 @@ public void Start() private DateTimeOffset _scheduledTimeEnd = Utils.NetworkTime.Now; //截止到该时间点的日程已经在Task队列中列入计划了 - private void SendToMatchedGroups(CalendarEvent e, string text) + private void SendToMatchedGroups(CalendarEvent e, Post post) { foreach (var group in Config.ReminderGroups.Where(g => g.Matches(e))) - _ = Service.TrySendGroupMessageAsync(group.GroupId, text); + _ = Service.TrySendGroupMessageAsync(group.GroupId, post); } [MethodImpl(MethodImplOptions.Synchronized)] //防止多线程同时更新时间节点 @@ -74,12 +74,18 @@ private void InvokeCheck() ScheduleReminder( period.StartTime.AddMinutes(-RemindBeforeStartMinutes) - now, e, - $""" - 日程提醒({period.StartTime:MM-dd HH:mm}): - {e.Summary} {e.Location} - {e.Description} - 将于 {RemindBeforeStartMinutes} 分钟后开始 - """ + StartPost( + period.StartTime, + e.End is null ? null : period.EndTime, + e, + RemindBeforeStartMinutes, + $""" + 日程提醒({period.StartTime:MM-dd HH:mm}): + {e.Summary} {e.Location} + {e.Description} + 将于 {RemindBeforeStartMinutes} 分钟后开始 + """ + ) ); } @@ -92,11 +98,15 @@ private void InvokeCheck() ScheduleReminder( period.EndTime.AddMinutes(-RemindBeforeEndMinutes) - now, e, - $""" - 日程提醒({period.EndTime:MM-dd HH:mm}): - {e.Summary} {e.Location} - 预计于 {RemindBeforeEndMinutes} 分钟后结束 - """ + EndPost( + e, + RemindBeforeEndMinutes, + $""" + 日程提醒({period.EndTime:MM-dd HH:mm}): + {e.Summary} {e.Location} + 预计于 {RemindBeforeEndMinutes} 分钟后结束 + """ + ) ); } } @@ -106,7 +116,37 @@ private void InvokeCheck() } } - private void ScheduleReminder(TimeSpan waiting, CalendarEvent e, string text) + internal static Post StartPost( + DateTimeOffset start, + DateTimeOffset? end, + CalendarEvent e, + int minutes, + string fallback + ) + { + var when = end is { } x ? $"🕘 {start:MM-dd HH:mm}–{x:HH:mm}" : $"🕘 {start:MM-dd HH:mm}"; + if (!string.IsNullOrWhiteSpace(e.Location)) + when += $" · 📍 {Post.Escape(e.Location)}"; + var markdown = $"⏰ **{Post.Escape(e.Summary ?? "")}** {minutes} 分钟后开始\n{when}"; + if (!string.IsNullOrWhiteSpace(e.Description)) + markdown += + "\n\n" + + string.Join( + "\n", + from line in e.Description.ReplaceLineEndings("\n").Trim().Split('\n') + select "> " + Post.Escape(line) + ); + return new(markdown, fallback) { Tags = ["日程"] }; + } + + internal static Post EndPost(CalendarEvent e, int minutes, string fallback) => + new($"🔚 **{Post.Escape(e.Summary ?? "")}** 预计 {minutes} 分钟后结束", fallback) + { + Tags = ["日程"], + Silent = true, + }; + + private void ScheduleReminder(TimeSpan waiting, CalendarEvent e, Post post) { if (waiting < TimeSpan.Zero) return; @@ -114,7 +154,7 @@ private void ScheduleReminder(TimeSpan waiting, CalendarEvent e, string text) .ContinueWith(_ => { Service.Log($"[日程] 发送提醒:{e.Summary}"); - SendToMatchedGroups(e, text); + SendToMatchedGroups(e, post); }); Service.Log($"[日程] 计划发送提醒:{e.Summary} ({waiting.TotalMinutes:F1}分钟后发送)"); } diff --git a/src/HuaJiBot.NET.Plugin.MessageBridge/HuaJiBot.NET.Plugin.MessageBridge.csproj b/src/HuaJiBot.NET.Plugin.MessageBridge/HuaJiBot.NET.Plugin.MessageBridge.csproj index de057c7..826340b 100644 --- a/src/HuaJiBot.NET.Plugin.MessageBridge/HuaJiBot.NET.Plugin.MessageBridge.csproj +++ b/src/HuaJiBot.NET.Plugin.MessageBridge/HuaJiBot.NET.Plugin.MessageBridge.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/HuaJiBot.NET.Plugin.MessageBridge/MinecraftPosts.cs b/src/HuaJiBot.NET.Plugin.MessageBridge/MinecraftPosts.cs new file mode 100644 index 0000000..9d96ddd --- /dev/null +++ b/src/HuaJiBot.NET.Plugin.MessageBridge/MinecraftPosts.cs @@ -0,0 +1,28 @@ +using HuaJiBot.NET.Bot; + +namespace HuaJiBot.NET.Plugin.MessageBridge; + +internal static class MinecraftPosts +{ + internal static Post Chat(string player, string message, string fallback) => + new($"**{Post.Escape(player)}**: {Post.Escape(message)}", fallback); + + internal static Post Join(string player, string fallback) => + new($"➕ {Post.Escape(player)} 加入了服务器", fallback) { Silent = true }; + + internal static Post Quit(string player, string fallback) => + new($"➖ {Post.Escape(player)} 离开了服务器", fallback) { Silent = true }; + + internal static Post Death(string message, string fallback) => + new($"💀 {Post.Escape(message)}", fallback) { Silent = true }; + + internal static Post Advancement(string player, string name, string? description, string fallback) => + new( + $"🏆 **{Post.Escape(player)}** 完成了进度 **{Post.Escape(name)}**" + + (string.IsNullOrWhiteSpace(description) ? "" : $"\n*{Post.Escape(description)}*"), + fallback + ) + { + Silent = true, + }; +} diff --git a/src/HuaJiBot.NET.Plugin.MessageBridge/PluginMain.cs b/src/HuaJiBot.NET.Plugin.MessageBridge/PluginMain.cs index 6b2edbc..e114974 100644 --- a/src/HuaJiBot.NET.Plugin.MessageBridge/PluginMain.cs +++ b/src/HuaJiBot.NET.Plugin.MessageBridge/PluginMain.cs @@ -213,21 +213,21 @@ PluginConfig.ClientInfo clientInfo _ = SendGroupMessageAsync( clientInfo, ClientEventType.Chat, - $"[{senderName}] <{name}> {msg}" + MinecraftPosts.Chat(name, msg, $"[{senderName}] <{name}> {msg}") ); break; case PlayerJoinPacket { Data.PlayerName: var name }: _onlinePlayers[name] = await SendGroupMessageAsync( clientInfo, ClientEventType.JoinLeft, - $"[{senderName}] {name} 加入了服务器" + MinecraftPosts.Join(name, $"[{senderName}] {name} 加入了服务器") ); break; case PlayerQuitPacket { Data.PlayerName: var name }: _ = SendGroupMessageAsync( clientInfo, ClientEventType.JoinLeft, - $"[{senderName}] {name} 离开了服务器" + MinecraftPosts.Quit(name, $"[{senderName}] {name} 离开了服务器") ); if (_onlinePlayers.TryRemove(name, out var allMsg)) { @@ -242,7 +242,7 @@ PluginConfig.ClientInfo clientInfo _ = SendGroupMessageAsync( clientInfo, ClientEventType.PlayerDeath, - $"[{senderName}] {msg}" + MinecraftPosts.Death(msg, $"[{senderName}] {msg}") ); break; case PlayerAchievementPacket @@ -258,7 +258,12 @@ PluginConfig.ClientInfo clientInfo _ = SendGroupMessageAsync( clientInfo, ClientEventType.PlayerAchievement, - $"[{senderName}] {name} 完成了进度 {achievementName} ({string.Join(",", criteria)}){Environment.NewLine}{description}" + MinecraftPosts.Advancement( + name, + achievementName, + description, + $"[{senderName}] {name} 完成了进度 {achievementName} ({string.Join(",", criteria)}){Environment.NewLine}{description}" + ) ); break; case GetPlayerListRequestPacket: //do not reply @@ -279,7 +284,7 @@ PluginConfig.ClientInfo clientInfo private async Task<(string groupId, string[] msgId)[]> SendGroupMessageAsync( PluginConfig.ClientInfo clientInfo, ClientEventType eventType, - string message + Post message ) { List<(string, string[])> msgIds = []; diff --git a/src/HuaJiBot.NET.Plugin.RepairTeam/PluginMain.cs b/src/HuaJiBot.NET.Plugin.RepairTeam/PluginMain.cs index 688a6aa..af0bcfa 100644 --- a/src/HuaJiBot.NET.Plugin.RepairTeam/PluginMain.cs +++ b/src/HuaJiBot.NET.Plugin.RepairTeam/PluginMain.cs @@ -1,4 +1,5 @@ using System.Text; +using HuaJiBot.NET.Bot; using HuaJiBot.NET.Commands; using HuaJiBot.NET.Events; using HuaJiBot.NET.Interfaces; @@ -110,6 +111,48 @@ internal static string FormatEvent(LogEventEntity e) return s.ToString().TrimEnd(); } + private static string? StateTag(string? action) => + action switch + { + "create" => "新报修", + "accept" => "已接单", + "cancel" => "已取消", + "drop" => "已放弃", + "commit" or "alterCommit" => "待审核", + "reject" => "已退回", + "close" => "已结单", + "update" => "已更新", + _ => null, + }; + + internal static Post EventPost(LogEventEntity e) + { + var lines = new List { $"🛠 **#{e.EventId} {Post.Escape(ActionText(e.Action))}**" }; + var device = string.Join( + " · ", + new[] { e.Model, e.Problem }.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => Post.Escape(x!)) + ); + if (device.Length > 0) + lines.Add(device); + if (!string.IsNullOrWhiteSpace(e.Description)) + lines.Add( + "\n" + + string.Join( + "\n", + from line in e.Description.ReplaceLineEndings("\n").Trim().Split('\n') + select "> " + Post.Escape(line) + ) + ); + var time = e.GmtCreate.ToOffset(NetworkTime.LocalTimeZoneOffset).ToString("MM-dd HH:mm"); + var member = string.IsNullOrWhiteSpace(e.MemberAlias) ? e.MemberId : e.MemberAlias; + lines.Add("\n" + (string.IsNullOrWhiteSpace(member) ? time : $"{Post.Escape(member)} · {time}")); + return new(string.Join("\n", lines), FormatEvent(e)) + { + Tags = StateTag(e.Action) is { } tag ? ["维修", tag] : ["维修"], + Links = [new("去处理", TicketDigest.PortalUrl)], + }; + } + private void OnMessageReceived(object? sender, string msg) { Service.Log(msg); @@ -127,9 +170,9 @@ private void OnMessageReceived(object? sender, string msg) Service.LogError("[RepairTeam] 无法解析维修事件", ex); return; } - var str = FormatEvent(e); + var post = EventPost(e); foreach (var group in Config.PushInfoGroup) - _ = Service.TrySendGroupMessageAsync(group, str); + _ = Service.TrySendGroupMessageAsync(group, post); } } diff --git a/src/HuaJiBot.NET.Plugin.RepairTeam/TicketDigest.cs b/src/HuaJiBot.NET.Plugin.RepairTeam/TicketDigest.cs index 2f418dd..2acb111 100644 --- a/src/HuaJiBot.NET.Plugin.RepairTeam/TicketDigest.cs +++ b/src/HuaJiBot.NET.Plugin.RepairTeam/TicketDigest.cs @@ -1,4 +1,5 @@ using System.Text; +using HuaJiBot.NET.Bot; namespace HuaJiBot.NET.Plugin.RepairTeam; @@ -25,6 +26,32 @@ private static double ThresholdDays(string status, PluginConfig config) => public static List Stalled(IEnumerable tickets, DateTimeOffset now, PluginConfig config) => tickets.Where(t => (now - t.LastActivity).TotalDays > ThresholdDays(t.Status, config)).ToList(); + public static Post ToPost(IReadOnlyCollection tickets, DateTimeOffset now, string fallback) + { + var sb = new StringBuilder($"🛠 **{tickets.Count} 张工单卡住了**"); + foreach (var (status, stageTitle) in Stages) + { + var stage = tickets.Where(t => t.Status == status).OrderBy(t => t.LastActivity).ToList(); + if (stage.Count == 0) + continue; + sb.Append($"\n\n**{stageTitle}**"); + foreach (var t in stage) + sb.Append($"\n• #{t.Id} ").AppendJoin(" · ", Fields(t, now).Select(Post.Escape)); + } + return new(sb.ToString(), fallback) + { + Tags = ["维修", "提醒"], + Links = [new("去处理", PortalUrl)], + }; + } + + private static IEnumerable Fields(Ticket t, DateTimeOffset now) + { + var days = (int)(now - t.LastActivity).TotalDays; + var fields = new[] { t.Model, t.Problem, t.Member, days < 1 ? "不到 1 天" : $"已 {days} 天" }; + return fields.Where(f => !string.IsNullOrWhiteSpace(f))!; + } + public static string Format(string title, IReadOnlyCollection tickets, DateTimeOffset now) { var sb = new StringBuilder(title).Append('\n'); @@ -35,11 +62,7 @@ public static string Format(string title, IReadOnlyCollection tickets, D continue; sb.Append(stageTitle).Append(":\n"); foreach (var t in stage) - { - var days = (int)(now - t.LastActivity).TotalDays; - var fields = new[] { t.Model, t.Problem, t.Member, days < 1 ? "不到 1 天" : $"已 {days} 天" }; - sb.Append($" #{t.Id} ").AppendJoin(" · ", fields.Where(f => !string.IsNullOrWhiteSpace(f))).Append('\n'); - } + sb.Append($" #{t.Id} ").AppendJoin(" · ", Fields(t, now)).Append('\n'); } return sb.Append($"去处理:{PortalUrl}").ToString(); } diff --git a/src/HuaJiBot.NET.Plugin.RepairTeam/TicketReminder.cs b/src/HuaJiBot.NET.Plugin.RepairTeam/TicketReminder.cs index a7e5519..f27a0b4 100644 --- a/src/HuaJiBot.NET.Plugin.RepairTeam/TicketReminder.cs +++ b/src/HuaJiBot.NET.Plugin.RepairTeam/TicketReminder.cs @@ -69,9 +69,13 @@ public async Task SendDigestAsync(DateTimeOffset now) var stalled = TicketDigest.Stalled(tickets, now, _config); if (stalled.Count == 0) return true; - var text = TicketDigest.Format($"【维修工单提醒】{stalled.Count} 张工单卡住了", stalled, now); + var post = TicketDigest.ToPost( + stalled, + now, + TicketDigest.Format($"【维修工单提醒】{stalled.Count} 张工单卡住了", stalled, now) + ); foreach (var group in _config.RemindGroups) - await _service.TrySendGroupMessageAsync(group, text); + await _service.TrySendGroupMessageAsync(group, post); return true; } diff --git a/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs b/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs index 059f483..245f4f5 100644 --- a/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs +++ b/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs @@ -42,6 +42,21 @@ private static string SentTo(RecordingAdapter adapter, string group) => adapter.Sends.Where(x => x.Target == group).SelectMany(x => x.Messages).OfType().Select(x => x.Text) ); + [Test] + public void WeeklySummary_PostsTaggedListOnTelegram() + { + var (adapter, reminder) = Setup(); + + reminder.SendWeeklySummary(Now); + + var post = adapter.Posts.First(x => x.Markdown.Contains("招新宣讲") && !x.Markdown.Contains("例会筹备")); + Assert.Multiple(() => + { + Assert.That(post.Markdown, Is.EqualTo("📢 **社团事务一周预告**(09月21日~09月28日)\n\n- 09月22日 **招新宣讲**")); + Assert.That(post.Tags, Is.EqualTo(new[] { "日程", "周预告" })); + }); + } + [Test] public void WeeklySummary_SendsEachGroupOnlyItsOwnEvents() { diff --git a/src/HuaJiBot.NET.UnitTest/Doubles.cs b/src/HuaJiBot.NET.UnitTest/Doubles.cs index bea5c5a..4ced28a 100644 --- a/src/HuaJiBot.NET.UnitTest/Doubles.cs +++ b/src/HuaJiBot.NET.UnitTest/Doubles.cs @@ -4,16 +4,22 @@ namespace HuaJiBot.NET.UnitTest; internal sealed class RecordingAdapter : TestAdapter { + /// What a non-Telegram adapter sends: posts replaced by their fallback. public readonly List<(string Target, SendingMessageBase[] Messages)> Sends = []; + public readonly List Posts = []; - public override Task SendGroupMessageAsync( + public override async Task SendGroupMessageAsync( string? robotId, string targetGroup, params SendingMessageBase[] messages ) { - Sends.Add((targetGroup, messages)); - return Task.FromResult(["42"]); + lock (Posts) + Posts.AddRange(messages.OfType()); + var sent = await messages.ExpandPostsAsync(); + lock (Sends) + Sends.Add((targetGroup, sent)); + return ["42"]; } } diff --git a/src/HuaJiBot.NET.UnitTest/PluginPostTest.cs b/src/HuaJiBot.NET.UnitTest/PluginPostTest.cs new file mode 100644 index 0000000..20215ee --- /dev/null +++ b/src/HuaJiBot.NET.UnitTest/PluginPostTest.cs @@ -0,0 +1,136 @@ +using HuaJiBot.NET.Bot; +using HuaJiBot.NET.Plugin.Calendar; +using HuaJiBot.NET.Plugin.MessageBridge; +using HuaJiBot.NET.Plugin.RepairTeam; +using Ical.Net.CalendarComponents; +using Newtonsoft.Json; +using RepairPlugin = HuaJiBot.NET.Plugin.RepairTeam.PluginMain; + +namespace HuaJiBot.NET.UnitTest; + +internal class PluginPostTest +{ + private static readonly DateTimeOffset Start = new(2026, 9, 25, 20, 30, 0, TimeSpan.FromHours(8)); + + [Test] + public async Task CalendarStart_ShowsTitleTimePlaceAndQuotedDescription() + { + var e = new CalendarEvent { Summary = "NWDC_x", Location = "实验室", Description = "带上电脑\n别迟到" }; + + var post = ReminderTask.StartPost(Start, Start.AddHours(2), e, 60, "old"); + var fallback = await post.Fallback(); + + Assert.Multiple(() => + { + Assert.That( + post.Markdown, + Is.EqualTo("⏰ **NWDC\\_x** 60 分钟后开始\n🕘 09-25 20:30–22:30 · 📍 实验室\n\n> 带上电脑\n> 别迟到") + ); + Assert.That(post.Tags, Is.EqualTo(new[] { "日程" })); + Assert.That(post.Silent, Is.False); + Assert.That(fallback, Is.EqualTo(new SendingMessageBase[] { new TextMessage("old") })); + }); + } + + [Test] + public void CalendarEnd_IsSilent() + { + var post = ReminderTask.EndPost(new CalendarEvent { Summary = "例会" }, 5, "old"); + + Assert.Multiple(() => + { + Assert.That(post.Markdown, Is.EqualTo("🔚 **例会** 预计 5 分钟后结束")); + Assert.That(post.Silent, Is.True); + }); + } + + [Test] + public async Task RepairEvent_ShowsTicketAndLinksPortal() + { + var e = JsonConvert.DeserializeObject( + """ + {"event_id":900001,"action":"create","member_id":"","member_alias":"张三", + "gmt_create":"2026-09-21T03:45:53Z","model":"ThinkPad","problem":"无法开机","description":"进水了"} + """ + )!; + + var post = RepairPlugin.EventPost(e); + var fallback = await post.Fallback(); + + Assert.Multiple(() => + { + Assert.That( + post.Markdown, + Is.EqualTo("🛠 **#900001 新报修**\nThinkPad · 无法开机\n\n> 进水了\n\n张三 · 09-21 11:45") + ); + Assert.That(post.Tags, Is.EqualTo(new[] { "维修", "新报修" })); + Assert.That(post.Links, Is.EqualTo(new[] { new LinkMessage("去处理", TicketDigest.PortalUrl) })); + Assert.That(fallback, Is.EqualTo(new SendingMessageBase[] { new TextMessage(RepairPlugin.FormatEvent(e)) })); + }); + } + + [Test] + public void TicketDigest_ListsStagesAsBulletLists() + { + var now = new DateTimeOffset(2026, 9, 23, 12, 0, 0, TimeSpan.FromHours(8)); + Ticket[] tickets = + [ + new(12, "open", "MacBook", "屏幕闪", null, now.AddDays(-5), now.AddDays(-3)), + new(13, "committed", null, "蓝屏", "李四", now.AddDays(-5), now.AddHours(-30)), + ]; + + var post = TicketDigest.ToPost(tickets, now, "old"); + + Assert.Multiple(() => + { + Assert.That( + post.Markdown, + Is.EqualTo( + "🛠 **2 张工单卡住了**\n\n**待接单**\n• #12 MacBook · 屏幕闪 · 已 3 天\n\n**待审核**\n• #13 蓝屏 · 李四 · 已 1 天" + ) + ); + Assert.That(post.Tags, Is.EqualTo(new[] { "维修", "提醒" })); + Assert.That(post.Links.Single().Url, Is.EqualTo(TicketDigest.PortalUrl)); + }); + } + + [Test] + public void MinecraftChat_BoldsPlayerAndIsNotSilent() + { + var post = MinecraftPosts.Chat("Steve_1", "hi *all*", "old"); + + Assert.Multiple(() => + { + Assert.That(post.Markdown, Is.EqualTo("**Steve\\_1**: hi \\*all\\*")); + Assert.That(post.Silent, Is.False); + Assert.That(post.Tags, Is.Empty); + }); + } + + [TestCase("join", "➕ Steve 加入了服务器")] + [TestCase("quit", "➖ Steve 离开了服务器")] + public void MinecraftJoinQuit_AreSilent(string kind, string markdown) + { + var post = kind == "join" ? MinecraftPosts.Join("Steve", "old") : MinecraftPosts.Quit("Steve", "old"); + + Assert.Multiple(() => + { + Assert.That(post.Markdown, Is.EqualTo(markdown)); + Assert.That(post.Silent, Is.True); + }); + } + + [Test] + public void MinecraftDeathAndAdvancement_AreSilent() + { + var death = MinecraftPosts.Death("Steve was slain", "old"); + var advancement = MinecraftPosts.Advancement("Steve", "Stone Age", "Mine stone", "old"); + + Assert.Multiple(() => + { + Assert.That(death.Markdown, Is.EqualTo("💀 Steve was slain")); + Assert.That(advancement.Markdown, Is.EqualTo("🏆 **Steve** 完成了进度 **Stone Age**\n*Mine stone*")); + Assert.That(death.Silent && advancement.Silent, Is.True); + }); + } +} diff --git a/src/HuaJiBot.NET/Bot/BotService.cs b/src/HuaJiBot.NET/Bot/BotService.cs index 1dfca29..86aa43a 100644 --- a/src/HuaJiBot.NET/Bot/BotService.cs +++ b/src/HuaJiBot.NET/Bot/BotService.cs @@ -31,6 +31,9 @@ public sealed record LinkMessage(string Text, string Url) : SendingMessageBase; public sealed record Post(string Markdown, Func> Fallback) : SendingMessageBase { + public Post(string markdown, params SendingMessageBase[] fallback) + : this(markdown, () => Task.FromResult(fallback)) { } + public Func>? Image { get; init; } public string[] Tags { get; init; } = []; public LinkMessage[] Links { get; init; } = [];