diff --git a/RegexBot/ModuleLoader.cs b/RegexBot/ModuleLoader.cs
index 6007b2b..43bda08 100644
--- a/RegexBot/ModuleLoader.cs
+++ b/RegexBot/ModuleLoader.cs
@@ -4,8 +4,6 @@ using System.Reflection;
namespace RegexBot;
static class ModuleLoader {
- private const string LogName = nameof(ModuleLoader);
-
///
/// Given the instance configuration, loads all appropriate types from file specified in it.
///
@@ -42,13 +40,12 @@ static class ModuleLoader {
where !type.IsAssignableFrom(typeof(RegexbotModule))
where type.GetCustomAttribute() != null
select type;
- k._svcLogging.DoInstanceLog(false, LogName, $"Scanning {asm.GetName().Name}");
+ k._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}");
var newmods = new List();
foreach (var t in eligibleTypes) {
var mod = Activator.CreateInstance(t, k)!;
- k._svcLogging.DoInstanceLog(false, LogName,
- $"---> Loading module {t.FullName}");
+ k._svcLogging.DoLog(false, nameof(ModuleLoader), $"---> Loading module {t.FullName}");
newmods.Add((RegexbotModule)mod);
}
return newmods;
diff --git a/RegexBot/Program.cs b/RegexBot/Program.cs
index 4ae93c7..e8d821d 100644
--- a/RegexBot/Program.cs
+++ b/RegexBot/Program.cs
@@ -50,19 +50,16 @@ class Program {
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
e.Cancel = true;
- _main._svcLogging.DoInstanceLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
+ _main._svcLogging.DoLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
- // 5 seconds of leeway - any currently running tasks will need time to finish executing
- var closeWait = Task.Delay(5000);
-
- // TODO periodic task service: stop processing, wait for all tasks to finish
- // TODO notify services of shutdown
-
- closeWait.Wait();
-
- bool success = _main.DiscordClient.StopAsync().Wait(1000);
- if (!success) _main._svcLogging.DoInstanceLog(false, nameof(RegexBot),
- "Failed to disconnect cleanly from Discord. Will force shut down.");
+ var finishingTasks = Task.Run(async () => {
+ // TODO periodic task service: stop processing, wait for all tasks to finish
+ // TODO notify services of shutdown
+ await _main.DiscordClient.StopAsync();
+ });
+
+ if (!finishingTasks.Wait(5000))
+ _main._svcLogging.DoLog(false, nameof(RegexBot), "Could not disconnect properly. Exiting...");
Environment.Exit(0);
}
}
diff --git a/RegexBot/RegexbotClient.cs b/RegexBot/RegexbotClient.cs
index f11c0b4..fc66b1f 100644
--- a/RegexBot/RegexbotClient.cs
+++ b/RegexBot/RegexbotClient.cs
@@ -37,6 +37,6 @@ public partial class RegexbotClient {
// Everything's ready to go. Print the welcome message here.
var ver = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
- _svcLogging.DoInstanceLog(false, nameof(RegexBot), $"{nameof(RegexBot)} v{ver}. https://github.com/NoiTheCat/RegexBot");
+ _svcLogging.DoLog(true, nameof(RegexBot), $"{nameof(RegexBot)} v{ver} - https://github.com/NoiTheCat/RegexBot");
}
}
diff --git a/RegexBot/RegexbotModule.cs b/RegexBot/RegexbotModule.cs
index 582af74..2a6fb16 100644
--- a/RegexBot/RegexbotModule.cs
+++ b/RegexBot/RegexbotModule.cs
@@ -69,18 +69,21 @@ public abstract class RegexbotModule {
protected EntityList GetModerators(ulong guild) => Bot._svcGuildState.DoGetModlist(guild);
///
- /// Appends a message to the specified guild log.
+ /// Emits a log message to the bot console that is associated with the specified guild.
///
/// /// The log message to send. Multi-line messages are acceptable.
- protected void Log(ulong guild, string message) => Bot._svcLogging.DoGuildLog(guild, Name, message);
+ protected void Log(SocketGuild guild, string? message) {
+ var gname = guild.Name ?? $"Guild ID {guild.Id}";
+ Bot._svcLogging.DoLog(false, $"{Name}] [{gname}", message);
+ }
///
- /// Sends a message to the instance log.
+ /// Emits a log message to the bot console and, optionally, the logging webhook.
///
/// The log message to send. Multi-line messages are acceptable.
///
/// Specifies if the log message should be sent to the reporting channel.
/// Only messages of very high importance should use this option.
///
- protected void PLog(string message, bool report = false) => Bot._svcLogging.DoInstanceLog(report, Name, message);
+ protected void Log(string message, bool report = false) => Bot._svcLogging.DoLog(report, Name, message);
}
diff --git a/RegexBot/Services/Logging/LoggingService.cs b/RegexBot/Services/Logging/LoggingService.cs
index acfab82..2e02e97 100644
--- a/RegexBot/Services/Logging/LoggingService.cs
+++ b/RegexBot/Services/Logging/LoggingService.cs
@@ -1,25 +1,30 @@
using Discord;
using Discord.Webhook;
-using RegexBot.Data;
+using System.Reflection;
+using System.Text;
namespace RegexBot.Services.Logging;
-
///
-/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
-/// For further information on log types, see documentation under .
+/// Implements program-wide logging.
///
class LoggingService : Service {
// NOTE: Service.Log's functionality is implemented here. DO NOT use within this class.
private readonly DiscordWebhookClient _instLogWebhook;
+ private readonly string? _logBasePath;
internal LoggingService(RegexbotClient bot) : base(bot) {
_instLogWebhook = new DiscordWebhookClient(bot.Config.InstanceLogTarget);
+ _logBasePath = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
+ + Path.DirectorySeparatorChar + "logs";
+ try {
+ if (!Directory.Exists(_logBasePath)) Directory.CreateDirectory(_logBasePath);
+ Directory.GetFiles(_logBasePath);
+ } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) {
+ _logBasePath = null;
+ Output(Name, "Cannot create or access logging directory. File logging will be disabled.");
+ }
- // Discord.Net log handling (client logging option is specified in Program.cs)
bot.DiscordClient.Log += DiscordClient_Log;
- // Let's also do the ready message
- bot.DiscordClient.Ready +=
- delegate { DoInstanceLog(true, nameof(RegexBot), "Connected and ready."); return Task.CompletedTask; };
}
///
@@ -27,30 +32,45 @@ class LoggingService : Service {
/// Only events with high importance are stored. Others are just printed to console.
///
private Task DiscordClient_Log(LogMessage arg) {
- bool important = arg.Severity != LogSeverity.Info;
- string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
- const string logSource = "Discord.Net";
+ var msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
if (arg.Exception != null) msg += "\n```\n" + arg.Exception.ToString() + "\n```";
- if (important) DoInstanceLog(true, logSource, msg);
- else ToConsole(logSource, msg);
+ var important = arg.Severity != LogSeverity.Info;
+ switch (arg.Message) { // Prevent webhook logs for these 'important' Discord.Net messages
+ case "Connecting":
+ case "Connected":
+ case "Ready":
+ case "Disconnecting":
+ case "Disconnected":
+ case "Resumed previous session":
+ case "Failed to resume previous session":
+ important = false;
+ break;
+ }
+ DoLog(important, "Discord.Net", msg);
return Task.CompletedTask;
}
- private static void ToConsole(string source, string message) {
- message ??= "(null)";
- var prefix = $"[{DateTimeOffset.UtcNow:u}] [{source}] ";
+ private void Output(string source, string message) {
+ var now = DateTimeOffset.UtcNow;
+ var output = new StringBuilder();
+ var prefix = $"[{now:u}] [{source}] ";
foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) {
- Console.WriteLine(prefix + line);
+ output.Append(prefix).AppendLine(line);
+ }
+ var outstr = output.ToString();
+ Console.Write(outstr);
+ if (_logBasePath != null) {
+ var filename = _logBasePath + Path.DirectorySeparatorChar + $"{now:yyyy-MM}.log";
+ File.AppendAllText(filename, outstr, Encoding.UTF8);
}
}
// Hooked
- internal void DoInstanceLog(bool report, string source, string? message) {
+ internal void DoLog(bool report, string source, string? message) {
message ??= "(null)";
- ToConsole(source, message);
-
+ Output(source, message);
if (report) Task.Run(() => ReportInstanceWebhook(source, message));
}
@@ -62,24 +82,8 @@ class LoggingService : Service {
Description = message
};
await _instLogWebhook.SendMessageAsync(embeds: new[] { e.Build() });
- } catch (Discord.Net.HttpException ex) {
- DoInstanceLog(false, Name, "Failed to send message to reporting channel: " + ex.Message);
- }
- }
-
- // Hooked
- public void DoGuildLog(ulong guild, string source, string message) {
- message ??= "(null)";
- try {
- using var db = new BotDatabaseContext();
- db.Add(new GuildLogLine() { GuildId = (long)guild, Source = source, Message = message });
- db.SaveChanges();
-#if DEBUG
- ToConsole($"DEBUG {guild} - {source}", message);
-#endif
} catch (Exception ex) {
- // Stack trace goes to console only.
- DoInstanceLog(false, Name, "Error when storing guild log line: " + ex.ToString());
+ DoLog(false, Name, "Failed to send message to reporting channel: " + ex.Message);
}
}
}
diff --git a/RegexBot/Services/ModuleState/ModuleStateService.cs b/RegexBot/Services/ModuleState/ModuleStateService.cs
index 0ccf3ed..d6aa513 100644
--- a/RegexBot/Services/ModuleState/ModuleStateService.cs
+++ b/RegexBot/Services/ModuleState/ModuleStateService.cs
@@ -27,10 +27,7 @@ class ModuleStateService : Service {
}
private async Task RefreshGuildState(SocketGuild arg) {
- bool success = await ProcessConfiguration(arg.Id);
-
- if (success) BotClient._svcLogging.DoInstanceLog(false, GuildLogSource, $"Configuration refreshed for guild ID {arg.Id}.");
- else BotClient._svcLogging.DoGuildLog(arg.Id, GuildLogSource, "Configuration was not refreshed due to errors.");
+ if (await ProcessConfiguration(arg.Id)) Log($"Configuration refreshed for server {arg.Id}.");
}
private Task RemoveGuildData(SocketGuild arg) {
@@ -79,7 +76,7 @@ class ModuleStateService : Service {
throw new InvalidCastException("Configuration is not valid JSON.");
}
} catch (Exception ex) when (ex is JsonReaderException or InvalidCastException) {
- BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource, $"A problem exists within the guild configuration: {ex.Message}");
+ Log($"Error loading configuration for server ID {guildId}: {ex.Message}");
return false;
}
@@ -90,25 +87,19 @@ class ModuleStateService : Service {
// Create guild state objects for all existing modules
var newStates = new Dictionary();
- foreach (var mod in BotClient.Modules) {
- var t = mod.GetType();
- var tn = t.Name;
+ foreach (var module in BotClient.Modules) {
+ var t = module.GetType();
try {
- try {
- var state = await mod.CreateGuildStateAsync(guildId, guildConf[tn]!);
- newStates.Add(t, state);
- } catch (Exception ex) when (ex is not ModuleLoadException) {
- Log("Unhandled exception while initializing guild state for module:\n" +
- $"Module: {tn} | " +
- $"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
- $"```\n{ex}\n```", true);
- BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource,
- "An internal error occurred when attempting to load new configuration.");
- return false;
- }
+ var state = await module.CreateGuildStateAsync(guildId, guildConf[module.Name]!);
+ newStates.Add(t, state);
} catch (ModuleLoadException ex) {
- BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource,
- $"{tn} has encountered an issue with its configuration: {ex.Message}");
+ Log($"{guildId}: Error reading configuration regarding {module.Name}: {ex.Message}");
+ return false;
+ } catch (Exception ex) when (ex is not ModuleLoadException) {
+ Log("Unhandled exception while initializing guild state for module:\n" +
+ $"Module: {module.Name} | " +
+ $"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
+ $"```\n{ex}\n```", true);
return false;
}
}
diff --git a/RegexBot/Services/Service.cs b/RegexBot/Services/Service.cs
index 6aa2b91..bdec16b 100644
--- a/RegexBot/Services/Service.cs
+++ b/RegexBot/Services/Service.cs
@@ -19,5 +19,5 @@ internal abstract class Service {
///
/// The log message to send. Multi-line messages are acceptable.
/// Specify if the log message should be sent to a reporting channel.
- protected void Log(string message, bool report = false) => BotClient._svcLogging.DoInstanceLog(report, Name, message);
+ protected void Log(string message, bool report = false) => BotClient._svcLogging.DoLog(report, Name, message);
}