Ported EntryAutoRole from RegexBot
Untested and missing some error handling at the moment.
This commit is contained in:
parent
9efb35a046
commit
6f7ffda63b
3 changed files with 236 additions and 0 deletions
|
@ -2,6 +2,7 @@
|
|||
using Discord.WebSocket;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
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="keepId">Specifies if the internal ID value should be stored if a match is found.</param>
|
||||
/// <returns>True if the ID is known.</returns>
|
||||
[Obsolete]
|
||||
public bool TryResolve(SocketGuild searchGuild, out ulong id, bool keepId, EntityType searchType)
|
||||
{
|
||||
if (Id.HasValue)
|
||||
|
@ -215,5 +217,35 @@ namespace Kerobot.Common
|
|||
else
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
127
Modules-PublicInstance/EntryRole/EntryRole.cs
Normal file
127
Modules-PublicInstance/EntryRole/EntryRole.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
Modules-PublicInstance/EntryRole/GuildData.cs
Normal file
77
Modules-PublicInstance/EntryRole/GuildData.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue