Reorganized project
Moved modules into the assembly itself to simplify development of further features and reduce complexity in building this project. Additionally, many small adjustments were made, including: - Add documentation to most public methods that had it missing - Minor style updates - Updated readme to reflect near-completion of this rewrite - Remove any last remaining references to old project name Kerobot - Update dependencies
This commit is contained in:
parent
53e0301edd
commit
1149f2800d
73 changed files with 333 additions and 313 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
@ -10,9 +10,9 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build",
|
"preLaunchTask": "build",
|
||||||
// If you have changed target frameworks, make sure to update the program path.
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
"program": "${workspaceFolder}/output/Debug/net6.0/RegexBot.dll",
|
"program": "${workspaceFolder}/bin/Debug/net6.0/RegexBot.dll",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}/RegexBot",
|
"cwd": "${workspaceFolder}",
|
||||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
"stopAtEntry": false
|
"stopAtEntry": false
|
||||||
|
|
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
|
@ -7,7 +7,7 @@
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"${workspaceFolder}/RegexBot.sln",
|
"${workspaceFolder}/RegexBot.csproj",
|
||||||
"/property:GenerateFullPaths=true",
|
"/property:GenerateFullPaths=true",
|
||||||
"/consoleloggerparameters:NoSummary"
|
"/consoleloggerparameters:NoSummary"
|
||||||
],
|
],
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
"type": "process",
|
"type": "process",
|
||||||
"args": [
|
"args": [
|
||||||
"publish",
|
"publish",
|
||||||
"${workspaceFolder}/RegexBot.sln",
|
"${workspaceFolder}/RegexBot.csproj",
|
||||||
"/property:GenerateFullPaths=true",
|
"/property:GenerateFullPaths=true",
|
||||||
"/consoleloggerparameters:NoSummary"
|
"/consoleloggerparameters:NoSummary"
|
||||||
],
|
],
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
"watch",
|
"watch",
|
||||||
"run",
|
"run",
|
||||||
"--project",
|
"--project",
|
||||||
"${workspaceFolder}/RegexBot.sln"
|
"${workspaceFolder}/RegexBot.csproj"
|
||||||
],
|
],
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
using Discord.WebSocket;
|
using System.Collections;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace RegexBot.Common;
|
namespace RegexBot.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values.
|
/// Represents a commonly-used configuration structure: an array of strings consisting of <see cref="EntityName"/> values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -64,6 +61,7 @@ public class EntityList : IEnumerable<EntityName> {
|
||||||
/// Checks if the parameters of the given <see cref="SocketMessage"/> matches with
|
/// Checks if the parameters of the given <see cref="SocketMessage"/> matches with
|
||||||
/// any entity specified in this list.
|
/// any entity specified in this list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="msg">The incoming message object with which to scan for a match.</param>
|
||||||
/// <param name="keepId">
|
/// <param name="keepId">
|
||||||
/// Specifies if EntityName instances within this list should have their internal ID value
|
/// Specifies if EntityName instances within this list should have their internal ID value
|
||||||
/// updated if found during the matching process.
|
/// updated if found during the matching process.
|
|
@ -1,7 +1,4 @@
|
||||||
using Discord.WebSocket;
|
namespace RegexBot.Common;
|
||||||
|
|
||||||
namespace RegexBot.Common;
|
|
||||||
|
|
||||||
/// <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
|
20
Common/EntityType.cs
Normal file
20
Common/EntityType.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
namespace RegexBot.Common;
|
||||||
|
/// <summary>
|
||||||
|
/// The type of entity specified in an <see cref="EntityName"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum EntityType {
|
||||||
|
/// <summary>Default value. Is never referenced in regular usage.</summary>
|
||||||
|
Unspecified,
|
||||||
|
/// <summary>
|
||||||
|
/// Userd when the <see cref="EntityName"/> represents a role.
|
||||||
|
/// </summary>
|
||||||
|
Role,
|
||||||
|
/// <summary>
|
||||||
|
/// Used when the <see cref="EntityName"/> represents a channel.
|
||||||
|
/// </summary>
|
||||||
|
Channel,
|
||||||
|
/// <summary>
|
||||||
|
/// Used when the <see cref="EntityName"/> represents a user.
|
||||||
|
/// </summary>
|
||||||
|
User
|
||||||
|
}
|
|
@ -1,16 +1,37 @@
|
||||||
using Discord.WebSocket;
|
namespace RegexBot.Common;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace RegexBot.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents commonly-used configuration regarding whitelist/blacklist filtering, including exemptions.
|
/// Represents commonly-used configuration regarding whitelist/blacklist filtering, including exemptions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FilterList {
|
public class FilterList {
|
||||||
public enum FilterMode { None, Whitelist, Blacklist }
|
/// <summary>
|
||||||
|
/// The mode at which the <see cref="FilterList"/>'s filter criteria is operating.
|
||||||
|
/// </summary>
|
||||||
|
public enum FilterMode {
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="FilterList"/> setting which does no filtering on the list.
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="FilterList"/> setting which excludes only entites not in the list, excluding those exempted.
|
||||||
|
/// </summary>
|
||||||
|
Whitelist,
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="FilterList"/> setting which allows all entities except those in the list, but allowing those exempted.
|
||||||
|
/// </summary>
|
||||||
|
Blacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the mode at which the <see cref="FilterList"/>'s filter criteria is operating.
|
||||||
|
/// </summary>
|
||||||
public FilterMode Mode { get; }
|
public FilterMode Mode { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the inner list that this instance is using for its filtering criteria.
|
||||||
|
/// </summary>
|
||||||
public EntityList FilteredList { get; }
|
public EntityList FilteredList { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of entities that may override filtering rules for this instance.
|
||||||
|
/// </summary>
|
||||||
public EntityList FilterExemptions { get; }
|
public EntityList FilterExemptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -70,6 +91,9 @@ public class FilterList {
|
||||||
/// Determines if the parameters of the given message match up against the filtering
|
/// Determines if the parameters of the given message match up against the filtering
|
||||||
/// rules described within this instance.
|
/// rules described within this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="msg">
|
||||||
|
/// The incoming message to be checked.
|
||||||
|
/// </param>
|
||||||
/// <param name="keepId">
|
/// <param name="keepId">
|
||||||
/// See equivalent documentation for <see cref="EntityList.IsListMatch(SocketMessage, bool)"/>.
|
/// See equivalent documentation for <see cref="EntityList.IsListMatch(SocketMessage, bool)"/>.
|
||||||
/// </param>
|
/// </param>
|
10
Common/Messages.cs
Normal file
10
Common/Messages.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace RegexBot.Common;
|
||||||
|
/// <summary>
|
||||||
|
/// Commonly used strings used throughout the bot and modules.
|
||||||
|
/// </summary>
|
||||||
|
public static class Messages {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a string generally appropriate to display in the event of a 403 error.
|
||||||
|
/// </summary>
|
||||||
|
public const string ForbiddenGenericError = "Failed to perform the action due to a permissions issue.";
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
@ -9,9 +7,24 @@ namespace RegexBot.Common;
|
||||||
/// Miscellaneous utility methods useful for the bot and modules.
|
/// Miscellaneous utility methods useful for the bot and modules.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Utilities {
|
public static class Utilities {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a compiled regex that matches a channel tag and pulls its snowflake value.
|
||||||
|
/// </summary>
|
||||||
public static Regex ChannelMention { get; } = new(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);
|
public static Regex ChannelMention { get; } = new(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a compiled 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);
|
public static Regex CustomEmoji { get; } = new(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a compiled 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);
|
public static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a compiled regex that matches a user tag and pulls its snowflake value.
|
||||||
|
/// </summary>
|
||||||
public static Regex UserMention { get; } = new(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
|
public static Regex UserMention { get; } = new(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
|
@ -1,7 +1,9 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace RegexBot.Data;
|
namespace RegexBot.Data;
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a database connection using the settings defined in the bot's global configuration.
|
||||||
|
/// </summary>
|
||||||
public class BotDatabaseContext : DbContext {
|
public class BotDatabaseContext : DbContext {
|
||||||
private static string? _npgsqlConnectionString;
|
private static string? _npgsqlConnectionString;
|
||||||
internal static string PostgresConnectionString {
|
internal static string PostgresConnectionString {
|
||||||
|
@ -17,21 +19,31 @@ public class BotDatabaseContext : DbContext {
|
||||||
set => _npgsqlConnectionString ??= value;
|
set => _npgsqlConnectionString ??= value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<GuildLogLine> GuildLog { get; set; } = null!;
|
/// <summary>
|
||||||
|
/// Retrieves the <seealso cref="CachedUser">user cache</seealso>.
|
||||||
|
/// </summary>
|
||||||
public DbSet<CachedUser> UserCache { get; set; } = null!;
|
public DbSet<CachedUser> UserCache { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the <seealso cref="CachedGuildUser">guild user cache</seealso>.
|
||||||
|
/// </summary>
|
||||||
public DbSet<CachedGuildUser> GuildUserCache { get; set; } = null!;
|
public DbSet<CachedGuildUser> GuildUserCache { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the <seealso cref="CachedGuildMessage">guild message cache</seealso>.
|
||||||
|
/// </summary>
|
||||||
public DbSet<CachedGuildMessage> GuildMessageCache { get; set; } = null!;
|
public DbSet<CachedGuildMessage> GuildMessageCache { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
/// <inheritdoc />
|
||||||
|
protected sealed override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
=> optionsBuilder
|
=> optionsBuilder
|
||||||
.UseNpgsql(PostgresConnectionString)
|
.UseNpgsql(PostgresConnectionString)
|
||||||
.UseSnakeCaseNamingConvention();
|
.UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
modelBuilder.Entity<GuildLogLine>(entity => entity.Property(e => e.Timestamp).HasDefaultValueSql("now()"));
|
|
||||||
modelBuilder.Entity<CachedUser>(entity => entity.Property(e => e.Discriminator).HasMaxLength(4).IsFixedLength());
|
modelBuilder.Entity<CachedUser>(entity => entity.Property(e => e.Discriminator).HasMaxLength(4).IsFixedLength());
|
||||||
modelBuilder.Entity<CachedGuildUser>(entity => {
|
modelBuilder.Entity<CachedGuildUser>(entity => {
|
||||||
entity.Navigation(e => e.User).AutoInclude();
|
|
||||||
entity.HasKey(e => new { e.UserId, e.GuildId });
|
entity.HasKey(e => new { e.UserId, e.GuildId });
|
||||||
entity.Property(e => e.FirstSeenTime).HasDefaultValueSql("now()");
|
entity.Property(e => e.FirstSeenTime).HasDefaultValueSql("now()");
|
||||||
});
|
});
|
62
Data/CachedGuildMessage.cs
Normal file
62
Data/CachedGuildMessage.cs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace RegexBot.Data;
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an item in the guild message cache.
|
||||||
|
/// </summary>
|
||||||
|
[Table("cache_guildmessages")]
|
||||||
|
public class CachedGuildMessage {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the message's snowflake ID.
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.None)]
|
||||||
|
public long MessageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the message author's snowflake ID.
|
||||||
|
/// </summary>
|
||||||
|
public long AuthorId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the associated guild's snowflake ID.
|
||||||
|
/// </summary>
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the corresponding channel's snowflake ID.
|
||||||
|
/// </summary>
|
||||||
|
public long ChannelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp showing when this message was originally created.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Though it's possible to draw this from <see cref="MessageId"/>, it is stored in the database
|
||||||
|
/// as a separate field for any possible necessary use via database queries.
|
||||||
|
/// </remarks>
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp, if any, showing when this message was last edited.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset? EditedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list of file names thata were attached to this message.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> AttachmentNames { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets this message's content.
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If included in the query, references the associated <seealso cref="CachedUser"/> for this entry.
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey(nameof(AuthorId))]
|
||||||
|
[InverseProperty(nameof(CachedUser.GuildMessages))]
|
||||||
|
public CachedUser Author { get; set; } = null!;
|
||||||
|
}
|
36
Data/CachedGuildUser.cs
Normal file
36
Data/CachedGuildUser.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace RegexBot.Data;
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an item in the guild user cache.
|
||||||
|
/// </summary>
|
||||||
|
[Table("cache_usersinguild")]
|
||||||
|
public class CachedGuildUser {
|
||||||
|
/// <inheritdoc cref="CachedUser.UserId"/>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the associated guild's snowflake ID.
|
||||||
|
/// </summary>
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="CachedUser.ULastUpdateTime"/>
|
||||||
|
public DateTimeOffset GULastUpdateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp showing when this cache entry was first added into the database.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset FirstSeenTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's cached nickname in the guild.
|
||||||
|
/// </summary>
|
||||||
|
public string? Nickname { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If included in the query, references the associated <seealso cref="CachedUser"/> for this entry.
|
||||||
|
/// </summary>
|
||||||
|
[ForeignKey(nameof(UserId))]
|
||||||
|
[InverseProperty(nameof(CachedUser.Guilds))]
|
||||||
|
public CachedUser User { get; set; } = null!;
|
||||||
|
}
|
48
Data/CachedUser.cs
Normal file
48
Data/CachedUser.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace RegexBot.Data;
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an item in the user cache.
|
||||||
|
/// </summary>
|
||||||
|
[Table("cache_users")]
|
||||||
|
public class CachedUser {
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's snowflake ID.
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.None)]
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp showing when this cache entry was last updated.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset ULastUpdateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's username value, without the discriminator.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's discriminator value.
|
||||||
|
/// </summary>
|
||||||
|
public string Discriminator { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the avatar URL, if any, for the associated user.
|
||||||
|
/// </summary>
|
||||||
|
public string? AvatarUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If included in the query, gets the list of associated <seealso cref="CachedGuildUser"/> entries for this entry.
|
||||||
|
/// </summary>
|
||||||
|
[InverseProperty(nameof(CachedGuildUser.User))]
|
||||||
|
public ICollection<CachedGuildUser> Guilds { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If included in the query, gets the list of associated <seealso cref="CachedGuildMessage"/> entries for this entry.
|
||||||
|
/// </summary>
|
||||||
|
[InverseProperty(nameof(CachedGuildMessage.Author))]
|
||||||
|
public ICollection<CachedGuildMessage> GuildMessages { get; set; } = null!;
|
||||||
|
}
|
3
Data/Migrations/.editorconfig
Normal file
3
Data/Migrations/.editorconfig
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[*.cs]
|
||||||
|
dotnet_analyzer_diagnostic.category-CodeQuality.severity = none
|
||||||
|
dotnet_diagnostic.CS1591.severity = none
|
|
@ -1,5 +1,4 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using RegexBot.Data;
|
using RegexBot.Data;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
|
@ -30,14 +29,9 @@ class InstanceConfig {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets up instance configuration object from file and command line parameters.
|
/// Sets up instance configuration object from file and command line parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal InstanceConfig(string[] cmdline) {
|
internal InstanceConfig() {
|
||||||
var opts = Options.ParseOptions(cmdline);
|
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
||||||
|
+ "." + Path.DirectorySeparatorChar + "config.json";
|
||||||
var path = opts.ConfigFile;
|
|
||||||
if (path == null) { // default: config.json in working directory
|
|
||||||
path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
|
||||||
+ "." + Path.DirectorySeparatorChar + "config.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
JObject conf;
|
JObject conf;
|
||||||
try {
|
try {
|
|
@ -1,7 +1,8 @@
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents errors that occur when a module attempts to create a new guild state object.
|
/// Represents an error occurring when a module attempts to create a new guild state object
|
||||||
|
/// (that is, read or refresh its configuration).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ModuleLoadException : Exception {
|
public class ModuleLoadException : Exception {
|
||||||
/// <summary>
|
/// <summary>
|
|
@ -3,7 +3,6 @@ using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
static class ModuleLoader {
|
static class ModuleLoader {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given the instance configuration, loads all appropriate types from file specified in it.
|
/// Given the instance configuration, loads all appropriate types from file specified in it.
|
||||||
|
@ -36,21 +35,21 @@ static class ModuleLoader {
|
||||||
return modules.AsReadOnly();
|
return modules.AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
static IEnumerable<RegexbotModule> LoadModulesFromAssembly(Assembly asm, RegexbotClient k) {
|
static IEnumerable<RegexbotModule> LoadModulesFromAssembly(Assembly asm, RegexbotClient rb) {
|
||||||
var eligibleTypes = from type in asm.GetTypes()
|
var eligibleTypes = from type in asm.GetTypes()
|
||||||
where !type.IsAssignableFrom(typeof(RegexbotModule))
|
where !type.IsAssignableFrom(typeof(RegexbotModule))
|
||||||
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
|
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
|
||||||
select type;
|
select type;
|
||||||
k._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}");
|
rb._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}");
|
||||||
|
|
||||||
var newreport = new StringBuilder("---> Found module(s):");
|
var newreport = new StringBuilder("---> Found module(s):");
|
||||||
var newmods = new List<RegexbotModule>();
|
var newmods = new List<RegexbotModule>();
|
||||||
foreach (var t in eligibleTypes) {
|
foreach (var t in eligibleTypes) {
|
||||||
var mod = Activator.CreateInstance(t, k)!;
|
var mod = Activator.CreateInstance(t, rb)!;
|
||||||
newreport.Append($" {t.Name}");
|
newreport.Append($" {t.Name}");
|
||||||
newmods.Add((RegexbotModule)mod);
|
newmods.Add((RegexbotModule)mod);
|
||||||
}
|
}
|
||||||
k._svcLogging.DoLog(false, nameof(ModuleLoader), newreport.ToString());
|
rb._svcLogging.DoLog(false, nameof(ModuleLoader), newreport.ToString());
|
||||||
return newmods;
|
return newmods;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using RegexBot.Data;
|
using RegexBot.Common;
|
||||||
|
using RegexBot.Data;
|
||||||
|
|
||||||
namespace RegexBot.Modules.ModCommands.Commands;
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
|
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
|
||||||
|
@ -132,7 +133,7 @@ abstract class BanKick : CommandConfig {
|
||||||
await ContinueInvoke(g, msg, reason, targetId, targetQuery, targetUser);
|
await ContinueInvoke(g, msg, reason, targetId, targetQuery, targetUser);
|
||||||
} catch (Discord.Net.HttpException ex) {
|
} catch (Discord.Net.HttpException ex) {
|
||||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
||||||
await msg.Channel.SendMessageAsync(":x: " + Strings.ForbiddenGenericError);
|
await msg.Channel.SendMessageAsync(":x: " + Messages.ForbiddenGenericError);
|
||||||
} else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) {
|
} else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) {
|
||||||
await msg.Channel.SendMessageAsync(":x: Encountered a 404 error when processing the request.");
|
await msg.Channel.SendMessageAsync(":x: Encountered a 404 error when processing the request.");
|
||||||
}
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
namespace RegexBot.Modules.ModCommands.Commands;
|
using RegexBot.Common;
|
||||||
|
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
class Unban : CommandConfig {
|
class Unban : CommandConfig {
|
||||||
private readonly string _usage;
|
private readonly string _usage;
|
||||||
|
|
||||||
|
@ -43,7 +45,7 @@ class Unban : CommandConfig {
|
||||||
} catch (Discord.Net.HttpException ex) {
|
} catch (Discord.Net.HttpException ex) {
|
||||||
const string FailPrefix = ":x: **Could not unban:** ";
|
const string FailPrefix = ":x: **Could not unban:** ";
|
||||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||||
await msg.Channel.SendMessageAsync(FailPrefix + Strings.ForbiddenGenericError);
|
await msg.Channel.SendMessageAsync(FailPrefix + Messages.ForbiddenGenericError);
|
||||||
else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
|
else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
|
||||||
await msg.Channel.SendMessageAsync(FailPrefix + "The specified user does not exist or is not in the ban list.");
|
await msg.Channel.SendMessageAsync(FailPrefix + "The specified user does not exist or is not in the ban list.");
|
||||||
else throw;
|
else throw;
|
|
@ -67,7 +67,7 @@ class ResponseExecutor {
|
||||||
var result = await runLine(param);
|
var result = await runLine(param);
|
||||||
_reports.Add((cmd, result));
|
_reports.Add((cmd, result));
|
||||||
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
||||||
_reports.Add((cmd, FromError(Strings.ForbiddenGenericError)));
|
_reports.Add((cmd, FromError(Messages.ForbiddenGenericError)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ class ResponseExecutor {
|
||||||
result = await _bot.KickAsync(_guild, $"Rule '{_rule.Label}'", _user.Id,
|
result = await _bot.KickAsync(_guild, $"Rule '{_rule.Label}'", _user.Id,
|
||||||
parameter, _rule.NotifyUserOfRemoval);
|
parameter, _rule.NotifyUserOfRemoval);
|
||||||
}
|
}
|
||||||
if (result.ErrorForbidden) return FromError(Strings.ForbiddenGenericError);
|
if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
|
||||||
if (result.ErrorNotFound) return FromError("The target user is no longer in the server.");
|
if (result.ErrorNotFound) return FromError("The target user is no longer in the server.");
|
||||||
if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
|
if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
|
||||||
return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM.");
|
return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM.");
|
|
@ -1,5 +1,6 @@
|
||||||
using Discord;
|
global using Discord.WebSocket;
|
||||||
using Discord.WebSocket;
|
global using Newtonsoft.Json.Linq;
|
||||||
|
using Discord;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
class Program {
|
class Program {
|
||||||
|
@ -10,13 +11,13 @@ class Program {
|
||||||
|
|
||||||
static RegexbotClient _main = null!;
|
static RegexbotClient _main = null!;
|
||||||
|
|
||||||
static async Task Main(string[] args) {
|
static async Task Main() {
|
||||||
StartTime = DateTimeOffset.UtcNow;
|
StartTime = DateTimeOffset.UtcNow;
|
||||||
Console.WriteLine("Bot start time: " + StartTime.ToString("u"));
|
Console.WriteLine("Bot start time: " + StartTime.ToString("u"));
|
||||||
|
|
||||||
InstanceConfig cfg;
|
InstanceConfig cfg;
|
||||||
try {
|
try {
|
||||||
cfg = new InstanceConfig(args); // Program may exit within here.
|
cfg = new InstanceConfig(); // Program may exit within here.
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Console.WriteLine(ex.Message);
|
Console.WriteLine(ex.Message);
|
||||||
Environment.ExitCode = 1;
|
Environment.ExitCode = 1;
|
||||||
|
@ -33,7 +34,6 @@ class Program {
|
||||||
AlwaysDownloadUsers = true
|
AlwaysDownloadUsers = true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kerobot class initialization - will set up services and modules
|
|
||||||
_main = new RegexbotClient(cfg, client);
|
_main = new RegexbotClient(cfg, client);
|
||||||
|
|
||||||
// Set up application close handler
|
// Set up application close handler
|
32
Readme.md
32
Readme.md
|
@ -1,26 +1,14 @@
|
||||||
# RegexBot
|
# RegexBot
|
||||||
**This branch is still a major work in progress, and is highly incomplete. See the legacy branch for the current working version.**
|
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J65TW2E)
|
||||||
|
|
||||||
RegexBot is a Discord moderation bot framework of sorts, inspired by the terrible state of Discord moderation tools a few years ago
|
RegexBot is a Discord moderation bot framework of sorts, inspired by the terrible state of Discord moderation tools a few years ago
|
||||||
combined with my tendency to overengineer things until they into pseudo-libraries of their own right.
|
combined with my tendency to overengineer things until they into pseudo-libraries of their own right.
|
||||||
|
|
||||||
### Features:
|
This bot includes a number of features which assist in handling the tedious details in a busy server with the goal of minimizing
|
||||||
* Provides a sort of in-between interface to Discord.Net that allows modules to be written for it, its benefits being:
|
the occurrence of hidden details, arbitrary restrictions, or annoyingly unmodifiable behavior. Its configuration allows for a very high
|
||||||
* Putting together disparate bot features under a common interface.
|
level of flexibility, ensuring that the bot behaves in accordance to the exact needs of your server without compromise.
|
||||||
* Reducing duplicate code potentially leading to an inconsistent user experience.
|
|
||||||
* Versatile JSON-based configuration.
|
|
||||||
* High detail logging and record-keeping prevents gaps in moderation that might occur with large public bots.
|
|
||||||
|
|
||||||
This repository also contains...
|
### Features
|
||||||
|
|
||||||
# RegexBot-Modules
|
|
||||||
An optional set of features to add to RegexBot, some of them inspired by Reddit's Automoderator.
|
|
||||||
|
|
||||||
This module provides a number of features to assist in watching over the tedious details in a busy server with no hidden details,
|
|
||||||
arbitrary restrictions, or unmodifiable behavior. Its configuration allows for a very high level of flexibility, ensuring that the bot
|
|
||||||
behaves in accordance to the exact needs of your server.
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
* Create rules based on regular expression patterns
|
* Create rules based on regular expression patterns
|
||||||
* Follow up with custom responses ranging from sending a DM to disciplinary action
|
* Follow up with custom responses ranging from sending a DM to disciplinary action
|
||||||
* Create pattern-based triggers to provide information and fun to your users
|
* Create pattern-based triggers to provide information and fun to your users
|
||||||
|
@ -29,6 +17,14 @@ behaves in accordance to the exact needs of your server.
|
||||||
* Make things interesting by setting triggers that only activate at random
|
* Make things interesting by setting triggers that only activate at random
|
||||||
* Individual rules and triggers can be whitelisted or blacklisted per-user, per-channel, or per-role
|
* Individual rules and triggers can be whitelisted or blacklisted per-user, per-channel, or per-role
|
||||||
* Exemptions to these filters can be applied for additional flexibility
|
* Exemptions to these filters can be applied for additional flexibility
|
||||||
|
* High detail logging and record-keeping prevents gaps in moderation that might occur with large public bots.
|
||||||
|
|
||||||
## Documentation
|
### Modules
|
||||||
|
As mentioned above, this bot also serves as a framework of sorts, allowing others to write their own modules and expand
|
||||||
|
the bot's feature set ever further. Its benefits are:
|
||||||
|
* Putting together disparate bot features under a common, consistent interface.
|
||||||
|
* Reducing duplicate code potentially leading to an inconsistent user experience.
|
||||||
|
* Versatile JSON-based configuration.
|
||||||
|
|
||||||
|
## User documentation
|
||||||
Coming soon?
|
Coming soon?
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
global using Discord.WebSocket;
|
|
||||||
global using Newtonsoft.Json.Linq;
|
|
|
@ -1,19 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<RootNamespace>RegexBot.Modules</RootNamespace>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<Authors>NoiTheCat</Authors>
|
|
||||||
<Description>A set of standard modules for use with RegexBot.</Description>
|
|
||||||
<BaseOutputPath>$(SolutionDir)\output</BaseOutputPath>
|
|
||||||
<UseCommonOutputDirectory>true</UseCommonOutputDirectory>
|
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\RegexBot\RegexBot.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
|
@ -6,10 +6,9 @@
|
||||||
<Authors>NoiTheCat</Authors>
|
<Authors>NoiTheCat</Authors>
|
||||||
<Description>Advanced and flexible Discord moderation bot.</Description>
|
<Description>Advanced and flexible Discord moderation bot.</Description>
|
||||||
<Version>0.0.1</Version>
|
<Version>0.0.1</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
<BaseOutputPath>$(SolutionDir)\output</BaseOutputPath>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -21,18 +20,17 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
<PackageReference Include="Discord.Net" Version="3.7.2" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.7.0" />
|
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.4" />
|
<PackageReference Include="Npgsql" Version="6.0.5" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
28
RegexBot.sln
28
RegexBot.sln
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 16
|
|
||||||
VisualStudioVersion = 16.0.30114.105
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegexBot", "RegexBot\RegexBot.csproj", "{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegexBot-Modules", "RegexBot-Modules\RegexBot-Modules.csproj", "{347A1912-4F7D-419B-A159-6671791568CC}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{F7CDACE1-C74E-451E-A9C2-ED717BF72C1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{347A1912-4F7D-419B-A159-6671791568CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{347A1912-4F7D-419B-A159-6671791568CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{347A1912-4F7D-419B-A159-6671791568CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{347A1912-4F7D-419B-A159-6671791568CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
|
@ -1,12 +0,0 @@
|
||||||
namespace RegexBot.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The type of entity specified in an <see cref="EntityName"/>.
|
|
||||||
/// </summary>
|
|
||||||
public enum EntityType {
|
|
||||||
/// <summary>Default value. Is never referenced in regular usage.</summary>
|
|
||||||
Unspecified,
|
|
||||||
Role,
|
|
||||||
Channel,
|
|
||||||
User
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
/// <summary>
|
|
||||||
/// Commonly used strings used throughout the program and modules.
|
|
||||||
/// </summary>
|
|
||||||
public static class Strings {
|
|
||||||
public const string ForbiddenGenericError = "Failed to perform the action due to a permissions issue.";
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace RegexBot.Data;
|
|
||||||
[Table("cache_messages")]
|
|
||||||
public class CachedGuildMessage {
|
|
||||||
[Key]
|
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public long MessageId { get; set; }
|
|
||||||
|
|
||||||
public long AuthorId { get; set; }
|
|
||||||
|
|
||||||
public long GuildId { get; set; }
|
|
||||||
|
|
||||||
public long ChannelId { get; set; }
|
|
||||||
|
|
||||||
public DateTimeOffset CreatedAt { get; set; }
|
|
||||||
|
|
||||||
public DateTimeOffset? EditedAt { get; set; }
|
|
||||||
|
|
||||||
public List<string> AttachmentNames { get; set; } = null!;
|
|
||||||
|
|
||||||
public string Content { get; set; } = null!;
|
|
||||||
|
|
||||||
[ForeignKey(nameof(AuthorId))]
|
|
||||||
[InverseProperty(nameof(CachedUser.GuildMessages))]
|
|
||||||
public CachedUser Author { get; set; } = null!;
|
|
||||||
|
|
||||||
internal new CachedGuildMessage MemberwiseClone() => (CachedGuildMessage)base.MemberwiseClone();
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace RegexBot.Data;
|
|
||||||
|
|
||||||
[Table("cache_userguild")]
|
|
||||||
public class CachedGuildUser {
|
|
||||||
public long UserId { get; set; }
|
|
||||||
public long GuildId { get; set; }
|
|
||||||
public DateTimeOffset GULastUpdateTime { get; set; }
|
|
||||||
public DateTimeOffset FirstSeenTime { get; set; }
|
|
||||||
public string? Nickname { get; set; }
|
|
||||||
|
|
||||||
[ForeignKey(nameof(UserId))]
|
|
||||||
[InverseProperty(nameof(CachedUser.Guilds))]
|
|
||||||
public CachedUser User { get; set; } = null!;
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace RegexBot.Data;
|
|
||||||
|
|
||||||
[Table("cache_user")]
|
|
||||||
public class CachedUser {
|
|
||||||
[Key]
|
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public long UserId { get; set; }
|
|
||||||
public DateTimeOffset ULastUpdateTime { get; set; }
|
|
||||||
public string Username { get; set; } = null!;
|
|
||||||
public string Discriminator { get; set; } = null!;
|
|
||||||
public string? AvatarUrl { get; set; }
|
|
||||||
|
|
||||||
[InverseProperty(nameof(CachedGuildUser.User))]
|
|
||||||
public ICollection<CachedGuildUser> Guilds { get; set; } = null!;
|
|
||||||
|
|
||||||
[InverseProperty(nameof(CachedGuildMessage.Author))]
|
|
||||||
public ICollection<CachedGuildMessage> GuildMessages { get; set; } = null!;
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace RegexBot.Data;
|
|
||||||
|
|
||||||
[Table("guild_log")]
|
|
||||||
[Index(nameof(GuildId))]
|
|
||||||
public class GuildLogLine {
|
|
||||||
public int Id { get; set; }
|
|
||||||
public long GuildId { get; set; }
|
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
|
||||||
public string Source { get; set; } = null!;
|
|
||||||
public string Message { get; set; } = null!;
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
using CommandLine;
|
|
||||||
using CommandLine.Text;
|
|
||||||
|
|
||||||
namespace RegexBot;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command line options
|
|
||||||
/// </summary>
|
|
||||||
class Options {
|
|
||||||
[Option('c', "config", Default = null,
|
|
||||||
HelpText = "Custom path to instance configuration. Defaults to config.json in bot directory.")]
|
|
||||||
public string ConfigFile { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Command line arguments parsed here. Depending on inputs, the program can exit here.
|
|
||||||
/// </summary>
|
|
||||||
public static Options ParseOptions(string[] args) {
|
|
||||||
// Parser will not write out to console by itself
|
|
||||||
var parser = new Parser(config => config.HelpWriter = null);
|
|
||||||
Options? opts = null;
|
|
||||||
|
|
||||||
var result = parser.ParseArguments<Options>(args);
|
|
||||||
result.WithParsed(p => opts = p);
|
|
||||||
result.WithNotParsed(p => {
|
|
||||||
// Taking some extra steps to modify the header to make it resemble our welcome message.
|
|
||||||
var ht = HelpText.AutoBuild(result);
|
|
||||||
ht.Heading += " - https://github.com/NoiTheCat/RegexBot";
|
|
||||||
Console.WriteLine(ht.ToString());
|
|
||||||
Environment.Exit(1);
|
|
||||||
});
|
|
||||||
return opts!;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace RegexBot;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies to the Kerobot module loader that the target class should be treated as a module instance.
|
|
||||||
/// When the program scans an assembly which has been specified in its instance configuration to be loaded,
|
|
||||||
/// the program searches for classes implementing <see cref="RegexbotModule"/> that also contain this attribute.
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
|
||||||
public class RegexbotModuleAttribute : Attribute { }
|
|
|
@ -1,8 +1,6 @@
|
||||||
using Discord.WebSocket;
|
using System.Reflection;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RegexBot client instance.
|
/// The RegexBot client instance.
|
||||||
/// </summary>
|
/// </summary>
|
|
@ -1,6 +1,4 @@
|
||||||
using Discord.WebSocket;
|
using RegexBot.Common;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using RegexBot.Common;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
8
RegexbotModuleAttribute.cs
Normal file
8
RegexbotModuleAttribute.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace RegexBot;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a hint to the module loader that the class it is applied to should be treated as a module instance.
|
||||||
|
/// When the program scans an assembly, it is scanned for classes which implement <see cref="RegexbotModule"/> and have this attribute.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public class RegexbotModuleAttribute : Attribute { }
|
|
@ -1,4 +1,5 @@
|
||||||
using Discord.Net;
|
using Discord.Net;
|
||||||
|
using RegexBot.Common;
|
||||||
|
|
||||||
// Instances of this class are created by CommonFunctionService and are meant to be sent to modules,
|
// Instances of this class are created by CommonFunctionService and are meant to be sent to modules,
|
||||||
// therefore we put this in the root RegexBot namespace despite being specific to this service.
|
// therefore we put this in the root RegexBot namespace despite being specific to this service.
|
||||||
|
@ -104,7 +105,7 @@ public class BanKickResult {
|
||||||
if (!MessageSendSuccess) msg += "\n(User was unable to receive notification message.)";
|
if (!MessageSendSuccess) msg += "\n(User was unable to receive notification message.)";
|
||||||
} else {
|
} else {
|
||||||
if (ErrorNotFound) msg += ": The specified user could not be found.";
|
if (ErrorNotFound) msg += ": The specified user could not be found.";
|
||||||
else if (ErrorForbidden) msg += ": " + Strings.ForbiddenGenericError;
|
else if (ErrorForbidden) msg += ": " + Messages.ForbiddenGenericError;
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
|
@ -1,15 +1,10 @@
|
||||||
using Discord.Net;
|
using Discord.Net;
|
||||||
using Discord.WebSocket;
|
|
||||||
|
|
||||||
namespace RegexBot.Services.CommonFunctions;
|
namespace RegexBot.Services.CommonFunctions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements certain common actions that modules may want to perform. Using this service to perform those
|
/// Implements certain common actions that modules may want to perform. Using this service to perform those
|
||||||
/// functions may help enforce a sense of consistency across modules when performing common actions, and may
|
/// 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.
|
/// inform services which provide any additional features the ability to respond to those actions ahead of time.
|
||||||
///
|
|
||||||
/// This is currently an experimental section. If it turns out to not be necessary, this service will be removed and
|
|
||||||
/// modules may resume executing common actions on their own.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class CommonFunctionsService : Service {
|
internal class CommonFunctionsService : Service {
|
||||||
public CommonFunctionsService(RegexbotClient bot) : base(bot) { }
|
public CommonFunctionsService(RegexbotClient bot) : base(bot) { }
|
|
@ -1,5 +1,4 @@
|
||||||
using Discord.WebSocket;
|
using RegexBot.Services.CommonFunctions;
|
||||||
using RegexBot.Services.CommonFunctions;
|
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
partial class RegexbotClient {
|
partial class RegexbotClient {
|
||||||
|
@ -29,7 +28,12 @@ partial class RegexbotClient {
|
||||||
/// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, bool)"/>, but making use of an
|
/// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, bool)"/>, but making use of an
|
||||||
/// EntityCache lookup to determine the target.
|
/// EntityCache lookup to determine the target.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="targetSearch">The EntityCache search string.</param>
|
/// <param name="guild">The guild in which to attempt the ban.</param>
|
||||||
|
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
|
||||||
|
/// <param name="targetSearch">The user which to perform the action to (as a query to the entity cache).</param>
|
||||||
|
/// <param name="purgeDays">Number of days of prior post history to delete on ban. Must be between 0-7.</param>
|
||||||
|
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
|
||||||
|
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action.</param>
|
||||||
public async Task<BanKickResult> BanAsync(SocketGuild guild,
|
public async Task<BanKickResult> BanAsync(SocketGuild guild,
|
||||||
string source,
|
string source,
|
||||||
string targetSearch,
|
string targetSearch,
|
||||||
|
@ -54,10 +58,7 @@ partial class RegexbotClient {
|
||||||
/// Reason for the action. Sent to the guild's audit log and, if
|
/// Reason for the action. Sent to the guild's audit log and, if
|
||||||
/// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target.
|
/// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="sendDMToTarget">
|
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action.</param>
|
||||||
/// Specify whether to send a direct message to the target user informing them of the action
|
|
||||||
/// (that is, a ban/kick message).
|
|
||||||
/// </param>
|
|
||||||
public Task<BanKickResult> KickAsync(SocketGuild guild,
|
public Task<BanKickResult> KickAsync(SocketGuild guild,
|
||||||
string source,
|
string source,
|
||||||
ulong targetUser,
|
ulong targetUser,
|
||||||
|
@ -69,7 +70,14 @@ partial class RegexbotClient {
|
||||||
/// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, bool)"/>, but making use of an
|
/// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, bool)"/>, but making use of an
|
||||||
/// EntityCache lookup to determine the target.
|
/// EntityCache lookup to determine the target.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="targetSearch">The EntityCache search string.</param>
|
/// <param name="guild">The guild in which to attempt the kick.</param>
|
||||||
|
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
|
||||||
|
/// <param name="targetSearch">The user which to perform the action towards (processed as a query to the entity cache).</param>
|
||||||
|
/// <param name="reason">
|
||||||
|
/// Reason for the action. Sent to the guild's audit log and, if
|
||||||
|
/// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action.</param>
|
||||||
public async Task<BanKickResult> KickAsync(SocketGuild guild,
|
public async Task<BanKickResult> KickAsync(SocketGuild guild,
|
||||||
string source,
|
string source,
|
||||||
string targetSearch,
|
string targetSearch,
|
|
@ -1,9 +1,10 @@
|
||||||
// Despite specific to CommonFunctionsService, this enum is meant to be visible by modules too,
|
namespace RegexBot;
|
||||||
// thus it is placed within the root namespace.
|
|
||||||
namespace RegexBot;
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies possible outcomes for the removal of a user from a guild.
|
/// Specifies possible outcomes for the removal of a user from a guild.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
// Despite specific to CommonFunctionsService, this enum is meant to be visible by modules too,
|
||||||
|
// thus it is placed within the root namespace.
|
||||||
|
// TODO Tends to be unused except internally. Look into removing.
|
||||||
public enum RemovalType {
|
public enum RemovalType {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default value. Not used in any actual circumstances.
|
/// Default value. Not used in any actual circumstances.
|
|
@ -2,9 +2,7 @@
|
||||||
|
|
||||||
namespace RegexBot.Services.EntityCache;
|
namespace RegexBot.Services.EntityCache;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides and maintains a database-backed cache of entities. Portions of information collected by this
|
/// Provides and maintains a database-backed cache of entities.
|
||||||
/// service may be used by modules, while other portions are useful only for external applications which may
|
|
||||||
/// require this information, such as an external web interface.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class EntityCacheService : Service {
|
class EntityCacheService : Service {
|
||||||
private readonly UserCachingSubservice _uc;
|
private readonly UserCachingSubservice _uc;
|
|
@ -1,5 +1,4 @@
|
||||||
using Discord.WebSocket;
|
using RegexBot.Data;
|
||||||
using RegexBot.Data;
|
|
||||||
using RegexBot.Services.EntityCache;
|
using RegexBot.Services.EntityCache;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
|
@ -1,5 +1,4 @@
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
|
||||||
using RegexBot.Data;
|
using RegexBot.Data;
|
||||||
using static RegexBot.RegexbotClient;
|
using static RegexBot.RegexbotClient;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Discord.WebSocket;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using RegexBot.Common;
|
using RegexBot.Common;
|
||||||
using RegexBot.Data;
|
using RegexBot.Data;
|
||||||
|
|
||||||
|
@ -101,8 +101,7 @@ class UserCachingSubservice {
|
||||||
internal CachedGuildUser? DoGuildUserQuery(ulong guildId, string search) {
|
internal CachedGuildUser? DoGuildUserQuery(ulong guildId, string search) {
|
||||||
static CachedGuildUser? innerQuery(ulong guildId, ulong? sID, (string name, string? disc)? nameSearch) {
|
static CachedGuildUser? innerQuery(ulong guildId, ulong? sID, (string name, string? disc)? nameSearch) {
|
||||||
var db = new BotDatabaseContext();
|
var db = new BotDatabaseContext();
|
||||||
|
var query = db.GuildUserCache.Include(gu => gu.User).Where(c => c.GuildId == (long)guildId);
|
||||||
var query = db.GuildUserCache.Where(c => c.GuildId == (long)guildId);
|
|
||||||
if (sID.HasValue)
|
if (sID.HasValue)
|
||||||
query = query.Where(c => c.UserId == (long)sID.Value);
|
query = query.Where(c => c.UserId == (long)sID.Value);
|
||||||
if (nameSearch != null) {
|
if (nameSearch != null) {
|
|
@ -1,7 +1,6 @@
|
||||||
using RegexBot.Services.Logging;
|
using RegexBot.Services.Logging;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
partial class RegexbotClient {
|
partial class RegexbotClient {
|
||||||
// Access set to internal for ModuleBase and Service base class
|
// Access set to internal for ModuleBase and Service base class
|
||||||
internal readonly LoggingService _svcLogging;
|
internal readonly LoggingService _svcLogging;
|
|
@ -1,7 +1,6 @@
|
||||||
using RegexBot.Services.ModuleState;
|
using RegexBot.Services.ModuleState;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
partial class RegexbotClient {
|
partial class RegexbotClient {
|
||||||
// Access set to internal for ModuleBase
|
// Access set to internal for ModuleBase
|
||||||
internal readonly ModuleStateService _svcGuildState;
|
internal readonly ModuleStateService _svcGuildState;
|
|
@ -1,11 +1,8 @@
|
||||||
using Discord.WebSocket;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using RegexBot.Common;
|
using RegexBot.Common;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace RegexBot.Services.ModuleState;
|
namespace RegexBot.Services.ModuleState;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements per-module storage and retrieval of guild-specific state data, most typically but not limited to configuration data.
|
/// Implements per-module storage and retrieval of guild-specific state data, most typically but not limited to configuration data.
|
||||||
/// To that end, this service handles loading and validation of per-guild configuration files.
|
/// To that end, this service handles loading and validation of per-guild configuration files.
|
||||||
|
@ -15,8 +12,6 @@ class ModuleStateService : Service {
|
||||||
private readonly Dictionary<ulong, EntityList> _moderators;
|
private readonly Dictionary<ulong, EntityList> _moderators;
|
||||||
private readonly Dictionary<ulong, Dictionary<Type, object?>> _stateData;
|
private readonly Dictionary<ulong, Dictionary<Type, object?>> _stateData;
|
||||||
|
|
||||||
const string GuildLogSource = "Configuration loader";
|
|
||||||
|
|
||||||
public ModuleStateService(RegexbotClient bot) : base(bot) {
|
public ModuleStateService(RegexbotClient bot) : base(bot) {
|
||||||
_moderators = new();
|
_moderators = new();
|
||||||
_stateData = new();
|
_stateData = new();
|
|
@ -1,5 +1,4 @@
|
||||||
namespace RegexBot.Services;
|
namespace RegexBot.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for services.
|
/// Base class for services.
|
||||||
/// </summary>
|
/// </summary>
|
Loading…
Reference in a new issue