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:
parent
2319c91fc7
commit
ffa5b5754b
14 changed files with 63 additions and 69 deletions
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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>())
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>())
|
||||
{
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue