Skip to content

Commit b05ff45

Browse files
konardclaude
andcommitted
Implement private messaging system for bot communications
- Add IPrivateMessageTrigger interface for private messaging support - Extend GitHubStorage with SendPrivateMessage and CreateMinimalIssueComment methods - Update IssueTracker to handle private message triggers differently - Convert LastCommitActivityTrigger and OrganizationLastMonthActivityTrigger to use private messaging - Add setup documentation and examples This addresses issue #37 by sending detailed bot messages to a private bot-communications repository instead of cluttering public issue threads. Users are notified via mentions and get email notifications, while the original issue threads remain clean and readable. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ac02fa5 commit b05ff45

File tree

6 files changed

+194
-11
lines changed

6 files changed

+194
-11
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Interfaces
4+
{
5+
/// <summary>
6+
/// <para>
7+
/// Defines a trigger that supports private messaging to users instead of public issue comments.
8+
/// </para>
9+
/// <para></para>
10+
/// </summary>
11+
public interface IPrivateMessageTrigger<TContext> : ITrigger<TContext>
12+
{
13+
/// <summary>
14+
/// <para>
15+
/// Gets the user login to send private message to.
16+
/// </para>
17+
/// <para></para>
18+
/// </summary>
19+
/// <param name="context">
20+
/// <para>The context.</para>
21+
/// <para></para>
22+
/// </param>
23+
/// <returns>
24+
/// <para>The user login string</para>
25+
/// <para></para>
26+
/// </returns>
27+
string GetTargetUserLogin(TContext context);
28+
29+
/// <summary>
30+
/// <para>
31+
/// Gets the subject for the private message.
32+
/// </para>
33+
/// <para></para>
34+
/// </summary>
35+
/// <param name="context">
36+
/// <para>The context.</para>
37+
/// <para></para>
38+
/// </param>
39+
/// <returns>
40+
/// <para>The message subject</para>
41+
/// <para></para>
42+
/// </returns>
43+
string GetMessageSubject(TContext context);
44+
45+
/// <summary>
46+
/// <para>
47+
/// Gets the private message content.
48+
/// </para>
49+
/// <para></para>
50+
/// </summary>
51+
/// <param name="context">
52+
/// <para>The context.</para>
53+
/// <para></para>
54+
/// </param>
55+
/// <returns>
56+
/// <para>The private message content</para>
57+
/// <para></para>
58+
/// </returns>
59+
Task<string> GetPrivateMessageContent(TContext context);
60+
}
61+
}

csharp/Platform.Bot/Trackers/IssueTracker.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,27 @@ public async Task Start(CancellationToken cancellationToken)
7474
}
7575
if (await trigger.Condition(issue))
7676
{
77-
await trigger.Action(issue);
77+
if (trigger is IPrivateMessageTrigger<Issue> privateMessageTrigger)
78+
{
79+
await HandlePrivateMessageTrigger(issue, privateMessageTrigger);
80+
}
81+
else
82+
{
83+
await trigger.Action(issue);
84+
}
7885
}
7986
}
8087
}
8188
}
89+
90+
private async Task HandlePrivateMessageTrigger(Issue issue, IPrivateMessageTrigger<Issue> trigger)
91+
{
92+
var targetUser = trigger.GetTargetUserLogin(issue);
93+
var subject = trigger.GetMessageSubject(issue);
94+
var messageContent = await trigger.GetPrivateMessageContent(issue);
95+
96+
var privateMessageIssue = await _storage.SendPrivateMessage(targetUser, subject, messageContent);
97+
await _storage.CreateMinimalIssueComment(issue.Repository.Id, issue.Number, targetUser, subject, privateMessageIssue);
98+
}
8299
}
83100
}

csharp/Platform.Bot/Triggers/LastCommitActivityTrigger.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace Platform.Bot.Triggers
1414
{
1515
using TContext = Issue;
16-
internal class LastCommitActivityTrigger : ITrigger<TContext>
16+
internal class LastCommitActivityTrigger : IPrivateMessageTrigger<TContext>
1717
{
1818
private readonly GitHubStorage _githubStorage;
1919

@@ -25,14 +25,30 @@ public async Task<bool> Condition(TContext issue)
2525
}
2626

2727
public async Task Action(TContext issue)
28+
{
29+
Console.WriteLine($"Issue {issue.Title} is processed: {issue.HtmlUrl}");
30+
await _githubStorage.Client.Issue.Update(issue.Repository.Owner.Login, issue.Repository.Name, issue.Number, new IssueUpdate() { State = ItemState.Closed });
31+
}
32+
33+
public string GetTargetUserLogin(TContext issue)
34+
{
35+
return issue.User.Login;
36+
}
37+
38+
public string GetMessageSubject(TContext issue)
39+
{
40+
return "Last 3 Months Commit Activity Report";
41+
}
42+
43+
public async Task<string> GetPrivateMessageContent(TContext issue)
2844
{
2945
var organizationName = issue.Repository.Owner.Login;
3046

3147
var allMembers = await _githubStorage.GetAllOrganizationMembers(organizationName);
3248
var allRepositories = await _githubStorage.GetAllRepositories(organizationName);
3349
if (!allRepositories.Any())
3450
{
35-
return;
51+
return "No repositories found in the organization.";
3652
}
3753

3854
var commitsPerUserInLast3Months = await allRepositories
@@ -54,16 +70,15 @@ public async Task Action(TContext issue)
5470
}
5571
return dictionary;
5672
});
73+
5774
StringBuilder messageSb = new();
5875
var ShortSummaryMessage = GetShortSummaryMessage(commitsPerUserInLast3Months.Select(pair => pair.Key).ToList());
5976
messageSb.Append(ShortSummaryMessage);
6077
messageSb.AppendLine("---");
6178
var detailedMessage = await GetDetailedMessage(commitsPerUserInLast3Months);
6279
messageSb.Append(detailedMessage);
63-
var message = messageSb.ToString();
64-
await _githubStorage.CreateIssueComment(issue.Repository.Id, issue.Number, message);
65-
Console.WriteLine($"Issue {issue.Title} is processed: {issue.HtmlUrl}");
66-
await _githubStorage.Client.Issue.Update(issue.Repository.Owner.Login, issue.Repository.Name, issue.Number, new IssueUpdate() { State = ItemState.Closed });
80+
81+
return messageSb.ToString();
6782
}
6883

6984
private string GetShortSummaryMessage(List<User> users)

csharp/Platform.Bot/Triggers/OrganizationLastMonthActivityTrigger.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Platform.Bot.Triggers
1717
/// <para></para>
1818
/// </summary>
1919
/// <seealso cref="ITrigger{Issue}"/>
20-
internal class OrganizationLastMonthActivityTrigger : ITrigger<TContext>
20+
internal class OrganizationLastMonthActivityTrigger : IPrivateMessageTrigger<TContext>
2121
{
2222
private readonly GitHubStorage _storage;
2323
private readonly Parser _parser = new();
@@ -62,11 +62,24 @@ internal class OrganizationLastMonthActivityTrigger : ITrigger<TContext>
6262
/// </param>
6363
public async Task Action(TContext context)
6464
{
65-
var issueService = _storage.Client.Issue;
65+
_storage.CloseIssue(context);
66+
}
67+
68+
public string GetTargetUserLogin(TContext context)
69+
{
70+
return context.User.Login;
71+
}
72+
73+
public string GetMessageSubject(TContext context)
74+
{
75+
return "Organization Last Month Activity Report";
76+
}
77+
78+
public async Task<string> GetPrivateMessageContent(TContext context)
79+
{
6680
var owner = context.Repository.Owner.Login;
6781
var activeUsersString = string.Join("\n", GetActiveUsers(GetIgnoredRepositories(_parser.Parse(context.Body)), owner));
68-
issueService.Comment.Create(owner, context.Repository.Name, context.Number, activeUsersString);
69-
_storage.CloseIssue(context);
82+
return $"# Organization Last Month Activity\n\nActive users in the last month:\n\n{activeUsersString}";
7083
}
7184

7285
/// <summary>

csharp/Storage/RemoteStorage/GitHubStorage.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,33 @@ public Task<IssueComment> CreateIssueComment(long repositoryId, int issueNumber,
307307
return Client.Issue.Comment.Create(repositoryId, issueNumber, message);
308308
}
309309

310+
public async Task<Issue> SendPrivateMessage(string userLogin, string subject, string message, string botCommunicationRepoName = "bot-communications")
311+
{
312+
try
313+
{
314+
var repo = await Client.Repository.Get(Owner, botCommunicationRepoName);
315+
var issueTitle = $"Message for @{userLogin}: {subject}";
316+
var issueBody = $"@{userLogin}\n\n{message}";
317+
318+
var newIssue = new NewIssue(issueTitle)
319+
{
320+
Body = issueBody
321+
};
322+
323+
return await Client.Issue.Create(repo.Id, newIssue);
324+
}
325+
catch (NotFoundException)
326+
{
327+
throw new InvalidOperationException($"Bot communication repository '{botCommunicationRepoName}' not found. Please create this repository first.");
328+
}
329+
}
330+
331+
public async Task<IssueComment> CreateMinimalIssueComment(long repositoryId, int issueNumber, string userLogin, string subject, Issue privateMessageIssue)
332+
{
333+
var message = $"@{userLogin} Bot message sent privately: [{subject}]({privateMessageIssue.HtmlUrl})";
334+
return await Client.Issue.Comment.Create(repositoryId, issueNumber, message);
335+
}
336+
310337
#endregion
311338

312339
#region Branch
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Bot Communication Setup
2+
3+
This example shows how to set up the private messaging system for the bot.
4+
5+
## Prerequisites
6+
7+
1. Create a private repository called `bot-communications` in your organization
8+
2. Make sure the bot has access to this repository
9+
10+
## How it works
11+
12+
When a trigger implements `IPrivateMessageTrigger<Issue>` instead of `ITrigger<Issue>`:
13+
14+
1. The bot generates the message content privately
15+
2. Creates an issue in the `bot-communications` repository with the message
16+
3. Mentions the target user in that issue (so they get email notification)
17+
4. Posts a minimal comment in the original issue with a link to the private message
18+
19+
## Example Usage
20+
21+
```csharp
22+
// Old way - creates big public comments
23+
internal class MyTrigger : ITrigger<Issue>
24+
{
25+
public async Task Action(Issue issue)
26+
{
27+
await _github.CreateIssueComment(issue.Repository.Id, issue.Number, "Very long message...");
28+
}
29+
}
30+
31+
// New way - sends private messages
32+
internal class MyTrigger : IPrivateMessageTrigger<Issue>
33+
{
34+
public string GetTargetUserLogin(Issue issue) => issue.User.Login;
35+
public string GetMessageSubject(Issue issue) => "Report Title";
36+
public async Task<string> GetPrivateMessageContent(Issue issue) => "Very long message...";
37+
public async Task Action(Issue issue)
38+
{
39+
// Only handle issue closing, messaging is automatic
40+
await _github.Client.Issue.Update(issue.Repository.Owner.Login, issue.Repository.Name, issue.Number, new IssueUpdate() { State = ItemState.Closed });
41+
}
42+
}
43+
```
44+
45+
## Benefits
46+
47+
- ✅ Reduces clutter in main issue threads
48+
- ✅ Users still get notified via email mentions
49+
- ✅ Messages are preserved in a dedicated space
50+
- ✅ Original issues stay clean and readable

0 commit comments

Comments
 (0)