Skip to content

Commit

Permalink
WIP: POC of fax, small PR to see if i am on the right path
Browse files Browse the repository at this point in the history
  • Loading branch information
spacedsweden committed Jan 12, 2024
1 parent 0c4da8d commit c5bf9ee
Show file tree
Hide file tree
Showing 13 changed files with 856 additions and 3 deletions.
79 changes: 78 additions & 1 deletion src/Sinch/Core/Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,85 @@ public Task<TResponse> Send<TResponse>(Uri uri, HttpMethod httpMethod,
return Send<object, TResponse>(uri, httpMethod, null, cancellationToken);
}


public async Task<TResponse> Send<TResponse>(Uri uri, HttpMethod httpMethod, HttpContent httpContent,
CancellationToken cancellationToken = default)
{
var retry = true;
while (true)
{
_logger?.LogDebug("Sending request to {uri}", uri);


#if DEBUG
Debug.WriteLine($"Request uri: {uri}");
Debug.WriteLine($"Request body: {httpContent?.ReadAsStringAsync(cancellationToken).Result}");
#endif

using var msg = new HttpRequestMessage();
msg.RequestUri = uri;
msg.Method = httpMethod;
msg.Content = httpContent;

string token;
// Due to all the additional params appSignAuth is requiring,
// it's makes sense to still keep it in Http to manage all the details.
// TODO: get insight how to refactor this ?!?!?!
if (_auth is ApplicationSignedAuth appSignAuth)
{
var now = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture);
const string headerName = "x-timestamp";
msg.Headers.Add(headerName, now);
token = appSignAuth.GetSignedAuth(
msg.Content?.ReadAsByteArrayAsync(cancellationToken).GetAwaiter().GetResult(),
msg.Method.ToString().ToUpperInvariant(), msg.RequestUri.PathAndQuery,
$"{headerName}:{now}", msg.Content?.Headers?.ContentType?.ToString());
retry = false;
}
else
{
// try force get new token if retrying
token = await _auth.GetAuthToken(force: !retry);
}

msg.Headers.Authorization = new AuthenticationHeaderValue(_auth.Scheme, token);

msg.Headers.Add("User-Agent", _userAgentHeaderValue);

var result = await _httpClient.SendAsync(msg, cancellationToken);

if (result.StatusCode == HttpStatusCode.Unauthorized && retry)
{
// will not retry when no "expired" header for a token.
const string wwwAuthenticateHeader = "www-authenticate";
if (_auth.Scheme == AuthSchemes.Bearer && true == result.Headers?.Contains(wwwAuthenticateHeader) &&
false == result.Headers?.GetValues(wwwAuthenticateHeader)?.Contains("expired"))
{
_logger?.LogDebug("OAuth Unauthorized");
}
else
{
retry = false;
continue;
}
}

await result.EnsureSuccessApiStatusCode();
_logger?.LogDebug("Finished processing request for {uri}", uri);
if (result.IsJson())
return await result.Content.ReadFromJsonAsync<TResponse>(cancellationToken: cancellationToken,
options: _jsonSerializerOptions)
?? throw new NullReferenceException(
$"{nameof(TResponse)} is null");

_logger?.LogWarning("Response is not json, but {content}",
await result.Content.ReadAsStringAsync(cancellationToken));
return default;
}
}

public async Task<TResponse> Send<TRequest, TResponse>(Uri uri, HttpMethod httpMethod, TRequest request,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default)
{
var retry = true;
while (true)
Expand Down
11 changes: 11 additions & 0 deletions src/Sinch/Faxes/Barcode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Sinch.Faxes
{

public class Barcode
{
public string type { get; set; }
public int page { get; set; }
public string value { get; set; }
}

}
10 changes: 10 additions & 0 deletions src/Sinch/Faxes/Direction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sinch.Faxes
{

public enum Direction
{
INBOUND,
OUTBOUND
}

}
10 changes: 10 additions & 0 deletions src/Sinch/Faxes/ErrorType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sinch.Faxes
{
public enum ErrorType
{
DOCUMENT_CONVERSION_ERROR,
CALL_ERROR,
FAX_ERROR

}
}
71 changes: 71 additions & 0 deletions src/Sinch/Faxes/Fax.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Sinch.Faxes
{

/// <summary>
/// Fax object, see https://developers.sinch.com/docs/fax for more information
/// </summary>
public class Fax
{
[ReadOnly(true)]
public string Id { get; init; }
/// <summary>
/// Diection fax was sent, inbound someone sent a fax to your sinch number, outbound you sent a fax to someone
/// </summary>
[ReadOnly(true)]
public Direction Direction { get; init; }
/// <summary>
/// e164 formatted phone number where the fax was from
/// </summary>
///
[DataType(DataType.PhoneNumber)]
public string From { get; init; }
public string To { get; set; }

[DataType(DataType.Url)]
public string ContentUrl { get; set; }
[ReadOnly(true)]
public int NumberOfPages { get; init; }
[ReadOnly(true)]
public FaxStatus Status { get; init; }
[ReadOnly(true)]
public Money Price { get; init; }
[ReadOnly(true)]
public Barcode[] BarCodes { get; init; }
[ReadOnly(true)]
public string CreateTime { get; init; }
[ReadOnly(true)]
public DateTime CompletedTime { get; init; }
public string HeaderText { get; set; }
public bool HeaderPageNumbers { get; set; } = true;
public string HeaderTimeZone { get; set; }
public int RetryDelaySeconds { get; set; }
public int CancelTimeoutMinutes { get; set; }
public Dictionary<string, string> Labels { get; set; }
[DataType(DataType.Url)]
public string CallbackUrl { get; set; }
/// <summary>
/// valid values aer multipart/form-data or application/json
/// </summary>
public string CallbackContentType { get; set; }
public ImageConversionMethod ImageConversionMethod { get; set; }
[ReadOnly(true)]
public ErrorType ErrorType { get; init; }
[ReadOnly(true)]
public int ErrorId { get; init; }
[ReadOnly(true)]
public string ErrorCode { get; init;}
[ReadOnly(true)]
public string ProjectId { get; init; }
public string ServiceId { get; set; }
public int MaxRetries { get; set; }
[ReadOnly(true)]
public int RetryCount { get; init; }
[ReadOnly(true)]
public string HasFile { get; init; }
}
}
29 changes: 29 additions & 0 deletions src/Sinch/Faxes/FaxClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Sinch.Core;
using Sinch.Logger;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sinch.Faxes
{
public class FaxClient
{
private readonly string projectId;
private readonly Uri uri;
private readonly LoggerFactory loggerFactory;
private readonly Http httpSnakeCase;

internal FaxClient(string projectId, Uri uri, Logger.LoggerFactory _loggerFactory, Core.Http httpSnakeCase) {
this.projectId = projectId;
this.uri = uri;
loggerFactory = _loggerFactory;
this.httpSnakeCase = httpSnakeCase;
Faxes = new Faxes(projectId, uri, loggerFactory?.Create<Faxes>(), httpSnakeCase);
// Services = new Services(projectId, uri, loggerFactory?.Create<Services>(), httpSnakeCase);
}
public Faxes Faxes { get; }
// public Services Services { get; }
}
}
11 changes: 11 additions & 0 deletions src/Sinch/Faxes/FaxStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Sinch.Faxes
{
public enum FaxStatus
{
QUEUED,
IN_PROGRESS,
COMPLETED,
FAILURE

}
}
142 changes: 142 additions & 0 deletions src/Sinch/Faxes/Faxes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using Sinch.Core;
using Sinch.Logger;
using Sinch.SMS.Batches.Send;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.IO.Enumeration;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reflection;
using System.Reflection.Emit;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Sinch.Faxes
{
public partial class Faxes
{
private readonly string projectId;
private readonly Uri uri;

private readonly Http http;
private ILoggerAdapter<Faxes> loggerAdapter;
private Http httpSnakeCase1;

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x, net6.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x, net6.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x, net6.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x, net6.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x, net6.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (6.0.x, net6.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x, net7.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x, net7.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x, net7.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x, net7.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x, net7.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (7.0.x, net7.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x, net8.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x, net8.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x, net8.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x, net8.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x, net8.0)

The field 'Faxes.httpSnakeCase1' is never used

Check warning on line 29 in src/Sinch/Faxes/Faxes.cs

View workflow job for this annotation

GitHub Actions / build (8.0.x, net8.0)

The field 'Faxes.httpSnakeCase1' is never used
private FileExtensionContentTypeProvider mimeMapper;


internal Faxes(string projectId, Uri uri, ILoggerAdapter<Faxes> loggerAdapter, Http httpCamelCase)
{
this.projectId = projectId;
this.uri = uri;
this.loggerAdapter = loggerAdapter;
this.http = httpCamelCase;
mimeMapper = new FileExtensionContentTypeProvider();
uri = new Uri(uri, $"/v3/projects/{projectId}/faxes");
}

public async Task<Fax> Send(string to, string filePath, string from = "")
{
var fileContent = new StreamContent(File.OpenRead(filePath));
var fileName = Path.GetFileName(filePath);
return await Send(to, fileContent, fileName, from);
}
public async Task<Fax> Send(string to, StreamContent file, string fileName, string from="")
{
var fax = new Fax
{
To = to,
From = from
};
return await Send(fax, file, fileName);

}

/// <summary>
///
/// </summary>
/// <param name="to">Number to send to</param>
/// <param name="filePath">Path to file to fax</param>
/// <param name="from">Sinch number you want to set as from </param>
/// <param name="headerText">Header text of fax</param>
/// <param name="headerPageNumbers">Print page number on fax default true</param>
/// <param name="headerTimeZone">Set specific timezone</param>
/// <param name="retryDelaySeconds">Duration between retries</param>
/// <param name="cancelTimeoutMinutes">Cancel retries or fax transmission after x minutes</param>
/// <param name="labels">Custom labels you can tag a fax with</param>
/// <param name="callbackUrl">Call back url to notify when fax is completed or failed</param>
/// <param name="callbackContentType">JSON or multipart</param>
/// <param name="imageConversionMethod">defautl halftone and best in most scenarios</param>
/// <param name="serviceId"></param>
/// <param name="maxRetries"></param>
/// <returns></returns>
public async Task<Fax> Send(string to, string filePath, string from = "", string headerText = "", string contentUrl="", bool headerPageNumbers = true, string headerTimeZone = "", int retryDelaySeconds = 60, int cancelTimeoutMinutes = 3, Dictionary<string, string> labels = null, string callbackUrl = "", string callbackContentType = "", ImageConversionMethod imageConversionMethod = ImageConversionMethod.HALFTONE, string serviceId = "", int maxRetries = 0)
{
var fileContent = new StreamContent(File.OpenRead(filePath));
var fileName = System.IO.Path.GetFileName(filePath);
var fax = new Fax
{
To = to,
From = from,
HeaderText = headerText,
ContentUrl = contentUrl,
HeaderPageNumbers = headerPageNumbers,
HeaderTimeZone = headerTimeZone,
RetryDelaySeconds = retryDelaySeconds,
CancelTimeoutMinutes = cancelTimeoutMinutes,
Labels = labels,
CallbackUrl = callbackUrl,
CallbackContentType = callbackContentType,
ImageConversionMethod = imageConversionMethod,
ServiceId = serviceId,
MaxRetries = maxRetries
};
return await Send(fax, fileContent, fileName);
}


private MultipartFormDataContent SerializeFaxToMultipart(Fax fax)
{
var content = new MultipartFormDataContent();
var props = typeof(Fax).GetProperties(BindingFlags.Instance | BindingFlags.Public |
BindingFlags.DeclaredOnly);
foreach (var prop in props)
{
var value = prop.GetValue(fax);
if (value != null)
{
content.Add(new StringContent(value.ToString()), prop.Name);
}
}
return content;
}
public async Task<Fax> Send(Fax fax, StreamContent fileContent,string fileName)
{
var content = SerializeFaxToMultipart(fax);
string contentType;
mimeMapper.TryGetContentType(fileName, out contentType);
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType ?? "application/octet-stream");
content.Add(fileContent, "file", fileName);
var result = await http.Send<Fax>(uri, HttpMethod.Post, content, default);
return result;
}

public Fax List()
{
throw new NotImplementedException();
}
public Fax List(ListOptions listOptions)
{
throw new NotImplementedException();
}
public Fax Get(string faxId)
{
throw new NotImplementedException();
}
}
}
9 changes: 9 additions & 0 deletions src/Sinch/Faxes/ImageConversionMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Sinch.Faxes
{
public enum ImageConversionMethod
{
HALFTONE,
MONOCHROME
}

}
6 changes: 6 additions & 0 deletions src/Sinch/Faxes/ListOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sinch.Faxes
{
public class ListOptions
{
}
}
Loading

0 comments on commit c5bf9ee

Please sign in to comment.