Skip to content

feat: full Dependency Injection support #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 30 additions & 62 deletions BabbleBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,77 +15,50 @@ internal class BabbleBot
internal static ServiceProvider ServiceProvider { get; private set; }
private readonly DiscordSocketClient _client;
private readonly Config _config;
private readonly ILogger<BabbleBot> _logger;




public BabbleBot(string logPath, string configFile)
{
// Generate a unique log file name based on the current date and time
Directory.CreateDirectory(logPath);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var logFilePath = Path.Combine(logPath, $"bot_{timestamp}.log");

ServiceProvider = new ServiceCollection()
.AddLogging((loggingBuilder) => loggingBuilder
.AddConsole()
.AddDebug()
.SetMinimumLevel(LogLevel.Debug)
.AddFile(Path.Combine(logPath, "latest.log"), append: false))
.BuildServiceProvider();
_logger = ServiceProvider.GetService<ILoggerFactory>()!.CreateLogger<BabbleBot>();
var services = new ServiceCollection();
services.AddLogging((loggingBuilder) => loggingBuilder
.AddConsole()
.AddDebug()
.SetMinimumLevel(LogLevel.Debug)
.AddFile(Path.Combine(logPath, "latest.log"), append: false));

var discordSocketConfig = new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.All,
UseInteractionSnowflakeDate = false, // Don't timeout if an order lookup takes more than 3 seconds
};
services.AddSingleton<Config>(provider => {
var logger = provider.GetService<ILoggerFactory>()!.CreateLogger<BabbleBot>();
var config = ConfigLoader.ResolveConfig(configFile, logger);
if (config == null)
{
Environment.Exit(0); // no config, nothing to do
}
return config;
});

_client = new DiscordSocketClient(discordSocketConfig);
_client.SetStatusAsync(UserStatus.Online);
_client.Log += DiscordClientLogged;
services.AddSingleton<DiscordSocketConfig>(DiscordSocketFactory.CreateSocketConfig);
services.AddSingleton<DiscordSocketClient>(DiscordSocketFactory.CreateSocketClient);

if (!File.Exists(configFile))
{
// Create default config
_config = new Config();
File.WriteAllText(configFile, JsonConvert.SerializeObject(_config, Formatting.Indented));
_logger.LogCritical("Config", "Config not found! Please assign a valid token.");
return;
}
services.AddSingleton<ChatMessageSender>();
services.AddSingleton<VerificationMessageSender>();
services.AddSingleton<SlashCommandSender>();

_config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(configFile))!;
InitializeMessagers();
}
ServiceProvider = services.BuildServiceProvider()!;

private void InitializeMessagers()
{
// Get all types that inherit from Messager
var messagerTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(Messager)));
_client = ServiceProvider.GetService<DiscordSocketClient>()!;
_config = ServiceProvider.GetService<Config>()!;

foreach (var type in messagerTypes)
{
try
{
// Find constructor that takes Config, DiscordSocketClient and ILogger
var constructor = type.GetConstructor(new[] { typeof(Config), typeof(DiscordSocketClient), typeof(ILogger) });
if (constructor != null)
{
var messager = (Messager)constructor.Invoke(new object[] { _config, _client, _logger });
_logger.LogInformation($"Initialized {type.Name}");
}
else
{
_logger.LogWarning($"Failed to find appropriate constructor for {type.Name}");
}
}
catch (Exception ex)
{
_logger.LogError($"Failed to initialize {type.Name}: {ex.Message}");
}
}
ServiceProvider.GetService<ChatMessageSender>();
ServiceProvider.GetService<VerificationMessageSender>();
ServiceProvider.GetService<SlashCommandSender>();
}

public async Task MainAsync()
{
await _client.LoginAsync(TokenType.Bot, _config.DiscordToken);
Expand All @@ -94,9 +67,4 @@ public async Task MainAsync()
// Block this task until the program is closed.
await Task.Delay(-1);
}

private async Task DiscordClientLogged(Discord.LogMessage message)
{
_logger.LogInformation(message.ToString());
}
}
2 changes: 2 additions & 0 deletions Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ internal class Config
public string ShopifyToken { get; set; } = "";
public string ShopifySite { get; } = "4fac42-f0.myshopify.com";
public int FuzzThreshold { get; } = 60;
public ulong guildId = 0;
public ulong[] admins = Array.Empty<ulong>();
}
44 changes: 44 additions & 0 deletions ConfigLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BabbleBot
{
class ConfigLoader
{

public static Config? ResolveConfig(string configFile, ILogger logger)
{
Config? config;
if (!File.Exists(configFile))
{
// Create default config
config = new Config();
// lets assume this doesn't throw. if it does we better crash anyway
File.WriteAllText(configFile, JsonConvert.SerializeObject(config, Formatting.Indented));

logger.LogCritical("Config not found! Please assign valid values.");
return null;
}

try
{
config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(configFile));
if (config == null)
{
logger.LogCritical("Config file deserialization error");
return null;
}
logger.LogInformation("Config file load succesfull");
return config;
} catch (Exception ex) {
logger.LogCritical("Config file deserialization error: {}", ex.Message);
return null;
}
}
}
}
45 changes: 45 additions & 0 deletions DiscordSocketFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Discord;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BabbleBot
{
internal class DiscordSocketFactory
{
#pragma warning disable IDE0060 // Remove unused parameter. DI Service Provider wants to have this argument present
public static DiscordSocketConfig CreateSocketConfig(IServiceProvider provider)
#pragma warning restore IDE0060 // Remove unused parameter
{
var discordSocketConfig = new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.All,
UseInteractionSnowflakeDate = false, // Don't timeout if an order lookup takes more than 3 seconds
};
return discordSocketConfig;
}

public static DiscordSocketClient CreateSocketClient(IServiceProvider provider)
{
var logger = provider.GetRequiredService<ILogger<DiscordSocketClient>>();
var discordSocketConfig = provider.GetRequiredService<DiscordSocketConfig>();

DiscordSocketClient client;
client = new DiscordSocketClient(discordSocketConfig);
client.SetStatusAsync(UserStatus.Online);
client.Log += async (message) =>
{
logger.LogInformation("{}", message.ToString());
await Task.CompletedTask;
};

return client;
}

}
}
6 changes: 3 additions & 3 deletions Messagers/ChatMessageSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ internal class ChatMessageSender : Messager
{
private const string HelpCommandPrefix = "!";

public ChatMessageSender(Config config, DiscordSocketClient client, ILogger logger) : base(config, client, logger)
public ChatMessageSender(Config config, DiscordSocketClient client, ILogger<ChatMessageSender> logger) : base(config, client, logger)
{
Client.MessageReceived += MessageReceivedAsync;
LoadResponses();
Expand All @@ -21,7 +21,7 @@ private async Task MessageReceivedAsync(SocketMessage message)
return;

// Admin commands
if (ADMIN_WHITELIST_ID.Contains(message.Author.Id))
if (Config.admins.Contains(message.Author.Id))
{
if (message.Content.ToLower().Trim() == $"{HelpCommandPrefix}{ReloadCommand}") // !reload
{
Expand All @@ -33,7 +33,7 @@ private async Task MessageReceivedAsync(SocketMessage message)

if (message.Content.StartsWith(HelpCommandPrefix))
{
var command = message.Content.Substring(HelpCommandPrefix.Length).Trim();
var command = message.Content[HelpCommandPrefix.Length..].Trim();
var response = GetHelpResponse(command.ToLower());
await SendResponseMessageAsync(message.Channel, response);
}
Expand Down
31 changes: 17 additions & 14 deletions Messagers/Messager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,50 @@ internal abstract class Messager
public Config Config { get; set; }
public DiscordSocketClient Client { get; set; }
public Dictionary<string, ResponseMessage> Responses;
protected const ulong BabbleGuild = 974302302179557416;
protected ILogger Logger;

/// <summary>
/// Discord user IDs for dfgHiatus, RamesTheGeneric, and SummerSky
/// </summary>
public static readonly ulong[] ADMIN_WHITELIST_ID = {
346338830011596800UL,
199983920639377410UL,
282909752042717194UL,
};

public static ResponseMessage DefaultResponse = new()
{
Messages = new[]
Messages = new[]
{
new Message { Content = "Sorry, I don't have help information for that command." }
}
};

public Messager(Config config, DiscordSocketClient client, ILogger logger)
public Messager(Config config, DiscordSocketClient client, ILogger<Messager> logger)
{
Config = config;
Client = client;
Logger = logger;

Responses = new Dictionary<string, ResponseMessage>();
LoadResponses();
}

protected void LoadResponses()
protected Dictionary<string, ResponseMessage>? LoadResponses()
{
const string ResponsesPath = "responses.json";
const string DefaultIdentifier = "default";

var json = File.ReadAllText(ResponsesPath);
Responses = JsonConvert.DeserializeObject<Dictionary<string, ResponseMessage>>(json)!;
try
{
var json = File.ReadAllText(ResponsesPath);
var responses = JsonConvert.DeserializeObject<Dictionary<string, ResponseMessage>>(json)!;

return responses;
}
catch (Exception ex)
{
Logger.LogCritical("Error loading responses: {}", ex.Message);
}
// Preload values
if (Responses.TryGetValue(DefaultIdentifier, out var defaultResponse))
{
DefaultResponse = defaultResponse;
}

return null;
}

protected ResponseMessage GetHelpResponse(string command)
Expand Down
6 changes: 3 additions & 3 deletions Messagers/SlashCommandSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace BabbleBot.Messagers;

internal class SlashCommandSender : Messager
{
public SlashCommandSender(Config config, DiscordSocketClient client, ILogger logger) : base(config, client, logger)
public SlashCommandSender(Config config, DiscordSocketClient client, ILogger<SlashCommandSender> logger) : base(config, client, logger)
{
Client.Ready += Client_Ready;
Client.SlashCommandExecuted += SlashCommandHandler;
Expand All @@ -31,10 +31,10 @@ private async Task Client_Ready()
{
await Client.CreateGlobalApplicationCommandAsync(command.Build());
}
catch (ApplicationCommandException exception)
catch (HttpException exception)
{
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
Logger.LogCritical("Slash Commands", json);
Logger.LogCritical("Slash Commands {}", json);
}
}
}
Expand Down
Loading