diff --git a/Module/VoteTempChannel/GuildConfiguration.cs b/Module/VoteTempChannel/Configuration.cs similarity index 92% rename from Module/VoteTempChannel/GuildConfiguration.cs rename to Module/VoteTempChannel/Configuration.cs index b3e6656..b5f5b7a 100644 --- a/Module/VoteTempChannel/GuildConfiguration.cs +++ b/Module/VoteTempChannel/Configuration.cs @@ -5,16 +5,17 @@ using System.Text.RegularExpressions; namespace Noikoio.RegexBot.Module.VoteTempChannel { - class GuildConfiguration + class Configuration { public string VoteCommand { get; } public string TempChannelName { get; } public TimeSpan ChannelBaseDuration { get; } public TimeSpan ChannelExtendDuration { get; } - public TimeSpan KeepaliveVoteDuration { get; } public int VotePassThreshold { get; } + public TimeSpan VotingDuration { get; } + public TimeSpan VotingCooldown { get; } - public GuildConfiguration(JObject j) + public Configuration(JObject j) { VoteCommand = j["VoteCommand"]?.Value(); if (string.IsNullOrWhiteSpace(VoteCommand)) @@ -39,7 +40,8 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel ChannelBaseDuration = ParseTimeConfig(j, "ChannelBaseDuration"); ChannelExtendDuration = ParseTimeConfig(j, "ChannelExtendDuration"); - KeepaliveVoteDuration = ParseTimeConfig(j, "KeepaliveVoteDuration"); + VotingDuration = ParseTimeConfig(j, "VotingDuration"); + VotingCooldown = ParseTimeConfig(j, "VotingCooldown"); } private TimeSpan ParseTimeConfig(JObject conf, string valueName) diff --git a/Module/VoteTempChannel/VoteStore.cs b/Module/VoteTempChannel/VoteStore.cs deleted file mode 100644 index 4e105de..0000000 --- a/Module/VoteTempChannel/VoteStore.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Noikoio.RegexBot.Module.VoteTempChannel -{ - /// - /// Handles keeping track of per-guild voting, along with cooldowns. - /// - class VoteStore - { - /* - * Votes are kept track for a total of five minutes beginning with the first vote. - * All votes are discarded after five minutes have elapsed, rather than the data - * staying for longer as more votes are added. - */ - private Dictionary _cooldown; - private Dictionary _votes; - - private class VoteData - { - public VoteData() - { - VotingStart = DateTimeOffset.UtcNow; - Voters = new List(); - } - - public DateTimeOffset VotingStart { get; } - public List Voters { get; } - } - - public VoteStore() - { - _cooldown = new Dictionary(); - _votes = new Dictionary(); - } - - // !! Hardcoded value: votes always expire after 5 minutes. - static readonly TimeSpan VoteExpiry = new TimeSpan(0, 5, 0); - /// - /// Call before accessing votes. Removes any stale voting entries. - /// - private void CleanVoteData() - { - IEnumerable> expiredEntries; - lock (_votes) - { - var now = DateTimeOffset.UtcNow; - expiredEntries = (from item in _votes - where now > item.Value.VotingStart + VoteExpiry - select new Tuple(item.Key, item.Value.VotingStart)) - .ToArray(); - - lock (_cooldown) - { - // For expiring votes, set a cooldown that starts at the time the - // vote had actually expired. - foreach (var item in expiredEntries) - { - _votes.Remove(item.Item1); - _cooldown.Add(item.Item1, item.Item2 + VoteExpiry); - } - } - - } - } - - // !! Hardcoded value: cooldowns last one hour. - static readonly TimeSpan CooldownExpiry = new TimeSpan(1, 0, 0); - private bool IsInCooldown(ulong guild) - { - lock (_cooldown) - { - // Clean up expired entries first... - var now = DateTimeOffset.UtcNow; - var expiredEntries = (from item in _cooldown - where now > item.Value + CooldownExpiry - select item.Key).ToArray(); - foreach (var item in expiredEntries) _cooldown.Remove(item); - - // And then the actual check: - return _cooldown.ContainsKey(guild); - } - } - - public void SetCooldown(ulong guild) - { - lock (_cooldown) _cooldown.Add(guild, DateTimeOffset.UtcNow); - } - - public void ClearCooldown(ulong guild) - { - lock (_cooldown) _cooldown.Remove(guild); - } - - /// - /// Attempts to log a vote by a given user. - /// - public VoteStatus AddVote(ulong guild, ulong user, out int voteCount) - { - voteCount = -1; - if (IsInCooldown(guild)) return VoteStatus.FailCooldown; - lock (_votes) - { - CleanVoteData(); - VoteData v; - if (!_votes.TryGetValue(guild, out v)) - { - v = new VoteData(); - _votes[guild] = v; - } - voteCount = v.Voters.Count; - - if (v.Voters.Contains(user)) return VoteStatus.FailVotedAlready; - - v.Voters.Add(user); - voteCount++; - return VoteStatus.Success; - } - } - - public void DelVote(ulong guild, ulong user) - { - lock (_votes) - { - if (_votes.TryGetValue(guild, out var v)) - { - v.Voters.Remove(user); - if (v.Voters.Count == 0) _votes.Remove(guild); - } - } - } - - /// - /// Clears voting data from within the specified guild. - /// - public void ClearVotes(ulong guild) - { - lock (_votes) _votes.Remove(guild); - } - } - - enum VoteStatus - { - Success, FailVotedAlready, FailCooldown - } -} diff --git a/Module/VoteTempChannel/VotingSession.cs b/Module/VoteTempChannel/VotingSession.cs new file mode 100644 index 0000000..dbc7cfa --- /dev/null +++ b/Module/VoteTempChannel/VotingSession.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace Noikoio.RegexBot.Module.VoteTempChannel +{ + /// + /// Keeps information on voting sessions and voting cooldowns. + /// + class VotingSession + { + private Configuration _conf; + private DateTimeOffset _initialVoteTime; + private List _votes; + + public DateTimeOffset? CooldownStart { get; private set; } + + public VotingSession() + { + CooldownStart = null; + _votes = new List(); + } + + /// + /// Counts a user vote. + /// + /// False if the user already has a vote counted. + public bool AddVote(ulong id) + { + // Mark the start of a new session, if applicable. + if (_votes.Count == 0) _initialVoteTime = DateTimeOffset.UtcNow; + if (_votes.Contains(id)) return false; + _votes.Add(id); + return true; + } + + /// + /// Checks if the voting session has expired. + /// To be called by the background task. This automatically resets state. + /// + public bool IsSessionExpired() + { + if (_votes.Count == 0) return false; + if (DateTimeOffset.UtcNow > _initialVoteTime + _conf.VotingDuration) + { + // Clear votes. And because we're clearing it due to an expiration, set a cooldown. + Reset(); + StartCooldown(); + return true; + } + return false; + } + + public void StartCooldown() + { + CooldownStart = DateTimeOffset.UtcNow; + } + + public bool IsInCooldown() + { + if (!CooldownStart.HasValue) return false; + return CooldownStart.Value + _conf.VotingCooldown > DateTimeOffset.UtcNow; + } + + /// + /// Resets the object to its initial state. + /// + public void Reset() + { + _votes.Clear(); + CooldownStart = null; + } + } +}