Several fixes after a round of testing

Fixed the following compilation errors:
-Moderators collection not initialized
-Outdated method signatures for ban and kick in ModuleBase
-Update author name in manifests
-Fixed incorrect method signature in AutoScriptResponder

Minor improvements:
-Updated external dependencies
-Remove unused variables in ConfDefinition of RegexModerator
-Improve parallel execution of matches?
-Send exception message on logging failure to reporting channel
-Slightly change ModuleLoader logging output
-Add Discord.Net unhandled exception output to logging
-Updated link to Github
-Changed GuildState exception handling message for conciseness

Fixes:
-SQL index creation in LoggingService
-SQL view creation in UserCacheService
-Add casts from ulong to long in SQL inserts
-External modules no longer loaded twice
-Non-Info messages from Discord.Net will now be reported
-User data had not been recorded at proper times
-Some modules had been returning null instead of Task with null
-Guild state exception handling should not have handled config errors
This commit is contained in:
Noikoio 2019-06-21 15:05:58 -07:00
parent 2319c91fc7
commit ffa5b5754b
14 changed files with 63 additions and 69 deletions

View file

@ -46,7 +46,7 @@ namespace Kerobot
// Everything's ready to go. Print the welcome message here.
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
InstanceLogAsync(false, "Kerobot",
$"This is Kerobot v{ver.ToString(3)}. https://github.com/Noikoio/Kerobot").Wait();
$"This is Kerobot v{ver.ToString(3)}. https://github.com/Noiiko/Kerobot").Wait();
// We return to Program.cs at this point.
}

View file

@ -1,14 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<StartupObject>Kerobot.Program</StartupObject>
<AssemblyVersion>0.0.1</AssemblyVersion>
<Authors>Noikoio</Authors>
<Authors>Noiiko</Authors>
<Company />
<Description>Advanced and flexible Discord moderation bot.</Description>
<FileVersion>0.0.1</FileVersion>
<Version>0.0.1</Version>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
@ -27,10 +25,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.4.3" />
<PackageReference Include="Discord.Net" Version="2.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="Npgsql" Version="4.0.5" />
<PackageReference Include="CommandLineParser" Version="2.5.0" />
<PackageReference Include="Discord.Net" Version="2.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Npgsql" Version="4.0.7" />
</ItemGroup>
</Project>

View file

@ -88,23 +88,20 @@ namespace Kerobot
/// <param name="targetUser">The user which to perform the action to.</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="dmMsg">
/// Message to DM the target user. Set null to disable. Instances of "%s" are replaced with the guild name
/// and instances of "%r" are replaced with the reason.
/// </param>
protected Task<BanKickResult> BanAsync(SocketGuild guild, string source, ulong targetUser, int purgeDays, string reason, string dmMsg)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, dmMsg);
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action being taken.</param>
protected Task<BanKickResult> BanAsync(SocketGuild guild, string source, ulong targetUser, int purgeDays, string reason, bool sendDMToTarget)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, sendDMToTarget);
/// <summary>
/// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, string)"/>, 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.
/// </summary>
/// <param name="targetSearch">The EntityCache search string.</param>
protected async Task<BanKickResult> BanAsync(SocketGuild guild, string source, string targetSearch, int purgeDays, string reason, string dmMsg)
protected async Task<BanKickResult> BanAsync(SocketGuild guild, string source, string targetSearch, int purgeDays, string reason, bool sendDMToTarget)
{
var result = await Kerobot.EcQueryUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true);
return await BanAsync(guild, source, result.UserID, purgeDays, reason, dmMsg);
return await BanAsync(guild, source, result.UserID, purgeDays, reason, sendDMToTarget);
}
/// <summary>
@ -117,23 +114,20 @@ namespace Kerobot
/// <param name="source">The user, if any, which requested the action to be taken.</param>
/// <param name="targetUser">The user which to perform the action to.</param>
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
/// <param name="dmMsg">
/// Message to DM the target user. Set null to disable. Instances of "%s" are replaced with the guild name
/// and instances of "%r" are replaced with the reason.
/// </param>
protected Task<BanKickResult> KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, string dmMsg)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, dmMsg);
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action being taken.</param>
protected Task<BanKickResult> KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, bool sendDMToTarget)
=> Kerobot.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, sendDMToTarget);
/// <summary>
/// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, string)"/>, 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.
/// </summary>
/// <param name="targetSearch">The EntityCache search string.</param>
protected async Task<BanKickResult> KickAsync(SocketGuild guild, string source, string targetSearch, string reason, string dmMsg)
protected async Task<BanKickResult> KickAsync(SocketGuild guild, string source, string targetSearch, string reason, bool sendDMToTarget)
{
var result = await Kerobot.EcQueryUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true);
return await KickAsync(guild, source, result.UserID, reason, dmMsg);
return await KickAsync(guild, source, result.UserID, reason, sendDMToTarget);
}
/// <summary>

View file

@ -45,7 +45,7 @@ namespace Kerobot
Console.WriteLine(ex.ToString());
Environment.Exit(2);
}
modules.AddRange(LoadModulesFromAssembly(a, k));
modules.AddRange(amods);
}
return modules.AsReadOnly();
}
@ -56,15 +56,14 @@ namespace Kerobot
where !type.IsAssignableFrom(typeof(ModuleBase))
where type.GetCustomAttribute<KerobotModuleAttribute>() != null
select type;
k.InstanceLogAsync(false, LogName,
$"{asm.GetName().Name} has {eligibleTypes.Count()} usable types");
k.InstanceLogAsync(false, LogName, $"Scanning {asm.GetName().Name}");
var newmods = new List<ModuleBase>();
foreach (var t in eligibleTypes)
{
var mod = Activator.CreateInstance(t, k);
k.InstanceLogAsync(false, LogName,
$"---> Instance created: {t.FullName}");
$"---> Loading module {t.FullName}");
newmods.Add((ModuleBase)mod);
}
return newmods;

View file

@ -66,7 +66,7 @@ namespace Kerobot.Services.EntityCache
c.CommandText = $"create or replace view {UserView} as " +
$"select {GlobalUserTable}.user_id, {GuildUserTable}.guild_id, {GuildUserTable}.first_seen, " +
$"{GuildUserTable}.cache_update_time, " +
$"{GlobalUserTable}.username, {GlobalUserTable}.discriminator, {GlobalUserTable}.nickname, " +
$"{GlobalUserTable}.username, {GlobalUserTable}.discriminator, {GuildUserTable}.nickname, " +
$"{GlobalUserTable}.avatar_url " +
$"from {GlobalUserTable} join {GuildUserTable} on {GlobalUserTable}.user_id = {GuildUserTable}.user_id";
await c.ExecuteNonQueryAsync();
@ -90,7 +90,7 @@ namespace Kerobot.Services.EntityCache
"on conflict (user_id) do update " +
"set cache_update_time = EXCLUDED.cache_update_time, username = EXCLUDED.username, " +
"discriminator = EXCLUDED.discriminator, avatar_url = EXCLUDED.avatar_url";
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = current.Id;
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)current.Id;
c.Parameters.Add("@Uname", NpgsqlDbType.Text).Value = current.Username;
c.Parameters.Add("@Disc", NpgsqlDbType.Text).Value = current.Discriminator;
var aurl = c.Parameters.Add("@Aurl", NpgsqlDbType.Text);
@ -106,6 +106,9 @@ namespace Kerobot.Services.EntityCache
private async Task DiscordClient_GuildMemberUpdated(SocketGuildUser old, SocketGuildUser current)
{
// Also update user data here, in case it's unknown (avoid foreign key constraint violation)
await DiscordClient_UserUpdated(old, current);
using (var db = await _kb.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
@ -113,11 +116,10 @@ namespace Kerobot.Services.EntityCache
c.CommandText = $"insert into {GuildUserTable} " +
"(user_id, guild_id, cache_update_time, nickname) values " +
"(@Uid, @Gid, now(), @Nname) " +
"on conflict (user_id) do update " +
"set cache_update_time = EXCLUDED.cache_update_time, username = EXCLUDED.username, " +
"discriminator = EXCLUDED.discriminator, avatar_url = EXCLUDED.avatar_url";
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = current.Id;
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = current.Guild.Id;
"on conflict (user_id, guild_id) do update " +
"set cache_update_time = EXCLUDED.cache_update_time, nickname = EXCLUDED.nickname";
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)current.Id;
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)current.Guild.Id;
var nname = c.Parameters.Add("@Nname", NpgsqlDbType.Text);
if (current.Nickname != null) nname.Value = current.Nickname;
else nname.Value = DBNull.Value;
@ -130,7 +132,7 @@ namespace Kerobot.Services.EntityCache
#endregion
#region Querying
private static Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
private static readonly Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
/// <summary>
/// See <see cref="Kerobot.EcQueryUser(ulong, string)"/>.
@ -174,12 +176,12 @@ namespace Kerobot.Services.EntityCache
var c = db.CreateCommand();
c.CommandText = $"select * from {UserView} " +
"where guild_id = @Gid";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guildId;
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
if (sID.HasValue)
{
c.CommandText += " and user_id = @Uid";
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = sID.Value;
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)sID.Value;
}
if (sName != null)

View file

@ -149,15 +149,15 @@ namespace Kerobot.Services.GuildState
var tn = t.Name;
try
{
object state;
try
{
state = await mod.CreateGuildStateAsync(guildId, guildConf[tn]); // can be null
var state = await mod.CreateGuildStateAsync(guildId, guildConf[tn]); // can be null
newStates.Add(t, new StateInfo(state, jstrHash));
}
catch (Exception ex)
catch (Exception ex) when (!(ex is ModuleLoadException))
{
Log("Encountered unhandled exception during guild state initialization:\n" +
$"Module: {tn}\n" +
Log("Unhandled exception while initializing guild state for module:\n" +
$"Module: {tn} | " +
$"Guild: {guildId} ({Kerobot.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
$"```\n{ex.ToString()}\n```", true).Wait();
Kerobot.GuildLogAsync(guildId, GuildLogSource,
@ -165,7 +165,6 @@ namespace Kerobot.Services.GuildState
"The bot owner has been notified.").Wait();
return false;
}
newStates.Add(t, new StateInfo(state, jstrHash));
}
catch (ModuleLoadException ex)
{

View file

@ -35,10 +35,10 @@ namespace Kerobot.Services.Logging
/// </summary>
private async Task DiscordClient_Log(LogMessage arg)
{
var ts = DateTimeOffset.UtcNow;
bool important = arg.Severity > LogSeverity.Info;
bool important = arg.Severity != LogSeverity.Info;
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
const string logSource = "Discord.Net";
if (arg.Exception != null) msg += "\n```\n" + arg.Exception.ToString() + "\n```";
if (important) await DoInstanceLogAsync(true, logSource, msg);
else FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
@ -64,7 +64,7 @@ namespace Kerobot.Services.Logging
using (var c = db.CreateCommand())
{
c.CommandText = "create index if not exists " +
$"{TableLog}_guildid_idx on {TableLog} guild_id";
$"{TableLog}_guild_id_idx on {TableLog} (guild_id)";
await c.ExecuteNonQueryAsync();
}
}
@ -78,8 +78,8 @@ namespace Kerobot.Services.Logging
{
c.CommandText = $"insert into {TableLog} (guild_id, log_timestamp, log_source, message) values"
+ "(@Gid, @Ts, @Src, @Msg)";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guildId;
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTz).Value = timestamp;
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
c.Prepare();
@ -126,6 +126,7 @@ namespace Kerobot.Services.Logging
}
// Report to logging channel if necessary and possible
// TODO replace with webhook?
var (g, c) = Kerobot.Config.InstanceLogReportTarget;
if ((insertException != null || report) &&
g != 0 && c != 0 && Kerobot.DiscordClient.ConnectionState == ConnectionState.Connected)
@ -142,7 +143,8 @@ namespace Kerobot.Services.Logging
{
Footer = new EmbedFooterBuilder() { Text = Name },
Timestamp = DateTimeOffset.UtcNow,
Description = "Error during recording to instance log.\nCheck the console.",
Description = "Error during recording to instance log: `" +
insertException.Message + "`\nCheck the console.",
Color = Color.DarkRed
};
await ch.SendMessageAsync("", embed: e.Build());

View file

@ -38,7 +38,7 @@ namespace Kerobot.Modules.AutoResponder
public override Task<object> CreateGuildStateAsync(ulong guild, JToken config)
{
// Guild state is a read-only IEnumerable<Definition>
if (config == null) return null;
if (config == null) return Task.FromResult<object>(null);
var guildDefs = new List<Definition>();
foreach (var defconf in config.Children<JProperty>())
{

View file

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Authors>Noikoio</Authors>
<Company>Noikoio</Company>
<Authors>Noiiko</Authors>
<Product>Kerobot</Product>
<Version>0.0.1</Version>
<Description>Essential functions for Kerobot which are available in the public bot instance.</Description>

View file

@ -18,8 +18,6 @@ namespace Kerobot.Modules.RegexModerator
class ConfDefinition
{
public string Label { get; }
readonly RegexModerator _module; // TODO is this needed?
readonly ulong _guild; // corresponding guild, for debug purposes. (is this needed?)
// Matching settings
readonly IEnumerable<Regex> _regex;
@ -40,10 +38,8 @@ namespace Kerobot.Modules.RegexModerator
public bool RemovalSendUserNotification; // send ban/kick notification to user?
public bool DeleteMessage { get; }
public ConfDefinition(RegexModerator instance, JObject def, ulong guildId)
public ConfDefinition(JObject def)
{
_module = instance;
Label = def["Label"].Value<string>();
if (string.IsNullOrWhiteSpace(Label))
throw new ModuleLoadException("A rule does not have a label defined.");

View file

@ -23,8 +23,9 @@ namespace Kerobot.Modules.RegexModerator
if (config == null) return Task.FromResult<object>(null);
var defs = new List<ConfDefinition>();
// TODO better error reporting during this process
foreach (var def in config.Children<JObject>())
defs.Add(new ConfDefinition(this, def, guildID));
defs.Add(new ConfDefinition(def));
if (defs.Count == 0) return Task.FromResult<object>(null);
return Task.FromResult<object>(defs.AsReadOnly());
@ -49,19 +50,23 @@ namespace Kerobot.Modules.RegexModerator
// Send further processing to thread pool.
// Match checking is a CPU-intensive task, thus very little checking is done here.
var msgProcessingTasks = new List<Task>();
foreach (var item in defs)
{
// Need to check sender's moderator status here. Definition can't access mod list.
var isMod = GetModerators(ch.Guild.Id).IsListMatch(msg, true);
var match = item.IsMatch(msg, isMod);
await Task.Run(async () => await ProcessMessage(item, msg, isMod));
msgProcessingTasks.Add(Task.Run(async () => await ProcessMessage(item, msg, isMod)));
}
await Task.WhenAll(msgProcessingTasks);
}
/// <summary>
/// Does further message checking and response execution.
/// Invocations of this method are meant to be on the thread pool.
/// Invocations of this method are meant to be placed onto a thread separate from the caller.
/// </summary>
private async Task ProcessMessage(ConfDefinition def, SocketMessage msg, bool isMod)
{

View file

@ -37,10 +37,10 @@ namespace Kerobot.Modules.AutoScriptResponder
await Task.WhenAll(tasks);
}
public override Task<object> CreateGuildStateAsync(JToken config)
public override Task<object> CreateGuildStateAsync(ulong guild, JToken config)
{
// Guild state is a read-only IEnumerable<Definition>
if (config == null) return null;
if (config == null) return Task.FromResult<object>(null);
var guildDefs = new List<Definition>();
foreach (var defconf in config.Children<JProperty>())
{

View file

@ -43,7 +43,7 @@ namespace Kerobot.Modules.EntryRole
public override Task<object> CreateGuildStateAsync(ulong guildID, JToken config)
{
if (config == null) return null;
if (config == null) return Task.FromResult<object>(null);
if (config.Type != JTokenType.Object)
throw new ModuleLoadException("Configuration is not properly defined.");

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Authors>Noikoio</Authors>
<Authors>Noiiko</Authors>
<Product>Kerobot</Product>
<Version>0.0.1</Version>
<Description>Kerobot modules with more specific purposes that may not work well in a public instance, but are manageable in self-hosted bot instances.</Description>