├── icon.png ├── CM.Text.Tests ├── Usings.cs ├── SerializationTests.cs ├── DeserializationTests.cs ├── CM.Text.Tests.csproj ├── BuilderTests.cs ├── BusinessMessagingApiTests.cs └── SuggestionsTests.cs ├── CM.Text ├── BusinessMessaging │ ├── Model │ │ ├── MultiChannel │ │ │ ├── ApplePayRequest.cs │ │ │ ├── Dial.cs │ │ │ ├── CarouselMessage.cs │ │ │ ├── CarouselCardWidth.cs │ │ │ ├── ViewLocationSuggestion.cs │ │ │ ├── MessageContext.cs │ │ │ ├── CalendarSuggestion.cs │ │ │ ├── DialSuggestion.cs │ │ │ ├── Carousel.cs │ │ │ ├── RichCard.cs │ │ │ ├── TemplateMessageContent.cs │ │ │ ├── LocationPushMessage.cs │ │ │ ├── OpenUrlSuggestion.cs │ │ │ ├── LineItem.cs │ │ │ ├── IRichMessage.cs │ │ │ ├── TemplateMessage.cs │ │ │ ├── ReplySuggestion.cs │ │ │ ├── CalendarOptions.cs │ │ │ ├── SuggestionBase.cs │ │ │ ├── MediaMessage.cs │ │ │ ├── MediaContent.cs │ │ │ ├── ViewLocationOptions.cs │ │ │ ├── TextMessage.cs │ │ │ ├── RichContent.cs │ │ │ ├── ApplePayConfiguration.cs │ │ │ ├── ContactMessage.cs │ │ │ ├── WhatsAppTemplate.cs │ │ │ └── WhatsAppInteractiveMessage.cs │ │ ├── Recipient.cs │ │ ├── Request.cs │ │ ├── HttpResponseBody.cs │ │ ├── ResponseMessageDetail.cs │ │ ├── Body.cs │ │ ├── Channel.cs │ │ └── Message.cs │ ├── CarouselBuilder.cs │ ├── BusinessMessagingApi.cs │ └── MessageBuilder.cs ├── Common │ └── Constant.cs ├── Identity │ ├── OtpResult.cs │ ├── OtpRequest.cs │ └── OtpRequestBuilder.cs ├── TextClientResult.cs ├── TextClientMessageDetail.cs ├── TextClientFactory.cs ├── TextClientStatusCode.cs ├── Interfaces │ └── ITextClient.cs ├── CM.Text.csproj └── TextClient.cs ├── .github └── workflows │ ├── main.yml │ └── nuget.yml ├── CM.Text.sln.DotSettings ├── LICENSE ├── CM.Text.sln ├── CHANGELOG.md ├── .editorconfig ├── .gitignore └── README.md /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmdotcom/text-sdk-dotnet/HEAD/icon.png -------------------------------------------------------------------------------- /CM.Text.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/ApplePayRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 4 | { 5 | /// 6 | /// ApplePay request rich message 7 | /// 8 | public class ApplePayRequest : IRichMessage 9 | { 10 | /// 11 | /// Gets or sets the apple pay configuration. 12 | /// 13 | [JsonPropertyName("payment")] 14 | public ApplePayConfiguration ApplePayConfiguration { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/Dial.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Contains information for a 8 | /// 9 | [PublicAPI] 10 | public class Dial 11 | { 12 | /// 13 | /// The number to call (in international format) 14 | /// 15 | [JsonPropertyName("phoneNumber")] 16 | public string PhoneNumber { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/Recipient.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model 5 | { 6 | /// 7 | /// A destination mobile number. 8 | /// 9 | [PublicAPI] 10 | public class Recipient 11 | { 12 | /// 13 | /// This value should be in international format. 14 | /// A single mobile number per request. Example: '00447911123456' 15 | /// 16 | [JsonPropertyName("number")] 17 | public string Number { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: .NET build & tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: | 19 | 8.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/CarouselMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A message containing multiple carousels. 8 | /// Currently only supported by / 9 | /// 10 | [PublicAPI] 11 | public class CarouselMessage : IRichMessage 12 | { 13 | /// 14 | /// Contains the rich cards 15 | /// 16 | [JsonPropertyName("carousel")] 17 | public Carousel Carousel { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CM.Text.Tests/SerializationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using CM.Text.BusinessMessaging.Model.MultiChannel; 3 | using FluentAssertions; 4 | 5 | namespace CM.Text.Tests 6 | { 7 | [TestClass] 8 | public class SerializationTests 9 | { 10 | [TestMethod] 11 | public void SerializeTextTest() 12 | { 13 | var textMessage = new TextMessage("testing correct serialization"); 14 | var serialized= JsonSerializer.Serialize(textMessage); 15 | 16 | serialized.Should().NotBeNullOrWhiteSpace(); 17 | serialized.Should().Contain("\"$type\":\"TextMessage\""); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/CarouselCardWidth.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Used by a to set the width 8 | /// 9 | [PublicAPI] 10 | [JsonConverter(typeof(JsonStringEnumConverter))] 11 | public enum CarouselCardWidth 12 | { 13 | /// 14 | /// Small cards 15 | /// 16 | Small, 17 | 18 | /// 19 | /// Medium sized cards 20 | /// 21 | Medium 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/ViewLocationSuggestion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Opens the navigation app of the user with a pin at the specified location in . 8 | /// 9 | [PublicAPI] 10 | public class ViewLocationSuggestion : SuggestionBase 11 | { 12 | /// 13 | /// The location options 14 | /// 15 | [JsonPropertyName("viewLocation")] 16 | public ViewLocationOptions Location { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/MessageContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Contextual properties of the message. Currently only applicable to 8 | /// Docs: https://developers.cm.com/messaging/docs/whatsapp-inbound#mt-replies-mo 9 | /// 10 | public class MessageContext 11 | { 12 | /// 13 | /// Message ID to which the current message is a reply 14 | /// 15 | [JsonPropertyName("message_id")] 16 | [CanBeNull] 17 | public string MessageId { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/CalendarSuggestion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A suggestion, used in . 8 | /// When the user clicks on the icon, it opens the calendar app of the user to add 9 | /// the new appointment. 10 | /// 11 | [PublicAPI] 12 | public class CalendarSuggestion : SuggestionBase 13 | { 14 | /// 15 | /// The options of the agenda item 16 | /// 17 | [JsonPropertyName("calendar")] 18 | public CalendarOptions Calendar { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/DialSuggestion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A suggestion, used in 8 | /// When you want to enable the user can call you, or listen to a recorded spoken message, 9 | /// this suggestion can be applied. When clicked starts a new call. 10 | /// 11 | [PublicAPI] 12 | public class DialSuggestion : SuggestionBase 13 | { 14 | /// 15 | /// The dial options 16 | /// 17 | [JsonPropertyName("dial")] 18 | public Dial Dial { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/Carousel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A carousel contains two or more s 8 | /// 9 | [PublicAPI] 10 | public class Carousel 11 | { 12 | /// 13 | /// The cards of the carousel 14 | /// 15 | [JsonPropertyName("cards")] 16 | public RichCard[] Cards { get; set; } 17 | 18 | /// 19 | /// The width for the items of the carousel 20 | /// 21 | [JsonPropertyName("cardWidth")] 22 | public CarouselCardWidth CarouselCardWidth { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/RichCard.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A rich card, which can be used for . 8 | /// 9 | [PublicAPI] 10 | public class RichCard : TextMessage 11 | { 12 | /// 13 | /// Optional: the header for a rich card 14 | /// 15 | [JsonPropertyName("header")] 16 | public string Header { get; set; } 17 | 18 | /// 19 | /// The image or video of the card. 20 | /// 21 | [JsonPropertyName("media")] 22 | public MediaContent Media { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/TemplateMessageContent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Used to send a template message, at this moment only supported in . 8 | /// 9 | [PublicAPI] 10 | public class TemplateMessageContent 11 | { 12 | /// 13 | /// The WhatsApp template message 14 | /// 15 | /// Templates need to be configured by CM and approved by WhatsApp before it is possible 16 | /// to send these messages. 17 | /// 18 | [JsonPropertyName("whatsapp")] 19 | public WhatsappTemplate Whatsapp { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CM.Text.Tests/DeserializationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using CM.Text.BusinessMessaging.Model.MultiChannel; 3 | using FluentAssertions; 4 | 5 | namespace CM.Text.Tests 6 | { 7 | [TestClass] 8 | public class DeserializationTests 9 | { 10 | [TestMethod] 11 | public void DeserializeTextTest() 12 | { 13 | var textMessage = "{\"$type\":\"TextMessage\",\"text\":\"testing correct deserialization\",\"tag\":null,\"suggestions\":null}"; 14 | var deserialized = JsonSerializer.Deserialize(textMessage); 15 | 16 | deserialized.Should().BeOfType(); 17 | var deserializedTextMessage = deserialized as TextMessage; 18 | deserializedTextMessage!.Text.Should().Be("testing correct deserialization"); 19 | } 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/Request.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace CM.Text.BusinessMessaging.Model 4 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 5 | { 6 | public class Request 7 | { 8 | public class MessagesEnvelope 9 | { 10 | [JsonPropertyName("authentication")] 11 | [JsonInclude] 12 | public Authentication Authentication { get; set; } 13 | 14 | [JsonPropertyName("msg")] 15 | public Message[] Messages { get; set; } 16 | } 17 | 18 | public class Authentication 19 | { 20 | [JsonPropertyName("producttoken")] 21 | public string ProductToken { get; set; } 22 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CM.Text/Common/Constant.cs: -------------------------------------------------------------------------------- 1 | namespace CM.Text.Common 2 | { 3 | internal static class Constant 4 | { 5 | internal static readonly string TextSdkReference = $"text-sdk-dotnet-{typeof(TextClient).Assembly.GetName().Version}"; 6 | 7 | internal const string BusinessMessagingGatewayJsonEndpoint = "https://gw.messaging.cm.com/v1.0/message"; 8 | 9 | internal const string OtpRequestEndpoint = "https://api.cm.com/otp/v2/otp"; 10 | internal const string OtpVerifyEndpointFormatter = "https://api.cm.com/otp/v2/otp/{0}/verify"; 11 | 12 | internal static readonly string BusinessMessagingGatewayMediaTypeJson = "application/json"; 13 | internal static readonly string BusinessMessagingBodyTypeAuto = "AUTO"; 14 | internal static readonly int BusinessMessagingMessagePartsMinDefault = 1; 15 | internal static readonly int BusinessMessagingMessagePartsMaxDefault = 8; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/HttpResponseBody.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace CM.Text.BusinessMessaging.Model 4 | { 5 | 6 | /// 7 | /// CM Messaging API response body containing API related info 8 | /// 9 | public class HttpResponseBody 10 | { 11 | /// 12 | /// API Request details 13 | /// 14 | [JsonInclude] 15 | public string details { get; private set; } 16 | 17 | /// 18 | /// JSON POST Error codes. Full description of each code available in the development documentation 19 | /// 20 | [JsonInclude] 21 | public int errorCode { get; private set; } 22 | 23 | /// 24 | /// Each message that was sent in the original request 25 | /// 26 | [JsonInclude] 27 | public ResponseMessageDetail[] messages { get; private set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/LocationPushMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Used to send a location, supported by (most) connections 8 | /// and . 9 | /// 10 | [PublicAPI] 11 | public class LocationPushMessage : IRichMessage 12 | { 13 | /// 14 | /// The location options to send. 15 | /// 16 | [JsonPropertyName("location")] 17 | public ViewLocationOptions Location { get; set; } 18 | 19 | /// 20 | /// Contextual properties of the message. Only applicable to 21 | /// 22 | [JsonPropertyName("context")] 23 | [CanBeNull] 24 | public MessageContext MessageContext { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/OpenUrlSuggestion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// This is used to give the user an option to open a link. 8 | /// 9 | /// 10 | /// For this can be an in-app link, 11 | /// which will only be shown when the app is installed. 12 | /// 13 | [PublicAPI] 14 | public class OpenUrlSuggestion : SuggestionBase 15 | { 16 | /// 17 | /// The url the end user can open 18 | /// 19 | /// 20 | /// For this can be an in-app link, 21 | /// which will only be shown when the app is installed. 22 | /// 23 | [JsonPropertyName("url")] 24 | public string Url { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CM.Text.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | RCS 3 | SMS 4 | True 5 | True 6 | True 7 | True 8 | True -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/LineItem.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text.Json.Serialization; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A set of line items that explain recurring payments and additional charges and discounts. 8 | /// 9 | public class LineItem 10 | { 11 | /// 12 | /// (Required) A short, localized description of the line item. 13 | /// 14 | [JsonPropertyName("label")] 15 | public string Label { get; set; } 16 | /// 17 | /// A value that indicates whether the line item is final or pending. 18 | /// 19 | [JsonPropertyName("type")] 20 | public string Type { get; set; } 21 | /// 22 | /// (Required) The monetary amount of the line item. 23 | /// 24 | [JsonPropertyName("amount")] 25 | public decimal Amount { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CM.Text.Tests/CM.Text.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/IRichMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// One element in a 8 | /// Requires a json derived type for serialization to work 9 | /// 10 | [PublicAPI] 11 | [JsonDerivedType(typeof(MediaMessage), nameof(MediaMessage))] 12 | [JsonDerivedType(typeof(ApplePayRequest), nameof(ApplePayRequest))] 13 | [JsonDerivedType(typeof(CarouselMessage), nameof(CarouselMessage))] 14 | [JsonDerivedType(typeof(ContactMessage), nameof(ContactMessage))] 15 | [JsonDerivedType(typeof(LocationPushMessage), nameof(LocationPushMessage))] 16 | [JsonDerivedType(typeof(TemplateMessage), nameof(TemplateMessage))] 17 | [JsonDerivedType(typeof(TextMessage), nameof(TextMessage))] 18 | [JsonDerivedType(typeof(WhatsAppInteractiveMessage), nameof(WhatsAppInteractiveMessage))] 19 | public interface IRichMessage 20 | { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/TemplateMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Used to send a template message, at this moment only supported in . 8 | /// 9 | [PublicAPI] 10 | public class TemplateMessage : IRichMessage 11 | { 12 | /// 13 | /// The Content of the WhatsApp template message 14 | /// 15 | /// Templates need to be configured by CM and approved by WhatsApp before it is possible 16 | /// to send these messages. 17 | /// 18 | [JsonPropertyName("template")] 19 | public TemplateMessageContent Content { get; set; } 20 | 21 | /// 22 | /// Contextual properties of the message 23 | /// 24 | [JsonPropertyName("context")] 25 | [CanBeNull] 26 | public MessageContext MessageContext { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/nuget.yml: -------------------------------------------------------------------------------- 1 | name: .NET nuget publish 2 | 3 | on: 4 | release: 5 | types: released 6 | 7 | jobs: 8 | nugetpush: 9 | name: Build, test and push to nuget 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: | 18 | 8.0.x 19 | - name: Restore dependencies 20 | run: dotnet restore 21 | - name: Build 22 | run: dotnet build --no-restore 23 | - name: Test 24 | run: dotnet test --no-build --verbosity normal 25 | - name: Publish to Nuget.org 26 | run: dotnet nuget push **/*.nupkg --api-key ${{ secrets.nuget_apikey }} -s https://api.nuget.org/v3/index.json 27 | - name: Add Github Packages source 28 | run: dotnet nuget add source --username cmdotcom --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/cmdotcom/index.json" 29 | - name: Publish to Github Packages 30 | run: dotnet nuget push **/*.nupkg -s github 31 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/ReplySuggestion.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A suggestion to give the user the option to give a quick reply in 8 | /// 9 | [PublicAPI] 10 | public class ReplySuggestion : SuggestionBase 11 | { 12 | /// 13 | /// Description of the Reply suggestion 14 | /// 15 | /// 16 | /// For 17 | /// 18 | [JsonPropertyName("description")] 19 | public string Description { get; set; } 20 | 21 | /// 22 | /// The thumbnail image of the Reply suggestion 23 | /// 24 | /// 25 | /// For 26 | /// 27 | [JsonPropertyName("media")] 28 | public MediaContent Media { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CM.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CM.Text/Identity/OtpResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace CM.Text.Identity 5 | { 6 | /// 7 | /// The result of an OTP request. 8 | /// 9 | public class OtpResult 10 | { 11 | /// 12 | /// The identifier of the OTP. 13 | /// 14 | [JsonPropertyName("id")] 15 | public string Id { get; set; } 16 | /// 17 | /// The channel used to send the code. 18 | /// 19 | [JsonPropertyName("channel")] 20 | public string Channel { get; set; } 21 | /// 22 | /// Indicates if the code was valid. 23 | /// 24 | [JsonPropertyName("verified")] 25 | public bool Verified { get; set; } 26 | /// 27 | /// The date the OTP was created. 28 | /// 29 | [JsonPropertyName("createdAt")] 30 | public DateTime CreatedAt { get; set; } 31 | /// 32 | /// The date the OTP will expire. 33 | /// 34 | [JsonPropertyName("expiresAt")] 35 | public DateTime ExpiresAt { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/CalendarOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using JetBrains.Annotations; 4 | 5 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 6 | { 7 | /// 8 | /// Contains information for a . 9 | /// 10 | [PublicAPI] 11 | public class CalendarOptions 12 | { 13 | /// 14 | /// The end of the appointment. 15 | /// 16 | [JsonPropertyName("endTime")] 17 | public DateTime EndTime { get; set; } 18 | 19 | /// 20 | /// The start of the appointment. 21 | /// 22 | [JsonPropertyName("startTime")] 23 | public DateTime StartTime { get; set; } 24 | 25 | /// 26 | /// The description which will appear in the calendar app 27 | /// 28 | [JsonPropertyName("description")] 29 | public string Description { get; set; } 30 | 31 | /// 32 | /// The title of the appointment which will appear in the calendar app 33 | /// 34 | [JsonPropertyName("title")] 35 | public string Title { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CM.Text/TextClientResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Text.Json.Serialization; 4 | using JetBrains.Annotations; 5 | 6 | namespace CM.Text 7 | { 8 | /// 9 | /// Data model that's returned after interaction with CM.com's Text interface. 10 | /// 11 | [PublicAPI] 12 | [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Json interface")] 13 | public class TextClientResult 14 | { 15 | /// 16 | /// Gets or sets the details for each message. 17 | /// 18 | /// 19 | /// The details. 20 | /// 21 | [JsonPropertyOrder(2)] 22 | public IEnumerable details { get; set; } 23 | 24 | /// 25 | /// Gets or sets the status code. 26 | /// 27 | /// 28 | /// The status code. 29 | /// 30 | [JsonPropertyOrder(1)] 31 | public TextClientStatusCode statusCode { get; set; } 32 | 33 | /// 34 | /// A message that describes the result. 35 | /// 36 | /// 37 | /// The status message. 38 | /// 39 | [JsonPropertyOrder(0)] 40 | public string statusMessage { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/SuggestionBase.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Suggestions can be used in several channels, not all channels 8 | /// support all suggestions. 9 | /// 10 | /// Requires a json derived type for serialization to work 11 | /// 12 | [PublicAPI] 13 | [JsonPolymorphic(TypeDiscriminatorPropertyName = "action")] 14 | [JsonDerivedType(typeof(CalendarSuggestion), "CreateCalendarEvent")] 15 | [JsonDerivedType(typeof(DialSuggestion), "Dial")] 16 | [JsonDerivedType(typeof(OpenUrlSuggestion), "openUrl")] 17 | [JsonDerivedType(typeof(ReplySuggestion), "reply")] 18 | [JsonDerivedType(typeof(ViewLocationSuggestion), "viewLocation")] 19 | public abstract class SuggestionBase 20 | { 21 | /// 22 | /// The text the end user will see 23 | /// 24 | [JsonPropertyName("label")] 25 | public string Label { get; set; } 26 | 27 | /// 28 | /// When the item is selected and the postback data is set, then the Postback data will be 29 | /// sent in a MO instead of the . 30 | /// 31 | [JsonPropertyName("postbackdata")] 32 | public string PostbackData { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/ResponseMessageDetail.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace CM.Text.BusinessMessaging.Model 4 | { 5 | /// 6 | /// CM Messaging API response containing message related info 7 | /// 8 | public class ResponseMessageDetail 9 | { 10 | /// 11 | /// Description of this message 12 | /// 13 | [JsonInclude] 14 | public string messageDetails { get; private set; } 15 | 16 | /// 17 | /// Error code specific to this message. 18 | /// 19 | [JsonInclude] 20 | public int messageErrorCode { get; private set; } 21 | 22 | /// 23 | /// How many parts this message was split in 24 | /// 25 | [JsonInclude] 26 | public int parts { get; private set; } 27 | 28 | /// 29 | /// The external reference sent to identify this message 30 | /// 31 | [JsonInclude] 32 | public string reference { get; private set; } 33 | 34 | /// 35 | /// Status of the message 36 | /// 37 | [JsonInclude] 38 | public string status { get; private set; } 39 | 40 | /// 41 | /// Recipient of this message 42 | /// 43 | [JsonInclude] 44 | public string to { get; private set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CM.Text.Tests/BuilderTests.cs: -------------------------------------------------------------------------------- 1 | using CM.Text.BusinessMessaging; 2 | using CM.Text.BusinessMessaging.Model; 3 | using CM.Text.BusinessMessaging.Model.MultiChannel; 4 | using FluentAssertions; 5 | 6 | namespace CM.Text.Tests 7 | { 8 | [TestClass] 9 | public class BuilderTests 10 | { 11 | [TestMethod] 12 | public void BuildTest() 13 | { 14 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 15 | 16 | var mediaName = "cm.com"; 17 | var mediaUri = "https://avatars3.githubusercontent.com/u/8234794?s=200&v=4"; 18 | var mediaType = "image/png"; 19 | 20 | builder 21 | .WithAllowedChannels(Channel.WhatsApp) 22 | .WithRichMessage( 23 | new MediaMessage( 24 | mediaName, 25 | mediaUri, 26 | mediaType 27 | ) 28 | ); 29 | var message = builder.Build(); 30 | 31 | message.Should().NotBeNull(); 32 | message.RichContent.Conversation.Should().NotBeNull(); 33 | message.RichContent.Conversation.Length.Should().Be(1); 34 | var media = (MediaMessage) message.RichContent.Conversation.First(); 35 | media.Media.MediaName.Should().Be(mediaName); 36 | media.Media.MediaUri.Should().Be(mediaUri); 37 | media.Media.MimeType.Should().Be(mediaType); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/CarouselBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CM.Text.BusinessMessaging.Model.MultiChannel; 3 | using JetBrains.Annotations; 4 | 5 | namespace CM.Text.BusinessMessaging 6 | { 7 | /// 8 | /// Builder class to construct a CarouselMessage. 9 | /// 10 | [PublicAPI] 11 | public class CarouselBuilder 12 | { 13 | private readonly List _cards = new List(); 14 | private readonly CarouselCardWidth _carouselCardWidth; 15 | 16 | /// 17 | /// Initializes the builder 18 | /// 19 | /// 20 | public CarouselBuilder(CarouselCardWidth carouselCardWidth) 21 | { 22 | _carouselCardWidth = carouselCardWidth; 23 | } 24 | 25 | /// 26 | /// Adds one card. 27 | /// 28 | /// 29 | /// 30 | public CarouselBuilder AddCard(RichCard card) 31 | { 32 | _cards.Add(card); 33 | return this; 34 | } 35 | 36 | /// 37 | /// Construct the carousel 38 | /// 39 | /// 40 | public CarouselMessage Build() 41 | { 42 | return new CarouselMessage 43 | { 44 | Carousel = new Carousel {Cards = _cards.ToArray(), CarouselCardWidth = _carouselCardWidth} 45 | }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/MediaMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A message, which can be used for rich content channels such as 8 | /// , and . 9 | /// 10 | [PublicAPI] 11 | public class MediaMessage : IRichMessage 12 | { 13 | /// 14 | /// Default constructor. 15 | /// 16 | public MediaMessage() 17 | { 18 | } 19 | 20 | /// 21 | /// Constructor setting values. 22 | /// 23 | /// 24 | /// 25 | /// 26 | public MediaMessage(string mediaName, string mediaUri, string mimeType) 27 | { 28 | Media = new MediaContent(mediaName, mediaUri, mimeType); 29 | } 30 | 31 | /// 32 | /// The image or video of the message. 33 | /// 34 | [JsonPropertyName("media")] 35 | public MediaContent Media { get; set; } 36 | 37 | /// 38 | /// Contextual properties of the message. Only applicable to 39 | /// 40 | [JsonPropertyName("context")] 41 | [CanBeNull] 42 | public MessageContext MessageContext { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/MediaContent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Object containing information about an image, a video or an audio file. 8 | /// 9 | [PublicAPI] 10 | public class MediaContent 11 | { 12 | /// 13 | /// Default constructor 14 | /// 15 | public MediaContent() 16 | { 17 | } 18 | 19 | /// 20 | /// Constructor which sets values 21 | /// 22 | /// 23 | /// 24 | /// 25 | public MediaContent(string mediaName, string mediaUri, string mimeType) 26 | { 27 | MediaName = mediaName; 28 | MediaUri = mediaUri; 29 | MimeType = mimeType; 30 | } 31 | 32 | /// 33 | /// The name of the image, audio or video. 34 | /// 35 | [JsonPropertyName("mediaName")] 36 | public string MediaName { get; set; } 37 | 38 | /// 39 | /// The location of the image, audio or video. 40 | /// 41 | [JsonPropertyName("mediaUri")] 42 | public string MediaUri { get; set; } 43 | 44 | /// 45 | /// The mimetype of the image, audio or video. 46 | /// 47 | [JsonPropertyName("mimeType")] 48 | public string MimeType { get; set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CM.Text/TextClientMessageDetail.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json.Serialization; 3 | using JetBrains.Annotations; 4 | 5 | namespace CM.Text 6 | { 7 | /// 8 | /// Data model that contains detailed message information per recipient. 9 | /// 10 | [PublicAPI] 11 | [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Json interface")] 12 | public class TextClientMessageDetail 13 | { 14 | /// 15 | /// Message details. 16 | /// 17 | /// 18 | /// The details. 19 | /// 20 | [JsonPropertyOrder(4)] 21 | public string details { get; set; } 22 | 23 | /// 24 | /// The amount of parts a message is split up to. 25 | /// 26 | /// 27 | /// The parts. 28 | /// 29 | [JsonPropertyOrder(3)] 30 | public int parts { get; set; } 31 | 32 | /// 33 | /// The reference to a message. 34 | /// 35 | /// 36 | /// The reference. 37 | /// 38 | [JsonPropertyOrder(0)] 39 | public string reference { get; set; } 40 | 41 | /// 42 | /// The status of a message. 43 | /// 44 | /// 45 | /// The status. 46 | /// 47 | [JsonPropertyOrder(1)] 48 | public string status { get; set; } 49 | 50 | /// 51 | /// The recipient. 52 | /// 53 | /// 54 | /// To. 55 | /// 56 | [JsonPropertyOrder(2)] 57 | public string to { get; set; } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CM.Text/TextClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using CM.Text.Interfaces; 4 | using JetBrains.Annotations; 5 | 6 | namespace CM.Text 7 | { 8 | /// 9 | /// Interface to a factory to create s for different api keys 10 | /// 11 | public interface ITextClientFactory 12 | { 13 | /// 14 | /// Gets a client for a specific product token. 15 | /// 16 | /// The api key to use in the new client. 17 | /// 18 | ITextClient GetClient(Guid apiKey); 19 | } 20 | 21 | /// 22 | /// Factory to create s for different api keys 23 | /// 24 | /// 25 | [PublicAPI] 26 | public class TextClientFactory : ITextClientFactory 27 | { 28 | private readonly HttpClient _httpClient; 29 | [CanBeNull] private readonly Uri _endPointOverride; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The HTTP client. 35 | /// (Optional) The end point to use, instead of the default "https://gw.messaging.cm.com/v1.0/message". 36 | public TextClientFactory(HttpClient httpClient, Uri endPointOverride = null) 37 | { 38 | _httpClient = httpClient; 39 | _endPointOverride = endPointOverride; 40 | } 41 | 42 | /// 43 | public ITextClient GetClient(Guid productToken) 44 | { 45 | return new TextClient(productToken, _httpClient, _endPointOverride); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/ViewLocationOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// The options for a in . 8 | /// 9 | [PublicAPI] 10 | public class ViewLocationOptions 11 | { 12 | /// 13 | /// Optional: The label to display at the pin 14 | /// 15 | [JsonPropertyName("label")] 16 | public string Label { get; set; } 17 | 18 | /// 19 | /// The latitude in degrees 20 | /// 21 | /// 51.603802 22 | /// Either Latitude and or is required 23 | [JsonPropertyName("latitude")] 24 | public string Latitude { get; set; } 25 | 26 | /// 27 | /// The longitude in degrees 28 | /// 29 | /// 4.770821 30 | /// Either and Longitude or is required 31 | [JsonPropertyName("longitude")] 32 | public string Longitude { get; set; } 33 | 34 | /// 35 | /// Search for this location instead of using the latitude/longitude. 36 | /// 37 | /// 38 | /// Either and or SearchQuery is required. 39 | /// For other connections both may be required. 40 | /// 41 | [JsonPropertyName("searchQuery")] 42 | public string SearchQuery { get; set; } 43 | 44 | /// 45 | /// Can be used in some connections to display a radius instead of only a pointer 46 | /// 47 | [JsonPropertyName("radius")] 48 | public int? Radius { get; set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/TextMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// A regular text message, replaces the for channels that support rich content 8 | /// (all channels except , and at this moment) 9 | /// 10 | [PublicAPI] 11 | public class TextMessage : IRichMessage 12 | { 13 | /// 14 | /// Construct an empty text message. 15 | /// 16 | public TextMessage() 17 | { 18 | } 19 | 20 | /// 21 | /// Construct a text message and initialise the 22 | /// 23 | /// 24 | public TextMessage(string text) 25 | { 26 | Text = text; 27 | } 28 | 29 | /// 30 | /// A plain text message, when used it replaces the 'SMS' body text. 31 | /// In , when used in combination with an header and/or media this will be set as the text of a rich card. 32 | /// 33 | [JsonPropertyName("text")] 34 | public string Text { get; set; } 35 | 36 | /// 37 | /// Tag to send important and/or personally relevant 1:1 updates to recipients. E.g. to notify a recipient of an update on a recent purchase. 38 | /// 39 | [JsonPropertyName("tag")] 40 | public string Tag { get; set; } 41 | 42 | /// 43 | /// The suggestions of a text message. 44 | /// 45 | [JsonPropertyName("suggestions")] 46 | public SuggestionBase[] Suggestions { get; set; } 47 | 48 | /// 49 | /// Contextual properties of the message. Currently only applicable to 50 | /// 51 | [JsonPropertyName("context")] 52 | [CanBeNull] 53 | public MessageContext MessageContext { get; set; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/Body.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model 5 | { 6 | /// 7 | /// Represents the Body element of the message. 8 | /// 9 | [PublicAPI] 10 | public class Body 11 | { 12 | /// 13 | /// The actual text body of the message. 14 | /// By default the CM gateway interprets messages as if sent with the standard 7 bit GSM encoding. 15 | /// If you want to send messages using e.g. Arabic, Cyrillic of Greek characters 16 | /// you will need to use the unicode UCS2 encoding. 17 | /// Set the to Auto to let the gateway do the encoding detection. 18 | /// Please note that there are a few limitations to using unicode encoded messages: 19 | /// Unicode messages can contain up to 70 characters. In the case of multipart messages, this becomes 66 characters per 20 | /// part. 21 | /// You will need to POST the XML or JSON file. A HTTP GET request cannot handle the Unicode characters 22 | /// Another note is that not all operators in the world are able to handle Unicode messages, so you will need to test 23 | /// for which operators it works. 24 | /// 25 | [JsonPropertyName("content")] 26 | public string Content { get; set; } 27 | 28 | /// 29 | /// When the type is set to 'auto' then the gateway will do the encoding detection. 30 | /// In case it detects characters that are not part of the GSM character set, 31 | /// the message will be delivered as Unicode. 32 | /// If the message contains more than 70 characters in Unicode format it will be split into a 33 | /// multipart message. 34 | /// You can limit the number of parts by setting the maximum number of message parts. 35 | /// 36 | /// 37 | [JsonPropertyName("type")] 38 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 39 | public string Type { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/RichContent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using JetBrains.Annotations; 4 | 5 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 6 | { 7 | /// 8 | /// Can be used by channels that support rich content (all channels except , 9 | /// and at this moment) 10 | /// 11 | [PublicAPI] 12 | public class RichContent 13 | { 14 | /// 15 | /// Initializes a rich content object 16 | /// 17 | public RichContent() 18 | { 19 | Conversation = null; 20 | Suggestions = null; 21 | } 22 | 23 | /// 24 | /// The messages. 25 | /// 26 | [JsonPropertyName("conversation")] 27 | public IRichMessage[] Conversation { get; set; } 28 | 29 | /// 30 | /// The suggestions 31 | /// 32 | [JsonPropertyName("suggestions")] 33 | public SuggestionBase[] Suggestions { get; set; } 34 | 35 | /// 36 | /// Adds a message, such as a or . 37 | /// 38 | /// 39 | public void AddConversationPart(IRichMessage part) 40 | { 41 | if (Conversation == null) 42 | Conversation = new[] {part}; 43 | else 44 | { 45 | var newArr = Conversation; 46 | Array.Resize(ref newArr, Conversation.Length + 1); 47 | newArr[newArr.Length - 1] = part; 48 | Conversation = newArr; 49 | } 50 | } 51 | 52 | /// 53 | /// Adds a suggestion 54 | /// 55 | /// 56 | public void AddSuggestion(SuggestionBase suggestion) 57 | { 58 | if (Suggestions == null) 59 | Suggestions = new[] {suggestion}; 60 | else 61 | { 62 | var newArr = Suggestions; 63 | Array.Resize(ref newArr, Suggestions.Length + 1); 64 | newArr[newArr.Length - 1] = suggestion; 65 | Suggestions = newArr; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CM.Text/TextClientStatusCode.cs: -------------------------------------------------------------------------------- 1 | namespace CM.Text 2 | { 3 | /// 4 | /// TextClientResult status codes 5 | /// 6 | public enum TextClientStatusCode 7 | { 8 | /// 9 | /// Ok 10 | /// 11 | Ok = 0, 12 | 13 | /// 14 | /// Authentication failed. 15 | /// 16 | AuthenticationFailed = 101, 17 | 18 | /// 19 | /// Insufficient balance. 20 | /// 21 | BalanceInsufficient = 102, 22 | 23 | /// 24 | /// Incorrect API key. 25 | /// 26 | ApiKeyIncorrect = 103, 27 | 28 | /// 29 | /// This request has one or more errors in its messages. Some or all messages have not been sent. 30 | /// 31 | NotAllSent = 201, 32 | 33 | /// 34 | /// Malformed request 35 | /// 36 | RequestMalformed = 202, 37 | 38 | /// 39 | /// The MSG array is incorrect. 40 | /// 41 | MsgArrayIncorrect = 203, 42 | 43 | /// 44 | /// From field invalid 45 | /// 46 | FromFieldInvalid = 301, 47 | 48 | /// 49 | /// To field invalid 50 | /// 51 | ToFieldInvalid = 302, 52 | 53 | /// 54 | /// MSISDN invalid 55 | /// 56 | MsisdnInvalid = 303, 57 | 58 | /// 59 | /// Body field invalid 60 | /// 61 | BodyFieldInvalid = 304, 62 | 63 | /// 64 | /// Field invalid 65 | /// 66 | FieldInvalid = 305, 67 | 68 | /// 69 | /// Spam filtered 70 | /// 71 | SpamFiltered = 401, 72 | 73 | /// 74 | /// Black listed 75 | /// 76 | BlackListed = 402, 77 | 78 | /// 79 | /// Rejected 80 | /// 81 | Rejected = 403, 82 | 83 | /// 84 | /// Internal server error 85 | /// 86 | InternalServerError = 500, 87 | 88 | /// 89 | /// Unknown error, please contact CM support 90 | /// 91 | Unknown = 999 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CM.Text.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CM.Text", "CM.Text\CM.Text.csproj", "{1EB6D5AC-4759-4A77-8D9D-F5B23C254B7A}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C893F008-8E43-4C38-ABF4-FA88F5549906}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | CHANGELOG.md = CHANGELOG.md 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CM.Text.Tests", "CM.Text.Tests\CM.Text.Tests.csproj", "{5389A890-EBC2-45F0-95E3-374B0BD239FF}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{8AC75C4D-CF2F-4E5F-85D2-4BB92C8ED1DE}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{83568C54-6C9B-4579-BC2A-E6739DE072D5}" 20 | ProjectSection(SolutionItems) = preProject 21 | .github\workflows\main.yml = .github\workflows\main.yml 22 | .github\workflows\nuget.yml = .github\workflows\nuget.yml 23 | EndProjectSection 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {1EB6D5AC-4759-4A77-8D9D-F5B23C254B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {1EB6D5AC-4759-4A77-8D9D-F5B23C254B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {1EB6D5AC-4759-4A77-8D9D-F5B23C254B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {1EB6D5AC-4759-4A77-8D9D-F5B23C254B7A}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {5389A890-EBC2-45F0-95E3-374B0BD239FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {5389A890-EBC2-45F0-95E3-374B0BD239FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {5389A890-EBC2-45F0-95E3-374B0BD239FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {5389A890-EBC2-45F0-95E3-374B0BD239FF}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(NestedProjects) = preSolution 44 | {83568C54-6C9B-4579-BC2A-E6739DE072D5} = {8AC75C4D-CF2F-4E5F-85D2-4BB92C8ED1DE} 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {33F57A4C-CDF2-44F4-8F81-45D5C71E3DE1} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/BusinessMessagingApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using CM.Text.BusinessMessaging.Model; 6 | 7 | namespace CM.Text.BusinessMessaging 8 | { 9 | internal static class BusinessMessagingApi 10 | { 11 | /// 12 | /// Gets the HTTP post body. 13 | /// 14 | /// The API key. 15 | /// The message text. 16 | /// From. 17 | /// To. 18 | /// The reference. 19 | /// 20 | internal static string GetHttpPostBody( 21 | Guid apiKey, 22 | string messageText, 23 | string from, 24 | IEnumerable to, 25 | string reference) 26 | { 27 | var message = new MessageBuilder(messageText, from, to.ToArray()).WithReference(reference) 28 | .Build(); 29 | return GetHttpPostBody(apiKey, message); 30 | } 31 | 32 | /// 33 | /// Gets the HTTP post body. 34 | /// 35 | /// The API key. 36 | /// The message to send. 37 | /// 38 | internal static string GetHttpPostBody(Guid apiKey, Message message) 39 | { 40 | return JsonSerializer.Serialize( 41 | new 42 | { 43 | messages = new Request.MessagesEnvelope 44 | { 45 | Authentication = new Request.Authentication {ProductToken = apiKey.ToString()}, 46 | Messages = new List {message}.ToArray() 47 | } 48 | } 49 | ); 50 | } 51 | 52 | /// 53 | /// Transforms the BusinessMessageApi request result to TextClientResult. 54 | /// 55 | /// Content of the request result. 56 | /// 57 | internal static TextClientResult GetTextApiResult(string requestResultContent) 58 | { 59 | var deserializedResponse = JsonSerializer.Deserialize(requestResultContent); 60 | 61 | return new TextClientResult 62 | { 63 | statusMessage = deserializedResponse.details, 64 | statusCode = (TextClientStatusCode) deserializedResponse.errorCode, 65 | details = deserializedResponse.messages.Select( 66 | message => new TextClientMessageDetail 67 | { 68 | reference = message.reference, 69 | status = message.status, 70 | to = message.to, 71 | parts = message.parts, 72 | details = message.messageDetails 73 | } 74 | ) 75 | .ToArray() 76 | }; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CM.Text/Interfaces/ITextClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CM.Text.BusinessMessaging.Model; 5 | using JetBrains.Annotations; 6 | 7 | namespace CM.Text.Interfaces 8 | { 9 | /// 10 | /// Interface to the client to send text messages. 11 | /// 12 | public interface ITextClient 13 | { 14 | /// 15 | /// Sends a message asynchronous. 16 | /// 17 | /// The message text. 18 | /// 19 | /// This is the sender name. The maximum length is 11 alphanumerical characters or 16 digits. Example: 'MyCompany'.
20 | /// For Twitter: use the Twitter Snowflake ID of the account you want to use as sender.
21 | /// For MobilePush: use the app key of the account you want to use as sender.
22 | /// For Facebook Messenger: use the Facebook Page ID of the account you want to use as sender.
23 | /// For Google Business Messages: use the Google Business Messages agent ID of the account you want to use as sender (without dashes).
24 | /// For Instagram: use the Instagram Account ID of the account you want to use as sender.
25 | /// For Telegram: use the Telegram Bot ID of the account you want to use as sender. 26 | /// 27 | /// 28 | /// These are the destination mobile numbers. Restrictions: this value should be in international format. 29 | /// Example: '00447911123456'.
30 | /// For Twitter: use the Twitter Snowflake ID.
31 | /// For Facebook Messenger: use the Facebook Page Scoped User ID (PSID).
32 | /// For Google Business Messages: use the Google Business Messages conversation ID (without dashes).
33 | /// For Instagram: use the Instagram Scoped User ID (IGSID).
34 | /// For Telegram: use the Telegram Chat ID. 35 | /// 36 | /// 37 | /// Here you can include your message reference. This information will be returned in a status 38 | /// report so you can match the message and it's status. Restrictions: 1 - 32 alphanumeric characters and reference 39 | /// will not work for demo accounts. 40 | /// 41 | /// The cancellation token. 42 | /// 43 | Task SendMessageAsync( 44 | string messageText, 45 | string from, 46 | IEnumerable to, 47 | [CanBeNull] string reference, 48 | CancellationToken cancellationToken = default); 49 | 50 | /// 51 | /// Sends a message asynchronous. 52 | /// 53 | /// The message to send. 54 | /// The cancellation token. 55 | /// 56 | Task SendMessageAsync( 57 | Message message, 58 | CancellationToken cancellationToken = default); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/ApplePayConfiguration.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text.Json.Serialization; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Object that describes the Apple Pay configuration 8 | /// 9 | public class ApplePayConfiguration 10 | { 11 | /// 12 | /// (Required) A unique identifier that represents a merchant for Apple Pay. 13 | /// 14 | [JsonPropertyName("merchantName")] 15 | public string MerchantName { get; set; } 16 | /// 17 | /// (Required) Description of the item being bought. 18 | /// 19 | [JsonPropertyName("description")] 20 | public string Description { get; set; } 21 | /// 22 | /// (Required) A unique identifier that represents a order 23 | /// 24 | [JsonPropertyName("orderReference")] 25 | public string OrderReference { get; set; } 26 | 27 | /// 28 | /// An array of line items explaining payments and additional charges. 29 | /// 30 | /// Line items are not required. However, the array cannot be empty if the lineItems key is present. 31 | /// 32 | [JsonPropertyName("lineItems")] 33 | public LineItem[] LineItems { get; set; } 34 | 35 | /// 36 | /// A dictionary containing the total. 37 | /// 38 | /// The total amount must be greater than zero to pass validation. 39 | /// 40 | [JsonPropertyName("total")] 41 | public decimal Total { get; set; } 42 | 43 | /// 44 | /// Email address of the Apple Pay contact. 45 | /// 46 | [JsonPropertyName("recipientEmail")] 47 | public string RecipientEmail { get; set; } 48 | /// 49 | /// Value indicating the currency code of the apple pay request 50 | /// 51 | /// Value must be in upper case. 52 | /// 53 | [JsonPropertyName("currencyCode")] 54 | public string CurrencyCode { get; set; } 55 | /// 56 | /// Country of the Apple Pay contact. 57 | /// 58 | /// Value must be in upper case. 59 | /// 60 | [JsonPropertyName("recipientCountryCode")] 61 | public string RecipientCountryCode { get; set; } 62 | /// 63 | /// The Language of the Country of the Apple Pay Contact 64 | /// 65 | /// Value must be in lower case. 66 | /// 67 | [JsonPropertyName("languageCountryCode")] 68 | public string languageCountryCode { get; set; } 69 | /// 70 | /// Value indicating that a billing address is required 71 | /// 72 | [JsonPropertyName("billingAddressRequired")] 73 | public bool BillingAddressRequired { get; set; } 74 | /// 75 | /// Value indicating that a shipping contact is required 76 | /// 77 | [JsonPropertyName("shippingContactRequired")] 78 | public bool ShippingContactRequired { get; set; } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CM.Text/Identity/OtpRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.Identity 5 | { 6 | /// 7 | /// A request to send an OTP towards an end-user. 8 | /// 9 | [PublicAPI] 10 | public class OtpRequest 11 | { 12 | /// 13 | /// Required: This is the sender name. 14 | /// The maximum length is 11 alphanumerical characters or 16 digits. Example: 'MyCompany' 15 | /// 16 | [JsonPropertyName("from")] 17 | public string From { get; set; } 18 | 19 | /// 20 | /// Required: The destination mobile numbers. 21 | /// This value should be in international format. 22 | /// A single mobile number per request. Example: '00447911123456' 23 | /// 24 | [JsonPropertyName("to")] 25 | public string To { get; set; } 26 | 27 | /// 28 | /// The length of the code (min 4, max 10). default: 5. 29 | /// 30 | [JsonPropertyName("digits")] 31 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 32 | public int? Digits { get; set; } 33 | 34 | /// 35 | /// The expiry in seconds (min 10, max 3600). default: 60 seconds. 36 | /// 37 | [JsonPropertyName("expiry")] 38 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 39 | public int? Expiry { get; set; } 40 | 41 | /// 42 | /// The channel to send the code. 43 | /// Supported values: auto, sms, push, whatsapp, voice, email. 44 | /// Channel auto is only available with a SOLiD subscription. 45 | /// 46 | [JsonPropertyName("channel")] 47 | public string Channel { get; set; } = "sms"; 48 | 49 | /// 50 | /// The locale, for WhatsApp supported values: en, nl, fr, de, it, es. 51 | /// Default: en 52 | /// 53 | /// For Voice: the spoken language in the voice call, 54 | /// supported values: de-DE, en-AU, en-GB, en-IN, en-US, es-ES, fr-CA, fr-FR, it-IT, ja-JP, nl-NL 55 | /// Default: en-GB. 56 | /// 57 | /// For Email: The locale for the email template. 58 | /// 59 | [JsonPropertyName("locale")] 60 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 61 | [CanBeNull] 62 | public string Locale { get; set; } 63 | 64 | /// 65 | /// The app key, when is 'push' 66 | /// 67 | [JsonPropertyName("pushAppKey")] 68 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 69 | [CanBeNull] 70 | public string PushAppKey { get; set; } 71 | 72 | /// 73 | /// For WhatsApp, set a custom message. You can use the placeholder {code}, this will be replaced by the actual code. 74 | /// Example: Your code is: {code}. This is only used as a fallback in case the message could not be delivered via WhatsApp. 75 | /// 76 | /// For email, Set a custom message to be used in the email message. Do not include the {code} placeholder. 77 | /// 78 | [JsonPropertyName("message")] 79 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 80 | [CanBeNull] 81 | public string Message { get; set; } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CM.Text/Identity/OtpRequestBuilder.cs: -------------------------------------------------------------------------------- 1 | 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.Identity 5 | { 6 | /// 7 | /// Builder class to construct messages 8 | /// 9 | [PublicAPI] 10 | public class OtpRequestBuilder 11 | { 12 | private readonly OtpRequest _otpRequest; 13 | 14 | /// 15 | /// Creates a new OtpRequestBuilder 16 | /// 17 | /// 18 | /// 19 | public OtpRequestBuilder(string from, string to) 20 | { 21 | _otpRequest = new OtpRequest { From = from, To = to }; 22 | } 23 | 24 | /// 25 | /// Constructs the request. 26 | /// 27 | /// 28 | public OtpRequest Build() 29 | { 30 | return _otpRequest; 31 | } 32 | 33 | /// 34 | /// Set the channel 35 | /// 36 | public OtpRequestBuilder WithChannel(string channel) 37 | { 38 | _otpRequest.Channel = channel; 39 | return this; 40 | } 41 | 42 | /// 43 | /// Sets The length of the code (min 4, max 10). default: 5. 44 | /// 45 | /// 46 | /// 47 | public OtpRequestBuilder WithDigits(int digits) 48 | { 49 | _otpRequest.Digits = digits; 50 | return this; 51 | } 52 | 53 | /// 54 | /// The expiry in seconds (min 10, max 3600). default: 60 seconds. 55 | /// 56 | public OtpRequestBuilder WithExpiry(int expiryInSeconds) 57 | { 58 | _otpRequest.Expiry = expiryInSeconds; 59 | return this; 60 | } 61 | 62 | /// 63 | /// The locale, for WhatsApp supported values: en, nl, fr, de, it, es. 64 | /// Default: en 65 | /// 66 | /// For Voice: the spoken language in the voice call, 67 | /// supported values: de-DE, en-AU, en-GB, en-IN, en-US, es-ES, fr-CA, fr-FR, it-IT, ja-JP, nl-NL 68 | /// Default: en-GB. 69 | /// 70 | /// For Email: The locale for the email template. 71 | /// 72 | /// 73 | /// 74 | public OtpRequestBuilder WithLocale(string locale) 75 | { 76 | _otpRequest.Locale = locale; 77 | return this; 78 | } 79 | 80 | /// 81 | /// The app key, when the channel is 'push' 82 | /// 83 | /// 84 | /// 85 | public OtpRequestBuilder WithPushAppKey(string pushAppKey) 86 | { 87 | _otpRequest.PushAppKey = pushAppKey; 88 | return this; 89 | } 90 | 91 | /// 92 | /// For WhatsApp, set a custom message. You can use the placeholder {code}, this will be replaced by the actual code. 93 | /// Example: Your code is: {code}. This is only used as a fallback in case the message could not be delivered via WhatsApp. 94 | /// 95 | /// For email, Set a custom message to be used in the email message. Do not include the {code} placeholder. 96 | /// 97 | /// 98 | /// 99 | public OtpRequestBuilder WithMessage(string message) 100 | { 101 | _otpRequest.Message = message; 102 | return this; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /CM.Text.Tests/BusinessMessagingApiTests.cs: -------------------------------------------------------------------------------- 1 | using CM.Text.BusinessMessaging; 2 | using CM.Text.BusinessMessaging.Model; 3 | using CM.Text.BusinessMessaging.Model.MultiChannel; 4 | using FluentAssertions; 5 | 6 | namespace CM.Text.Tests 7 | { 8 | [TestClass] 9 | public class BusinessMessagingApiTests 10 | { 11 | [TestMethod] 12 | public void TestPostBody() 13 | { 14 | var guid = Guid.NewGuid(); 15 | var message = "This is a unit test"; 16 | var sender = "CM.com"; 17 | var reference = "ReferenceForMeToFind"; 18 | var number1 = "0031612345678"; 19 | var number2 = "0031612345679"; 20 | 21 | var data = BusinessMessagingApi.GetHttpPostBody(guid, message, sender, 22 | new[] {number1, number2}, reference); 23 | 24 | data.Should().NotBeNull(); 25 | //Simple to check if all values survived our logic 26 | data.Should().Contain(guid.ToString()); 27 | data.Should().Contain(message); 28 | data.Should().Contain(sender); 29 | data.Should().Contain(reference); 30 | data.Should().Contain(number1); 31 | data.Should().Contain(number2); 32 | } 33 | 34 | [TestMethod] 35 | public void TestRichPostBody() 36 | { 37 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 38 | 39 | var mediaName = "cm.com icon"; 40 | var mediaUri = "https://avatars3.githubusercontent.com/u/8234794"; 41 | var mediaType = "image/png"; 42 | 43 | builder 44 | .WithAllowedChannels(Channel.WhatsApp) 45 | .WithRichMessage( 46 | new MediaMessage( 47 | mediaName, 48 | mediaUri, 49 | mediaType 50 | ) 51 | ); 52 | var message = builder.Build(); 53 | 54 | Guid fakeApiKey = Guid.NewGuid(); 55 | var data = BusinessMessagingApi.GetHttpPostBody(fakeApiKey, message); 56 | 57 | data.Should().NotBeNull(); 58 | //Simple to check if all values survived our logic 59 | data.Should().Contain(fakeApiKey.ToString(), "the api key should be present in the body"); 60 | data.Should().Contain(mediaName, "the media name needs to be sent"); 61 | data.Should().Contain(mediaType, "the media type has to be sent"); 62 | data.Should().Contain(mediaUri, "the media url has to be sent"); 63 | } 64 | 65 | 66 | [TestMethod] 67 | public void TestResponseBody() 68 | { 69 | var guid = Guid.NewGuid(); 70 | // Arrange 71 | string message = @"{ 72 | ""messages"": [{ 73 | ""to"": ""0031612345678"", 74 | ""parts"": 1, 75 | ""status"": ""Accepted"", 76 | ""reference"": ""test-reference-1"", 77 | ""messageErrorCode"": 0, 78 | ""messageDetails"": null 79 | }], 80 | ""details"": ""Created 1 message(s)"", 81 | ""errorCode"": 0 82 | }"; 83 | 84 | 85 | var data = BusinessMessagingApi.GetTextApiResult(message); 86 | 87 | data.Should().NotBeNull(); 88 | //Simple to check if all values survived our logic 89 | data.details.Should().NotBeNull(); 90 | data.details.First().reference.Should().Be("test-reference-1"); 91 | data.details.First().status.Should().Be("Accepted"); 92 | data.details.First().to.Should().Be("0031612345678"); 93 | data.details.First().parts.Should().Be(1); 94 | data.details.First().details.Should().BeNull(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CM.Text/CM.Text.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0 5 | CM.com Messaging team 6 | CM.com 7 | CM Text SDK 8 | A software development kit to provide ways to interact with CM.com's Text/messaging service. 9 | messaging, text, sms, email, push, whatsapp, viber, wechat, rcs, voice, business messaging, telegram, conversational, sms chat, chat, sms 2.0, rbm, mms, rich sms, rich communication services, telegram 10 | https://github.com/cmdotcom/text-sdk-dotnet 11 | Github 12 | $([System.DateTime]::UtcNow.Year) CM.com 13 | LICENSE 14 | icon.png 15 | $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../CHANGELOG.md")) 16 | 2.12.0 17 | https://github.com/cmdotcom/text-sdk-dotnet 18 | en 19 | true 20 | 2.12.0 21 | 2.12.0 22 | True 23 | README.md 24 | 25 | 26 | 27 | True 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | 9999 36 | True 37 | 1701;1702; 38 | 39 | 40 | 41 | 42 | 9999 43 | True 44 | 1701;1702; 45 | 46 | 47 | 48 | 49 | 9999 50 | True 51 | 1701;1702; 52 | 53 | 54 | 55 | 56 | 9999 57 | True 58 | 1701;1702; 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | PreserveNewest 73 | 74 | 75 | 76 | 77 | 78 | True 79 | \ 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.12.0] - 2025-08-01 8 | ### Updated 9 | - Updated dependencies and .NET version 10 | 11 | ## [2.11.1] - 2025-01-31 12 | ### Added 13 | - Derived type declarations on `SuggestionBase` 14 | 15 | ## [2.11.0] - 2025-01-10 16 | ### Added 17 | - Context.MessageId on RichMessages supporting WhatsApp for referencing a previous message 18 | 19 | ## [2.10.0] - 2024-12-30 20 | ### Added 21 | - Polymorphic type discriminators on IRichMessage for deserialization 22 | 23 | ### Changed 24 | - Updated `System.Text.Json` to 8.0.5 fixing a [high impact security vulnerability](https://github.com/advisories/GHSA-hh2w-p6rv-4g7w) 25 | 26 | ## [2.9.0] - 2024-06-20 27 | ### Changed 28 | - New global messaging endpoint 29 | 30 | ## [2.8.0] - 2024-01-08 31 | ### Added 32 | - Implementing OTP Request and verify 33 | 34 | ## [2.7.0] - 2023-06-26 35 | ### Added 36 | - Whatsapp Multi-Product Template Messages 37 | 38 | ## [2.6.0] - 2022-12-07 39 | ### Added 40 | - System.Text.Json, replaces Newtsonsoft, possibly breaking 41 | - Add Telegram as tag for the project 42 | - Multi target to .NET 7 and use .NET 7 included System.Text.Json 43 | ### Changed 44 | - Enable "Treat Warnings as Errors" to adhere to code guidelines 45 | - Give internal response body messagerrorcode the correct type 46 | - Some internal refactoring 47 | - Changed PackageIconUrl > PackageIcon and added icon manually as it PackageIconUrl was deprecated 48 | - Change RichContent interface to support derived types 49 | ### Removed 50 | - Newtsonsoft.Json dependency 51 | 52 | ## [2.5.2] - 2022-10-14 53 | - Enable nupkg generation in project 54 | 55 | ## [2.5.1] - 2022-10-12 56 | - Upped Newtonsoft.Json 57 | 58 | ## [2.5.0] - 2022-06-17 59 | - Add Telegram 60 | 61 | ## [2.4.1] - 2022-02-01 62 | - Fix support for a custom end point URI in second SendMessage method 63 | 64 | ## [2.4.0] - 2022-01-31 65 | - Fix failure on body.type set to null 66 | - Add support for a custom end point URI 67 | 68 | ## [2.3.0] - 2021-10-27 69 | - Add TextClientFactory 70 | 71 | ## [2.2.1] - 2021-09-10 72 | - Add support for reply buttons with media 73 | 74 | ## [2.2.0] - 2021-09-09 75 | - Add support for interactive whatsapp messages 76 | 77 | ## [2.1.0] - 2021-07-08 78 | - Ignore HybridAppKey when null instead of empty, to support Notifire flow of channel MobilePush 79 | - Add DefaultValueHandling to RichContent property of Message model. 80 | - Add Tag property to TextMessage model used within a rich message. 81 | - Add Suggestions property to TextMessage model used within a rich message. 82 | - Add AppleBusinessChat as channel type, and marked iMessage as obsolete. 83 | 84 | ## [2.0.6] - 2021-07-06 85 | - Add Instagram 86 | 87 | ## [2.0.5] - 2021-05-25 88 | - Correct JSON formatting of MediaContent 89 | 90 | ## [2.0.4] - 2021-05-20 91 | - Added media en description to ReplySuggestion 92 | 93 | ## [2.0.3] - 2021-04-08 94 | - Add Facebook Messenger and Google Business Messages channel 95 | 96 | ## [2.0.2] - 2020-12-23 97 | - Add MobilePush channel 98 | 99 | ## [2.0.1] - 2020-11-23 100 | - Add support for interactive templates 101 | 102 | ## [2.0.0] - 2020-08-26 103 | - Removed Localizable Params because it is Deprecated by Facebook, 104 | - Also updated the WhatsApp Template Signature based on Facebook spec updates. 105 | 106 | ## [1.8.0] - 2020-06-24 107 | - Add Twitter support 108 | 109 | ## [1.7.0] - 2020-02-14 110 | - Able to set validity period. (@GuidoNeele) 111 | - Fixed issue when adding suggestions. 112 | - Resolved typo's in documentation. 113 | 114 | ## [1.6.1] - 2020-02-14 115 | - Version is equal to 1.7.0, due to not working release flow. 116 | 117 | ## [1.6.0] - 2020-02-07 118 | - Add Rich templates + ApplePay 119 | 120 | ## [1.5.2] - 2019-12-04 121 | - Fix WhatsApp template signature (@Michel16867) 122 | 123 | ## [1.5.0] - 2019-11-01 124 | - Added several RichFeatures. 125 | 1. TemplateMessage 126 | 2. LocationPushMessage 127 | 3. ContactMessage 128 | 129 | ## [1.4.0] - 2019-03-05 130 | - Fix typo in conversation 131 | 132 | ## [1.3.0] - 2019-02-26 133 | - Add license information 134 | 135 | ## [1.2.0] - 2019-02-22 136 | - Add multichannel options 137 | 138 | ## [1.1.0] - 2019-02-22 139 | - Add option to pass http client to constructor 140 | 141 | ## [1.0.1] - 2018-11-08 142 | - Initial release 143 | -------------------------------------------------------------------------------- /CM.Text.Tests/SuggestionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using CM.Text.BusinessMessaging.Model.MultiChannel; 3 | using FluentAssertions; 4 | 5 | namespace CM.Text.Tests 6 | { 7 | [TestClass] 8 | public class SuggestionsTests 9 | { 10 | [TestMethod] 11 | public void SerializationCalendarSuggestionTest() 12 | { 13 | var calenderSuggestion = new CalendarSuggestion() 14 | { 15 | Calendar = new CalendarOptions() 16 | { 17 | Title = "Appointment selection", 18 | Description = "Schedule an appointment with us", 19 | EndTime = DateTime.UtcNow, 20 | StartTime = DateTime.UtcNow, 21 | } 22 | }; 23 | 24 | var serialized= JsonSerializer.Serialize(calenderSuggestion); 25 | 26 | serialized.Should().NotBeNullOrWhiteSpace(); 27 | 28 | var deserialized = JsonSerializer.Deserialize(serialized); 29 | 30 | deserialized.Should().BeOfType(); 31 | var deserializedCalendarSuggestion = deserialized as CalendarSuggestion; 32 | deserializedCalendarSuggestion.Should().BeEquivalentTo(calenderSuggestion); 33 | } 34 | 35 | [TestMethod] 36 | public void SerializationDialSuggestionTest() 37 | { 38 | var dialSuggestion = new DialSuggestion() 39 | { 40 | Dial = new Dial() 41 | { 42 | PhoneNumber = "0031612345678" 43 | } 44 | }; 45 | 46 | var serialized= JsonSerializer.Serialize(dialSuggestion); 47 | 48 | serialized.Should().NotBeNullOrWhiteSpace(); 49 | 50 | var deserialized = JsonSerializer.Deserialize(serialized); 51 | 52 | deserialized.Should().BeOfType(); 53 | var deserializedDialSuggestion = deserialized as DialSuggestion; 54 | deserializedDialSuggestion.Should().BeEquivalentTo(dialSuggestion); 55 | } 56 | 57 | [TestMethod] 58 | public void SerializationOpenUrlSuggestionTest() 59 | { 60 | var openUrlSuggestion = new OpenUrlSuggestion() 61 | { 62 | Url = "https://www.cm.com" 63 | }; 64 | 65 | var serialized= JsonSerializer.Serialize(openUrlSuggestion); 66 | 67 | serialized.Should().NotBeNullOrWhiteSpace(); 68 | 69 | var deserialized = JsonSerializer.Deserialize(serialized); 70 | 71 | deserialized.Should().BeOfType(); 72 | var deserializedOpenUrlSuggestion = deserialized as OpenUrlSuggestion; 73 | deserializedOpenUrlSuggestion.Should().BeEquivalentTo(openUrlSuggestion); 74 | } 75 | 76 | [TestMethod] 77 | public void SerializationViewLocationSuggestionTest() 78 | { 79 | var viewLocationSuggestion = new ViewLocationSuggestion() 80 | { 81 | Location = new ViewLocationOptions() 82 | { 83 | Label = "CM.com headquarters", 84 | Latitude = "51.602885", 85 | Longitude = "4.7683932", 86 | SearchQuery = "CM.com headquarters", 87 | Radius = 5 88 | } 89 | }; 90 | 91 | var serialized= JsonSerializer.Serialize(viewLocationSuggestion); 92 | 93 | serialized.Should().NotBeNullOrWhiteSpace(); 94 | 95 | var deserialized = JsonSerializer.Deserialize(serialized); 96 | 97 | deserialized.Should().BeOfType(); 98 | var deserializedViewLocationSuggestion = deserialized as ViewLocationSuggestion; 99 | deserializedViewLocationSuggestion.Should().BeEquivalentTo(viewLocationSuggestion); 100 | } 101 | 102 | [TestMethod] 103 | public void SerializationReplySuggestionTest() 104 | { 105 | var replySuggestion = new ReplySuggestion() 106 | { 107 | Label = "Some label", 108 | PostbackData = "LABEL", 109 | Description = "Description of the label", 110 | Media = new MediaContent("Test image", "https://example.com", "image/jpg") 111 | }; 112 | var serialized= JsonSerializer.Serialize(replySuggestion); 113 | 114 | serialized.Should().NotBeNullOrWhiteSpace(); 115 | 116 | var deserialized = JsonSerializer.Deserialize(serialized); 117 | 118 | deserialized.Should().BeOfType(); 119 | var deserializedReplySuggestion = deserialized as ReplySuggestion; 120 | deserializedReplySuggestion.Should().BeEquivalentTo(replySuggestion); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/Channel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | using JetBrains.Annotations; 5 | 6 | namespace CM.Text.BusinessMessaging.Model 7 | { 8 | /// 9 | /// Used by to force a message to only use certain routes. 10 | /// 11 | /// 12 | /// Note that for channels other than SMS, CM needs to configure the out going flows. 13 | /// For those flows to work, we need to be contacted. 14 | /// 15 | [PublicAPI] 16 | [JsonConverter(typeof(JsonStringEnumConverter))] 17 | public enum Channel 18 | { 19 | /// 20 | /// Messages will be sent as SMS text messages 21 | /// 22 | SMS, 23 | 24 | /// 25 | /// Send messages using WhatsApp for business 26 | /// 27 | /// 28 | /// Note that CM needs to configure this for you to work. 29 | /// 30 | WhatsApp, 31 | 32 | /// 33 | /// Sends messages to push using Hybrid messages. 34 | /// See also https://developers.cm.com/messaging/docs 35 | /// 36 | /// Works only when is set 37 | Push, 38 | 39 | /// 40 | /// Messages will be sent over RCS. 41 | /// 42 | /// 43 | /// Note that CM needs to configure this for you to work. 44 | /// 45 | RCS, 46 | 47 | /// 48 | /// Messages will be sent over Viber. 49 | /// 50 | /// 51 | /// Note that CM needs to configure this for you to work. 52 | /// 53 | Viber, 54 | 55 | /// 56 | /// Messages will be sent using text to speech. 57 | /// 58 | /// 59 | /// Note that CM needs to configure this for you to work. 60 | /// 61 | Voice, 62 | 63 | /// 64 | /// Messages will be sent over Apple Business Chat. 65 | /// 66 | /// 67 | /// Note that CM needs to configure this for you to work. 68 | /// 69 | [Obsolete("Use 'Channel.AppleBusinessChat' instead")] 70 | // ReSharper disable once InconsistentNaming 71 | iMessage, 72 | 73 | /// 74 | /// Messages will be sent over Apple Business Chat. 75 | /// 76 | /// 77 | /// Note that CM needs to configure this for you to work. 78 | /// 79 | [EnumMember(Value = "Apple Business Chat")] 80 | AppleBusinessChat, 81 | 82 | /// 83 | /// Messages will be sent over Line. 84 | /// 85 | /// 86 | /// Note that CM needs to configure this for you to work. 87 | /// 88 | Line, 89 | 90 | /// 91 | /// Messages will be sent over Twitter. 92 | /// 93 | /// 94 | /// Note that CM needs to configure this for you to work. 95 | /// 96 | Twitter, 97 | 98 | /// 99 | /// Messages will be sent over the new Push channel. 100 | /// 101 | /// 102 | /// This channel is the successor of the "Push" channel. 103 | /// Contact CM for information on how to migrate your current Push integration 104 | /// 105 | MobilePush, 106 | 107 | /// 108 | /// Messages will be sent over Facebook Messenger. 109 | /// 110 | /// 111 | /// Note that CM needs to configure this for you to work. 112 | /// 113 | [EnumMember(Value = "Facebook Messenger")] 114 | FacebookMessenger, 115 | 116 | /// 117 | /// Messages will be sent over Google Business Messages. 118 | /// 119 | /// 120 | /// Note that CM needs to configure this for you to work. 121 | /// 122 | [EnumMember(Value = "Google Business Messages")] 123 | GoogleBusinessMessages, 124 | 125 | /// 126 | /// Messages will be sent over Instagram. 127 | /// 128 | /// 129 | /// Note that CM needs to configure this for you to work. 130 | /// 131 | [EnumMember(Value = "Instagram")] 132 | Instagram, 133 | 134 | /// 135 | /// Messages will be sent over Telegram. 136 | /// 137 | /// 138 | /// Note that CM needs to configure this for you to work. 139 | /// 140 | [EnumMember(Value = "Telegram Messenger")] 141 | Telegram, 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | 5 | root = true 6 | 7 | # All files 8 | [*] 9 | indent_style = space 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8-bom 16 | 17 | ############################### 18 | # .NET Coding Conventions # 19 | ############################### 20 | 21 | [*.{cs,vb}] 22 | # Organize usings 23 | dotnet_sort_system_directives_first = true 24 | dotnet_separate_import_directive_groups = false 25 | 26 | # this. preferences 27 | dotnet_style_qualification_for_field = false:silent 28 | dotnet_style_qualification_for_property = false:silent 29 | dotnet_style_qualification_for_method = false:silent 30 | dotnet_style_qualification_for_event = false:silent 31 | 32 | # Language keywords vs BCL types preferences 33 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 34 | dotnet_style_predefined_type_for_member_access = true:silent 35 | 36 | # Parentheses preferences 37 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 39 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 40 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 41 | 42 | # Modifier preferences 43 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 44 | dotnet_style_readonly_field = true:suggestion 45 | 46 | # Expression-level preferences 47 | dotnet_style_object_initializer = true:suggestion 48 | dotnet_style_collection_initializer = true:suggestion 49 | dotnet_style_explicit_tuple_names = true:suggestion 50 | dotnet_style_null_propagation = true:suggestion 51 | dotnet_style_coalesce_expression = true:suggestion 52 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 53 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 54 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 55 | dotnet_style_prefer_auto_properties = true:silent 56 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 57 | dotnet_style_prefer_conditional_expression_over_return = true:silent 58 | 59 | ############################### 60 | # Naming Conventions # 61 | ############################### 62 | 63 | # Style Definitions 64 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 65 | 66 | # Use PascalCase for constant fields 67 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 69 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 70 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 71 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 72 | dotnet_naming_symbols.constant_fields.required_modifiers = const 73 | 74 | ############################### 75 | # C# Code Style Rules # 76 | ############################### 77 | 78 | [*.cs] 79 | # var preferences 80 | csharp_style_var_for_built_in_types = true:silent 81 | csharp_style_var_when_type_is_apparent = true:silent 82 | csharp_style_var_elsewhere = true:silent 83 | 84 | # Expression-bodied members 85 | csharp_style_expression_bodied_methods = false:silent 86 | csharp_style_expression_bodied_constructors = false:silent 87 | csharp_style_expression_bodied_operators = false:silent 88 | csharp_style_expression_bodied_properties = true:silent 89 | csharp_style_expression_bodied_indexers = true:silent 90 | csharp_style_expression_bodied_accessors = true:silent 91 | 92 | # Pattern-matching preferences 93 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 94 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 95 | 96 | # Null-checking preferences 97 | csharp_style_throw_expression = true:suggestion 98 | csharp_style_conditional_delegate_call = true:suggestion 99 | 100 | # Modifier preferences 101 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 102 | 103 | # Expression-level preferences 104 | csharp_prefer_braces = true:silent 105 | csharp_style_deconstructed_variable_declaration = true:suggestion 106 | csharp_prefer_simple_default_expression = true:suggestion 107 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 108 | csharp_style_inlined_variable_declaration = true:suggestion 109 | 110 | ############################### 111 | # C# Formatting Rules # 112 | ############################### 113 | 114 | # New line preferences 115 | csharp_new_line_before_open_brace = all 116 | csharp_new_line_before_else = true 117 | csharp_new_line_before_catch = true 118 | csharp_new_line_before_finally = true 119 | csharp_new_line_before_members_in_object_initializers = true 120 | csharp_new_line_before_members_in_anonymous_types = true 121 | csharp_new_line_between_query_expression_clauses = true 122 | 123 | # Indentation preferences 124 | csharp_indent_case_contents = true 125 | csharp_indent_switch_labels = true 126 | csharp_indent_labels = flush_left 127 | 128 | # Space preferences 129 | csharp_space_after_cast = false 130 | csharp_space_after_keywords_in_control_flow_statements = true 131 | csharp_space_between_method_call_parameter_list_parentheses = false 132 | csharp_space_between_method_declaration_parameter_list_parentheses = false 133 | csharp_space_between_parentheses = false 134 | csharp_space_before_colon_in_inheritance_clause = true 135 | csharp_space_after_colon_in_inheritance_clause = true 136 | csharp_space_around_binary_operators = before_and_after 137 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 138 | csharp_space_between_method_call_name_and_opening_parenthesis = false 139 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 140 | csharp_space_after_comma = true 141 | csharp_space_after_dot = false 142 | 143 | # Wrapping preferences 144 | csharp_preserve_single_line_statements = true 145 | csharp_preserve_single_line_blocks = true 146 | 147 | ################################## 148 | # Visual Basic Code Style Rules # 149 | ################################## 150 | 151 | [*.vb] 152 | # Modifier preferences 153 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | # StyleCop 57 | StyleCopReport.xml 58 | 59 | # Files built by Visual Studio 60 | *_i.c 61 | *_p.c 62 | *_h.h 63 | *.ilk 64 | *.meta 65 | *.obj 66 | *.iobj 67 | *.pch 68 | *.pdb 69 | *.ipdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.svclog 85 | *.scc 86 | 87 | # Chutzpah Test files 88 | _Chutzpah* 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opendb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | *.VC.db 99 | *.VC.VC.opendb 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | *.sap 106 | 107 | # Visual Studio Trace Files 108 | *.e2e 109 | 110 | # TFS 2012 Local Workspace 111 | $tf/ 112 | 113 | # Guidance Automation Toolkit 114 | *.gpState 115 | 116 | # ReSharper is a .NET coding add-in 117 | _ReSharper*/ 118 | *.[Rr]e[Ss]harper 119 | *.DotSettings.user 120 | 121 | # JustCode is a .NET coding add-in 122 | .JustCode 123 | 124 | # TeamCity is a build add-in 125 | _TeamCity* 126 | 127 | # DotCover is a Code Coverage Tool 128 | *.dotCover 129 | 130 | # AxoCover is a Code Coverage Tool 131 | .axoCover/* 132 | !.axoCover/settings.json 133 | 134 | # Visual Studio code coverage results 135 | *.coverage 136 | *.coveragexml 137 | 138 | # NCrunch 139 | _NCrunch_* 140 | .*crunch*.local.xml 141 | nCrunchTemp_* 142 | 143 | # MightyMoose 144 | *.mm.* 145 | AutoTest.Net/ 146 | 147 | # Web workbench (sass) 148 | .sass-cache/ 149 | 150 | # Installshield output folder 151 | [Ee]xpress/ 152 | 153 | # DocProject is a documentation generator add-in 154 | DocProject/buildhelp/ 155 | DocProject/Help/*.HxT 156 | DocProject/Help/*.HxC 157 | DocProject/Help/*.hhc 158 | DocProject/Help/*.hhk 159 | DocProject/Help/*.hhp 160 | DocProject/Help/Html2 161 | DocProject/Help/html 162 | 163 | # Click-Once directory 164 | publish/ 165 | 166 | # Publish Web Output 167 | *.[Pp]ublish.xml 168 | *.azurePubxml 169 | # Note: Comment the next line if you want to checkin your web deploy settings, 170 | # but database connection strings (with potential passwords) will be unencrypted 171 | *.pubxml 172 | *.publishproj 173 | 174 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 175 | # checkin your Azure Web App publish settings, but sensitive information contained 176 | # in these scripts will be unencrypted 177 | PublishScripts/ 178 | 179 | # NuGet Packages 180 | *.nupkg 181 | # The packages folder can be ignored because of Package Restore 182 | **/[Pp]ackages/* 183 | # except build/, which is used as an MSBuild target. 184 | !**/[Pp]ackages/build/ 185 | # Uncomment if necessary however generally it will be regenerated when needed 186 | #!**/[Pp]ackages/repositories.config 187 | # NuGet v3's project.json files produces more ignorable files 188 | *.nuget.props 189 | *.nuget.targets 190 | 191 | # Microsoft Azure Build Output 192 | csx/ 193 | *.build.csdef 194 | 195 | # Microsoft Azure Emulator 196 | ecf/ 197 | rcf/ 198 | 199 | # Windows Store app package directories and files 200 | AppPackages/ 201 | BundleArtifacts/ 202 | Package.StoreAssociation.xml 203 | _pkginfo.txt 204 | *.appx 205 | 206 | # Visual Studio cache files 207 | # files ending in .cache can be ignored 208 | *.[Cc]ache 209 | # but keep track of directories ending in .cache 210 | !*.[Cc]ache/ 211 | 212 | # Others 213 | ClientBin/ 214 | ~$* 215 | *~ 216 | *.dbmdl 217 | *.dbproj.schemaview 218 | *.jfm 219 | *.pfx 220 | *.publishsettings 221 | orleans.codegen.cs 222 | 223 | # Including strong name files can present a security risk 224 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 225 | #*.snk 226 | 227 | # Since there are multiple workflows, uncomment next line to ignore bower_components 228 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 229 | #bower_components/ 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | ServiceFabricBackup/ 242 | *.rptproj.bak 243 | 244 | # SQL Server files 245 | *.mdf 246 | *.ldf 247 | *.ndf 248 | 249 | # Business Intelligence projects 250 | *.rdl.data 251 | *.bim.layout 252 | *.bim_*.settings 253 | *.rptproj.rsuser 254 | 255 | # Microsoft Fakes 256 | FakesAssemblies/ 257 | 258 | # GhostDoc plugin setting file 259 | *.GhostDoc.xml 260 | 261 | # Node.js Tools for Visual Studio 262 | .ntvs_analysis.dat 263 | node_modules/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | 331 | # Local History for Visual Studio 332 | .localhistory/ 333 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/MessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using CM.Text.BusinessMessaging.Model; 4 | using CM.Text.BusinessMessaging.Model.MultiChannel; 5 | using CM.Text.Common; 6 | using JetBrains.Annotations; 7 | 8 | namespace CM.Text.BusinessMessaging 9 | { 10 | /// 11 | /// Builder class to construct messages 12 | /// 13 | [PublicAPI] 14 | public class MessageBuilder 15 | { 16 | private readonly Message _message; 17 | private RichContent _richContent; 18 | 19 | /// 20 | /// Creates a new MessageBuilder 21 | /// 22 | /// 23 | /// 24 | /// 25 | public MessageBuilder(string messageText, string from, params string[] to) 26 | { 27 | _message = new Message 28 | { 29 | Body = new Body 30 | { 31 | Content = messageText, 32 | Type = Constant.BusinessMessagingBodyTypeAuto 33 | }, 34 | Recipients = to.Select(toEntry => new Recipient { Number = toEntry }) 35 | .ToArray(), 36 | From = from, 37 | CustomGrouping3 = Constant.TextSdkReference 38 | }; 39 | } 40 | 41 | /// 42 | /// Constructs the message. 43 | /// 44 | /// 45 | public Message Build() 46 | { 47 | _message.RichContent = _richContent; 48 | return _message; 49 | } 50 | 51 | /// 52 | /// Adds the allowed channels field, which forces a message to only use certain routes. 53 | /// You can define a list of which channels you want your message to use. 54 | /// Not defining any channels will be interpreted as allowing all channels. 55 | /// 56 | /// 57 | /// Note that for channels other than SMS, CM needs to configure the out going flows. 58 | /// For those flows to work, we need to be contacted. 59 | /// 60 | public MessageBuilder WithAllowedChannels(params Channel[] channels) 61 | { 62 | _message.AllowedChannels = channels; 63 | return this; 64 | } 65 | 66 | /// 67 | /// Add a reference to the message. 68 | /// 69 | /// 70 | /// 71 | public MessageBuilder WithReference(string reference) 72 | { 73 | _message.Reference = reference; 74 | return this; 75 | } 76 | 77 | /// 78 | /// Add a validity period to the message. 79 | /// 80 | /// 81 | /// You can supply the time zone for the validity period using either of the following formats: 82 | /// 83 | /// Absolute date and time: 84 | /// 85 | /// 2017-04-20 11:50:05 GMT 86 | /// 2017-04-20 11:50:05+8 87 | /// 2017-04-20 11:55:05-07:00 88 | /// If no time zone was specified, CE(S)T will be used. (CM local time) 89 | /// 90 | /// Relative offset in hour(h) or minute(m) 91 | /// 92 | /// 2h 93 | /// 30m 94 | /// You can set the validity in either hours or minutes. A combination of both is not supported. 95 | /// 96 | /// 97 | /// 98 | public MessageBuilder WithValidityPeriod(string period) 99 | { 100 | _message.Validity = period; 101 | return this; 102 | } 103 | 104 | /// 105 | /// Adds a message that replaces the for channels that support 106 | /// rich content (all channels except , 107 | /// and at this moment) 108 | /// 109 | /// 110 | /// 111 | public MessageBuilder WithRichMessage(IRichMessage richMessage) 112 | { 113 | if (_richContent == null) 114 | _richContent = new RichContent(); 115 | 116 | _richContent.AddConversationPart(richMessage); 117 | return this; 118 | } 119 | 120 | /// 121 | /// Adds suggestions to the message. It is dependent on the channel that is used which 122 | /// suggestions are supported. 123 | /// 124 | /// 125 | /// 126 | public MessageBuilder WithSuggestions(params SuggestionBase[] suggestions) 127 | { 128 | if (_richContent == null) 129 | _richContent = new RichContent(); 130 | 131 | _richContent.Suggestions = suggestions; 132 | return this; 133 | } 134 | 135 | /// 136 | /// Used for Hybrid messaging, see https://developers.cm.com/messaging/docs for more information 137 | /// Messages will be sent over the channel. 138 | /// 139 | public MessageBuilder WitHybridAppKey(Guid appKey) 140 | { 141 | _message.HybridAppKey = appKey; 142 | return this; 143 | } 144 | 145 | /// 146 | /// Adds a WhatsApp template message that replaces the for WhatsApp 147 | /// please note that you need to have approved wa templates. 148 | /// 149 | /// 150 | /// 151 | public MessageBuilder WithTemplate(TemplateMessage template) 152 | { 153 | if (_richContent == null) 154 | _richContent = new RichContent(); 155 | 156 | _richContent.AddConversationPart(template); 157 | return this; 158 | } 159 | 160 | /// 161 | /// Adds a WhatsApp interactive message that replaces the for WhatsApp 162 | /// 163 | /// 164 | /// 165 | public MessageBuilder WithInteractive(WhatsAppInteractiveMessage interactive) 166 | { 167 | if (_richContent == null) 168 | _richContent = new RichContent(); 169 | 170 | _richContent.AddConversationPart(interactive); 171 | return this; 172 | } 173 | 174 | /// 175 | /// Adds a ApplePay request . 176 | /// 177 | /// 178 | /// 179 | public MessageBuilder WithApplePay(ApplePayRequest applePayRequest) 180 | { 181 | if (_richContent == null) 182 | _richContent = new RichContent(); 183 | 184 | _richContent.AddConversationPart(applePayRequest); 185 | return this; 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/ContactMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// Used to send one or multiple contacts, for now only supported in . 8 | /// 9 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 10 | /// 11 | [PublicAPI] 12 | public class ContactMessage : IRichMessage 13 | { 14 | /// 15 | /// The contacts to send. 16 | /// 17 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 18 | /// 19 | [JsonPropertyName("contacts")] 20 | public Contact[] Contacts { get; set; } 21 | 22 | /// 23 | /// Contextual properties of the message 24 | /// 25 | [JsonPropertyName("context")] 26 | [CanBeNull] 27 | public MessageContext MessageContext { get; set; } 28 | } 29 | 30 | /// 31 | /// Represents 1 contact. 32 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 33 | /// 34 | public class Contact 35 | { 36 | /// 37 | /// Full contact address(es) 38 | /// 39 | [JsonPropertyName("addresses")] 40 | public ContactAddress[] ContactAddresses { get; set; } 41 | 42 | /// 43 | /// YYYY-MM-DD formatted string of the birthday of the contact 44 | /// 45 | [JsonPropertyName("birthday")] 46 | public string Birthday { get; set; } 47 | 48 | /// 49 | /// Contact email address(es) 50 | /// 51 | [JsonPropertyName("emails")] 52 | public ContactEmail[] EmailAddresses { get; set; } 53 | 54 | /// 55 | /// Full contact name 56 | /// 57 | [JsonPropertyName("name")] 58 | public ContactName Name { get; set; } 59 | 60 | /// 61 | /// Contact organization information 62 | /// 63 | [JsonPropertyName("org")] 64 | public ContactOrganization Organization { get; set; } 65 | 66 | /// 67 | /// Contact phone number(s) 68 | /// 69 | [JsonPropertyName("phones")] 70 | public ContactPhoneNumber[] PhoneNumbers { get; set; } 71 | 72 | /// 73 | /// Contact URL(s) 74 | /// 75 | [JsonPropertyName("urls")] 76 | public ContactUrl[] Urls { get; set; } 77 | } 78 | 79 | /// 80 | /// One address of a contact 81 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 82 | /// 83 | public class ContactAddress 84 | { 85 | /// 86 | /// City name 87 | /// 88 | [JsonPropertyName("city")] 89 | public string City { get; set; } 90 | /// 91 | /// Full country name 92 | /// 93 | [JsonPropertyName("country")] 94 | public string Country { get; set; } 95 | /// 96 | /// Two-letter country abbreviation 97 | /// 98 | [JsonPropertyName("country_code")] 99 | public string CountryCode { get; set; } 100 | /// 101 | /// State abbreviation 102 | /// 103 | [JsonPropertyName("state")] 104 | public string State { get; set; } 105 | /// 106 | /// Street number and name 107 | /// 108 | [JsonPropertyName("street")] 109 | public string Street { get; set; } 110 | /// 111 | /// Standard Values: HOME, WORK 112 | /// 113 | [JsonPropertyName("type")] 114 | public string Type { get; set; } 115 | /// 116 | /// ZIP code 117 | /// 118 | [JsonPropertyName("zip")] 119 | public string ZipCode { get; set; } 120 | } 121 | 122 | /// 123 | /// One email address of a contact. 124 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 125 | /// 126 | public class ContactEmail 127 | { 128 | /// 129 | /// Email address 130 | /// 131 | [JsonPropertyName("email")] 132 | public string EmailAddress { get; set; } 133 | /// 134 | /// Standard Values: HOME, WORK 135 | /// 136 | [JsonPropertyName("type")] 137 | public string Type { get; set; } 138 | } 139 | 140 | /// 141 | /// The name of a contact. 142 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 143 | /// 144 | public class ContactName 145 | { 146 | /// 147 | /// First name 148 | /// 149 | [JsonPropertyName("first_name")] 150 | public string FirstName { get; set; } 151 | /// 152 | /// Last name 153 | /// 154 | [JsonPropertyName("last_name")] 155 | public string LastName { get; set; } 156 | /// 157 | /// Middle name 158 | /// 159 | [JsonPropertyName("middle_name")] 160 | public string MiddleName { get; set; } 161 | /// 162 | /// Name prefix 163 | /// 164 | [JsonPropertyName("name_prefix")] 165 | public string NamePrefix { get; set; } 166 | /// 167 | /// Name suffix 168 | /// 169 | [JsonPropertyName("name_suffix")] 170 | public string NameSuffix { get; set; } 171 | /// 172 | /// Full name as it normally appears 173 | /// 174 | [JsonPropertyName("formatted_name")] 175 | public string FormattedName { get; set; } 176 | } 177 | /// 178 | /// The organization of a contact 179 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 180 | /// 181 | public class ContactOrganization 182 | { 183 | /// 184 | /// Name of the contact's company 185 | /// 186 | [JsonPropertyName("company")] 187 | public string Company { get; set; } 188 | /// 189 | /// Name of the contact's department 190 | /// 191 | [JsonPropertyName("department")] 192 | public string Department { get; set; } 193 | /// 194 | /// Contact's business title 195 | /// 196 | [JsonPropertyName("title")] 197 | public string Title { get; set; } 198 | } 199 | 200 | /// 201 | /// Phone number of a contact 202 | /// 203 | public class ContactPhoneNumber 204 | { 205 | /// 206 | /// The phone number of the contact 207 | /// 208 | [JsonPropertyName("phone")] 209 | public string Phone { get; set; } 210 | /// 211 | /// Standard Values: CELL, MAIN, IPHONE, HOME, WORK 212 | /// 213 | [JsonPropertyName("type")] 214 | public string Type { get; set; } 215 | } 216 | 217 | /// 218 | /// An Url for a contact 219 | /// See also https://developers.facebook.com/docs/whatsapp/api/messages/others#contacts 220 | /// 221 | public class ContactUrl 222 | { 223 | /// 224 | /// URL 225 | /// 226 | [JsonPropertyName("url")] 227 | public string Url { get; set; } 228 | /// 229 | /// Standard Values: HOME, WORK 230 | /// 231 | [JsonPropertyName("type")] 232 | public string Type { get; set; } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /CM.Text/TextClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using CM.Text.BusinessMessaging; 10 | using CM.Text.BusinessMessaging.Model; 11 | using CM.Text.Common; 12 | using CM.Text.Identity; 13 | using CM.Text.Interfaces; 14 | using JetBrains.Annotations; 15 | 16 | namespace CM.Text 17 | { 18 | /// 19 | /// This class provides methods to send text messages. 20 | /// 21 | [PublicAPI] 22 | public class TextClient : ITextClient 23 | { 24 | private static readonly Lazy ClientSingletonLazy = new Lazy(); 25 | private readonly Guid _apiKey; 26 | [CanBeNull] private readonly Uri _endPointOverride; 27 | private readonly HttpClient _httpClient; 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// The API key. 33 | [PublicAPI] 34 | [SuppressMessage("ReSharper", "IntroduceOptionalParameters.Global", Justification = "Backwards compatibility")] 35 | public TextClient(Guid apiKey) : this(apiKey, null) 36 | { 37 | } 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | /// The API key. 43 | /// An optional HTTP client. 44 | [PublicAPI] 45 | [SuppressMessage("ReSharper", "IntroduceOptionalParameters.Global", Justification = "Binary backwards compatibility")] 46 | public TextClient(Guid apiKey, [CanBeNull] HttpClient httpClient): this(apiKey, httpClient, null) 47 | { 48 | } 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | /// The API key. 54 | /// An optional HTTP client. 55 | /// (Optional) The end point to use, instead of the default "https://gw.messaging.cm.com/v1.0/message". 56 | [PublicAPI] 57 | public TextClient(Guid apiKey, [CanBeNull] HttpClient httpClient, [CanBeNull] Uri endPointOverride) 58 | { 59 | _apiKey = apiKey; 60 | _httpClient = httpClient ?? ClientSingletonLazy.Value; 61 | _endPointOverride = endPointOverride; 62 | } 63 | 64 | /// 65 | [PublicAPI] 66 | public async Task SendMessageAsync( 67 | string messageText, 68 | string from, 69 | IEnumerable to, 70 | string reference, 71 | CancellationToken cancellationToken = default(CancellationToken)) 72 | { 73 | using (var request = new HttpRequestMessage( 74 | HttpMethod.Post, 75 | _endPointOverride ?? new Uri(Constant.BusinessMessagingGatewayJsonEndpoint) 76 | )) 77 | { 78 | request.Content = new StringContent( 79 | BusinessMessagingApi.GetHttpPostBody(_apiKey, messageText, from, to, reference), 80 | Encoding.UTF8, 81 | Constant.BusinessMessagingGatewayMediaTypeJson 82 | ); 83 | 84 | using (var requestResult = await _httpClient.SendAsync(request, cancellationToken) 85 | .ConfigureAwait(false)) 86 | { 87 | cancellationToken.ThrowIfCancellationRequested(); 88 | 89 | return BusinessMessagingApi.GetTextApiResult( 90 | await requestResult.Content.ReadAsStringAsync() 91 | .ConfigureAwait(false) 92 | ); 93 | } 94 | } 95 | } 96 | 97 | /// 98 | /// Sends a message asynchronously. 99 | /// 100 | /// The message to send. 101 | /// The cancellation token. 102 | /// 103 | [PublicAPI] 104 | public async Task SendMessageAsync( 105 | Message message, 106 | CancellationToken cancellationToken = default(CancellationToken)) 107 | { 108 | using (var request = new HttpRequestMessage( 109 | HttpMethod.Post, 110 | _endPointOverride ?? new Uri(Constant.BusinessMessagingGatewayJsonEndpoint) 111 | )) 112 | { 113 | request.Content = new StringContent( 114 | BusinessMessagingApi.GetHttpPostBody(_apiKey, message), 115 | Encoding.UTF8, 116 | Constant.BusinessMessagingGatewayMediaTypeJson 117 | ); 118 | 119 | using (var requestResult = await _httpClient.SendAsync(request, cancellationToken) 120 | .ConfigureAwait(false)) 121 | { 122 | cancellationToken.ThrowIfCancellationRequested(); 123 | 124 | return BusinessMessagingApi.GetTextApiResult( 125 | await requestResult.Content.ReadAsStringAsync() 126 | .ConfigureAwait(false) 127 | ); 128 | } 129 | } 130 | } 131 | 132 | /// 133 | /// Sends an One Time Password asynchronously. 134 | /// 135 | /// The otp to send. 136 | /// The cancellation token. 137 | /// 138 | [PublicAPI] 139 | public async Task SendOtpAsync( 140 | OtpRequest otpRequest, 141 | CancellationToken cancellationToken = default(CancellationToken)) 142 | { 143 | using (var request = new HttpRequestMessage( 144 | HttpMethod.Post, 145 | _endPointOverride ?? new Uri(Constant.OtpRequestEndpoint) 146 | )) 147 | { 148 | request.Content = new StringContent( 149 | JsonSerializer.Serialize(otpRequest), 150 | Encoding.UTF8, 151 | Constant.BusinessMessagingGatewayMediaTypeJson 152 | ); 153 | 154 | return await SendOtpApiRequestAsync(request, cancellationToken); 155 | } 156 | } 157 | 158 | /// 159 | /// Checks an One Time Password asynchronously. 160 | /// 161 | /// id of the OTP to check. 162 | /// The code the end user used 163 | /// The cancellation token. 164 | /// 165 | [PublicAPI] 166 | public async Task VerifyOtpAsync( 167 | string id, 168 | string code, 169 | CancellationToken cancellationToken = default(CancellationToken)) 170 | { 171 | using (var request = new HttpRequestMessage( 172 | HttpMethod.Post, 173 | _endPointOverride ?? new Uri(string.Format(Constant.OtpVerifyEndpointFormatter, id)) 174 | )) 175 | { 176 | request.Content = new StringContent( 177 | JsonSerializer.Serialize(new { code = code } ), 178 | Encoding.UTF8, 179 | Constant.BusinessMessagingGatewayMediaTypeJson 180 | ); 181 | 182 | return await SendOtpApiRequestAsync(request, cancellationToken); 183 | } 184 | } 185 | 186 | private async Task SendOtpApiRequestAsync(HttpRequestMessage request, 187 | CancellationToken cancellationToken) 188 | { 189 | request.Headers.Add("X-CM-ProductToken", _apiKey.ToString()); 190 | using (var requestResult = await _httpClient.SendAsync(request, cancellationToken) 191 | .ConfigureAwait(false)) 192 | { 193 | cancellationToken.ThrowIfCancellationRequested(); 194 | 195 | return JsonSerializer.Deserialize( 196 | await requestResult.Content.ReadAsStringAsync() 197 | .ConfigureAwait(false) 198 | ); 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using CM.Text.BusinessMessaging.Model.MultiChannel; 4 | using JetBrains.Annotations; 5 | 6 | namespace CM.Text.BusinessMessaging.Model 7 | { 8 | /// 9 | /// Represents one message. One message can be sent to multiple recipients. 10 | /// 11 | [PublicAPI] 12 | public class Message 13 | { 14 | /// 15 | /// The allowed channels field forces a message to only use certain routes. 16 | /// In this field you can define a list of which channels you want your message to use. 17 | /// Not defining any channels will be interpreted as allowing all channels. 18 | /// 19 | /// 20 | /// Note that for channels other than SMS, CM needs to configure the out going flows. 21 | /// For those flows to work, we need to be contacted. 22 | /// 23 | [JsonPropertyName("allowedChannels")] 24 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 25 | public Channel[] AllowedChannels { get; set; } 26 | 27 | /// 28 | /// Required: The actual text body of the message. 29 | /// 30 | [JsonPropertyName("body")] 31 | public Body Body { get; set; } 32 | 33 | /// 34 | /// The custom grouping field is an optional field that can be used to tag messages. 35 | /// These tags are be used by CM products, like the Transactions API. 36 | /// Applying custom grouping names to messages helps filter your messages. 37 | /// With up to three levels of custom grouping fields that can be set, subsets of messages can be 38 | /// further broken down. The custom grouping name can be up to 100 characters of your choosing. 39 | /// It’s recommended to limit the number of unique custom groupings to 1000. 40 | /// Please contact support in case you would like to exceed this number. 41 | /// 42 | [JsonPropertyName("customGrouping")] 43 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 44 | public string CustomGrouping { get; set; } 45 | 46 | /// 47 | /// The custom grouping2 field, like is an optional field that can be used to tag 48 | /// messages. 49 | /// These tags are be used by CM products, like the Transactions API. 50 | /// Applying custom grouping names to messages helps filter your messages. 51 | /// With up to three levels of custom grouping fields that can be set, subsets of messages can be 52 | /// further broken down. The custom grouping name can be up to 100 characters of your choosing. 53 | /// It’s recommended to limit the number of unique custom groupings to 1000. 54 | /// Please contact support in case you would like to exceed this number. 55 | /// 56 | [JsonPropertyName("customGrouping2")] 57 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 58 | public string CustomGrouping2 { get; set; } 59 | 60 | /// 61 | /// The custom grouping3 field, like and is an optional 62 | /// field that can be used to tag messages. 63 | /// These tags are be used by CM products, like the Transactions API. 64 | /// Applying custom grouping names to messages helps filter your messages. 65 | /// With up to three levels of custom grouping fields that can be set, subsets of messages can be 66 | /// further broken down. The custom grouping name can be up to 100 characters of your choosing. 67 | /// It’s recommended to limit the number of unique custom groupings to 1000. 68 | /// Please contact support in case you would like to exceed this number. 69 | /// 70 | /// Default value within this SDK is 71 | [JsonPropertyName("customGrouping3")] 72 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 73 | public string CustomGrouping3 { get; set; } 74 | 75 | /// 76 | /// Required: This is the sender name. 77 | /// The maximum length is 11 alphanumerical characters or 16 digits. Example: 'MyCompany' 78 | /// 79 | [JsonPropertyName("from")] 80 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 81 | public string From { get; set; } 82 | 83 | /// 84 | /// Used for Hybrid messaging, see https://developers.cm.com/messaging/docs for more information 85 | /// Messages will be sent over the channel. 86 | /// 87 | [JsonPropertyName("appKey")] 88 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 89 | public Guid? HybridAppKey { get; set; } 90 | 91 | /// 92 | /// Used when sending multipart or concatenated SMS messages and always used together with 93 | /// . 94 | /// Indicate the minimum and maximum of message parts that you allow the gateway to send for this 95 | /// message. 96 | /// Technically the gateway will first check if a message is larger than 160 characters, if so, the 97 | /// message will be cut into multiple 153 characters parts limited by these parameters. 98 | /// 99 | [JsonPropertyName("maximumNumberOfMessageParts")] 100 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 101 | public int? MaximumNumberOfMessageParts { get; set; } 102 | 103 | /// 104 | /// Used when sending multipart or concatenated SMS messages and always used together with 105 | /// . 106 | /// Indicate the minimum and maximum of message parts that you allow the gateway to send for this 107 | /// message. 108 | /// Technically the gateway will first check if a message is larger than 160 characters, if so, the 109 | /// message will be cut into multiple 153 characters parts limited by these parameters. 110 | /// 111 | [JsonPropertyName("minimumNumberOfMessageParts")] 112 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 113 | public int? MinimumNumberOfMessageParts { get; set; } 114 | 115 | /// 116 | /// Required: The destination mobile numbers. 117 | /// This value should be in international format. 118 | /// A single mobile number per request. Example: '00447911123456' 119 | /// 120 | [JsonPropertyName("to")] 121 | public Recipient[] Recipients { get; set; } 122 | 123 | /// 124 | /// Optional: For each message you send, you can set a reference. 125 | /// The given reference will be used in the status reports and MO replies for the message, 126 | /// so you can link the messages to the sent batch. 127 | /// For more information on status reports, see: 128 | /// https://developers.cm.com/messaging/docs/incoming-status-report 129 | /// The given reference must be between 1 - 32 alphanumeric characters, and will not work using demo accounts. 130 | /// 131 | [JsonPropertyName("reference")] 132 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 133 | public string Reference { get; set; } 134 | 135 | /// 136 | /// Can be used by channels that support rich content (all channels except , 137 | /// and at this moment) 138 | /// 139 | [JsonPropertyName("richContent")] 140 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 141 | public RichContent RichContent { get; set; } 142 | 143 | /// 144 | /// Optional: For each message you send, you can set a validity. 145 | /// Specify a time at which a delayed message can be considered irrelevant, you can supply an absolute date & time 146 | /// or a relative offset. A message is considered failed if it was not successfully delivered before that time. 147 | /// And via a Status Report we inform you this was the case. 148 | /// For more information on status reports, see: 149 | /// https://developers.cm.com/messaging/docs/incoming-status-report 150 | /// You can supply the time zone for the validity period using either of the following formats: 151 | /// 152 | /// Absolute date and time: 153 | /// 154 | /// 2017-04-20 11:50:05 GMT 155 | /// 2017-04-20 11:50:05+8 156 | /// 2017-04-20 11:55:05-07:00 157 | /// If no time zone was specified, CE(S)T will be used. (CM local time) 158 | /// 159 | /// Relative offset in hour(H) or minute(M) 160 | /// 161 | /// 2h 162 | /// 30m 163 | /// You can set the validity in either hours or minutes. A combination of both is not supported. 164 | /// 165 | [JsonPropertyName("validity")] 166 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 167 | public string Validity { get; set; } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/WhatsAppTemplate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using JetBrains.Annotations; 4 | 5 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 6 | { 7 | /// 8 | /// WhatsApp template, see https://developers.facebook.com/docs/whatsapp/api/messages/message-templates for more information 9 | /// Used only in . 10 | /// 11 | /// Templates need to be configured by CM and approved by WhatsApp before it is possible 12 | /// to send these messages. 13 | /// 14 | [PublicAPI] 15 | public class WhatsappTemplate 16 | { 17 | /// 18 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 19 | /// The namespace that will be used 20 | /// 21 | [JsonPropertyName("namespace")] 22 | public string Namespace { get; set; } 23 | 24 | /// 25 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 26 | /// The element name that indicates which template to use within the namespace 27 | /// 28 | [JsonPropertyName("element_name")] 29 | public string Name { get; set; } 30 | 31 | /// 32 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 33 | /// Allows for the specification of a deterministic or fallback language. 34 | /// 35 | /// The language parameter sets the language policy for an Message Template; 36 | /// you can set it to either fallback or deterministic. 37 | /// 38 | [JsonPropertyName("language")] 39 | public Language Language { get; set; } 40 | 41 | 42 | /// 43 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 44 | /// This field is an array of values to apply to variables in the template 45 | /// 46 | public TemplateComponents[] Components { get; set; } 47 | } 48 | 49 | /// 50 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 51 | /// This field is an array of values to apply to variables in the template 52 | /// 53 | [PublicAPI] 54 | public class LocalizableParam 55 | { 56 | /// 57 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 58 | /// 59 | /// Default text if localization fails 60 | /// 61 | [JsonPropertyName("default")] 62 | public string Default { get; set; } 63 | 64 | /// 65 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 66 | /// 67 | /// If the currency object is used, it contains required parameters currency_code and amount_1000. 68 | /// 69 | [JsonPropertyName("currency")] 70 | public TemplateCurrency Currency { get; set; } 71 | 72 | /// 73 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 74 | /// 75 | /// If the date_time object is used, further definition of the date and time is required. 76 | /// 77 | [JsonPropertyName("date_time")] 78 | public TemplateDateTime DateTime { get; set; } 79 | 80 | } 81 | 82 | /// 83 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 84 | /// 85 | /// Object to make it possible to use a localized currency format. 86 | /// If the currency object is used, it contains required parameters currency_code and amount_1000. 87 | /// 88 | [PublicAPI] 89 | public class TemplateCurrency 90 | { 91 | /// 92 | /// The Fallback amount 93 | /// 94 | [JsonPropertyName("fallback_value")] 95 | public string FallbackValue { get; set; } 96 | 97 | /// 98 | /// Currency code, for example USD or EUR 99 | /// 100 | [JsonPropertyName("code")] 101 | public string CurrencyCode { get; set; } 102 | 103 | /// 104 | /// Amount in currency_code times 1000 105 | /// 106 | /// 50110 EUR becomes €50.11 in the message 107 | [JsonPropertyName("amount_1000")] 108 | public long Amount { get; set; } 109 | } 110 | 111 | /// 112 | /// Used to localize a date/time in a message based on the end-users settings. 113 | /// 114 | public class TemplateDateTime 115 | { 116 | /// 117 | /// The fallback date in UTC format 118 | /// 119 | /// There will be no checking whether this is correct, 120 | [JsonPropertyName("fallback_value")] 121 | public string FallbackValue { get; } 122 | 123 | /// 124 | /// The day of the week as specified in Facebook documentation 125 | /// Options: "MONDAY", 1, "TUESDAY", 2, "WEDNESDAY", 3, "THURSDAY", 4, "FRIDAY", 5, "SATURDAY", 6, "SUNDAY", 7 126 | /// see https://developers.facebook.com/docs/whatsapp/message-templates/localization 127 | /// 128 | /// 129 | /// There will be no checking whether this is correct, 130 | [JsonPropertyName("day_of_week")] 131 | public int DayOfWeek { get; } 132 | /// 133 | /// The day of the month. 134 | /// 135 | [JsonPropertyName("day_of_month")] 136 | public int DayOfMonth { get; } 137 | /// 138 | /// The year. 139 | /// 140 | [JsonPropertyName("year")] 141 | public int Year { get; } 142 | /// 143 | /// The month. 144 | /// 145 | [JsonPropertyName("month")] 146 | public int Month { get; } 147 | /// 148 | /// The hour (24 hour notation) 149 | /// 150 | [JsonPropertyName("hour")] 151 | public int Hour { get; } 152 | /// 153 | /// The minute of the hour. 154 | /// 155 | [JsonPropertyName("minute")] 156 | public int Minute { get; } 157 | 158 | /// 159 | /// Constructor initializing the component. 160 | /// 161 | /// 162 | public TemplateDateTime(DateTime moment) 163 | { 164 | FallbackValue = moment.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'"); 165 | Minute = moment.Minute; 166 | Hour = moment.Hour; 167 | Month = moment.Month; 168 | Year = moment.Year; 169 | DayOfMonth = moment.Day; 170 | DayOfWeek = moment.DayOfWeek == System.DayOfWeek.Friday ? 7 : (int)moment.DayOfWeek; 171 | } 172 | } 173 | 174 | 175 | /// 176 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 177 | /// The language parameter sets the language policy for an Message Template; 178 | /// you can set it to either fallback or deterministic. 179 | /// 180 | public class Language 181 | { 182 | /// 183 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 184 | /// The code of the language or locale to use — Accepts both language and language_locale formats (e.g., en and en_US). 185 | /// 186 | [JsonPropertyName("code")] 187 | public string Code { get; set; } 188 | 189 | /// 190 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 191 | /// Options: fallback, deterministic 192 | /// The language policy the message should follow 193 | /// 194 | [JsonPropertyName("policy")] 195 | public string Policy { get; set; } 196 | } 197 | /// 198 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 199 | /// Dynamic content of the message. Separated in in different sections. 200 | /// 201 | public class TemplateComponents 202 | { 203 | /// 204 | /// Required. describes the component type. Possible values: header, content, footer. 205 | /// 206 | [JsonPropertyName("type")] 207 | public string Type { get; set; } 208 | 209 | /// 210 | /// Can be empty. describes the Type of button being created. Possible values: quick_reply, url. 211 | /// 212 | [JsonPropertyName("sub_type")] 213 | public string SubType { get; set; } 214 | /// 215 | /// Can be empty. Position index of the button. 216 | /// You can have up to 3 buttons using index values of 0-2. 217 | /// 218 | [JsonPropertyName("index")] 219 | public int Index { get; set; } 220 | 221 | /// 222 | /// Can be empty. Array containing the dynamic content of the message. 223 | /// 224 | [JsonPropertyName("parameters")] 225 | public ComponentParameters[] ComponentParameters { get; set; } 226 | } 227 | /// 228 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 229 | /// Dynamic content of a media template message. 230 | /// 231 | public class ComponentParameters 232 | { 233 | /// 234 | /// Describes the parameter type. Possible values: text, currency, date_time, image, document. 235 | /// 236 | [JsonPropertyName("type")] 237 | public string Type { get; set; } 238 | 239 | /// 240 | /// Only to be filled in when `type` = `text`. 241 | /// 242 | [JsonPropertyName("text")] 243 | public string Text { get; set; } 244 | /// 245 | /// Only to be filled in when `type` = `document` or `image`. 246 | /// 247 | [JsonPropertyName("media")] 248 | public MediaContent Media { get; set; } 249 | 250 | /// 251 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 252 | /// 253 | /// Default text if localization fails 254 | /// 255 | [JsonPropertyName("default")] 256 | public string Default { get; set; } 257 | 258 | /// 259 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 260 | /// 261 | /// If the currency object is used, it contains required parameters currency_code and amount_1000. 262 | /// 263 | [JsonPropertyName("currency")] 264 | public TemplateCurrency Currency { get; set; } 265 | 266 | /// 267 | /// Source: https://developers.facebook.com/docs/whatsapp/api/messages/message-templates 268 | /// 269 | /// If the date_time object is used, further definition of the date and time is required. 270 | /// 271 | [JsonPropertyName("date_time")] 272 | public TemplateDateTime DateTime { get; set; } 273 | 274 | /// 275 | /// Developer-defined payload that will be returned when the button is clicked in addition to the display text on the button 276 | /// Required for quick_reply buttons 277 | /// 278 | [JsonPropertyName("payload")] 279 | public string Payload { get; set; } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /CM.Text/BusinessMessaging/Model/MultiChannel/WhatsAppInteractiveMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using JetBrains.Annotations; 3 | 4 | namespace CM.Text.BusinessMessaging.Model.MultiChannel 5 | { 6 | /// 7 | /// WhatsApp interactive messages, see https://developers.facebook.com/docs/whatsapp/guides/interactive-messages for more information 8 | /// Used only in . 9 | /// 10 | [PublicAPI] 11 | public class WhatsAppInteractiveMessage : IRichMessage 12 | { 13 | 14 | /// 15 | /// Gets or sets the content of the WhatsApp interactive message 16 | /// 17 | [JsonPropertyName("interactive")] 18 | public WhatsAppInteractiveContent whatsAppInteractiveContent { get; set; } 19 | 20 | /// 21 | /// Contextual properties of the message 22 | /// 23 | [JsonPropertyName("context")] 24 | [CanBeNull] 25 | public MessageContext MessageContext { get; set; } 26 | } 27 | 28 | /// 29 | /// WhatsApp interactive content. 30 | /// Used only in . 31 | /// 32 | 33 | [PublicAPI] 34 | public class WhatsAppInteractiveContent 35 | { 36 | /// 37 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 38 | /// The Type that will be used, 39 | /// either list or button 40 | /// 41 | [JsonPropertyName("type")] 42 | public string Type { get; set; } 43 | /// 44 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 45 | /// Your message’s header. 46 | /// 47 | [JsonPropertyName("header")] 48 | public InteractiveHeader Header { get; set; } 49 | /// 50 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 51 | /// Required Your message’s body. 52 | /// 53 | [JsonPropertyName("body")] 54 | public InteractiveBody Body { get; set; } 55 | /// 56 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 57 | /// Required Your message’s footer. 58 | /// 59 | [JsonPropertyName("footer")] 60 | public InteractiveFooter Footer { get; set; } 61 | /// 62 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 63 | ///Required. Inside action, you must nest: 64 | ///a button field with your button’s content, and 65 | ///at least one section object (maximum of 10). 66 | /// 67 | [JsonPropertyName("action")] 68 | public InteractiveAction Action { get; set; } 69 | } 70 | 71 | /// 72 | /// WhatsApp interactive header. 73 | /// Used only in . 74 | /// 75 | public class InteractiveHeader 76 | { 77 | /// 78 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 79 | /// Required. The header type you would like to use.Supported values are: 80 | ///text: Used for List Messages and Reply Buttons. 81 | /// 82 | [JsonPropertyName("type")] 83 | public string Type { get; set; } 84 | /// 85 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 86 | /// Required if type is set to text. 87 | /// Text for the header.Formatting allows emojis, but not markdown. 88 | /// 89 | [JsonPropertyName("text")] 90 | public string Text { get; set; } 91 | 92 | /// 93 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 94 | /// Gets or sets the media. 95 | /// 96 | [JsonPropertyName("media")] 97 | public MediaContent Media { get; set; } 98 | } 99 | 100 | /// 101 | /// WhatsApp interactive body. 102 | /// Used only in . 103 | /// 104 | public class InteractiveBody 105 | { 106 | /// 107 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 108 | /// Required 109 | /// The body content of the message. Emojis and markdown are supported. Links are supported. 110 | /// Maximum length: 1024 characters. 111 | /// 112 | [JsonPropertyName("text")] 113 | public string Text { get; set; } 114 | } 115 | 116 | /// 117 | /// WhatsApp interactive footer. 118 | /// Used only in . 119 | /// 120 | public class InteractiveFooter 121 | { 122 | /// 123 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 124 | /// Required if the footer object is present. 125 | /// The footer content. Emojis and markdown are supported. Links are supported. 126 | /// Maximum length: 60 characters 127 | /// 128 | [JsonPropertyName("text")] 129 | public string Text { get; set; } 130 | } 131 | 132 | /// 133 | /// WhatsApp interactive action. 134 | /// Used only in . 135 | /// 136 | public class InteractiveAction 137 | { 138 | /// 139 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 140 | /// Required for List Messages. 141 | /// Button content. It cannot be an empty string and must be unique within the message 142 | /// Does not allow emojis or markdown. 143 | /// 144 | [JsonPropertyName("button")] 145 | public string Button { get; set; } 146 | /// 147 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 148 | /// Required for Reply Button Messages. 149 | /// 150 | [JsonPropertyName("buttons")] 151 | public InteractiveButton[] Buttons { get; set; } 152 | /// 153 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 154 | /// Required for List Messages. 155 | /// 156 | [JsonPropertyName("sections")] 157 | public InteractiveSection[] Sections { get; set; } 158 | /// 159 | /// Source: https://developers.facebook.com/docs/whatsapp/on-premises/reference/messages#action-object 160 | /// Product’s unique identifier 161 | /// Required for Single-Product Messages and Multi-Product Messages. 162 | /// 163 | [JsonPropertyName("product_retailer_id")] 164 | public string ProductRetailerId { get; set; } 165 | /// 166 | /// Source: https://developers.facebook.com/docs/whatsapp/on-premises/reference/messages#action-object 167 | /// Unique identifier of the Facebook catalog linked to your WhatsApp Business 168 | /// Required for Single-Product Messages and Multi-Product Messages. 169 | /// 170 | [JsonPropertyName("catalog_id")] 171 | public string CatalogId { get; set; } 172 | } 173 | 174 | /// 175 | /// WhatsApp interactive button. 176 | /// Used only in . 177 | /// 178 | public class InteractiveButton 179 | { 180 | /// 181 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 182 | /// type: only supported type is reply (for Reply Button Messages). 183 | /// 184 | [JsonPropertyName("type")] 185 | public string Type { get; set; } 186 | /// 187 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 188 | /// Button title.It cannot be an empty string and must be unique within the message. 189 | /// Does not allow emojis or markdown. Maximum length: 20 characters. 190 | /// 191 | [JsonPropertyName("title")] 192 | public string Title { get; set; } 193 | /// 194 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 195 | /// id: Unique identifier for your button. This ID is returned in the webhook when the button is clicked by the user. 196 | /// Maximum length: 256 characters. 197 | /// 198 | [JsonPropertyName("id")] 199 | public string Id { get; set; } 200 | /// 201 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 202 | /// Reply Message for your button. 203 | /// 204 | [JsonPropertyName("reply")] 205 | public ReplyMessage Reply { get; set; } 206 | } 207 | 208 | /// 209 | /// WhatsApp reply message. 210 | /// Used only in . 211 | /// 212 | public class ReplyMessage 213 | { 214 | /// 215 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 216 | /// id: Unique identifier for your reply message. 217 | /// 218 | [JsonPropertyName("id")] 219 | public string Id { get; set; } 220 | /// 221 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 222 | /// title: title for your reply button. 223 | /// 224 | [JsonPropertyName("title")] 225 | public string Title { get; set; } 226 | } 227 | 228 | /// 229 | /// WhatsApp interactive section. 230 | /// Used only in . 231 | /// 232 | public class InteractiveSection 233 | { 234 | /// 235 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 236 | /// Title of the section. 237 | /// Maximum length: 24 characters. 238 | /// 239 | [JsonPropertyName("title")] 240 | public string Title { get; set; } 241 | /// 242 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 243 | /// Contains a list of rows. 244 | /// 245 | [JsonPropertyName("rows")] 246 | public Rows[] Rows { get; set; } 247 | /// 248 | /// Source: https://developers.facebook.com/docs/whatsapp/on-premises/reference/messages#action-object 249 | /// Array of product objects. There is a minimum of 1 product per section and a maximum of 30 products across all sections. 250 | /// Required for Multi-Product Messages. 251 | /// 252 | [JsonPropertyName("product_items")] 253 | public WhatsAppProduct[] ProductItems { get; set; } 254 | } 255 | 256 | /// 257 | /// WhatsApp interactive section row. 258 | /// Used only in . 259 | /// 260 | public class Rows 261 | { 262 | /// 263 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 264 | /// Title of the row. 265 | /// 266 | [JsonPropertyName("title")] 267 | public string Title { get; set; } 268 | /// 269 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 270 | /// Id of the row. 271 | /// 272 | [JsonPropertyName("id")] 273 | public string Id { get; set; } 274 | /// 275 | /// Source: https://developers.facebook.com/docs/whatsapp/guides/interactive-messages 276 | /// Description of the row. 277 | /// 278 | [JsonPropertyName("description")] 279 | public string Description { get; set; } 280 | } 281 | 282 | /// 283 | /// https://developers.facebook.com/docs/whatsapp/on-premises/reference/messages#action-object 284 | /// Required for Multi-Product Messages. 285 | /// 286 | public class WhatsAppProduct 287 | { 288 | /// 289 | /// Unique identifier of the product in a catalog 290 | /// 291 | [JsonPropertyName("product_retailer_id")] 292 | public string ProductRetailerId { get; set; } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![.NET build & tests](https://github.com/cmdotcom/text-sdk-dotnet/actions/workflows/main.yml/badge.svg)](https://github.com/cmdotcom/text-sdk-dotnet/actions/workflows/main.yml) 2 | [![NuGetV](https://img.shields.io/nuget/v/CM.Text.svg "NuGet Version")](https://www.nuget.org/packages/CM.Text) 3 | [![NuGetDownloads](https://img.shields.io/nuget/dt/CM.Text.svg "NuGet downloads")](https://www.nuget.org/packages/CM.Text) 4 | 5 | # CM Text SDK 6 | A software development kit to provide ways to interact with CM.com's Text service. API's used: 7 | - [Messaging](https://developers.cm.com/messaging/docs) 8 | 9 | # Usage 10 | 11 | ## Instantiate the client 12 | Using your unique `ApiKey` (or product token) which authorizes you on the CM platform. 13 | Always keep this key secret! 14 | 15 | The product token can be found in the [Channels](https://www.cm.com/app/channels) application on the platform, under the `Gateway` section. 16 | 17 | ```cs 18 | var client = new TextClient(new Guid(ConfigurationManager.AppSettings["ApiKey"])); 19 | ``` 20 | 21 | ## Send a message 22 | By calling `SendMessageAsync` and providing message text, sender name, recipient phone number(s) and a reference (optional). 23 | 24 | ```cs 25 | var result = await client.SendMessageAsync("Message_Text", "Sender_Name", new List { "Recipient_PhoneNumber" }, "Your_Reference").ConfigureAwait(false); 26 | ``` 27 | 28 | ## Get the result 29 | `SendMessageAsync` returns an object of type `TextClientResult`, example: 30 | 31 | ```cs 32 | { 33 | "statusMessage": "Created 1 message(s)", 34 | "statusCode": 201, 35 | "details": [ 36 | { 37 | "reference": "Example_Reference", 38 | "status": "Accepted", 39 | "to": "Example_PhoneNumber", 40 | "parts": 1, 41 | "details": null 42 | }, 43 | { 44 | "reference": "Example_Reference2", 45 | "status": "Rejected", 46 | "to": "Example_PhoneNumber2", 47 | "parts": 0, 48 | "details": "A body without content was found" 49 | } 50 | ] 51 | } 52 | ``` 53 | 54 | ## Sending a rich message 55 | By using the `MessageBuilder` it is possible to create images with media for channels such as WhatsApp and RCS 56 | ```cs 57 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 58 | var client = new TextClient(apiKey); 59 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 60 | builder 61 | .WithAllowedChannels(Channel.WhatsApp) 62 | .WithRichMessage( 63 | new MediaMessage( 64 | "cm.com", 65 | "https://avatars3.githubusercontent.com/u/8234794?s=200&v=4", 66 | "image/png" 67 | ) 68 | ); 69 | var message = builder.Build(); 70 | var result = await client.SendMessageAsync(message); 71 | ``` 72 | 73 | ## Status codes 74 | For all possible status codes, please reference the `TextClientStatusCode` enum. 75 | 76 | 77 | ## Sending a WhatsApp template message 78 | By using the `MessageBuilder` it is possible to create template messages. Please note that this is WhatsApp only and your template needs to be approved before sending. 79 | For more info please check our documentation: https://www.cm.com/en-en/app/docs/api/business-messaging-api/1.0/index#whatsapp-template-message 80 | ```cs 81 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 82 | var client = new TextClient(apiKey); 83 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 84 | builder 85 | .WithAllowedChannels(Channel.WhatsApp) 86 | .WithTemplate(new TemplateMessage() { 87 | Content = new TemplateMessageContent() { 88 | Whatsapp = new WhatsappTemplate() { 89 | Name = "template-name", 90 | Namespace = "the-namespace-of-template", 91 | Language = new Language() { 92 | Code = "en", 93 | Policy = "deterministic" 94 | }, 95 | Components = new TemplateComponents[] { 96 | new TemplateComponents() { 97 | Type = "body", 98 | ComponentParameters = new ComponentParameters[] { 99 | new ComponentParameters() { 100 | Type = "text", 101 | Text = "firstname" 102 | } 103 | } 104 | }, 105 | } 106 | } 107 | } 108 | }); 109 | 110 | var message = builder.Build(); 111 | var result = await client.SendMessageAsync(message); 112 | ``` 113 | ## Sending a rich WhatsApp template message 114 | It is also possible to send a rich template with an image! 115 | 116 | ```cs 117 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 118 | var client = new TextClient(apiKey); 119 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 120 | builder 121 | .WithAllowedChannels(Channel.WhatsApp) 122 | .WithTemplate(new TemplateMessage() { 123 | Content = new TemplateMessageContent() { 124 | Whatsapp = new WhatsappTemplate() { 125 | Name = "template-name", 126 | Namespace = "the-namespace-of-template", 127 | Language = new Language() { 128 | Code = "en", 129 | Policy = "deterministic" 130 | }, 131 | Components = new TemplateComponents[] { 132 | new TemplateComponents() { 133 | Type = "header", 134 | ComponentParameters = new ComponentParameters[] { 135 | new ComponentParameters() { 136 | Type = "image", 137 | Media = new MediaContent() { 138 | MediaName = "cm.com", 139 | MediaUri = "https://avatars3.githubusercontent.com/u/8234794?s=200&v=4" 140 | } 141 | } 142 | } 143 | }, 144 | new TemplateComponents() { 145 | Type = "body", 146 | ComponentParameters = new ComponentParameters[] { 147 | new ComponentParameters() { 148 | Type = "text", 149 | Text = "firstname" 150 | } 151 | } 152 | }, 153 | } 154 | } 155 | } 156 | }); 157 | 158 | var message = builder.Build(); 159 | var result = await client.SendMessageAsync(message); 160 | ``` 161 | ## Sending a WhatsApp template message with date and Currency 162 | It is also possible to send a rich template with an currency and an date! 163 | please note that the timezone is in UTC format 164 | 165 | ```cs 166 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 167 | var client = new TextClient(apiKey); 168 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 169 | builder 170 | .WithAllowedChannels(Channel.WhatsApp) 171 | .WithTemplate(new TemplateMessage() { 172 | Content = new TemplateMessageContent() { 173 | Whatsapp = new WhatsappTemplate() { 174 | Name = "template-name", 175 | Namespace = "the-namespace-of-template", 176 | Language = new Language() 177 | { 178 | Code = "en", 179 | Policy = "deterministic" 180 | }, 181 | Components = new TemplateComponents[] { 182 | new TemplateComponents() { 183 | Type = "header", 184 | ComponentParameters = new ComponentParameters[] { 185 | new ComponentParameters() { 186 | Type = "image", 187 | Media = new MediaContent() { 188 | MediaName = "cm.com", 189 | MediaUri = "https://avatars3.githubusercontent.com/u/8234794?s=200&v=4" 190 | }, 191 | } 192 | } 193 | }, 194 | new TemplateComponents() 195 | { 196 | Type = "body", 197 | ComponentParameters = new ComponentParameters[] 198 | { 199 | new ComponentParameters() 200 | { 201 | Type = "currency", 202 | Currency = new TemplateCurrency() 203 | { 204 | FallbackValue = "$100.99", 205 | Amount = 100990, 206 | CurrencyCode = "USD" 207 | } 208 | }, 209 | new ComponentParameters() 210 | { 211 | Type = "date_time", 212 | DateTime = new TemplateDateTime(DateTime.Now) 213 | } 214 | }} 215 | } 216 | } 217 | } 218 | }); 219 | 220 | var message = builder.Build(); 221 | var result = await client.SendMessageAsync(message); 222 | ``` 223 | ## Sending interactive template messages 224 | Interactive templates allows you to send templates that include buttons. 225 | For more info please visit https://www.cm.com/app/docs/en/api/business-messaging-api/1.0/index#/whatsapp-template-message 226 | ```cs 227 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 228 | var client = new TextClient(apiKey); 229 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 230 | builder.WithAllowedChannels(Channel.WhatsApp).WithTemplate(new TemplateMessage() { 231 | Content = new TemplateMessageContent() { 232 | Whatsapp = new WhatsappTemplate() { 233 | Name = "Template name", 234 | Namespace = "whatsapp template id", 235 | Language = new Language() { 236 | Code = "en", 237 | Policy = "deterministic" 238 | }, 239 | Components = new TemplateComponents[] { 240 | new TemplateComponents() { 241 | Type = "body", 242 | ComponentParameters = new ComponentParameters[] { 243 | new ComponentParameters() { 244 | Type = "text", 245 | Text = "your message here" 246 | } 247 | } 248 | }, 249 | new TemplateComponents() { 250 | Type = "button", 251 | SubType = "quick_reply", 252 | Index = 0, 253 | ComponentParameters = new ComponentParameters[] { 254 | new ComponentParameters() { 255 | Type = "payload", 256 | Payload = "developer defined payload" 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | }); 264 | 265 | var message = builder.Build(); 266 | var result = await client.SendMessageAsync(message); 267 | ``` 268 | ## Sending an Apple Pay Request 269 | It is now possible to send an apple pay request only possible in Apple Business Chat 270 | 271 | ```cs 272 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 273 | var client = new TextClient(apiKey); 274 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 275 | builder 276 | .WithAllowedChannels(Channel.iMessage) 277 | .WithApplePay(new ApplePayRequest() 278 | { 279 | ApplePayConfiguration = new ApplePayConfiguration() 280 | { 281 | Total = 1, 282 | RecipientCountryCode = "recipient-country-code", 283 | CurrencyCode = "currency-code", 284 | Description = "product-description", 285 | RecipientEmail = "recipient-email", 286 | languageCountryCode = "language-country-code", 287 | OrderReference = "unique-order-guid", 288 | MerchantName = "merchant-name", 289 | LineItems = new LineItem[] 290 | { 291 | new LineItem() 292 | { 293 | Amount = 1, 294 | Label = "product-name", 295 | Type = "final-or-pending" 296 | }, 297 | } 298 | } 299 | }); 300 | 301 | var message = builder.Build(); 302 | var result = await client.SendMessageAsync(message); 303 | ``` 304 | ## Sending WhatsApp interactive messages 305 | It is now possible to send list messages and reply buttons without using templates 306 | only supported in WhatsApp 307 | 308 | ```cs 309 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 310 | var client = new TextClient(apiKey); 311 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 312 | builder.WithAllowedChannels(Channel.WhatsApp).WithInteractive(new CM.Text.BusinessMessaging.Model.MultiChannel.WhatsAppInteractiveMessage() 313 | { 314 | whatsAppInteractiveContent = new CM.Text.BusinessMessaging.Model.MultiChannel.WhatsAppInteractiveContent() 315 | { 316 | Type = "list", 317 | Header = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveHeader() 318 | { 319 | Type = "text", 320 | Text = "List message example" 321 | }, 322 | Body = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveBody() 323 | { 324 | Text = "checkout our list message demo" 325 | }, 326 | Action = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveAction() 327 | { 328 | Button = "button text", 329 | Sections = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveSection[] 330 | { 331 | new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveSection() 332 | { 333 | Title = "Select an option", 334 | Rows = new CM.Text.BusinessMessaging.Model.MultiChannel.Rows[] 335 | { 336 | new CM.Text.BusinessMessaging.Model.MultiChannel.Rows() 337 | { 338 | Id = "unique Id", 339 | Title = "unique title1", 340 | Description = "description text" 341 | }, 342 | new CM.Text.BusinessMessaging.Model.MultiChannel.Rows() 343 | { 344 | Id = "unique Id2", 345 | Title = "unique title2", 346 | Description = "description text" 347 | }, 348 | } 349 | } 350 | } 351 | }, 352 | Footer = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveFooter() 353 | { 354 | Text = "footer text" 355 | } 356 | } 357 | }); 358 | 359 | var message = builder.Build(); 360 | var result = await client.SendMessageAsync(message); 361 | ``` 362 | 363 | Only with Reply buttons you can send media like image,video or document 364 | see following example. 365 | 366 | ```cs 367 | var apiKey = new Guid(ConfigurationManager.AppSettings["ApiKey"]); 368 | var client = new TextClient(apiKey); 369 | var builder = new MessageBuilder("Message Text", "Sender_name", "Recipient_PhoneNumber"); 370 | builder.WithAllowedChannels(Channel.WhatsApp).WithInteractive(new CM.Text.BusinessMessaging.Model.MultiChannel.WhatsAppInteractiveMessage() 371 | { 372 | whatsAppInteractiveContent = new CM.Text.BusinessMessaging.Model.MultiChannel.WhatsAppInteractiveContent() 373 | { 374 | Type = "button", 375 | Header = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveHeader() 376 | { 377 | Type = "image", 378 | Media = new CM.Text.BusinessMessaging.Model.MultiChannel.MediaContent() 379 | { 380 | MediaUri = "https://www.cm.com/cdn/web/blog/content/logo-cmcom.png" 381 | } 382 | 383 | }, 384 | Body = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveBody() 385 | { 386 | Text = "checkout our reply message demo" 387 | }, 388 | Action = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveAction() 389 | { 390 | Buttons = new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveButton[] 391 | { 392 | new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveButton() 393 | { 394 | Type = "reply", 395 | Reply = new CM.Text.BusinessMessaging.Model.MultiChannel.ReplyMessage() 396 | { 397 | Id = "unique-postback-id1", 398 | Title = "First Button" 399 | } 400 | }, 401 | new CM.Text.BusinessMessaging.Model.MultiChannel.InteractiveButton() 402 | { 403 | Type = "reply", 404 | Reply = new CM.Text.BusinessMessaging.Model.MultiChannel.ReplyMessage() 405 | { 406 | Id = "unique-postback-id2", 407 | Title = "Second Button " 408 | } 409 | } 410 | } 411 | } 412 | } 413 | }); 414 | 415 | var message = builder.Build(); 416 | var result = await client.SendMessageAsync(message); 417 | ``` 418 | 419 | ## Using the OTP API 420 | Send a simple OTP code 421 | ```cs 422 | var client = new TextClient(new Guid(ConfigurationManager.AppSettings["ApiKey"])); 423 | var otpBuilder = new OtpRequestBuilder("Sender_name", "Recipient_PhoneNumber"); 424 | otpBuilder.WithMessage("Your otp code is {code}."); 425 | var result = await textClient.SendOtpAsync(otpBuilder.Build()); 426 | ``` 427 | 428 | Verify the response code 429 | ```cs 430 | var verifyResult = client.VerifyOtp("OTP-ID", "code"); 431 | bool isValid = verifyResult.Verified; 432 | ``` 433 | 434 | For more advanced scenarios see also https://developers.cm.com/identity/docs/one-time-password-create --------------------------------------------------------------------------------