Reimplemented EntityList, FilterList
Based on similar classes from RegexBot. -EntityList no longer separates by entity type. -Both types allow caching their respective EntityName ID values. -FilterList supports loading from custom field names.
This commit is contained in:
parent
9b6104030d
commit
1fbc9bca7f
3 changed files with 284 additions and 16 deletions
144
Kerobot/Common/EntityList.cs
Normal file
144
Kerobot/Common/EntityList.cs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Kerobot.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values.
|
||||||
|
/// </summary>
|
||||||
|
public class EntityList : IEnumerable<EntityName>
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyCollection<EntityName> _innerList;
|
||||||
|
|
||||||
|
/// <summary>Gets an enumerable collection of all role names defined in this list.</summary>
|
||||||
|
public IEnumerable<EntityName> Roles
|
||||||
|
=> _innerList.Where(n => n.Type == EntityType.Role);
|
||||||
|
|
||||||
|
/// <summary>Gets an enumerable collection of all channel names defined in this list.</summary>
|
||||||
|
public IEnumerable<EntityName> Channels
|
||||||
|
=> _innerList.Where(n => n.Type == EntityType.Channel);
|
||||||
|
|
||||||
|
/// <summary>Gets an enumerable collection of all user names defined in this list.</summary>
|
||||||
|
public IEnumerable<EntityName> Users
|
||||||
|
=> _innerList.Where(n => n.Type == EntityType.User);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new EntityList instance with no data.
|
||||||
|
/// </summary>
|
||||||
|
public EntityList() : this(null, false) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new EntityList instance using the given JSON token as input.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">JSON array to be used for input. For ease of use, null values are also accepted.</param>
|
||||||
|
/// <param name="enforceTypes">Specifies if all entities defined in configuration must have their type specified.</param>
|
||||||
|
/// <exception cref="ArgumentException">The input is not a JSON array.</exception>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Unintiutively, this exception is thrown if a user-provided configuration value is blank.
|
||||||
|
/// </exception>
|
||||||
|
/// <exception cref="FormatException">
|
||||||
|
/// When enforceTypes is set, this is thrown if an EntityName results in having its Type be Unspecified.
|
||||||
|
/// </exception>
|
||||||
|
public EntityList(JToken input, bool enforceTypes)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
_innerList = new List<EntityName>().AsReadOnly();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Type != JTokenType.Array)
|
||||||
|
throw new ArgumentException("JToken input must be a JSON array.");
|
||||||
|
var inputArray = (JArray)input;
|
||||||
|
|
||||||
|
var list = new List<EntityName>();
|
||||||
|
foreach (var item in inputArray.Values<string>())
|
||||||
|
{
|
||||||
|
var itemName = new EntityName(item);
|
||||||
|
if (enforceTypes && itemName.Type == EntityType.Unspecified)
|
||||||
|
throw new FormatException($"The following value is not prefixed: {item}");
|
||||||
|
list.Add(itemName);
|
||||||
|
}
|
||||||
|
_innerList = list.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Entity matching
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the parameters of the given <see cref="SocketMessage"/> matches with
|
||||||
|
/// any entity specified in this list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keepId">
|
||||||
|
/// Specifies if EntityName instances within this list should have their internal ID value
|
||||||
|
/// updated if found during the matching process.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// True if the message author exists in this list, or if the message's channel exists in this list,
|
||||||
|
/// or if the message author contains a role that exists in this list.
|
||||||
|
/// </returns>
|
||||||
|
public bool IsListMatch(SocketMessage msg, bool keepId)
|
||||||
|
{
|
||||||
|
var author = msg.Author as SocketGuildUser;
|
||||||
|
var authorRoles = author.Roles;
|
||||||
|
var channel = msg.Channel;
|
||||||
|
|
||||||
|
foreach (var entry in this)
|
||||||
|
{
|
||||||
|
if (entry.Type == EntityType.Role)
|
||||||
|
{
|
||||||
|
if (entry.Id.HasValue)
|
||||||
|
{
|
||||||
|
return authorRoles.Any(r => r.Id == entry.Id.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var r in authorRoles)
|
||||||
|
{
|
||||||
|
if (!string.Equals(r.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
|
||||||
|
if (keepId) entry.SetId(r.Id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (entry.Type == EntityType.Channel)
|
||||||
|
{
|
||||||
|
if (entry.Id.HasValue)
|
||||||
|
{
|
||||||
|
return entry.Id.Value == channel.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.Equals(channel.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
|
||||||
|
if (keepId) entry.SetId(channel.Id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // User
|
||||||
|
{
|
||||||
|
if (entry.Id.HasValue)
|
||||||
|
{
|
||||||
|
return entry.Id.Value == author.Id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.Equals(author.Username, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
|
||||||
|
if (keepId) entry.SetId(author.Id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public override string ToString() => $"Entity list contains {_innerList.Count} item(s).";
|
||||||
|
|
||||||
|
public IEnumerator<EntityName> GetEnumerator() => _innerList.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,12 +8,19 @@ namespace Kerobot.Common
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of entity specified in an <see cref="EntityName"/>.
|
/// The type of entity specified in an <see cref="EntityName"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum EntityType { Unspecified, Role, Channel, User }
|
public enum EntityType
|
||||||
|
{
|
||||||
|
/// <summary>Default value. Is never referenced in regular usage.</summary>
|
||||||
|
Unspecified,
|
||||||
|
Role,
|
||||||
|
Channel,
|
||||||
|
User
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Helper class that holds an entity's name, ID, or both.
|
/// Helper class that holds an entity's name, ID, or both.
|
||||||
/// Meant to be used during configuration processing in cases where the configuration expects
|
/// Meant to be used during configuration processing in cases where the configuration expects
|
||||||
/// an entity name to be defined in a certain way and may or may not include its snowflake ID.
|
/// an entity name to be defined in a certain way which may or may not include its snowflake ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EntityName
|
public class EntityName
|
||||||
{
|
{
|
||||||
|
@ -33,7 +40,6 @@ namespace Kerobot.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; private set; }
|
public string Name { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
// TODO elsewhere: find a way to emit a warning if the user specified a name without ID in configuration.
|
// TODO elsewhere: find a way to emit a warning if the user specified a name without ID in configuration.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -42,6 +48,7 @@ namespace Kerobot.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">Input string in EntityName format.</param>
|
/// <param name="input">Input string in EntityName format.</param>
|
||||||
/// <exception cref="ArgumentNullException">Input string is null or blank.</exception>
|
/// <exception cref="ArgumentNullException">Input string is null or blank.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Input string cannot be resolved to an entity type.</exception>
|
||||||
public EntityName(string input)
|
public EntityName(string input)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
@ -54,9 +61,11 @@ namespace Kerobot.Common
|
||||||
if (input[0] == '&') Type = EntityType.Role;
|
if (input[0] == '&') Type = EntityType.Role;
|
||||||
else if (input[0] == '#') Type = EntityType.Channel;
|
else if (input[0] == '#') Type = EntityType.Channel;
|
||||||
else if (input[0] == '@') Type = EntityType.User;
|
else if (input[0] == '@') Type = EntityType.User;
|
||||||
|
|
||||||
if (Type != EntityType.Unspecified) input = input.Substring(1);
|
|
||||||
}
|
}
|
||||||
|
if (Type == EntityType.Unspecified)
|
||||||
|
throw new ArgumentException("Entity type unable to be inferred by given input.");
|
||||||
|
|
||||||
|
input = input.Substring(1); // Remove prefix
|
||||||
|
|
||||||
// Input contains ID/Label separator?
|
// Input contains ID/Label separator?
|
||||||
int separator = input.IndexOf("::");
|
int separator = input.IndexOf("::");
|
||||||
|
@ -93,16 +102,22 @@ namespace Kerobot.Common
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void SetId(ulong id)
|
||||||
|
{
|
||||||
|
if (!Id.HasValue) Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
#region Name to ID resolving
|
#region Name to ID resolving
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to determine the corresponding ID if not already known.
|
/// Attempts to determine the corresponding ID if not already known.
|
||||||
/// Searches the given guild for it and stores it into this instance if found.
|
/// Searches the specified guild and stores it into this instance if found.
|
||||||
/// Immediately returns the ID if it is already known by this instance.
|
/// Places the ID into <paramref name="id"/> when and if the result is known.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="searchType">The entity type to which this instance corresponds to.</param>
|
/// <param name="searchType">The entity type to which this instance corresponds to.</param>
|
||||||
/// <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>
|
||||||
/// <returns>True if the ID is known.</returns>
|
/// <returns>True if the ID is known.</returns>
|
||||||
public bool TryResolve(SocketGuild searchGuild, out ulong id, EntityType searchType = EntityType.Unspecified)
|
public bool TryResolve(SocketGuild searchGuild, out ulong id, bool keepId, EntityType searchType)
|
||||||
{
|
{
|
||||||
if (Id.HasValue)
|
if (Id.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -141,7 +156,7 @@ namespace Kerobot.Common
|
||||||
{
|
{
|
||||||
if (resolver.Invoke(item))
|
if (resolver.Invoke(item))
|
||||||
{
|
{
|
||||||
Id = item.Id;
|
if (keepId) Id = item.Id;
|
||||||
id = Id.Value;
|
id = Id.Value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -175,23 +190,23 @@ namespace Kerobot.Common
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the appropriate prefix corresponding to an EntityType.
|
/// Returns the appropriate prefix corresponding to an EntityType.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Prefix(EntityType t)
|
public static char Prefix(EntityType t)
|
||||||
{
|
{
|
||||||
switch (t)
|
switch (t)
|
||||||
{
|
{
|
||||||
case EntityType.Role: return "&";
|
case EntityType.Role: return '&';
|
||||||
case EntityType.Channel: return "#";
|
case EntityType.Channel: return '#';
|
||||||
case EntityType.User: return "@";
|
case EntityType.User: return '@';
|
||||||
default: return "";
|
default: return '\0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a string representation of this EntityName, in EntityName format.
|
/// Returns a string representation of this item in proper EntityName format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
string pf = Prefix(Type);
|
char pf = Prefix(Type);
|
||||||
|
|
||||||
if (Id.HasValue && Name != null)
|
if (Id.HasValue && Name != null)
|
||||||
return $"{pf}{Id.Value}::{Name}";
|
return $"{pf}{Id.Value}::{Name}";
|
||||||
|
|
109
Kerobot/Common/FilterList.cs
Normal file
109
Kerobot/Common/FilterList.cs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kerobot.Common
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents commonly-used configuration regarding whitelist/blacklist filtering, including exemptions.
|
||||||
|
/// </summary>
|
||||||
|
public class FilterList
|
||||||
|
{
|
||||||
|
public enum FilterMode { None, Whitelist, Blacklist }
|
||||||
|
|
||||||
|
public FilterMode Mode { get; }
|
||||||
|
public EntityList FilteredList { get; }
|
||||||
|
public EntityList FilterExemptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up a FilterList instance with the given JSON configuration section.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">
|
||||||
|
/// JSON object in which to attempt to find the given whitelist, blacklist, and/or excemption keys.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="whitelistKey">The key in which to search for the whitelist. Set as null to disable.</param>
|
||||||
|
/// <param name="blacklistKey">The key in which to search for the blacklist. Set as null to disable.</param>
|
||||||
|
/// <param name="exemptKey">The key in which to search for filter exemptions. Set as null to disable.</param>
|
||||||
|
/// <exception cref="FormatException">
|
||||||
|
/// Thrown if there is a problem with input. The exception message specifies the reason.
|
||||||
|
/// </exception>
|
||||||
|
public FilterList(
|
||||||
|
JObject config,
|
||||||
|
string whitelistKey = "whitelist",
|
||||||
|
string blacklistKey = "blacklist",
|
||||||
|
string exemptKey = "exempt")
|
||||||
|
{
|
||||||
|
if ((whitelistKey != null && config[whitelistKey] != null) &&
|
||||||
|
(blacklistKey != null && config[blacklistKey] != null))
|
||||||
|
{
|
||||||
|
throw new FormatException($"Cannot have both '{whitelistKey}' and '{blacklistKey}' defined at once.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JToken valueSrc = null;
|
||||||
|
if (whitelistKey != null)
|
||||||
|
{
|
||||||
|
// Try getting a whitelist
|
||||||
|
valueSrc = config[whitelistKey];
|
||||||
|
Mode = FilterMode.Whitelist;
|
||||||
|
}
|
||||||
|
else if (valueSrc != null && blacklistKey != null)
|
||||||
|
{
|
||||||
|
// Try getting a blacklist
|
||||||
|
valueSrc = config[blacklistKey];
|
||||||
|
Mode = FilterMode.Blacklist;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Got neither. Have an empty list.
|
||||||
|
Mode = FilterMode.None;
|
||||||
|
FilteredList = new EntityList();
|
||||||
|
FilterExemptions = new EntityList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the specified array is actually an array.
|
||||||
|
if (valueSrc.Type != JTokenType.Array) throw new ArgumentException("Given list must be a JSON array.");
|
||||||
|
FilteredList = new EntityList((JArray)valueSrc, true);
|
||||||
|
|
||||||
|
// Verify the same for the exemption list.
|
||||||
|
FilterExemptions = new EntityList();
|
||||||
|
if (exemptKey != null)
|
||||||
|
{
|
||||||
|
var exc = config[exemptKey];
|
||||||
|
if (exc != null && exc.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
FilterExemptions = new EntityList(exc, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if the parameters of the given message match up against the filtering
|
||||||
|
/// rules described within this instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keepId">
|
||||||
|
/// See equivalent documentation for <see cref="EntityList.IsListMatch(SocketMessage, bool)"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// True if the author or associated channel exists in and is not exempted by this instance.
|
||||||
|
/// </returns>
|
||||||
|
public bool IsFiltered(SocketMessage msg, bool keepId)
|
||||||
|
{
|
||||||
|
if (Mode == FilterMode.None) return false;
|
||||||
|
|
||||||
|
bool inFilter = FilteredList.IsListMatch(msg, keepId);
|
||||||
|
if (Mode == FilterMode.Whitelist)
|
||||||
|
{
|
||||||
|
if (!inFilter) return true;
|
||||||
|
return FilterExemptions.IsListMatch(msg, keepId);
|
||||||
|
}
|
||||||
|
else if (Mode == FilterMode.Blacklist)
|
||||||
|
{
|
||||||
|
if (!inFilter) return false;
|
||||||
|
return !FilterExemptions.IsListMatch(msg, keepId);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("it is not possible for this to happen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue