From 7353eb7c218392b4be66ec028ebb2046b0553b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=81=D0=BB=D0=B0=D0=BD=20=D0=95=D1=80=D0=B3=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=B2?= Date: Sun, 28 Jun 2026 22:43:16 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D0=B8=D1=87=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoiceChannelManagementService.cs | 56 ++++++++++++++++--- .../ChannelsManagementModule.cs | 39 +++++++++++++ ...DiscordBotFileBasedConfigurationService.cs | 29 ++++++++++ .../IDiscordBotConfigurationService.cs | 4 ++ DiscordChannelsBot.csproj | 14 ++--- Dockerfile | 4 +- Models/ApplicationDbContext.cs | 10 ++++ Models/DiscordGuildConfiguration.cs | 2 + Models/UserVoiceChannelName.cs | 10 ++++ global.json | 2 +- 10 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 Models/UserVoiceChannelName.cs diff --git a/CommandManagement/ChannelManagement/VoiceChannelManagementService.cs b/CommandManagement/ChannelManagement/VoiceChannelManagementService.cs index a1881bc..eb26c31 100644 --- a/CommandManagement/ChannelManagement/VoiceChannelManagementService.cs +++ b/CommandManagement/ChannelManagement/VoiceChannelManagementService.cs @@ -15,10 +15,13 @@ public class VoiceChannelManagementService : IVoiceChannelManagementService private readonly Dictionary _voiceChannelDeletionCheckCancellationTokenSourcesDictionary; + private readonly HashSet _managedVoiceChannels; + public VoiceChannelManagementService(IServiceProvider serviceProvider) { _voiceChannelDeletionCheckCancellationTokenSourcesDictionary = new Dictionary(); + _managedVoiceChannels = new HashSet(); _discordBotConfigurationService = serviceProvider.GetRequiredService(); _discordClient = serviceProvider.GetRequiredService(); @@ -53,6 +56,7 @@ public async Task CreateVoiceChannelAsync(IGuild guild, string name, GuildGroups } } + _managedVoiceChannels.Add(voiceChannel.Id); RunDeletionCheckAsync(voiceChannel.Id); } @@ -88,43 +92,79 @@ private async Task AllowOnlyRolesAsync(IGuild guild, IVoiceChannel voiceChannel, } } - private Task UserVoiceStateUpdatedHandleAsync(SocketUser user, SocketVoiceState originalState, + private async Task UserVoiceStateUpdatedHandleAsync(SocketUser user, SocketVoiceState originalState, SocketVoiceState updatedState) { + if (updatedState.VoiceChannel != null) + { + var voiceChannel = updatedState.VoiceChannel; + CancelDeletionCheckIfExists(voiceChannel.Id); + + await HandleCreatorChannelJoinAsync(user, voiceChannel); + } + if (originalState.VoiceChannel != null) { var voiceChannel = originalState.VoiceChannel; - if (voiceChannel.ConnectedUsers.Count == 0) + if (_managedVoiceChannels.Contains(voiceChannel.Id) && voiceChannel.ConnectedUsers.Count == 0) { RunDeletionCheckAsync(voiceChannel.Id); } } + } - if (updatedState.VoiceChannel != null) + private async Task HandleCreatorChannelJoinAsync(SocketUser user, SocketVoiceChannel joinedChannel) + { + var guild = joinedChannel.Guild; + var guildConfiguration = await _discordBotConfigurationService.GetGuildConfigurationAsync(guild.Id); + + if (guildConfiguration?.VoiceChannelCreatorChannelId == null || + joinedChannel.Id != guildConfiguration.VoiceChannelCreatorChannelId.Value) { - var voiceChannel = updatedState.VoiceChannel; - CancelDeletionCheckIfExists(voiceChannel.Id); + return; + } + + if (user is not SocketGuildUser guildUser) + { + return; } - return Task.CompletedTask; + var customName = await _discordBotConfigurationService.GetUserChannelNameAsync(guild.Id, guildUser.Id); + var channelName = !string.IsNullOrWhiteSpace(customName?.Name) + ? customName.Name + : $"{guildUser.DisplayName ?? guildUser.Username}'s channel"; + + var newChannel = await guild.CreateVoiceChannelAsync(channelName, + channel => channel.CategoryId = joinedChannel.CategoryId); + + _managedVoiceChannels.Add(newChannel.Id); + + await guildUser.ModifyAsync(properties => properties.Channel = newChannel); + + RunDeletionCheckAsync(newChannel.Id); } private async void RunDeletionCheckAsync(ulong voiceChannelId) { + CancelDeletionCheckIfExists(voiceChannelId); + var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - _voiceChannelDeletionCheckCancellationTokenSourcesDictionary.Add(voiceChannelId, cancellationTokenSource); + _voiceChannelDeletionCheckCancellationTokenSourcesDictionary[voiceChannelId] = cancellationTokenSource; try { - await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken); + await Task.Delay(TimeSpan.FromMinutes(3), cancellationToken); var voiceChannel = (SocketVoiceChannel) _discordClient.GetChannel(voiceChannelId); if (voiceChannel != null && voiceChannel.ConnectedUsers.Count == 0) { await voiceChannel.DeleteAsync(); + _managedVoiceChannels.Remove(voiceChannelId); } + + _voiceChannelDeletionCheckCancellationTokenSourcesDictionary.Remove(voiceChannelId); } catch (TaskCanceledException) { diff --git a/CommandManagement/CommandHandling/ChannelsManagementModule.cs b/CommandManagement/CommandHandling/ChannelsManagementModule.cs index 45699db..76d1148 100644 --- a/CommandManagement/CommandHandling/ChannelsManagementModule.cs +++ b/CommandManagement/CommandHandling/ChannelsManagementModule.cs @@ -74,4 +74,43 @@ public async Task SetVoiceCategory([Summary("Category")] string category) await Context.Interaction.RespondAsync( $"Теперь голосовые каналы будут создаваться в категории **{category}**.", ephemeral: true); } + + [RequireUserPermission(ChannelPermission.ManageChannels)] + [SlashCommand("voice-creator", + "Set a voice channel that automatically creates a personal channel when a user joins it")] + [RequireContext(ContextType.Guild)] + public async Task SetVoiceCreator([Summary("Channel")] IVoiceChannel channel) + { + var guildConfiguration = await DiscordBotConfigurationService.GetGuildConfigurationAsync(Context.Guild.Id); + + if (guildConfiguration == null) + { + guildConfiguration = new DiscordGuildConfiguration + { + Id = Context.Guild.Id, + VoiceChannelCreatorChannelId = channel.Id + }; + + await DiscordBotConfigurationService.SaveAsync(guildConfiguration); + } + else + { + guildConfiguration.VoiceChannelCreatorChannelId = channel.Id; + + await DiscordBotConfigurationService.UpdateAsync(guildConfiguration); + } + + await Context.Interaction.RespondAsync( + $"Теперь канал **{channel.Name}** будет создавать персональные каналы при входе.", ephemeral: true); + } + + [SlashCommand("mychannelname", "Set the name of the personal channel that is created when you join the creator")] + [RequireContext(ContextType.Guild)] + public async Task SetMyChannelName([Summary("Name")] string name) + { + await DiscordBotConfigurationService.SetUserChannelNameAsync(Context.Guild.Id, Context.User.Id, name); + + await Context.Interaction.RespondAsync( + $"Теперь твой персональный канал будет называться **{name}**.", ephemeral: true); + } } \ No newline at end of file diff --git a/Configuration/DiscordBotFileBasedConfigurationService.cs b/Configuration/DiscordBotFileBasedConfigurationService.cs index 6055593..51564f0 100644 --- a/Configuration/DiscordBotFileBasedConfigurationService.cs +++ b/Configuration/DiscordBotFileBasedConfigurationService.cs @@ -28,4 +28,33 @@ public async Task SaveAsync(DiscordGuildConfiguration discordGuildConfiguration) await _applicationDbContext.GuildConfigurations.AddAsync(discordGuildConfiguration); await _applicationDbContext.SaveChangesAsync(); } + + public async ValueTask GetUserChannelNameAsync(ulong guildId, ulong userId) + { + return await _applicationDbContext.UserVoiceChannelNames + .FindAsync(guildId, userId); + } + + public async Task SetUserChannelNameAsync(ulong guildId, ulong userId, string name) + { + var existing = await _applicationDbContext.UserVoiceChannelNames + .FindAsync(guildId, userId); + + if (existing == null) + { + await _applicationDbContext.UserVoiceChannelNames.AddAsync(new UserVoiceChannelName + { + GuildId = guildId, + UserId = userId, + Name = name + }); + } + else + { + existing.Name = name; + _applicationDbContext.UserVoiceChannelNames.Update(existing); + } + + await _applicationDbContext.SaveChangesAsync(); + } } \ No newline at end of file diff --git a/Configuration/IDiscordBotConfigurationService.cs b/Configuration/IDiscordBotConfigurationService.cs index eb9b505..8a6ef75 100644 --- a/Configuration/IDiscordBotConfigurationService.cs +++ b/Configuration/IDiscordBotConfigurationService.cs @@ -9,4 +9,8 @@ public interface IDiscordBotConfigurationService Task UpdateAsync(DiscordGuildConfiguration discordGuildConfiguration); Task SaveAsync(DiscordGuildConfiguration discordGuildConfiguration); + + ValueTask GetUserChannelNameAsync(ulong guildId, ulong userId); + + Task SetUserChannelNameAsync(ulong guildId, ulong userId, string name); } \ No newline at end of file diff --git a/DiscordChannelsBot.csproj b/DiscordChannelsBot.csproj index 86a81e7..6adad60 100644 --- a/DiscordChannelsBot.csproj +++ b/DiscordChannelsBot.csproj @@ -2,19 +2,19 @@ Exe - net7.0 + net10.0 enable disable Linux - - - - - - + + + + + + diff --git a/Dockerfile b/Dockerfile index 4e006e9..a5e14cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base +FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["DiscordChannelsBot.csproj", "./"] RUN dotnet restore "DiscordChannelsBot.csproj" diff --git a/Models/ApplicationDbContext.cs b/Models/ApplicationDbContext.cs index 5ce49fd..17113c8 100644 --- a/Models/ApplicationDbContext.cs +++ b/Models/ApplicationDbContext.cs @@ -11,4 +11,14 @@ public ApplicationDbContext(DbContextOptions options) : ba } public DbSet GuildConfigurations { get; set; } + + public DbSet UserVoiceChannelNames { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasKey(name => new {name.GuildId, name.UserId}); + } } \ No newline at end of file diff --git a/Models/DiscordGuildConfiguration.cs b/Models/DiscordGuildConfiguration.cs index d0078ea..ad79463 100644 --- a/Models/DiscordGuildConfiguration.cs +++ b/Models/DiscordGuildConfiguration.cs @@ -5,4 +5,6 @@ public class DiscordGuildConfiguration public ulong Id { get; init; } public string VoiceChannelCreationCategory { get; set; } + + public ulong? VoiceChannelCreatorChannelId { get; set; } } \ No newline at end of file diff --git a/Models/UserVoiceChannelName.cs b/Models/UserVoiceChannelName.cs new file mode 100644 index 0000000..f9c467d --- /dev/null +++ b/Models/UserVoiceChannelName.cs @@ -0,0 +1,10 @@ +namespace DiscordChannelsBot.Models; + +public class UserVoiceChannelName +{ + public ulong GuildId { get; init; } + + public ulong UserId { get; init; } + + public string Name { get; set; } +} diff --git a/global.json b/global.json index 36e1a9e..9a523dc 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.0", + "version": "10.0.0", "rollForward": "latestMajor", "allowPrerelease": false } From b453b36a3e8e09fcf9e2c92c0cbd30483f24a3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=81=D0=BB=D0=B0=D0=BD=20=D0=95=D1=80=D0=B3=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=B2?= Date: Sun, 28 Jun 2026 22:54:17 +0300 Subject: [PATCH 2/2] Fix InteractionService DI registration --- Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index 70c6bb7..fa70ee2 100644 --- a/Program.cs +++ b/Program.cs @@ -59,7 +59,8 @@ private static void ConfigureServices(HostBuilderContext context, IServiceCollec GatewayIntents = GatewayIntents.All })) // .AddSingleton() - .AddSingleton() + .AddSingleton(provider => + new InteractionService(provider.GetRequiredService())) .AddSingleton() .AddSingleton() .AddSingleton()