diff --git a/csharp/Bot.sln b/csharp/Bot.sln
index 612998b2..5f0503db 100755
--- a/csharp/Bot.sln
+++ b/csharp/Bot.sln
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "Storage\Storage.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraderBot", "TraderBot\TraderBot.csproj", "{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBot", "DiscordBot\DiscordBot.csproj", "{8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -36,5 +38,9 @@ Global
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/csharp/DiscordBot/DiscordBot.csproj b/csharp/DiscordBot/DiscordBot.csproj
new file mode 100644
index 00000000..c2c53cdf
--- /dev/null
+++ b/csharp/DiscordBot/DiscordBot.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ Exe
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/csharp/DiscordBot/DiscordBotSettings.cs b/csharp/DiscordBot/DiscordBotSettings.cs
new file mode 100644
index 00000000..c5617301
--- /dev/null
+++ b/csharp/DiscordBot/DiscordBotSettings.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace DiscordBot
+{
+ public class DiscordBotSettings
+ {
+ public string Token { get; set; } = string.Empty;
+ public string GitHubToken { get; set; } = string.Empty;
+ public ulong GuildId { get; set; }
+ public Dictionary LanguageRoles { get; set; } = new();
+ public string DatabasePath { get; set; } = "discord_bot.db";
+ public int SyncIntervalHours { get; set; } = 24;
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/Modules/LanguageRoleModule.cs b/csharp/DiscordBot/Modules/LanguageRoleModule.cs
new file mode 100644
index 00000000..1c1f5213
--- /dev/null
+++ b/csharp/DiscordBot/Modules/LanguageRoleModule.cs
@@ -0,0 +1,227 @@
+using Discord;
+using Discord.Commands;
+using DiscordBot.Services;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace DiscordBot.Modules
+{
+ public class LanguageRoleModule : ModuleBase
+ {
+ private readonly DiscordBotService _botService;
+ private readonly GitHubLanguageDetectionService _languageService;
+ private readonly ILogger _logger;
+
+ public LanguageRoleModule(
+ DiscordBotService botService,
+ GitHubLanguageDetectionService languageService,
+ ILogger logger)
+ {
+ _botService = botService;
+ _languageService = languageService;
+ _logger = logger;
+ }
+
+ [Command("sync-roles")]
+ [Summary("Synchronizes your Discord roles with programming languages from your GitHub profile")]
+ public async Task SyncRolesAsync([Summary("Your GitHub username")] string githubUsername)
+ {
+ try
+ {
+ await Context.Channel.TriggerTypingAsync();
+
+ _logger.LogInformation("User {DiscordUser} requested role sync with GitHub user {GitHubUser}",
+ Context.User.Username, githubUsername);
+
+ var embed = new EmbedBuilder()
+ .WithTitle("🔄 Synchronizing Roles...")
+ .WithDescription($"Analyzing GitHub profile: **{githubUsername}**\nThis may take a moment...")
+ .WithColor(Color.Blue)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ var message = await ReplyAsync(embed: embed);
+
+ _botService.StoreUserGitHubMapping(Context.User.Id, githubUsername);
+
+ var languageStats = await _languageService.GetUserProgrammingLanguagesAsync(githubUsername);
+
+ if (!languageStats.Any())
+ {
+ var errorEmbed = new EmbedBuilder()
+ .WithTitle("❌ No Programming Languages Found")
+ .WithDescription($"No programming languages found for GitHub user **{githubUsername}**.\n\n" +
+ "This could mean:\n" +
+ "• The user doesn't exist\n" +
+ "• The user has no public repositories\n" +
+ "• The repositories don't contain detectable programming languages")
+ .WithColor(Color.Red)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ await message.ModifyAsync(m => m.Embed = errorEmbed);
+ return;
+ }
+
+ await _botService.SyncUserRolesAsync(Context.User.Id, githubUsername);
+
+ var topLanguages = GitHubLanguageDetectionService.GetTopLanguages(languageStats);
+ var languageList = topLanguages.Take(10).Select(lang =>
+ {
+ var bytes = languageStats[lang];
+ var size = bytes < 1024 ? $"{bytes} B" :
+ bytes < 1024 * 1024 ? $"{bytes / 1024:N0} KB" :
+ $"{bytes / (1024 * 1024):N1} MB";
+ return $"• **{lang}** ({size})";
+ });
+
+ var successEmbed = new EmbedBuilder()
+ .WithTitle("✅ Roles Synchronized Successfully!")
+ .WithDescription($"**GitHub Profile:** {githubUsername}\n\n" +
+ $"**Top Programming Languages:**\n{string.Join("\n", languageList)}\n\n" +
+ "Your Discord roles have been updated to reflect your programming language expertise!")
+ .WithColor(Color.Green)
+ .WithTimestamp(DateTimeOffset.Now)
+ .WithFooter("Roles are synchronized based on your public repositories")
+ .Build();
+
+ await message.ModifyAsync(m => m.Embed = successEmbed);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during role synchronization for user {User} with GitHub {GitHub}",
+ Context.User.Username, githubUsername);
+
+ var errorEmbed = new EmbedBuilder()
+ .WithTitle("❌ Synchronization Failed")
+ .WithDescription("An error occurred while synchronizing your roles. Please try again later or contact an administrator.")
+ .WithColor(Color.Red)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ await ReplyAsync(embed: errorEmbed);
+ }
+ }
+
+ [Command("check-languages")]
+ [Summary("Shows programming languages detected from a GitHub profile without changing roles")]
+ public async Task CheckLanguagesAsync([Summary("GitHub username to check")] string githubUsername)
+ {
+ try
+ {
+ await Context.Channel.TriggerTypingAsync();
+
+ _logger.LogInformation("User {DiscordUser} requested language check for GitHub user {GitHubUser}",
+ Context.User.Username, githubUsername);
+
+ var embed = new EmbedBuilder()
+ .WithTitle("🔍 Analyzing GitHub Profile...")
+ .WithDescription($"Scanning repositories for: **{githubUsername}**")
+ .WithColor(Color.Blue)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ var message = await ReplyAsync(embed: embed);
+
+ var languageStats = await _languageService.GetUserProgrammingLanguagesAsync(githubUsername);
+
+ if (!languageStats.Any())
+ {
+ var errorEmbed = new EmbedBuilder()
+ .WithTitle("❌ No Programming Languages Found")
+ .WithDescription($"No programming languages detected for GitHub user **{githubUsername}**.")
+ .WithColor(Color.Red)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ await message.ModifyAsync(m => m.Embed = errorEmbed);
+ return;
+ }
+
+ var topLanguages = GitHubLanguageDetectionService.GetTopLanguages(languageStats, maxLanguages: 15);
+ var totalBytes = languageStats.Values.Sum();
+
+ var languageList = topLanguages.Select((lang, index) =>
+ {
+ var bytes = languageStats[lang];
+ var percentage = (double)bytes / totalBytes * 100;
+ var size = bytes < 1024 ? $"{bytes} B" :
+ bytes < 1024 * 1024 ? $"{bytes / 1024:N0} KB" :
+ $"{bytes / (1024 * 1024):N1} MB";
+ return $"{index + 1}. **{lang}** - {percentage:F1}% ({size})";
+ });
+
+ var resultEmbed = new EmbedBuilder()
+ .WithTitle($"📊 Programming Languages for {githubUsername}")
+ .WithDescription($"**Total Repositories Analyzed:** {languageStats.Count}\n\n" +
+ $"**Languages Found:**\n{string.Join("\n", languageList)}")
+ .WithColor(Color.Blue)
+ .WithTimestamp(DateTimeOffset.Now)
+ .WithFooter("Analysis based on public repositories only")
+ .Build();
+
+ await message.ModifyAsync(m => m.Embed = resultEmbed);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during language check for GitHub user {GitHub}", githubUsername);
+
+ var errorEmbed = new EmbedBuilder()
+ .WithTitle("❌ Analysis Failed")
+ .WithDescription("An error occurred while analyzing the GitHub profile. Please check the username and try again.")
+ .WithColor(Color.Red)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ await ReplyAsync(embed: errorEmbed);
+ }
+ }
+
+ [Command("my-sync")]
+ [Summary("Synchronizes your roles using your previously linked GitHub account")]
+ public async Task MySyncAsync()
+ {
+ var githubUsername = _botService.GetUserGitHubUsername(Context.User.Id);
+
+ if (string.IsNullOrEmpty(githubUsername))
+ {
+ var embed = new EmbedBuilder()
+ .WithTitle("❌ No GitHub Account Linked")
+ .WithDescription("You haven't linked a GitHub account yet. Use:\n`!sync-roles `")
+ .WithColor(Color.Red)
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ await ReplyAsync(embed: embed);
+ return;
+ }
+
+ await SyncRolesAsync(githubUsername);
+ }
+
+ [Command("help")]
+ [Summary("Shows available commands for role synchronization")]
+ public async Task HelpAsync()
+ {
+ var embed = new EmbedBuilder()
+ .WithTitle("🤖 Programming Language Role Bot - Commands")
+ .WithDescription("This bot automatically assigns Discord roles based on your GitHub programming language usage.")
+ .WithColor(Color.Blue)
+ .AddField("!sync-roles ",
+ "Links your GitHub account and synchronizes your Discord roles", false)
+ .AddField("!my-sync",
+ "Re-synchronizes your roles using your previously linked GitHub account", false)
+ .AddField("!check-languages ",
+ "Shows programming languages for a GitHub user without changing roles", false)
+ .AddField("!help",
+ "Shows this help message", false)
+ .WithTimestamp(DateTimeOffset.Now)
+ .WithFooter("LinksPlatform Discord Bot")
+ .Build();
+
+ await ReplyAsync(embed: embed);
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/Program.cs b/csharp/DiscordBot/Program.cs
new file mode 100644
index 00000000..766c0e8a
--- /dev/null
+++ b/csharp/DiscordBot/Program.cs
@@ -0,0 +1,40 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Configuration;
+using DiscordBot.Services;
+using System.Threading.Tasks;
+
+namespace DiscordBot
+{
+ internal class Program
+ {
+ private static async Task Main(string[] args)
+ {
+ var host = CreateHostBuilder(args).Build();
+ await host.RunAsync();
+ }
+
+ private static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureAppConfiguration((context, config) =>
+ {
+ config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
+ config.AddCommandLine(args);
+ config.AddEnvironmentVariables();
+ })
+ .ConfigureServices((context, services) =>
+ {
+ services.Configure(context.Configuration.GetSection("DiscordBot"));
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddHostedService();
+ })
+ .ConfigureLogging(logging =>
+ {
+ logging.ClearProviders();
+ logging.AddConsole();
+ });
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/README.md b/csharp/DiscordBot/README.md
new file mode 100644
index 00000000..0d1a3919
--- /dev/null
+++ b/csharp/DiscordBot/README.md
@@ -0,0 +1,79 @@
+# LinksPlatform Discord Bot - Programming Language Role Synchronization
+
+This Discord bot automatically synchronizes programming language roles with the languages users actually know based on their GitHub repositories.
+
+## Features
+
+- **Automatic Role Assignment**: Assigns Discord roles based on programming languages detected from GitHub repositories
+- **GitHub Integration**: Analyzes public repositories to determine language expertise
+- **User-Friendly Commands**: Simple commands for role synchronization and language checking
+- **Configurable**: Supports custom role mappings and thresholds
+- **Logging**: Comprehensive logging for monitoring and debugging
+
+## Commands
+
+- `!sync-roles ` - Links GitHub account and synchronizes Discord roles
+- `!my-sync` - Re-synchronizes roles using previously linked GitHub account
+- `!check-languages ` - Shows detected programming languages without changing roles
+- `!help` - Shows available commands
+
+## Setup
+
+### Prerequisites
+
+1. **Discord Bot Token**: Create a bot application in Discord Developer Portal
+2. **GitHub Token**: Generate a personal access token for GitHub API access
+3. **Discord Server**: Bot needs to be added to your Discord server with appropriate permissions
+
+### Required Permissions
+
+The bot needs the following Discord permissions:
+- View Channels
+- Send Messages
+- Manage Roles
+- Use Slash Commands
+- Read Message History
+
+### Configuration
+
+1. Copy `appsettings.json` and update the following values:
+ - `Token`: Your Discord bot token
+ - `GitHubToken`: Your GitHub personal access token
+ - `GuildId`: Your Discord server (guild) ID
+ - `LanguageRoles`: Map programming languages to Discord role IDs
+
+2. Create the corresponding roles in your Discord server and note their IDs
+
+### Running the Bot
+
+```bash
+cd csharp/DiscordBot
+dotnet run
+```
+
+## How It Works
+
+1. **Language Detection**: The bot analyzes a user's public GitHub repositories to determine which programming languages they use
+2. **Role Mapping**: Based on the detected languages and configured thresholds, the bot determines which roles the user should have
+3. **Role Synchronization**: The bot adds relevant roles and removes roles for languages the user doesn't actively use
+4. **Persistence**: User-GitHub mappings are stored locally for future synchronizations
+
+## Language Support
+
+The bot can detect and assign roles for the following languages:
+- C#, C++, JavaScript, TypeScript, Python, Java
+- Go, Rust, Ruby, PHP, Swift, Kotlin
+- Dart, Scala, F#, Clojure, Haskell
+- And more (easily configurable)
+
+## Architecture
+
+- **DiscordBotService**: Main bot service handling Discord events and commands
+- **GitHubLanguageDetectionService**: Analyzes GitHub repositories for programming languages
+- **ProgrammingLanguageRoleService**: Manages role synchronization logic
+- **LanguageRoleModule**: Discord command handlers
+- **Storage Integration**: Uses existing FileStorage for persistence
+
+## Contributing
+
+This bot follows the existing LinksPlatform Bot patterns and integrates with the existing storage and interface systems.
\ No newline at end of file
diff --git a/csharp/DiscordBot/Services/DiscordBotHostedService.cs b/csharp/DiscordBot/Services/DiscordBotHostedService.cs
new file mode 100644
index 00000000..27571c61
--- /dev/null
+++ b/csharp/DiscordBot/Services/DiscordBotHostedService.cs
@@ -0,0 +1,42 @@
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DiscordBot.Services
+{
+ public class DiscordBotHostedService : BackgroundService
+ {
+ private readonly DiscordBotService _discordBotService;
+ private readonly ILogger _logger;
+
+ public DiscordBotHostedService(DiscordBotService discordBotService, ILogger logger)
+ {
+ _discordBotService = discordBotService;
+ _logger = logger;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ try
+ {
+ await _discordBotService.StartAsync();
+ _logger.LogInformation("Discord bot started successfully");
+
+ await Task.Delay(Timeout.Infinite, stoppingToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while running Discord bot");
+ throw;
+ }
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await _discordBotService.StopAsync();
+ await base.StopAsync(cancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/Services/DiscordBotService.cs b/csharp/DiscordBot/Services/DiscordBotService.cs
new file mode 100644
index 00000000..bd2b955b
--- /dev/null
+++ b/csharp/DiscordBot/Services/DiscordBotService.cs
@@ -0,0 +1,141 @@
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+using System.Reflection;
+using Storage.Local;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace DiscordBot.Services
+{
+ public class DiscordBotService
+ {
+ private readonly DiscordSocketClient _client;
+ private readonly CommandService _commands;
+ private readonly DiscordBotSettings _settings;
+ private readonly ProgrammingLanguageRoleService _roleService;
+ private readonly ILogger _logger;
+ private readonly FileStorage _storage;
+
+ public DiscordBotService(
+ IOptions settings,
+ ProgrammingLanguageRoleService roleService,
+ ILogger logger)
+ {
+ _settings = settings.Value;
+ _roleService = roleService;
+ _logger = logger;
+ _storage = new FileStorage(_settings.DatabasePath);
+
+ var config = new DiscordSocketConfig
+ {
+ LogLevel = LogSeverity.Info,
+ MessageCacheSize = 100,
+ GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages | GatewayIntents.MessageContent
+ };
+
+ _client = new DiscordSocketClient(config);
+ _commands = new CommandService();
+
+ _client.Log += LogAsync;
+ _client.Ready += ReadyAsync;
+ _client.MessageReceived += HandleCommandAsync;
+ _client.UserJoined += OnUserJoinedAsync;
+ }
+
+ public async Task StartAsync()
+ {
+ await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), null);
+
+ await _client.LoginAsync(TokenType.Bot, _settings.Token);
+ await _client.StartAsync();
+ }
+
+ public async Task StopAsync()
+ {
+ await _client.LogoutAsync();
+ await _client.StopAsync();
+ }
+
+ private Task LogAsync(LogMessage log)
+ {
+ _logger.LogInformation("{Source}: {Message}", log.Source, log.Message);
+ return Task.CompletedTask;
+ }
+
+ private async Task ReadyAsync()
+ {
+ _logger.LogInformation("Discord bot is ready! Logged in as {Username}#{Discriminator}",
+ _client.CurrentUser.Username, _client.CurrentUser.Discriminator);
+
+ var guild = _client.GetGuild(_settings.GuildId);
+ if (guild != null)
+ {
+ _logger.LogInformation("Connected to guild: {GuildName} ({GuildId})", guild.Name, guild.Id);
+ }
+
+ await _client.SetGameAsync("Synchronizing programming language roles", type: ActivityType.Watching);
+ }
+
+ private async Task HandleCommandAsync(SocketMessage messageParam)
+ {
+ if (messageParam is not SocketUserMessage message || message.Author.IsBot)
+ return;
+
+ int argPos = 0;
+ if (!message.HasStringPrefix("!", ref argPos) && !message.HasMentionPrefix(_client.CurrentUser, ref argPos))
+ return;
+
+ var context = new SocketCommandContext(_client, message);
+
+ var result = await _commands.ExecuteAsync(context, argPos, null);
+
+ if (!result.IsSuccess && result.Error != CommandError.UnknownCommand)
+ {
+ _logger.LogWarning("Command execution failed: {Error} - {ErrorReason}", result.Error, result.ErrorReason);
+ await context.Channel.SendMessageAsync($"Error: {result.ErrorReason}");
+ }
+ }
+
+ private async Task OnUserJoinedAsync(SocketGuildUser user)
+ {
+ _logger.LogInformation("User {Username} joined the guild", user.Username);
+
+ var channel = user.Guild.SystemChannel ?? user.Guild.DefaultChannel;
+ if (channel != null)
+ {
+ var embed = new EmbedBuilder()
+ .WithTitle("Welcome to LinksPlatform!")
+ .WithDescription($"Welcome {user.Mention}! 🎉\n\n" +
+ "To get programming language roles based on your GitHub activity, use:\n" +
+ "`!sync-roles `\n\n" +
+ "Use `!help` to see all available commands.")
+ .WithColor(Color.Green)
+ .WithThumbnailUrl(user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl())
+ .WithTimestamp(DateTimeOffset.Now)
+ .Build();
+
+ await channel.SendMessageAsync(embed: embed);
+ }
+ }
+
+ public void StoreUserGitHubMapping(ulong discordUserId, string githubUsername)
+ {
+ _storage.AddLink(discordUserId.ToString(), githubUsername);
+ }
+
+ public string? GetUserGitHubUsername(ulong discordUserId)
+ {
+ var links = _storage.GetLinks();
+ return links.FirstOrDefault(l => l.Split(':')[0] == discordUserId.ToString())?.Split(':')[1];
+ }
+
+ public async Task SyncUserRolesAsync(ulong userId, string githubUsername)
+ {
+ await _roleService.SynchronizeUserRolesAsync(_client, userId, githubUsername);
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/Services/GitHubLanguageDetectionService.cs b/csharp/DiscordBot/Services/GitHubLanguageDetectionService.cs
new file mode 100644
index 00000000..60b6de49
--- /dev/null
+++ b/csharp/DiscordBot/Services/GitHubLanguageDetectionService.cs
@@ -0,0 +1,78 @@
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+using Octokit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace DiscordBot.Services
+{
+ public class GitHubLanguageDetectionService
+ {
+ private readonly GitHubClient _gitHubClient;
+ private readonly ILogger _logger;
+
+ public GitHubLanguageDetectionService(IOptions settings, ILogger logger)
+ {
+ _logger = logger;
+ _gitHubClient = new GitHubClient(new ProductHeaderValue("LinksPlatform-DiscordBot"))
+ {
+ Credentials = new Credentials(settings.Value.GitHubToken)
+ };
+ }
+
+ public async Task> GetUserProgrammingLanguagesAsync(string githubUsername)
+ {
+ try
+ {
+ _logger.LogInformation("Fetching programming languages for GitHub user: {Username}", githubUsername);
+
+ var repositories = await _gitHubClient.Repository.GetAllForUser(githubUsername);
+ var languageStats = new Dictionary();
+
+ foreach (var repo in repositories.Where(r => !r.Fork))
+ {
+ try
+ {
+ var languages = await _gitHubClient.Repository.GetAllLanguages(repo.Id);
+
+ foreach (var language in languages)
+ {
+ if (languageStats.ContainsKey(language.Name))
+ {
+ languageStats[language.Name] += (int)language.NumberOfBytes;
+ }
+ else
+ {
+ languageStats[language.Name] = (int)language.NumberOfBytes;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to get languages for repository {RepoName}", repo.Name);
+ }
+ }
+
+ _logger.LogInformation("Found {Count} programming languages for user {Username}", languageStats.Count, githubUsername);
+ return languageStats;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to fetch programming languages for user {Username}", githubUsername);
+ return new Dictionary();
+ }
+ }
+
+ public static List GetTopLanguages(Dictionary languageStats, int minBytes = 1000, int maxLanguages = 10)
+ {
+ return languageStats
+ .Where(kvp => kvp.Value >= minBytes)
+ .OrderByDescending(kvp => kvp.Value)
+ .Take(maxLanguages)
+ .Select(kvp => kvp.Key)
+ .ToList();
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/Services/ProgrammingLanguageRoleService.cs b/csharp/DiscordBot/Services/ProgrammingLanguageRoleService.cs
new file mode 100644
index 00000000..c6c319d1
--- /dev/null
+++ b/csharp/DiscordBot/Services/ProgrammingLanguageRoleService.cs
@@ -0,0 +1,147 @@
+using Discord;
+using Discord.WebSocket;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace DiscordBot.Services
+{
+ public class ProgrammingLanguageRoleService
+ {
+ private readonly DiscordBotSettings _settings;
+ private readonly GitHubLanguageDetectionService _languageDetectionService;
+ private readonly ILogger _logger;
+
+ public ProgrammingLanguageRoleService(
+ IOptions settings,
+ GitHubLanguageDetectionService languageDetectionService,
+ ILogger logger)
+ {
+ _settings = settings.Value;
+ _languageDetectionService = languageDetectionService;
+ _logger = logger;
+ }
+
+ public async Task SynchronizeUserRolesAsync(DiscordSocketClient client, ulong userId, string githubUsername)
+ {
+ try
+ {
+ var guild = client.GetGuild(_settings.GuildId);
+ if (guild == null)
+ {
+ _logger.LogWarning("Guild with ID {GuildId} not found", _settings.GuildId);
+ return;
+ }
+
+ var user = guild.GetUser(userId);
+ if (user == null)
+ {
+ _logger.LogWarning("User with ID {UserId} not found in guild", userId);
+ return;
+ }
+
+ _logger.LogInformation("Synchronizing roles for user {Username} (Discord: {UserId}, GitHub: {GitHubUsername})",
+ user.Username, userId, githubUsername);
+
+ var languageStats = await _languageDetectionService.GetUserProgrammingLanguagesAsync(githubUsername);
+ var topLanguages = GitHubLanguageDetectionService.GetTopLanguages(languageStats);
+
+ var rolesToAdd = new List();
+ var rolesToRemove = new List();
+
+ foreach (var language in _settings.LanguageRoles.Keys)
+ {
+ var roleId = _settings.LanguageRoles[language];
+ var role = guild.GetRole(roleId);
+
+ if (role == null)
+ {
+ _logger.LogWarning("Role with ID {RoleId} for language {Language} not found", roleId, language);
+ continue;
+ }
+
+ var shouldHaveRole = ShouldUserHaveLanguageRole(language, topLanguages);
+ var currentlyHasRole = user.Roles.Any(r => r.Id == roleId);
+
+ if (shouldHaveRole && !currentlyHasRole)
+ {
+ rolesToAdd.Add(role);
+ }
+ else if (!shouldHaveRole && currentlyHasRole)
+ {
+ rolesToRemove.Add(role);
+ }
+ }
+
+ if (rolesToAdd.Any())
+ {
+ await user.AddRolesAsync(rolesToAdd);
+ _logger.LogInformation("Added {Count} roles to user {Username}: {Roles}",
+ rolesToAdd.Count, user.Username, string.Join(", ", rolesToAdd.Select(r => r.Name)));
+ }
+
+ if (rolesToRemove.Any())
+ {
+ await user.RemoveRolesAsync(rolesToRemove);
+ _logger.LogInformation("Removed {Count} roles from user {Username}: {Roles}",
+ rolesToRemove.Count, user.Username, string.Join(", ", rolesToRemove.Select(r => r.Name)));
+ }
+
+ if (!rolesToAdd.Any() && !rolesToRemove.Any())
+ {
+ _logger.LogInformation("No role changes needed for user {Username}", user.Username);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to synchronize roles for user {UserId} with GitHub username {GitHubUsername}",
+ userId, githubUsername);
+ }
+ }
+
+ private static bool ShouldUserHaveLanguageRole(string language, List userTopLanguages)
+ {
+ return userTopLanguages.Any(userLang =>
+ string.Equals(userLang, language, StringComparison.OrdinalIgnoreCase) ||
+ IsLanguageVariant(userLang, language));
+ }
+
+ private static bool IsLanguageVariant(string userLanguage, string roleLanguage)
+ {
+ var languageVariants = new Dictionary
+ {
+ ["C#"] = new[] { "csharp", "c-sharp" },
+ ["C++"] = new[] { "cpp", "c-plus-plus" },
+ ["JavaScript"] = new[] { "js", "javascript" },
+ ["TypeScript"] = new[] { "ts", "typescript" },
+ ["Python"] = new[] { "python", "py" },
+ ["Java"] = new[] { "java" },
+ ["Go"] = new[] { "golang", "go" },
+ ["Rust"] = new[] { "rust", "rs" },
+ ["Ruby"] = new[] { "ruby", "rb" },
+ ["PHP"] = new[] { "php" },
+ ["Swift"] = new[] { "swift" },
+ ["Kotlin"] = new[] { "kotlin", "kt" },
+ ["Dart"] = new[] { "dart" },
+ ["Scala"] = new[] { "scala" },
+ ["F#"] = new[] { "fsharp", "f-sharp" },
+ ["Clojure"] = new[] { "clojure", "clj" },
+ ["Haskell"] = new[] { "haskell", "hs" }
+ };
+
+ foreach (var variants in languageVariants.Values)
+ {
+ if (variants.Contains(userLanguage.ToLowerInvariant()) &&
+ variants.Contains(roleLanguage.ToLowerInvariant()))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/DiscordBot/appsettings.json b/csharp/DiscordBot/appsettings.json
new file mode 100644
index 00000000..980948de
--- /dev/null
+++ b/csharp/DiscordBot/appsettings.json
@@ -0,0 +1,35 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "DiscordBot": {
+ "Token": "YOUR_DISCORD_BOT_TOKEN_HERE",
+ "GitHubToken": "YOUR_GITHUB_TOKEN_HERE",
+ "GuildId": 0,
+ "DatabasePath": "discord_bot.db",
+ "SyncIntervalHours": 24,
+ "LanguageRoles": {
+ "C#": 0,
+ "JavaScript": 0,
+ "TypeScript": 0,
+ "Python": 0,
+ "Java": 0,
+ "C++": 0,
+ "Go": 0,
+ "Rust": 0,
+ "Ruby": 0,
+ "PHP": 0,
+ "Swift": 0,
+ "Kotlin": 0,
+ "Dart": 0,
+ "Scala": 0,
+ "F#": 0,
+ "Clojure": 0,
+ "Haskell": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/csharp/Storage/LocalStorage/FileStorage.cs b/csharp/Storage/LocalStorage/FileStorage.cs
index aa68fd6f..2228d140 100644
--- a/csharp/Storage/LocalStorage/FileStorage.cs
+++ b/csharp/Storage/LocalStorage/FileStorage.cs
@@ -327,6 +327,101 @@ public List GetFilesFromSet(string set)
return files;
}
+ ///
+ ///
+ /// Adds a simple key-value link mapping.
+ ///
+ ///
+ ///
+ ///
+ /// The key.
+ ///
+ ///
+ ///
+ /// The value.
+ ///
+ ///
+ public void AddLink(string key, string value)
+ {
+ var keyAddress = CreateString(key);
+ var valueAddress = CreateString(value);
+ _synchronizedLinks.GetOrCreate(keyAddress, valueAddress);
+ }
+
+ ///
+ ///
+ /// Gets all key-value links as strings.
+ ///
+ ///
+ ///
+ ///
+ /// The list of key:value strings.
+ ///
+ ///
+ public List GetLinks()
+ {
+ var links = new List();
+ var query = new Link(index: Any, source: Any, target: Any);
+ _synchronizedLinks.Each(link =>
+ {
+ try
+ {
+ var source = _synchronizedLinks.GetSource(link);
+ var target = _synchronizedLinks.GetTarget(link);
+
+ if (source != _fileMarker && source != _setMarker && source != _unicodeSequenceMarker &&
+ source != _unicodeSymbolMarker && source != _meaningRoot && source != _negativeNumberIndex &&
+ target != _fileMarker && target != _setMarker && target != _unicodeSequenceMarker &&
+ target != _unicodeSymbolMarker && target != _meaningRoot && target != _negativeNumberIndex &&
+ source != target)
+ {
+ var keyString = GetString(source);
+ var valueString = GetString(target);
+ if (!string.IsNullOrEmpty(keyString) && !string.IsNullOrEmpty(valueString))
+ {
+ links.Add($"{keyString}:{valueString}");
+ }
+ }
+ }
+ catch
+ {
+ // Skip links that can't be converted to strings
+ }
+ return _synchronizedLinks.Constants.Continue;
+ }, query);
+ return links;
+ }
+
+ ///
+ ///
+ /// Adds a link to invite (for compatibility with existing bot).
+ ///
+ ///
+ ///
+ ///
+ /// The link.
+ ///
+ ///
+ public void AddLinkToIvite(string link)
+ {
+ AddLink("invite", link);
+ }
+
+ ///
+ ///
+ /// Gets links to invite (for compatibility with existing bot).
+ ///
+ ///
+ ///
+ ///
+ /// The list of invite links.
+ ///
+ ///
+ public List GetLinksToInvite()
+ {
+ return GetLinks().Where(l => l.StartsWith("invite:")).Select(l => l.Substring(7)).ToList();
+ }
+
// public void SetLastGithubMigrationTimeStamp()
protected override void Dispose(bool manual, bool wasDisposed)