Implement style suggestions
This commit is contained in:
parent
7668c82cf9
commit
cc148d5257
18 changed files with 54 additions and 61 deletions
|
@ -1,11 +1,12 @@
|
|||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace RegexBot.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;
|
||||
private readonly ReadOnlyCollection<EntityName> _innerList;
|
||||
|
||||
/// <summary>Gets an enumerable collection of all role names defined in this list.</summary>
|
||||
public IEnumerable<EntityName> Roles
|
||||
|
|
|
@ -11,7 +11,7 @@ public class RateLimit<T> where T : notnull {
|
|||
/// Time until an entry within this instance expires, in seconds.
|
||||
/// </summary>
|
||||
public int Timeout { get; }
|
||||
private Dictionary<T, DateTime> Entries { get; } = new Dictionary<T, DateTime>();
|
||||
private Dictionary<T, DateTime> Entries { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RateLimit<T>"/> instance with the default timeout value.
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using Discord;
|
||||
using RegexBot.Data;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
|
@ -9,26 +8,30 @@ namespace RegexBot.Common;
|
|||
/// <summary>
|
||||
/// Miscellaneous utility methods useful for the bot and modules.
|
||||
/// </summary>
|
||||
public static class Utilities {
|
||||
public static partial class Utilities {
|
||||
/// <summary>
|
||||
/// Gets a compiled regex that matches a channel tag and pulls its snowflake value.
|
||||
/// Gets a precompiled regex that matches a channel tag and pulls its snowflake value.
|
||||
/// </summary>
|
||||
public static Regex ChannelMention { get; } = new(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
[GeneratedRegex(@"<#(?<snowflake>\d+)>")]
|
||||
public static partial Regex ChannelMentionRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a compiled regex that matches a custom emoji and pulls its name and ID.
|
||||
/// Gets a precompiled regex that matches a custom emoji and pulls its name and ID.
|
||||
/// </summary>
|
||||
public static Regex CustomEmoji { get; } = new(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>", RegexOptions.Compiled);
|
||||
[GeneratedRegex(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>")]
|
||||
public static partial Regex CustomEmojiRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a compiled regex that matches a fully formed Discord handle, extracting the name and discriminator.
|
||||
/// Gets a precompiled regex that matches a fully formed Discord handle, extracting the name and discriminator.
|
||||
/// </summary>
|
||||
public static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
|
||||
[GeneratedRegex(@"(.+)#(\d{4}(?!\d))")]
|
||||
public static partial Regex DiscriminatorSearchRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a compiled regex that matches a user tag and pulls its snowflake value.
|
||||
/// Gets a precompiled regex that matches a user tag and pulls its snowflake value.
|
||||
/// </summary>
|
||||
public static Regex UserMention { get; } = new(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
[GeneratedRegex(@"<@!?(?<snowflake>\d+)>")]
|
||||
public static partial Regex UserMentionRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Performs common checks on the specified message to see if it fits all the criteria of a
|
||||
|
@ -77,7 +80,7 @@ public static class Utilities {
|
|||
/// If given string is in an EntityName format, returns a displayable representation of it based on
|
||||
/// a cache query. Otherwise, returns the input string as-is.
|
||||
/// </summary>
|
||||
[return: NotNullIfNotNull("input")]
|
||||
[return: NotNullIfNotNull(nameof(input))]
|
||||
public static string? TryFromEntityNameString(string? input, RegexbotClient bot) {
|
||||
string? result = null;
|
||||
try {
|
||||
|
|
|
@ -51,9 +51,8 @@ class Configuration {
|
|||
throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration.");
|
||||
}
|
||||
|
||||
var dbconf = conf["DatabaseOptions"]?.Value<JObject>();
|
||||
if (dbconf == null) throw new Exception("Database settings were not specified in configuration.");
|
||||
// TODO more detailed database configuration? password file, other advanced authentication settings... look into this.
|
||||
var dbconf = (conf["DatabaseOptions"]?.Value<JObject>())
|
||||
?? throw new Exception("Database settings were not specified in configuration.");
|
||||
Host = ReadConfKey<string>(dbconf, nameof(Host), false);
|
||||
Database = ReadConfKey<string?>(dbconf, nameof(Database), false);
|
||||
Username = ReadConfKey<string>(dbconf, nameof(Username), true);
|
||||
|
|
|
@ -4,9 +4,4 @@
|
|||
/// Represents an error occurring when a module attempts to create a new guild state object
|
||||
/// (that is, read or refresh its configuration).
|
||||
/// </summary>
|
||||
public class ModuleLoadException : Exception {
|
||||
/// <summary>
|
||||
/// Initializes this exception class with the specified error message.
|
||||
/// </summary>
|
||||
public ModuleLoadException(string message) : base(message) { }
|
||||
}
|
||||
public class ModuleLoadException(string message) : Exception(message) { }
|
||||
|
|
|
@ -38,7 +38,7 @@ static class ModuleLoader {
|
|||
return modules.AsReadOnly();
|
||||
}
|
||||
|
||||
static IEnumerable<RegexbotModule> LoadModulesFromAssembly(Assembly asm, RegexbotClient rb) {
|
||||
static List<RegexbotModule> LoadModulesFromAssembly(Assembly asm, RegexbotClient rb) {
|
||||
var eligibleTypes = from type in asm.GetTypes()
|
||||
where !type.IsAssignableFrom(typeof(RegexbotModule))
|
||||
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
|
||||
|
|
|
@ -45,7 +45,7 @@ internal class AutoResponder : RegexbotModule {
|
|||
if (def.Command == null) {
|
||||
await msg.Channel.SendMessageAsync(def.GetResponse());
|
||||
} else {
|
||||
var cmdline = def.Command.Split(new char[] { ' ' }, 2);
|
||||
var cmdline = def.Command.Split([' '], 2);
|
||||
|
||||
var ps = new ProcessStartInfo() {
|
||||
FileName = cmdline[0],
|
||||
|
|
|
@ -83,7 +83,7 @@ internal sealed class EntryRole : RegexbotModule, IDisposable {
|
|||
foreach (var g in DiscordClient.Guilds) {
|
||||
subworkers.Add(RoleApplyGuildSubWorker(g));
|
||||
}
|
||||
Task.WaitAll(subworkers.ToArray());
|
||||
Task.WaitAll([.. subworkers]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ class GuildData {
|
|||
|
||||
const int WaitTimeMax = 600; // 10 minutes
|
||||
|
||||
public GuildData(JObject conf) : this(conf, new Dictionary<ulong, DateTimeOffset>()) { }
|
||||
public GuildData(JObject conf) : this(conf, []) { }
|
||||
|
||||
public GuildData(JObject conf, Dictionary<ulong, DateTimeOffset> _waitingList) {
|
||||
WaitingList = _waitingList;
|
||||
|
|
|
@ -17,9 +17,9 @@ class ModuleConfig {
|
|||
Label = def[nameof(Label)]?.Value<string>()
|
||||
?? throw new ModuleLoadException($"'{nameof(Label)}' was not defined in a command definition.");
|
||||
var cmd = CreateCommandInstance(instance, def);
|
||||
if (commands.ContainsKey(cmd.Command)) {
|
||||
if (commands.TryGetValue(cmd.Command, out CommandConfig? existing)) {
|
||||
throw new ModuleLoadException(
|
||||
$"{Label}: The command name '{cmd.Command}' is already in use by '{commands[cmd.Command].Label}'.");
|
||||
$"{Label}: The command name '{cmd.Command}' is already in use by '{existing.Label}'.");
|
||||
}
|
||||
commands.Add(cmd.Command, cmd);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ class ResponseExecutor {
|
|||
_user = (SocketGuildUser)msg.Author;
|
||||
_guild = _user.Guild;
|
||||
|
||||
_reports = new();
|
||||
_reports = [];
|
||||
Log = logger;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,9 +23,7 @@ class ModuleConfig {
|
|||
} catch (FormatException) {
|
||||
throw new ModuleLoadException($"'{item.Name}' is not specified as a role.");
|
||||
}
|
||||
var role = name.FindRoleIn(g);
|
||||
if (role == null) throw new ModuleLoadException($"Unable to find role '{name}'.");
|
||||
|
||||
var role = name.FindRoleIn(g) ?? throw new ModuleLoadException($"Unable to find role '{name}'.");
|
||||
var channels = Utilities.LoadStringOrStringArray(item.Value);
|
||||
if (channels.Count == 0) throw new ModuleLoadException($"One or more channels must be defined under '{name}'.");
|
||||
foreach (var id in channels) {
|
||||
|
|
|
@ -10,24 +10,20 @@ namespace RegexBot;
|
|||
/// <remarks>
|
||||
/// Implementing classes should not rely on local variables to store runtime or state data for guilds.
|
||||
/// Instead, use <see cref="CreateGuildStateAsync"/> and <see cref="GetGuildState"/>.
|
||||
/// <br/><br/>
|
||||
/// Additionally, do not assume that <see cref="DiscordClient"/> is available during the constructor.
|
||||
/// </remarks>
|
||||
public abstract class RegexbotModule {
|
||||
public abstract class RegexbotModule(RegexbotClient bot) {
|
||||
/// <summary>
|
||||
/// Retrieves the bot instance.
|
||||
/// </summary>
|
||||
public RegexbotClient Bot { get; }
|
||||
public RegexbotClient Bot { get; } = bot;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Discord client instance.
|
||||
/// </summary>
|
||||
public DiscordSocketClient DiscordClient { get => Bot.DiscordClient; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a module is being loaded.
|
||||
/// At this point, all bot services are available, but Discord is not. Do not use <see cref="DiscordClient"/>.
|
||||
/// </summary>
|
||||
public RegexbotModule(RegexbotClient bot) => Bot = bot;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of this module.
|
||||
/// </summary>
|
||||
|
|
|
@ -6,10 +6,9 @@ namespace RegexBot.Services.CommonFunctions {
|
|||
/// functions may help enforce a sense of consistency across modules when performing common actions, and may
|
||||
/// inform services which provide any additional features the ability to respond to those actions ahead of time.
|
||||
/// </summary>
|
||||
internal partial class CommonFunctionsService : Service {
|
||||
internal partial class CommonFunctionsService(RegexbotClient bot) : Service(bot) {
|
||||
// Note: Several classes within this service created by its hooks are meant to be sent to modules,
|
||||
// therefore those public classes are placed into the root RegexBot namespace for the developer's convenience.
|
||||
public CommonFunctionsService(RegexbotClient bot) : base(bot) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ class UserCachingSubservice {
|
|||
if (sID.HasValue)
|
||||
query = query.Where(c => c.UserId == (long)sID.Value);
|
||||
if (nameSearch != null) {
|
||||
query = query.Where(c => c.Username.ToLower() == nameSearch.Value.name.ToLower());
|
||||
query = query.Where(c => c.Username.Equals(nameSearch.Value.name, StringComparison.OrdinalIgnoreCase));
|
||||
if (nameSearch.Value.disc != null) query = query.Where(c => c.Discriminator == nameSearch.Value.disc);
|
||||
}
|
||||
query = query.OrderByDescending(e => e.ULastUpdateTime);
|
||||
|
@ -95,7 +95,7 @@ class UserCachingSubservice {
|
|||
}
|
||||
|
||||
// Is search actually a ping? Extract ID.
|
||||
var m = Utilities.UserMention.Match(search);
|
||||
var m = Utilities.UserMentionRegex().Match(search);
|
||||
if (m.Success) search = m.Groups["snowflake"].Value;
|
||||
|
||||
// Is search a number? Assume ID, proceed to query.
|
||||
|
@ -117,8 +117,9 @@ class UserCachingSubservice {
|
|||
if (sID.HasValue)
|
||||
query = query.Where(c => c.UserId == (long)sID.Value);
|
||||
if (nameSearch != null) {
|
||||
query = query.Where(c => (c.Nickname != null && c.Nickname.ToLower() == nameSearch.Value.name.ToLower()) ||
|
||||
c.User.Username.ToLower() == nameSearch.Value.name.ToLower());
|
||||
query = query.Where(c => (c.Nickname != null
|
||||
&& c.Nickname.Equals(nameSearch.Value.name, StringComparison.OrdinalIgnoreCase))
|
||||
|| c.User.Username.Equals(nameSearch.Value.name, StringComparison.OrdinalIgnoreCase));
|
||||
if (nameSearch.Value.disc != null) query = query.Where(c => c.User.Discriminator == nameSearch.Value.disc);
|
||||
}
|
||||
query = query.OrderByDescending(e => e.GULastUpdateTime);
|
||||
|
@ -127,7 +128,7 @@ class UserCachingSubservice {
|
|||
}
|
||||
|
||||
// Is search actually a ping? Extract ID.
|
||||
var m = Utilities.UserMention.Match(search);
|
||||
var m = Utilities.UserMentionRegex().Match(search);
|
||||
if (m.Success) search = m.Groups["snowflake"].Value;
|
||||
|
||||
// Is search a number? Assume ID, proceed to query.
|
||||
|
@ -144,7 +145,7 @@ class UserCachingSubservice {
|
|||
private static (string, string?) SplitNameAndDiscriminator(string input) {
|
||||
string name;
|
||||
string? disc = null;
|
||||
var split = Utilities.DiscriminatorSearch.Match(input);
|
||||
var split = Utilities.DiscriminatorSearchRegex().Match(input);
|
||||
if (split.Success) {
|
||||
name = split.Groups[1].Value;
|
||||
disc = split.Groups[2].Value;
|
||||
|
|
|
@ -52,7 +52,7 @@ class LoggingService : Service {
|
|||
var now = DateTimeOffset.Now;
|
||||
var output = new StringBuilder();
|
||||
var prefix = $"[{now:s}] [{source}] ";
|
||||
foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) {
|
||||
foreach (var line in message.Split(["\r\n", "\n"], StringSplitOptions.None)) {
|
||||
output.Append(prefix).AppendLine(line);
|
||||
}
|
||||
var outstr = output.ToString();
|
||||
|
|
|
@ -6,12 +6,12 @@ namespace RegexBot.Services.ModuleState;
|
|||
/// </summary>
|
||||
class ModuleStateService : Service {
|
||||
private readonly Dictionary<ulong, EntityList> _moderators;
|
||||
private readonly Dictionary<ulong, Dictionary<Type, object?>> _stateData;
|
||||
private readonly Dictionary<ulong, Dictionary<Type, object?>> _guildStates;
|
||||
private readonly JObject _serverConfs;
|
||||
|
||||
public ModuleStateService(RegexbotClient bot, JObject servers) : base(bot) {
|
||||
_moderators = new();
|
||||
_stateData = new();
|
||||
_moderators = [];
|
||||
_guildStates = [];
|
||||
_serverConfs = servers;
|
||||
|
||||
bot.DiscordClient.GuildAvailable += RefreshGuildState;
|
||||
|
@ -25,17 +25,20 @@ class ModuleStateService : Service {
|
|||
}
|
||||
|
||||
private Task RemoveGuildData(SocketGuild arg) {
|
||||
_stateData.Remove(arg.Id);
|
||||
_guildStates.Remove(arg.Id);
|
||||
_moderators.Remove(arg.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Hooked
|
||||
public T? DoGetStateObj<T>(ulong guildId, Type t) {
|
||||
if (_stateData.ContainsKey(guildId) && _stateData[guildId].ContainsKey(t)) {
|
||||
// Leave handling of potential InvalidCastException to caller.
|
||||
return (T?)_stateData[guildId][t];
|
||||
if (_guildStates.TryGetValue(guildId, out var guildConfs)) {
|
||||
if (guildConfs.TryGetValue(t, out var moduleConf)) {
|
||||
// Leave handling of potential InvalidCastException to caller.
|
||||
return (T?)moduleConf;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
|
@ -71,7 +74,7 @@ class ModuleStateService : Service {
|
|||
}
|
||||
}
|
||||
_moderators[guild.Id] = mods;
|
||||
_stateData[guild.Id] = newStates;
|
||||
_guildStates[guild.Id] = newStates;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,11 @@
|
|||
/// Services provide core and shared functionality for this program. Modules are expected to call into services
|
||||
/// directly or indirectly in order to access bot features.
|
||||
/// </remarks>
|
||||
internal abstract class Service {
|
||||
public RegexbotClient BotClient { get; }
|
||||
internal abstract class Service(RegexbotClient bot) {
|
||||
public RegexbotClient BotClient { get; } = bot;
|
||||
|
||||
public string Name => GetType().Name;
|
||||
|
||||
public Service(RegexbotClient bot) => BotClient = bot;
|
||||
|
||||
/// <summary>
|
||||
/// Emits a log message.
|
||||
/// </summary>
|
||||
|
|
Loading…
Reference in a new issue