From 0b3311edc9f3eb575373c7a7f27b009b556277a6 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 06:28:43 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #110 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Bot/issues/110 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d30d49da --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Bot/issues/110 +Your prepared branch: issue-110-9c30687a +Your prepared working directory: /tmp/gh-issue-solver-1757734118988 + +Proceed. \ No newline at end of file From ef359ae70731fe65ecfcbee39e1931c3e6a5a929 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 06:43:22 +0300 Subject: [PATCH 2/3] Implement Discord and GitHub bot flows order sync based on contribution work amount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- csharp/Platform.Bot/Platform.Bot.csproj | 1 + csharp/Platform.Bot/Program.cs | 39 ++- .../Services/ContributionTrackingService.cs | 169 ++++++++++++ .../Services/DiscordRoleSyncService.cs | 202 +++++++++++++++ .../Triggers/FlowsOrderSyncTrigger.cs | 245 ++++++++++++++++++ 5 files changed, 652 insertions(+), 4 deletions(-) create mode 100644 csharp/Platform.Bot/Services/ContributionTrackingService.cs create mode 100644 csharp/Platform.Bot/Services/DiscordRoleSyncService.cs create mode 100644 csharp/Platform.Bot/Triggers/FlowsOrderSyncTrigger.cs diff --git a/csharp/Platform.Bot/Platform.Bot.csproj b/csharp/Platform.Bot/Platform.Bot.csproj index 2828772d..59924a96 100644 --- a/csharp/Platform.Bot/Platform.Bot.csproj +++ b/csharp/Platform.Bot/Platform.Bot.csproj @@ -8,6 +8,7 @@ + diff --git a/csharp/Platform.Bot/Program.cs b/csharp/Platform.Bot/Program.cs index 521a6b95..111460d0 100644 --- a/csharp/Platform.Bot/Program.cs +++ b/csharp/Platform.Bot/Program.cs @@ -15,6 +15,7 @@ using Platform.Bot.Trackers; using Platform.Bot.Triggers; using Platform.Bot.Triggers.Decorators; +using Platform.Bot.Services; namespace Platform.Bot { @@ -73,6 +74,15 @@ private static async Task Main(string[] args) description: "Minimum interaction interval in seconds.", getDefaultValue: () => 60); + var discordBotTokenOption = new Option( + name: "--discord-bot-token", + description: "Discord bot token for role synchronization."); + + var enableFlowsSyncOption = new Option( + name: "--enable-flows-sync", + description: "Enable flows order sync functionality.", + getDefaultValue: () => true); + var rootCommand = new RootCommand("Sample app for System.CommandLine") { githubUserNameOption, @@ -80,10 +90,12 @@ private static async Task Main(string[] args) githubApplicationNameOption, databaseFilePathOption, fileSetNameOption, - minimumInteractionIntervalOption + minimumInteractionIntervalOption, + discordBotTokenOption, + enableFlowsSyncOption }; - rootCommand.SetHandler(async (githubUserName, githubApiToken, githubApplicationName, databaseFilePath, fileSetName, minimumInteractionInterval) => + rootCommand.SetHandler(async (githubUserName, githubApiToken, githubApplicationName, databaseFilePath, fileSetName, minimumInteractionInterval, discordBotToken, enableFlowsSync) => { Debug.WriteLine($"Nickname: {githubUserName}"); Debug.WriteLine($"GitHub API Token: {githubApiToken}"); @@ -91,11 +103,30 @@ private static async Task Main(string[] args) Debug.WriteLine($"Database File Path: {databaseFilePath?.FullName}"); Debug.WriteLine($"File Set Name: {fileSetName}"); Debug.WriteLine($"Minimum Interaction Interval: {minimumInteractionInterval} seconds"); + Debug.WriteLine($"Discord Bot Token: {(string.IsNullOrEmpty(discordBotToken) ? "Not provided" : "Provided")}"); + Debug.WriteLine($"Flows Sync Enabled: {enableFlowsSync}"); var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename); Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close"); var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName); - 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)); + + var triggers = new List> + { + 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) + }; + + if (enableFlowsSync) + { + var discordService = new DiscordRoleSyncService(discordBotToken ?? string.Empty); + triggers.Add(new AdminAuthorIssueTriggerDecorator(new FlowsOrderSyncTrigger(githubStorage, dbContext, discordService), githubStorage)); + } + + var issueTracker = new IssueTracker(githubStorage, triggers.ToArray()); var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage)); var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations"))); var cancellation = new CancellationTokenSource(); @@ -114,7 +145,7 @@ private static async Task Main(string[] args) } } }, - githubUserNameOption, githubApiTokenOption, githubApplicationNameOption, databaseFilePathOption, fileSetNameOption, minimumInteractionIntervalOption); + githubUserNameOption, githubApiTokenOption, githubApplicationNameOption, databaseFilePathOption, fileSetNameOption, minimumInteractionIntervalOption, discordBotTokenOption, enableFlowsSyncOption); return await rootCommand.InvokeAsync(args); } diff --git a/csharp/Platform.Bot/Services/ContributionTrackingService.cs b/csharp/Platform.Bot/Services/ContributionTrackingService.cs new file mode 100644 index 00000000..8956ef67 --- /dev/null +++ b/csharp/Platform.Bot/Services/ContributionTrackingService.cs @@ -0,0 +1,169 @@ +using Octokit; +using Storage.Remote.GitHub; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Platform.Bot.Services +{ + public class UserContribution + { + public User User { get; set; } = null!; + public int CommitCount { get; set; } + public int PullRequestCount { get; set; } + public int IssueCount { get; set; } + public int CodeReviewCount { get; set; } + public double WorkScore { get; set; } + public int Rank { get; set; } + } + + public class ContributionTrackingService + { + private readonly GitHubStorage _githubStorage; + + public ContributionTrackingService(GitHubStorage githubStorage) + { + _githubStorage = githubStorage; + } + + public async Task> GetOrganizationContributions(string organizationName, DateTime since) + { + var allMembers = await _githubStorage.GetAllOrganizationMembers(organizationName); + var allRepositories = await _githubStorage.GetAllRepositories(organizationName); + + var contributions = new Dictionary(); + + foreach (var member in allMembers) + { + contributions[member.Id] = new UserContribution + { + User = member, + CommitCount = 0, + PullRequestCount = 0, + IssueCount = 0, + CodeReviewCount = 0 + }; + } + + foreach (var repository in allRepositories.Where(r => !r.Private)) + { + await TrackCommitContributions(repository, contributions, since); + await TrackPullRequestContributions(repository, contributions, since); + await TrackIssueContributions(repository, contributions, since); + await TrackCodeReviewContributions(repository, contributions, since); + } + + var contributionList = contributions.Values.ToList(); + CalculateWorkScores(contributionList); + AssignRanks(contributionList); + + return contributionList.OrderByDescending(c => c.WorkScore).ToList(); + } + + private async Task TrackCommitContributions(Repository repository, Dictionary contributions, DateTime since) + { + try + { + var commits = await _githubStorage.GetCommits(repository.Id, new CommitRequest { Since = since }); + foreach (var commit in commits) + { + if (commit.Author != null && contributions.ContainsKey(commit.Author.Id)) + { + contributions[commit.Author.Id].CommitCount++; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error tracking commits for repository {repository.Name}: {ex.Message}"); + } + } + + private async Task TrackPullRequestContributions(Repository repository, Dictionary contributions, DateTime since) + { + try + { + var pullRequests = _githubStorage.GetPullRequests(repository.Owner.Login, repository.Name); + foreach (var pr in pullRequests.Where(pr => pr.CreatedAt >= since)) + { + if (pr.User != null && contributions.ContainsKey(pr.User.Id)) + { + contributions[pr.User.Id].PullRequestCount++; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error tracking pull requests for repository {repository.Name}: {ex.Message}"); + } + } + + private async Task TrackIssueContributions(Repository repository, Dictionary contributions, DateTime since) + { + try + { + var issues = _githubStorage.GetIssues(repository.Owner.Login, repository.Name); + foreach (var issue in issues.Where(i => i.CreatedAt >= since)) + { + if (issue.User != null && contributions.ContainsKey(issue.User.Id)) + { + contributions[issue.User.Id].IssueCount++; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error tracking issues for repository {repository.Name}: {ex.Message}"); + } + } + + private async Task TrackCodeReviewContributions(Repository repository, Dictionary contributions, DateTime since) + { + try + { + var pullRequests = _githubStorage.GetPullRequests(repository.Owner.Login, repository.Name); + foreach (var pr in pullRequests.Where(pr => pr.CreatedAt >= since)) + { + foreach (var reviewer in pr.RequestedReviewers) + { + if (contributions.ContainsKey(reviewer.Id)) + { + contributions[reviewer.Id].CodeReviewCount++; + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error tracking code reviews for repository {repository.Name}: {ex.Message}"); + } + } + + private void CalculateWorkScores(List contributions) + { + const double commitWeight = 1.0; + const double pullRequestWeight = 3.0; + const double issueWeight = 1.5; + const double codeReviewWeight = 2.0; + + foreach (var contribution in contributions) + { + contribution.WorkScore = + (contribution.CommitCount * commitWeight) + + (contribution.PullRequestCount * pullRequestWeight) + + (contribution.IssueCount * issueWeight) + + (contribution.CodeReviewCount * codeReviewWeight); + } + } + + private void AssignRanks(List contributions) + { + var sortedContributions = contributions.OrderByDescending(c => c.WorkScore).ToList(); + for (int i = 0; i < sortedContributions.Count; i++) + { + sortedContributions[i].Rank = i + 1; + } + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Bot/Services/DiscordRoleSyncService.cs b/csharp/Platform.Bot/Services/DiscordRoleSyncService.cs new file mode 100644 index 00000000..45e930b7 --- /dev/null +++ b/csharp/Platform.Bot/Services/DiscordRoleSyncService.cs @@ -0,0 +1,202 @@ +using Discord; +using Discord.WebSocket; +using Platform.Bot.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Platform.Bot.Services +{ + public class RoleTier + { + public string RoleName { get; set; } = null!; + public double MinWorkScore { get; set; } + public Color RoleColor { get; set; } + public string Description { get; set; } = null!; + } + + public class DiscordRoleSyncService + { + private readonly DiscordSocketClient _discordClient; + private readonly List _roleTiers; + + public DiscordRoleSyncService(string discordToken) + { + _discordClient = new DiscordSocketClient(); + _roleTiers = InitializeRoleTiers(); + } + + private List InitializeRoleTiers() + { + return new List + { + new RoleTier { RoleName = "Contributor Legend", MinWorkScore = 100, RoleColor = Color.Gold, Description = "Exceptional contributors with outstanding work" }, + new RoleTier { RoleName = "Senior Contributor", MinWorkScore = 50, RoleColor = Color.Purple, Description = "Highly active contributors" }, + new RoleTier { RoleName = "Active Contributor", MinWorkScore = 20, RoleColor = Color.Blue, Description = "Regular contributors" }, + new RoleTier { RoleName = "Contributor", MinWorkScore = 5, RoleColor = Color.Green, Description = "Getting started contributors" }, + new RoleTier { RoleName = "Member", MinWorkScore = 0, RoleColor = Color.LightGrey, Description = "Organization members" } + }; + } + + public async Task ConnectAsync(string token) + { + await _discordClient.LoginAsync(TokenType.Bot, token); + await _discordClient.StartAsync(); + + _discordClient.Ready += OnReady; + _discordClient.Log += LogAsync; + } + + private async Task OnReady() + { + Console.WriteLine($"Discord bot {_discordClient.CurrentUser} is connected!"); + } + + private Task LogAsync(LogMessage log) + { + Console.WriteLine(log.ToString()); + return Task.CompletedTask; + } + + public async Task SyncRolesWithContributions(ulong guildId, List contributions, Dictionary githubToDiscordMapping) + { + var guild = _discordClient.GetGuild(guildId); + if (guild == null) + { + Console.WriteLine($"Guild with ID {guildId} not found."); + return; + } + + await EnsureRolesExist(guild); + + var roles = guild.Roles.Where(r => _roleTiers.Any(tier => tier.RoleName == r.Name)).ToList(); + + foreach (var contribution in contributions) + { + var githubLogin = contribution.User.Login; + if (!githubToDiscordMapping.ContainsKey(githubLogin)) + { + Console.WriteLine($"No Discord mapping found for GitHub user: {githubLogin}"); + continue; + } + + var discordUserId = githubToDiscordMapping[githubLogin]; + var discordUser = guild.GetUser(discordUserId); + + if (discordUser == null) + { + Console.WriteLine($"Discord user with ID {discordUserId} not found in guild."); + continue; + } + + await AssignAppropriateRole(discordUser, contribution.WorkScore, roles); + } + } + + private async Task EnsureRolesExist(SocketGuild guild) + { + foreach (var tier in _roleTiers) + { + var existingRole = guild.Roles.FirstOrDefault(r => r.Name == tier.RoleName); + if (existingRole == null) + { + await guild.CreateRoleAsync(tier.RoleName, + color: tier.RoleColor, + isMentionable: true, + options: new RequestOptions { AuditLogReason = $"Created role for contribution tier: {tier.Description}" }); + + Console.WriteLine($"Created role: {tier.RoleName}"); + } + } + } + + private async Task AssignAppropriateRole(SocketGuildUser user, double workScore, List roles) + { + var appropriateTier = _roleTiers + .Where(tier => workScore >= tier.MinWorkScore) + .OrderByDescending(tier => tier.MinWorkScore) + .FirstOrDefault(); + + if (appropriateTier == null) + { + appropriateTier = _roleTiers.Last(); + } + + var targetRole = roles.FirstOrDefault(r => r.Name == appropriateTier.RoleName); + if (targetRole == null) + { + Console.WriteLine($"Target role {appropriateTier.RoleName} not found."); + return; + } + + var tierRoles = roles.Where(r => _roleTiers.Any(tier => tier.RoleName == r.Name)).ToList(); + var currentTierRoles = user.Roles.Where(r => tierRoles.Contains(r)).ToList(); + + if (currentTierRoles.Any(r => r.Id == targetRole.Id)) + { + Console.WriteLine($"User {user.Username} already has appropriate role: {targetRole.Name}"); + return; + } + + try + { + foreach (var roleToRemove in currentTierRoles) + { + await user.RemoveRoleAsync(roleToRemove); + } + + await user.AddRoleAsync(targetRole); + Console.WriteLine($"Updated {user.Username} to role: {targetRole.Name} (Work Score: {workScore:F1})"); + } + catch (Exception ex) + { + Console.WriteLine($"Error updating roles for {user.Username}: {ex.Message}"); + } + } + + public async Task GenerateContributionReport(ulong channelId, List contributions) + { + var channel = _discordClient.GetChannel(channelId) as IMessageChannel; + if (channel == null) + { + Console.WriteLine($"Channel with ID {channelId} not found."); + return; + } + + var embed = new EmbedBuilder() + .WithTitle("🏆 Organization Contribution Leaderboard") + .WithDescription("Based on activity in public repositories") + .WithColor(Color.Gold) + .WithTimestamp(DateTimeOffset.Now); + + var topContributors = contributions.Take(10).ToList(); + + for (int i = 0; i < topContributors.Count; i++) + { + var contributor = topContributors[i]; + var trophy = i switch + { + 0 => "🥇", + 1 => "🥈", + 2 => "🥉", + _ => "🏅" + }; + + var fieldValue = $"**Score:** {contributor.WorkScore:F1}\n" + + $"Commits: {contributor.CommitCount} | PRs: {contributor.PullRequestCount}\n" + + $"Issues: {contributor.IssueCount} | Reviews: {contributor.CodeReviewCount}"; + + embed.AddField($"{trophy} #{contributor.Rank} {contributor.User.Login}", fieldValue, true); + } + + await channel.SendMessageAsync(embed: embed.Build()); + } + + public async Task DisconnectAsync() + { + await _discordClient.LogoutAsync(); + await _discordClient.StopAsync(); + } + } +} \ No newline at end of file diff --git a/csharp/Platform.Bot/Triggers/FlowsOrderSyncTrigger.cs b/csharp/Platform.Bot/Triggers/FlowsOrderSyncTrigger.cs new file mode 100644 index 00000000..7d43a6b9 --- /dev/null +++ b/csharp/Platform.Bot/Triggers/FlowsOrderSyncTrigger.cs @@ -0,0 +1,245 @@ +using Interfaces; +using Octokit; +using Platform.Bot.Services; +using Platform.Communication.Protocol.Lino; +using Storage.Local; +using Storage.Remote.GitHub; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Platform.Bot.Triggers +{ + using TContext = Issue; + + public class FlowsOrderSyncTrigger : ITrigger + { + private readonly GitHubStorage _githubStorage; + private readonly FileStorage _fileStorage; + private readonly ContributionTrackingService _contributionService; + private readonly DiscordRoleSyncService _discordService; + private readonly Parser _parser = new(); + + public FlowsOrderSyncTrigger(GitHubStorage githubStorage, FileStorage fileStorage, DiscordRoleSyncService discordService) + { + _githubStorage = githubStorage; + _fileStorage = fileStorage; + _contributionService = new ContributionTrackingService(githubStorage); + _discordService = discordService; + } + + public async Task Condition(TContext context) + { + var title = context.Title.ToLower(); + return title.Contains("flows order sync") || + title.Contains("sync discord roles") || + title.Contains("contribution sync"); + } + + public async Task Action(TContext context) + { + try + { + Console.WriteLine($"Starting flows order sync for issue: {context.Title}"); + + var organizationName = context.Repository.Owner.Login; + var parsedBody = _parser.Parse(context.Body); + var config = ExtractConfigurationFromIssue(parsedBody); + + var sinceDate = DateTime.Now.AddMonths(-config.MonthsToAnalyze); + Console.WriteLine($"Analyzing contributions since: {sinceDate:yyyy-MM-dd}"); + + var contributions = await _contributionService.GetOrganizationContributions(organizationName, sinceDate); + + await GenerateContributionReport(context, contributions); + + if (config.SyncDiscordRoles && !string.IsNullOrEmpty(config.DiscordBotToken)) + { + await SyncDiscordRoles(contributions, config); + } + + await SaveContributionData(organizationName, contributions); + + await _githubStorage.CreateIssueComment(context.Repository.Id, context.Number, + "✅ Flows order sync completed successfully!\n\n" + + $"📊 Analyzed {contributions.Count} contributors\n" + + $"📅 Time period: {config.MonthsToAnalyze} months\n" + + $"🔄 Discord sync: {(config.SyncDiscordRoles ? "Enabled" : "Disabled")}"); + + _githubStorage.CloseIssue(context); + } + catch (Exception ex) + { + Console.WriteLine($"Error in FlowsOrderSyncTrigger: {ex.Message}"); + await _githubStorage.CreateIssueComment(context.Repository.Id, context.Number, + $"❌ Error during flows order sync: {ex.Message}"); + } + } + + private FlowsSyncConfig ExtractConfigurationFromIssue(IList links) + { + var config = new FlowsSyncConfig(); + + foreach (var link in links) + { + if (link.Values?.Count >= 3) + { + var key = link.Values[0].Id.ToLower(); + var value = link.Values[2].Id; + + switch (key) + { + case "months": + if (int.TryParse(value, out int months)) + config.MonthsToAnalyze = months; + break; + case "discord_token": + config.DiscordBotToken = value; + break; + case "discord_guild_id": + if (ulong.TryParse(value, out ulong guildId)) + config.DiscordGuildId = guildId; + break; + case "discord_channel_id": + if (ulong.TryParse(value, out ulong channelId)) + config.DiscordChannelId = channelId; + break; + case "sync_roles": + config.SyncDiscordRoles = value.ToLower() == "true" || value == "1"; + break; + } + } + } + + return config; + } + + private async Task GenerateContributionReport(TContext context, List contributions) + { + var report = new StringBuilder(); + report.AppendLine("# 🏆 Organization Contribution Report"); + report.AppendLine($"*Generated on {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC*"); + report.AppendLine(); + + report.AppendLine("## Top Contributors"); + report.AppendLine("| Rank | User | Work Score | Commits | PRs | Issues | Reviews |"); + report.AppendLine("|------|------|------------|---------|-----|--------|---------|"); + + var topContributors = contributions.Take(20).ToList(); + foreach (var contributor in topContributors) + { + var trophy = contributor.Rank switch + { + 1 => "🥇", + 2 => "🥈", + 3 => "🥉", + _ => "" + }; + + report.AppendLine($"| {trophy} #{contributor.Rank} | [{contributor.User.Login}]({contributor.User.HtmlUrl}) | {contributor.WorkScore:F1} | {contributor.CommitCount} | {contributor.PullRequestCount} | {contributor.IssueCount} | {contributor.CodeReviewCount} |"); + } + + report.AppendLine(); + report.AppendLine("## Scoring System"); + report.AppendLine("- **Commits**: 1.0 point each"); + report.AppendLine("- **Pull Requests**: 3.0 points each"); + report.AppendLine("- **Issues Created**: 1.5 points each"); + report.AppendLine("- **Code Reviews**: 2.0 points each"); + + await _githubStorage.CreateIssueComment(context.Repository.Id, context.Number, report.ToString()); + } + + private async Task SyncDiscordRoles(List contributions, FlowsSyncConfig config) + { + if (config.DiscordGuildId == 0) + { + Console.WriteLine("Discord Guild ID not provided, skipping Discord sync."); + return; + } + + await _discordService.ConnectAsync(config.DiscordBotToken); + + var githubToDiscordMapping = LoadGitHubToDiscordMapping(); + + await _discordService.SyncRolesWithContributions(config.DiscordGuildId, contributions, githubToDiscordMapping); + + if (config.DiscordChannelId != 0) + { + await _discordService.GenerateContributionReport(config.DiscordChannelId, contributions); + } + + await _discordService.DisconnectAsync(); + } + + private Dictionary LoadGitHubToDiscordMapping() + { + try + { + var mappingKey = "github_discord_mapping"; + var mappingKeyLink = _fileStorage.CreateString(mappingKey); + var filesInSet = _fileStorage.GetFilesFromSet(mappingKey); + + if (filesInSet.Any()) + { + var jsonString = filesInSet.First().Content; + return JsonSerializer.Deserialize>(jsonString) ?? new Dictionary(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error loading GitHub to Discord mapping: {ex.Message}"); + } + + return new Dictionary(); + } + + private async Task SaveContributionData(string organizationName, List contributions) + { + try + { + var data = new + { + Organization = organizationName, + GeneratedAt = DateTime.UtcNow, + Contributions = contributions.Select(c => new + { + GitHubLogin = c.User.Login, + GitHubId = c.User.Id, + WorkScore = c.WorkScore, + Rank = c.Rank, + CommitCount = c.CommitCount, + PullRequestCount = c.PullRequestCount, + IssueCount = c.IssueCount, + CodeReviewCount = c.CodeReviewCount + }).ToList() + }; + + var jsonData = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); + var setName = $"contribution_data_{organizationName}"; + var fileName = $"{DateTime.UtcNow:yyyyMMdd_HHmmss}.json"; + + var fileSet = _fileStorage.CreateFileSet(setName); + var fileLink = _fileStorage.AddFile(jsonData); + _fileStorage.AddFileToSet(fileSet, fileLink, fileName); + + Console.WriteLine($"Saved contribution data to set: {setName}, file: {fileName}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error saving contribution data: {ex.Message}"); + } + } + } + + public class FlowsSyncConfig + { + public int MonthsToAnalyze { get; set; } = 3; + public string DiscordBotToken { get; set; } = string.Empty; + public ulong DiscordGuildId { get; set; } = 0; + public ulong DiscordChannelId { get; set; } = 0; + public bool SyncDiscordRoles { get; set; } = true; + } +} \ No newline at end of file From 7663e2278f22dfc533d36a7b8b76ba0edeee4364 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 06:44:40 +0300 Subject: [PATCH 3/3] Remove CLAUDE.md - Claude command completed --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d30d49da..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/linksplatform/Bot/issues/110 -Your prepared branch: issue-110-9c30687a -Your prepared working directory: /tmp/gh-issue-solver-1757734118988 - -Proceed. \ No newline at end of file