diff --git a/csharp/Interfaces/IPrivateMessageTrigger.cs b/csharp/Interfaces/IPrivateMessageTrigger.cs new file mode 100644 index 00000000..37a86a33 --- /dev/null +++ b/csharp/Interfaces/IPrivateMessageTrigger.cs @@ -0,0 +1,61 @@ +using System.Threading.Tasks; + +namespace Interfaces +{ + /// + /// + /// Defines a trigger that supports private messaging to users instead of public issue comments. + /// + /// + /// + public interface IPrivateMessageTrigger : ITrigger + { + /// + /// + /// Gets the user login to send private message to. + /// + /// + /// + /// + /// The context. + /// + /// + /// + /// The user login string + /// + /// + string GetTargetUserLogin(TContext context); + + /// + /// + /// Gets the subject for the private message. + /// + /// + /// + /// + /// The context. + /// + /// + /// + /// The message subject + /// + /// + string GetMessageSubject(TContext context); + + /// + /// + /// Gets the private message content. + /// + /// + /// + /// + /// The context. + /// + /// + /// + /// The private message content + /// + /// + Task GetPrivateMessageContent(TContext context); + } +} \ No newline at end of file diff --git a/csharp/Platform.Bot/Trackers/IssueTracker.cs b/csharp/Platform.Bot/Trackers/IssueTracker.cs index 1c53a4c9..f64ec023 100644 --- a/csharp/Platform.Bot/Trackers/IssueTracker.cs +++ b/csharp/Platform.Bot/Trackers/IssueTracker.cs @@ -74,10 +74,27 @@ public async Task Start(CancellationToken cancellationToken) } if (await trigger.Condition(issue)) { - await trigger.Action(issue); + if (trigger is IPrivateMessageTrigger privateMessageTrigger) + { + await HandlePrivateMessageTrigger(issue, privateMessageTrigger); + } + else + { + await trigger.Action(issue); + } } } } } + + private async Task HandlePrivateMessageTrigger(Issue issue, IPrivateMessageTrigger trigger) + { + var targetUser = trigger.GetTargetUserLogin(issue); + var subject = trigger.GetMessageSubject(issue); + var messageContent = await trigger.GetPrivateMessageContent(issue); + + var privateMessageIssue = await _storage.SendPrivateMessage(targetUser, subject, messageContent); + await _storage.CreateMinimalIssueComment(issue.Repository.Id, issue.Number, targetUser, subject, privateMessageIssue); + } } } diff --git a/csharp/Platform.Bot/Triggers/LastCommitActivityTrigger.cs b/csharp/Platform.Bot/Triggers/LastCommitActivityTrigger.cs index 07fbe5c8..4fe32d6e 100644 --- a/csharp/Platform.Bot/Triggers/LastCommitActivityTrigger.cs +++ b/csharp/Platform.Bot/Triggers/LastCommitActivityTrigger.cs @@ -13,7 +13,7 @@ namespace Platform.Bot.Triggers { using TContext = Issue; - internal class LastCommitActivityTrigger : ITrigger + internal class LastCommitActivityTrigger : IPrivateMessageTrigger { private readonly GitHubStorage _githubStorage; @@ -25,6 +25,22 @@ public async Task Condition(TContext issue) } public async Task Action(TContext issue) + { + Console.WriteLine($"Issue {issue.Title} is processed: {issue.HtmlUrl}"); + await _githubStorage.Client.Issue.Update(issue.Repository.Owner.Login, issue.Repository.Name, issue.Number, new IssueUpdate() { State = ItemState.Closed }); + } + + public string GetTargetUserLogin(TContext issue) + { + return issue.User.Login; + } + + public string GetMessageSubject(TContext issue) + { + return "Last 3 Months Commit Activity Report"; + } + + public async Task GetPrivateMessageContent(TContext issue) { var organizationName = issue.Repository.Owner.Login; @@ -32,7 +48,7 @@ public async Task Action(TContext issue) var allRepositories = await _githubStorage.GetAllRepositories(organizationName); if (!allRepositories.Any()) { - return; + return "No repositories found in the organization."; } var commitsPerUserInLast3Months = await allRepositories @@ -54,16 +70,15 @@ public async Task Action(TContext issue) } return dictionary; }); + StringBuilder messageSb = new(); var ShortSummaryMessage = GetShortSummaryMessage(commitsPerUserInLast3Months.Select(pair => pair.Key).ToList()); messageSb.Append(ShortSummaryMessage); messageSb.AppendLine("---"); var detailedMessage = await GetDetailedMessage(commitsPerUserInLast3Months); messageSb.Append(detailedMessage); - var message = messageSb.ToString(); - await _githubStorage.CreateIssueComment(issue.Repository.Id, issue.Number, message); - Console.WriteLine($"Issue {issue.Title} is processed: {issue.HtmlUrl}"); - await _githubStorage.Client.Issue.Update(issue.Repository.Owner.Login, issue.Repository.Name, issue.Number, new IssueUpdate() { State = ItemState.Closed }); + + return messageSb.ToString(); } private string GetShortSummaryMessage(List users) diff --git a/csharp/Platform.Bot/Triggers/OrganizationLastMonthActivityTrigger.cs b/csharp/Platform.Bot/Triggers/OrganizationLastMonthActivityTrigger.cs index da82aa48..5d66e8ce 100644 --- a/csharp/Platform.Bot/Triggers/OrganizationLastMonthActivityTrigger.cs +++ b/csharp/Platform.Bot/Triggers/OrganizationLastMonthActivityTrigger.cs @@ -17,7 +17,7 @@ namespace Platform.Bot.Triggers /// /// /// - internal class OrganizationLastMonthActivityTrigger : ITrigger + internal class OrganizationLastMonthActivityTrigger : IPrivateMessageTrigger { private readonly GitHubStorage _storage; private readonly Parser _parser = new(); @@ -62,11 +62,24 @@ internal class OrganizationLastMonthActivityTrigger : ITrigger /// public async Task Action(TContext context) { - var issueService = _storage.Client.Issue; + _storage.CloseIssue(context); + } + + public string GetTargetUserLogin(TContext context) + { + return context.User.Login; + } + + public string GetMessageSubject(TContext context) + { + return "Organization Last Month Activity Report"; + } + + public async Task GetPrivateMessageContent(TContext context) + { var owner = context.Repository.Owner.Login; var activeUsersString = string.Join("\n", GetActiveUsers(GetIgnoredRepositories(_parser.Parse(context.Body)), owner)); - issueService.Comment.Create(owner, context.Repository.Name, context.Number, activeUsersString); - _storage.CloseIssue(context); + return $"# Organization Last Month Activity\n\nActive users in the last month:\n\n{activeUsersString}"; } /// diff --git a/csharp/Storage/RemoteStorage/GitHubStorage.cs b/csharp/Storage/RemoteStorage/GitHubStorage.cs index 888a7426..a10cb906 100644 --- a/csharp/Storage/RemoteStorage/GitHubStorage.cs +++ b/csharp/Storage/RemoteStorage/GitHubStorage.cs @@ -307,6 +307,33 @@ public Task CreateIssueComment(long repositoryId, int issueNumber, return Client.Issue.Comment.Create(repositoryId, issueNumber, message); } + public async Task SendPrivateMessage(string userLogin, string subject, string message, string botCommunicationRepoName = "bot-communications") + { + try + { + var repo = await Client.Repository.Get(Owner, botCommunicationRepoName); + var issueTitle = $"Message for @{userLogin}: {subject}"; + var issueBody = $"@{userLogin}\n\n{message}"; + + var newIssue = new NewIssue(issueTitle) + { + Body = issueBody + }; + + return await Client.Issue.Create(repo.Id, newIssue); + } + catch (NotFoundException) + { + throw new InvalidOperationException($"Bot communication repository '{botCommunicationRepoName}' not found. Please create this repository first."); + } + } + + public async Task CreateMinimalIssueComment(long repositoryId, int issueNumber, string userLogin, string subject, Issue privateMessageIssue) + { + var message = $"@{userLogin} Bot message sent privately: [{subject}]({privateMessageIssue.HtmlUrl})"; + return await Client.Issue.Comment.Create(repositoryId, issueNumber, message); + } + #endregion #region Branch diff --git a/examples/bot-communication-setup.md b/examples/bot-communication-setup.md new file mode 100644 index 00000000..4c922f37 --- /dev/null +++ b/examples/bot-communication-setup.md @@ -0,0 +1,50 @@ +# Bot Communication Setup + +This example shows how to set up the private messaging system for the bot. + +## Prerequisites + +1. Create a private repository called `bot-communications` in your organization +2. Make sure the bot has access to this repository + +## How it works + +When a trigger implements `IPrivateMessageTrigger` instead of `ITrigger`: + +1. The bot generates the message content privately +2. Creates an issue in the `bot-communications` repository with the message +3. Mentions the target user in that issue (so they get email notification) +4. Posts a minimal comment in the original issue with a link to the private message + +## Example Usage + +```csharp +// Old way - creates big public comments +internal class MyTrigger : ITrigger +{ + public async Task Action(Issue issue) + { + await _github.CreateIssueComment(issue.Repository.Id, issue.Number, "Very long message..."); + } +} + +// New way - sends private messages +internal class MyTrigger : IPrivateMessageTrigger +{ + public string GetTargetUserLogin(Issue issue) => issue.User.Login; + public string GetMessageSubject(Issue issue) => "Report Title"; + public async Task GetPrivateMessageContent(Issue issue) => "Very long message..."; + public async Task Action(Issue issue) + { + // Only handle issue closing, messaging is automatic + await _github.Client.Issue.Update(issue.Repository.Owner.Login, issue.Repository.Name, issue.Number, new IssueUpdate() { State = ItemState.Closed }); + } +} +``` + +## Benefits + +- ✅ Reduces clutter in main issue threads +- ✅ Users still get notified via email mentions +- ✅ Messages are preserved in a dedicated space +- ✅ Original issues stay clean and readable \ No newline at end of file