using Discord.Rest; using Discord.WebSocket; using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; using System; using System.Threading; using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.VoteTempChannel { /// /// Enables users to vote for the creation of a temporary channel. /// Deletes the channel after a set period of inactivity. /// class VoteTempChannel : BotModule { Task _backgroundWorker; CancellationTokenSource _backgroundWorkerCancel; public VoteTempChannel(DiscordSocketClient client) : base(client) { client.MessageReceived += VoteChecking; client.MessageReceived += TemporaryChannelActivityCheck; _backgroundWorkerCancel = new CancellationTokenSource(); _backgroundWorker = Task.Factory.StartNew(BackgroundCheckingTask, _backgroundWorkerCancel.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } public override Task CreateInstanceState(JToken configSection) { if (configSection == null) return Task.FromResult(null); if (configSection.Type == JTokenType.Object) { return Task.FromResult(new GuildInformation((JObject)configSection)); } throw new RuleImportException("Configuration not of a valid type."); } /// /// Listens for voting commands. /// private async Task VoteChecking(SocketMessage arg) { if (arg.Author.IsBot) return; var guild = (arg.Channel as SocketTextChannel)?.Guild; if (guild == null) return; var info = GetState(guild.Id); if (info == null) return; // Only check the designated voting channel if (!string.Equals(arg.Channel.Name, info.Config.VoteChannel, StringComparison.InvariantCultureIgnoreCase)) return; // Check if command invoked if (!arg.Content.StartsWith(info.Config.VoteCommand, StringComparison.InvariantCultureIgnoreCase)) return; // Check if we're accepting votes. Locking here; other tasks may alter this data. bool cooldown; bool voteCounted = false; string newChannelName = null; lock (info) { if (info.GetTemporaryChannel(guild) != null) return; // channel exists, do nothing if (info.Voting.AwaitingInitialVote()) { // Vote not in effect. Ignore those not allowed to initiate a vote (if configured). if (!info.Config.VoteStarters.IsEmpty() && !info.Config.VoteStarters.ExistsInList(arg)) return; } cooldown = info.Voting.IsInCooldown(); if (!cooldown) { voteCounted = info.Voting.AddVote(arg.Author.Id, out var voteCount); if (voteCount >= info.Config.VotePassThreshold) { newChannelName = info.Config.TempChannelName; } } // Prepare new temporary channel while we're still locking if (newChannelName != null) info.TempChannelLastActivity = DateTime.UtcNow; } if (cooldown) { await arg.Channel.SendMessageAsync(":x: Cooldown in effect. Try again later."); return; } if (!voteCounted) { await arg.Channel.SendMessageAsync(":x: You have already voted."); return; } if (newChannelName != null) { RestTextChannel newCh; try { newCh = await guild.CreateTextChannelAsync(newChannelName); } catch (Discord.Net.HttpException ex) { await Log($"Failed to create temporary channel: {ex.Message}"); await arg.Channel.SendMessageAsync(":x: Failed to create new channel! Notify the bot operator."); return; } await newCh.SendMessageAsync($"Welcome to <#{newCh.Id}>!" + "\nBe aware that this channel is temporary and **will** be deleted later."); newChannelName = newCh.Id.ToString(); // For use in the confirmation message } await arg.Channel.SendMessageAsync(":white_check_mark: Channel creation vote has been counted." + (newChannelName != null ? $"\n<#{newChannelName}> has been created!" : "")); } /// /// Listens for any message sent to the temporary channel. /// Updates the corresponding internal value. /// private Task TemporaryChannelActivityCheck(SocketMessage arg) { if (arg.Author.IsBot) return Task.CompletedTask; var guild = (arg.Channel as SocketTextChannel)?.Guild; if (guild == null) return Task.CompletedTask; var info = GetState(guild.Id); if (info == null) return Task.CompletedTask; lock (info) { var tch = info.GetTemporaryChannel(guild); if (tch == null) return Task.CompletedTask; if (arg.Channel.Name == tch.Name) { info.TempChannelLastActivity = DateTimeOffset.UtcNow; } } return Task.CompletedTask; } #region Background tasks /// /// Two functions: Removes expired channels, and announces expired votes. /// private async Task BackgroundCheckingTask() { while (!_backgroundWorkerCancel.IsCancellationRequested) { try { await Task.Delay(12000, _backgroundWorkerCancel.Token); } catch (TaskCanceledException) { return; } foreach (var g in Client.Guilds) { try { var info = GetState(g.Id); if (info == null) continue; await BackgroundTempChannelExpiryCheck(g, info); await BackgroundVoteSessionExpiryCheck(g, info); } catch (Exception ex) { Log("Unhandled exception in background task when processing a single guild.").Wait(); Log(ex.ToString()).Wait(); } } } } private async Task BackgroundTempChannelExpiryCheck(SocketGuild g, GuildInformation info) { SocketGuildChannel ch = null; lock (info) { ch = info.GetTemporaryChannel(g); if (ch == null) return; // No temporary channel. Nothing to do. if (!info.IsTempChannelExpired()) return; // If we got this far, the channel's expiring. Start the voting cooldown. info.Voting.StartCooldown(); } await ch.DeleteAsync(); } private async Task BackgroundVoteSessionExpiryCheck(SocketGuild g, GuildInformation info) { bool act; string nameTest; lock (info) { act = info.Voting.IsSessionExpired(); nameTest = info.Config.VoteChannel; } if (!act) return; // Determine the voting channel; will send announcement there. SocketTextChannel outCh = null; foreach (var ch in g.TextChannels) { if (string.Equals(ch.Name, nameTest, StringComparison.InvariantCultureIgnoreCase)) { outCh = ch; break; } } if (outCh == null) { // Huh. Bad config? return; } await outCh.SendMessageAsync(":x: Not enough votes were placed for channel creation."); } #endregion } }