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