diff --git a/src/dotnet-nugetize/SponsorLink.cs b/src/dotnet-nugetize/SponsorLink.cs deleted file mode 100644 index 637813fb..00000000 --- a/src/dotnet-nugetize/SponsorLink.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.NetworkInformation; -using System.Numerics; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Spectre.Console; - -namespace NuGetize; - -/// -/// The kind of SponsorLink diagnostic to report. -/// -public enum DiagnosticKind -{ - /// - /// The SponsorLink GitHub is not installed on the user's personal account. - /// - AppNotInstalled, - /// - /// The user is not sponsoring the given sponsor account. - /// - UserNotSponsoring, - /// - /// The user has the SponsorLink GitHub app installed and is sponsoring the given sponsor account. - /// - Thanks -} - -class SponsorLink -{ - const int DefaultMaxPause = 4000; - const int DefaultQuietDays = 15; - static TimeSpan NetworkTimeout { get; } = TimeSpan.FromSeconds(1); - - static readonly DateTime installTime = File.GetCreationTime(Assembly.GetExecutingAssembly().ManifestModule.FullyQualifiedName); - static readonly Random rnd = new(); - - readonly HttpClient http; - readonly string sponsorable; - readonly string product; - readonly string packageId; - readonly string version; - readonly int pauseMin; - readonly int pauseMax; - readonly int quietDays; - - public static async Task CreateAsync( - string sponsorable, string product, - string? packageId = default, - string? version = default, - int pauseMin = 0, - int pauseMax = DefaultMaxPause, - int? quietDays = default) - { - var proxy = WebRequest.GetSystemWebProxy(); - var useProxy = !proxy.IsBypassed(new Uri("https://cdn.devlooped.com")); - - HttpMessageHandler handler; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) - { - // When running on Windows + .NET Framework, this guarantees proper proxy settings behavior automatically - handler = new WinHttpHandler - { - ReceiveDataTimeout = NetworkTimeout, - ReceiveHeadersTimeout = NetworkTimeout, - SendTimeout = NetworkTimeout - }; - } - else if (useProxy) - { - handler = new HttpClientHandler - { - UseProxy = true, - Proxy = proxy, - DefaultProxyCredentials = CredentialCache.DefaultCredentials - }; - } - else - { - handler = new HttpClientHandler(); - } - - var http = new HttpClient(handler) - { - // Customize network timeout so we don't become unusable when target is - // unreachable (i.e. a proxy prevents access or misconfigured) - Timeout = NetworkTimeout - }; - - if (quietDays == null) - { - try - { - // Reads settings from storage, best-effort - var ini = await http.GetStringAsync("https://cdn.devlooped.com/sponsorlink/settings.ini"); - var values = ini - .Split(new[] { "\r\n", "\r" }, StringSplitOptions.RemoveEmptyEntries) - .Where(x => x[0] != '#') - .Select(x => x.Split(new[] { '=' }, 2)) - .ToDictionary(x => x[0].Trim(), x => x[1].Trim()); - - if (values.TryGetValue("quiet", out var value) && int.TryParse(value, out var days)) - quietDays = days; - else - quietDays = DefaultQuietDays; - } - catch - { - quietDays = DefaultQuietDays; - } - } - - return new SponsorLink(http, sponsorable, product, - packageId ?? product, version, pauseMin, pauseMax, quietDays.Value); - } - - public SponsorLink(HttpClient http, string sponsorable, string product, string packageId, string? version, int pauseMin, int pauseMax, int quietDays) - => (this.http, this.sponsorable, this.product, this.packageId, this.version, this.pauseMin, this.pauseMax, this.quietDays) - = (http, sponsorable, product, packageId, version, pauseMin, pauseMax, quietDays); - - public async Task CheckAsync(string projectDir) - { - // If there is no network at all, don't do anything. - if (!NetworkInterface.GetIsNetworkAvailable()) - return default; - - var email = GetEmail(Path.GetDirectoryName(projectDir)); - // No email configured in git. Weird. - if (string.IsNullOrEmpty(email)) - return default; - - var data = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(email)); - var hash = Base62.Encode(BigInteger.Abs(new BigInteger(data))); - - // Check app install and sponsoring status - var installed = await CheckUrlAsync($"https://cdn.devlooped.com/sponsorlink/apps/{hash}?account={sponsorable}&product={product}&package={packageId}&version={version}", default); - // Timeout, network error, proxy config issue, etc., exit quickly - if (installed == null) - return default; - - var sponsoring = await CheckUrlAsync($"https://cdn.devlooped.com/sponsorlink/{sponsorable}/{hash}?account={sponsorable}&product={product}&package={packageId}&version={version}", default); - if (sponsoring == null) - return default; - - var kind = - installed == false ? DiagnosticKind.AppNotInstalled : - sponsoring == false ? DiagnosticKind.UserNotSponsoring : - DiagnosticKind.Thanks; - - var (warn, pause, suffix) = GetPause(); - if (!warn) - return kind; - - //if (installed == false) - // AnsiConsole.Write(new Paragraph($":warning: {ThisAssembly.Strings.AppNotInstalled.Message(product, sponsorable, suffix)}", new Style(Color.Yellow, decoration: Decoration.Bold))); - //else if (sponsoring == false) - // AnsiConsole.Write(new Paragraph($":warning: {ThisAssembly.Strings.UserNotSponsoring.Message(product, sponsorable, suffix)}", new Style(Color.Yellow, decoration: Decoration.Bold))); - - return kind; - } - - static string? GetEmail(string workingDirectory) - { - try - { - var proc = Process.Start(new ProcessStartInfo("git", "config --get user.email") - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = workingDirectory - }); - proc.WaitForExit(); - - // Couldn't run git config, so we can't check for sponsorship, no email to check. - if (proc.ExitCode != 0) - return null; - - return proc.StandardOutput.ReadToEnd().Trim(); - } - catch - { - // Git not even installed. - } - - return null; - } - - (bool warn, int pause, string suffix) GetPause() - { - var daysOld = (int)DateTime.Now.Subtract(installTime).TotalDays; - - // Never warn during the quiet days. - if (daysOld < quietDays) - return (false, 0, ""); - - if (quietDays == 0 && daysOld == 0) - daysOld = 1; - - // At this point, daysOld is greater than quietDays and greater than 1. - var nonQuietDays = daysOld - quietDays; - // Turn days pause (starting at 1sec max pause into milliseconds, used for the pause. - var daysMaxPause = nonQuietDays * 1000; - - // From second day, the max pause will increase from days old until the max pause. - return GetPaused(rnd.Next(pauseMin, Math.Min(daysMaxPause, pauseMax))); - } - - static (bool warn, int pause, string suffix) GetPaused(int pause) - => (true, pause, pause > 0 ? $"Run paused for {pause}ms" : ""); - - async Task CheckUrlAsync(string url, CancellationToken cancellation) - { - try - { - // We perform a GET since that can be cached by the CDN, but HEAD cannot. - var response = await http.GetAsync(url, cancellation); - return response.IsSuccessStatusCode; - } - catch - { - return null; - } - } -} diff --git a/src/dotnet-nugetize/SponsorLinkChecker.cs b/src/dotnet-nugetize/SponsorLinkChecker.cs index 6bf4faf3..e55bf61e 100644 --- a/src/dotnet-nugetize/SponsorLinkChecker.cs +++ b/src/dotnet-nugetize/SponsorLinkChecker.cs @@ -11,7 +11,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Spectre.Console; namespace Devlooped;