Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fax hooks #75

Merged
merged 3 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions examples/WebApi/Controllers/HandleFaxEventController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Sinch.Fax.Faxes;
using Sinch.Fax.Hooks;

namespace WebApiExamples.Controllers
{
[ApiController]
[Route("fax")]
public class HandleFaxEventController : ControllerBase
{
private List<string> _incomingFaxIds = new(); // track fax ids which was sent

[HttpPost]
[Route("event-json")]
public async Task<IActionResult> HandleJsonEvent([FromBody] IFaxEvent faxEvent)
{
switch (faxEvent)
{
case IncomingFaxEvent incomingFaxEvent:
asein-sinch marked this conversation as resolved.
Show resolved Hide resolved
// just track fax ids for future
_incomingFaxIds.Add(incomingFaxEvent.Fax.Id);

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 'item' in 'void List<string>.Add(string item)'.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 'item' in 'void List<string>.Add(string item)'.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 'item' in 'void List<string>.Add(string item)'.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 'item' in 'void List<string>.Add(string item)'.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 'item' in 'void List<string>.Add(string item)'.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 22 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 'item' in 'void List<string>.Add(string item)'.
break;
case CompletedFaxEvent completedFaxEvent:
// download if fax completed
foreach (var file in completedFaxEvent.Files)
{
var bytes = Convert.FromBase64String(file.File);

Check warning on line 28 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'.

Check warning on line 28 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'.

Check warning on line 28 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'.

Check warning on line 28 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'.

Check warning on line 28 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'.

Check warning on line 28 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Possible null reference argument for parameter 's' in 'byte[] Convert.FromBase64String(string s)'.
var contents = new MemoryStream(bytes);
var fileName = completedFaxEvent.Fax.Id + "." + file.FileType.Value.ToLower();

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.

Check warning on line 30 in examples/WebApi/Controllers/HandleFaxEventController.cs

View workflow job for this annotation

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

Dereference of a possibly null reference.
await SaveFile(fileName, contents);
}
break;
default:
return BadRequest();
}

return Ok();
}

// To handle multi part form data event, utilize provided class below
public class FormDataFaxEvent
{
[BindProperty(Name = "event")]
public string? Event { get; set; }

public IFormFile? File { get; set; }

[BindProperty(Name = "eventTime")]
public DateTime? EventTime { get; set; }

[BindProperty(Name = "fax")]
public string? Fax { get; set; }
}

[HttpPost]
[Route("event-form-data")]
public async Task<IActionResult> HandleJsonEvent([FromForm] FormDataFaxEvent faxEvent)
{
if (string.IsNullOrEmpty(faxEvent.Event) || string.IsNullOrEmpty(faxEvent.Fax))
{
return BadRequest();
}

var @event = new FaxEventType(faxEvent.Event);
var fax = JsonSerializer.Deserialize<Fax>(faxEvent.Fax)!;

if (faxEvent.File != null)
{
await using var stream = faxEvent.File.OpenReadStream();
await SaveFile(faxEvent.File.FileName, stream);

return Ok($"I caught event - {@event}; of fax id - {fax.Id}; with filename: {faxEvent.File.FileName}!");
}

return BadRequest();
}

private static async Task SaveFile(string fileName, Stream stream)
{
const string directory = @"C:\Downloads\";
if (!Path.Exists(directory))
{
Directory.CreateDirectory(directory);
}

await using var fileStream =
new FileStream(Path.Combine(directory, fileName), FileMode.Create,
FileAccess.Write);
await stream.CopyToAsync(fileStream);
}
}
}
6 changes: 3 additions & 3 deletions src/Sinch/Fax/Faxes/SendFaxRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ public ValueTask DisposeAsync()
}
}

/// TODO: think about if it's worth implementing base64 send
internal sealed class Base64File
/// TODO: implement sending base 64 files.
public sealed class Base64File
{
/// <summary>
/// Base64 encoded file content.
Expand All @@ -165,7 +165,7 @@ internal sealed class Base64File
}

[JsonConverter(typeof(EnumRecordJsonConverter<FileType>))]
internal record FileType(string Value) : EnumRecord(Value)
public record FileType(string Value) : EnumRecord(Value)
{
// ReSharper disable InconsistentNaming

Expand Down
18 changes: 18 additions & 0 deletions src/Sinch/Fax/Hooks/CompletedFaxEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Sinch.Fax.Faxes;

namespace Sinch.Fax.Hooks
{
public class CompletedFaxEvent : GenericFaxEvent, IFaxEvent
{
public override FaxEventType Event { get; } = FaxEventType.CompletedFax;

/// <summary>
/// Base64 list of files
/// </summary>
[JsonPropertyName("files")]
public List<Base64File> Files { get; set; } = new();
}
}
105 changes: 105 additions & 0 deletions src/Sinch/Fax/Hooks/IFaxEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sinch.Core;
using Sinch.Fax.Faxes;

namespace Sinch.Fax.Hooks
{
[JsonInterfaceConverter(typeof(FaxEventConverter))]
public interface IFaxEvent
{
/// <summary>
/// The different events that can trigger a webhook
/// </summary>
[JsonPropertyName("event")]
public FaxEventType Event { get; }
}

public abstract class GenericFaxEvent

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm mistaken, it seems that some properties are not declared to support an event with application/json content. I would have expected to see file and fileType

{
/// <summary>
/// The different events that can trigger a webhook
/// </summary>
[JsonPropertyName("event")]
public abstract FaxEventType Event { get; }

/// <summary>
/// Time of the event.
/// </summary>
[JsonPropertyName("eventTime")]
public DateTime? EventTime { get; set; }


/// <summary>
/// Gets or Sets Fax
/// </summary>
[JsonPropertyName("fax")]
public Faxes.Fax? Fax { get; set; }



/// <summary>
/// Returns the string presentation of the object
/// </summary>
/// <returns>String presentation of the object</returns>
public override string ToString()
{
var sb = new StringBuilder();
sb.Append($"class {nameof(IFaxEvent)} {{\n");
sb.Append($" {nameof(Event)}: ").Append(Event).Append('\n');
sb.Append($" {nameof(EventTime)}: ").Append(EventTime).Append('\n');
sb.Append($" {nameof(Fax)}: ").Append(Fax).Append('\n');
sb.Append("}\n");
return sb.ToString();
}
}

[JsonConverter(typeof(EnumRecordJsonConverter<FaxEventType>))]
public record FaxEventType(string Value) : EnumRecord(Value)
{
public static readonly FaxEventType IncomingFax = new("INCOMING_FAX");
public static readonly FaxEventType CompletedFax = new("FAX_COMPLETED");
}

public class FaxEventConverter : JsonConverter<IFaxEvent>
{
public override IFaxEvent? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var elem = JsonElement.ParseValue(ref reader);
var descriptor = elem.EnumerateObject().FirstOrDefault(x => x.Name == "event");
var type = descriptor.Value.GetString();

if (type == FaxEventType.IncomingFax.Value)
{
return elem.Deserialize<IncomingFaxEvent>(options);
}

if (type == FaxEventType.CompletedFax.Value)
{
return elem.Deserialize<CompletedFaxEvent>(options);
}

throw new JsonException($"Failed to match verification method object, got {descriptor.Name}");
}

public override void Write(Utf8JsonWriter writer, IFaxEvent value, JsonSerializerOptions options)
asein-sinch marked this conversation as resolved.
Show resolved Hide resolved
{
switch (value)
{
case IncomingFaxEvent incomingFaxEvent:
JsonSerializer.Serialize(writer, incomingFaxEvent, options);
break;
case CompletedFaxEvent completedFaxEvent:
JsonSerializer.Serialize(writer, completedFaxEvent, options);
break;
default:
throw new ArgumentOutOfRangeException(nameof(value),
$"Cannot find a matching class for the interface {nameof(IFaxEvent)}");
}
}
}
}
19 changes: 19 additions & 0 deletions src/Sinch/Fax/Hooks/IncomingFaxEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
using Sinch.Fax.Faxes;

namespace Sinch.Fax.Hooks
{
public class IncomingFaxEvent : GenericFaxEvent, IFaxEvent
{
public override FaxEventType Event { get; } = FaxEventType.IncomingFax;

/// <summary>
/// Base64 encoded file content.
/// </summary>
[JsonPropertyName("file")]
public string? File { get; set; }

[JsonPropertyName("fileType")]
public FileType? FileType { get; set; }
}
}
38 changes: 38 additions & 0 deletions tests/Sinch.Tests/Fax/HooksTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Text.Json;
using FluentAssertions;
using Sinch.Fax.Faxes;
using Sinch.Fax.Hooks;
using Xunit;

namespace Sinch.Tests.Fax
{
public class HooksTests
{
[Fact]
public void DeserializeIncomingFax()
{
string json =
"{ \"event\": \"INCOMING_FAX\", \"eventTime\": \"2021-11-01T23:25:39Z\", \"fax\": { \"id\": \"01HDFHACK1YN7CCDYRA6ZRMA8Z\", \"direction\": \"INBOUND\", \"from\": \"+14155552222\", \"to\": \"+14155553333\", \"numberOfPages\": 1, \"status\": \"COMPLETED\", \"price\": { \"amount\": 0.07, \"currencyCode\": \"USD\" }, \"createTime\": \"2021-11-01T23:25:39Z\", \"completedTime\": \"2021-11-01T23:25:39Z\", \"callbackUrl\": \"https://www.my-website.com/callback\", \"callbackUrlContentType\": \"multipart/form-data\", \"imageConversionMethod\": \"HALFTONE\", \"projectId\": \"YOUR_PROJECT_ID\", \"serviceId\": \"YOUR_SERVICE_ID\" }, \"file\": \"ZmFzZGZkYXNkYWY=\", \"fileType\":\"PDF\" }";

var @event = JsonSerializer.Deserialize<IFaxEvent>(json);
var fax = @event.Should().BeOfType<IncomingFaxEvent>().Which;
fax.EventTime.Should().Be(new DateTime(2021, 11, 01, 23, 25, 39));
fax.Fax!.Price!.Amount.Should().Be(0.07f);
fax.File.Should().BeEquivalentTo("ZmFzZGZkYXNkYWY=");
fax.FileType.Should().BeEquivalentTo(FileType.PDF);
}

[Fact]
public void DeserializeCompletedFax()
{
string json =
"{ \"event\": \"FAX_COMPLETED\", \"eventTime\": \"2021-11-01T23:25:39Z\", \"fax\": { \"id\": \"01HDFHACK1YN7CCDYRA6ZRMA8Z\", \"direction\": \"INBOUND\", \"from\": \"+14155552222\", \"to\": \"+14155553333\", \"numberOfPages\": 1, \"status\": \"COMPLETED\", \"price\": { \"amount\": 0.07, \"currencyCode\": \"USD\" }, \"createTime\": \"2021-11-01T23:25:39Z\", \"completedTime\": \"2021-11-01T23:25:39Z\", \"callbackUrl\": \"https://www.my-website.com/callback\", \"callbackUrlContentType\": \"multipart/form-data\", \"imageConversionMethod\": \"HALFTONE\", \"projectId\": \"YOUR_PROJECT_ID\", \"serviceId\": \"YOUR_SERVICE_ID\" }, \"files\": [ {\"file\":\"ZmFzZGZkYXNkYWY=\", \"fileType\":\"PDF\" } ]}";

var @event = JsonSerializer.Deserialize<IFaxEvent>(json);
var fax = @event.Should().BeOfType<CompletedFaxEvent>().Which;
fax.EventTime.Should().Be(new DateTime(2021, 11, 01, 23, 25, 39));
fax.Fax!.Price!.Amount.Should().Be(0.07f);
}
}
}
Loading