From 63ac5d4fc5e07d49e0884e28094c33155e1e2018 Mon Sep 17 00:00:00 2001 From: Jofairden Date: Sun, 25 May 2025 21:20:31 +0200 Subject: [PATCH 1/6] Split classes and added some docs --- .gitignore | 2 ++ Components/GuildConfig.cs | 36 +++++++++++++++++++--- Components/GuildTag.cs | 31 +++++++++++++++++++ Components/ModInfo.cs | 7 +---- Components/SiteStatus.cs | 8 ----- Components/SiteStatusCode.cs | 7 +++++ Components/VoteData.cs | 6 ++++ Interactions/ModNameAutocompleteHandler.cs | 19 ++++++++++++ Modules/BaseModules.cs | 24 ++------------- Modules/ConfigModuleBase.cs | 33 ++++++++++++++++++++ Modules/InteractionModule.cs | 11 ++----- Preconditions/HasRolePermission.cs | 7 +++-- Preconditions/ServerOwnerOnly.cs | 3 ++ 13 files changed, 144 insertions(+), 50 deletions(-) create mode 100644 Components/SiteStatusCode.cs create mode 100644 Components/VoteData.cs create mode 100644 Interactions/ModNameAutocompleteHandler.cs create mode 100644 Modules/ConfigModuleBase.cs diff --git a/.gitignore b/.gitignore index bff679b..e72d995 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ bld/ [Oo]bj/ [Ll]og/ +# VSCODE +.vscode/ # Visual Studio 2015/2017 cache/options directory .vs/ .vs/**/* diff --git a/Components/GuildConfig.cs b/Components/GuildConfig.cs index c761247..42eb95b 100644 --- a/Components/GuildConfig.cs +++ b/Components/GuildConfig.cs @@ -6,20 +6,44 @@ namespace tModloaderDiscordBot.Components { + /// + /// Defines the stored configuration of a guild + /// public sealed class GuildConfig { + /// + /// The guild's snowflake Id this configuration belongs to + /// public ulong GuildId; - public IList SiteStatuses = new List(); - public IList GuildTags = new List(); - public BotPermissions Permissions = new BotPermissions(); + + /// + /// A list of site statuses being tracked by this configuration + /// + public IList SiteStatuses = []; + + /// + /// The tags that belong to this configuration + /// + public IList GuildTags = []; + + /// + /// The bot's permissions + /// + public BotPermissions Permissions = new(); [JsonIgnore] private GuildConfigService _guildConfigService; + /// + /// Initializes the configuration + /// public void Initialize(GuildConfigService guildConfigService) { _guildConfigService = guildConfigService; } + /// + /// Creates a new configuration instance for the given guild + /// public GuildConfig(SocketGuild guild) { if (guild != null) @@ -28,9 +52,13 @@ public GuildConfig(SocketGuild guild) } } + /// + /// Attempts to update this configuration and write it to storage + /// + /// public async Task Update() { - if (_guildConfigService == null) + if (_guildConfigService == null) return false; await _guildConfigService.UpdateCacheForConfig(this); diff --git a/Components/GuildTag.cs b/Components/GuildTag.cs index 69edb19..4c304d3 100644 --- a/Components/GuildTag.cs +++ b/Components/GuildTag.cs @@ -3,15 +3,46 @@ namespace tModloaderDiscordBot.Components { + /// + /// Defines a tag in a guild + /// public sealed class GuildTag { + /// + /// The owner's snowflake Id + /// public ulong OwnerId; + + /// + /// The tag's name + /// public string Name; + + /// + /// The tag's value + /// public string Value; + + /// + /// Whether the tag is global + /// public bool IsGlobal; + /// + /// Returns whether the given snowflake id is the owner of this tag + /// public bool IsOwner(ulong id) => OwnerId == id; + + /// + /// Returns whether the tag's name matches the given name + /// + /// + /// public bool MatchesName(string name) => Name.EqualsIgnoreCase(name); + + /// + /// Returns whether the given key is valid for use after being sanitized + /// public static bool IsKeyValid(string key) => Format.Sanitize(key).Equals(key) && !key.Contains(" "); } } diff --git a/Components/ModInfo.cs b/Components/ModInfo.cs index c967f10..5bec674 100644 --- a/Components/ModInfo.cs +++ b/Components/ModInfo.cs @@ -150,10 +150,5 @@ public override string ToString() } } - public class VoteData - { - public int score; - public int votes_up; - public int votes_down; - } + } \ No newline at end of file diff --git a/Components/SiteStatus.cs b/Components/SiteStatus.cs index 2c9e60c..f13b3b1 100644 --- a/Components/SiteStatus.cs +++ b/Components/SiteStatus.cs @@ -6,14 +6,6 @@ namespace tModloaderDiscordBot.Components { - public enum SiteStatusCode - { - Offline, - Online, - Unknown, - Invalid - } - public class SiteStatus { [JsonIgnore] diff --git a/Components/SiteStatusCode.cs b/Components/SiteStatusCode.cs new file mode 100644 index 0000000..4417f73 --- /dev/null +++ b/Components/SiteStatusCode.cs @@ -0,0 +1,7 @@ + public enum SiteStatusCode + { + Offline, + Online, + Unknown, + Invalid + } \ No newline at end of file diff --git a/Components/VoteData.cs b/Components/VoteData.cs new file mode 100644 index 0000000..57a83e8 --- /dev/null +++ b/Components/VoteData.cs @@ -0,0 +1,6 @@ + public class VoteData + { + public int score; + public int votes_up; + public int votes_down; + } \ No newline at end of file diff --git a/Interactions/ModNameAutocompleteHandler.cs b/Interactions/ModNameAutocompleteHandler.cs new file mode 100644 index 0000000..3b99e19 --- /dev/null +++ b/Interactions/ModNameAutocompleteHandler.cs @@ -0,0 +1,19 @@ +using Discord; +using Discord.Interactions; +using System; +using System.Linq; +using System.Threading.Tasks; +using tModloaderDiscordBot.Services; + +namespace tModloaderDiscordBot.Interactions +{ + public class ModNameAutocompleteHandler : AutocompleteHandler + { + public override Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) + { + string userInput = autocompleteInteraction.Data.Current.Value.ToString(); + var mods = ModService.Mods.Where(m => m.Contains(userInput, StringComparison.CurrentCultureIgnoreCase)).Take(10).Select(x => new AutocompleteResult(x, x)); + return Task.FromResult(AutocompletionResult.FromSuccess(mods)); + } + } +} diff --git a/Modules/BaseModules.cs b/Modules/BaseModules.cs index c10a2fd..9115beb 100644 --- a/Modules/BaseModules.cs +++ b/Modules/BaseModules.cs @@ -5,30 +5,12 @@ namespace tModloaderDiscordBot.Modules { + /// + /// Defines the base starting point for any command + /// public abstract class BotModuleBase : ModuleBase { // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable public CommandService CommandService { get; set; } } - - public abstract class ConfigModuleBase : BotModuleBase - { - public GuildConfigService GuildConfigService { get; set; } - [DontInject] public GuildConfig Config { get; set; } - - // Note: Context is set before execute, not available in constructor - protected override void BeforeExecute(CommandInfo command) - { - base.BeforeExecute(command); - - if (GuildConfigService == null) - throw new Exception("Failed to get guild config service"); - - Config = GuildConfigService.GetConfig(Context.Guild.Id); - if (Config == null) - throw new Exception("Failed to get guild config"); - - Config.Initialize(GuildConfigService); - } - } } diff --git a/Modules/ConfigModuleBase.cs b/Modules/ConfigModuleBase.cs new file mode 100644 index 0000000..3e366ef --- /dev/null +++ b/Modules/ConfigModuleBase.cs @@ -0,0 +1,33 @@ +using System; +using Discord.Commands; +using tModloaderDiscordBot.Components; +using tModloaderDiscordBot.Modules; +using tModloaderDiscordBot.Services; + +/// +/// Defines the base starting point of any command requiring access to the Guild's configuration +/// +public abstract class ConfigModuleBase : BotModuleBase +{ + public GuildConfigService GuildConfigService { get; set; } + + /// + /// The Guild's configuration which the command was executed in + /// + [DontInject] public GuildConfig Config { get; set; } + + // Note: Context is set before execute, not available in constructor + protected override void BeforeExecute(CommandInfo command) + { + base.BeforeExecute(command); + + if (GuildConfigService == null) + throw new Exception("Failed to get guild config service"); + + Config = GuildConfigService.GetConfig(Context.Guild.Id); + if (Config == null) + throw new Exception("Failed to get guild config"); + + Config.Initialize(GuildConfigService); + } +} \ No newline at end of file diff --git a/Modules/InteractionModule.cs b/Modules/InteractionModule.cs index b21a910..95e1064 100644 --- a/Modules/InteractionModule.cs +++ b/Modules/InteractionModule.cs @@ -6,6 +6,7 @@ using tModloaderDiscordBot.Services; using System.Net; using tModloaderDiscordBot.Utils; +using tModloaderDiscordBot.Interactions; namespace tModloaderDiscordBot.Modules { @@ -40,13 +41,5 @@ public async Task WikiSearch(string searchTerm) } } - public class ModNameAutocompleteHandler : AutocompleteHandler - { - public override async Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) - { - string userInput = autocompleteInteraction.Data.Current.Value.ToString(); - var mods = ModService.Mods.Where(m => m.Contains(userInput, StringComparison.CurrentCultureIgnoreCase)).Take(10).Select(x => new AutocompleteResult(x, x)); - return AutocompletionResult.FromSuccess(mods); - } - } + } diff --git a/Preconditions/HasRolePermission.cs b/Preconditions/HasRolePermission.cs index 33cacab..10a5dda 100644 --- a/Preconditions/HasRolePermission.cs +++ b/Preconditions/HasRolePermission.cs @@ -9,14 +9,17 @@ namespace tModloaderDiscordBot.Preconditions { + /// + /// Defines an attribute which will check if the user has the permisisons required when executing a command + /// internal class HasPermissionAttribute : PreconditionAttribute { public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { bool CheckIfBlocked(IGuildUser user, BotPermissions gPerms) { - return gPerms.IsBlocked(context.User.Id) - || user.RoleIds.Any(gPerms.IsBlocked); + return gPerms.IsBlocked(context.User.Id) + || user.RoleIds.Any(gPerms.IsBlocked); } bool HasPermissions(string key, IGuildUser user, BotPermissions gPerms) diff --git a/Preconditions/ServerOwnerOnly.cs b/Preconditions/ServerOwnerOnly.cs index 87365e4..39e1629 100644 --- a/Preconditions/ServerOwnerOnly.cs +++ b/Preconditions/ServerOwnerOnly.cs @@ -4,6 +4,9 @@ namespace tModloaderDiscordBot.Preconditions { + /// + /// Defines an attribute which will check if the user is the server owner when executing a command + /// internal class ServerOwnerOnly : PreconditionAttribute { public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) From 144fd1eda0e242c3f19a437e6b6a720e500a7675 Mon Sep 17 00:00:00 2001 From: Jofairden Date: Sun, 25 May 2025 22:37:12 +0200 Subject: [PATCH 2/6] Factories --- Factories/CommandServiceFactory.cs | 34 +++ Factories/DiscordClientFactory.cs | 26 +++ Factories/ServiceProviderFactory.cs | 43 ++++ Program.cs | 227 +++++++++++-------- Services/BanAppealChannelService.cs | 4 +- Services/CrosspostService.cs | 4 +- Services/LegacyModService.cs | 2 +- Services/SupportChannelAutoMessageService.cs | 6 +- 8 files changed, 240 insertions(+), 106 deletions(-) create mode 100644 Factories/CommandServiceFactory.cs create mode 100644 Factories/DiscordClientFactory.cs create mode 100644 Factories/ServiceProviderFactory.cs diff --git a/Factories/CommandServiceFactory.cs b/Factories/CommandServiceFactory.cs new file mode 100644 index 0000000..238c440 --- /dev/null +++ b/Factories/CommandServiceFactory.cs @@ -0,0 +1,34 @@ +using Discord; +using Discord.Commands; + +namespace tModloaderDiscordBot.Factories +{ + internal class CommandServiceFactory + { + private CommandServiceFactory() + { + + } + + public static CommandServiceConfig CreateCommandServiceConfig() + { + return new CommandServiceConfig + { + DefaultRunMode = RunMode.Async, + CaseSensitiveCommands = false, +#if TESTBOT + LogLevel = LogSeverity.Critical, + ThrowOnError = true, +#else + LogLevel = LogSeverity.Debug, + ThrowOnError = false +#endif + }; + } + + public static CommandService CreateCommandService() + { + return new CommandService(CreateCommandServiceConfig()); + } + } +} diff --git a/Factories/DiscordClientFactory.cs b/Factories/DiscordClientFactory.cs new file mode 100644 index 0000000..a2214fa --- /dev/null +++ b/Factories/DiscordClientFactory.cs @@ -0,0 +1,26 @@ +using Discord; +using Discord.WebSocket; + +namespace tModloaderDiscordBot.Factories +{ + internal class DiscordClientFactory + { + private DiscordClientFactory() { } + + public static DiscordSocketConfig CreateDiscordSocketConfig() + { + return new DiscordSocketConfig + { + GatewayIntents = (GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers) & ~(GatewayIntents.GuildScheduledEvents | GatewayIntents.GuildInvites), + AlwaysDownloadUsers = true, + LogLevel = LogSeverity.Verbose, + MessageCacheSize = 100 + }; + } + + public static IDiscordClient CreateDiscordSocketClient() + { + return new DiscordSocketClient(CreateDiscordSocketConfig()); + } + } +} diff --git a/Factories/ServiceProviderFactory.cs b/Factories/ServiceProviderFactory.cs new file mode 100644 index 0000000..57b5c1d --- /dev/null +++ b/Factories/ServiceProviderFactory.cs @@ -0,0 +1,43 @@ +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Reflection; +using System.Resources; +using tModloaderDiscordBot.Services; + +namespace tModloaderDiscordBot.Factories +{ + internal class ServiceProviderFactory + { + private ServiceProviderFactory() { } + + private static Assembly EntryAssembly => Assembly.GetEntryAssembly(); + private static readonly ResourceManager ResourceManager = new("tModloaderDiscordBot.Properties.Resources", EntryAssembly); + + public static IServiceCollection CreateServiceCollection() + { + return new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + //.AddSingleton() + // How to use resources: + //_services.GetRequiredService().GetString("key") + .AddSingleton(ResourceManager) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } + } +} diff --git a/Program.cs b/Program.cs index efe1884..9e7f6c1 100644 --- a/Program.cs +++ b/Program.cs @@ -4,97 +4,96 @@ using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; using System; -using System.Resources; using System.Threading.Tasks; +using tModloaderDiscordBot.Factories; using tModloaderDiscordBot.Services; namespace tModloaderDiscordBot { public class Program { +#if TESTBOT + private static readonly string _envTokenKey = "TmlTestToken"; +#else + private static readonly string _envTokenKey = "TmlBotToken"; +#endif + public static bool Ready; public static void Main(string[] args) - => new Program().StartAsync().GetAwaiter().GetResult(); + => new Program().RunAsync().GetAwaiter().GetResult(); + internal static IUser BotOwner; - private CommandService _commandService; - private DiscordSocketClient _client; - private IServiceProvider _services; - private LoggingService _loggingService; - private InteractionService _interactionService; - //private ReactionRoleService _reactionRoleService; - private async Task StartAsync() - { - IServiceProvider BuildServiceProvider() - { - return new ServiceCollection() - .AddSingleton(_client) - .AddSingleton(_commandService) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - //.AddSingleton() - // How to use resources: - //_services.GetRequiredService().GetString("key") - .AddSingleton(new ResourceManager("tModloaderDiscordBot.Properties.Resources", GetType().Assembly)) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .BuildServiceProvider(); - } - _client = new DiscordSocketClient(new DiscordSocketConfig - { - GatewayIntents = (GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers) & ~(GatewayIntents.GuildScheduledEvents | GatewayIntents.GuildInvites), - AlwaysDownloadUsers = true, - LogLevel = LogSeverity.Verbose, - MessageCacheSize = 100 - }); - _commandService = new CommandService(new CommandServiceConfig - { - DefaultRunMode = Discord.Commands.RunMode.Async, - CaseSensitiveCommands = false, -#if TESTBOT - LogLevel = LogSeverity.Critical, - ThrowOnError = true, -#else - LogLevel = LogSeverity.Debug, - ThrowOnError = false -#endif - }); + /// + /// Returns a service from the service provider + /// + public T GetService() => ServiceProvider.GetRequiredService(); + + /// + /// The interaction service + /// + public InteractionService InteractionService { get; private set; } + + /// + /// The command service + /// + public CommandService CommandService => _commandService.Value; + + /// + /// The service collection + /// + public ServiceCollection ServiceCollection => _serviceCollection.Value as ServiceCollection; + + /// + /// The service provider + /// + public ServiceProvider ServiceProvider => _serviceCollection.Value.BuildServiceProvider(); - _services = BuildServiceProvider(); - await _services.GetRequiredService().InitializeAsync(); - _services.GetRequiredService(); - _services.GetRequiredService().Initialize(); - _services.GetRequiredService(); + /// + /// The Discord Client instance + /// + public DiscordSocketClient DiscordClient => _client.Value as DiscordSocketClient; - _client.Ready += ClientReady; - _client.GuildAvailable += ClientGuildAvailable; - _client.LatencyUpdated += ClientLatencyUpdated; + /// + /// The Discord client, provided by the factory + /// + private readonly Lazy _client = new(DiscordClientFactory.CreateDiscordSocketClient); - // Begin our connection once everything is hooked up and ready to go - // Because this is async, this returns immediately (connection is handled on a separate thread by the con manager) - await _client.StartAsync().ContinueWith(async _ => + /// + /// The service provider, provided by the factory + /// + private readonly Lazy _commandService = new(CommandServiceFactory.CreateCommandService); + + /// + /// The service provider, provided by the factory + /// + private readonly Lazy _serviceCollection = new(ServiceProviderFactory.CreateServiceCollection); + + private string _token => Environment.GetEnvironmentVariable(_envTokenKey); + + + /// + /// Initializes the program and starts the bot + /// + private async Task RunAsync() + { + if (_token == null) { -#if TESTBOT - await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("TestBotToken"), validateToken: true); -#else - await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("TmlBotToken"), validateToken: true); -#endif - }); + await Console.Out.WriteLineAsync("No token environment variable was found. The bot requires one to run. Did you make sure to have set the variable?"); + await Console.Out.WriteLineAsync("If you wish to save a token, paste it and press enter:"); + var input = await Console.In.ReadLineAsync(); + Environment.SetEnvironmentVariable(_envTokenKey, input); + } + + await InstallServices(); + await InitializeServices(); + await AddEventHandlers(); + + await LoginAsync(); + await StartAsync(); Console.Title = $@"tModLoader Bot - {DateTime.Now}"; await Console.Out.WriteLineAsync($"https://discordapp.com/api/oauth2/authorize?client_id=&scope=bot"); @@ -102,11 +101,43 @@ await _client.StartAsync().ContinueWith(async _ => await Task.Delay(-1); } + private Task InstallServices() + { + ServiceCollection.AddSingleton(DiscordClient); + ServiceCollection.AddSingleton(CommandService); + return Task.CompletedTask; + } + + private async Task InitializeServices() + { + await GetService().InitializeAsync(); + GetService().Initialize(); + GetService(); + } + + private Task AddEventHandlers() + { + DiscordClient.Ready += ClientReady; + DiscordClient.GuildAvailable += ClientGuildAvailable; + DiscordClient.LatencyUpdated += ClientLatencyUpdated; + return Task.CompletedTask; + } + + private async Task StartAsync() + { + await DiscordClient.StartAsync(); + } + + private async Task LoginAsync() + { + await DiscordClient.LoginAsync(TokenType.Bot, _token, validateToken: true); + } + private async Task ClientLatencyUpdated(int i, int j) { UserStatus newUserStatus = UserStatus.Online; - switch (_client.ConnectionState) + switch (DiscordClient.ConnectionState) { case ConnectionState.Disconnected: newUserStatus = UserStatus.DoNotDisturb; @@ -116,44 +147,44 @@ private async Task ClientLatencyUpdated(int i, int j) break; } - await _client.SetStatusAsync(newUserStatus); + await DiscordClient.SetStatusAsync(newUserStatus); } private async Task ClientReady() { Ready = false; - await _client.SetGameAsync("Bot is starting..."); - await _client.SetStatusAsync(UserStatus.DoNotDisturb); + await DiscordClient.SetGameAsync("Bot is starting..."); + await DiscordClient.SetStatusAsync(UserStatus.DoNotDisturb); - BotOwner = (await _client.GetApplicationInfoAsync()).Owner; + BotOwner = (await DiscordClient.GetApplicationInfoAsync()).Owner; - await _services.GetRequiredService().SetupAsync(); - await _services.GetRequiredService().UpdateAsync(); - await _services.GetRequiredService().Initialize().Maintain(); - await _services.GetRequiredService().Initialize().Maintain(); + await GetService().SetupAsync(); + await GetService().UpdateAsync(); + await GetService().Initialize().Maintain(); + await GetService().Initialize().Maintain(); //await _reactionRoleService.Maintain(_client); - await _services.GetRequiredService().Log(new LogMessage(LogSeverity.Info, "ClientReady", "Done.")); + await GetService().Log(new LogMessage(LogSeverity.Info, "ClientReady", "Done.")); // await _client.SetGameAsync("tModLoader " + LegacyModService.tMLVersion); TODO: Report the latest stable automatically? Would need to retrieve it each launch since it changes frequently. - await _client.SetGameAsync("tModLoader"); - await ClientLatencyUpdated(_client.Latency, _client.Latency); + await DiscordClient.SetGameAsync("tModLoader"); + await ClientLatencyUpdated(DiscordClient.Latency, DiscordClient.Latency); #if !TESTBOT - var botChannel = (ISocketMessageChannel)await _client.GetChannelAsync(242228770855976960); + var botChannel = (ISocketMessageChannel)await DiscordClient.GetChannelAsync(242228770855976960); await botChannel.SendMessageAsync("Bot has started successfully."); #endif - _interactionService = new InteractionService(_client); - await _interactionService.AddModulesAsync(System.Reflection.Assembly.GetEntryAssembly(), _services); + InteractionService = new InteractionService(DiscordClient); + await InteractionService.AddModulesAsync(System.Reflection.Assembly.GetEntryAssembly(), ServiceProvider); #if TESTBOT - await _interactionService.RegisterCommandsToGuildAsync(276235094622994433); // replace this is testing on your own server. + await InteractionService.RegisterCommandsToGuildAsync(1236004871543718040); // replace this is testing on your own server. #else - await _interactionService.RegisterCommandsToGuildAsync(103110554649894912); - // await interactionService.RegisterCommandsGloballyAsync(); + await InteractionService.RegisterCommandsToGuildAsync(103110554649894912); + // await InteractionService.RegisterCommandsGloballyAsync(); #endif - _client.InteractionCreated += async interaction => + DiscordClient.InteractionCreated += async interaction => { - var ctx = new SocketInteractionContext(_client, interaction); - await _interactionService.ExecuteCommandAsync(ctx, _services); + var ctx = new SocketInteractionContext(DiscordClient, interaction); + await InteractionService.ExecuteCommandAsync(ctx, ServiceProvider); }; Ready = true; @@ -161,10 +192,10 @@ private async Task ClientReady() private async Task ClientGuildAvailable(SocketGuild arg) { - await _services.GetRequiredService().SetupAsync(); - await _services.GetRequiredService().Setup(); - await _services.GetRequiredService().Setup(); - await _services.GetRequiredService().Setup(); + await GetService().SetupAsync(); + //await ServiceProvider.GetRequiredService().SetupAsync(); + await GetService().SetupAsync(); + await GetService().SetupAsync(); return; } } diff --git a/Services/BanAppealChannelService.cs b/Services/BanAppealChannelService.cs index 33750b5..c3f9780 100644 --- a/Services/BanAppealChannelService.cs +++ b/Services/BanAppealChannelService.cs @@ -26,7 +26,7 @@ public BanAppealChannelService(IServiceProvider services) : base(services) _client.GuildMemberUpdated += HandleGuildMemberUpdated; } - internal async Task Setup() + internal async Task SetupAsync() { if (!_isSetup) _isSetup = await Task.Run(() => @@ -43,7 +43,7 @@ internal async Task Setup() private async Task HandleGuildMemberUpdated(Cacheable before, SocketGuildUser after) { - if (!await Setup()) + if (!await SetupAsync()) return; await before.GetOrDownloadAsync().ContinueWith(async task => diff --git a/Services/CrosspostService.cs b/Services/CrosspostService.cs index 509d777..d76df0e 100644 --- a/Services/CrosspostService.cs +++ b/Services/CrosspostService.cs @@ -32,7 +32,7 @@ public CrosspostService(IServiceProvider services) : base(services) private async Task _client_MessageReceived(SocketMessage socketMessage) { - if (!await Setup()) + if (!await SetupAsync()) return; if (socketMessage?.Channel?.Id != crosspostChannelId) @@ -48,7 +48,7 @@ private async Task _client_MessageReceived(SocketMessage socketMessage) await message.CrosspostAsync(); } - internal async Task Setup() + internal async Task SetupAsync() { if (!_isSetup) _isSetup = await Task.Run(async () => diff --git a/Services/LegacyModService.cs b/Services/LegacyModService.cs index 46cec3c..b0a09bc 100644 --- a/Services/LegacyModService.cs +++ b/Services/LegacyModService.cs @@ -69,7 +69,7 @@ public async Task Maintain() { await Log("Starting 1.3 maintenance"); await Log("Skipping 1.3 maintenance, too buggy."); - return; + //return; // Create dirs Directory.CreateDirectory(ModDir); diff --git a/Services/SupportChannelAutoMessageService.cs b/Services/SupportChannelAutoMessageService.cs index 41a68c7..9cf32ba 100644 --- a/Services/SupportChannelAutoMessageService.cs +++ b/Services/SupportChannelAutoMessageService.cs @@ -38,7 +38,7 @@ public SupportChannelAutoMessageService(IServiceProvider services) : base(servic private async Task _client_MessageReceived(SocketMessage message) { - if (!await Setup()) + if (!await SetupAsync()) return; if (message?.Channel?.Id != supportForumPinnedThread?.Id) @@ -47,7 +47,7 @@ private async Task _client_MessageReceived(SocketMessage message) await message.DeleteAsync(); } - internal async Task Setup() + internal async Task SetupAsync() { if (!_isSetup) _isSetup = await Task.Run(async () => @@ -67,7 +67,7 @@ internal async Task Setup() private async Task _client_ThreadCreated(SocketThreadChannel thread) { - if (!await Setup()) + if (!await SetupAsync()) return; if (thread.ParentChannel != supportForum) From abf59cefce0530708676fbd0775b70987e5abea6 Mon Sep 17 00:00:00 2001 From: Jofairden Date: Tue, 27 May 2025 16:44:43 +0200 Subject: [PATCH 3/6] Update target framework to .NET 8.0 (LTS) --- README.md | 70 ++++++++++++++++++++++--------------- tModloaderDiscordBot.csproj | 11 +++--- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index c0a4fc6..f6aaef0 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,58 @@ # tModLoader-Discord-Bot -A Discord bot written in C# using Discord.Net to serve the TMODLOADER Discord server. Uses .NET Core 2.0+ -This bot is primarily written and designed to serve the TMODLOADER community. -TMODLOADER is a piece of software [available on steam](https://store.steampowered.com/app/1281930/tModLoader/) that allows you to play [Terraria](https://terraria.org/) with mods. +A Discord bot written in C# using the Discord.Net library to serve the tModLoader Discord server. -# Testing -To test the bot, you'll need a to provide the program with valid credentials. You'll need to test the bot on your own server. To do this, first visit the [Discord Developer Portal](https://discord.com/developers/applications) and click `New Application`. Provide your bot with a name, then click `Create`. Something like "My-tModLoader-bot" will work fine. Now, click `Bot` on the left, then click `Add Bot`, then click `Yes, do it!`. You'll now need to click the `Copy` button in the `Token` section. Next, we'll need to provide those credentials to our program. To do this, in the visual studio solution explorer, right click on `tModLoaderDiscordBot` and click `Properties`. In the window that appears, click `Open debug launch profiles UI` in the `Debug` section. Now, navigate to the `Environment variables` section. Type in `TestBotToken` for the name and paste in your copied bot token into the value column. Close that window. - -We'll also need to do this with the `SteamWebAPIKey` retrieved from [Register Steam Web API Key](https://steamcommunity.com/dev/apikey). - -Next, we need to invite the bot to a server you have admin permissions in. Follow the instructions in [here](https://github.com/jagrosh/MusicBot/wiki/Adding-Your-Bot-To-Your-Server), except before clicking `copy` first click `Administrator` in the `Bot Permissions` section that appeared. +Built with .NET Core. -Now, the bot is registered to the server and the bot credentials are in the project. Now, make sure the solution configuration is set to `Debug - Test Bot` and you are good to debug. When you debug, you should see the bot appear in your server after it has finished initializing, once that is done you can test your code as usual. - -Make sure to set the token in an environment variable called 'TestBotToken' or change it in Program.cs +tModLoader is a piece of software [available on steam](https://store.steampowered.com/app/1281930/tModLoader/) that allows you to play [Terraria](https://terraria.org/) with mods. -# Can I use this bot? -No. This bot was specifically made for our TMODLOADER server. -If you wish to have a bot with certain features this bot has, the best thing you can do is join our Discord server and contact one of the developers. +## License +The default license is 'all-rights reserved'. This means, all rights to this work are reserved to its author(s). In this case, that's the tModLoader team contributors. -# License -The default license is 'all-rights reserved'. This means, all rights to this work are reserved to its author(s). In this case, that's me, Jofairden, and any other contributors. I made this bot, it is my code. The fact that it is open-source does not mean you can take my code and claim it as your own. Again, feel free to learn from it, but please be so polite to not steal it. +## Can I use this bot? +You can, but be advised to change the functionality to your needs. +The bot was written with single-server use in mind. -# Technical details -This bot is written in C# using .NET Core and the Discord.Net library. -.NET Core runs natively on linux, this allows the bot to run as 24/7 service for our server. -The Discord.Net library makes it easy to develop bots using C#, and provides many features that enhance the bot itself. -It also is built around asynchronous code design, which makes the bot itself more responsive. +That means some features may not be suitable for your server. -# Functions -Our bot's feaures are specifically designed for the TMODLOADER Discord server. +## Bot Features +Our bot's feaures are specifically designed for the tModLoader Discord server. These include but are not limited to: -1) A tag system. Tags be retrieved, edited and made global. Other users can also get global tags. Useful for storing information that is frequently given and otherwise needs to be typed. +1) A tag system. Tags be retrieved, edited and made global. Other users can also retrieve global tags. Useful for storing information that is frequently given and otherwise needs to be typed. 2) Retrieve mod information that is uploaded to the mod browser. 3) Retrieve the status of certain websites important to modders. This would include our own website and also sites such as github, our documentation etc. 4) A permission system (grant user/role based permission for commands or modules). 5) A logging service offering flexible logging options. -6) A configuration service to store settings and information of services provided by the bot. -7) A sticky role feature that allows remembering of roles even if a user leaves the server and comes back later. We use this for our softban role. +6) A configuration service to store settings and information of services provided by the bot in JSON format. +7) A sticky role feature that allows remembering of roles even if a user leaves the server and comes back later. We use this feature primarily for our softban role. 8) Anti-spam detection that will mute a user and delete their spam messages, and also will automatically kick them if they keep spammming. 9) A vote delete system that allows members to delete content they don't like to see. -# No database -This bot does not use a database. Here is why. Using a database adds a certain complexity to your application you may or may not want to deal with. For us, we haven't seen many use-cases to be using a database. It sure is fast, and probably much faster than our IO-Read/Writes, however speed of these operations is of no importance to us for now. Not using a database allows for easy data handling as IO operations are innate to the programming language itself, and making data copies (including time-based back-ups) is very easy and scalable for files. \ No newline at end of file +# Commands +The bot contains quite a lot of distinct features, checkout the Modules folder! + +# Testing +To test the bot, you'll need a to provide the program with valid credentials. You'll need to test the bot on your own server. To do this, first visit the [Discord Developer Portal](https://discord.com/developers/applications) and click `New Application`. Provide your bot with a name, then click `Create`. Something like "My-tModLoader-bot" will work fine. Now, click `Bot` on the left, then click `Add Bot`, then click `Yes, do it!`. You'll now need to click the `Copy` button in the `Token` section. Next, we'll need to provide those credentials to our program. To do this, in the visual studio solution explorer, right click on `tModLoaderDiscordBot` and click `Properties`. In the window that appears, click `Open debug launch profiles UI` in the `Debug` section. Now, navigate to the `Environment variables` section. Type in `TestBotToken` for the name and paste in your copied bot token into the value column. Close that window. + +We'll also need to do this with the `SteamWebAPIKey` retrieved from [Register Steam Web API Key](https://steamcommunity.com/dev/apikey). + +Next, we need to invite the bot to a server you have admin permissions in. Follow the instructions in [here](https://github.com/jagrosh/MusicBot/wiki/Adding-Your-Bot-To-Your-Server), except before clicking `copy` first click `Administrator` in the `Bot Permissions` section that appeared. + +Now, the bot is registered to the server and the bot credentials are in the project. Now, make sure the solution configuration is set to `Debug - Test Bot` and you are good to debug. When you debug, you should see the bot appear in your server after it has finished initializing, once that is done you can test your code as usual. + +Make sure to set the token in an environment variable called 'TestBotToken'. + +# Guild Configuration +The bot is able to handle mutliple Guild configurations at once. Each Guild get its own designated configuration, which will be stored to a JSON file locally. + +If you are debugging the application, you can find it in the debug folder: + +`..\\bin\Debug - Test Bot\netcoreapp3.1\data` + +See the GuildConfig class in de Components folder for more info. + +# Adding to Program.cas +If you intend to change `Program.cs` please use delegated / lazy properties. This helps us avoid unnecessary logic. +It is recommended to use a factory class if some class needs to be initialized. There are some examples in de Factories folder. +When making a factory class, give it a sensible "Create" function which will create and return the necessary object. \ No newline at end of file diff --git a/tModloaderDiscordBot.csproj b/tModloaderDiscordBot.csproj index af81b6b..2ed76a8 100644 --- a/tModloaderDiscordBot.csproj +++ b/tModloaderDiscordBot.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net8.0 tModloaderDiscordBot.Program linux-arm Debug;Release;Debug - Test Bot @@ -19,10 +19,11 @@ - - - - + + + + + From d3e2325d80b6666e5d505030f276047886c09ce8 Mon Sep 17 00:00:00 2001 From: Jofairden Date: Wed, 28 May 2025 21:14:04 +0200 Subject: [PATCH 4/6] MediatR --- Components/SiteStatus.cs | 2 + DiscordEventListener.cs | 36 +++++++++++++ Factories/ServiceProviderFactory.cs | 56 ++++++++++---------- Handlers/MessageReceivedHandler.cs | 15 ++++++ Notifications/MessageReceivedNotification.cs | 13 +++++ Program.cs | 26 +++------ ProgramExtensions.cs | 18 +++++++ tModloaderDiscordBot.csproj | 4 +- 8 files changed, 123 insertions(+), 47 deletions(-) create mode 100644 DiscordEventListener.cs create mode 100644 Handlers/MessageReceivedHandler.cs create mode 100644 Notifications/MessageReceivedNotification.cs create mode 100644 ProgramExtensions.cs diff --git a/Components/SiteStatus.cs b/Components/SiteStatus.cs index f13b3b1..18256ef 100644 --- a/Components/SiteStatus.cs +++ b/Components/SiteStatus.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json; @@ -64,6 +65,7 @@ public Task Revalidate() internal bool Ping() { + //new HttpClient() var request = WebRequest.Create(Address); return request.GetResponse() is HttpWebResponse response && response.StatusCode == HttpStatusCode.OK; } diff --git a/DiscordEventListener.cs b/DiscordEventListener.cs new file mode 100644 index 0000000..6b14fc5 --- /dev/null +++ b/DiscordEventListener.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using Discord.WebSocket; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using tModloaderDiscordBot.Notifications; + +namespace tModloaderDiscordBot; + +/// +/// Listens to Discord's events and creates notifications for them commands and forwards it to Mediatr +/// +public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope) +{ + private readonly CancellationToken _cancellationToken = new CancellationTokenSource().Token; + + private IMediator Mediator + { + get + { + var scope = serviceScope.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + } + } + + public async Task SetupAsync() + { + client.MessageReceived += OnMessageReceivedAsync; + await Task.CompletedTask; + } + + private Task OnMessageReceivedAsync(SocketMessage arg) + { + return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); + } +} \ No newline at end of file diff --git a/Factories/ServiceProviderFactory.cs b/Factories/ServiceProviderFactory.cs index 57b5c1d..964438a 100644 --- a/Factories/ServiceProviderFactory.cs +++ b/Factories/ServiceProviderFactory.cs @@ -1,43 +1,43 @@ -using Discord.Commands; -using Discord.WebSocket; +using MediatR; using Microsoft.Extensions.DependencyInjection; -using System; using System.Reflection; using System.Resources; using tModloaderDiscordBot.Services; namespace tModloaderDiscordBot.Factories { - internal class ServiceProviderFactory + internal static class ServiceProviderFactory { - private ServiceProviderFactory() { } + private static Assembly EntryAssembly => Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly(); - private static Assembly EntryAssembly => Assembly.GetEntryAssembly(); - private static readonly ResourceManager ResourceManager = new("tModloaderDiscordBot.Properties.Resources", EntryAssembly); + private static readonly ResourceManager ResourceManager = + new("tModloaderDiscordBot.Properties.Resources", EntryAssembly); public static IServiceCollection CreateServiceCollection() { return new ServiceCollection() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - //.AddSingleton() - // How to use resources: - //_services.GetRequiredService().GetString("key") - .AddSingleton(ResourceManager) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddMediatR(typeof(Program)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + //.AddSingleton() + // How to use resources: + //_services.GetRequiredService().GetString("key") + .AddSingleton(ResourceManager) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); } } -} +} \ No newline at end of file diff --git a/Handlers/MessageReceivedHandler.cs b/Handlers/MessageReceivedHandler.cs new file mode 100644 index 0000000..920ddc7 --- /dev/null +++ b/Handlers/MessageReceivedHandler.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using tModloaderDiscordBot.Notifications; + +namespace tModloaderDiscordBot.Handlers; + +public class MessageReceivedHandler : INotificationHandler +{ + public Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + var message = notification.Message; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Notifications/MessageReceivedNotification.cs b/Notifications/MessageReceivedNotification.cs new file mode 100644 index 0000000..0fa2ccc --- /dev/null +++ b/Notifications/MessageReceivedNotification.cs @@ -0,0 +1,13 @@ +using System; +using Discord.WebSocket; +using MediatR; + +namespace tModloaderDiscordBot.Notifications; + +/// +/// A message received event from Discord +/// +public class MessageReceivedNotification(SocketMessage message) : INotification +{ + public SocketMessage Message { get; } = message ?? throw new ArgumentNullException(nameof(message)); +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 9e7f6c1..24f8433 100644 --- a/Program.cs +++ b/Program.cs @@ -4,6 +4,7 @@ using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; using System; +using System.Reflection; using System.Threading.Tasks; using tModloaderDiscordBot.Factories; using tModloaderDiscordBot.Services; @@ -20,13 +21,15 @@ public class Program public static bool Ready; + /// + /// Starts the application + /// public static void Main(string[] args) => new Program().RunAsync().GetAwaiter().GetResult(); internal static IUser BotOwner; - /// /// Returns a service from the service provider /// @@ -72,8 +75,7 @@ public static void Main(string[] args) /// private readonly Lazy _serviceCollection = new(ServiceProviderFactory.CreateServiceCollection); - private string _token => Environment.GetEnvironmentVariable(_envTokenKey); - + private string? _token => Environment.GetEnvironmentVariable(_envTokenKey); /// /// Initializes the program and starts the bot @@ -112,7 +114,6 @@ private async Task InitializeServices() { await GetService().InitializeAsync(); GetService().Initialize(); - GetService(); } private Task AddEventHandlers() @@ -135,19 +136,7 @@ private async Task LoginAsync() private async Task ClientLatencyUpdated(int i, int j) { - UserStatus newUserStatus = UserStatus.Online; - - switch (DiscordClient.ConnectionState) - { - case ConnectionState.Disconnected: - newUserStatus = UserStatus.DoNotDisturb; - break; - case ConnectionState.Connecting: - newUserStatus = UserStatus.Idle; - break; - } - - await DiscordClient.SetStatusAsync(newUserStatus); + await DiscordClient.SetStatusAsync(DiscordClient.ConnectionState.ToUserStatus()); } private async Task ClientReady() @@ -159,6 +148,7 @@ private async Task ClientReady() BotOwner = (await DiscordClient.GetApplicationInfoAsync()).Owner; await GetService().SetupAsync(); + await GetService().SetupAsync(); await GetService().UpdateAsync(); await GetService().Initialize().Maintain(); await GetService().Initialize().Maintain(); @@ -174,7 +164,7 @@ private async Task ClientReady() #endif InteractionService = new InteractionService(DiscordClient); - await InteractionService.AddModulesAsync(System.Reflection.Assembly.GetEntryAssembly(), ServiceProvider); + await InteractionService.AddModulesAsync(Assembly.GetEntryAssembly(), ServiceProvider); #if TESTBOT await InteractionService.RegisterCommandsToGuildAsync(1236004871543718040); // replace this is testing on your own server. #else diff --git a/ProgramExtensions.cs b/ProgramExtensions.cs new file mode 100644 index 0000000..644e0e0 --- /dev/null +++ b/ProgramExtensions.cs @@ -0,0 +1,18 @@ +using Discord; + +namespace tModloaderDiscordBot; + +public static class ProgramExtensions +{ + public static UserStatus ToUserStatus(this ConnectionState state) + { + return state switch + { + ConnectionState.Connected => UserStatus.Online, + ConnectionState.Disconnected => UserStatus.DoNotDisturb, + ConnectionState.Connecting => UserStatus.Idle, + ConnectionState.Disconnecting => UserStatus.DoNotDisturb, + _ => UserStatus.Online + }; + } +} \ No newline at end of file diff --git a/tModloaderDiscordBot.csproj b/tModloaderDiscordBot.csproj index 2ed76a8..a270cf6 100644 --- a/tModloaderDiscordBot.csproj +++ b/tModloaderDiscordBot.csproj @@ -6,7 +6,8 @@ tModloaderDiscordBot.Program linux-arm Debug;Release;Debug - Test Bot - latest + 12 + enable @@ -21,6 +22,7 @@ + From 2735698cdc027cd17eb8bd045a5b979c6adf3e67 Mon Sep 17 00:00:00 2001 From: Jofairden Date: Wed, 28 May 2025 22:31:39 +0200 Subject: [PATCH 5/6] First try at interaction notifications --- ...ensions.cs => ConnectionStateExtensions.cs | 2 +- Handlers/BaseCommandHandler.cs | 39 +++++++++++++++++++ Handlers/MessageReceivedHandler.cs | 15 ------- Handlers/ModCommandHandler.cs | 29 ++++++++++++++ Interactions/ModNameAutocompleteHandler.cs | 12 ++++-- Modules/InteractionModule.cs | 39 ++++++++----------- Notifications/IInteractionNotification.cs | 11 ++++++ Notifications/InteractionNotification.cs | 14 +++++++ Notifications/ModCommandNotification.cs | 21 ++++++++++ Program.cs | 38 +++++++++--------- SocketInteractionContextExtensions.cs | 17 ++++++++ app.manifest | 11 ++++++ tModloaderDiscordBot.csproj | 5 +++ 13 files changed, 192 insertions(+), 61 deletions(-) rename ProgramExtensions.cs => ConnectionStateExtensions.cs (89%) create mode 100644 Handlers/BaseCommandHandler.cs delete mode 100644 Handlers/MessageReceivedHandler.cs create mode 100644 Handlers/ModCommandHandler.cs create mode 100644 Notifications/IInteractionNotification.cs create mode 100644 Notifications/InteractionNotification.cs create mode 100644 Notifications/ModCommandNotification.cs create mode 100644 SocketInteractionContextExtensions.cs create mode 100644 app.manifest diff --git a/ProgramExtensions.cs b/ConnectionStateExtensions.cs similarity index 89% rename from ProgramExtensions.cs rename to ConnectionStateExtensions.cs index 644e0e0..a03727e 100644 --- a/ProgramExtensions.cs +++ b/ConnectionStateExtensions.cs @@ -2,7 +2,7 @@ namespace tModloaderDiscordBot; -public static class ProgramExtensions +public static class ConnectionStateExtensions { public static UserStatus ToUserStatus(this ConnectionState state) { diff --git a/Handlers/BaseCommandHandler.cs b/Handlers/BaseCommandHandler.cs new file mode 100644 index 0000000..f326079 --- /dev/null +++ b/Handlers/BaseCommandHandler.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; +using Discord.Interactions; +using MediatR; +using tModloaderDiscordBot.Modules; +using tModloaderDiscordBot.Notifications; + +namespace tModloaderDiscordBot.Handlers; + +/// +/// A base class that can be used for command handlers. +/// Use the unpack method to unpack any notification information +/// into local variables or properties. This base class already contains the +/// IInteractionModuleBase which the command originated from. +/// +public abstract class BaseCommandHandler : InteractionModuleBase, INotificationHandler where T : IInteractionNotification +{ + protected T Command { get; private set; } + + protected CancellationToken CancellationToken { get; private set; } + + protected SocketInteractionContext Context { get; private set; } + + protected InteractionModule InteractionModule { get; private set; } + + /// + /// Use this to run the command + /// + public Task Handle(T notification, CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + Context = notification.Context; + InteractionModule = notification.InteractionModule; + Command = notification; + return Task.Run(() => RunCommand(notification), cancellationToken); + } + + protected abstract Task RunCommand(T notification); +} \ No newline at end of file diff --git a/Handlers/MessageReceivedHandler.cs b/Handlers/MessageReceivedHandler.cs deleted file mode 100644 index 920ddc7..0000000 --- a/Handlers/MessageReceivedHandler.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using tModloaderDiscordBot.Notifications; - -namespace tModloaderDiscordBot.Handlers; - -public class MessageReceivedHandler : INotificationHandler -{ - public Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) - { - var message = notification.Message; - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/Handlers/ModCommandHandler.cs b/Handlers/ModCommandHandler.cs new file mode 100644 index 0000000..7e5e0bc --- /dev/null +++ b/Handlers/ModCommandHandler.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using tModloaderDiscordBot.Modules; +using tModloaderDiscordBot.Notifications; +using tModloaderDiscordBot.Services; +using tModloaderDiscordBot.Utils; + +namespace tModloaderDiscordBot.Handlers; + +public class ModCommandHandler : BaseCommandHandler +{ + protected override async Task RunCommand(ModCommandNotification notification) + { + string? modName = notification.ModName.RemoveWhitespace(); + modName = + ModService.Mods.FirstOrDefault(m => + string.Equals(m, modName, StringComparison.CurrentCultureIgnoreCase)) ?? null; + + if (modName == null) + { + await RespondAsync("Mod with that name doesn't exist", ephemeral: true); + return; + } + + var embed = await DefaultModule.GenerateModEmbed(modName, Context.Interaction.User); + await RespondAsync(embed: embed); + } +} \ No newline at end of file diff --git a/Interactions/ModNameAutocompleteHandler.cs b/Interactions/ModNameAutocompleteHandler.cs index 3b99e19..6354ca1 100644 --- a/Interactions/ModNameAutocompleteHandler.cs +++ b/Interactions/ModNameAutocompleteHandler.cs @@ -9,11 +9,15 @@ namespace tModloaderDiscordBot.Interactions { public class ModNameAutocompleteHandler : AutocompleteHandler { - public override Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) + public override async Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) { - string userInput = autocompleteInteraction.Data.Current.Value.ToString(); - var mods = ModService.Mods.Where(m => m.Contains(userInput, StringComparison.CurrentCultureIgnoreCase)).Take(10).Select(x => new AutocompleteResult(x, x)); - return Task.FromResult(AutocompletionResult.FromSuccess(mods)); + string? userInput = autocompleteInteraction.Data.Current.Value.ToString(); + if (userInput == null) { + return AutocompletionResult.FromError(new ArgumentNullException(nameof(userInput))); + } + + var mods = ModService.Mods.Where(m => m.Contains(userInput, StringComparison.CurrentCultureIgnoreCase)).Take(25).Select(x => new AutocompleteResult(x, x)); + return AutocompletionResult.FromSuccess(mods); } } } diff --git a/Modules/InteractionModule.cs b/Modules/InteractionModule.cs index 95e1064..ae91a8c 100644 --- a/Modules/InteractionModule.cs +++ b/Modules/InteractionModule.cs @@ -1,11 +1,8 @@ -using Discord; -using Discord.Interactions; -using System; -using System.Linq; +using Discord.Interactions; using System.Threading.Tasks; -using tModloaderDiscordBot.Services; using System.Net; -using tModloaderDiscordBot.Utils; +using MediatR; +using Microsoft.Extensions.DependencyInjection; using tModloaderDiscordBot.Interactions; namespace tModloaderDiscordBot.Modules @@ -14,22 +11,19 @@ namespace tModloaderDiscordBot.Modules /// For commands using the interaction framework /// https://discordnet.dev/guides/int_framework/intro.html /// - public class InteractionModule : InteractionModuleBase + public class InteractionModule( + IServiceScopeFactory serviceScope + ) : InteractionModuleBase { + /// + /// The MediatR instance returned by dependency injection + /// + private readonly IMediator _mediator = serviceScope.CreateScope().ServiceProvider.GetRequiredService(); + [SlashCommand("mod", "Shows info about a mod")] - public async Task Mod([Discord.Interactions.Summary("mod-name"), Autocomplete(typeof(ModNameAutocompleteHandler))] string modName) + public async Task Mod([Summary("mod-name"), Autocomplete(typeof(ModNameAutocompleteHandler))] string modName) { - modName = modName.RemoveWhitespace(); - modName = ModService.Mods.FirstOrDefault(m => string.Equals(m, modName, StringComparison.CurrentCultureIgnoreCase)); - if (modName == null) - { - await RespondAsync($"Mod with that name doesn't exist", ephemeral: true); - return; - } - - Embed embed = await DefaultModule.GenerateModEmbed(modName, Context.Interaction.User); - await RespondAsync("", embed: embed); - //await RespondAsync($"Your choice: {parameterWithAutocompletion}", ephemeral: true); + await _mediator.Publish(Context.ToModCommand(this, modName)); } [SlashCommand("ws", "Generates a search for a term in tModLoader wiki")] @@ -37,9 +31,8 @@ public async Task WikiSearch(string searchTerm) { searchTerm = searchTerm.Trim(); string encoded = WebUtility.UrlEncode(searchTerm); - await RespondAsync($"tModLoader Wiki results for {searchTerm}: "); + await RespondAsync( + $"tModLoader Wiki results for {searchTerm}: "); } } - - -} +} \ No newline at end of file diff --git a/Notifications/IInteractionNotification.cs b/Notifications/IInteractionNotification.cs new file mode 100644 index 0000000..9e7ee54 --- /dev/null +++ b/Notifications/IInteractionNotification.cs @@ -0,0 +1,11 @@ +using Discord.Interactions; +using MediatR; +using tModloaderDiscordBot.Modules; + +namespace tModloaderDiscordBot.Notifications; + +public interface IInteractionNotification : INotification +{ + public SocketInteractionContext Context { get; } + public InteractionModule InteractionModule { get; } +} \ No newline at end of file diff --git a/Notifications/InteractionNotification.cs b/Notifications/InteractionNotification.cs new file mode 100644 index 0000000..95fddf6 --- /dev/null +++ b/Notifications/InteractionNotification.cs @@ -0,0 +1,14 @@ +using System.Threading; +using Discord.Interactions; +using tModloaderDiscordBot.Modules; + +namespace tModloaderDiscordBot.Notifications; + +public class InteractionNotification( + SocketInteractionContext context, + InteractionModule interactionModule +) : IInteractionNotification +{ + public SocketInteractionContext Context { get; } = context; + public InteractionModule InteractionModule { get; } = interactionModule; +} \ No newline at end of file diff --git a/Notifications/ModCommandNotification.cs b/Notifications/ModCommandNotification.cs new file mode 100644 index 0000000..7ec4cb7 --- /dev/null +++ b/Notifications/ModCommandNotification.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using Discord.Interactions; +using Discord.WebSocket; +using tModloaderDiscordBot.Modules; + +namespace tModloaderDiscordBot.Notifications; + +/// +/// A mod command being executed +/// +public class ModCommandNotification( + SocketInteractionContext ctx, + InteractionModule interactionModule, + string mod +) : InteractionNotification(ctx, interactionModule) +{ + public string ModName { get; } = mod; + + public ISocketMessageChannel Channel { get; } = ctx.Channel ?? throw new ArgumentNullException(nameof(Channel)); +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 24f8433..26a2234 100644 --- a/Program.cs +++ b/Program.cs @@ -33,7 +33,7 @@ public static void Main(string[] args) /// /// Returns a service from the service provider /// - public T GetService() => ServiceProvider.GetRequiredService(); + public T GetService() where T : notnull => ServiceProvider.GetRequiredService(); /// /// The interaction service @@ -73,9 +73,10 @@ public static void Main(string[] args) /// /// The service provider, provided by the factory /// - private readonly Lazy _serviceCollection = new(ServiceProviderFactory.CreateServiceCollection); + private readonly Lazy _serviceCollection = + new(ServiceProviderFactory.CreateServiceCollection); - private string? _token => Environment.GetEnvironmentVariable(_envTokenKey); + private string? _token => Environment.GetEnvironmentVariable(_envTokenKey, EnvironmentVariableTarget.Machine); /// /// Initializes the program and starts the bot @@ -84,10 +85,11 @@ private async Task RunAsync() { if (_token == null) { - await Console.Out.WriteLineAsync("No token environment variable was found. The bot requires one to run. Did you make sure to have set the variable?"); + await Console.Out.WriteLineAsync( + "No token environment variable was found. The bot requires one to run. Did you make sure to have set the variable?"); await Console.Out.WriteLineAsync("If you wish to save a token, paste it and press enter:"); var input = await Console.In.ReadLineAsync(); - Environment.SetEnvironmentVariable(_envTokenKey, input); + Environment.SetEnvironmentVariable(_envTokenKey, input, EnvironmentVariableTarget.Machine); } await InstallServices(); @@ -158,35 +160,35 @@ private async Task ClientReady() // await _client.SetGameAsync("tModLoader " + LegacyModService.tMLVersion); TODO: Report the latest stable automatically? Would need to retrieve it each launch since it changes frequently. await DiscordClient.SetGameAsync("tModLoader"); await ClientLatencyUpdated(DiscordClient.Latency, DiscordClient.Latency); -#if !TESTBOT - var botChannel = (ISocketMessageChannel)await DiscordClient.GetChannelAsync(242228770855976960); - await botChannel.SendMessageAsync("Bot has started successfully."); -#endif +// #if !TESTBOT +// var botChannel = (ISocketMessageChannel)await DiscordClient.GetChannelAsync(242228770855976960); +// await botChannel.SendMessageAsync("Bot has started successfully."); +// #endif InteractionService = new InteractionService(DiscordClient); await InteractionService.AddModulesAsync(Assembly.GetEntryAssembly(), ServiceProvider); #if TESTBOT await InteractionService.RegisterCommandsToGuildAsync(1236004871543718040); // replace this is testing on your own server. #else - await InteractionService.RegisterCommandsToGuildAsync(103110554649894912); + await InteractionService.RegisterCommandsToGuildAsync(1236004871543718040); // await InteractionService.RegisterCommandsGloballyAsync(); #endif - DiscordClient.InteractionCreated += async interaction => - { - var ctx = new SocketInteractionContext(DiscordClient, interaction); - await InteractionService.ExecuteCommandAsync(ctx, ServiceProvider); - }; - + DiscordClient.InteractionCreated += OnDiscordClientOnInteractionCreated; Ready = true; } + private async Task OnDiscordClientOnInteractionCreated(SocketInteraction interaction) + { + var ctx = new SocketInteractionContext(DiscordClient, interaction); + await InteractionService.ExecuteCommandAsync(ctx, ServiceProvider); + } + private async Task ClientGuildAvailable(SocketGuild arg) { await GetService().SetupAsync(); //await ServiceProvider.GetRequiredService().SetupAsync(); await GetService().SetupAsync(); await GetService().SetupAsync(); - return; } } -} +} \ No newline at end of file diff --git a/SocketInteractionContextExtensions.cs b/SocketInteractionContextExtensions.cs new file mode 100644 index 0000000..437ac76 --- /dev/null +++ b/SocketInteractionContextExtensions.cs @@ -0,0 +1,17 @@ +using Discord.Interactions; +using tModloaderDiscordBot.Modules; +using tModloaderDiscordBot.Notifications; + +namespace tModloaderDiscordBot; + +public static class SocketInteractionContextExtensions +{ + public static ModCommandNotification ToModCommand( + this SocketInteractionContext ctx, + InteractionModule interactionModule, + string mod + ) + { + return new ModCommandNotification(ctx, interactionModule, mod); + } +} \ No newline at end of file diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..46d2ace --- /dev/null +++ b/app.manifest @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/tModloaderDiscordBot.csproj b/tModloaderDiscordBot.csproj index a270cf6..f1e4df8 100644 --- a/tModloaderDiscordBot.csproj +++ b/tModloaderDiscordBot.csproj @@ -8,12 +8,17 @@ Debug;Release;Debug - Test Bot 12 enable + false TRACE;TESTBOT false + + + app.manifest + TRACE From d82f217e1c4c9e8dd49e8d308ec4b237de691b1a Mon Sep 17 00:00:00 2001 From: Jofairden Date: Wed, 28 May 2025 22:57:26 +0200 Subject: [PATCH 6/6] Working now --- Handlers/BaseCommandHandler.cs | 32 ++++++++++++++++++++++++++------ Handlers/ModCommandHandler.cs | 12 ++++++++---- Services/ModService.cs | 3 ++- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Handlers/BaseCommandHandler.cs b/Handlers/BaseCommandHandler.cs index f326079..e7ca511 100644 --- a/Handlers/BaseCommandHandler.cs +++ b/Handlers/BaseCommandHandler.cs @@ -1,6 +1,8 @@ using System.Threading; using System.Threading.Tasks; +using Discord; using Discord.Interactions; +using Discord.WebSocket; using MediatR; using tModloaderDiscordBot.Modules; using tModloaderDiscordBot.Notifications; @@ -9,18 +11,31 @@ namespace tModloaderDiscordBot.Handlers; /// /// A base class that can be used for command handlers. -/// Use the unpack method to unpack any notification information -/// into local variables or properties. This base class already contains the -/// IInteractionModuleBase which the command originated from. +/// This base class already unpacks some useful variables, such as +/// the Context property, which can be used to reply using the interaction. /// -public abstract class BaseCommandHandler : InteractionModuleBase, INotificationHandler where T : IInteractionNotification +public abstract class BaseCommandHandler : INotificationHandler where T : IInteractionNotification { + /// + /// The command (notification) that was ran) + /// protected T Command { get; private set; } protected CancellationToken CancellationToken { get; private set; } - protected SocketInteractionContext Context { get; private set; } - + /// + /// The context of the interaction + /// + protected SocketInteractionContext Context { get; private set; } + + /// + /// The interaction, which can be replied to + /// + protected SocketInteraction Interaction { get; private set; } + + /// + /// The interaction module this command was ran in + /// protected InteractionModule InteractionModule { get; private set; } /// @@ -32,8 +47,13 @@ public Task Handle(T notification, CancellationToken cancellationToken) Context = notification.Context; InteractionModule = notification.InteractionModule; Command = notification; + Interaction = notification.InteractionModule.Context.Interaction; + if (cancellationToken.IsCancellationRequested) return Task.CompletedTask; return Task.Run(() => RunCommand(notification), cancellationToken); } + /// + /// Runs the command + /// protected abstract Task RunCommand(T notification); } \ No newline at end of file diff --git a/Handlers/ModCommandHandler.cs b/Handlers/ModCommandHandler.cs index 7e5e0bc..cd854b0 100644 --- a/Handlers/ModCommandHandler.cs +++ b/Handlers/ModCommandHandler.cs @@ -1,4 +1,5 @@ -using System; +using Discord.WebSocket; +using System; using System.Linq; using System.Threading.Tasks; using tModloaderDiscordBot.Modules; @@ -8,6 +9,9 @@ namespace tModloaderDiscordBot.Handlers; +/// +/// Executes the mod command, which replies with an embed +/// public class ModCommandHandler : BaseCommandHandler { protected override async Task RunCommand(ModCommandNotification notification) @@ -19,11 +23,11 @@ protected override async Task RunCommand(ModCommandNotification notification) if (modName == null) { - await RespondAsync("Mod with that name doesn't exist", ephemeral: true); + await Interaction.RespondAsync("Mod with that name doesn't exist", ephemeral: true); return; } - var embed = await DefaultModule.GenerateModEmbed(modName, Context.Interaction.User); - await RespondAsync(embed: embed); + var embed = await DefaultModule.GenerateModEmbed(modName, Context.Interaction.User as SocketUser); + await Interaction.RespondAsync(embed: embed); } } \ No newline at end of file diff --git a/Services/ModService.cs b/Services/ModService.cs index 4a4a78c..9400560 100644 --- a/Services/ModService.cs +++ b/Services/ModService.cs @@ -75,8 +75,9 @@ public async Task Maintain() } // Needs to maintain data + if(false) //if (dateDiff == TimeSpan.MinValue || dateDiff.TotalHours > 5.99d) - if (todayUTC.Date != savedBinaryDate.Date) + //if (todayUTC.Date != savedBinaryDate.Date) { //await Log($"Maintenance determined: over 6 hours. Updating..."); await Log($"Maintenance determined: new day. Updating...");