using Catalogger.Backend.Bot.Commands; using Catalogger.Backend.Bot.Responders; using Catalogger.Backend.Cache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Queries; 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.Interactivity.Services; using Remora.Rest.Core; using Serilog; using Serilog.Events; 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) { var config = builder.Configuration.Get() ?? new(); 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) .WriteTo.Console(); // 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() .AddScoped() .AddScoped() .AddSingleton() .AddSingleton() .AddSingleton(InMemoryDataService.Instance); 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"); 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"); } }