Skip to content
Snippets Groups Projects

Expanding Voice Channels

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    The snippet can be accessed without any authentication.
    Authored by Josh Harris
    Edited
    ExpandingVoiceChannelsModule.cs 14.98 KiB
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using ContinuumDotNet.Extensions;
    using ContinuumDotNet.Logging;
    using ContinuumDotNet.Models;
    using ContinuumDotNet.Modules.Base;
    using ContinuumDotNet.Preconditions;
    using ContinuumDotNet.Preconditions.TextCommands;
    using ContinuumDotNet.Services;
    using Discord;
    using Discord.Commands;
    using Discord.WebSocket;
    using Serilog;
    using SerilogTimings;
    
    namespace ContinuumDotNet.Modules
    {
        [Group("auto-expanding")]
        [Alias("autoexpanding", "expand", "evc")]
        [RequireContext(ContextType.Guild)]
        public class ExpandingVoiceChannelModule : ContinuumModule<SocketCommandContext>
        {
            private readonly ExpandingVoiceChannelService _evcService;
            private readonly DiscordSocketClient _client;
            private readonly ILogger _logger;
    
            public ExpandingVoiceChannelModule(DiscordSocketClient client, ILogger logger,
                ExpandingVoiceChannelService evcService)
            {
                _client = client;
                _logger = logger.ForContext<ExpandingVoiceChannelModule>();
                _evcService = evcService;
            }
    
            protected override void RunOnce()
            {
                _client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated;
            }
    
            [TextCommandPermissions(PermissionLevel.PowerUser)]
            [Command("list")]
            [Alias("info")]
            public async Task ListAsync()
            {
                var guild = Context.Guild;
                var evcList = await _evcService.GetManyAsync(Context.Guild.Id);
    
                if (evcList.Count == 0)
                {
                    await ReplyAsync("You have no channels with auto-expansion enabled.");
                    return;
                }
    
                var embed = new EmbedBuilder
                {
                    Title = $"You have {evcList.Count} channel(s) setup for auto-expansion",
                    Color = Color.Gold
                };
    
                foreach (var evc in evcList)
                {
                    var channel = guild.GetVoiceChannel(evc.TemplateId);
                    if (channel is null) continue;
                    var details = new StringBuilder();
                    details.AppendLine($"TemplateId: {evc.TemplateId}");
                    details.AppendLine($"NameTemplate: {evc.TemplateName}");
                    details.AppendLine($"Type: {evc.EvcType}");
                    details.AppendLine($"CreatedChannelCount: {evc.CreatedChannelIds.Count}");
                    embed.AddField(channel.Name, details.ToString(), true);
                }
    
                await ReplyAsync("", false, embed.Build());
            }
    
            [TextCommandPermissions(PermissionLevel.Moderator)]
            [Command("set", RunMode = RunMode.Async)]
            [Alias("add", "create")]
            public async Task AddAsync(IVoiceChannel channel = null, EvcType type = EvcType.JoinToCreate,
                string customTemplate = "")
            {
                channel ??= await Context.VoiceChannel();
                if (channel is null)
                {
                    await ReplyAsync("You need to specify, or be in, a voice channel to use that command");
                    return;
                }
    
                var guild = Context.Guild;
    
                customTemplate = type switch
                {
                    EvcType.JoinToCreate => string.IsNullOrWhiteSpace(customTemplate) ? "{1} #{0}" : customTemplate,
                    EvcType.KeepAvailable => string.IsNullOrWhiteSpace(customTemplate)
                        ? $"{channel.Name} #{{0}}"
                        : customTemplate,
                    _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
                };
    
                var evc = await _evcService.CreateAsync(guild.Id, channel.Id, type, customTemplate);
                if (evc is null)
                {
                    await ReplyAsync($"Added `{channel.Name}` as a new expanding voice channel");
                }
                else
                {
                    await ReplyAsync($"Updated the `{channel.Name}` expanding voice channel");
                }
    
                if (type == EvcType.KeepAvailable)
                {
                    await channel.ModifyAsync(vc => vc.Name = string.Format(customTemplate, 1));
                }
            }
    
            [TextCommandPermissions(PermissionLevel.Moderator)]
            [Command("unset")]
            [Alias("remove", "delete")]
            public async Task UnsetAsync(IVoiceChannel channel = null)
            {
                channel ??= await Context.VoiceChannel();
                if (channel is null)
                {
                    await ReplyAsync("You need to be in, or specify, a voice channel to use that command");
                    return;
                }
    
                var guild = Context.Guild;
    
                var evc = await _evcService.DeleteAsync(guild.Id, channel.Id);
                if (evc is null)
                {
                    await ReplyAsync("That channel is not being automatically expanded");
                    return;
                }
    
                foreach (var vc in evc.CreatedChannelIds.Select(channelId => guild.GetVoiceChannel(channelId))
                    .Where(vc => vc is not null))
                {
                    await vc.DeleteAsync();
                }
    
                await ReplyAsync("Channel has been removed from automatic expansion");
            }
    
            private Task ClientOnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState,
                SocketVoiceState newState)
            {
                Task.Run(() => HandleUserVoiceStateUpdate(user, oldState, newState)).ConfigureAwait(false);
                return Task.CompletedTask;
            }
    
            private async Task HandleUserVoiceStateUpdate(SocketUser user, SocketVoiceState oldState,
                SocketVoiceState newState)
            {
                if (user is not SocketGuildUser guildUser) return;
    
                _logger.Information(
                    "VoiceStateUpdate: {GuildUser} - {OldChannelName} -> {NewChannelName}", 
                    guildUser.Id,
                    oldState.VoiceChannel?.Name ?? "null",
                    newState.VoiceChannel?.Name ?? "null");
    
                if (oldState.VoiceChannel == newState.VoiceChannel)
                {
                    //In the same channel. Don't care what they're doing
                    return;
                }
    
                if (oldState.VoiceChannel is not null)
                {
                    await UserLeaveVoiceChannel(guildUser, oldState);
                }
    
                if (newState.VoiceChannel is not null)
                {
                    await UserJoinVoiceChannel(guildUser, newState);
                }
            }
    
            private async Task UserJoinVoiceChannel(SocketGuildUser user, SocketVoiceState newState)
            {
                ExpandingVoiceChannel evc = null;
                using (Operation.Time("Getting evc with GuildID {GuildId} and ChannelId {ChannelId}", user.Guild.Id,
                    newState.VoiceChannel.Id))
                {
                    evc = await _evcService.GetFromAnyAsync(user.Guild.Id, newState.VoiceChannel.Id);
                }
                
                if (evc is null) return;
    
                _logger.Information("VoiceStateUpdate relates to a {Type} EVC", evc.EvcType);
                switch (evc.EvcType)
                {
                    case EvcType.JoinToCreate:
                        await JoinToCreateEvcConnect(evc, user, newState.VoiceChannel);
                        break;
                    case EvcType.KeepAvailable:
                        await KeepAvailableEvcConnect(evc, user, newState.VoiceChannel);
                        break;
                    default:
                        return;
                }
            }
    
            private async Task JoinToCreateEvcConnect(ExpandingVoiceChannel evc, SocketGuildUser user,
                IVoiceChannel channel)
            {
                var propertyBag = new PropertyBagEnricher()
                    .AddGuild(user.Guild)
                    .AddGuildUser(user)
                    .AddEvc(evc);
                var evcLogger = _logger.ForContext(propertyBag);
    
                // We joined something _other than_ the template, ignore
                if (channel.Id != evc.TemplateId) return;
    
                var newName = string.Format(evc.TemplateName, evc.CreatedChannelIds.Count + 1, channel.Name);
                var newChannel = await CreateVoiceChannelCopy(channel, newName, evc.CreatedChannelIds.Count + 1);
                propertyBag.AddChannel(newChannel);
                await user.ModifyAsync(x => x.ChannelId = newChannel.Id);
    
                evcLogger.Information("User joined a EVC, new channel created");
    
                var success = await _evcService.AddCreatedChannel(evc.GuildId, evc.TemplateId, newChannel.Id);
                if (!success)
                {
                    evcLogger.Error("Channel created but EVC update returned non-success, channel could be orphaned");
                }
            }
    
            private async Task KeepAvailableEvcConnect(ExpandingVoiceChannel evc, SocketGuildUser user,
                IVoiceChannel channel)
            {
                var propertyBag = new PropertyBagEnricher()
                    .AddGuild(user.Guild)
                    .AddGuildUser(user)
                    .AddEvc(evc);
                var evcLogger = _logger.ForContext(propertyBag);
                var guild = user.Guild;
    
                var emptyChannels = guild.VoiceChannels.Where(
                    vc => (evc.CreatedChannelIds.Contains(vc.Id) || evc.TemplateId == vc.Id) && vc.ConnectedUsers.Count == 0
                ).ToList();
                if (emptyChannels.Count > 0)
                {
                    evcLogger.Information("Still have {Count} empty channels available, no create action needed",
                        emptyChannels.Count);
                    return;
                }
    
                var newName = string.Format(evc.TemplateName, evc.CreatedChannelIds.Count + 2);
                var newChannel = await CreateVoiceChannelCopy(channel, newName, evc.CreatedChannelIds.Count + 1);
                propertyBag.AddChannel(newChannel);
    
                evcLogger.Information("Ran out of empty channels, new channel created");
    
                var success = await _evcService.AddCreatedChannel(evc.GuildId, evc.TemplateId, newChannel.Id);
                if (!success)
                {
                    evcLogger.Error("Channel created but EVC update returned non-success, channel could be orphaned!");
                }
            }
    
            private async Task UserLeaveVoiceChannel(SocketGuildUser user, SocketVoiceState oldState)
            {
                ExpandingVoiceChannel evc = null;
                using (Operation.Time("Getting evc with GuildID {GuildId} and ChannelId {ChannelId}", user.Guild.Id,
                    oldState.VoiceChannel.Id))
                {
                    evc = await _evcService.GetFromAnyAsync(user.Guild.Id, oldState.VoiceChannel.Id);
                }
                
                if (evc is null) return;
    
                _logger.Information("VoiceStateUpdate relates to a {Type} EVC", evc.EvcType);
                switch (evc.EvcType)
                {
                    case EvcType.JoinToCreate:
                        await JoinToCreateEvcDisconnect(evc, user, oldState.VoiceChannel);
                        break;
                    case EvcType.KeepAvailable:
                        await KeepAvailableEvcDisconnect(evc, user, oldState.VoiceChannel);
                        break;
                    default:
                        return;
                }
            }
    
            private async Task JoinToCreateEvcDisconnect(ExpandingVoiceChannel evc, SocketGuildUser user,
                SocketVoiceChannel channel)
            {
                var propertyBag = new PropertyBagEnricher()
                    .AddGuild(user.Guild)
                    .AddChannel(channel)
                    .AddGuildUser(user)
                    .AddEvc(evc);
                var evcLogger = _logger.ForContext(propertyBag);
    
                // We left the template, ignore
                if (channel.Id == evc.TemplateId) return;
    
                // Channel is empty, clean it up
                if (channel.ConnectedUsers.Count == 0)
                {
                    evcLogger.Information("User left a EVC and now it's empty, channel deleted");
    
                    await channel.DeleteAsync(new RequestOptions {RetryMode = RetryMode.AlwaysRetry});
                    var success = await _evcService.DeleteCreatedChannel(
                        evc.GuildId,
                        evc.TemplateId,
                        channel.Id);
                    if (!success)
                    {
                        evcLogger.Error("Channel deleted but EVC update returned non-success, channel could be orphaned");
                    }
                }
            }
    
            private async Task KeepAvailableEvcDisconnect(ExpandingVoiceChannel evc, SocketGuildUser user,
                SocketVoiceChannel channel)
            {
                var propertyBag = new PropertyBagEnricher()
                    .AddGuild(user.Guild)
                    .AddChannel(channel)
                    .AddGuildUser(user)
                    .AddEvc(evc);
                var evcLogger = _logger.ForContext(propertyBag);
                var guild = user.Guild;
    
                var emptyChannelsList = guild.VoiceChannels.Where(
                    vc => (evc.CreatedChannelIds.Contains(vc.Id) || evc.TemplateId == vc.Id) && vc.ConnectedUsers.Count == 0
                ).ToList();
                var channelsToDeleteList = emptyChannelsList.Where(vc => vc.Id != evc.TemplateId);
                var channelsToDelete = new Stack<SocketVoiceChannel>(channelsToDeleteList);
                if (emptyChannelsList.Count <= 1)
                {
                    evcLogger.Information("There are {Count} empty channels available, no cleanup action needed",
                        emptyChannelsList.Count);
                    return;
                }
    
                while (channelsToDelete.TryPeek(out _))
                {
                    var deletableChannel = channelsToDelete.Pop();
    
                    evcLogger.Information("Channel {Channel} is empty, deleting", deletableChannel.Name);
                    await deletableChannel.DeleteAsync(new RequestOptions {RetryMode = RetryMode.AlwaysRetry});
                    var success = await _evcService.DeleteCreatedChannel(
                        evc.GuildId,
                        evc.TemplateId,
                        deletableChannel.Id);
                    if (!success)
                    {
                        evcLogger.Error("Channel deleted but EVC update returned non-success, channel could be orphaned!");
                    }
                }
            }
    
            private static async Task<IVoiceChannel> CreateVoiceChannelCopy(IVoiceChannel original, string name, int orderIncrement = 1)
            {
                return await original.Guild.CreateVoiceChannelAsync(
                    name,
                    properties =>
                    {
                        properties.Bitrate = original.Bitrate;
                        properties.UserLimit = original.UserLimit;
                        properties.CategoryId = original.CategoryId;
                        properties.Position = original.Position + orderIncrement;
                        properties.PermissionOverwrites = original.PermissionOverwrites.ToList();
                    }, new RequestOptions {RetryMode = RetryMode.AlwaysRetry});
            }
        }
    }
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment