Skip to content

Commit c899674

Browse files
konardclaude
andcommitted
Implement Discord bot with programming language role synchronization
This commit adds a comprehensive Discord bot that synchronizes user roles with programming languages detected from their GitHub repositories. Features: - Analyzes public GitHub repositories to detect programming languages - Automatically assigns/removes Discord roles based on language usage - Supports 15+ programming languages with configurable role mappings - User-friendly commands: !sync-roles, !my-sync, !check-languages, !help - Rich Discord embeds with detailed language statistics - Comprehensive logging and error handling - Integration with existing LinksPlatform storage system Technical implementation: - Modern .NET 8 architecture with dependency injection - Discord.Net library for Discord API integration - Octokit for GitHub API integration - Microsoft.Extensions.Hosting for service lifecycle management - Configurable through appsettings.json - Added supporting methods to FileStorage for key-value mappings 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2d61367 commit c899674

File tree

12 files changed

+926
-0
lines changed

12 files changed

+926
-0
lines changed

csharp/Bot.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Storage", "Storage\Storage.
1010
EndProject
1111
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraderBot", "TraderBot\TraderBot.csproj", "{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}"
1212
EndProject
13+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBot", "DiscordBot\DiscordBot.csproj", "{8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}"
14+
EndProject
1315
Global
1416
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1517
Debug|Any CPU = Debug|Any CPU
@@ -36,5 +38,9 @@ Global
3638
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Debug|Any CPU.Build.0 = Debug|Any CPU
3739
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Release|Any CPU.ActiveCfg = Release|Any CPU
3840
{FAE89FE2-17C5-4AD6-98EC-84002CC4C672}.Release|Any CPU.Build.0 = Release|Any CPU
41+
{8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42+
{8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
43+
{8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
44+
{8B5F7E91-2D44-4F6C-8A5A-1E9C3B4D5F7E}.Release|Any CPU.Build.0 = Release|Any CPU
3945
EndGlobalSection
4046
EndGlobal
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<OutputType>Exe</OutputType>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Discord.Net" Version="3.15.3" />
11+
<PackageReference Include="Octokit" Version="13.0.1" />
12+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
13+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
14+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
15+
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\Storage\Storage.csproj" />
20+
<ProjectReference Include="..\Interfaces\Interfaces.csproj" />
21+
</ItemGroup>
22+
</Project>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Collections.Generic;
2+
3+
namespace DiscordBot
4+
{
5+
public class DiscordBotSettings
6+
{
7+
public string Token { get; set; } = string.Empty;
8+
public string GitHubToken { get; set; } = string.Empty;
9+
public ulong GuildId { get; set; }
10+
public Dictionary<string, ulong> LanguageRoles { get; set; } = new();
11+
public string DatabasePath { get; set; } = "discord_bot.db";
12+
public int SyncIntervalHours { get; set; } = 24;
13+
}
14+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
using Discord;
2+
using Discord.Commands;
3+
using DiscordBot.Services;
4+
using Microsoft.Extensions.Logging;
5+
using System;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
9+
namespace DiscordBot.Modules
10+
{
11+
public class LanguageRoleModule : ModuleBase<SocketCommandContext>
12+
{
13+
private readonly DiscordBotService _botService;
14+
private readonly GitHubLanguageDetectionService _languageService;
15+
private readonly ILogger<LanguageRoleModule> _logger;
16+
17+
public LanguageRoleModule(
18+
DiscordBotService botService,
19+
GitHubLanguageDetectionService languageService,
20+
ILogger<LanguageRoleModule> logger)
21+
{
22+
_botService = botService;
23+
_languageService = languageService;
24+
_logger = logger;
25+
}
26+
27+
[Command("sync-roles")]
28+
[Summary("Synchronizes your Discord roles with programming languages from your GitHub profile")]
29+
public async Task SyncRolesAsync([Summary("Your GitHub username")] string githubUsername)
30+
{
31+
try
32+
{
33+
await Context.Channel.TriggerTypingAsync();
34+
35+
_logger.LogInformation("User {DiscordUser} requested role sync with GitHub user {GitHubUser}",
36+
Context.User.Username, githubUsername);
37+
38+
var embed = new EmbedBuilder()
39+
.WithTitle("🔄 Synchronizing Roles...")
40+
.WithDescription($"Analyzing GitHub profile: **{githubUsername}**\nThis may take a moment...")
41+
.WithColor(Color.Blue)
42+
.WithTimestamp(DateTimeOffset.Now)
43+
.Build();
44+
45+
var message = await ReplyAsync(embed: embed);
46+
47+
_botService.StoreUserGitHubMapping(Context.User.Id, githubUsername);
48+
49+
var languageStats = await _languageService.GetUserProgrammingLanguagesAsync(githubUsername);
50+
51+
if (!languageStats.Any())
52+
{
53+
var errorEmbed = new EmbedBuilder()
54+
.WithTitle("❌ No Programming Languages Found")
55+
.WithDescription($"No programming languages found for GitHub user **{githubUsername}**.\n\n" +
56+
"This could mean:\n" +
57+
"• The user doesn't exist\n" +
58+
"• The user has no public repositories\n" +
59+
"• The repositories don't contain detectable programming languages")
60+
.WithColor(Color.Red)
61+
.WithTimestamp(DateTimeOffset.Now)
62+
.Build();
63+
64+
await message.ModifyAsync(m => m.Embed = errorEmbed);
65+
return;
66+
}
67+
68+
await _botService.SyncUserRolesAsync(Context.User.Id, githubUsername);
69+
70+
var topLanguages = GitHubLanguageDetectionService.GetTopLanguages(languageStats);
71+
var languageList = topLanguages.Take(10).Select(lang =>
72+
{
73+
var bytes = languageStats[lang];
74+
var size = bytes < 1024 ? $"{bytes} B" :
75+
bytes < 1024 * 1024 ? $"{bytes / 1024:N0} KB" :
76+
$"{bytes / (1024 * 1024):N1} MB";
77+
return $"• **{lang}** ({size})";
78+
});
79+
80+
var successEmbed = new EmbedBuilder()
81+
.WithTitle("✅ Roles Synchronized Successfully!")
82+
.WithDescription($"**GitHub Profile:** {githubUsername}\n\n" +
83+
$"**Top Programming Languages:**\n{string.Join("\n", languageList)}\n\n" +
84+
"Your Discord roles have been updated to reflect your programming language expertise!")
85+
.WithColor(Color.Green)
86+
.WithTimestamp(DateTimeOffset.Now)
87+
.WithFooter("Roles are synchronized based on your public repositories")
88+
.Build();
89+
90+
await message.ModifyAsync(m => m.Embed = successEmbed);
91+
}
92+
catch (Exception ex)
93+
{
94+
_logger.LogError(ex, "Error during role synchronization for user {User} with GitHub {GitHub}",
95+
Context.User.Username, githubUsername);
96+
97+
var errorEmbed = new EmbedBuilder()
98+
.WithTitle("❌ Synchronization Failed")
99+
.WithDescription("An error occurred while synchronizing your roles. Please try again later or contact an administrator.")
100+
.WithColor(Color.Red)
101+
.WithTimestamp(DateTimeOffset.Now)
102+
.Build();
103+
104+
await ReplyAsync(embed: errorEmbed);
105+
}
106+
}
107+
108+
[Command("check-languages")]
109+
[Summary("Shows programming languages detected from a GitHub profile without changing roles")]
110+
public async Task CheckLanguagesAsync([Summary("GitHub username to check")] string githubUsername)
111+
{
112+
try
113+
{
114+
await Context.Channel.TriggerTypingAsync();
115+
116+
_logger.LogInformation("User {DiscordUser} requested language check for GitHub user {GitHubUser}",
117+
Context.User.Username, githubUsername);
118+
119+
var embed = new EmbedBuilder()
120+
.WithTitle("🔍 Analyzing GitHub Profile...")
121+
.WithDescription($"Scanning repositories for: **{githubUsername}**")
122+
.WithColor(Color.Blue)
123+
.WithTimestamp(DateTimeOffset.Now)
124+
.Build();
125+
126+
var message = await ReplyAsync(embed: embed);
127+
128+
var languageStats = await _languageService.GetUserProgrammingLanguagesAsync(githubUsername);
129+
130+
if (!languageStats.Any())
131+
{
132+
var errorEmbed = new EmbedBuilder()
133+
.WithTitle("❌ No Programming Languages Found")
134+
.WithDescription($"No programming languages detected for GitHub user **{githubUsername}**.")
135+
.WithColor(Color.Red)
136+
.WithTimestamp(DateTimeOffset.Now)
137+
.Build();
138+
139+
await message.ModifyAsync(m => m.Embed = errorEmbed);
140+
return;
141+
}
142+
143+
var topLanguages = GitHubLanguageDetectionService.GetTopLanguages(languageStats, maxLanguages: 15);
144+
var totalBytes = languageStats.Values.Sum();
145+
146+
var languageList = topLanguages.Select((lang, index) =>
147+
{
148+
var bytes = languageStats[lang];
149+
var percentage = (double)bytes / totalBytes * 100;
150+
var size = bytes < 1024 ? $"{bytes} B" :
151+
bytes < 1024 * 1024 ? $"{bytes / 1024:N0} KB" :
152+
$"{bytes / (1024 * 1024):N1} MB";
153+
return $"{index + 1}. **{lang}** - {percentage:F1}% ({size})";
154+
});
155+
156+
var resultEmbed = new EmbedBuilder()
157+
.WithTitle($"📊 Programming Languages for {githubUsername}")
158+
.WithDescription($"**Total Repositories Analyzed:** {languageStats.Count}\n\n" +
159+
$"**Languages Found:**\n{string.Join("\n", languageList)}")
160+
.WithColor(Color.Blue)
161+
.WithTimestamp(DateTimeOffset.Now)
162+
.WithFooter("Analysis based on public repositories only")
163+
.Build();
164+
165+
await message.ModifyAsync(m => m.Embed = resultEmbed);
166+
}
167+
catch (Exception ex)
168+
{
169+
_logger.LogError(ex, "Error during language check for GitHub user {GitHub}", githubUsername);
170+
171+
var errorEmbed = new EmbedBuilder()
172+
.WithTitle("❌ Analysis Failed")
173+
.WithDescription("An error occurred while analyzing the GitHub profile. Please check the username and try again.")
174+
.WithColor(Color.Red)
175+
.WithTimestamp(DateTimeOffset.Now)
176+
.Build();
177+
178+
await ReplyAsync(embed: errorEmbed);
179+
}
180+
}
181+
182+
[Command("my-sync")]
183+
[Summary("Synchronizes your roles using your previously linked GitHub account")]
184+
public async Task MySyncAsync()
185+
{
186+
var githubUsername = _botService.GetUserGitHubUsername(Context.User.Id);
187+
188+
if (string.IsNullOrEmpty(githubUsername))
189+
{
190+
var embed = new EmbedBuilder()
191+
.WithTitle("❌ No GitHub Account Linked")
192+
.WithDescription("You haven't linked a GitHub account yet. Use:\n`!sync-roles <your-github-username>`")
193+
.WithColor(Color.Red)
194+
.WithTimestamp(DateTimeOffset.Now)
195+
.Build();
196+
197+
await ReplyAsync(embed: embed);
198+
return;
199+
}
200+
201+
await SyncRolesAsync(githubUsername);
202+
}
203+
204+
[Command("help")]
205+
[Summary("Shows available commands for role synchronization")]
206+
public async Task HelpAsync()
207+
{
208+
var embed = new EmbedBuilder()
209+
.WithTitle("🤖 Programming Language Role Bot - Commands")
210+
.WithDescription("This bot automatically assigns Discord roles based on your GitHub programming language usage.")
211+
.WithColor(Color.Blue)
212+
.AddField("!sync-roles <github-username>",
213+
"Links your GitHub account and synchronizes your Discord roles", false)
214+
.AddField("!my-sync",
215+
"Re-synchronizes your roles using your previously linked GitHub account", false)
216+
.AddField("!check-languages <github-username>",
217+
"Shows programming languages for a GitHub user without changing roles", false)
218+
.AddField("!help",
219+
"Shows this help message", false)
220+
.WithTimestamp(DateTimeOffset.Now)
221+
.WithFooter("LinksPlatform Discord Bot")
222+
.Build();
223+
224+
await ReplyAsync(embed: embed);
225+
}
226+
}
227+
}

csharp/DiscordBot/Program.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Configuration;
5+
using DiscordBot.Services;
6+
using System.Threading.Tasks;
7+
8+
namespace DiscordBot
9+
{
10+
internal class Program
11+
{
12+
private static async Task Main(string[] args)
13+
{
14+
var host = CreateHostBuilder(args).Build();
15+
await host.RunAsync();
16+
}
17+
18+
private static IHostBuilder CreateHostBuilder(string[] args) =>
19+
Host.CreateDefaultBuilder(args)
20+
.ConfigureAppConfiguration((context, config) =>
21+
{
22+
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
23+
config.AddCommandLine(args);
24+
config.AddEnvironmentVariables();
25+
})
26+
.ConfigureServices((context, services) =>
27+
{
28+
services.Configure<DiscordBotSettings>(context.Configuration.GetSection("DiscordBot"));
29+
services.AddSingleton<DiscordBotService>();
30+
services.AddSingleton<ProgrammingLanguageRoleService>();
31+
services.AddSingleton<GitHubLanguageDetectionService>();
32+
services.AddHostedService<DiscordBotHostedService>();
33+
})
34+
.ConfigureLogging(logging =>
35+
{
36+
logging.ClearProviders();
37+
logging.AddConsole();
38+
});
39+
}
40+
}

0 commit comments

Comments
 (0)