Expanding Voice Channels
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});
}
}
}
Please register or sign in to comment