diff --git a/Module/VoteTempChannel/ChannelManager.cs b/Module/VoteTempChannel/ChannelManager.cs
index 7f8e94f..7a8a6ac 100644
--- a/Module/VoteTempChannel/ChannelManager.cs
+++ b/Module/VoteTempChannel/ChannelManager.cs
@@ -1,7 +1,6 @@
using Discord.Rest;
using Discord.WebSocket;
using System;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -10,17 +9,11 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
///
/// Keeps track of existing channels and expiry information. Manages data persistence.
///
- class ChannelManager : IDisposable
+ class ChannelManager
{
readonly VoteTempChannel _out;
readonly DiscordSocketClient _client;
- ///
- /// Key = guild, Value = expiry time
- /// Must lock!
- ///
- readonly Dictionary _trackedChannels;
-
readonly CancellationTokenSource _token;
readonly Task _bgTask;
@@ -28,37 +21,8 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
{
_out = module;
_client = client;
- _token = new CancellationTokenSource();
- _bgTask = Task.Factory.StartNew(ChannelExpirationChecker, _token.Token,
- TaskCreationOptions.LongRunning, TaskScheduler.Default);
- _trackedChannels = new Dictionary();
}
- public void Dispose()
- {
- _token.Cancel();
- _token.Dispose();
- }
-
- #region Querying
- ///
- /// Determines if the given guild has a temporary channel that is up for a renewal vote.
- ///
- public bool IsUpForRenewal(SocketGuild guild, Configuration info)
- {
- DateTimeOffset tcExp;
- lock (_trackedChannels)
- {
- if (!_trackedChannels.TryGetValue(guild.Id, out var val)) return false;
- tcExp = val.Item1;
- }
-
- var renewThreshold = tcExp - info.KeepaliveVoteDuration;
- return DateTimeOffset.UtcNow > renewThreshold;
- }
-
- #endregion
-
#region Channel entry manipulation
///
/// Creates the temporary channel.
@@ -84,27 +48,8 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
throw new ApplicationException("Failed to create the channel. Internal error message: " + ex.Message);
}
- // Channel creation succeeded. Regardless of persistent state, at least add it to in-memory cache.
- lock (_trackedChannels) _trackedChannels.Add(guild.Id, (channelExpiryTime, false));
-
return newCh;
}
-
- ///
- /// For an existing temporary channel, extends its lifetime by a predetermined amount.
- ///
- public async Task ExtendChannelExpirationAsync(SocketGuild guild, Configuration info)
- {
- DateTimeOffset newExpiration;
- lock (_trackedChannels)
- {
- if (!_trackedChannels.ContainsKey(guild.Id)) return; // how did we even get here?
-
- newExpiration = _trackedChannels[guild.Id].Item1;
- newExpiration+= info.ChannelExtendDuration;
- _trackedChannels[guild.Id] = (newExpiration, false);
- }
- }
///
/// Sets the given guild's temporary channel as up for immediate expiration.
@@ -118,99 +63,7 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
_trackedChannels[guild.Id] = (DateTimeOffset.UtcNow, true);
}
}
-
- ///
- /// Removes the given guild from the cache.
- ///
- public void DropCacheEntry(SocketGuild guild)
- {
- lock (_trackedChannels) _trackedChannels.Remove(guild.Id);
- }
#endregion
-
- ///
- /// Background task. Handles channel deletion on expiry.
- ///
- private async Task ChannelExpirationChecker()
- {
- while (!_token.Token.IsCancellationRequested)
- {
- lock (_trackedChannels)
- {
- var now = DateTimeOffset.UtcNow;
- var cachePostRemove = new List(); // list of items to remove; can't remove while iterating
- var cacheWarnSet = new List(); // list of items to update the announce flag; can't change while iterating
- foreach (var item in _trackedChannels)
- {
- var g = _client.GetGuild(item.Key);
- if (g == null)
- {
- // Cached guild is not known, somehow...
- cachePostRemove.Add(item.Key);
- continue;
- }
-
- var conf = _out.GetConfig(item.Key);
- if (conf == null)
- {
- // Cached guild has no config, somehow...
- cachePostRemove.Add(item.Key);
- continue;
- }
-
- var ch = FindTemporaryChannel(g, conf);
- if (ch == null)
- {
- // Temporary channel no longer exists.
- // Assume it's been deleted early, but do not start a cooldown.
- cachePostRemove.Add(item.Key);
- continue;
- }
-
- if (now > item.Value.Item1)
- {
- // Process channel removal
- try
- {
- ch.DeleteAsync().Wait();
- _out._votes.SetCooldown(ch.Guild.Id);
- }
- catch (Discord.Net.HttpException)
- {
- // On deletion error, attempt to report the issue. Discard from cache.
- try
- {
- ch.SendMessageAsync("Warning: Unable to remove temporary channel. It must now be done manually.");
- }
- catch (Discord.Net.HttpException) { }
- cachePostRemove.Add(item.Key);
- continue;
- }
- cachePostRemove.Add(item.Key);
- }
- else if (item.Value.Item2 == false && IsUpForRenewal(ch.Guild, conf))
- {
- // Process channel renewal warning
- ch.SendMessageAsync("This channel is nearing expiration! Vote to extend it by issuing " +
- $"the `{conf.VoteCommand}` command.").Wait();
- cacheWarnSet.Add(item.Key);
- }
- }
- foreach (var guildId in cachePostRemove)
- {
- _trackedChannels.Remove(guildId);
- }
- foreach (var id in cacheWarnSet)
- {
- var newdata = (_trackedChannels[id].Item1, true);
- _trackedChannels.Remove(id);
- _trackedChannels.Add(id, newdata);
- }
- }
-
- try { await Task.Delay(12 * 1000, _token.Token); }
- catch (TaskCanceledException) { break; }
- }
- }
+
}
}
diff --git a/Module/VoteTempChannel/Configuration.cs b/Module/VoteTempChannel/Configuration.cs
index b5f5b7a..d321aab 100644
--- a/Module/VoteTempChannel/Configuration.cs
+++ b/Module/VoteTempChannel/Configuration.cs
@@ -7,14 +7,42 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
{
class Configuration
{
+ ///
+ /// Command used to vote for the channel's creation.
+ ///
public string VoteCommand { get; }
+
+ ///
+ /// Name of the temporary channel, without prefix.
+ ///
public string TempChannelName { get; }
- public TimeSpan ChannelBaseDuration { get; }
- public TimeSpan ChannelExtendDuration { get; }
+
+ ///
+ /// Amount of time that the temporary channel can exist without activity before expiring and being deleted.
+ ///
+ public TimeSpan ChannelDuration { get; }
+
+ ///
+ /// Number of votes needed to create the channel.
+ ///
public int VotePassThreshold { get; }
+
+ ///
+ /// Amount of time that a voting session can last starting from its initial vote.
+ ///
public TimeSpan VotingDuration { get; }
+
+ ///
+ /// Amount of time to wait before another vote may be initiated, either after a failed vote
+ /// or from expiration of the temporary channel.
+ ///
public TimeSpan VotingCooldown { get; }
+ ///
+ /// Channel name in which voting takes place.
+ ///
+ public string VotingChannel { get; }
+
public Configuration(JObject j)
{
VoteCommand = j["VoteCommand"]?.Value();
@@ -23,11 +51,8 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
if (VoteCommand.Contains(" "))
throw new RuleImportException("'VoteCommand' must not contain spaces.");
- TempChannelName = j["TempChannelName"]?.Value();
- if (string.IsNullOrWhiteSpace(TempChannelName))
- throw new RuleImportException("'TempChannelName' must be specified.");
- if (!Regex.IsMatch(TempChannelName, @"^([A-Za-z0-9]|[-_ ])+$"))
- throw new RuleImportException("'TempChannelName' contains one or more invalid characters.");
+ TempChannelName = ParseChannelNameConfig(j, "TempChannelName");
+ VotingChannel = ParseChannelNameConfig(j, "VotingChannel");
var vptProp = j["VotePassThreshold"];
if (vptProp == null)
@@ -38,12 +63,21 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
if (VotePassThreshold <= 0)
throw new NotImplementedException("'VotePassThreshold' must be greater than zero.");
- ChannelBaseDuration = ParseTimeConfig(j, "ChannelBaseDuration");
- ChannelExtendDuration = ParseTimeConfig(j, "ChannelExtendDuration");
+ ChannelDuration = ParseTimeConfig(j, "ChannelDuration");
VotingDuration = ParseTimeConfig(j, "VotingDuration");
VotingCooldown = ParseTimeConfig(j, "VotingCooldown");
}
+ private string ParseChannelNameConfig(JObject conf, string valueName)
+ {
+ var value = j[valueName]?.Value();
+ if (string.IsNullOrWhiteSpace(value))
+ throw new RuleImportException($"'{valueName}' must be specified.");
+ if (!Regex.IsMatch(TempChannelName, @"^([A-Za-z0-9]|[-_ ])+$"))
+ throw new RuleImportException($"'{valueName}' contains one or more invalid characters.");
+ return value;
+ }
+
private TimeSpan ParseTimeConfig(JObject conf, string valueName)
{
var inputstr = conf[valueName]?.Value();
diff --git a/Module/VoteTempChannel/GuildInformation.cs b/Module/VoteTempChannel/GuildInformation.cs
new file mode 100644
index 0000000..90362d8
--- /dev/null
+++ b/Module/VoteTempChannel/GuildInformation.cs
@@ -0,0 +1,48 @@
+using Discord.WebSocket;
+using Newtonsoft.Json.Linq;
+using System;
+
+namespace Noikoio.RegexBot.Module.VoteTempChannel
+{
+ ///
+ /// Guild state object. Contains known information about the guild.
+ /// Contains helper functions that may involve usage of data contained within.
+ ///
+ class GuildInformation
+ {
+ public Configuration Config { get; }
+ public VotingSession Voting { get; }
+
+ ///
+ /// Timestamp of last activity in the temporary channel.
+ /// Used to determine its expiration.
+ ///
+ public DateTimeOffset TempChannelLastActivity { get; set; }
+
+ public GuildInformation(JObject conf)
+ {
+ // In case temp channel exists as we (re)start, begin a new timer for it.
+ TempChannelLastActivity = DateTimeOffset.UtcNow;
+
+ Config = new Configuration(conf);
+ Voting = new VotingSession();
+ }
+
+ public SocketTextChannel GetTemporaryChannel(SocketGuild guild)
+ {
+ foreach (var ch in guild.TextChannels)
+ {
+ if (string.Equals(ch.Name, Config.TempChannelName, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return ch;
+ }
+ }
+ return null;
+ }
+
+ public bool IsTempChannelExpired()
+ {
+ return DateTimeOffset.UtcNow > TempChannelLastActivity + Config.ChannelDuration;
+ }
+ }
+}
diff --git a/Module/VoteTempChannel/VoteTempChannel.cs b/Module/VoteTempChannel/VoteTempChannel.cs
index 4c0091d..97e82c8 100644
--- a/Module/VoteTempChannel/VoteTempChannel.cs
+++ b/Module/VoteTempChannel/VoteTempChannel.cs
@@ -4,6 +4,7 @@ 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
@@ -15,6 +16,8 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
///
class VoteTempChannel : BotModule
{
+ Task _backgroundWorker;
+ CancellationTokenSource _backgroundWorkerCancel;
ChannelManager _chMgr;
internal VoteStore _votes;
@@ -27,11 +30,15 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
client.GuildAvailable += GuildEnter;
client.LeftGuild += GuildLeave;
client.MessageReceived += Client_MessageReceived;
+
+ _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);
+ var conf = GetState(arg.Id);
if (conf != null) await _chMgr.RecheckExpiryInformation(arg, conf);
}
@@ -88,7 +95,7 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
}
}
- private async Task HandleVote_TempChannelNotExists(SocketMessage arg, SocketGuild guild, GuildConfiguration conf, int voteCount)
+ private async Task HandleVote_TempChannelNotExists(SocketMessage arg, SocketGuild guild, Configuration conf, int voteCount)
{
bool threshold = voteCount >= conf.VotePassThreshold;
RestTextChannel newCh = null;
@@ -106,7 +113,7 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
+ "\nPlease note that this channel is temporary and *will* be deleted at a later time.");
}
- private async Task HandleVote_TempChannelExists(SocketMessage arg, SocketGuild guild, GuildConfiguration conf, int voteCount)
+ 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))
@@ -136,15 +143,83 @@ namespace Noikoio.RegexBot.Module.VoteTempChannel
if (configSection == null) return Task.FromResult