diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 057a8b9..19d30ab 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
- dotnet-version: 7.0.x
+ dotnet-version: 8.0.x
- name: Add NuGet source
run: dotnet nuget add source --username oliverbooth --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/oliverbooth/index.json"
- name: Restore dependencies
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index 9c76953..8292fef 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -17,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
- dotnet-version: 7.0.x
+ dotnet-version: 8.0.x
- name: Add GitHub NuGet source
run: dotnet nuget add source --username oliverbooth --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/oliverbooth/index.json"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6e91b3a..dff8b5d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
- dotnet-version: 7.0.x
+ dotnet-version: 8.0.x
- name: Add GitHub NuGet source
run: dotnet nuget add source --username oliverbooth --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/oliverbooth/index.json"
diff --git a/Marco/Commands/MacroCommand.cs b/Marco/Commands/MacroCommand.cs
index 02db2fa..ad5c4bd 100644
--- a/Marco/Commands/MacroCommand.cs
+++ b/Marco/Commands/MacroCommand.cs
@@ -10,14 +10,17 @@ namespace Marco.Commands;
internal sealed class MacroCommand : ApplicationCommandModule
{
private readonly MacroService _macroService;
+ private readonly MacroCooldownService _cooldownService;
///
/// Initializes a new instance of the class.
///
/// The macro service.
- public MacroCommand(MacroService macroService)
+ /// The cooldown service.
+ public MacroCommand(MacroService macroService, MacroCooldownService cooldownService)
{
_macroService = macroService;
+ _cooldownService = cooldownService;
}
[SlashCommand("macro", "Executes a macro.")]
@@ -32,11 +35,18 @@ public async Task MacroAsync(InteractionContext context,
return;
}
- if (macro.ChannelId.HasValue && macro.ChannelId.Value != context.Channel.Id)
+ DiscordChannel channel = context.Channel;
+
+ if (macro.ChannelId.HasValue && macro.ChannelId.Value != channel.Id)
{
await context.CreateResponseAsync($"The macro `{macroName}` cannot be executed here.", true).ConfigureAwait(false);
return;
}
+ if (_cooldownService.IsOnCooldown(channel, macro))
+ {
+ await context.CreateResponseAsync($"The macro `{macroName}` is on cooldown because it was very recently executed.", true).ConfigureAwait(false);
+ return;
+ }
var builder = new DiscordInteractionResponseBuilder();
string response = macro.Response;
@@ -49,5 +59,6 @@ public async Task MacroAsync(InteractionContext context,
builder.WithContent(response);
await context.CreateResponseAsync(builder).ConfigureAwait(false);
+ _cooldownService.UpdateCooldown(channel, macro);
}
}
diff --git a/Marco/Logging/ColorfulConsoleTarget.cs b/Marco/Logging/ColorfulConsoleTarget.cs
deleted file mode 100644
index 27bfc03..0000000
--- a/Marco/Logging/ColorfulConsoleTarget.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Text;
-using NLog;
-using NLog.Targets;
-using LogLevel = NLog.LogLevel;
-
-namespace Marco.Logging;
-
-///
-/// Represents an NLog target which supports colorful output to stdout.
-///
-internal sealed class ColorfulConsoleTarget : TargetWithLayout
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The name of the log target.
- public ColorfulConsoleTarget(string name)
- {
- Name = name;
- }
-
- ///
- protected override void Write(LogEventInfo logEvent)
- {
- var message = new StringBuilder();
- message.Append(Layout.Render(logEvent));
-
- if (logEvent.Level == LogLevel.Warn)
- Console.ForegroundColor = ConsoleColor.Yellow;
- else if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Fatal)
- Console.ForegroundColor = ConsoleColor.Red;
-
- if (logEvent.Exception is { } exception)
- message.Append($": {exception}");
-
- Console.WriteLine(message);
- Console.ResetColor();
- }
-}
diff --git a/Marco/Logging/LogFileTarget.cs b/Marco/Logging/LogFileTarget.cs
deleted file mode 100644
index 3a5bfac..0000000
--- a/Marco/Logging/LogFileTarget.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Text;
-using Marco.Services;
-using NLog;
-using NLog.Targets;
-
-namespace Marco.Logging;
-
-///
-/// Represents an NLog target which writes its output to a log file on disk.
-///
-internal sealed class LogFileTarget : TargetWithLayout
-{
- private readonly LoggingService _loggingService;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The name of the log target.
- /// The .
- public LogFileTarget(string name, LoggingService loggingService)
- {
- _loggingService = loggingService;
- Name = name;
- }
-
- ///
- protected override void Write(LogEventInfo logEvent)
- {
- _loggingService.ArchiveLogFilesAsync(false).GetAwaiter().GetResult();
-
- using FileStream stream = _loggingService.LogFile.Open(FileMode.Append, FileAccess.Write);
- using var writer = new StreamWriter(stream, Encoding.UTF8);
- writer.Write(Layout.Render(logEvent));
-
- if (logEvent.Exception is { } exception)
- writer.Write($": {exception}");
-
- writer.WriteLine();
- }
-}
diff --git a/Marco/Marco.csproj b/Marco/Marco.csproj
index d185758..7bc8a48 100644
--- a/Marco/Marco.csproj
+++ b/Marco/Marco.csproj
@@ -2,11 +2,11 @@
Exe
- net7.0
+ net8.0
enable
enable
Linux
- 2.1.1
+ 2.2.0
@@ -32,15 +32,17 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Marco/Program.cs b/Marco/Program.cs
index 016ac53..2800e7e 100644
--- a/Marco/Program.cs
+++ b/Marco/Program.cs
@@ -5,39 +5,42 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using NLog.Extensions.Logging;
+using Serilog;
+using Serilog.Extensions.Logging;
using X10D.Hosting.DependencyInjection;
Directory.CreateDirectory("data");
-await Host.CreateDefaultBuilder(args)
- .ConfigureAppConfiguration(builder => builder.AddJsonFile("data/config.json", true, true))
- .ConfigureLogging(builder =>
- {
- builder.ClearProviders();
- builder.AddNLog();
- })
- .ConfigureServices(services =>
- {
- services.AddSingleton(new DiscordClient(new DiscordConfiguration
- {
- Token = Environment.GetEnvironmentVariable("DISCORD_TOKEN"),
- LoggerFactory = new NLogLoggerFactory(),
- Intents = DiscordIntents.AllUnprivileged | DiscordIntents.GuildMessages | DiscordIntents.MessageContents
- }));
-
- services.AddHostedSingleton();
-
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
-
- services.AddHostedSingleton();
- services.AddHostedSingleton();
-
- services.AddDbContext();
-
- services.AddHostedSingleton();
- })
- .UseConsoleLifetime()
- .RunConsoleAsync();
+Log.Logger = new LoggerConfiguration()
+ .WriteTo.Console()
+ .WriteTo.File("logs/latest.log", rollingInterval: RollingInterval.Day)
+#if DEBUG
+ .MinimumLevel.Debug()
+#endif
+ .CreateLogger();
+
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+builder.Configuration.AddJsonFile("data/config.json", true, true);
+builder.Logging.ClearProviders();
+builder.Logging.AddSerilog();
+
+builder.Services.AddSingleton(new DiscordClient(new DiscordConfiguration
+{
+ Token = Environment.GetEnvironmentVariable("DISCORD_TOKEN"),
+ LoggerFactory = new SerilogLoggerFactory(),
+ Intents = DiscordIntents.AllUnprivileged | DiscordIntents.GuildMessages | DiscordIntents.MessageContents
+}));
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services.AddHostedSingleton();
+builder.Services.AddHostedSingleton();
+
+builder.Services.AddDbContext();
+
+builder.Services.AddHostedSingleton();
+
+IHost app = builder.Build();
+await app.RunAsync();
diff --git a/Marco/Services/BotService.cs b/Marco/Services/BotService.cs
index 8e5722a..eef4636 100644
--- a/Marco/Services/BotService.cs
+++ b/Marco/Services/BotService.cs
@@ -7,8 +7,7 @@
using DSharpPlus.SlashCommands;
using Marco.Commands;
using Microsoft.Extensions.Hosting;
-using NLog;
-using ILogger = NLog.ILogger;
+using Microsoft.Extensions.Logging;
namespace Marco.Services;
@@ -17,18 +16,19 @@ namespace Marco.Services;
///
internal sealed class BotService : BackgroundService
{
- private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();
-
+ private readonly ILogger _logger;
private readonly IServiceProvider _serviceProvider;
private readonly DiscordClient _discordClient;
///
/// Initializes a new instance of the class.
///
+ /// The logger.
/// The service provider.
/// The Discord client.
- public BotService(IServiceProvider serviceProvider, DiscordClient discordClient)
+ public BotService(ILogger logger, IServiceProvider serviceProvider, DiscordClient discordClient)
{
+ _logger = logger;
_serviceProvider = serviceProvider;
_discordClient = discordClient;
@@ -58,7 +58,7 @@ public override Task StopAsync(CancellationToken cancellationToken)
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
StartedAt = DateTimeOffset.UtcNow;
- Logger.Info($"Marco v{Version} is starting...");
+ _logger.LogInformation("Marco v{Version} is starting...", Version);
_discordClient.UseInteractivity();
@@ -67,7 +67,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Services = _serviceProvider
});
- Logger.Info("Registering commands...");
+ _logger.LogInformation("Registering commands...");
slashCommands.RegisterCommands();
slashCommands.RegisterCommands();
slashCommands.RegisterCommands();
@@ -75,7 +75,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
slashCommands.RegisterCommands();
slashCommands.RegisterCommands();
- Logger.Info("Connecting to Discord...");
+ _logger.LogInformation("Connecting to Discord...");
_discordClient.Ready += OnReady;
RegisterEvents(slashCommands);
@@ -85,17 +85,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
private Task OnReady(DiscordClient sender, ReadyEventArgs e)
{
- Logger.Info("Discord client ready");
+ _logger.LogInformation("Discord client ready");
return Task.CompletedTask;
}
- private static void RegisterEvents(SlashCommandsExtension slashCommands)
+ private void RegisterEvents(SlashCommandsExtension slashCommands)
{
slashCommands.AutocompleteErrored += (_, args) =>
{
- Logger.Error(args.Exception, "An exception was thrown when performing autocomplete");
+ _logger.LogError(args.Exception, "An exception was thrown when performing autocomplete");
if (args.Exception is DiscordException discordException)
- Logger.Error($"API response: {discordException.JsonMessage}");
+ _logger.LogError("API response: {Message}", discordException.JsonMessage);
return Task.CompletedTask;
};
@@ -103,16 +103,19 @@ private static void RegisterEvents(SlashCommandsExtension slashCommands)
slashCommands.SlashCommandInvoked += (_, args) =>
{
var optionsString = "";
- if (args.Context.Interaction?.Data?.Options is { } options)
+ InteractionContext context = args.Context;
+
+ if (context.Interaction?.Data?.Options is { } options)
optionsString = $" {string.Join(" ", options.Select(o => $"{o?.Name}: '{o?.Value}'"))}";
- Logger.Info($"{args.Context.User} ran slash command /{args.Context.CommandName}{optionsString}");
+ _logger.LogInformation("{User} ran slash command /{Command}{Options}", context.User, context.CommandName, optionsString);
return Task.CompletedTask;
};
slashCommands.ContextMenuInvoked += (_, args) =>
{
- DiscordInteractionResolvedCollection? resolved = args.Context.Interaction?.Data?.Resolved;
+ ContextMenuContext context = args.Context;
+ DiscordInteractionResolvedCollection? resolved = context.Interaction?.Data?.Resolved;
var properties = new List();
if (resolved?.Attachments?.Count > 0)
properties.Add($"attachments: {string.Join(", ", resolved.Attachments.Select(a => a.Value.Url))}");
@@ -127,9 +130,7 @@ private static void RegisterEvents(SlashCommandsExtension slashCommands)
if (resolved?.Users?.Count > 0)
properties.Add($"users: {string.Join(", ", resolved.Users.Select(r => r.Value.Id))}");
- Logger.Info($"{args.Context.User} invoked context menu '{args.Context.CommandName}' with resolved " +
- string.Join("; ", properties));
-
+ _logger.LogInformation("{User} invoked context menu '{CommandName}' with resolved {Properties}", context.User, context.CommandName, string.Join("; ", properties));
return Task.CompletedTask;
};
@@ -143,9 +144,9 @@ private static void RegisterEvents(SlashCommandsExtension slashCommands)
}
string? name = context.Interaction.Data.Name;
- Logger.Error(args.Exception, $"An exception was thrown when executing context menu '{name}'");
+ _logger.LogError(args.Exception, "An exception was thrown when executing context menu \'{Name}\'", name);
if (args.Exception is DiscordException discordException)
- Logger.Error($"API response: {discordException.JsonMessage}");
+ _logger.LogError("API response: {Message}", discordException.JsonMessage);
return Task.CompletedTask;
};
@@ -160,9 +161,9 @@ private static void RegisterEvents(SlashCommandsExtension slashCommands)
}
string? name = context.Interaction.Data.Name;
- Logger.Error(args.Exception, $"An exception was thrown when executing slash command '{name}'");
+ _logger.LogError(args.Exception, "An exception was thrown when executing slash command \'{Name}\'", name);
if (args.Exception is DiscordException discordException)
- Logger.Error($"API response: {discordException.JsonMessage}");
+ _logger.LogError("API response: {Message}", discordException.JsonMessage);
return Task.CompletedTask;
};
diff --git a/Marco/Services/LoggingService.cs b/Marco/Services/LoggingService.cs
deleted file mode 100644
index be07602..0000000
--- a/Marco/Services/LoggingService.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System.IO.Compression;
-using Marco.Logging;
-using Microsoft.Extensions.Hosting;
-using NLog;
-using NLog.Config;
-using NLog.LayoutRenderers;
-using NLog.Layouts;
-using LogLevel = NLog.LogLevel;
-
-namespace Marco.Services;
-
-///
-/// Represents a class which implements a logging service that supports multiple log targets.
-///
-///
-/// This class implements a logging structure similar to that of Minecraft, where historic logs are compressed to a .gz and
-/// the latest log is found in logs/latest.log.
-///
-internal sealed class LoggingService : BackgroundService
-{
- private const string LogFileName = "logs/latest.log";
-
- ///
- /// Initializes a new instance of the class.
- ///
- public LoggingService()
- {
- LogFile = new FileInfo(LogFileName);
- }
-
- ///
- /// Gets or sets the log file.
- ///
- /// The log file.
- public FileInfo LogFile { get; set; }
-
- ///
- /// Archives any existing log files.
- ///
- public async Task ArchiveLogFilesAsync(bool archiveToday = true)
- {
- var latestFile = new FileInfo(LogFile.FullName);
- if (!latestFile.Exists) return;
-
- DateTime lastWrite = latestFile.LastWriteTime;
- string lastWriteDate = $"{lastWrite:yyyy-MM-dd}";
- var version = 0;
- string name;
-
- if (!archiveToday && lastWrite.Date == DateTime.Today) return;
-
- while (File.Exists(name = Path.Combine(LogFile.Directory!.FullName, $"{lastWriteDate}-{++version}.log.gz")))
- {
- // body ignored
- }
-
- await using (FileStream source = latestFile.OpenRead())
- {
- await using FileStream output = File.Create(name);
- await using var gzip = new GZipStream(output, CompressionMode.Compress);
- await source.CopyToAsync(gzip);
- }
-
- latestFile.Delete();
- }
-
- ///
- protected override Task ExecuteAsync(CancellationToken stoppingToken)
- {
- LogFile.Directory?.Create();
-
- LayoutRenderer.Register("TheTime", info => info.TimeStamp.ToString("HH:mm:ss"));
- LayoutRenderer.Register("PluginName", info => info.LoggerName);
-
- Layout? layout = Layout.FromString("[${TheTime} ${level:uppercase=true}] [${PluginName}] ${message}");
- var config = new LoggingConfiguration();
- var fileLogger = new LogFileTarget("FileLogger", this) {Layout = layout};
- var consoleLogger = new ColorfulConsoleTarget("ConsoleLogger") {Layout = layout};
-
-#if DEBUG
- LogLevel minLevel = LogLevel.Debug;
-#else
- LogLevel minLevel = LogLevel.Info;
- if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ENABLE_DEBUG_LOGGING")))
- minLevel = LogLevel.Debug;
-#endif
- config.AddRule(minLevel, LogLevel.Fatal, consoleLogger);
- config.AddRule(minLevel, LogLevel.Fatal, fileLogger);
-
- LogManager.Configuration = config;
-
- return ArchiveLogFilesAsync();
- }
-}
diff --git a/Marco/Services/MacroListeningService.cs b/Marco/Services/MacroListeningService.cs
index f37f6bb..a64902a 100644
--- a/Marco/Services/MacroListeningService.cs
+++ b/Marco/Services/MacroListeningService.cs
@@ -4,7 +4,7 @@
using Marco.Configuration;
using Marco.Data;
using Microsoft.Extensions.Hosting;
-using NLog;
+using Microsoft.Extensions.Logging;
namespace Marco.Services;
@@ -13,8 +13,7 @@ namespace Marco.Services;
///
internal sealed class MacroListeningService : BackgroundService
{
- private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();
-
+ private readonly ILogger _logger;
private readonly DiscordClient _discordClient;
private readonly ConfigurationService _configurationService;
private readonly MacroService _macroService;
@@ -23,17 +22,20 @@ internal sealed class MacroListeningService : BackgroundService
///
/// Initializes a new instance of the class.
///
+ /// The logger.
/// The Discord client.
/// The configuration service.
/// The macro service.
/// The macro cooldown service.
public MacroListeningService(
+ ILogger logger,
DiscordClient discordClient,
ConfigurationService configurationService,
MacroService macroService,
MacroCooldownService cooldownService
)
{
+ _logger = logger;
_discordClient = discordClient;
_configurationService = configurationService;
_macroService = macroService;
@@ -81,14 +83,14 @@ private async Task OnMessageCreated(DiscordClient sender, MessageCreateEventArgs
{
if (_cooldownService.IsOnCooldown(channel, macro))
{
- Logger.Info($"{e.Author} used channel macro '{command}' in {channel} but is on cooldown");
+ _logger.LogInformation("{Author} used channel macro \'{Command}\' in {Channel} but is on cooldown", e.Author, command, channel);
if (cooldownEmoji is not null)
await e.Message.CreateReactionAsync(cooldownEmoji).ConfigureAwait(false);
}
else
{
- Logger.Info($"{e.Author} used channel macro '{command}' in {channel}");
+ _logger.LogInformation("{Author} used channel macro \'{Command}\' in {Channel}", e.Author, command, channel);
if (successEmoji is not null)
await e.Message.CreateReactionAsync(successEmoji).ConfigureAwait(false);
@@ -101,14 +103,14 @@ private async Task OnMessageCreated(DiscordClient sender, MessageCreateEventArgs
{
if (_cooldownService.IsOnCooldown(channel, macro))
{
- Logger.Info($"{e.Author} used global macro '{command}' in {channel} but is on cooldown");
+ _logger.LogInformation("{Author} used global macro \'{Command}\' in {Channel} but is on cooldown", e.Author, command, channel);
if (cooldownEmoji is not null)
await e.Message.CreateReactionAsync(cooldownEmoji).ConfigureAwait(false);
}
else
{
- Logger.Info($"{e.Author} used global macro '{command}' in {channel}");
+ _logger.LogInformation("{Author} used global macro \'{Command}\' in {Channel}", e.Author, command, channel);
if (successEmoji is not null)
await e.Message.CreateReactionAsync(successEmoji).ConfigureAwait(false);
@@ -140,7 +142,7 @@ private async Task OnMessageCreated(DiscordClient sender, MessageCreateEventArgs
}
else
{
- Logger.Info($"{e.Author} used unknown macro '{command}' in {channel}");
+ _logger.LogInformation("{Author} used unknown macro \'{Command}\' in {Channel}", e.Author, command, channel);
if (unknownEmoji is not null)
await e.Message.CreateReactionAsync(unknownEmoji).ConfigureAwait(false);
diff --git a/Marco/Services/MacroService.cs b/Marco/Services/MacroService.cs
index 2e0dd4a..9419d05 100644
--- a/Marco/Services/MacroService.cs
+++ b/Marco/Services/MacroService.cs
@@ -7,7 +7,7 @@
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using NLog;
+using Microsoft.Extensions.Logging;
using X10D.Collections;
namespace Marco.Services;
@@ -17,7 +17,7 @@ namespace Marco.Services;
///
internal sealed class MacroService : BackgroundService
{
- private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();
+ private readonly ILogger _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly DiscordClient _discordClient;
private readonly Dictionary> _macros = new();
@@ -25,10 +25,12 @@ internal sealed class MacroService : BackgroundService
///
/// Initializes a new instance of the class.
///
+ /// The logger.
/// The scope factory.
/// The Discord client.
- public MacroService(IServiceScopeFactory scopeFactory, DiscordClient discordClient)
+ public MacroService(ILogger logger, IServiceScopeFactory scopeFactory, DiscordClient discordClient)
{
+ _logger = logger;
_scopeFactory = scopeFactory;
_discordClient = discordClient;
}
@@ -324,7 +326,7 @@ public async Task UpdateFromDatabaseAsync(DiscordGuild guild)
macros[aliases[index]] = macro;
}
- Logger.Info($"Loaded {macros.Count} macros for {guild}");
+ _logger.LogInformation("Loaded {Count} macros for {Guild}", macros.Count, guild);
}
///
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..b5b37b6
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "8.0.0",
+ "rollForward": "latestMajor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file