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>
|
||||
/// The type of entity specified in an <see cref="EntityName"/>.
|
||||
/// </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>
|
||||
/// Helper class that holds an entity's name, ID, or both.
|
||||
/// 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>
|
||||
public class EntityName
|
||||
{
|
||||
|
@ -33,7 +40,6 @@ namespace Kerobot.Common
|
|||
/// </summary>
|
||||
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.
|
||||
|
||||
/// <summary>
|
||||
|
@ -42,6 +48,7 @@ namespace Kerobot.Common
|
|||
/// </summary>
|
||||
/// <param name="input">Input string in EntityName format.</param>
|
||||
/// <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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
|
@ -54,9 +61,11 @@ namespace Kerobot.Common
|
|||
if (input[0] == '&') Type = EntityType.Role;
|
||||
else if (input[0] == '#') Type = EntityType.Channel;
|
||||
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?
|
||||
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
|
||||
/// <summary>
|
||||
/// Attempts to determine the corresponding ID if not already known.
|
||||
/// Searches the given guild for it and stores it into this instance if found.
|
||||
/// Immediately returns the ID if it is already known by this instance.
|
||||
/// Searches the specified guild and stores it into this instance if found.
|
||||
/// Places the ID into <paramref name="id"/> when and if the result is known.
|
||||
/// </summary>
|
||||
/// <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="keepId">Specifies if the internal ID value should be stored if a match is found.</param>
|
||||
/// <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)
|
||||
{
|
||||
|
@ -141,7 +156,7 @@ namespace Kerobot.Common
|
|||
{
|
||||
if (resolver.Invoke(item))
|
||||
{
|
||||
Id = item.Id;
|
||||
if (keepId) Id = item.Id;
|
||||
id = Id.Value;
|
||||
return true;
|
||||
}
|
||||
|
@ -175,23 +190,23 @@ namespace Kerobot.Common
|
|||
/// <summary>
|
||||
/// Returns the appropriate prefix corresponding to an EntityType.
|
||||
/// </summary>
|
||||
public static string Prefix(EntityType t)
|
||||
public static char Prefix(EntityType t)
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case EntityType.Role: return "&";
|
||||
case EntityType.Channel: return "#";
|
||||
case EntityType.User: return "@";
|
||||
default: return "";
|
||||
case EntityType.Role: return '&';
|
||||
case EntityType.Channel: return '#';
|
||||
case EntityType.User: return '@';
|
||||
default: return '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of this EntityName, in EntityName format.
|
||||
/// Returns a string representation of this item in proper EntityName format.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
string pf = Prefix(Type);
|
||||
char pf = Prefix(Type);
|
||||
|
||||
if (Id.HasValue && Name != null)
|
||||
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