Unify WebBrowser API in regards to nullable bodies (#2593)

* Unify logic for nullable bodies

* Update ArchiWebHandler.cs

* Misc
This commit is contained in:
Łukasz Domeradzki 2022-06-01 21:13:40 +02:00 committed by GitHub
parent d82df0074f
commit 715ed034df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 56 additions and 165 deletions

View file

@ -41,7 +41,7 @@ internal static class CatAPI {
ObjectResponse<MeowResponse>? response = await webBrowser.UrlGetToJsonObject<MeowResponse>(request).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return null;
}

View file

@ -530,7 +530,7 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionInProgress, appTokens.Count, packageTokens.Count, depotKeys.Count));
OptionalObjectResponse<ResponseData>? response = await ASF.WebBrowser.UrlPostToOptionalJsonObject<ResponseData, RequestData>(request, data: requestData, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
ObjectResponse<ResponseData>? response = await ASF.WebBrowser.UrlPostToJsonObject<ResponseData, RequestData>(request, data: requestData, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
if (response == null) {
ASF.ArchiLogger.LogGenericWarning(ArchiSteamFarm.Localization.Strings.WarningFailed);

View file

@ -93,7 +93,7 @@ internal static class ArchiNet {
ObjectResponse<ChecksumResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject<ChecksumResponse>(request).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return null;
}

View file

@ -181,7 +181,7 @@ public sealed class ArchiWebHandler : IDisposable {
response = await UrlGetToJsonObjectWithSession<InventoryResponse>(request, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors, rateLimitingDelay: rateLimitingDelay).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
throw new HttpRequestException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(response)));
}
@ -210,7 +210,7 @@ public sealed class ArchiWebHandler : IDisposable {
}
}
if (response == null) {
if (response?.Content == null) {
throw new HttpRequestException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, nameof(response)));
}
@ -434,7 +434,7 @@ public sealed class ArchiWebHandler : IDisposable {
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
response = await UrlPostToJsonObjectWithSession<TradeOfferSendResponse>(request, data: data, referer: referer, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return (false, mobileTradeOfferIDs);
}
@ -453,7 +453,7 @@ public sealed class ArchiWebHandler : IDisposable {
}
}
if (response == null) {
if (response?.Content == null) {
return (false, mobileTradeOfferIDs);
}
@ -1231,7 +1231,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<ResultResponse>? response = await UrlPostToJsonObjectWithSession<ResultResponse>(request, data: data).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return false;
}
@ -1263,7 +1263,7 @@ public sealed class ArchiWebHandler : IDisposable {
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
response = await UrlPostToJsonObjectWithSession<TradeOfferAcceptResponse>(request, data: data, referer: referer, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return (false, false);
}
@ -1282,7 +1282,7 @@ public sealed class ArchiWebHandler : IDisposable {
}
}
return response != null ? (true, response.Content.RequiresMobileConfirmation) : (false, false);
return response?.Content != null ? (true, response.Content.RequiresMobileConfirmation) : (false, false);
}
internal async Task<(EResult Result, EPurchaseResultDetail PurchaseResult)> AddFreeLicense(uint subID) {
@ -1354,7 +1354,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<ResultResponse>? response = await UrlPostToJsonObjectWithSession<ResultResponse>(request, data: data).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return false;
}
@ -1400,7 +1400,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<NewDiscoveryQueueResponse>? response = await UrlPostToJsonObjectWithSession<NewDiscoveryQueueResponse>(request, data: data).ConfigureAwait(false);
return response?.Content.Queue;
return response?.Content?.Queue;
}
internal async Task<HashSet<TradeOffer>?> GetActiveTradeOffers() {
@ -1717,7 +1717,7 @@ public sealed class ArchiWebHandler : IDisposable {
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return null;
}
@ -1763,7 +1763,7 @@ public sealed class ArchiWebHandler : IDisposable {
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return null;
}
@ -1855,7 +1855,7 @@ public sealed class ArchiWebHandler : IDisposable {
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
IElement? htmlNode = response?.Content.SelectSingleNode("//div[@class='pagecontent']/script");
IElement? htmlNode = response?.Content?.SelectSingleNode("//div[@class='pagecontent']/script");
if (htmlNode == null) {
// Trade can be no longer valid
@ -2005,7 +2005,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<BooleanResponse>? response = await UrlGetToJsonObjectWithSession<BooleanResponse>(request).ConfigureAwait(false);
return response?.Content.Success;
return response?.Content?.Success;
}
internal async Task<bool?> HandleConfirmations(string deviceID, string confirmationHash, uint time, IReadOnlyCollection<Confirmation> confirmations, bool accept) {
@ -2059,7 +2059,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<BooleanResponse>? response = await UrlPostToJsonObjectWithSession<BooleanResponse>(request, data: data).ConfigureAwait(false);
return response?.Content.Success;
return response?.Content?.Success;
}
internal async Task<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string? parentalCode = null) {
@ -2253,7 +2253,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<RedeemWalletResponse>? response = await UrlPostToJsonObjectWithSession<RedeemWalletResponse>(request, data: data).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return null;
}
@ -2292,7 +2292,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<ResultResponse>? response = await UrlPostToJsonObjectWithSession<ResultResponse>(request, data: data).ConfigureAwait(false);
return response?.Content.Result == EResult.OK;
return response?.Content?.Result == EResult.OK;
}
private async Task<(ESteamApiKeyState State, string? Key)> GetApiKeyState() {
@ -2300,7 +2300,7 @@ public sealed class ArchiWebHandler : IDisposable {
using HtmlDocumentResponse? response = await UrlGetToHtmlDocumentWithSession(request, checkSessionPreemptively: false).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return (ESteamApiKeyState.Timeout, null);
}
@ -2570,7 +2570,7 @@ public sealed class ArchiWebHandler : IDisposable {
ObjectResponse<AccessTokenResponse>? response = await UrlGetToJsonObjectWithSession<AccessTokenResponse>(request).ConfigureAwait(false);
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
return !string.IsNullOrEmpty(response?.Content.Data.WebAPIToken) ? (true, response!.Content.Data.WebAPIToken) : (false, null);
return !string.IsNullOrEmpty(response?.Content?.Data.WebAPIToken) ? (true, response!.Content!.Data.WebAPIToken) : (false, null);
}
private async Task<(bool Success, string? Result)> ResolveApiKey() {

View file

@ -73,19 +73,19 @@ internal static class GitHub {
Uri request = new($"{SharedInfo.ProjectURL}/wiki/{page}/_history");
using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
if (response == null) {
return null;
}
if (response.StatusCode.IsClientErrorCode()) {
if (response?.StatusCode.IsClientErrorCode() == true) {
return response.StatusCode switch {
HttpStatusCode.NotFound => new Dictionary<string, DateTime>(0),
_ => null
};
}
if (response?.Content == null) {
return null;
}
IEnumerable<IElement> revisionNodes = response.Content.SelectNodes("//li[contains(@class, 'wiki-history-revision')]");
Dictionary<string, DateTime> result = new();
@ -148,7 +148,7 @@ internal static class GitHub {
using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false);
if (response == null) {
if (response?.Content == null) {
return null;
}

View file

@ -23,22 +23,19 @@ using System;
using System.Threading.Tasks;
using AngleSharp;
using AngleSharp.Dom;
using ArchiSteamFarm.Core;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Web.Responses;
public sealed class HtmlDocumentResponse : BasicResponse, IDisposable {
[PublicAPI]
public IDocument Content { get; }
public IDocument? Content { get; }
private HtmlDocumentResponse(BasicResponse basicResponse, IDocument content) : base(basicResponse) {
ArgumentNullException.ThrowIfNull(basicResponse);
public HtmlDocumentResponse(BasicResponse basicResponse) : base(basicResponse) => ArgumentNullException.ThrowIfNull(basicResponse);
Content = content ?? throw new ArgumentNullException(nameof(content));
}
private HtmlDocumentResponse(BasicResponse basicResponse, IDocument content) : this(basicResponse) => Content = content ?? throw new ArgumentNullException(nameof(content));
public void Dispose() => Content.Dispose();
public void Dispose() => Content?.Dispose();
[PublicAPI]
public static async Task<HtmlDocumentResponse?> Create(StreamResponse streamResponse) {
@ -46,14 +43,8 @@ public sealed class HtmlDocumentResponse : BasicResponse, IDisposable {
IBrowsingContext context = BrowsingContext.New();
try {
IDocument document = await context.OpenAsync(req => req.Content(streamResponse.Content, true)).ConfigureAwait(false);
IDocument document = await context.OpenAsync(request => request.Content(streamResponse.Content, true)).ConfigureAwait(false);
return new HtmlDocumentResponse(streamResponse, document);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericWarningException(e);
return null;
}
return new HtmlDocumentResponse(streamResponse, document);
}
}

View file

@ -26,11 +26,9 @@ namespace ArchiSteamFarm.Web.Responses;
public sealed class ObjectResponse<T> : BasicResponse {
[PublicAPI]
public T Content { get; }
public T? Content { get; }
public ObjectResponse(BasicResponse basicResponse, T content) : base(basicResponse) {
ArgumentNullException.ThrowIfNull(basicResponse);
public ObjectResponse(BasicResponse basicResponse, T? content) : this(basicResponse) => Content = content;
Content = content ?? throw new ArgumentNullException(nameof(content));
}
public ObjectResponse(BasicResponse basicResponse) : base(basicResponse) => ArgumentNullException.ThrowIfNull(basicResponse);
}

View file

@ -1,38 +0,0 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Web.Responses;
public sealed class OptionalObjectResponse<T> : BasicResponse {
[PublicAPI]
public T? Content { get; }
public OptionalObjectResponse(BasicResponse basicResponse, T? content) : base(basicResponse) {
ArgumentNullException.ThrowIfNull(basicResponse);
Content = content;
}
public OptionalObjectResponse(BasicResponse basicResponse) : base(basicResponse) => ArgumentNullException.ThrowIfNull(basicResponse);
}

View file

@ -266,6 +266,10 @@ public sealed class WebBrowser : IDisposable {
try {
return await HtmlDocumentResponse.Create(response).ConfigureAwait(false);
} catch (Exception e) {
if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) {
return new HtmlDocumentResponse(response);
}
ArchiLogger.LogGenericWarningException(e);
ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
}
@ -332,6 +336,10 @@ public sealed class WebBrowser : IDisposable {
obj = serializer.Deserialize<T>(jsonReader);
} catch (Exception e) {
if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) {
return new ObjectResponse<T>(response);
}
ArchiLogger.LogGenericWarningException(e);
ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
@ -339,6 +347,10 @@ public sealed class WebBrowser : IDisposable {
}
if (obj is null) {
if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) {
return new ObjectResponse<T>(response);
}
ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(obj)));
continue;
@ -573,6 +585,10 @@ public sealed class WebBrowser : IDisposable {
try {
return await HtmlDocumentResponse.Create(response).ConfigureAwait(false);
} catch (Exception e) {
if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) {
return new HtmlDocumentResponse(response);
}
ArchiLogger.LogGenericWarningException(e);
ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
}
@ -599,82 +615,6 @@ public sealed class WebBrowser : IDisposable {
throw new ArgumentOutOfRangeException(nameof(rateLimitingDelay));
}
for (byte i = 0; i < maxTries; i++) {
if ((i > 0) && (rateLimitingDelay > 0)) {
await Task.Delay(rateLimitingDelay).ConfigureAwait(false);
}
StreamResponse? response = await UrlPostToStream(request, headers, data, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay).ConfigureAwait(false);
if (response == null) {
// Request timed out, try again
continue;
}
await using (response.ConfigureAwait(false)) {
if (response.StatusCode.IsRedirectionCode()) {
if (!requestOptions.HasFlag(ERequestOptions.ReturnRedirections)) {
// We're not handling this error, do not try again
break;
}
} else if (response.StatusCode.IsClientErrorCode()) {
if (!requestOptions.HasFlag(ERequestOptions.ReturnClientErrors)) {
// We're not handling this error, do not try again
break;
}
} else if (response.StatusCode.IsServerErrorCode()) {
if (!requestOptions.HasFlag(ERequestOptions.ReturnServerErrors)) {
// We're not handling this error, try again
continue;
}
}
TResult? obj;
try {
using StreamReader steamReader = new(response.Content);
using JsonReader jsonReader = new JsonTextReader(steamReader);
JsonSerializer serializer = new();
obj = serializer.Deserialize<TResult>(jsonReader);
} catch (Exception e) {
ArchiLogger.LogGenericWarningException(e);
ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
continue;
}
if (obj is null) {
ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(obj)));
continue;
}
return new ObjectResponse<TResult>(response, obj);
}
}
if (maxTries > 1) {
ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, maxTries));
ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request));
}
return null;
}
[PublicAPI]
public async Task<OptionalObjectResponse<TResult>?> UrlPostToOptionalJsonObject<TResult, TData>(Uri request, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, TData? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) where TData : class {
ArgumentNullException.ThrowIfNull(request);
if (maxTries == 0) {
throw new ArgumentOutOfRangeException(nameof(maxTries));
}
if (rateLimitingDelay < 0) {
throw new ArgumentOutOfRangeException(nameof(rateLimitingDelay));
}
for (byte i = 0; i < maxTries; i++) {
if ((i > 0) && (rateLimitingDelay > 0)) {
await Task.Delay(rateLimitingDelay).ConfigureAwait(false);
@ -716,7 +656,7 @@ public sealed class WebBrowser : IDisposable {
obj = serializer.Deserialize<TResult>(jsonReader);
} catch (Exception e) {
if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) {
return new OptionalObjectResponse<TResult>(response);
return new ObjectResponse<TResult>(response);
}
ArchiLogger.LogGenericWarningException(e);
@ -727,7 +667,7 @@ public sealed class WebBrowser : IDisposable {
if (obj is null) {
if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) {
return new OptionalObjectResponse<TResult>(response);
return new ObjectResponse<TResult>(response);
}
ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(obj)));
@ -735,7 +675,7 @@ public sealed class WebBrowser : IDisposable {
continue;
}
return new OptionalObjectResponse<TResult>(response, obj);
return new ObjectResponse<TResult>(response, obj);
}
}