diff --git a/Module/VoteTempChannel/ChannelManager.cs b/Module/VoteTempChannel/ChannelManager.cs deleted file mode 100644 index 7a8a6ac..0000000 --- a/Module/VoteTempChannel/ChannelManager.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Discord.Rest; -using Discord.WebSocket; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Noikoio.RegexBot.Module.VoteTempChannel -{ - /// - /// Keeps track of existing channels and expiry information. Manages data persistence. - /// - class ChannelManager - { - readonly VoteTempChannel _out; - readonly DiscordSocketClient _client; - - readonly CancellationTokenSource _token; - readonly Task _bgTask; - - public ChannelManager(VoteTempChannel module, DiscordSocketClient client) - { - _out = module; - _client = client; - } - - #region Channel entry manipulation - /// - /// Creates the temporary channel. - /// - /// - /// Various causes. Send exception message to log and channel if thrown. - /// - public async Task CreateChannelAndEntryAsync(SocketGuild guild, Configuration info) - { - lock (_trackedChannels) - { - // Disregard if already in cache. (How did we get here?) - if (_trackedChannels.ContainsKey(guild.Id)) return null; - } - - RestTextChannel newCh = null; - try - { - newCh = await guild.CreateTextChannelAsync(info.TempChannelName); - } - catch (Discord.Net.HttpException ex) - { - throw new ApplicationException("Failed to create the channel. Internal error message: " + ex.Message); - } - - return newCh; - } - - /// - /// Sets the given guild's temporary channel as up for immediate expiration. - /// Use this to properly remove a temporary channel. - /// - public async Task SetChannelEarlyExpiry(SocketGuild guild) - { - lock (_trackedChannels) - { - if (!_trackedChannels.ContainsKey(guild.Id)) return; // how did we even get here? - _trackedChannels[guild.Id] = (DateTimeOffset.UtcNow, true); - } - } - #endregion - - } -} diff --git a/Module/VoteTempChannel/VoteTempChannel.cs b/Module/VoteTempChannel/VoteTempChannel.cs index 97e82c8..9f86815 100644 --- a/Module/VoteTempChannel/VoteTempChannel.cs +++ b/Module/VoteTempChannel/VoteTempChannel.cs @@ -10,133 +10,23 @@ using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.VoteTempChannel { /// - /// "Entry point" for VoteTempChannel feature. - /// Handles activation command depending on guild state. Also holds information on - /// temporary channels currently active. + /// 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; - ChannelManager _chMgr; - internal VoteStore _votes; public VoteTempChannel(DiscordSocketClient client) : base(client) { - _chMgr = new ChannelManager(this, client); - _votes = new VoteStore(); - - client.JoinedGuild += GuildEnter; - client.GuildAvailable += GuildEnter; - client.LeftGuild += GuildLeave; - client.MessageReceived += Client_MessageReceived; + client.MessageReceived += VoteChecking; + client.MessageReceived += TemporaryChannelActivityCheck; _backgroundWorkerCancel = new CancellationTokenSource(); _backgroundWorker = Task.Factory.StartNew(BackgroundCheckingTask, _backgroundWorkerCancel.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } - - private async Task GuildEnter(SocketGuild arg) - { - var conf = GetState(arg.Id); - if (conf != null) await _chMgr.RecheckExpiryInformation(arg, conf); - } - - private Task GuildLeave(SocketGuild arg) - { - _chMgr.DropCacheEntry(arg); - return Task.CompletedTask; - } - - // Handles all vote logic - private async Task Client_MessageReceived(SocketMessage arg) - { - if (arg.Author.IsBot) return; - if (arg.Channel is IDMChannel) return; - var guild = (arg.Channel as SocketTextChannel)?.Guild; - if (guild == null) return; - var conf = GetConfig(guild.Id); - if (conf == null) return; - - if (!arg.Content.StartsWith(conf.VoteCommand, StringComparison.InvariantCultureIgnoreCase)) return; - - var voteResult = _votes.AddVote(guild.Id, arg.Author.Id, out int voteCount); - if (voteResult == VoteStatus.FailCooldown) - { - await arg.Channel.SendMessageAsync(":x: Cooldown in effect. Try again later."); - return; - } - - const string VoteError = ":x: You have already placed your vote."; - - if (_chMgr.HasExistingTemporaryChannel(guild, conf)) - { - // Ignore votes not coming from the temporary channel itself. - if (!string.Equals(arg.Channel.Name, conf.TempChannelName, StringComparison.InvariantCultureIgnoreCase)) - { - _votes.DelVote(guild.Id, arg.Author.Id); - return; - } - if (voteResult == VoteStatus.FailVotedAlready) - { - await arg.Channel.SendMessageAsync(VoteError); - return; - } - await HandleVote_TempChannelExists(arg, guild, conf, voteCount); - } - else - { - if (voteResult == VoteStatus.FailVotedAlready) - { - await arg.Channel.SendMessageAsync(VoteError); - return; - } - await HandleVote_TempChannelNotExists(arg, guild, conf, voteCount); - } - } - - private async Task HandleVote_TempChannelNotExists(SocketMessage arg, SocketGuild guild, Configuration conf, int voteCount) - { - bool threshold = voteCount >= conf.VotePassThreshold; - RestTextChannel newCh = null; - - if (threshold) - { - newCh = await _chMgr.CreateChannelAndEntryAsync(guild, conf); - _votes.ClearVotes(guild.Id); - } - - await arg.Channel.SendMessageAsync(":white_check_mark: Channel creation vote has been counted." - + (threshold ? $"\n<#{newCh.Id}> is now available!" : "")); - if (newCh != null) - await newCh.SendMessageAsync($"Welcome to <#{newCh.Id}>!" - + "\nPlease note that this channel is temporary and *will* be deleted at a later time."); - } - - private async Task HandleVote_TempChannelExists(SocketMessage arg, SocketGuild guild, Configuration conf, int voteCount) - { - // It's been checked that the incoming message originated from the temporary channel itself before coming here. - if (!_chMgr.IsUpForRenewal(guild, conf)) - { - // TODO consider changing 'renewal' to 'extension' in other references, because the word makes more sense - if (conf.ChannelExtendDuration != TimeSpan.Zero) - await arg.Channel.SendMessageAsync(":x: Cannot currently vote for a time extension. Try again later."); - else - await arg.Channel.SendMessageAsync(":x: This channel's duration may not be extended."); - _votes.ClearVotes(guild.Id); - return; - } - - bool threshold = voteCount >= conf.VotePassThreshold; - if (threshold) - { - _votes.ClearVotes(guild.Id); - await _chMgr.ExtendChannelExpirationAsync(guild, conf); - } - - await arg.Channel.SendMessageAsync(":white_check_mark: Extension vote has been counted." - + (threshold ? "\nThis channel's duration has been extended." : "")); - } public override Task CreateInstanceState(JToken configSection) { @@ -148,6 +38,103 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel 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 conf = GetState(guild.Id); + if (conf == null) return; + + // Only check the designated voting channel + if (!string.Equals(arg.Channel.Name, conf.Config.VotingChannel, + StringComparison.InvariantCultureIgnoreCase)) return; + + // Check if command invoked + if (!arg.Content.StartsWith(conf.Config.VoteCommand, StringComparison.InvariantCultureIgnoreCase)) return; + + // Check if we're accepting votes. Locking here; background task may be using this. + bool cooldown; + bool voteCounted = false; + string newChannelName = null; + lock (conf) + { + if (conf.GetTemporaryChannel(guild) != null) return; // channel exists, nothing to vote for + cooldown = conf.Voting.IsInCooldown(); + if (!cooldown) + { + voteCounted = conf.Voting.AddVote(arg.Author.Id, out var voteCount); + if (voteCount >= conf.Config.VotePassThreshold) + { + newChannelName = conf.Config.TempChannelName; + } + } + + // Prepare new temporary channel while we're still locking state + if (newChannelName != null) conf.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 conf = GetState(guild.Id); + if (conf == null) return Task.CompletedTask; + + lock (conf) + { + var tch = conf.GetTemporaryChannel(guild); + if (arg.Channel.Name == tch.Name) + { + conf.TempChannelLastActivity = DateTimeOffset.UtcNow; + } + } + + return Task.CompletedTask; + } + #region Background tasks /// /// Two functions: Removes expired channels, and announces expired votes. diff --git a/Module/VoteTempChannel/VotingSession.cs b/Module/VoteTempChannel/VotingSession.cs index 23ccc99..cd2dbdb 100644 --- a/Module/VoteTempChannel/VotingSession.cs +++ b/Module/VoteTempChannel/VotingSession.cs @@ -23,12 +23,17 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel /// Counts a user vote. /// /// False if the user already has a vote counted. - public bool AddVote(ulong id) + public bool AddVote(ulong id, out int voteCount) { // Mark the start of a new session, if applicable. if (_votes.Count == 0) _initialVoteTime = DateTimeOffset.UtcNow; - if (_votes.Contains(id)) return false; + if (_votes.Contains(id)) + { + voteCount = -1; + return false; + } _votes.Add(id); + voteCount = _votes.Count; return true; }