using BlueLaminate.Core.Listings; using BlueLaminate.Core.Options; using BlueLaminate.Core.Skins; using BlueLaminate.EFCore.DependencyInjection; using BlueLaminate.Scraper.Browser; using BlueLaminate.Scraper.CsFloat; using BlueLaminate.Scraper.CsMoney; using BlueLaminate.Scraper.Proxies; using BlueLaminate.Scraper.Skins; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BlueLaminate.Core.DependencyInjection; /// /// The single composition root for BlueLaminate's application logic. Any frontend /// — the CLI today, a web UI later — wires up the whole stack with one call: /// services.AddBlueLaminateCore(configuration). Nothing about the database, /// the CSFloat client, or the sweep/sync services is duplicated per host. /// public static class ServiceCollectionExtensions { private const string CsFloatHttpClient = "csfloat"; private const string CatalogHttpClient = "catalog"; public static IServiceCollection AddBlueLaminateCore( this IServiceCollection services, IConfiguration configuration) { var connectionString = configuration.GetConnectionString("SkinTracker") ?? throw new InvalidOperationException( "Connection string 'SkinTracker' is not configured. Set it via user secrets (dev) " + "or the ConnectionStrings__SkinTracker environment variable (prod)."); // Database (DbContext is registered scoped by the EFCore extension). services.AddSkinTrackerData(connectionString); // Options bound from configuration. The CsFloat API key falls back to the // legacy CSFLOAT_API_KEY environment variable so existing setups keep working. services.AddOptions() .Bind(configuration.GetSection(CsFloatOptions.SectionName)) .Configure(o => { if (string.IsNullOrWhiteSpace(o.ApiKey)) { o.ApiKey = configuration["CSFLOAT_API_KEY"]; } }) .ValidateDataAnnotations() .ValidateOnStart(); services.AddOptions() .Bind(configuration.GetSection(SkinCatalogOptions.SectionName)); services.AddOptions() .Bind(configuration.GetSection(SweepOptions.SectionName)); services.AddOptions() .Bind(configuration.GetSection(CsMoneyOptions.SectionName)); // Typed-handler pooling via IHttpClientFactory; clients are scoped so a // command's handler and the service it drives share one instance (and thus // the same LastRateLimit) within a single request scope. services.AddHttpClient(CsFloatHttpClient, ConfigureHttpClient); services.AddHttpClient(CatalogHttpClient, ConfigureHttpClient); services.AddScoped(sp => new CsFloatListingsClient( sp.GetRequiredService().CreateClient(CsFloatHttpClient), sp.GetRequiredService>().Value, sp.GetRequiredService>())); services.AddScoped(sp => new SkinCatalogClient( sp.GetRequiredService().CreateClient(CatalogHttpClient), sp.GetRequiredService>().Value)); // Residential proxy provider (IPRoyal). Credentials come from configuration // — IPROYAL_USERNAME / IPROYAL_PASSWORD env vars in practice. Resolution // throws a clear error only when a proxy-using command actually needs it, so // API-only commands (sync, fetch) run without proxy creds configured. services.AddSingleton(sp => { var username = configuration["IPROYAL_USERNAME"]; var password = configuration["IPROYAL_PASSWORD"]; if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { throw new InvalidOperationException( "IPRoyal credentials are not configured. Set IPROYAL_USERNAME and " + "IPROYAL_PASSWORD (env vars or user secrets) before running a proxy command."); } return new IpRoyalProxyProvider(username, password); }); // cs.money is driven through a real, non-headless browser (Selenium/Edge, // zero CDP) routed through a local forwarding proxy that chains to the // residential gateway, not an HttpClient. services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(sp => new CsMoneyCaptureService( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>().Value, sp.GetRequiredService>())); // Application services (constructor injection; DbContext keeps them scoped). services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } private static void ConfigureHttpClient(HttpClient http) { http.Timeout = TimeSpan.FromMinutes(2); http.DefaultRequestHeaders.UserAgent.ParseAdd("BlueLaminate"); } }