Skip to content

Commit 00e1c6e

Browse files
konardclaude
andcommitted
Implement language-specific user tagging functionality for issue #24
- Add CallUsersByLanguageTrigger to handle commands like "! C++" - Support 30+ programming languages with aliases (e.g., JS -> JavaScript) - Search GitHub repositories by language and tag top contributors - Integrate trigger into IssueTracker in Program.cs - Add experimental test script to verify trigger logic The bot now responds to "! <language>" commands by: 1. Validating the programming language 2. Searching for top repositories in that language 3. Finding and mentioning active developers 4. Posting a comment with @mentions to call developers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d0a25ab commit 00e1c6e

File tree

3 files changed

+253
-1
lines changed

3 files changed

+253
-1
lines changed

csharp/Platform.Bot/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private static async Task<int> Main(string[] args)
9595
var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename);
9696
Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close");
9797
var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName);
98-
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
98+
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new CallUsersByLanguageTrigger(githubStorage), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
9999
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
100100
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
101101
var cancellation = new CancellationTokenSource();
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Interfaces;
6+
using Octokit;
7+
using Storage.Remote.GitHub;
8+
9+
namespace Platform.Bot.Triggers
10+
{
11+
using TContext = Issue;
12+
13+
/// <summary>
14+
/// <para>
15+
/// Represents the call users by language trigger.
16+
/// </para>
17+
/// <para></para>
18+
/// </summary>
19+
/// <seealso cref="ITrigger{TContext}"/>
20+
internal class CallUsersByLanguageTrigger : ITrigger<TContext>
21+
{
22+
private readonly GitHubStorage _storage;
23+
private readonly Dictionary<string, string> _languageMap;
24+
25+
/// <summary>
26+
/// <para>
27+
/// Initializes a new <see cref="CallUsersByLanguageTrigger"/> instance.
28+
/// </para>
29+
/// <para></para>
30+
/// </summary>
31+
/// <param name="storage">
32+
/// <para>A git hub api.</para>
33+
/// <para></para>
34+
/// </param>
35+
public CallUsersByLanguageTrigger(GitHubStorage storage)
36+
{
37+
_storage = storage;
38+
_languageMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
39+
{
40+
{"C++", "C++"},
41+
{"CPP", "C++"},
42+
{"C#", "C#"},
43+
{"CSHARP", "C#"},
44+
{"C", "C"},
45+
{"Java", "Java"},
46+
{"JavaScript", "JavaScript"},
47+
{"JS", "JavaScript"},
48+
{"Python", "Python"},
49+
{"PY", "Python"},
50+
{"Ruby", "Ruby"},
51+
{"RB", "Ruby"},
52+
{"Go", "Go"},
53+
{"Golang", "Go"},
54+
{"PHP", "PHP"},
55+
{"Swift", "Swift"},
56+
{"Kotlin", "Kotlin"},
57+
{"Rust", "Rust"},
58+
{"TypeScript", "TypeScript"},
59+
{"TS", "TypeScript"},
60+
{"Scala", "Scala"},
61+
{"R", "R"},
62+
{"Dart", "Dart"},
63+
{"Lua", "Lua"},
64+
{"Perl", "Perl"},
65+
{"Haskell", "Haskell"},
66+
{"Clojure", "Clojure"},
67+
{"F#", "F#"},
68+
{"FSHARP", "F#"},
69+
{"Objective-C", "Objective-C"},
70+
{"Shell", "Shell"},
71+
{"PowerShell", "PowerShell"},
72+
{"HTML", "HTML"},
73+
{"CSS", "CSS"},
74+
{"Vim", "Vim script"},
75+
{"Assembly", "Assembly"},
76+
{"MATLAB", "MATLAB"},
77+
{"Groovy", "Groovy"},
78+
{"Elixir", "Elixir"},
79+
{"Erlang", "Erlang"},
80+
{"Julia", "Julia"},
81+
{"Nim", "Nim"},
82+
{"Crystal", "Crystal"}
83+
};
84+
}
85+
86+
/// <summary>
87+
/// <para>
88+
/// Actions the context.
89+
/// </para>
90+
/// <para></para>
91+
/// </summary>
92+
/// <param name="context">
93+
/// <para>The context.</para>
94+
/// <para></para>
95+
/// </param>
96+
public async Task Action(TContext context)
97+
{
98+
var languageText = context.Body.Trim().Substring(1).Trim();
99+
100+
if (!_languageMap.TryGetValue(languageText, out var language))
101+
{
102+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
103+
$"Sorry, I don't recognize the programming language '{languageText}'. " +
104+
$"Supported languages include: {string.Join(", ", _languageMap.Keys.Take(10))} and more.");
105+
return;
106+
}
107+
108+
try
109+
{
110+
// Search for repositories with the specified language using raw query
111+
var query = $"language:{language}";
112+
var repositorySearchRequest = new SearchRepositoriesRequest(query)
113+
{
114+
SortField = RepoSearchSort.Stars,
115+
Order = SortDirection.Descending,
116+
PerPage = 100
117+
};
118+
119+
var repositoryResults = await _storage.Client.Search.SearchRepo(repositorySearchRequest);
120+
121+
if (!repositoryResults.Items.Any())
122+
{
123+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
124+
$"No repositories found with {languageText} as the primary language.");
125+
return;
126+
}
127+
128+
// Get unique users from these repositories
129+
var uniqueUsers = repositoryResults.Items
130+
.Where(repo => repo.Owner != null)
131+
.GroupBy(repo => repo.Owner.Login)
132+
.Select(g => g.First().Owner)
133+
.Take(10)
134+
.ToList();
135+
136+
if (!uniqueUsers.Any())
137+
{
138+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
139+
$"No users found with {languageText} repositories.");
140+
return;
141+
}
142+
143+
var userMentions = uniqueUsers
144+
.Select(user => $"@{user.Login}")
145+
.ToList();
146+
147+
var message = $"Calling all {languageText} developers! 👋\n\n{string.Join(" ", userMentions)}\n\n" +
148+
$"Found {uniqueUsers.Count} top {languageText} developers based on repository stars.";
149+
150+
await _storage.CreateIssueComment(context.Repository.Id, context.Number, message);
151+
}
152+
catch (Exception ex)
153+
{
154+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
155+
$"Sorry, I encountered an error while searching for {languageText} developers: {ex.Message}");
156+
}
157+
}
158+
159+
/// <summary>
160+
/// <para>
161+
/// Determines whether this instance condition.
162+
/// </para>
163+
/// <para></para>
164+
/// </summary>
165+
/// <param name="context">
166+
/// <para>The context.</para>
167+
/// <para></para>
168+
/// </param>
169+
/// <returns>
170+
/// <para>The bool</para>
171+
/// <para></para>
172+
/// </returns>
173+
public async Task<bool> Condition(TContext context)
174+
{
175+
return !string.IsNullOrEmpty(context.Body) &&
176+
context.Body.Trim().StartsWith("!") &&
177+
context.Body.Trim().Length > 1 &&
178+
context.Body.Trim().Split(' ').Length >= 1;
179+
}
180+
}
181+
}

experiments/test_trigger.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Octokit;
4+
using Platform.Bot.Triggers;
5+
6+
namespace TestTrigger
7+
{
8+
class Program
9+
{
10+
static async Task Main(string[] args)
11+
{
12+
// Test trigger condition logic without actual GitHub API calls
13+
var trigger = new CallUsersByLanguageTrigger(null);
14+
15+
// Test cases
16+
var testIssues = new[]
17+
{
18+
new { Body = "! C++", Expected = true, Description = "Valid C++ call" },
19+
new { Body = "! Python", Expected = true, Description = "Valid Python call" },
20+
new { Body = "!JavaScript", Expected = true, Description = "Valid JavaScript call without space" },
21+
new { Body = "Hello world", Expected = false, Description = "Regular issue body" },
22+
new { Body = "!", Expected = false, Description = "Just exclamation mark" },
23+
new { Body = "", Expected = false, Description = "Empty body" },
24+
};
25+
26+
Console.WriteLine("Testing CallUsersByLanguageTrigger conditions:");
27+
Console.WriteLine("=" + new string('=', 50));
28+
29+
foreach (var test in testIssues)
30+
{
31+
var mockIssue = CreateMockIssue(test.Body);
32+
var result = await trigger.Condition(mockIssue);
33+
var status = result == test.Expected ? "✓ PASS" : "✗ FAIL";
34+
Console.WriteLine($"{status} {test.Description}: '{test.Body}' -> {result} (expected: {test.Expected})");
35+
}
36+
}
37+
38+
static Issue CreateMockIssue(string body)
39+
{
40+
// Create a basic mock issue with the given body
41+
// Note: This is a simplified mock for testing purposes
42+
return new Issue(
43+
url: "https://api.github.com/repos/test/test/issues/1",
44+
htmlUrl: "https://github.com/test/test/issues/1",
45+
commentsUrl: "https://api.github.com/repos/test/test/issues/1/comments",
46+
eventsUrl: "https://api.github.com/repos/test/test/issues/1/events",
47+
number: 1,
48+
state: ItemState.Open,
49+
title: "Test Issue",
50+
body: body,
51+
user: null,
52+
labels: null,
53+
assignee: null,
54+
assignees: null,
55+
milestone: null,
56+
comments: 0,
57+
pullRequest: null,
58+
closedAt: null,
59+
createdAt: DateTimeOffset.Now,
60+
updatedAt: DateTimeOffset.Now,
61+
closedBy: null,
62+
nodeId: "test",
63+
locked: false,
64+
repository: null,
65+
reactions: null,
66+
activeLockReason: null,
67+
stateReason: null
68+
);
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)