diff --git a/PCL.Neo.Core/Models/Minecraft/Mod/ModInfoReader.README.md b/PCL.Neo.Core/Models/Minecraft/Mod/ModInfoReader.README.md new file mode 100644 index 00000000..e69de29b diff --git a/PCL.Neo.Core/Service/Accounts/AccountService.cs b/PCL.Neo.Core/Service/Accounts/AccountService.cs index 17164d93..3e1aa795 100644 --- a/PCL.Neo.Core/Service/Accounts/AccountService.cs +++ b/PCL.Neo.Core/Service/Accounts/AccountService.cs @@ -14,14 +14,6 @@ public class AccountService : IAccountService private readonly string _selectedAccountFilePath; private readonly JsonSerializerOptions _jsonOptions; - // TODO: 配置Yggdrasil服务API密钥 - // fix: yggdrasil doesn't have any access key ( from: whitecat346) - // /cc @BL077 - private readonly string _yggdrasilApiKey = ""; - - // TODO: 配置默认的Yggdrasil API超时时间(毫秒) - private readonly int _yggdrasilApiTimeoutMs = 10000; - private List _cachedAccounts = []; private string? _selectedAccountUuid; private bool _isLoaded; @@ -437,54 +429,44 @@ public async Task RefreshMicrosoftAccountAsync(MsaAccount account) return account; } - this.LogAccountInfo($"开始刷新微软账户令牌: {account.UserName}"); - - var refreshResult = await _microsoftAuthService.RefreshTokenAsync(account.OAuthToken.RefreshToken); - if (refreshResult.IsFailure) - { - this.LogAccountError($"刷新微软账户令牌失败: {account.UserName}", refreshResult.Error); - throw refreshResult.Error!; - } - - var tokenInfo = refreshResult.Value; - this.LogAccountDebug("获取到新的OAuth令牌"); - - // 获取Minecraft令牌 - var mcTokenResult = await _microsoftAuthService.GetUserMinecraftAccessTokenAsync(tokenInfo.AccessToken); - if (mcTokenResult.IsFailure) + try { - this.LogAccountError($"获取Minecraft令牌失败: {account.UserName}", mcTokenResult.Error); - throw mcTokenResult.Error; - } + this.LogAccountInfo($"开始刷新微软账户令牌: {account.UserName}"); - this.LogAccountDebug("获取到新的Minecraft令牌"); + var refreshResult = await _microsoftAuthService.RefreshTokenAsync(account.OAuthToken.RefreshToken); - // 获取用户信息 - var accountInfoResult = await _microsoftAuthService.GetUserAccountInfoAsync(mcTokenResult.Value); - if (accountInfoResult.IsFailure) - { - this.LogAccountError($"获取账户信息失败: {account.UserName}", accountInfoResult.Error); - throw accountInfoResult.Error!; - } + this.LogAccountDebug("获取到新的OAuth令牌"); - var accountInfo = accountInfoResult.Value; + // 获取Minecraft令牌 + var mcTokenResult = + await _microsoftAuthService.GetUserMinecraftAccessTokenAsync(refreshResult.AccessToken); - // 创建更新后的账户 - var updatedAccount = account with - { - OAuthToken = tokenInfo, - McAccessToken = mcTokenResult.Value, - UserName = accountInfo.UserName, - Skins = accountInfo.Skins, - Capes = accountInfo.Capes - }; + this.LogAccountDebug("获取到新的Minecraft令牌"); - this.LogAccountInfo($"微软账户令牌刷新成功: {updatedAccount.UserName}"); + // 获取用户信息 + var accountInfoResult = await _microsoftAuthService.GetUserAccountInfoAsync(mcTokenResult); - // 保存更新后的账户 - await SaveAccountAsync(updatedAccount); + // 创建更新后的账户 + var updatedAccount = account with + { + OAuthToken = refreshResult, + McAccessToken = mcTokenResult, + UserName = accountInfoResult.UserName, + Skins = accountInfoResult.Skins, + Capes = accountInfoResult.Capes + }; + this.LogAccountInfo($"微软账户令牌刷新成功: {updatedAccount.UserName}"); + + // 保存更新后的账户 + await SaveAccountAsync(updatedAccount); - return updatedAccount; + return updatedAccount; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } catch (Exception ex) when (ex is not ArgumentNullException) { diff --git a/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/DeviceFlowState.cs b/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/DeviceFlowState.cs index 14523a6c..b2a37a4e 100644 --- a/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/DeviceFlowState.cs +++ b/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/DeviceFlowState.cs @@ -12,11 +12,11 @@ public class DeviceFlowAwaitUser(string userCode, string verificationUri) : Devi public class DeviceFlowPolling : DeviceFlowState; -public class DeviceFlowDeclined : DeviceFlowState; +public class DeviceFlowDeclined : Exception; -public class DeviceFlowExpired : DeviceFlowState; +public class DeviceFlowExpired : Exception; -public class DeviceFlowBadVerificationCode : DeviceFlowState; +public class DeviceFlowBadVerificationCode : Exception; public class DeviceFlowGetAccountInfo : DeviceFlowState; @@ -26,7 +26,7 @@ public class DeviceFlowSucceeded(MsaAccount account) : DeviceFlowState public MsaAccount Account { get; } = account; } -public class DeviceFlowUnkonw : DeviceFlowState; +public class DeviceFlowUnkonw : Exception; public class DeviceFlowInternetError : DeviceFlowState; diff --git a/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/IMicrosoftAuthService.cs b/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/IMicrosoftAuthService.cs index eb2b3bfe..0c47eda0 100644 --- a/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/IMicrosoftAuthService.cs +++ b/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/IMicrosoftAuthService.cs @@ -17,7 +17,7 @@ public interface IMicrosoftAuthService /// 获取设备码 /// /// 获取到的设备码信息,会在失败的时候返回异常 - Task> RequestDeviceCodeAsync(); + Task RequestDeviceCodeAsync(); /// /// 开始轮询服务器 @@ -25,7 +25,7 @@ public interface IMicrosoftAuthService /// 设备码 /// 轮询间隔 /// 轮询结果 - Task> PollForTokenAsync(string deviceCode, + Task PollForTokenAsync(string deviceCode, int interval); /// @@ -33,19 +33,19 @@ public interface IMicrosoftAuthService /// /// 通行Token /// 玩家信息 - Task> GetUserMinecraftAccessTokenAsync(string accessToken); + Task GetUserMinecraftAccessTokenAsync(string accessToken); /// /// 获取玩家的账户信息 /// /// 需要的Token /// 账户信息 - Task> GetUserAccountInfoAsync(string accessToken); + Task GetUserAccountInfoAsync(string accessToken); /// /// 刷新玩家的OAuth2 Token /// /// OAuth2的刷新Token /// 新的Token - Task> RefreshTokenAsync(string refreshToken); + Task RefreshTokenAsync(string refreshToken); } diff --git a/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/MicrosoftAuthService.cs b/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/MicrosoftAuthService.cs index b7257f68..705cb57a 100644 --- a/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/MicrosoftAuthService.cs +++ b/PCL.Neo.Core/Service/Accounts/MicrosoftAuth/MicrosoftAuthService.cs @@ -1,118 +1,77 @@ -using PCL.Neo.Core.Service.Accounts.Exceptions; using PCL.Neo.Core.Service.Accounts.OAuthService; -using PCL.Neo.Core.Service.Accounts.OAuthService.Exceptions; using PCL.Neo.Core.Service.Accounts.Storage; -using PCL.Neo.Core.Utils; +using PCL.Neo.Core.Utils.Net; using System.Diagnostics; using System.Net.Http.Headers; using System.Reactive.Linq; -using System.Text.Json; namespace PCL.Neo.Core.Service.Accounts.MicrosoftAuth; public class MicrosoftAuthService : IMicrosoftAuthService { + private readonly INetService _netService = new NetService("MicrosoftAuthService"); + /// public IObservable StartDeviceCodeFlow() { return Observable.Create(async observer => { - // get device code - var deviceCodeResult = await RequestDeviceCodeAsync().ConfigureAwait(false); - if (deviceCodeResult.IsFailure) + try { - observer.OnError(deviceCodeResult.Error.Exception!); - return; - } + // get device code + var deviceCodeInfo = await RequestDeviceCodeAsync().ConfigureAwait(false); - var deviceCodeInfo = deviceCodeResult.Value; + // show for user and open browser + OpenBrowserAsync(deviceCodeInfo.VerificationUri); + observer.OnNext(new DeviceFlowAwaitUser(deviceCodeInfo.UserCode, deviceCodeInfo.VerificationUri)); - // show for user and open browser - OpenBrowserAsync(deviceCodeInfo.VerificationUri); - observer.OnNext(new DeviceFlowAwaitUser(deviceCodeInfo.UserCode, deviceCodeInfo.VerificationUri)); - - // polling server - var tokenResult = await PollForTokenAsync(deviceCodeInfo.DeviceCode, deviceCodeInfo.Interval) - .ConfigureAwait(false); - observer.OnNext(new DeviceFlowPolling()); - - if (tokenResult.IsFailure) - { - observer.OnNext(tokenResult.Error); - return; - } + // polling server + observer.OnNext(new DeviceFlowPolling()); + var tokenInfo = await PollForTokenAsync(deviceCodeInfo.DeviceCode, deviceCodeInfo.Interval) + .ConfigureAwait(false); - var tokenInfo = tokenResult.Value; + // get user mc token + var mcToken = await GetUserMinecraftAccessTokenAsync(tokenInfo.AccessToken).ConfigureAwait(false); - // get user mc token - var mcToken = await GetUserMinecraftAccessTokenAsync(tokenInfo.AccessToken).ConfigureAwait(false); - if (mcToken.IsFailure) - { - observer.OnError(mcToken.Error); - return; + // get user account info + var accountInfo = await GetUserAccountInfoAsync(mcToken).ConfigureAwait(false); + var account = new MsaAccount + { + McAccessToken = mcToken, + OAuthToken = new OAuthTokenData(tokenInfo.AccessToken, tokenInfo.RefreshToken, tokenInfo.ExpiresIn), + UserName = accountInfo.UserName, + UserProperties = string.Empty, + Uuid = accountInfo.Uuid, + Capes = accountInfo.Capes, + Skins = accountInfo.Skins + }; + + observer.OnNext(new DeviceFlowSucceeded(account)); } - - // get user account info - var accountInfoResult = await GetUserAccountInfoAsync(mcToken.Value).ConfigureAwait(false); - if (accountInfoResult.IsFailure) + catch (Exception e) { - observer.OnError(accountInfoResult.Error!); - return; + observer.OnError(e); } - var accountInfo = accountInfoResult.Value; - - var account = new MsaAccount - { - McAccessToken = mcToken.Value, - OAuthToken = new OAuthTokenData(tokenInfo.AccessToken, tokenInfo.RefreshToken, tokenInfo.ExpiresIn), - UserName = accountInfo.UserName, - UserProperties = string.Empty, - Uuid = accountInfo.Uuid, - Capes = accountInfo.Capes, - Skins = accountInfo.Skins - }; - - observer.OnNext(new DeviceFlowSucceeded(account)); observer.OnCompleted(); }); } /// - public async Task> RequestDeviceCodeAsync() + public async Task RequestDeviceCodeAsync() { var content = new FormUrlEncodedContent(OAuthData.FormUrlReqData.DeviceCodeData.Value) { Headers = { ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded") } }; - try - { - var temp = await Net.SendHttpRequestAsync(HttpMethod.Post, - OAuthData.RequestUrls.DeviceCode.Value, content).ConfigureAwait(false); - var result = new DeviceCodeData.DeviceCodeInfo(temp.DeviceCode, temp.UserCode, temp.VerificationUri, - temp.Interval); - return Result.Ok(result); - } - catch (HttpRequestException e) - { - return Result.Fail(new HttpError(null, - "Network error while requesting device code.", Exception: e)); - } - catch (JsonException e) - { - return Result.Fail(new HttpError(null, - "Failed to parse device code response.", Exception: e)); - } - catch (Exception e) - { - return Result.Fail(new HttpError(null, - "An unexpected error occurred.", Exception: e)); - } + var result = await _netService.SendAsync(HttpMethod.Post, + OAuthData.RequestUrls.DeviceCode.Value.ToString()).ConfigureAwait(false); + return result; } /// - public async Task> PollForTokenAsync( + public async Task PollForTokenAsync( string deviceCode, int interval) { var tempInterval = interval; @@ -126,116 +85,73 @@ public IObservable StartDeviceCodeFlow() Headers = { ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded") } }; + var request = new HttpRequestMessage(HttpMethod.Post, OAuthData.RequestUrls.TokenUri.Value); + request.Content = msg; + while (true) { - try - { - await Task.Delay(tempInterval).ConfigureAwait(false); + await Task.Delay(tempInterval).ConfigureAwait(false); - var tempResult = - await Net.SendHttpRequestAsync(HttpMethod.Post, - OAuthData.RequestUrls.TokenUri.Value, msg).ConfigureAwait(false); + var tempResult = await _netService + .SendAsync(request).ConfigureAwait(false); - // handle response - if (!string.IsNullOrEmpty(tempResult.Error)) + // handle response + if (!string.IsNullOrEmpty(tempResult.Error)) + { + switch (tempResult.Error) { - switch (tempResult.Error) - { - case "authorization_declined": - return Result.Fail( - new DeviceFlowError(new DeviceFlowDeclined(), null)); - case "expired_token": - return Result.Fail( - new DeviceFlowError(new DeviceFlowExpired(), null)); - case "bad_verification_code": - return Result.Fail( - new DeviceFlowError(new DeviceFlowDeclined(), null)); - case "slow_down": - tempInterval = Math.Min(tempInterval * 2, 900); // Adjust polling interval - continue; - case "authorization_pending": - continue; // Keep polling - default: - return Result.Fail( - new DeviceFlowError(new DeviceFlowUnkonw(), null)); - } + case "authorization_declined": + throw new DeviceFlowDeclined(); + case "expired_token": + throw new DeviceFlowExpired(); + case "bad_verification_code": + throw new DeviceFlowDeclined(); + case "slow_down": + tempInterval = Math.Min(tempInterval * 2, 900); // Adjust polling interval + continue; + case "authorization_pending": + continue; // Keep polling + default: + throw new DeviceFlowUnkonw(); } + } - // create result - var result = new DeviceCodeData.DeviceCodeAccessToken(tempResult.AccessToken, - tempResult.RefreshToken, - DateTimeOffset.UtcNow.AddSeconds((double)tempResult.ExpiresIn)); + // create result + var result = new DeviceCodeData.DeviceCodeAccessToken(tempResult.AccessToken, + tempResult.RefreshToken, + DateTimeOffset.UtcNow.AddSeconds(tempResult.ExpiresIn)); - return Result.Ok(result); - } - catch (HttpRequestException e) - { - return Result.Fail( - new DeviceFlowError(new DeviceFlowInternetError(), e)); - } - catch (JsonException e) - { - return Result.Fail( - new DeviceFlowError(new DeviceFlowJsonError(), e)); - } - catch (Exception e) - { - return Result.Fail( - new DeviceFlowError(new DeviceFlowUnkonw(), e)); - } + return result; } } /// - public async Task> GetUserMinecraftAccessTokenAsync( + public async Task GetUserMinecraftAccessTokenAsync( string accessToken) { - try - { - var minecraftToken = await OAuth.GetMinecraftTokenAsync(accessToken).ConfigureAwait(false); - return Result.Ok(minecraftToken); - } - catch (NotHaveGameException e) - { - return Result.Fail( - new NotHaveGameException(e.Message)); - } + var minecraftToken = await OAuth.GetMinecraftTokenAsync(accessToken).ConfigureAwait(false); + return minecraftToken; } /// - public async Task> GetUserAccountInfoAsync(string accessToken) + public async Task GetUserAccountInfoAsync(string accessToken) { - try - { - var playerInfo = await MinecraftInfo.GetPlayerUuidAsync(accessToken).ConfigureAwait(false); - var capes = MinecraftInfo.CollectCapes(playerInfo.Capes); - var skins = MinecraftInfo.CollectSkins(playerInfo.Skins); - var uuid = playerInfo.Uuid; + var playerInfo = await MinecraftInfo.GetPlayerUuidAsync(accessToken).ConfigureAwait(false); + var capes = MinecraftInfo.CollectCapes(playerInfo.Capes); + var skins = MinecraftInfo.CollectSkins(playerInfo.Skins); + var uuid = playerInfo.Uuid; - return Result.Ok( - new DeviceCodeData.McAccountInfo(skins, capes, playerInfo.Name, uuid)); - } - catch (Exception e) - { - return Result.Fail(e); - } + return new DeviceCodeData.McAccountInfo(skins, capes, playerInfo.Name, uuid); } /// - public async Task> RefreshTokenAsync(string refreshToken) + public async Task RefreshTokenAsync(string refreshToken) { var newToken = await OAuth.RefreshTokenAsync(refreshToken).ConfigureAwait(false); - try - { - var newTokenData = new OAuthTokenData(newToken.AccessToken, newToken.RefreshToken, - new DateTimeOffset(DateTime.Now, TimeSpan.FromSeconds(newToken.ExpiresIn))); + var newTokenData = new OAuthTokenData(newToken.AccessToken, newToken.RefreshToken, + new DateTimeOffset(DateTime.Now, TimeSpan.FromSeconds(newToken.ExpiresIn))); - return Result.Ok(newTokenData); - } - catch (Exception e) - { - return Result.Fail(e); - } + return newTokenData; } private static void OpenBrowserAsync(string requiredUrl) @@ -243,7 +159,7 @@ private static void OpenBrowserAsync(string requiredUrl) var processStartInfo = new ProcessStartInfo { - FileName = requiredUrl, UseShellExecute = true + FileName = requiredUrl, UseShellExecute = false }; // #WARN this method may cant run on linux and macos Process.Start(processStartInfo); diff --git a/PCL.Neo.Core/Service/Accounts/OAuthService/MinecraftInfo.cs b/PCL.Neo.Core/Service/Accounts/OAuthService/MinecraftInfo.cs index 9dababfc..0d3e42bc 100644 --- a/PCL.Neo.Core/Service/Accounts/OAuthService/MinecraftInfo.cs +++ b/PCL.Neo.Core/Service/Accounts/OAuthService/MinecraftInfo.cs @@ -1,13 +1,18 @@ using PCL.Neo.Core.Service.Accounts.Storage; -using PCL.Neo.Core.Utils; +using PCL.Neo.Core.Utils.Net; namespace PCL.Neo.Core.Service.Accounts.OAuthService; -public class MinecraftInfo +public static class MinecraftInfo { public static List CollectSkins( - IEnumerable skins) + IEnumerable? skins) { + if (skins == null) + { + return []; + } + return skins.Select(skin => new { skin, @@ -26,8 +31,13 @@ public static List CollectSkins( } public static List CollectCapes( - IEnumerable capes) + IEnumerable? capes) { + if (capes == null) + { + return []; + } + return capes.Select(cape => new { cape, @@ -51,20 +61,20 @@ public static async Task GetMinecraftAccessTokenAsync(string uhs, string IdentityToken = $"XBL3.0 x={uhs};{xstsToken}" }; - var response = await Net.SendHttpRequestAsync( - HttpMethod.Post, - OAuthData.RequestUrls.MinecraftAccessTokenUri.Value, - jsonContent); + var response = await StaticNet + .SendJsonAsync( + HttpMethod.Post, + OAuthData.RequestUrls.MinecraftAccessTokenUri.Value, + jsonContent); return response.AccessToken; } public static async Task IsHaveGameAsync(string accessToken) { - var response = await Net.SendHttpRequestAsync( - HttpMethod.Get, - OAuthData.RequestUrls.CheckHasMc.Value, - bearerToken: accessToken); + var response = await StaticNet.SendAsync(HttpMethod.Get, + OAuthData.RequestUrls.CheckHasMc.Value, accessToken); return response.Items.Any(it => !string.IsNullOrEmpty(it.Signature)); } @@ -72,9 +82,9 @@ public static async Task IsHaveGameAsync(string accessToken) public static async Task GetPlayerUuidAsync(string accessToken) { - return await Net.SendHttpRequestAsync( - HttpMethod.Get, - OAuthData.RequestUrls.PlayerUuidUri.Value, - bearerToken: accessToken); + var response = await StaticNet.SendAsync(HttpMethod.Get, + OAuthData.RequestUrls.PlayerUuidUri.Value, accessToken); + + return response; } } diff --git a/PCL.Neo.Core/Service/Accounts/OAuthService/OAuth.cs b/PCL.Neo.Core/Service/Accounts/OAuthService/OAuth.cs index 41939a9a..5653c139 100644 --- a/PCL.Neo.Core/Service/Accounts/OAuthService/OAuth.cs +++ b/PCL.Neo.Core/Service/Accounts/OAuthService/OAuth.cs @@ -1,11 +1,9 @@ using PCL.Neo.Core.Service.Accounts.OAuthService.Exceptions; -using PCL.Neo.Core.Utils; +using PCL.Neo.Core.Utils.Net; using System.Diagnostics.CodeAnalysis; namespace PCL.Neo.Core.Service.Accounts.OAuthService; -#pragma warning disable IL2026 // fixed by DynamicDependency - public static class OAuth { public static async Task RefreshTokenAsync(string refreshToken) @@ -15,10 +13,13 @@ public static class OAuth ["refresh_token"] = refreshToken }; - return await Net.SendHttpRequestAsync( - HttpMethod.Post, - OAuthData.RequestUrls.TokenUri.Value, - new FormUrlEncodedContent(authTokenData)); + var requeset = new HttpRequestMessage(HttpMethod.Post, OAuthData.RequestUrls.TokenUri.Value) + { + Content = new FormUrlEncodedContent(authTokenData) + }; + + var response = await StaticNet.SendAsync(requeset); + return response; } [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(OAuthData.RequireData))] @@ -29,11 +30,14 @@ public static class OAuth { Properties = new OAuthData.RequireData.XboxLiveAuthRequire.PropertiesData(accessToken) }; + var request = new HttpRequestMessage(HttpMethod.Post, OAuthData.RequestUrls.XboxLiveAuth.Value); + var response = await StaticNet + .SendJsonAsync( + HttpMethod.Post, + OAuthData.RequestUrls.XboxLiveAuth.Value, + jsonContent); - return await Net.SendHttpRequestAsync( - HttpMethod.Post, - OAuthData.RequestUrls.XboxLiveAuth.Value, - jsonContent); + return response; } [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(OAuthData.RequireData))] @@ -42,10 +46,11 @@ public static async Task GetXstsTokenAsync(string xblToken) var jsonContent = new OAuthData.RequireData.XstsRequire(new OAuthData.RequireData.XstsRequire.PropertiesData([xblToken])); - var response = await Net.SendHttpRequestAsync( - HttpMethod.Post, - OAuthData.RequestUrls.XstsAuth.Value, - jsonContent); + var response = + await StaticNet.SendJsonAsync( + HttpMethod.Post, + OAuthData.RequestUrls.XstsAuth.Value, + jsonContent); return response.Token; } diff --git a/PCL.Neo.Core/Service/Accounts/OAuthService/OAuthData.cs b/PCL.Neo.Core/Service/Accounts/OAuthService/OAuthData.cs index c1b7d784..9f05585c 100644 --- a/PCL.Neo.Core/Service/Accounts/OAuthService/OAuthData.cs +++ b/PCL.Neo.Core/Service/Accounts/OAuthService/OAuthData.cs @@ -16,19 +16,19 @@ public static class RequestUrls /// 获取授权码模式下的授权码地址 /// public static readonly Lazy AuthCodeUri = new(() => - new Uri(OAuth2BaseUri.Value, "authorize")); + new Uri(OAuth2BaseUri.Value, "/authorize")); /// /// 获取设备码模式下的授权码地址 /// public static readonly Lazy DeviceCode = new(() => - new Uri(OAuth2BaseUri.Value, "devicecode")); + new Uri(OAuth2BaseUri.Value, "/devicecode")); /// /// 获取令牌 /// public static readonly Lazy TokenUri = new(() => - new Uri(OAuth2BaseUri.Value, "token")); + new Uri(OAuth2BaseUri.Value, "/token")); /// /// XboxLive验证地址 @@ -66,12 +66,12 @@ public static class FormUrlReqData private static OAuth2Configurations? _configurations; private static OAuth2Configurations Configurations => - _configurations ??= ConfigurationManager.Instance.GetConfiguration(); + _configurations ??= ConfigurationManager.Instance.GetConfiguration()!; /// /// 获取授权码的地址 /// - public static Lazy GetAuthCodeData = new(() => + public static readonly Lazy GetAuthCodeData = new(() => $"{RequestUrls.AuthCodeUri}?client_id={Configurations.ClientId}&response_type=code&redirect_uri=127.0.0.1:{Configurations.RedirectPort}&response_mode=query&scope=XboxLive.signin offline_access"); /// @@ -93,8 +93,7 @@ public static class FormUrlReqData new Dictionary { { "grant_type", "urn:ietf:params:oauth:grant-type:device_code" }, - { "client_id", Configurations.ClientId }, - { "device_code", "" } + { "client_id", Configurations.ClientId } }.ToImmutableDictionary()); /// @@ -105,7 +104,6 @@ public static class FormUrlReqData new Dictionary { { "client_id", Configurations.ClientId }, - { "code", "" }, { "grant_type", "authorization_code" }, { "redirect_uri", $"127.0.0.1:{Configurations.RedirectPort}" }, { "scope", "XboxLive.signin offline_access" } @@ -119,7 +117,6 @@ public static class FormUrlReqData { { "client_id", Configurations.ClientId }, { "client_secret", Configurations.ClientSecret }, - { "refresh_token", "" }, { "grant_type", "refresh_token" }, { "scope", "XboxLive.signin offline_access" } }.ToImmutableDictionary()); @@ -185,13 +182,13 @@ public sealed record AccessTokenResponse public sealed record UserAuthStateResponse { [property: JsonPropertyName("expires_in")] - public int? ExpiresIn { get; set; } + public required int ExpiresIn { get; set; } [property: JsonPropertyName("access_token")] - public string? AccessToken { get; set; } + public required string AccessToken { get; set; } [property: JsonPropertyName("refresh_token")] - public string? RefreshToken { get; set; } + public required string RefreshToken { get; set; } [property: JsonPropertyName("error")] public string? Error { get; set; } diff --git a/PCL.Neo.Core/Utils/Net.cs b/PCL.Neo.Core/Utils/Net.cs deleted file mode 100644 index 78b6682a..00000000 --- a/PCL.Neo.Core/Utils/Net.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Net.Http.Headers; -using System.Net.Http.Json; - -namespace PCL.Neo.Core.Utils; - -#pragma warning disable IL2026 // will fixed by dynamic dependency -public static class Net -{ - public static async Task SendHttpRequestAsync( - HttpMethod method, - Uri url, - object? content = null, - string? bearerToken = null) - { - using var request = new HttpRequestMessage(method, url); - - // 设置请求体 - if (content != null) - { - if (content is FormUrlEncodedContent formContent) - { - request.Content = formContent; - } - else - { - request.Content = JsonContent.Create(content); - } - } - - // 设置授权头 - if (!string.IsNullOrEmpty(bearerToken)) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); - } - - // 发送请求 - using var response = await Shared.HttpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - - // 解析响应 - var result = await response.Content.ReadFromJsonAsync().ConfigureAwait(false); - ArgumentNullException.ThrowIfNull(result); - - return result; - } -} diff --git a/PCL.Neo.Core/Utils/Net/HttpClientPool.cs b/PCL.Neo.Core/Utils/Net/HttpClientPool.cs new file mode 100644 index 00000000..966a8a22 --- /dev/null +++ b/PCL.Neo.Core/Utils/Net/HttpClientPool.cs @@ -0,0 +1,143 @@ +using System.Collections.Concurrent; + +// ReSharper disable InconsistentNaming + +namespace PCL.Neo.Core.Utils.Net; + +/// +/// 提供HTTP客户端池的静态类,防止多次创建HTTP客户端引起的性能消耗 +/// +internal static class HttpClientPool +{ + private static readonly ConcurrentDictionary _clients = new(); + private static readonly Timer _cleanupTimer; + private static readonly object _cleanupLock = new(); + + static HttpClientPool() + { + // 每60秒检查一次过期的HttpClient + _cleanupTimer = new Timer(CleanupExpiredClients, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60)); + } + + internal class HttpClientInfo(HttpClient client, DateTime expirationTime, bool inUse) + { + public HttpClient Client { get; } = client; + public DateTime ExpirationTime { get; } = expirationTime; + + public bool InUse { get; set; } = inUse; + } + + /// + /// 添加HTTP客户端到池中,可以指定自动销毁时间 + /// + /// 客户端名称 + /// HTTP客户端实例 + /// 客户端生命周期,默认为1小时 + public static HttpClientInfo AddHttpClient(string name, HttpClient client, TimeSpan? lifetime = null) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(client); + + var expirationTime = DateTime.UtcNow.Add(lifetime ?? TimeSpan.FromMinutes(5)); + var clientInfo = new HttpClientInfo(client, expirationTime, true); + + if (!_clients.TryAdd(name, clientInfo)) + { + // 如果已存在同名客户端,先释放旧的再添加新的 + if (_clients.TryRemove(name, out var oldClientInfo)) + { + oldClientInfo.Client.Dispose(); + } + + _clients.TryAdd(name, clientInfo); + } + + return clientInfo; + } + + /// + /// 获取指定名称的HTTP客户端实例,如果不存在或已过期则创建一个新的实例 + /// + /// 客户端名称 + /// 新建客户端的生命周期,默认为1小时 + /// HTTP客户端实例 + public static HttpClientInfo GetClient(string name, TimeSpan? lifetime = null) + { + ArgumentNullException.ThrowIfNull(name); + + if (_clients.TryGetValue(name, out var clientInfo)) + { + if (DateTime.UtcNow < clientInfo.ExpirationTime) + { + return clientInfo; + } + + // 如果客户端已过期,移除并释放它 + if (_clients.TryRemove(name, out var expiredClientInfo)) + { + expiredClientInfo.Client.Dispose(); + } + } + + // 创建新的客户端 + var client = new HttpClient(); + var returnClientInfo = AddHttpClient(name, client, lifetime); + return returnClientInfo; + } + + /// + /// 释放指定名称的HTTP客户端实例 + /// + /// 客户端名称 + /// 是否成功释放 + public static bool ReleaseClient(string name) + { + ArgumentNullException.ThrowIfNull(name); + + if (_clients.TryRemove(name, out var clientInfo)) + { + clientInfo.InUse = false; + clientInfo.Client.Dispose(); + return true; + } + + return false; + } + + /// + /// 清理所有过期的HTTP客户端 + /// + private static void CleanupExpiredClients(object? state) + { + // 使用锁防止多个清理操作同时进行 + if (!Monitor.TryEnter(_cleanupLock)) + { + return; + } + + try + { + var now = DateTime.UtcNow; + var expiredClients = _clients.Where(kvp => now >= kvp.Value.ExpirationTime) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var name in expiredClients) + { + if (_clients.TryRemove(name, out var clientInfo)) + { + if (clientInfo.InUse) + { + continue; + } + + clientInfo.Client.Dispose(); + } + } + } + finally + { + Monitor.Exit(_cleanupLock); + } + } +} diff --git a/PCL.Neo.Core/Utils/Net/INetService.cs b/PCL.Neo.Core/Utils/Net/INetService.cs new file mode 100644 index 00000000..30207ca7 --- /dev/null +++ b/PCL.Neo.Core/Utils/Net/INetService.cs @@ -0,0 +1,13 @@ +namespace PCL.Neo.Core.Utils.Net; + +public interface INetService +{ + Task SendAsync(HttpMethod httpMethod, string url); + Task SendAsync(HttpRequestMessage request); + + Task SendAsync(HttpMethod httpMethod, string url); + Task SendAsync(HttpRequestMessage request); + + Task SendJsonAsync(HttpMethod httpMethod, string url, TJson content); + Task SendJsonAsync(HttpMethod httpMethod, string url, TJson content); +} diff --git a/PCL.Neo.Core/Utils/Net/NetService.cs b/PCL.Neo.Core/Utils/Net/NetService.cs new file mode 100644 index 00000000..563498ca --- /dev/null +++ b/PCL.Neo.Core/Utils/Net/NetService.cs @@ -0,0 +1,321 @@ +using PCL.Neo.Core.Utils.Logger; +using System.Text; +using System.Text.Json; + +namespace PCL.Neo.Core.Utils.Net; + +/// +/// HTTP客户端服务实现 +/// +public class NetService : INetService, IDisposable +{ + private readonly string _token; + private readonly string _httpClientName; + private readonly HttpClientPool.HttpClientInfo _httpClientInfo; + + /// + /// 构造函数 + /// + /// HTTP客户端名称 + /// 基础URL(可选) + /// 令牌(可选) + public NetService( + string httpClientName, + string? baseUrl = null, + string? token = null) + { + _httpClientName = httpClientName ?? throw new ArgumentNullException(nameof(httpClientName)); + var baseUrl1 = baseUrl?.TrimEnd('/') ?? string.Empty; + _token = token ?? string.Empty; + _httpClientInfo = HttpClientPool.AddHttpClient(_httpClientName, new HttpClient + { + BaseAddress = new Uri(baseUrl1) + }); + } + + /// + /// 发送无内容信息 + /// + /// HTTP方法 + /// 目标URL + /// HTTP响应消息 + public async Task SendAsync(HttpMethod httpMethod, string url) + { + var httpClient = CreateHttpClient(); + + LogRequest(httpMethod, url, "无内容"); + + try + { + var request = new HttpRequestMessage(httpMethod, url); + var response = await httpClient.SendAsync(request); + + LogResponse(response); + return response; + } + catch (Exception ex) + { + LogError(httpMethod, url, ex); + throw; + } + } + + /// + public async Task SendAsync(HttpRequestMessage request) + { + var httpMethod = request.Method; + var url = request.RequestUri?.ToString() ?? string.Empty; + var httpClient = CreateHttpClient(); + + LogRequest(httpMethod, url, nameof(request)); + + try + { + var response = await httpClient.SendAsync(request); + + LogResponse(response); + return response; + } + catch (Exception e) + { + LogError(httpMethod, url, e); + throw; + } + } + + /// + /// 发送无内容信息,返回JSON反序列化对象 + /// + /// 返回对象类型 + /// HTTP方法 + /// 目标URL + /// 反序列化后的对象 + public async Task SendAsync(HttpMethod httpMethod, string url) + { + var httpClient = CreateHttpClient(); + + LogRequest(httpMethod, url, "无内容", typeof(T)); + + try + { + var request = new HttpRequestMessage(httpMethod, url); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + + LogResponse(response); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + +#nullable disable + var result = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result; + } + catch (Exception ex) + { + LogError(httpMethod, url, ex); + throw; + } +#nullable restore + } + + /// + public async Task SendAsync(HttpRequestMessage request) + { + var httpClient = CreateHttpClient(); + var method = request.Method; + var url = request.RequestUri?.ToString() ?? string.Empty; + LogRequest(method, url, nameof(request), typeof(T)); + + try + { + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + + LogResponse(response); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + +#nullable disable + var result = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result; + } + catch (Exception ex) + { + LogError(method, url, ex); + throw; + } +#nullable restore + } + + /// + /// 发送JSON内容 + /// + /// 发送的Json数据类型 + /// HTTP方法 + /// 目标URL + /// 要发送的内容对象 + /// HTTP响应消息 + public async Task SendJsonAsync(HttpMethod httpMethod, string url, TJson content) + { + var httpClient = CreateHttpClient(); + + LogRequest(httpMethod, url, "JSON内容", typeof(TJson)); + + try + { + var request = new HttpRequestMessage(httpMethod, url); + + if (content != null) + { + var jsonContent = JsonSerializer.Serialize(content); + request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + } + + var response = await httpClient.SendAsync(request); + + LogResponse(response); + return response; + } + catch (Exception ex) + { + LogError(httpMethod, url, ex); + throw; + } + } + + /// + /// 发送JSON内容,返回JSON反序列化对象 + /// + /// 返回对象类型 + /// 发送的Json数据类型 + /// HTTP方法 + /// 目标URL + /// 要发送的内容对象 + /// 反序列化后的对象 + public async Task SendJsonAsync(HttpMethod httpMethod, string url, TJson content) + { + var httpClient = CreateHttpClient(); + + LogRequest(httpMethod, url, "JSON内容", content?.GetType(), typeof(TResult)); + + try + { + var request = new HttpRequestMessage(httpMethod, url); + + var jsonContent = JsonSerializer.Serialize(content); + request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var response = await httpClient.SendAsync(request); + + LogResponse(response); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync(); +#nullable disable + var result = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result; + } + catch (Exception ex) + { + LogError(httpMethod, url, ex); + throw; + } +#nullable restore + } + + /// + /// 创建HTTP客户端 + /// + /// 配置好的HTTP客户端 + private HttpClient CreateHttpClient() + { + var httpClient = _httpClientInfo.Client; + // 如果有令牌,添加到请求头 + if (!string.IsNullOrEmpty(_token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token); + } + + return httpClient; + } + + /// + /// 记录请求日志 + /// + /// HTTP方法 + /// 目标URL + /// 内容类型描述 + /// 请求对象类型 + /// 响应对象类型 + private void LogRequest( + HttpMethod httpMethod, + string url, + string contentType, + Type? requestType = null, + Type? responseType = null) + { + var logMessage = $"HTTP请求 - " + + $"客户端: {_httpClientName} | " + + $"方法: {httpMethod} | " + + $"URL: {url} | " + + $"内容类型: {contentType}"; + + if (requestType != null) + logMessage += $" | 请求类型: {requestType.Name}"; + + if (responseType != null) + logMessage += $" | 响应类型: {responseType.Name}"; + + NewLogger.Logger.LogInformation(logMessage); + } + + /// + /// 记录响应日志 + /// + /// HTTP响应 + private void LogResponse(HttpResponseMessage response) + { + var logMessage = $"HTTP响应 - " + + $"客户端: {_httpClientName} | " + + $"状态码: {(int)response.StatusCode} {response.StatusCode} | " + + $"内容类型: {response.Content?.Headers?.ContentType?.MediaType ?? "未知"}"; + + NewLogger.Logger.LogInformation(logMessage); + } + + /// + /// 记录错误日志 + /// + /// HTTP方法 + /// 目标URL + /// 异常信息 + private void LogError(HttpMethod httpMethod, string url, Exception exception) + { + var logMessage = $"HTTP错误 - " + + $"客户端: {_httpClientName} | " + + $"方法: {httpMethod} | " + + $"URL: {url} | " + + $"错误: {exception.Message}"; + + NewLogger.Logger.LogError(logMessage); + } + + /// + public void Dispose() + { + _httpClientInfo.InUse = false; + } +} diff --git a/PCL.Neo.Core/Utils/Net/README.md b/PCL.Neo.Core/Utils/Net/README.md new file mode 100644 index 00000000..43beee83 --- /dev/null +++ b/PCL.Neo.Core/Utils/Net/README.md @@ -0,0 +1,85 @@ +# NetService 和 StaticNet 使用文档 + +## NetService + +### 概述 +NetService 是一个基于依赖注入模式的 HTTP 客户端服务实现,提供了丰富的 HTTP 请求功能,包括发送普通请求、发送 JSON 数据以及处理响应。 + +### 使用说明 + +#### 构造 NetService 实例 +NetService 需要通过依赖注入来使用。 + +```csharp +// 手动实例化 +INetService netService = new NetService("MyHttpClient", "https://api.example.com", "my-token"); +``` + +#### 发送 HTTP 请求 + +1. 发送无内容的请求: + +```csharp +var response = await netService.SendAsync(HttpMethod.Get, "https://api.example.com/resource"); +``` + +2. 发送带内容的请求: + +```csharp +var request = new HttpRequestMessage(HttpMethod.Post, "https://api.example.com/resource") +{ + Content = new StringContent("{\"key\":\"value\"}", Encoding.UTF8, "application/json") +}; +var response = await netService.SendAsync(request); +``` + +3. 发送 JSON 数据并获取反序列化结果: + +```csharp +var result = await netService.SendJsonAsync(HttpMethod.Post, "https://api.example.com/resource", new MyRequestType { Key = "value" }); +``` + +### 注意事项 +1. **依赖注入**:推荐使用依赖注入来管理 NetService 实例。 +2. **令牌管理**:确保传递正确的令牌以进行身份验证。 +3. **异常处理**:捕获可能的网络异常,例如 `HttpRequestException`。 +4. **线程安全**:NetService 的方法是线程安全的,可以在多线程环境中使用。 + +--- + +## StaticNet + +### 概述 +StaticNet 是一个静态类,提供了简单易用的 HTTP 客户端服务功能,适合快速实现 HTTP 请求。 + +### 使用说明 + +#### 发送 HTTP 请求 + +1. 发送无内容的请求: + +```csharp +var response = await StaticNet.SendAsync(HttpMethod.Get, new Uri("https://api.example.com/resource")); +``` + +2. 发送带内容的请求: + +```csharp +var request = new HttpRequestMessage(HttpMethod.Post, "https://api.example.com/resource") +{ + Content = new StringContent("{\"key\":\"value\"}", Encoding.UTF8, "application/json") +}; +var response = await StaticNet.SendAsync(request); +``` + +3. 发送 JSON 数据并获取反序列化结果: + +```csharp +var result = await StaticNet.SendJsonAsync(HttpMethod.Post, new Uri("https://api.example.com/resource"), new MyRequestType { Key = "value" }); +``` + +### 注意事项 +1. **静态类**:StaticNet 是静态类,无需通过依赖注入管理。 +2. **令牌管理**:确保传递正确的令牌以进行身份验证。 +3. **异常处理**:捕获可能的网络异常,例如 `HttpRequestException`。 +4. **线程安全**:StaticNet 的方法是线程安全的,可以在多线程环境中使用。 diff --git a/PCL.Neo.Core/Utils/Net/StaticNet.cs b/PCL.Neo.Core/Utils/Net/StaticNet.cs new file mode 100644 index 00000000..c58daf97 --- /dev/null +++ b/PCL.Neo.Core/Utils/Net/StaticNet.cs @@ -0,0 +1,348 @@ +using PCL.Neo.Core.Utils.Logger; +using System.Text; +using System.Text.Json; + +namespace PCL.Neo.Core.Utils.Net; + +/// +/// HTTP客户端服务实现 +/// +public static class StaticNet +{ + private static readonly string SharedName; + + static StaticNet() + { + var sharedName = Guid.NewGuid().ToString()[..8]; + var clientInfo = HttpClientPool.AddHttpClient(sharedName, new HttpClient(), TimeSpan.FromMinutes(5)); + clientInfo.InUse = false; + SharedName = sharedName; + } + + /// + /// 发送无内容信息 + /// + /// HTTP方法 + /// 目标URL + /// 令牌 + /// HTTP响应消息 + public static async Task SendAsync(HttpMethod httpMethod, Uri url, string? token = null) + { + var strUrl = url.ToString(); + LogRequest(httpMethod, strUrl, "无内容"); + + try + { + var httpClient = GetHttpClient(ref token); + if (!string.IsNullOrEmpty(token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + var request = new HttpRequestMessage(httpMethod, url); + var response = await httpClient.SendAsync(request); + + LogResponse(response); + return response; + } + catch (Exception ex) + { + LogError(httpMethod, strUrl, ex); + throw; + } + } + + /// + /// 发送信息 + /// + /// 请求内容 + /// 令牌 + /// HTTP响应消息 + public static async Task SendAsync(HttpRequestMessage request, string? token = null) + { + var httpMethod = request.Method; + var url = request.RequestUri?.ToString() ?? string.Empty; + LogRequest(httpMethod, url, nameof(request)); + + try + { + var httpClient = GetHttpClient(ref token); + if (!string.IsNullOrEmpty(token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + var response = await httpClient.SendAsync(request); + + LogResponse(response); + return response; + } + catch (Exception e) + { + LogError(httpMethod, url, e); + throw; + } + } + + /// + /// 发送无内容信息,返回JSON反序列化对象 + /// + /// 返回对象类型 + /// HTTP方法 + /// 目标URL + /// 令牌 + /// 反序列化后的对象 + public static async Task SendAsync(HttpMethod httpMethod, Uri url, string? token = null) + { + var strUrl = url.ToString(); + LogRequest(httpMethod, strUrl, "无内容", typeof(T)); + + try + { + var httpClient = GetHttpClient(ref token); + if (!string.IsNullOrEmpty(token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + var request = new HttpRequestMessage(httpMethod, url); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + + LogResponse(response); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + +#nullable disable + var result = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result; + } + catch (Exception ex) + { + LogError(httpMethod, strUrl, ex); + throw; + } +#nullable restore + } + + /// + /// 发送信息 + /// + /// 请求内容 + /// 令牌 + /// HTTP响应消息 + public static async Task SendAsync(HttpRequestMessage request, string? token = null) + { + var method = request.Method; + var url = request.RequestUri?.ToString() ?? string.Empty; + LogRequest(method, url, nameof(request), typeof(T)); + + try + { + var httpClient = GetHttpClient(ref token); + if (!string.IsNullOrEmpty(token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + + LogResponse(response); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + +#nullable disable + var result = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result; + } + catch (Exception ex) + { + LogError(method, url, ex); + throw; + } +#nullable restore + } + + /// + /// 发送JSON内容 + /// + /// 发送的Json数据类型 + /// HTTP方法 + /// 目标URL + /// 令牌 + /// 要发送的内容对象 + /// HTTP响应消息 + public static async Task SendJsonAsync(HttpMethod httpMethod, Uri url, TJson content, + string? token = null) + { + var strUrl = url.ToString(); + + LogRequest(httpMethod, strUrl, "JSON内容", typeof(TJson)); + + try + { + var request = new HttpRequestMessage(httpMethod, url); + + if (content != null) + { + var jsonContent = JsonSerializer.Serialize(content); + request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + } + + var httpClient = GetHttpClient(ref token); + if (!string.IsNullOrEmpty(token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + var response = await httpClient.SendAsync(request); + + LogResponse(response); + return response; + } + catch (Exception ex) + { + LogError(httpMethod, strUrl, ex); + throw; + } + } + + /// + /// 发送JSON内容,返回JSON反序列化对象 + /// + /// 返回对象类型 + /// 发送的Json数据类型 + /// HTTP方法 + /// 目标URL + /// 令牌 + /// 要发送的内容对象 + /// 反序列化后的对象 + public static async Task SendJsonAsync(HttpMethod httpMethod, Uri url, TJson content, + string? token = null) + { + var strUrl = url.ToString(); + + LogRequest(httpMethod, strUrl, "JSON内容", content?.GetType(), typeof(TResult)); + + try + { + var request = new HttpRequestMessage(httpMethod, url); + + var jsonContent = JsonSerializer.Serialize(content); + request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var httpClient = GetHttpClient(ref token); + if (!string.IsNullOrEmpty(token)) + { + httpClient.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + var response = await httpClient.SendAsync(request); + + LogResponse(response); + + response.EnsureSuccessStatusCode(); + var jsonString = await response.Content.ReadAsStringAsync(); +#nullable disable + var result = JsonSerializer.Deserialize(jsonString, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return result; + } + catch (Exception ex) + { + LogError(httpMethod, strUrl, ex); + throw; + } +#nullable restore + } + + /// + /// 记录请求日志 + /// + /// HTTP方法 + /// 目标URL + /// 内容类型描述 + /// 请求对象类型 + /// 响应对象类型 + private static void LogRequest( + HttpMethod httpMethod, + string url, + string contentType, + Type? requestType = null, + Type? responseType = null) + { + var logMessage = $"HTTP请求 - " + + $"方法: {httpMethod} | " + + $"URL: {url} | " + + $"内容类型: {contentType}"; + + if (requestType != null) + logMessage += $" | 请求类型: {requestType.Name}"; + + if (responseType != null) + logMessage += $" | 响应类型: {responseType.Name}"; + + NewLogger.Logger.LogInformation(logMessage); + } + + /// + /// 记录响应日志 + /// + /// HTTP响应 + private static void LogResponse(HttpResponseMessage response) + { + var logMessage = $"HTTP响应 - " + + $"状态码: {(int)response.StatusCode} {response.StatusCode} | " + + $"内容类型: {response.Content?.Headers?.ContentType?.MediaType ?? "未知"}"; + + NewLogger.Logger.LogInformation(logMessage); + } + + /// + /// 记录错误日志 + /// + /// HTTP方法 + /// 目标URL + /// 异常信息 + private static void LogError(HttpMethod httpMethod, string url, Exception exception) + { + var logMessage = $"HTTP错误 - " + + $"方法: {httpMethod} | " + + $"URL: {url} | " + + $"错误: {exception.Message}"; + + NewLogger.Logger.LogError(logMessage); + } + + private static HttpClient GetHttpClient(ref string? token) + { + if (string.IsNullOrEmpty(token)) + { + return new HttpClient(); + } + else + { + var clientInfo = HttpClientPool.GetClient(SharedName); + clientInfo.InUse = false; + return clientInfo.Client; + } + } +} diff --git a/PCL.Neo.Core/Utils/Result.cs b/PCL.Neo.Core/Utils/Result.cs deleted file mode 100644 index 6b38bc9e..00000000 --- a/PCL.Neo.Core/Utils/Result.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace PCL.Neo.Core.Utils; - -public class Result -{ - private readonly TError _error; - private readonly TOk _ok; - - private Result(TOk ok) - { - IsSuccess = true; - _ok = ok; - _error = default; - } - - private Result(TError error) - { - IsSuccess = false; - _ok = default; - _error = error; - } - - public bool IsSuccess { get; } - public bool IsFailure => !IsSuccess; - - public TOk Value => IsSuccess ? _ok : throw new InvalidOperationException("Result is a failure."); - public TError Error => IsFailure ? _error : throw new InvalidOperationException("Result is success."); - - public static Result Ok(TOk value) - { - return new Result(value); - } - - public static Result Fail(TError error) - { - return new Result(error); - } - - public TResult Match(Func onSuccess, Func onFailure) - { - ArgumentNullException.ThrowIfNull(onSuccess); - ArgumentNullException.ThrowIfNull(onFailure); - - return IsSuccess ? onSuccess(_ok) : onFailure(_error); - } - - public void Switch(Action onSuccess, Action onFailure) - { - ArgumentNullException.ThrowIfNull(onSuccess); - ArgumentNullException.ThrowIfNull(onFailure); - - if (IsSuccess) - { - onSuccess(_ok); - } - else - { - onFailure(_error); - } - } - - public Result Map(Func mapFunc) - { - ArgumentNullException.ThrowIfNull(mapFunc); - return IsSuccess - ? Result.Ok(mapFunc(_ok)) - : Result.Fail(_error); - } - - public Result MapError(Func mapFunc) - { - ArgumentNullException.ThrowIfNull(mapFunc); - return IsFailure - ? Result.Fail(mapFunc(_error)) - : Result.Ok(_ok); - } - - public Result AndThen( - Func> continuationFunc) - { - ArgumentNullException.ThrowIfNull(continuationFunc); - return IsSuccess ? continuationFunc(_ok) : Result.Fail(_error); - } - - public static implicit operator Result(TOk value) - { - return Ok(value); - } -} diff --git a/PCL.Neo/App.axaml.cs b/PCL.Neo/App.axaml.cs index 9224493a..c9b347da 100644 --- a/PCL.Neo/App.axaml.cs +++ b/PCL.Neo/App.axaml.cs @@ -8,6 +8,7 @@ using PCL.Neo.Core.Service.Accounts; using PCL.Neo.Core.Service.Accounts.MicrosoftAuth; using PCL.Neo.Core.Service.Game; +using PCL.Neo.Core.Utils.Net; using PCL.Neo.Services; using PCL.Neo.ViewModels; using PCL.Neo.Views; @@ -40,6 +41,7 @@ private static IServiceProvider ConfigureServices() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .BuildServiceProvider(); } diff --git a/PCL.Neo/Utils/BoolToOnlineStatusConverter.cs b/PCL.Neo/Utils/BoolToOnlineStatusConverter.cs index bc744ecd..a444567d 100644 --- a/PCL.Neo/Utils/BoolToOnlineStatusConverter.cs +++ b/PCL.Neo/Utils/BoolToOnlineStatusConverter.cs @@ -6,7 +6,7 @@ namespace PCL.Neo.Utils; public class BoolToOnlineStatusConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is bool isOnline) {