You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

336 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Amazon.SellingPartnerApiSDK.AmazonSpApiSDK.Models.Exceptions;
using Amazon.SellingPartnerApiSDK.AmazonSpApiSDK.Models.Token;
using Amazon.SellingPartnerApiSDK.Misc;
using Amazon.SellingPartnerApiSDK.Parameters;
using Newtonsoft.Json;
using RestSharp;
using RestSharp.Serializers.NewtonsoftJson;
using Method = RestSharp.Method;
using static Amazon.SellingPartnerApiSDK.Misc.Constants;
using static Amazon.SellingPartnerApiSDK.AmazonSpApiSDK.Models.Token.CacheTokenData;
namespace Amazon.SellingPartnerApiSDK.Services
{
public class RequestService : ApiUrls
{
public static readonly string AccessTokenHeaderName = "x-amz-access-token";
public static readonly string SecurityTokenHeaderName = "x-amz-security-token";
public static readonly string ShippingBusinessIdHeaderName = "x-amzn-shipping-business-id";
private const string RateLimitLimitHeaderName = "x-amzn-RateLimit-Limit";
protected RequestService(AmazonCredential amazonCredential)
{
AmazonCredential = amazonCredential;
AmazonSandboxUrl = amazonCredential.MarketPlace.Region.SandboxHostUrl;
AmazonProductionUrl = amazonCredential.MarketPlace.Region.HostUrl;
}
protected RestClient RequestClient { get; set; }
protected RestRequest Request { get; set; }
protected AmazonCredential AmazonCredential { get; set; }
protected string AmazonSandboxUrl { get; set; }
protected string AmazonProductionUrl { get; set; }
protected string AccessToken { get; set; }
protected IList<KeyValuePair<string, string>> LastHeaders { get; set; }
protected string ApiBaseUrl => AmazonCredential.Environment == Constants.Environments.Sandbox
? AmazonSandboxUrl
: AmazonProductionUrl;
public IList<KeyValuePair<string, string>> LastResponseHeader => LastHeaders;
private void CreateRequest(string url, Method method)
{
if (string.IsNullOrWhiteSpace(AmazonCredential.ProxyAddress))
{
var options = new RestClientOptions(ApiBaseUrl);
RequestClient = new RestClient(options,
configureSerialization: s => s.UseNewtonsoftJson());
}
else
{
var options = new RestClientOptions(ApiBaseUrl)
{
Proxy = new WebProxy()
{
Address = new Uri(AmazonCredential.ProxyAddress)
}
};
RequestClient = new RestClient(options,
configureSerialization: s => s.UseNewtonsoftJson());
}
Request = new RestRequest(url, method);
}
protected async Task CreateAuthorizedRequestAsync(string url, Method method,
List<KeyValuePair<string, string>> queryParameters = null, object postJsonObj = null,
CacheTokenData.TokenDataType tokenDataType = CacheTokenData.TokenDataType.Normal, object parameter = null,
CancellationToken cancellationToken = default)
{
if (parameter is IParameterBasedPII piiObject && piiObject.IsNeedRestrictedDataToken)
{
await RefreshTokenAsync(CacheTokenData.TokenDataType.PII, piiObject.RestrictedDataTokenRequest, cancellationToken);
}
else
{
await RefreshTokenAsync(tokenDataType, cancellationToken: cancellationToken);
}
CreateRequest(url, method);
if (postJsonObj != null)
AddJsonBody(postJsonObj);
if (queryParameters != null)
AddQueryParameters(queryParameters);
}
private async Task<T> ExecuteRequestTry<T>(RateLimitType rateLimitType = RateLimitType.UNSET,
CancellationToken cancellationToken = default) where T : new()
{
RestHeader();
AddAccessToken();
AddShippingBusinessId();
var response = await RequestClient.ExecuteAsync<T>(Request, cancellationToken);
LogRequest(Request, response);
SaveLastRequestHeader(response.Headers);
await SleepForRateLimit(response.Headers, rateLimitType, cancellationToken);
ParseResponse(response);
if (response.StatusCode == HttpStatusCode.OK && !string.IsNullOrEmpty(response.Content) &&
response.Data == null) response.Data = JsonConvert.DeserializeObject<T>(response.Content);
return response.Data;
}
private void SaveLastRequestHeader(IReadOnlyCollection<HeaderParameter> parameters)
{
LastHeaders = new List<KeyValuePair<string, string>>();
foreach (RestSharp.Parameter parameter in parameters ?? Enumerable.Empty<HeaderParameter>())
if (parameter != null && parameter.Name != null && parameter.Value != null)
LastHeaders.Add(new KeyValuePair<string, string>(parameter.Name, parameter.Value.ToString()));
}
private void LogRequest(RestRequest request, RestResponse response)
{
if (AmazonCredential.IsDebugMode)
{
var requestToLog = new
{
resource = request.Resource,
parameters = request.Parameters.Select(parameter => new
{
name = parameter.Name,
value = parameter.Value,
type = parameter.Type.ToString()
}),
method = request.Method.ToString()
};
var responseToLog = new
{
statusCode = response.StatusCode,
content = response.Content,
headers = response.Headers,
responseUri = response.ResponseUri,
errorMessage = response.ErrorMessage
};
Console.WriteLine("\n\n");
Console.WriteLine("Request completed, \nRequest: {0} \n\nResponse: {1}",
JsonConvert.SerializeObject(requestToLog), JsonConvert.SerializeObject(responseToLog));
}
}
private void RestHeader()
{
lock (Request)
{
Request?.Parameters?.RemoveParameter(AccessTokenHeaderName);
Request?.Parameters?.RemoveParameter(SecurityTokenHeaderName);
Request?.Parameters?.RemoveParameter(ShippingBusinessIdHeaderName);
}
}
protected async Task<T> ExecuteRequestAsync<T>(RateLimitType rateLimitType = RateLimitType.UNSET,
CancellationToken cancellationToken = default) where T : new()
{
var tryCount = 0;
while (true)
try
{
return await ExecuteRequestTry<T>(rateLimitType, cancellationToken);
}
catch (AmazonQuotaExceededException ex)
{
if (tryCount >= AmazonCredential.MaxThrottledRetryCount)
{
if (AmazonCredential.IsDebugMode)
Console.WriteLine("已达到最大的尝试次数");
throw;
}
cancellationToken.ThrowIfCancellationRequested();
AmazonCredential.UsagePlansTimings[rateLimitType].Delay();
tryCount++;
}
}
private async Task SleepForRateLimit(IReadOnlyCollection<Parameter> headers,
RateLimitType rateLimitType = RateLimitType.UNSET, CancellationToken cancellationToken = default)
{
try
{
decimal rate = 0;
var limitHeader = headers.FirstOrDefault(a => a.Name == RateLimitLimitHeaderName);
if (limitHeader != null)
{
var rateLimitValue = limitHeader.Value.ToString();
decimal.TryParse(rateLimitValue, NumberStyles.Any, CultureInfo.InvariantCulture, out rate);
}
if (AmazonCredential.IsActiveLimitRate)
{
if (rateLimitType == RateLimitType.UNSET)
{
if (rate > 0)
{
var sleepTime = (int)(1 / rate * 1000);
await Task.Delay(sleepTime, cancellationToken);
}
}
else
{
if (rate > 0) AmazonCredential.UsagePlansTimings[rateLimitType].SetRateLimit(rate);
var time = AmazonCredential.UsagePlansTimings[rateLimitType].NextRate(rateLimitType);
}
}
}
catch (Exception)
{
// ignored
}
}
private void ParseResponse(RestResponse response)
{
if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ||
response.StatusCode == HttpStatusCode.Created || response.StatusCode == HttpStatusCode.NoContent)
return;
else if (response.StatusCode == HttpStatusCode.NotFound)
{
throw new AmazonNotFoundException("Resource that you are looking for is not found", response);
}
else
{
if (AmazonCredential.IsDebugMode)
Console.WriteLine("Amazon Api didn't respond with Okay, see exception for more details" +
response.Content);
var errorResponse = response.Content.ConvertToErrorResponse();
if (errorResponse != null)
{
var error = errorResponse.Errors.FirstOrDefault();
switch (error.Code)
{
case "Unauthorized":
throw new AmazonUnauthorizedException(error.Message, response);
case "InvalidSignature":
throw new AmazonInvalidSignatureException(error.Message, response);
case "InvalidInput":
throw new AmazonInvalidInputException(error.Message, error.Details, response);
case "QuotaExceeded":
throw new AmazonQuotaExceededException(error.Message, response);
case "InternalFailure":
throw new AmazonInternalErrorException(error.Message, response);
}
}
}
if (response.StatusCode == HttpStatusCode.BadRequest)
{
throw new AmazonBadRequestException(
"BadRequest see https://developer-docs.amazon.com/sp-api/changelog/api-request-validation-for-400-errors-with-html-response for advice",
response);
}
throw new AmazonException("Amazon Api didn't respond with Okay, see exception for more details", response);
}
private void AddQueryParameters(List<KeyValuePair<string, string>> queryParameters)
{
if (queryParameters != null)
queryParameters.ForEach(qp => Request.AddQueryParameter(qp.Key, qp.Value));
}
private void AddJsonBody(object jsonData)
{
var json = JsonConvert.SerializeObject(jsonData);
Request.AddJsonBody(json);
}
private void AddAccessToken()
{
lock (Request)
{
Request.AddOrUpdateHeader(AccessTokenHeaderName, AccessToken);
}
}
private void AddShippingBusinessId()
{
if (AmazonCredential.ShippingBusiness.HasValue)
Request.AddOrUpdateHeader(ShippingBusinessIdHeaderName,
AmazonCredential.ShippingBusiness.Value.GetEnumMemberValue());
}
private async Task RefreshTokenAsync(CacheTokenData.TokenDataType tokenDataType = CacheTokenData.TokenDataType.Normal,
CreateRestrictedDataTokenRequest requestPii = null, CancellationToken cancellationToken = default)
{
var token = AmazonCredential.GetToken(tokenDataType);
if (token == null)
{
if (tokenDataType == CacheTokenData.TokenDataType.PII)
{
var pii = await CreateRestrictedDataTokenAsync(requestPii, cancellationToken);
if (pii != null)
token = new TokenResponse
{
access_token = pii.RestrictedDataToken,
expires_in = pii.ExpiresIn
};
else
throw new ArgumentNullException(nameof(pii));
}
else
{
token = await TokenGeneration.RefreshAccessTokenAsync(AmazonCredential, tokenDataType,
cancellationToken);
}
AmazonCredential.SetToken(tokenDataType, token);
}
AccessToken = token.access_token;
}
private async Task<CreateRestrictedDataTokenResponse> CreateRestrictedDataTokenAsync(
CreateRestrictedDataTokenRequest createRestrictedDataTokenRequest,
CancellationToken cancellationToken = default)
{
await CreateAuthorizedRequestAsync(TokenApiUrls.RestrictedDataToken, Method.Post,
postJsonObj: createRestrictedDataTokenRequest, cancellationToken: cancellationToken);
var response = await ExecuteRequestAsync<CreateRestrictedDataTokenResponse>(
RateLimitType.Token_CreateRestrictedDataToken, cancellationToken: cancellationToken);
return response;
}
}
}