Ported EntryAutoRole from RegexBot

Untested and missing some error handling at the moment.
This commit is contained in:
Noikoio 2019-02-17 15:59:02 -08:00
parent 9efb35a046
commit 6f7ffda63b
3 changed files with 236 additions and 0 deletions

View file

@ -2,6 +2,7 @@
using Discord.WebSocket; using Discord.WebSocket;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Kerobot.Common namespace Kerobot.Common
{ {
@ -117,6 +118,7 @@ namespace Kerobot.Common
/// <param name="id">If known, outputs the ID of the corresponding entity.</param> /// <param name="id">If known, outputs the ID of the corresponding entity.</param>
/// <param name="keepId">Specifies if the internal ID value should be stored if a match is found.</param> /// <param name="keepId">Specifies if the internal ID value should be stored if a match is found.</param>
/// <returns>True if the ID is known.</returns> /// <returns>True if the ID is known.</returns>
[Obsolete]
public bool TryResolve(SocketGuild searchGuild, out ulong id, bool keepId, EntityType searchType) public bool TryResolve(SocketGuild searchGuild, out ulong id, bool keepId, EntityType searchType)
{ {
if (Id.HasValue) if (Id.HasValue)
@ -215,5 +217,35 @@ namespace Kerobot.Common
else else
return $"{pf}{Name}"; return $"{pf}{Name}";
} }
#region Helper methods
/// <summary>
/// Attempts to find the corresponding role within the given guild.
/// </summary>
/// <param name="guild">The guild in which to search for the role.</param>
/// <param name="updateMissingID">
/// Specifies if this EntityName instance should keep the snowflake ID of the
/// corresponding role found in this guild, if it is not already known by this instance.
/// </param>
/// <returns></returns>
public SocketRole FindRoleIn(SocketGuild guild, bool updateMissingID = false)
{
if (this.Type != EntityType.Role)
throw new ArgumentException("This EntityName instance must correspond to a Role.");
bool dirty = false; // flag to update ID if possible regardless of updateMissingID setting
if (this.Id.HasValue)
{
var role = guild.GetRole(Id.Value);
if (role != null) return role;
else dirty = true; // only set if ID already existed but is now invalid
}
var r = guild.Roles.FirstOrDefault(rq => string.Equals(rq.Name, this.Name, StringComparison.OrdinalIgnoreCase));
if (r != null && (updateMissingID || dirty)) this.Id = r.Id;
return r;
}
#endregion
} }
} }

View file

@ -0,0 +1,127 @@
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Kerobot.Modules.EntryRole
{
/// <summary>
/// Automatically sets a role onto users entering the guild.
/// </summary>
// TODO add persistent role support, make it an option
[KerobotModule]
public class EntryRole : ModuleBase
{
readonly Task _workerTask;
readonly CancellationTokenSource _workerTaskToken; // TODO make use of this when possible
public EntryRole(Kerobot kb) : base(kb)
{
DiscordClient.UserJoined += DiscordClient_UserJoined;
DiscordClient.UserLeft += DiscordClient_UserLeft;
_workerTaskToken = new CancellationTokenSource();
_workerTask = Task.Factory.StartNew(RoleApplyWorker, _workerTaskToken.Token,
TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
private Task DiscordClient_UserJoined(SocketGuildUser arg)
{
GetGuildState<GuildData>(arg.Guild.Id)?.WaitlistAdd(arg.Id);
return Task.CompletedTask;
}
private Task DiscordClient_UserLeft(SocketGuildUser arg)
{
GetGuildState<GuildData>(arg.Guild.Id)?.WaitlistRemove(arg.Id);
return Task.CompletedTask;
}
public override Task<object> CreateGuildStateAsync(JToken config)
{
if (config == null) return null;
// preserve previously running timers?
// research: can GetState be called here or is it undefined?
if (config.Type != JTokenType.Object)
throw new ModuleLoadException("Configuration is not properly defined.");
return Task.FromResult<object>(new GuildData((JObject)config));
}
/// <summary>
/// Main worker task.
/// </summary>
private async Task RoleApplyWorker()
{
while (!_workerTaskToken.IsCancellationRequested)
{
await Task.Delay(5000);
var subworkers = new List<Task>();
foreach (var g in DiscordClient.Guilds)
{
subworkers.Add(RoleApplyGuildSubWorker(g));
}
Task.WaitAll(subworkers.ToArray());
}
}
/// <summary>
/// Guild-specific processing by worker task.
/// </summary>
internal async Task RoleApplyGuildSubWorker(SocketGuild g)
{
var gconf = GetGuildState<GuildData>(g.Id);
if (gconf == null) return;
// Get users to be affected
ulong[] userIds;
lock (gconf.WaitingList)
{
if (gconf.WaitingList.Count == 0) return;
var now = DateTimeOffset.UtcNow;
var queryIds = from item in gconf.WaitingList
where item.Value > now
select item.Key;
userIds = queryIds.ToArray();
foreach (var item in userIds) gconf.WaitingList.Remove(item);
}
var gusers = new List<SocketGuildUser>();
foreach (var item in userIds)
{
var gu = g.GetUser(item);
if (gu == null) continue; // silently drop unknown users (is this fine?)
gusers.Add(gu);
}
if (gusers.Count == 0) return;
// Attempt to get role.
var targetRole = gconf.TargetRole.FindRoleIn(g, true);
if (targetRole == null)
{
// Notify of this failure.
string failList = "";
foreach (var item in gusers) failList += $", {item.Username}#{item.Discriminator}";
await LogAsync(g.Id, "Unable to find role to apply. (Was the role deleted?) " +
"Failed to set role to the following users: " + failList.Substring(2));
}
// Apply roles
foreach (var item in gusers)
{
// TODO exception handling and notification on forbidden
if (item.Roles.Contains(targetRole)) continue;
await item.AddRoleAsync(targetRole);
}
}
}
}

View file

@ -0,0 +1,77 @@
using Kerobot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace Kerobot.Modules.EntryRole
{
/// <summary>
/// Contains configuration data as well as per-guild timers for those awaiting role assignment.
/// </summary>
class GuildData
{
/// <summary>
/// Lock on self.
/// </summary>
public Dictionary<ulong, DateTimeOffset> WaitingList { get; }
/// <summary>
/// Role to apply.
/// </summary>
public EntityName TargetRole { get; }
/// <summary>
/// Time to wait until applying the role, in seconds.
/// </summary>
public int WaitTime { get; }
const int WaitTimeMax = 600; // 10 minutes
public GuildData(JObject conf)
{
var cfgRole = conf["Role"]?.Value<string>();
if (string.IsNullOrWhiteSpace(cfgRole))
throw new ModuleLoadException("Role value not specified.");
try
{
TargetRole = new EntityName(cfgRole);
}
catch (ArgumentException)
{
throw new ModuleLoadException("Role config value was not properly specified to be a role.");
}
try
{
WaitTime = conf["WaitTime"].Value<int>();
}
catch (NullReferenceException)
{
throw new ModuleLoadException("WaitTime value not specified.");
}
catch (InvalidCastException)
{
throw new ModuleLoadException("WaitTime value must be a number.");
}
if (WaitTime > WaitTimeMax)
{
// don't silently correct it
throw new ModuleLoadException($"WaitTime value may not exceed {WaitTimeMax} seconds.");
}
if (WaitTime < 0)
{
throw new ModuleLoadException("WaitTime value may not be negative.");
}
}
public void WaitlistAdd(ulong userId)
{
lock (WaitingList) WaitingList.Add(userId, DateTimeOffset.UtcNow.AddSeconds(WaitTime));
}
public void WaitlistRemove(ulong userId)
{
lock (WaitingList) WaitingList.Remove(userId);
}
}
}