From 8bd781b47181e8569707bdcb9326864c503dea5a Mon Sep 17 00:00:00 2001 From: Volodymyr Lisovskyi Date: Mon, 6 May 2024 11:27:24 +0200 Subject: [PATCH] feat: add OmniMessageOverride (#66) --- Sinch.sln.DotSettings | 6 +- .../Messages/Message/AppMessage.cs | 13 ++++ .../Messages/Message/CardMessage.cs | 2 +- .../Messages/Message/CarouselMessage.cs | 2 +- .../Messages/Message/ChoiceMessage.cs | 2 +- .../Messages/Message/ContactInfoMessage.cs | 2 +- .../Messages/Message/IOmniMessageOverride.cs | 65 +++++++++++++++++++ .../Messages/Message/ListMessage.cs | 2 +- .../Messages/Message/LocationMessage.cs | 2 +- .../Messages/Message/MediaMessage.cs | 6 +- .../Messages/Message/TemplateMessage.cs | 2 +- .../Messages/Message/TextMessage.cs | 2 +- .../Sinch.Tests/Conversation/MessagesTests.cs | 64 ++++++++++++++++++ .../Conversation/SendMessageTests.cs | 15 +++++ 14 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 src/Sinch/Conversation/Messages/Message/IOmniMessageOverride.cs diff --git a/Sinch.sln.DotSettings b/Sinch.sln.DotSettings index 59b766d4..f760c7ad 100644 --- a/Sinch.sln.DotSettings +++ b/Sinch.sln.DotSettings @@ -1,8 +1,6 @@ - + True + True True True True diff --git a/src/Sinch/Conversation/Messages/Message/AppMessage.cs b/src/Sinch/Conversation/Messages/Message/AppMessage.cs index 10a3c3a8..fdae5582 100644 --- a/src/Sinch/Conversation/Messages/Message/AppMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/AppMessage.cs @@ -118,6 +118,9 @@ public AppMessage(ContactInfoMessage contactInfoMessage) /// public Dictionary? ChannelSpecificMessage { get; set; } + + public Dictionary? ExplicitChannelOmniMessage { get; set; } + /// public Agent? Agent { get; set; } } @@ -135,6 +138,16 @@ public interface IChannelSpecificMessage public MessageType MessageType { get; } } + + + [JsonConverter(typeof(EnumRecordJsonConverter))] + public record ChannelSpecificTemplate(string Value) : EnumRecord(Value) + { + public static readonly ChannelSpecificTemplate WhatsApp = new ChannelSpecificTemplate("WHATSAPP"); + public static readonly ChannelSpecificTemplate KakaoTalk = new ChannelSpecificTemplate("KAKAOTALK"); + public static readonly ChannelSpecificTemplate WeChat = new ChannelSpecificTemplate("WECHAT"); + } + public class ChannelSpecificMessageJsonInterfaceConverter : JsonConverter { public override IChannelSpecificMessage Read(ref Utf8JsonReader reader, Type typeToConvert, diff --git a/src/Sinch/Conversation/Messages/Message/CardMessage.cs b/src/Sinch/Conversation/Messages/Message/CardMessage.cs index 7e05a4c7..847f70ec 100644 --- a/src/Sinch/Conversation/Messages/Message/CardMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/CardMessage.cs @@ -7,7 +7,7 @@ namespace Sinch.Conversation.Messages.Message /// /// Message containing text, media and choices. /// - public sealed class CardMessage + public sealed class CardMessage : IOmniMessageOverride { /// /// Gets or Sets Height diff --git a/src/Sinch/Conversation/Messages/Message/CarouselMessage.cs b/src/Sinch/Conversation/Messages/Message/CarouselMessage.cs index 541b5fa7..3f41a6cc 100644 --- a/src/Sinch/Conversation/Messages/Message/CarouselMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/CarouselMessage.cs @@ -3,7 +3,7 @@ namespace Sinch.Conversation.Messages.Message { - public class CarouselMessage + public sealed class CarouselMessage : IOmniMessageOverride { /// /// A list of up to 10 cards. diff --git a/src/Sinch/Conversation/Messages/Message/ChoiceMessage.cs b/src/Sinch/Conversation/Messages/Message/ChoiceMessage.cs index 333a852e..7a35fd9b 100644 --- a/src/Sinch/Conversation/Messages/Message/ChoiceMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/ChoiceMessage.cs @@ -4,7 +4,7 @@ namespace Sinch.Conversation.Messages.Message { - public class ChoiceMessage + public sealed class ChoiceMessage : IOmniMessageOverride { /// /// The number of choices is limited to 10. diff --git a/src/Sinch/Conversation/Messages/Message/ContactInfoMessage.cs b/src/Sinch/Conversation/Messages/Message/ContactInfoMessage.cs index 2f585020..4647e221 100644 --- a/src/Sinch/Conversation/Messages/Message/ContactInfoMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/ContactInfoMessage.cs @@ -8,7 +8,7 @@ namespace Sinch.Conversation.Messages.Message /// /// Message containing contact information. /// - public sealed class ContactInfoMessage + public sealed class ContactInfoMessage : IOmniMessageOverride { /// /// Gets or Sets Name diff --git a/src/Sinch/Conversation/Messages/Message/IOmniMessageOverride.cs b/src/Sinch/Conversation/Messages/Message/IOmniMessageOverride.cs new file mode 100644 index 00000000..598eccd8 --- /dev/null +++ b/src/Sinch/Conversation/Messages/Message/IOmniMessageOverride.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Sinch.Core; + +namespace Sinch.Conversation.Messages.Message +{ + [JsonInterfaceConverter(typeof(OmniMessageOverrideJsonConverter))] + public interface IOmniMessageOverride + { + } + + public class OmniMessageOverrideJsonConverter : JsonConverter + { + private readonly Dictionary _propNameToTypeMap = new() + { + { "text_message", typeof(TextMessage) }, + { "media_message", typeof(MediaMessage) }, + { "choice_message", typeof(ChoiceMessage) }, + { "card_message", typeof(CardMessage) }, + { "carousel_message", typeof(CarouselMessage) }, + { "location_message", typeof(LocationMessage) }, + { "contact_info_message", typeof(ContactInfoMessage) }, + { "list_message", typeof(ListMessage) }, + { "template_reference", typeof(TemplateReference) }, + }; + + public override IOmniMessageOverride? Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + var elem = JsonElement.ParseValue(ref reader); + + foreach (var entry in _propNameToTypeMap) + { + if (elem.TryGetProperty(entry.Key, out var value)) + { + return value.Deserialize(entry.Value, options) as IOmniMessageOverride; + } + } + + throw new JsonException( + $"Failed to match {nameof(IOmniMessageOverride)}, got json element: {elem.ToString()}"); + } + + public override void Write(Utf8JsonWriter writer, IOmniMessageOverride value, JsonSerializerOptions options) + { + var type = value.GetType(); + var matchingType = _propNameToTypeMap.FirstOrDefault(x => x.Value == type); + if (matchingType.Key is null) + { + throw new InvalidOperationException( + $"Value is not in range of expected types - actual type is {type.FullName}"); + } + + + JsonSerializer.Serialize(writer, new Dictionary + { + // dynamically cast IOmniMessageTemplate to specific type, e.g. TextMessage so avoid recursive infinite write + { matchingType.Key, Convert.ChangeType(value, type) } + }, options); + } + } +} diff --git a/src/Sinch/Conversation/Messages/Message/ListMessage.cs b/src/Sinch/Conversation/Messages/Message/ListMessage.cs index 25c37ad6..c0082124 100644 --- a/src/Sinch/Conversation/Messages/Message/ListMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/ListMessage.cs @@ -10,7 +10,7 @@ namespace Sinch.Conversation.Messages.Message /// /// A message containing a list of options to choose from /// - public sealed class ListMessage + public sealed class ListMessage : IOmniMessageOverride { /// /// A title for the message that is displayed near the products or choices. diff --git a/src/Sinch/Conversation/Messages/Message/LocationMessage.cs b/src/Sinch/Conversation/Messages/Message/LocationMessage.cs index b0d6c0cb..f0c0ab21 100644 --- a/src/Sinch/Conversation/Messages/Message/LocationMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/LocationMessage.cs @@ -5,7 +5,7 @@ namespace Sinch.Conversation.Messages.Message /// /// Message containing geographic location. /// - public sealed class LocationMessage + public sealed class LocationMessage : IOmniMessageOverride { /// /// Gets or Sets Coordinates diff --git a/src/Sinch/Conversation/Messages/Message/MediaMessage.cs b/src/Sinch/Conversation/Messages/Message/MediaMessage.cs index 355b9938..3846215b 100644 --- a/src/Sinch/Conversation/Messages/Message/MediaMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/MediaMessage.cs @@ -6,7 +6,7 @@ namespace Sinch.Conversation.Messages.Message /// /// A message containing a media component, such as an image, document, or video. /// - public sealed class MediaMessage + public sealed class MediaMessage : IOmniMessageOverride { /// /// An optional parameter. Will be used where it is natively supported. @@ -23,6 +23,10 @@ public sealed class MediaMessage public Uri Url { get; set; } = null!; #endif + /// + /// Overrides the media file name. + /// + public string? FilenameOverride { get; set; } /// /// Returns the string presentation of the object diff --git a/src/Sinch/Conversation/Messages/Message/TemplateMessage.cs b/src/Sinch/Conversation/Messages/Message/TemplateMessage.cs index a6639a7c..629a9d24 100644 --- a/src/Sinch/Conversation/Messages/Message/TemplateMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/TemplateMessage.cs @@ -45,7 +45,7 @@ public override string ToString() /// The referenced template can be an omnichannel template stored in Conversation API Template Store /// as AppMessage or it can reference external channel-specific template such as WhatsApp Business Template. /// - public sealed class TemplateReference + public sealed class TemplateReference : IOmniMessageOverride { /// /// The ID of the template. diff --git a/src/Sinch/Conversation/Messages/Message/TextMessage.cs b/src/Sinch/Conversation/Messages/Message/TextMessage.cs index 6d5f9056..f54ec0e9 100644 --- a/src/Sinch/Conversation/Messages/Message/TextMessage.cs +++ b/src/Sinch/Conversation/Messages/Message/TextMessage.cs @@ -5,7 +5,7 @@ namespace Sinch.Conversation.Messages.Message /// /// A message containing only text. /// - public sealed class TextMessage + public sealed class TextMessage : IOmniMessageOverride { /// /// A message containing only text. diff --git a/tests/Sinch.Tests/Conversation/MessagesTests.cs b/tests/Sinch.Tests/Conversation/MessagesTests.cs index 4b4a9bb2..dd5e33ef 100644 --- a/tests/Sinch.Tests/Conversation/MessagesTests.cs +++ b/tests/Sinch.Tests/Conversation/MessagesTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -13,6 +14,7 @@ using Sinch.Conversation.Messages.List; using Sinch.Conversation.Messages.Message; using Sinch.Conversation.Messages.Message.ChannelSpecificMessages.WhatsApp; +using Sinch.Core; using Xunit; namespace Sinch.Tests.Conversation @@ -356,5 +358,67 @@ public void DeserializeFlowMessage() var dict = JsonSerializer.Deserialize>(json); dict[ConversationChannel.WhatsApp].Should().BeEquivalentTo(_flowMessage); } + + + [Theory] + [ClassData(typeof(OmniMessageTestData))] + public void DeserializeOmniMessageOverride(string json, object dataToCheck) + { + var dict = JsonSerializer + .Deserialize>(json, + options: new JsonSerializerOptions() + { + PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance + }); + dict.Should().ContainKey(ChannelSpecificTemplate.WhatsApp).WhoseValue.Should() + .BeEquivalentTo(dataToCheck); + } + } + + public class OmniMessageTestData : IEnumerable + { + private static readonly string Text = @" + { + ""WHATSAPP"": { + ""text_message"": { + ""text"": ""hello"" + } + } + }"; + private static readonly string Media = @" + { + ""WHATSAPP"": { + ""media_message"": { + ""url"": ""https://hello.net"" + } + } + }"; + private static readonly string Template = @" + { + ""WHATSAPP"": { + ""template_reference"": { + ""template_id"": ""id"", + ""version"": ""3"" + } + } + }"; + + private readonly List _data = new() + { + new object[] { Text, new TextMessage("hello") }, + new object[] { Media, new MediaMessage() + { + Url = new Uri("https://hello.net") + }}, + new object[] { Template, new TemplateReference() + { + TemplateId = "id", + Version = "3" + }}, + }; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/tests/Sinch.Tests/Conversation/SendMessageTests.cs b/tests/Sinch.Tests/Conversation/SendMessageTests.cs index 08cbf812..92e5db52 100644 --- a/tests/Sinch.Tests/Conversation/SendMessageTests.cs +++ b/tests/Sinch.Tests/Conversation/SendMessageTests.cs @@ -449,6 +449,16 @@ public async Task SendAllParams() _baseMessageExpected.ttl = "1800s"; _baseMessageExpected.queue = "HIGH_PRIORITY"; _baseMessageExpected.message_metadata = "meta"; + _baseMessageExpected.message.explicit_channel_omni_message = new + { + WECHAT = new + { + text_message = new + { + text = "hello" + } + } + }; _baseRequest.Recipient = new Identified { @@ -474,6 +484,11 @@ public async Task SendAllParams() _baseRequest.Ttl = "1800s"; _baseRequest.Queue = MessageQueue.HighPriority; _baseRequest.MessageMetadata = "meta"; + _baseRequest.Message.ExplicitChannelOmniMessage = + new Dictionary() + { + { ChannelSpecificTemplate.WeChat, new TextMessage("hello") } + }; HttpMessageHandlerMock .When(HttpMethod.Post,