diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index 9b39fd3049..fc428541be 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -77,3 +77,21 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} run: dotnet test UniGetUI.Windows.slnx --no-restore --verbosity q --nologo /p:Platform=x64 /p:UseSharedCompilation=false /m:1 + + - name: Report full-trim publish warnings + working-directory: src + shell: pwsh + continue-on-error: true + run: | + dotnet build-server shutdown + dotnet publish UniGetUI.Avalonia/UniGetUI.Avalonia.csproj ` + --no-restore ` + --configuration Release ` + --runtime win-x64 ` + --self-contained true ` + --output "${{ runner.temp }}\unigetui-full-trim-publish" ` + /p:Platform=x64 ` + /p:TrimMode=full ` + /p:TrimmerSingleWarn=false ` + /p:UseSharedCompilation=false ` + --verbosity minimal diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index b1ab251e77..ccb15dc159 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -11,6 +11,7 @@ #if AVALONIA_DIAGNOSTICS_ENABLED using Avalonia.Diagnostics; #endif +using UniGetUI.Avalonia.Assets.Styles; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.Views; using UniGetUI.Avalonia.Views.DialogPages; @@ -37,18 +38,9 @@ public override void Initialize() #endif } - // ResourceInclude is flagged with RequiresUnreferencedCode because, in general, it can load - // resources from other assemblies that trimming might remove. Styles.WindowsMica.axaml is an - // avares resource embedded in THIS assembly, so it is never trimmed — the warning is safe to - // suppress here. (It can't be declared in XAML because the merge is conditional at runtime.) - [UnconditionalSuppressMessage("Trimming", "IL2026", - Justification = "Styles.WindowsMica.axaml is an avares resource in this assembly and is not trimmed.")] private void ApplyWindowsMicaStyling() { - Resources.MergedDictionaries.Add(new ResourceInclude((Uri?)null) - { - Source = new Uri("avares://UniGetUI/Assets/Styles/Styles.WindowsMica.axaml") - }); + Resources.MergedDictionaries.Add(new WindowsMicaStyles()); // Give flyouts/menus/tooltips a native acrylic backdrop (DWM) so they blur + tint // from behind and adapt to the theme. MicaWindowHelper.EnableAcrylicPopups(); diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml index 97d41ebc9c..073cc8779e 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="UniGetUI.Avalonia.Assets.Styles.WindowsMicaStyles"> + $(NoWarn);MVVMTK0045;AVLN3001 win-x64;win-arm64 win-arm64 win-$(Platform) @@ -147,7 +148,6 @@ - diff --git a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs index af8558ab28..f99ca9b1be 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs @@ -1,10 +1,10 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; -using Octokit; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Core.Logging; using UniGetUI.Core.Tools; +using UniGetUI.Interface; using MvvmRelayCommand = CommunityToolkit.Mvvm.Input.RelayCommand; namespace UniGetUI.Avalonia.ViewModels.Controls; @@ -57,10 +57,10 @@ private async Task RefreshAsync() { try { - var client = GitHubAuthService.CreateGitHubClient(); + using var client = GitHubAuthService.CreateGitHubClient(); if (client is not null) { - User user = await client.User.Current(); + GitHubUser user = await client.GetCurrentUserAsync(); displayName = string.IsNullOrEmpty(user.Name) ? $"@{user.Login}" : $"{user.Name} (@{user.Login})"; diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs index c4933e7d6f..d0f3e43562 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs @@ -156,20 +156,24 @@ private void GenerateLoginState() private async Task GenerateLogoutState() { - var client = GitHubAuthService.CreateGitHubClient() + using var client = GitHubAuthService.CreateGitHubClient() ?? throw new InvalidOperationException("Authenticated but cannot create GitHub client."); - var user = await client.User.Current(); + var user = await client.GetCurrentUserAsync(); IsLoggedIn = true; - GitHubUserTitle = CoreTools.Translate("You are logged in as {0} (@{1})", user.Name, user.Login); + string displayName = string.IsNullOrWhiteSpace(user.Name) ? user.Login : user.Name; + GitHubUserTitle = CoreTools.Translate("You are logged in as {0} (@{1})", displayName, user.Login); GitHubUserSubtitle = CoreTools.Translate("Nice! Backups will be uploaded to a private gist on your account"); try { using var http = new HttpClient(CoreTools.GenericHttpClientParameters); - var bytes = await http.GetByteArrayAsync(user.AvatarUrl); - using var ms = new MemoryStream(bytes); - GitHubAvatarBitmap = new Bitmap(ms); + if (!string.IsNullOrWhiteSpace(user.AvatarUrl)) + { + var bytes = await http.GetByteArrayAsync(user.AvatarUrl); + using var ms = new MemoryStream(bytes); + GitHubAvatarBitmap = new Bitmap(ms); + } } catch (Exception ex) { diff --git a/src/UniGetUI.Interface.IpcApi/GitHubApiClient.cs b/src/UniGetUI.Interface.IpcApi/GitHubApiClient.cs new file mode 100644 index 0000000000..8c64bd8e1c --- /dev/null +++ b/src/UniGetUI.Interface.IpcApi/GitHubApiClient.cs @@ -0,0 +1,393 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using UniGetUI.Core.Data; +using UniGetUI.Core.Tools; + +namespace UniGetUI.Interface; + +public sealed class GitHubApiClient : IDisposable +{ + private static readonly Uri GitHubBaseUri = new("https://github.com/"); + private static readonly Uri GitHubApiBaseUri = new("https://api.github.com/"); + private readonly HttpClient _httpClient; + + public GitHubApiClient(string? token = null) + { + _httpClient = new HttpClient(CoreTools.GenericHttpClientParameters) + { + BaseAddress = GitHubApiBaseUri, + }; + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString); + _httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json"); + _httpClient.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); + + if (!string.IsNullOrWhiteSpace(token)) + { + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", token); + } + } + + public static Uri GetOAuthLoginUrl( + string clientId, + Uri redirectUri, + IEnumerable scopes + ) + { + string query = BuildQueryString( + new Dictionary + { + ["client_id"] = clientId, + ["redirect_uri"] = redirectUri.ToString(), + ["scope"] = string.Join(' ', scopes), + } + ); + return new Uri(GitHubBaseUri, "login/oauth/authorize" + query); + } + + public Task CreateAccessTokenAsync( + string clientId, + string clientSecret, + string code, + Uri redirectUri + ) + { + return PostOAuthFormAsync( + "login/oauth/access_token", + new Dictionary + { + ["client_id"] = clientId, + ["client_secret"] = clientSecret, + ["code"] = code, + ["redirect_uri"] = redirectUri.ToString(), + }, + GitHubJsonContext.Default.GitHubOAuthToken + ); + } + + public Task InitiateDeviceFlowAsync( + string clientId, + IEnumerable scopes, + CancellationToken cancellationToken = default + ) + { + return PostOAuthFormAsync( + "login/device/code", + new Dictionary + { + ["client_id"] = clientId, + ["scope"] = string.Join(' ', scopes), + }, + GitHubJsonContext.Default.GitHubDeviceFlow, + cancellationToken + ); + } + + public Task CreateAccessTokenForDeviceFlowAsync( + string clientId, + GitHubDeviceFlow deviceFlow, + CancellationToken cancellationToken = default + ) + { + return PostOAuthFormAsync( + "login/oauth/access_token", + new Dictionary + { + ["client_id"] = clientId, + ["device_code"] = deviceFlow.DeviceCode, + ["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code", + }, + GitHubJsonContext.Default.GitHubOAuthToken, + cancellationToken + ); + } + + public Task GetCurrentUserAsync(CancellationToken cancellationToken = default) + { + return GetJsonAsync("user", GitHubJsonContext.Default.GitHubUser, cancellationToken); + } + + public Task> GetCurrentUserGistsAsync( + CancellationToken cancellationToken = default + ) + { + return GetJsonAsync( + "gists", + GitHubJsonContext.Default.IReadOnlyListGitHubGist, + cancellationToken + ); + } + + public Task GetGistAsync(string gistId, CancellationToken cancellationToken = default) + { + return GetJsonAsync( + "gists/" + Uri.EscapeDataString(gistId), + GitHubJsonContext.Default.GitHubGist, + cancellationToken + ); + } + + public Task CreateGistAsync( + string description, + bool isPublic, + IReadOnlyDictionary files, + CancellationToken cancellationToken = default + ) + { + return SendGistAsync( + HttpMethod.Post, + "gists", + description, + isPublic, + files, + cancellationToken + ); + } + + public Task EditGistAsync( + string gistId, + string description, + IReadOnlyDictionary files, + CancellationToken cancellationToken = default + ) + { + return SendGistAsync( + HttpMethod.Patch, + "gists/" + Uri.EscapeDataString(gistId), + description, + isPublic: false, + files, + cancellationToken + ); + } + + private async Task SendGistAsync( + HttpMethod method, + string relativeUri, + string description, + bool isPublic, + IReadOnlyDictionary files, + CancellationToken cancellationToken + ) + { + var request = new GitHubGistRequest + { + Description = description, + IsPublic = method == HttpMethod.Post ? isPublic : null, + Files = files.ToDictionary( + file => file.Key, + file => new GitHubGistFileRequest { Content = file.Value }, + StringComparer.Ordinal + ), + }; + + using var message = new HttpRequestMessage(method, relativeUri) + { + Content = CreateJsonContent(request, GitHubJsonContext.Default.GitHubGistRequest), + }; + return await SendAsync(message, GitHubJsonContext.Default.GitHubGist, cancellationToken); + } + + private async Task GetJsonAsync( + string relativeUri, + JsonTypeInfo typeInfo, + CancellationToken cancellationToken + ) + { + using var message = new HttpRequestMessage(HttpMethod.Get, relativeUri); + return await SendAsync(message, typeInfo, cancellationToken); + } + + private async Task PostOAuthFormAsync( + string relativeUri, + IReadOnlyDictionary values, + JsonTypeInfo typeInfo, + CancellationToken cancellationToken = default + ) + { + using var message = new HttpRequestMessage(HttpMethod.Post, new Uri(GitHubBaseUri, relativeUri)) + { + Content = new FormUrlEncodedContent( + values + .Where(value => !string.IsNullOrWhiteSpace(value.Value)) + .Select(value => new KeyValuePair(value.Key, value.Value!)) + ), + }; + message.Headers.Accept.Clear(); + message.Headers.Accept.ParseAdd("application/json"); + return await SendAsync(message, typeInfo, cancellationToken); + } + + private async Task SendAsync( + HttpRequestMessage message, + JsonTypeInfo typeInfo, + CancellationToken cancellationToken + ) + { + using HttpResponseMessage response = await _httpClient.SendAsync(message, cancellationToken); + string content = await response.Content.ReadAsStringAsync(cancellationToken); + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException( + $"GitHub request failed with HTTP {(int)response.StatusCode} ({response.ReasonPhrase}): {GetErrorMessage(content)}" + ); + } + + return JsonSerializer.Deserialize(content, typeInfo) + ?? throw new InvalidOperationException("GitHub returned an empty response."); + } + + private static StringContent CreateJsonContent(T value, JsonTypeInfo typeInfo) + { + return new StringContent( + JsonSerializer.Serialize(value, typeInfo), + System.Text.Encoding.UTF8, + "application/json" + ); + } + + private static string GetErrorMessage(string content) + { + if (string.IsNullOrWhiteSpace(content)) + return "No response body."; + + try + { + GitHubError? error = JsonSerializer.Deserialize( + content, + GitHubJsonContext.Default.GitHubError + ); + if (!string.IsNullOrWhiteSpace(error?.ErrorDescription)) + return error.ErrorDescription; + if (!string.IsNullOrWhiteSpace(error?.Error)) + return error.Error; + if (!string.IsNullOrWhiteSpace(error?.Message)) + return error.Message; + } + catch (JsonException) + { + return content; + } + + return content; + } + + private static string BuildQueryString(IReadOnlyDictionary values) + { + string query = string.Join( + '&', + values + .Where(value => !string.IsNullOrWhiteSpace(value.Value)) + .Select(value => + Uri.EscapeDataString(value.Key) + "=" + Uri.EscapeDataString(value.Value!)) + ); + return string.IsNullOrEmpty(query) ? "" : "?" + query; + } + + public void Dispose() + { + _httpClient.Dispose(); + } +} + +public sealed class GitHubOAuthToken +{ + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = ""; +} + +public sealed class GitHubDeviceFlow +{ + [JsonPropertyName("device_code")] + public string DeviceCode { get; set; } = ""; + + [JsonPropertyName("user_code")] + public string UserCode { get; set; } = ""; + + [JsonPropertyName("verification_uri")] + public string VerificationUri { get; set; } = ""; + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("interval")] + public int Interval { get; set; } +} + +public sealed class GitHubUser +{ + [JsonPropertyName("login")] + public string Login { get; set; } = ""; + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } +} + +public sealed class GitHubGist +{ + [JsonPropertyName("id")] + public string Id { get; set; } = ""; + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("files")] + public Dictionary Files { get; set; } = []; +} + +public sealed class GitHubGistFile +{ + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } +} + +internal sealed class GitHubGistRequest +{ + [JsonPropertyName("description")] + public string Description { get; set; } = ""; + + [JsonPropertyName("public")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? IsPublic { get; set; } + + [JsonPropertyName("files")] + public Dictionary Files { get; set; } = []; +} + +internal sealed class GitHubGistFileRequest +{ + [JsonPropertyName("content")] + public string Content { get; set; } = ""; +} + +internal sealed class GitHubError +{ + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("error")] + public string? Error { get; set; } + + [JsonPropertyName("error_description")] + public string? ErrorDescription { get; set; } +} + +[JsonSourceGenerationOptions] +[JsonSerializable(typeof(GitHubOAuthToken))] +[JsonSerializable(typeof(GitHubDeviceFlow))] +[JsonSerializable(typeof(GitHubUser))] +[JsonSerializable(typeof(GitHubGist))] +[JsonSerializable(typeof(GitHubGistFile))] +[JsonSerializable(typeof(GitHubGistRequest))] +[JsonSerializable(typeof(GitHubGistFileRequest))] +[JsonSerializable(typeof(GitHubError))] +[JsonSerializable(typeof(IReadOnlyList))] +internal sealed partial class GitHubJsonContext : JsonSerializerContext; diff --git a/src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs b/src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs index 13f39bbccf..9a427aee06 100644 --- a/src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs +++ b/src/UniGetUI.Interface.IpcApi/IpcBackupApi.cs @@ -1,4 +1,3 @@ -using Octokit; using UniGetUI.Core.Data; using UniGetUI.Core.Logging; using UniGetUI.Core.SecureSettings; @@ -107,7 +106,7 @@ public static class IpcBackupApi private sealed class PendingGitHubDeviceFlow { - public required OauthDeviceFlowResponse DeviceFlow { get; init; } + public required GitHubDeviceFlow DeviceFlow { get; init; } public required DateTimeOffset ExpiresAtUtc { get; init; } } @@ -169,12 +168,10 @@ public static async Task StartGitHubDeviceFlowAsync( request ??= new IpcGitHubDeviceFlowRequest(); EnsureGitHubClientConfigured(); - var client = CreateAnonymousGitHubClient(); - var deviceFlow = await client.Oauth.InitiateDeviceFlow( - new OauthDeviceFlowRequest(Secrets.GetGitHubClientId()) - { - Scopes = { "read:user", "gist" }, - }, + using var client = CreateAnonymousGitHubClient(); + var deviceFlow = await client.InitiateDeviceFlowAsync( + Secrets.GetGitHubClientId(), + ["read:user", "gist"], CancellationToken.None ); @@ -218,8 +215,8 @@ public static async Task CompleteGitHubDeviceFlowAsync() try { - var client = CreateAnonymousGitHubClient(); - var token = await client.Oauth.CreateAccessTokenForDeviceFlow( + using var client = CreateAnonymousGitHubClient(); + var token = await client.CreateAccessTokenForDeviceFlowAsync( Secrets.GetGitHubClientId(), pending.DeviceFlow, CancellationToken.None @@ -231,8 +228,8 @@ public static async Task CompleteGitHubDeviceFlowAsync() } SecureGHTokenManager.StoreToken(token.AccessToken); - var userClient = CreateAuthenticatedGitHubClient(token.AccessToken); - var user = await userClient.User.Current(); + using var userClient = CreateAuthenticatedGitHubClient(token.AccessToken); + var user = await userClient.GetCurrentUserAsync(); Settings.SetValue(Settings.K.GitHubUserLogin, user.Login ?? string.Empty); ClearPendingGitHubDeviceFlow(); @@ -273,8 +270,9 @@ public static async Task SignOutGitHubAsync() public static async Task> ListCloudBackupsAsync() { - var (client, user) = await GetAuthenticatedGitHubContextAsync(); - var backupGist = await GetBackupGistAsync(client, user.Login, createIfMissing: false); + using var client = CreateAuthenticatedGitHubClient(); + await GetAuthenticatedGitHubUserAsync(client); + var backupGist = await GetBackupGistAsync(client, createIfMissing: false); if (backupGist is null) { return []; @@ -300,22 +298,17 @@ public static async Task CreateCloudBackupAsync() { var packages = GetInstalledPackagesForBackup(); string bundleContents = await IpcBundleApi.CreateBundleAsync(packages); - var (client, user) = await GetAuthenticatedGitHubContextAsync(); - var backupGist = await GetBackupGistAsync(client, user.Login, createIfMissing: true) + using var client = CreateAuthenticatedGitHubClient(); + await GetAuthenticatedGitHubUserAsync(client); + var backupGist = await GetBackupGistAsync(client, createIfMissing: true) ?? throw new InvalidOperationException("The GitHub backup gist could not be created."); string fileKey = BuildGistFileKey(); - var update = new GistUpdate { Description = GistDescription }; - if (backupGist.Files.ContainsKey(fileKey)) - { - update.Files[fileKey] = new GistFileUpdate { Content = bundleContents }; - } - else - { - update.Files.Add(fileKey, new GistFileUpdate { Content = bundleContents }); - } - - await client.Gist.Edit(backupGist.Id, update); + await client.EditGistAsync( + backupGist.Id, + GistDescription, + new Dictionary { [fileKey] = bundleContents } + ); return new IpcCloudBackupUploadResult { Status = "success", @@ -446,8 +439,8 @@ private static async Task GetGitHubAuthInfoAsync() try { - var client = CreateAuthenticatedGitHubClient(); - var user = await client.User.Current(); + using var client = CreateAuthenticatedGitHubClient(); + var user = await client.GetCurrentUserAsync(); if (!string.IsNullOrWhiteSpace(user.Login)) { Settings.SetValue(Settings.K.GitHubUserLogin, user.Login); @@ -479,12 +472,12 @@ private static void EnsureGitHubClientConfigured() } } - private static GitHubClient CreateAnonymousGitHubClient() + private static GitHubApiClient CreateAnonymousGitHubClient() { - return new GitHubClient(new ProductHeaderValue("UniGetUI", CoreData.VersionName)); + return new GitHubApiClient(); } - private static GitHubClient CreateAuthenticatedGitHubClient(string? token = null) + private static GitHubApiClient CreateAuthenticatedGitHubClient(string? token = null) { token ??= SecureGHTokenManager.GetToken(); if (string.IsNullOrWhiteSpace(token)) @@ -492,34 +485,31 @@ private static GitHubClient CreateAuthenticatedGitHubClient(string? token = null throw new InvalidOperationException("GitHub authentication is required for cloud backups."); } - return new GitHubClient(new ProductHeaderValue("UniGetUI", CoreData.VersionName)) - { - Credentials = new Credentials(token), - }; + return new GitHubApiClient(token); } - private static async Task<(GitHubClient Client, User User)> GetAuthenticatedGitHubContextAsync() + private static async Task GetAuthenticatedGitHubUserAsync(GitHubApiClient client) { - var client = CreateAuthenticatedGitHubClient(); - var user = await client.User.Current(); + var user = await client.GetCurrentUserAsync(); if (!string.IsNullOrWhiteSpace(user.Login)) { Settings.SetValue(Settings.K.GitHubUserLogin, user.Login); } - return (client, user); + return user; } private static async Task GetCloudBackupContentsAsync(string key) { - var (client, user) = await GetAuthenticatedGitHubContextAsync(); - var backupGist = await GetBackupGistAsync(client, user.Login, createIfMissing: false); + using var client = CreateAuthenticatedGitHubClient(); + await GetAuthenticatedGitHubUserAsync(client); + var backupGist = await GetBackupGistAsync(client, createIfMissing: false); if (backupGist is null) { throw new InvalidOperationException("No cloud backups are available for the current account."); } - var fullGist = await client.Gist.Get(backupGist.Id); + var fullGist = await client.GetGistAsync(backupGist.Id); var file = fullGist.Files.FirstOrDefault(candidate => candidate.Key.StartsWith(PackageBackupStartingKey, StringComparison.Ordinal) && candidate.Key.EndsWith(key, StringComparison.Ordinal)); @@ -532,13 +522,12 @@ private static async Task GetCloudBackupContentsAsync(string key) return file.Value.Content; } - private static async Task GetBackupGistAsync( - GitHubClient client, - string userLogin, + private static async Task GetBackupGistAsync( + GitHubApiClient client, bool createIfMissing ) { - var candidates = await client.Gist.GetAllForUser(userLogin); + var candidates = await client.GetCurrentUserGistsAsync(); var backupGist = candidates.FirstOrDefault(candidate => candidate.Description?.EndsWith(GistDescriptionEndingKey, StringComparison.Ordinal) == true @@ -549,9 +538,11 @@ bool createIfMissing return backupGist; } - var newGist = new NewGist { Description = GistDescription, Public = false }; - newGist.Files.Add("- UniGetUI Package Backups", ReadMeContents); - return await client.Gist.Create(newGist); + return await client.CreateGistAsync( + GistDescription, + isPublic: false, + new Dictionary { ["- UniGetUI Package Backups"] = ReadMeContents } + ); } private static string BuildGistFileKey() diff --git a/src/UniGetUI.Interface.IpcApi/UniGetUI.Interface.IpcApi.csproj b/src/UniGetUI.Interface.IpcApi/UniGetUI.Interface.IpcApi.csproj index 968ac0ebd5..64dbfb9469 100644 --- a/src/UniGetUI.Interface.IpcApi/UniGetUI.Interface.IpcApi.csproj +++ b/src/UniGetUI.Interface.IpcApi/UniGetUI.Interface.IpcApi.csproj @@ -14,7 +14,6 @@ -