diff --git a/csharp/Platform.Bot/Program.cs b/csharp/Platform.Bot/Program.cs index 521a6b95..9c4a4739 100644 --- a/csharp/Platform.Bot/Program.cs +++ b/csharp/Platform.Bot/Program.cs @@ -95,7 +95,7 @@ private static async Task Main(string[] args) 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 issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new TopByTechnologyTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage)); 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(); diff --git a/csharp/Platform.Bot/README.md b/csharp/Platform.Bot/README.md index 6ab28651..1fb16aef 100644 --- a/csharp/Platform.Bot/README.md +++ b/csharp/Platform.Bot/README.md @@ -40,3 +40,26 @@ dotnet run MyNickname ghp_123 MyAppName db.links HelloWorldSet ```shell ./run.sh NICKNAME TOKEN APP_NAME ``` + +## Features + +The bot responds to GitHub issues with specific triggers: + +- **Hello World**: Create issues with title "hello world" to test the bot +- **Organization Last Month Activity**: Create issues with title "organization last month activity" to get member activity +- **Top by Technology**: Create issues with title "Top by technology [TECHNOLOGY_NAME]" to get users ranked by their activity with specific technologies + +### Top by Technology + +The "Top by technology" feature analyzes repositories in your organization and ranks users based on: +- Commit activity in repositories that contain the specified technology +- Overall contribution activity weighted by technology usage +- Repository language analysis and file patterns + +Example usage: +- "Top by technology CUDA" - Find users working with CUDA +- "Top by technology Qt" - Find users working with Qt +- "Top by technology Docker" - Find users working with Docker +- "Top by technology React" - Find users working with React + +The bot will analyze the last 3 months of activity and return the top 10 contributors for the specified technology. diff --git a/csharp/Platform.Bot/Triggers/TopByTechnologyTrigger.cs b/csharp/Platform.Bot/Triggers/TopByTechnologyTrigger.cs new file mode 100644 index 00000000..15d0e573 --- /dev/null +++ b/csharp/Platform.Bot/Triggers/TopByTechnologyTrigger.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Interfaces; +using Octokit; +using Platform.Communication.Protocol.Lino; +using Storage.Remote.GitHub; + +namespace Platform.Bot.Triggers +{ + using TContext = Issue; + /// + /// + /// Represents the top by technology trigger. + /// + /// + /// + /// + public class TopByTechnologyTrigger : ITrigger + { + private readonly GitHubStorage _storage; + private readonly Parser _parser = new(); + + /// + /// + /// Initializes a new instance. + /// + /// + /// + /// + /// A storage. + /// + /// + public TopByTechnologyTrigger(GitHubStorage storage) => _storage = storage; + + /// + /// + /// Determines whether this instance condition. + /// + /// + /// + /// + /// The context. + /// + /// + /// + /// The bool + /// + /// + public async Task Condition(TContext context) => context.Title.ToLower().StartsWith("top by technology"); + + /// + /// + /// Actions the context. + /// + /// + /// + /// + /// The context. + /// + /// + public async Task Action(TContext context) + { + var issueService = _storage.Client.Issue; + var owner = context.Repository.Owner.Login; + var technology = ExtractTechnology(context.Title); + + if (string.IsNullOrEmpty(technology)) + { + await issueService.Comment.Create(owner, context.Repository.Name, context.Number, + "Please specify a technology. Example: 'Top by technology CUDA' or 'Top by technology Qt'"); + return; + } + + var topUsers = await GetTopUsersByTechnology(owner, technology); + var responseMessage = BuildTopByTechnologyResponse(technology, topUsers); + + await issueService.Comment.Create(owner, context.Repository.Name, context.Number, responseMessage); + _storage.CloseIssue(context); + } + + /// + /// + /// Extracts technology name from the issue title. + /// + /// + /// + /// + /// The issue title. + /// + /// + /// + /// The technology name or empty string. + /// + /// + private string ExtractTechnology(string title) + { + const string prefix = "top by technology"; + var lowerTitle = title.ToLower(); + if (!lowerTitle.StartsWith(prefix)) + return string.Empty; + + var technology = title.Substring(prefix.Length).Trim(); + return technology; + } + + /// + /// + /// Gets the top users by technology based on repository languages and commit activity. + /// + /// + /// + /// + /// The organization owner. + /// + /// + /// + /// The technology to search for. + /// + /// + /// + /// A dictionary of user login and their score. + /// + /// + private async Task> GetTopUsersByTechnology(string owner, string technology) + { + var userScores = new Dictionary(); + var repositories = await _storage.GetAllRepositories(owner); + var threeMonthsAgo = DateTime.Now.AddMonths(-3); + + foreach (var repository in repositories) + { + try + { + // Check if repository contains the technology + var repositoryContainsTechnology = await RepositoryContainsTechnology(repository, technology); + if (!repositoryContainsTechnology) + continue; + + // Get commits from the last 3 months + var commits = await _storage.GetCommits(repository.Id, new CommitRequest { Since = threeMonthsAgo }); + + foreach (var commit in commits) + { + if (commit.Author?.Login != null) + { + var authorLogin = commit.Author.Login; + if (!userScores.ContainsKey(authorLogin)) + userScores[authorLogin] = 0; + + // Score based on commit activity in technology-related repositories + userScores[authorLogin] += 1; + } + } + + // Additional scoring for repository contributors + var languages = await _storage.Client.Repository.GetAllLanguages(repository.Id); + var technologyWeight = GetTechnologyWeight(languages, technology); + + if (technologyWeight > 0) + { + var contributors = await _storage.Client.Repository.GetAllContributors(repository.Id); + foreach (var contributor in contributors.Take(10)) // Top 10 contributors + { + if (!userScores.ContainsKey(contributor.Login)) + userScores[contributor.Login] = 0; + + userScores[contributor.Login] += contributor.Contributions * technologyWeight / 100; + } + } + } + catch (Exception) + { + // Skip repositories that cause errors (private repos, API limits, etc.) + continue; + } + } + + return userScores; + } + + /// + /// + /// Determines if a repository contains the specified technology. + /// + /// + /// + /// + /// The repository. + /// + /// + /// + /// The technology to check for. + /// + /// + /// + /// True if the repository contains the technology. + /// + /// + private async Task RepositoryContainsTechnology(Repository repository, string technology) + { + try + { + var lowerTechnology = technology.ToLower(); + + // Check repository name and description + if (repository.Name.ToLower().Contains(lowerTechnology) || + (repository.Description?.ToLower().Contains(lowerTechnology) ?? false)) + { + return true; + } + + // Check topics + if (repository.Topics?.Any(topic => topic.ToLower().Contains(lowerTechnology)) ?? false) + { + return true; + } + + // Check languages + var languages = await _storage.Client.Repository.GetAllLanguages(repository.Id); + if (languages.Any(lang => lang.Name.ToLower().Contains(lowerTechnology))) + { + return true; + } + + // Check for common technology file patterns + var technologyPatterns = GetTechnologyFilePatterns(lowerTechnology); + if (technologyPatterns.Any()) + { + try + { + var contents = await _storage.Client.Repository.Content.GetAllContents(repository.Id); + return contents.Any(content => technologyPatterns.Any(pattern => + content.Name.ToLower().Contains(pattern))); + } + catch + { + // If we can't access contents, skip this check + } + } + + return false; + } + catch + { + return false; + } + } + + /// + /// + /// Gets file patterns associated with a technology. + /// + /// + /// + /// + /// The technology name. + /// + /// + /// + /// A list of file patterns. + /// + /// + private List GetTechnologyFilePatterns(string technology) + { + var patterns = new List(); + + switch (technology) + { + case "cuda": + patterns.AddRange(new[] { ".cu", ".cuh", "cuda", "nvcc" }); + break; + case "qt": + patterns.AddRange(new[] { ".pro", ".pri", ".ui", ".qrc", "qmake", "cmake" }); + break; + case "react": + patterns.AddRange(new[] { "react", ".jsx", ".tsx", "package.json" }); + break; + case "docker": + patterns.AddRange(new[] { "dockerfile", "docker-compose", ".dockerignore" }); + break; + case "kubernetes": + case "k8s": + patterns.AddRange(new[] { ".yaml", ".yml", "helm", "kubectl" }); + break; + case "tensorflow": + patterns.AddRange(new[] { "tensorflow", ".pb", ".h5", "keras" }); + break; + case "pytorch": + patterns.AddRange(new[] { "pytorch", "torch", ".pth", ".pt" }); + break; + default: + patterns.Add(technology); + break; + } + + return patterns; + } + + /// + /// + /// Gets the weight of a technology based on repository languages. + /// + /// + /// + /// + /// The repository languages. + /// + /// + /// + /// The technology name. + /// + /// + /// + /// The technology weight. + /// + /// + private int GetTechnologyWeight(IReadOnlyList languages, string technology) + { + var lowerTechnology = technology.ToLower(); + var totalBytes = languages.Sum(l => l.NumberOfBytes); + + if (totalBytes == 0) + return 0; + + foreach (var language in languages) + { + if (language.Name.ToLower().Contains(lowerTechnology)) + { + return (int)((language.NumberOfBytes * 100) / totalBytes); + } + } + + return 0; + } + + /// + /// + /// Builds the response message for top users by technology. + /// + /// + /// + /// + /// The technology name. + /// + /// + /// + /// The user scores dictionary. + /// + /// + /// + /// The formatted response message. + /// + /// + private string BuildTopByTechnologyResponse(string technology, Dictionary userScores) + { + if (!userScores.Any()) + { + return $"No users found with activity in {technology} repositories in the last 3 months."; + } + + var sortedUsers = userScores.OrderByDescending(kv => kv.Value).Take(10); + var response = $"# Top Contributors for {technology}\n\n"; + response += "Based on commits and contributions in repositories containing this technology (last 3 months):\n\n"; + + var rank = 1; + foreach (var user in sortedUsers) + { + response += $"{rank}. @{user.Key} - {user.Value} points\n"; + rank++; + } + + response += "\n*Points are calculated based on commits in technology-related repositories and overall contribution activity.*"; + + return response; + } + } +} \ No newline at end of file