Skip to content

Commit ef359ae

Browse files
konardclaude
andcommitted
Implement Discord and GitHub bot flows order sync based on contribution work amount
This commit implements a comprehensive flows order sync system that: - Tracks contributor activity across organization's public repositories - Calculates work scores based on commits, PRs, issues, and code reviews - Automatically syncs Discord roles based on contribution levels - Provides detailed contribution reports and leaderboards - Supports configurable time periods and role tiers Key components: - ContributionTrackingService: Aggregates GitHub activity metrics - DiscordRoleSyncService: Manages Discord role assignments and reports - FlowsOrderSyncTrigger: Orchestrates the sync process via GitHub issues - Updated Program.cs with Discord bot token and sync configuration options The system creates role tiers (Contributor Legend, Senior Contributor, Active Contributor, Contributor, Member) and assigns users based on their calculated work scores. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 0b3311e commit ef359ae

File tree

5 files changed

+652
-4
lines changed

5 files changed

+652
-4
lines changed

csharp/Platform.Bot/Platform.Bot.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="CommandLineParser" Version="2.9.1" />
11+
<PackageReference Include="Discord.Net" Version="3.15.3" />
1112
<PackageReference Include="Octokit" Version="7.0.1" />
1213
<PackageReference Include="Platform.Communication.Protocol.Lino" Version="0.4.0" />
1314
<PackageReference Include="Platform.Data.Doublets.Sequences" Version="0.1.1" />

csharp/Platform.Bot/Program.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Platform.Bot.Trackers;
1616
using Platform.Bot.Triggers;
1717
using Platform.Bot.Triggers.Decorators;
18+
using Platform.Bot.Services;
1819

1920
namespace Platform.Bot
2021
{
@@ -73,29 +74,59 @@ private static async Task<int> Main(string[] args)
7374
description: "Minimum interaction interval in seconds.",
7475
getDefaultValue: () => 60);
7576

77+
var discordBotTokenOption = new Option<string?>(
78+
name: "--discord-bot-token",
79+
description: "Discord bot token for role synchronization.");
80+
81+
var enableFlowsSyncOption = new Option<bool>(
82+
name: "--enable-flows-sync",
83+
description: "Enable flows order sync functionality.",
84+
getDefaultValue: () => true);
85+
7686
var rootCommand = new RootCommand("Sample app for System.CommandLine")
7787
{
7888
githubUserNameOption,
7989
githubApiTokenOption,
8090
githubApplicationNameOption,
8191
databaseFilePathOption,
8292
fileSetNameOption,
83-
minimumInteractionIntervalOption
93+
minimumInteractionIntervalOption,
94+
discordBotTokenOption,
95+
enableFlowsSyncOption
8496
};
8597

86-
rootCommand.SetHandler(async (githubUserName, githubApiToken, githubApplicationName, databaseFilePath, fileSetName, minimumInteractionInterval) =>
98+
rootCommand.SetHandler(async (githubUserName, githubApiToken, githubApplicationName, databaseFilePath, fileSetName, minimumInteractionInterval, discordBotToken, enableFlowsSync) =>
8799
{
88100
Debug.WriteLine($"Nickname: {githubUserName}");
89101
Debug.WriteLine($"GitHub API Token: {githubApiToken}");
90102
Debug.WriteLine($"Application Name: {githubApplicationName}");
91103
Debug.WriteLine($"Database File Path: {databaseFilePath?.FullName}");
92104
Debug.WriteLine($"File Set Name: {fileSetName}");
93105
Debug.WriteLine($"Minimum Interaction Interval: {minimumInteractionInterval} seconds");
106+
Debug.WriteLine($"Discord Bot Token: {(string.IsNullOrEmpty(discordBotToken) ? "Not provided" : "Provided")}");
107+
Debug.WriteLine($"Flows Sync Enabled: {enableFlowsSync}");
94108

95109
var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename);
96110
Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close");
97111
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));
112+
113+
var triggers = new List<ITrigger<Issue>>
114+
{
115+
new HelloWorldTrigger(githubStorage, dbContext, fileSetName),
116+
new OrganizationLastMonthActivityTrigger(githubStorage),
117+
new LastCommitActivityTrigger(githubStorage),
118+
new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage),
119+
new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage),
120+
new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage)
121+
};
122+
123+
if (enableFlowsSync)
124+
{
125+
var discordService = new DiscordRoleSyncService(discordBotToken ?? string.Empty);
126+
triggers.Add(new AdminAuthorIssueTriggerDecorator(new FlowsOrderSyncTrigger(githubStorage, dbContext, discordService), githubStorage));
127+
}
128+
129+
var issueTracker = new IssueTracker(githubStorage, triggers.ToArray());
99130
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
100131
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
101132
var cancellation = new CancellationTokenSource();
@@ -114,7 +145,7 @@ private static async Task<int> Main(string[] args)
114145
}
115146
}
116147
},
117-
githubUserNameOption, githubApiTokenOption, githubApplicationNameOption, databaseFilePathOption, fileSetNameOption, minimumInteractionIntervalOption);
148+
githubUserNameOption, githubApiTokenOption, githubApplicationNameOption, databaseFilePathOption, fileSetNameOption, minimumInteractionIntervalOption, discordBotTokenOption, enableFlowsSyncOption);
118149

119150
return await rootCommand.InvokeAsync(args);
120151
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using Octokit;
2+
using Storage.Remote.GitHub;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
8+
namespace Platform.Bot.Services
9+
{
10+
public class UserContribution
11+
{
12+
public User User { get; set; } = null!;
13+
public int CommitCount { get; set; }
14+
public int PullRequestCount { get; set; }
15+
public int IssueCount { get; set; }
16+
public int CodeReviewCount { get; set; }
17+
public double WorkScore { get; set; }
18+
public int Rank { get; set; }
19+
}
20+
21+
public class ContributionTrackingService
22+
{
23+
private readonly GitHubStorage _githubStorage;
24+
25+
public ContributionTrackingService(GitHubStorage githubStorage)
26+
{
27+
_githubStorage = githubStorage;
28+
}
29+
30+
public async Task<List<UserContribution>> GetOrganizationContributions(string organizationName, DateTime since)
31+
{
32+
var allMembers = await _githubStorage.GetAllOrganizationMembers(organizationName);
33+
var allRepositories = await _githubStorage.GetAllRepositories(organizationName);
34+
35+
var contributions = new Dictionary<int, UserContribution>();
36+
37+
foreach (var member in allMembers)
38+
{
39+
contributions[member.Id] = new UserContribution
40+
{
41+
User = member,
42+
CommitCount = 0,
43+
PullRequestCount = 0,
44+
IssueCount = 0,
45+
CodeReviewCount = 0
46+
};
47+
}
48+
49+
foreach (var repository in allRepositories.Where(r => !r.Private))
50+
{
51+
await TrackCommitContributions(repository, contributions, since);
52+
await TrackPullRequestContributions(repository, contributions, since);
53+
await TrackIssueContributions(repository, contributions, since);
54+
await TrackCodeReviewContributions(repository, contributions, since);
55+
}
56+
57+
var contributionList = contributions.Values.ToList();
58+
CalculateWorkScores(contributionList);
59+
AssignRanks(contributionList);
60+
61+
return contributionList.OrderByDescending(c => c.WorkScore).ToList();
62+
}
63+
64+
private async Task TrackCommitContributions(Repository repository, Dictionary<int, UserContribution> contributions, DateTime since)
65+
{
66+
try
67+
{
68+
var commits = await _githubStorage.GetCommits(repository.Id, new CommitRequest { Since = since });
69+
foreach (var commit in commits)
70+
{
71+
if (commit.Author != null && contributions.ContainsKey(commit.Author.Id))
72+
{
73+
contributions[commit.Author.Id].CommitCount++;
74+
}
75+
}
76+
}
77+
catch (Exception ex)
78+
{
79+
Console.WriteLine($"Error tracking commits for repository {repository.Name}: {ex.Message}");
80+
}
81+
}
82+
83+
private async Task TrackPullRequestContributions(Repository repository, Dictionary<int, UserContribution> contributions, DateTime since)
84+
{
85+
try
86+
{
87+
var pullRequests = _githubStorage.GetPullRequests(repository.Owner.Login, repository.Name);
88+
foreach (var pr in pullRequests.Where(pr => pr.CreatedAt >= since))
89+
{
90+
if (pr.User != null && contributions.ContainsKey(pr.User.Id))
91+
{
92+
contributions[pr.User.Id].PullRequestCount++;
93+
}
94+
}
95+
}
96+
catch (Exception ex)
97+
{
98+
Console.WriteLine($"Error tracking pull requests for repository {repository.Name}: {ex.Message}");
99+
}
100+
}
101+
102+
private async Task TrackIssueContributions(Repository repository, Dictionary<int, UserContribution> contributions, DateTime since)
103+
{
104+
try
105+
{
106+
var issues = _githubStorage.GetIssues(repository.Owner.Login, repository.Name);
107+
foreach (var issue in issues.Where(i => i.CreatedAt >= since))
108+
{
109+
if (issue.User != null && contributions.ContainsKey(issue.User.Id))
110+
{
111+
contributions[issue.User.Id].IssueCount++;
112+
}
113+
}
114+
}
115+
catch (Exception ex)
116+
{
117+
Console.WriteLine($"Error tracking issues for repository {repository.Name}: {ex.Message}");
118+
}
119+
}
120+
121+
private async Task TrackCodeReviewContributions(Repository repository, Dictionary<int, UserContribution> contributions, DateTime since)
122+
{
123+
try
124+
{
125+
var pullRequests = _githubStorage.GetPullRequests(repository.Owner.Login, repository.Name);
126+
foreach (var pr in pullRequests.Where(pr => pr.CreatedAt >= since))
127+
{
128+
foreach (var reviewer in pr.RequestedReviewers)
129+
{
130+
if (contributions.ContainsKey(reviewer.Id))
131+
{
132+
contributions[reviewer.Id].CodeReviewCount++;
133+
}
134+
}
135+
}
136+
}
137+
catch (Exception ex)
138+
{
139+
Console.WriteLine($"Error tracking code reviews for repository {repository.Name}: {ex.Message}");
140+
}
141+
}
142+
143+
private void CalculateWorkScores(List<UserContribution> contributions)
144+
{
145+
const double commitWeight = 1.0;
146+
const double pullRequestWeight = 3.0;
147+
const double issueWeight = 1.5;
148+
const double codeReviewWeight = 2.0;
149+
150+
foreach (var contribution in contributions)
151+
{
152+
contribution.WorkScore =
153+
(contribution.CommitCount * commitWeight) +
154+
(contribution.PullRequestCount * pullRequestWeight) +
155+
(contribution.IssueCount * issueWeight) +
156+
(contribution.CodeReviewCount * codeReviewWeight);
157+
}
158+
}
159+
160+
private void AssignRanks(List<UserContribution> contributions)
161+
{
162+
var sortedContributions = contributions.OrderByDescending(c => c.WorkScore).ToList();
163+
for (int i = 0; i < sortedContributions.Count; i++)
164+
{
165+
sortedContributions[i].Rank = i + 1;
166+
}
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)