Added EntryAutoRole

This commit is contained in:
Noikoio 2018-03-21 14:06:32 -07:00
parent 2deca3abad
commit 8e80e0241b
6 changed files with 249 additions and 4 deletions

View file

@ -0,0 +1,186 @@
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.EntryAutoRole
{
/// <summary>
/// Automatically sets a specified role
/// </summary>
class EntryAutoRole : BotModule
{
public override string Name => "EntryAutoRole";
private List<AutoRoleEntry> _roleWaitlist;
private object _roleWaitLock = new object();
// TODO make use of this later if/when some shutdown handler gets added
// (else it continues running in debug after the client has been disposed)
private readonly CancellationTokenSource _workerCancel;
// Config:
// Role: string - Name or ID of the role to apply. Takes EntityName format.
// WaitTime: number - Amount of time in seconds to wait until applying the role to a new user.
public EntryAutoRole(DiscordSocketClient client) : base(client)
{
client.GuildAvailable += Client_GuildAvailable;
client.UserJoined += Client_UserJoined;
client.UserLeft += Client_UserLeft;
_roleWaitlist = new List<AutoRoleEntry>();
_workerCancel = new CancellationTokenSource();
Task.Factory.StartNew(Worker, _workerCancel.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
[ConfigSection("EntryAutoRole")]
public override Task<object> ProcessConfiguration(JToken configSection)
{
if (configSection.Type != JTokenType.Object)
{
throw new RuleImportException("Configuration for this section is invalid.");
}
return Task.FromResult<object>(new ModuleConfig((JObject)configSection));
}
private Task Client_GuildAvailable(SocketGuild arg)
{
var conf = (ModuleConfig)GetConfig(arg.Id);
if (conf == null) return Task.CompletedTask;
SocketRole trole = GetRole(arg);
if (trole == null) return Task.CompletedTask;
lock (_roleWaitLock)
foreach (var item in arg.Users)
{
if (item.IsBot) continue;
if (item.IsWebhook) continue;
if (item.Roles.Contains(trole)) continue;
_roleWaitlist.Add(new AutoRoleEntry()
{
GuildId = arg.Id,
UserId = item.Id,
ExpireTime = DateTimeOffset.UtcNow.AddSeconds(conf.TimeDelay)
});
}
return Task.CompletedTask;
}
private Task Client_UserLeft(SocketGuildUser arg)
{
if (GetConfig(arg.Guild.Id) == null) return Task.CompletedTask;
lock (_roleWaitLock) _roleWaitlist.RemoveAll(m => m.GuildId == arg.Guild.Id && m.UserId == arg.Id);
return Task.CompletedTask;
}
private Task Client_UserJoined(SocketGuildUser arg)
{
if (GetConfig(arg.Guild.Id) == null) return Task.CompletedTask;
lock (_roleWaitLock) _roleWaitlist.Add(new AutoRoleEntry()
{
GuildId = arg.Guild.Id,
UserId = arg.Id,
ExpireTime = DateTimeOffset.UtcNow.AddSeconds(((ModuleConfig)GetConfig(arg.Guild.Id)).TimeDelay)
});
return Task.CompletedTask;
}
// can return null
private SocketRole GetRole(SocketGuild g)
{
var conf = (ModuleConfig)GetConfig(g.Id);
if (conf == null) return null;
var roleInfo = conf.Role;
if (roleInfo.Id.HasValue)
{
var result = g.GetRole(roleInfo.Id.Value);
if (result != null) return result;
}
else
{
foreach (var role in g.Roles)
if (string.Equals(roleInfo.Name, role.Name)) return role;
}
Log("Unable to find role in " + g.Name).Wait();
return null;
}
struct AutoRoleEntry
{
public ulong GuildId;
public ulong UserId;
public DateTimeOffset ExpireTime;
}
async Task Worker()
{
while (!_workerCancel.IsCancellationRequested)
{
await Task.Delay(5000);
AutoRoleEntry[] jobsList;
lock (_roleWaitLock)
{
var chk = DateTimeOffset.UtcNow;
// Attempt to avoid throttling: only 3 per run are processed
var jobs = _roleWaitlist.Where(i => chk > i.ExpireTime).Take(3);
jobsList = jobs.ToArray(); // force evaluation
// remove selected entries from current list
foreach (var item in jobsList)
{
_roleWaitlist.Remove(item);
}
}
// Temporary SocketRole cache. key = guild ID
Dictionary<ulong, SocketRole> cr = new Dictionary<ulong, SocketRole>();
foreach (var item in jobsList)
{
if (_workerCancel.IsCancellationRequested) return;
// do we have the guild?
var g = Client.GetGuild(item.GuildId);
if (g == null) continue; // bot probably left the guild
// do we have the user?
var u = g.GetUser(item.UserId);
if (u == null) continue; // user is probably gone
// do we have the role?
SocketRole r;
if (!cr.TryGetValue(g.Id, out r))
{
r = GetRole(g);
if (r != null) cr[g.Id] = r;
}
if (r == null)
{
await Log($"Skipping {g.Name}/{u.ToString()}");
await Log("Was the role renamed or deleted?");
}
// do the work
try
{
await u.AddRoleAsync(r);
}
catch (Discord.Net.HttpException ex)
{
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
{
await Log($"WARNING: Cannot set roles! Skipping {g.Name}/{u.ToString()}");
}
}
}
}
}
}
}

View file

@ -0,0 +1,35 @@
using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem;
namespace Noikoio.RegexBot.Module.EntryAutoRole
{
class ModuleConfig
{
private EntityName _cfgRole;
private int _cfgTime;
public EntityName Role => _cfgRole;
public int TimeDelay => _cfgTime;
public ModuleConfig(JObject conf)
{
var cfgRole = conf["Role"]?.Value<string>();
if (string.IsNullOrWhiteSpace(cfgRole))
throw new RuleImportException("Role was not specified.");
_cfgRole = new EntityName(cfgRole, EntityType.Role);
var inTime = conf["WaitTime"]?.Value<string>();
if (string.IsNullOrWhiteSpace(inTime))
throw new RuleImportException("WaitTime was not specified.");
if (!int.TryParse(inTime, out _cfgTime))
{
throw new RuleImportException("WaitTime must be a numeric value.");
}
if (_cfgTime < 0)
{
throw new RuleImportException("WaitTime must be a positive integer.");
}
}
}
}

View file

@ -58,6 +58,7 @@ namespace Noikoio.RegexBot
new Module.AutoMod.AutoMod(_client), new Module.AutoMod.AutoMod(_client),
new Module.ModCommands.CommandListener(_client), new Module.ModCommands.CommandListener(_client),
new Module.AutoRespond.AutoRespond(_client), new Module.AutoRespond.AutoRespond(_client),
new Module.EntryAutoRole.EntryAutoRole(_client),
// EntityCache loads before anything using it // EntityCache loads before anything using it
new EntityCache.Module(_client), new EntityCache.Module(_client),

View file

@ -4,7 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<RootNamespace>Noikoio.RegexBot</RootNamespace> <RootNamespace>Noikoio.RegexBot</RootNamespace>
<AssemblyVersion>2.5.0.0</AssemblyVersion> <AssemblyVersion>2.5.1.0</AssemblyVersion>
<Description>Highly configurable Discord moderation bot</Description> <Description>Highly configurable Discord moderation bot</Description>
<Authors>Noikoio</Authors> <Authors>Noikoio</Authors>
<Company /> <Company />

21
docs/entryautorole.md Normal file
View file

@ -0,0 +1,21 @@
## EntryAutoRole
EntryAutoRole is a component that automatically assigns a role to users after a set amount of time. It is useful for limiting access to incoming users and as a basic means of controlling raids.
Roles set by this component do not persist. Should a user leave the server and rejoin, they will not be given the role again immediately and must wait to have it reassigned.
Sample within a [server definition](serverdef.html):
```
"EntryAutoRole": {
"Role": "123451234512345::Newbie",
"WaitTime": 600
}
```
### Configuration options
EntryAutoRole is simple to configure. All the following values are **required**.
* Role (*string*) - The role to set. If specified by string, it will search for a role matching that name. If specified by ID, the ID will be used and server managers are free to edit the role name without modifying this value.
* If a name is given, then an role matching the name will be applied. Renaming the role will cause the component to fail to make use of the role until configuration is updated.
* If an ID is specified, server managers are free to rename the role and still have it be used by the bot.
* To find your role IDs, you may use a tool such as [Role ID Query Bot](https://discordapp.com/oauth2/authorize?client_id=425050329068077057&scope=bot).
* WaitTime (*number*) - Amount of time in seconds to wait until a new user is applied the role.

View file

@ -26,6 +26,8 @@ The following is a list of accepted members within a server definition.
* id (*integer*) - **Required.** A value containing the server's [unique ID](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-). * id (*integer*) - **Required.** A value containing the server's [unique ID](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).
* name (*string*) - Preferably a readable version of the server's name. Not used for anything other than internal logging. * name (*string*) - Preferably a readable version of the server's name. Not used for anything other than internal logging.
* moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModCommands* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details. * moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModCommands* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details.
* [automod](automod.html) (*name/value pairs*) - See respective page. * [automod](automod.html) - See respective page.
* [autoresponses](autorespond.html) (*name/value pairs*) - See respective page. * [autoresponses](autorespond.html) - See respective page.
* [ModCommands](modcommands.html) (*name/value pairs*) - See respective page. * [EntryAutoRole](entryautorole.html) - See respective page.
* [ModCommands](modcommands.html) - See respective page.
* [ModLogs](modlogs.html) - See respective page.