using Catalogger.Backend.Bot; using Catalogger.Backend.Bot.Commands; using Catalogger.Backend.Bot.Responders.Messages; using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Cache.RedisCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Database.Redis; using Catalogger.Backend.Services; using Microsoft.EntityFrameworkCore; using NodaTime; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Services; using Remora.Discord.Gateway.Extensions; using Remora.Discord.Interactivity.Services; using Remora.Rest.Core; using Serilog; using Serilog.Events; using Serilog.Sinks.SystemConsole.Themes; namespace Catalogger.Backend.Extensions; public static class StartupExtensions { /// /// Adds Serilog to this service collection. This method also initializes Serilog, so it should be called as early as possible, before any log calls. /// public static WebApplicationBuilder AddSerilog( this WebApplicationBuilder builder, Config config ) { var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Is(config.Logging.LogEventLevel) // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. // Serilog doesn't disable the built-in logs, so we do it here. .MinimumLevel.Override("Microsoft", LogEventLevel.Information) .MinimumLevel.Override( "Microsoft.EntityFrameworkCore.Database.Command", config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal ) .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) // The default theme doesn't support light mode .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen); // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. builder.Services.AddSerilog().AddSingleton(Log.Logger = logCfg.CreateLogger()); return builder; } public static Config AddConfiguration(this WebApplicationBuilder builder) { builder.Configuration.Sources.Clear(); builder.Configuration.AddConfiguration(); var config = builder.Configuration.Get() ?? new(); builder.Services.AddSingleton(config); return config; } public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder) { var file = Environment.GetEnvironmentVariable("CATALOGGER_CONFIG_FILE") ?? "config.ini"; return builder .SetBasePath(Directory.GetCurrentDirectory()) .AddIniFile(file, optional: false, reloadOnChange: false) .AddEnvironmentVariables(); } public static IServiceCollection AddCustomServices(this IServiceCollection services) => services .AddSingleton(SystemClock.Instance) .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddScoped() .AddSingleton() .AddSingleton() .AddScoped() .AddSingleton() .AddSingleton() .AddSingleton(InMemoryDataService.Instance) .AddSingleton() .AddHostedService(serviceProvider => serviceProvider.GetRequiredService() ); public static IHostBuilder AddShardedDiscordService( this IHostBuilder builder, Func tokenFactory ) => builder.ConfigureServices( (_, services) => services .AddDiscordGateway(tokenFactory) .AddSingleton() .AddHostedService() ); public static IServiceCollection MaybeAddRedisCaches( this IServiceCollection services, Config config ) { if (config.Database.Redis == null) { return services .AddSingleton() .AddSingleton() .AddSingleton(); } return services .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); } public static async Task Initialize(this WebApplication app) { await using var scope = app.Services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().ForContext(); logger.Information("Starting Catalogger.NET"); CataloggerMetrics.Startup = scope .ServiceProvider.GetRequiredService() .GetCurrentInstant(); await using (var db = scope.ServiceProvider.GetRequiredService()) { var migrationCount = (await db.Database.GetPendingMigrationsAsync()).Count(); if (migrationCount != 0) { logger.Information("Applying {Count} database migrations", migrationCount); await db.Database.MigrateAsync(); } else logger.Information("There are no pending migrations"); } var config = scope.ServiceProvider.GetRequiredService(); var slashService = scope.ServiceProvider.GetRequiredService(); if (config.Discord.ApplicationId == 0) { logger.Warning( "Application ID not set in config. Fetching and setting it now, but for future restarts, please add it to config.ini as Discord.ApplicationId." ); var restApi = scope.ServiceProvider.GetRequiredService(); var application = await restApi.GetCurrentApplicationAsync().GetOrThrow(); config.Discord.ApplicationId = application.ID.ToUlong(); logger.Information( "Current application ID is {ApplicationId}", config.Discord.ApplicationId ); } if (config.Discord.SyncCommands) { if (config.Discord.CommandsGuildId != null) { logger.Information( "Syncing application commands with guild {GuildId}", config.Discord.CommandsGuildId ); await slashService.UpdateSlashCommandsAsync( guildID: DiscordSnowflake.New(config.Discord.CommandsGuildId.Value) ); } else { logger.Information("Syncing application commands globally"); await slashService.UpdateSlashCommandsAsync(); } } else logger.Information( "Not syncing slash commands, Discord.SyncCommands is false or unset" ); } }