diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index ee01a08..00e3d35 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -16,7 +16,7 @@ jobs: dotnet-version: '10.x' - name: Run focused tests - run: dotnet test src/HuaJiBot.NET.UnitTest --filter 'FullyQualifiedName~DailySummaryTest|FullyQualifiedName~AsciiArtToolsTest|FullyQualifiedName~WebDataServiceTest|FullyQualifiedName~VisionInputTest|FullyQualifiedName~FlueBridgeTest' + run: dotnet test src/HuaJiBot.NET.UnitTest --filter 'FullyQualifiedName~DailySummaryTest|FullyQualifiedName~AsciiArtToolsTest|FullyQualifiedName~WebDataServiceTest|FullyQualifiedName~VisionInputTest|FullyQualifiedName~FlueBridgeTest|FullyQualifiedName~ClubAffairsReminderTest|FullyQualifiedName~CalendarOutputTest' format-check: runs-on: ubuntu-latest diff --git a/src/HuaJiBot.NET.Plugin.Calendar/CalendarExtensions.cs b/src/HuaJiBot.NET.Plugin.Calendar/CalendarExtensions.cs index 5c5e4c2..a1f0d05 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/CalendarExtensions.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/CalendarExtensions.cs @@ -9,6 +9,14 @@ namespace HuaJiBot.NET.Plugin.Calendar; internal static partial class CalendarExtensions { + internal const string SkipNotificationKeyword = "[skip notification]"; + + internal static bool ShouldSkipNotification(this CalendarEvent calendarEvent) => + calendarEvent.Description?.Contains( + SkipNotificationKeyword, + StringComparison.OrdinalIgnoreCase + ) == true; + /// /// Wraps an ical.net and converts its times to /// . The is required to diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.README.md b/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.README.md index c7c862e..74be05f 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.README.md +++ b/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.README.md @@ -40,6 +40,9 @@ —— 社团事务提醒机器人 🤖 ``` +### 3. 跳过通知 + `[skip notification]`(不区分大小写),系统不会发送该日程的开始提醒、结束提醒、每周汇总或每日临期提醒。日程查询仍会显示该日程。占位日程修复后,请从描述中移除该标记。 + ## 配置说明 在插件配置文件中,可以通过 `ClubAffairsReminder` 节点进行配置: diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs b/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs index 081587b..5bde338 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ClubAffairsReminder.cs @@ -149,7 +149,11 @@ internal bool SendWeeklySummary(DateTimeOffset now) return false; } var weekEnd = now.AddDays(7); - var events = Calendar.GetEvents(now, weekEnd).OrderBy(x => x.period.StartTime).ToList(); + var events = Calendar + .GetEvents(now, weekEnd) + .Where(x => !x.e.ShouldSkipNotification()) + .OrderBy(x => x.period.StartTime) + .ToList(); foreach (var group in Config.ReminderGroups) { var groupEvents = events.Where(x => group.Matches(x.e)).ToList(); @@ -183,7 +187,7 @@ internal bool SendDailyReminder(DateTimeOffset now) // Only events starting tomorrow, so a multi-day event is not repeated every day. var events = Calendar .GetEvents(tomorrow, dayAfterTomorrow) - .Where(x => x.period.StartTime >= tomorrow) + .Where(x => x.period.StartTime >= tomorrow && !x.e.ShouldSkipNotification()) .OrderBy(x => x.period.StartTime) .ToList(); foreach (var group in Config.ReminderGroups) diff --git a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs index 694c60c..f8f4b41 100644 --- a/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs +++ b/src/HuaJiBot.NET.Plugin.Calendar/ReminderTask.cs @@ -69,7 +69,12 @@ private void InvokeCheck() var remindEnd = end.AddMinutes(RemindBeforeStartMinutes); foreach (var (period, e) in Calendar.GetEvents(remindStart, remindEnd)) { - if (e.IsAllDay || period.StartTime < remindStart || period.StartTime > remindEnd) + if ( + e.IsAllDay + || e.ShouldSkipNotification() + || period.StartTime < remindStart + || period.StartTime > remindEnd + ) continue; ScheduleReminder( period.StartTime.AddMinutes(-RemindBeforeStartMinutes) - now, @@ -93,7 +98,13 @@ private void InvokeCheck() remindEnd = end.AddMinutes(RemindBeforeEndMinutes); foreach (var (period, e) in Calendar.GetEvents(remindStart, remindEnd)) { - if (e.IsAllDay || e.End is null || period.EndTime < remindStart || period.EndTime > remindEnd) + if ( + e.IsAllDay + || e.End is null + || e.ShouldSkipNotification() + || period.EndTime < remindStart + || period.EndTime > remindEnd + ) continue; ScheduleReminder( period.EndTime.AddMinutes(-RemindBeforeEndMinutes) - now, diff --git a/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs b/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs index 245f4f5..33cdac5 100644 --- a/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs +++ b/src/HuaJiBot.NET.UnitTest/ClubAffairsReminderTest.cs @@ -9,10 +9,16 @@ internal class ClubAffairsReminderTest { private static readonly DateTimeOffset Now = new(2026, 9, 21, 9, 0, 0, TimeSpan.FromHours(8)); - private static CalendarEvent Event(string summary, DateTimeOffset start, DateTimeOffset end) => + private static CalendarEvent Event( + string summary, + DateTimeOffset start, + DateTimeOffset end, + string? description = null + ) => new() { Summary = summary, + Description = description, Start = new CalDateTime(start.UtcDateTime, "UTC"), End = new CalDateTime(end.UtcDateTime, "UTC"), }; @@ -82,6 +88,51 @@ public void DailyReminder_SkipsEventsThatStartedEarlier() Assert.That(SentTo(adapter, "all"), Does.Contain("招新宣讲").And.Not.Contain("例会筹备")); } + [Test] + public void Reminders_SkipEventsMarkedWithSkipNotification() + { + var calendar = new Ical.Net.Calendar(); + calendar.Events.Add( + Event( + "NWDC (待定)", + Now.AddHours(25), + Now.AddHours(27), + "具体日期待确认\n[skip notification]" + ) + ); + calendar.Events.Add(Event("正式活动", Now.AddHours(26), Now.AddHours(28))); + var config = new PluginConfig + { + ReminderGroups = [new() { GroupId = "all", Mode = FilterMode.BlackList, Keywords = [] }], + }; + var adapter = new RecordingAdapter(); + var reminder = new ClubAffairsReminder(adapter, config, () => calendar); + + Assert.Multiple(() => + { + Assert.That(reminder.SendWeeklySummary(Now), Is.True); + Assert.That(reminder.SendDailyReminder(Now), Is.True); + Assert.That(SentTo(adapter, "all"), Does.Contain("正式活动").And.Not.Contain("NWDC")); + }); + } + + [TestCase("[skip notification]")] + [TestCase("Details\n[SKIP NOTIFICATION]\n")] + public void SkipNotificationKeyword_IsCaseInsensitive(string description) + { + var calendarEvent = Event("Placeholder", Now, Now.AddHours(1), description); + + Assert.That(calendarEvent.ShouldSkipNotification(), Is.True); + } + + [Test] + public void SkipNotificationKeyword_DoesNotHideOtherEvents() + { + var calendarEvent = Event("Real event", Now, Now.AddHours(1), "No notification marker"); + + Assert.That(calendarEvent.ShouldSkipNotification(), Is.False); + } + [Test] public void Reminders_WithoutCalendar_AreRetriedLater() {