diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b4f5c9a1..08e34858 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -30,6 +30,7 @@ jobs: - 6039:6039 - 6040:6040 - 6041:6041 + - 6042:6042 - 6043:6043 - 6044:6044 diff --git a/README.md b/README.md index 30712b55..db08bcf4 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,10 @@ Sinch client provides access to the following Sinch products: - Numbers - SMS - Verification +- Voice - Work-in-Progress Conversation -Usage example of the `numbers` product, assuming `sinch` is type of `SinchClient`: +Usage example of the `numbers` product, assuming `sinch` is a type of `ISinchClient`: ```csharp using Sinch.Numbers.Active.List; diff --git a/src/Sinch/Conversation/Apps/Apps.cs b/src/Sinch/Conversation/Apps/Apps.cs index 5d414028..653eb0aa 100644 --- a/src/Sinch/Conversation/Apps/Apps.cs +++ b/src/Sinch/Conversation/Apps/Apps.cs @@ -19,7 +19,7 @@ namespace Sinch.Conversation.Apps /// for each underlying connected channel. /// The app has a list of conversations between itself and different contacts which share the same project. /// - public interface ISinchConversationApp + public interface ISinchConversationApps { /// /// You can create a new Conversation API app using the API. @@ -80,7 +80,7 @@ public interface ISinchConversationApp Task Update(string appId, UpdateAppRequest request, CancellationToken cancellationToken = default); } - internal class Apps : ISinchConversationApp + internal class Apps : ISinchConversationApps { private readonly Uri _baseAddress; private readonly IHttp _http; diff --git a/src/Sinch/Conversation/Contacts/Contact.cs b/src/Sinch/Conversation/Contacts/Contact.cs new file mode 100644 index 00000000..261dbe75 --- /dev/null +++ b/src/Sinch/Conversation/Contacts/Contact.cs @@ -0,0 +1,166 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Sinch.Conversation.Messages; +using Sinch.Core; + +namespace Sinch.Conversation.Contacts +{ + public sealed class Contact + { + /// + /// Tracks the fields which where initialized. + /// + private readonly ISet _setFields = new HashSet(); + + private List _channelIdentities; + private List _channelPriority; + private string _displayName; + private string _email; + private string _externalId; + private string _id; + private ConversationLanguage _language; + private string _metadata; + + /// + /// List of channel identities. + /// + public List ChannelIdentities + { + get => _channelIdentities; + set + { + _setFields.Add(nameof(ChannelIdentities)); + _channelIdentities = value; + } + } + + + /// + /// List of channels defining the channel priority. + /// + public List ChannelPriority + { + get => _channelPriority; + set + { + _setFields.Add(nameof(ChannelPriority)); + _channelPriority = value; + } + } + + + /// + /// The display name. A default 'Unknown' will be assigned if left empty. + /// + public string DisplayName + { + get => _displayName; + set + { + _setFields.Add(nameof(DisplayName)); + _displayName = value; + } + } + + + /// + /// Email of the contact. + /// + public string Email + { + get => _email; + set + { + _setFields.Add(nameof(Email)); + _email = value; + } + } + + + /// + /// Contact identifier in an external system. + /// + public string ExternalId + { + get => _externalId; + set + { + _setFields.Add(nameof(ExternalId)); + _externalId = value; + } + } + + + /// + /// The ID of the contact. + /// + public string Id + { + get => _id; + set + { + _setFields.Add(nameof(Id)); + _id = value; + } + } + + + /// + /// Gets or Sets Language + /// + public ConversationLanguage Language + { + get => _language; + set + { + _setFields.Add(nameof(Language)); + _language = value; + } + } + + + /// + /// Metadata associated with the contact. Up to 1024 characters long. + /// + public string Metadata + { + get => _metadata; + set + { + _setFields.Add(nameof(Metadata)); + _metadata = value; + } + } + + /// + /// Get the comma separated snake_case list of properties which were directly initialized in this object. + /// If, for example, DisplayName and Metadata were set, will return display_name,metadata + /// + /// + internal string GetPropertiesMask() + { + return string.Join(',', _setFields.Select(StringUtils.ToSnakeCase)); + } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class Contact {\n"); + sb.Append(" ChannelIdentities: ").Append(ChannelIdentities).Append("\n"); + sb.Append(" ChannelPriority: ").Append(ChannelPriority).Append("\n"); + sb.Append(" DisplayName: ").Append(DisplayName).Append("\n"); + sb.Append(" Email: ").Append(Email).Append("\n"); + sb.Append(" ExternalId: ").Append(ExternalId).Append("\n"); + sb.Append(" Id: ").Append(Id).Append("\n"); + sb.Append(" Language: ").Append(Language).Append("\n"); + sb.Append(" Metadata: ").Append(Metadata).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + } +} diff --git a/src/Sinch/Conversation/Contacts/Contacts.cs b/src/Sinch/Conversation/Contacts/Contacts.cs new file mode 100644 index 00000000..fd16a743 --- /dev/null +++ b/src/Sinch/Conversation/Contacts/Contacts.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Sinch.Conversation.Contacts.Create; +using Sinch.Conversation.Contacts.GetChannelProfile; +using Sinch.Conversation.Contacts.List; +using Sinch.Core; +using Sinch.Logger; + +namespace Sinch.Conversation.Contacts +{ + /// + /// A contact is a collection that groups together underlying connected channel recipient identities. It's tied to a + /// specific project and is therefore considered public to all apps sharing the same project. Most contact creation and + /// maintenance is handled by the Conversation API's automatic [contact + /// management](https://developers.sinch.com/docs/conversation/contact-management/ processes. However, you can also use + /// API calls to manually manage your contacts.

+ /// + /// + /// Field + /// Description + /// + /// + /// Channel identities + /// List of channel identities specifying how the contact is identified on underlying channels + /// + /// + /// Channel priority + /// + /// Specifies the channel priority order used when sending messages to this contact. This can be + /// overridden by message specific channel priority order. + /// + /// + /// + /// Display name + /// Optional display name used in chat windows and other UIs + /// + /// + /// Email + /// Optional Email of the contact + /// + /// + /// External id + /// Optional identifier of the contact in external systems + /// + /// + /// Metadata + /// Optional metadata associated with the contact. + /// + /// + ///
+ public interface ISinchConversationContacts + { + /// + /// Returns a specific contact as specified by the contact ID. Note that, if a WhatsApp contact is returned, the + /// display_name field of that contact may be populated with the WhatsApp display name (if the name is already stored + /// on the server and the display_name field has not been overwritten by the user). + /// + /// The unique ID of the contact. + /// + /// + Task Get(string contactId, CancellationToken cancellationToken = default); + + /// + /// Most Conversation API contacts are [created + /// automatically](https://developers.sinch.com/docs/conversation/contact-management/) when a message is sent to a new + /// recipient. You can also create a new contact manually using this API call. + /// + /// + Task Create(CreateContactRequest request, CancellationToken cancellationToken = default); + + /// + /// List all contacts in the project. Note that, if a WhatsApp contact is returned, the display_name field of that + /// contact may be populated with the WhatsApp display name (if the name is already stored on the server and the + /// display_name field has not been overwritten by the user). + /// + /// + /// + /// + Task List(ListContactsRequest request, CancellationToken cancellationToken = default); + + /// + /// See , but lists all contacts automatically. + /// + /// + /// + /// + IAsyncEnumerable ListAuto(ListContactsRequest request, CancellationToken cancellationToken = default); + + /// + /// Delete a contact as specified by the contact ID. + /// + /// The unique ID of the contact. + /// + /// + Task Delete(string contactId, CancellationToken cancellationToken = default); + + /// + /// Get user profile from a specific channel. Only supported on MESSENGER, INSTAGRAM, VIBER and LINE channels. Note + /// that, in order to retrieve a WhatsApp display name, you can use the Get a Contact or List Contacts operations, + /// which will populate the display_name field of each returned contact with the WhatsApp display name (if the name is + /// already stored on the server and the display_name field has not been overwritten by the user). + /// + /// + /// + /// + Task GetChannelProfile(GetChannelProfileRequest request, + CancellationToken cancellationToken = default); + + /// + /// Updates a contact as specified by the contact ID. + /// + /// + /// + /// + Task Update(Contact contact, CancellationToken cancellationToken = default); + + /// + /// Merge two contacts. The remaining contact will contain all conversations that the removed contact did. If both + /// contacts had conversations within the same App, messages from the removed contact will be merged into corresponding + /// active conversations in the destination contact. Channel identities will be moved from the source contact to the + /// destination contact only for channels that weren't present there before. Moved channel identities will be placed at + /// the bottom of the channel priority list. Optional fields from the source contact will be copied only if + /// corresponding fields in the destination contact are empty The contact being removed cannot be referenced after this + /// call. + /// + /// The unique ID of the contact that should be kept when merging two contacts. + /// The ID of the contact that should be removed. + /// + /// + Task Merge(string destinationId, string sourceId, CancellationToken cancellationToken = default); + } + + internal class Contacts : ISinchConversationContacts + { + private readonly Uri _baseAddress; + private readonly IHttp _http; + private readonly ILoggerAdapter _logger; + private readonly string _projectId; + + public Contacts(string projectId, Uri baseAddress, ILoggerAdapter logger, + IHttp http) + { + _projectId = projectId; + _baseAddress = baseAddress; + _logger = logger; + _http = http; + } + + /// + public Task Get(string contactId, CancellationToken cancellationToken = default) + { + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts/{contactId}"); + _logger?.LogDebug("Getting a {contactId} for a {projectId}", contactId, _projectId); + return _http.Send(uri, HttpMethod.Get, + cancellationToken); + } + + /// + public Task Create(CreateContactRequest request, CancellationToken cancellationToken = default) + { + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts"); + _logger?.LogDebug("Creating a contact for a {projectId}", _projectId); + return _http.Send(uri, HttpMethod.Post, request, + cancellationToken); + } + + /// + public Task List(ListContactsRequest request, + CancellationToken cancellationToken = default) + { + var query = Utils.ToSnakeCaseQueryString(request); + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts?{query}"); + _logger?.LogDebug("Listing contacts for {projectId}", _projectId); + return _http.Send(uri, HttpMethod.Get, cancellationToken); + } + + /// + public async IAsyncEnumerable ListAuto(ListContactsRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + _logger?.LogDebug("Auto Listing contacts for {projectId}", _projectId); + do + { + var query = Utils.ToSnakeCaseQueryString(request); + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts?{query}"); + var response = + await _http.Send(uri, HttpMethod.Get, cancellationToken); + request.PageToken = response.NextPageToken; + foreach (var contact in response.Contacts) yield return contact; + } while (!string.IsNullOrEmpty(request.PageToken)); + } + + /// + public Task Delete(string contactId, CancellationToken cancellationToken = default) + { + _logger?.LogDebug("Deleting a {contactId} from {projectId}", contactId, _projectId); + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts/{contactId}"); + return _http.Send(uri, HttpMethod.Delete, cancellationToken); + } + + /// + public Task GetChannelProfile(GetChannelProfileRequest request, + CancellationToken cancellationToken = default) + { + _logger?.LogDebug("Getting a profile for {projectId} of {channel}", _projectId, request.Channel); + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts:getChannelProfile"); + return _http.Send(uri, HttpMethod.Post, request, + cancellationToken); + } + + /// + public Task Update(Contact contact, CancellationToken cancellationToken = default) + { + _logger?.LogDebug("Updating a {contactId} of {projectId}", contact.Id, _projectId); + // the update_mask param will regulate which properties to set. + // Keep in mind that no depth is supported: for example, you cannot mask channel_identities.identity + var uri = new Uri(_baseAddress, + $"/v1/projects/{_projectId}/contacts/{contact.Id}?update_mask={contact.GetPropertiesMask()}"); + return _http.Send(uri, HttpMethod.Patch, contact, + cancellationToken); + } + + /// + public Task Merge(string destinationId, string sourceId, CancellationToken cancellationToken = default) + { + _logger?.LogDebug("Merging contacts from {sourceId} to {destinationId} for {projectId}", sourceId, + destinationId, _projectId); + var uri = new Uri(_baseAddress, $"/v1/projects/{_projectId}/contacts/{destinationId}:merge"); + return _http.Send(uri, HttpMethod.Post, new + { + source_id = sourceId, + // NOTE: keep in mind while this enum has only one value, it can change in the future. + strategy = "MERGE" + }, + cancellationToken); + } + } +} diff --git a/src/Sinch/Conversation/Contacts/Create/CreateContactRequest.cs b/src/Sinch/Conversation/Contacts/Create/CreateContactRequest.cs new file mode 100644 index 00000000..39fc2d86 --- /dev/null +++ b/src/Sinch/Conversation/Contacts/Create/CreateContactRequest.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Text; +using Sinch.Conversation.Messages; + +namespace Sinch.Conversation.Contacts.Create +{ + public class CreateContactRequest + { + /// + /// List of channel identities. Array must contain at least one item. + /// +#if NET7_0_OR_GREATER + public required List ChannelIdentities { get; set; } +#else + public List ChannelIdentities { get; set; } +#endif + + /// + /// Gets or Sets Language + /// +#if NET7_0_OR_GREATER + public required string Language { get; set; } +#else + public string Language { get; set; } +#endif + + /// + /// List of channels defining the channel priority. The channel at the top of the list is tried first. + /// + public List ChannelPriority { get; set; } + + + /// + /// The display name. A default 'Unknown' will be assigned if left empty. + /// + public string DisplayName { get; set; } + + + /// + /// Email of the contact. + /// + public string Email { get; set; } + + + /// + /// Contact identifier in an external system. + /// + public string ExternalId { get; set; } + + + /// + /// Metadata associated with the contact. Up to 1024 characters long. + /// + public string Metadata { get; set; } + + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class ContactCreateRequest {\n"); + sb.Append(" ChannelIdentities: ").Append(ChannelIdentities).Append("\n"); + sb.Append(" ChannelPriority: ").Append(ChannelPriority).Append("\n"); + sb.Append(" DisplayName: ").Append(DisplayName).Append("\n"); + sb.Append(" Email: ").Append(Email).Append("\n"); + sb.Append(" ExternalId: ").Append(ExternalId).Append("\n"); + sb.Append(" Language: ").Append(Language).Append("\n"); + sb.Append(" Metadata: ").Append(Metadata).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + } +} diff --git a/src/Sinch/Conversation/Contacts/GetChannelProfile/ChannelProfile.cs b/src/Sinch/Conversation/Contacts/GetChannelProfile/ChannelProfile.cs new file mode 100644 index 00000000..31c800a3 --- /dev/null +++ b/src/Sinch/Conversation/Contacts/GetChannelProfile/ChannelProfile.cs @@ -0,0 +1,10 @@ +namespace Sinch.Conversation.Contacts.GetChannelProfile +{ + public class ChannelProfile + { + /// + /// The profile name. + /// + public string ProfileName { get; set; } + } +} diff --git a/src/Sinch/Conversation/Contacts/GetChannelProfile/ChannelProfileConversationChannel.cs b/src/Sinch/Conversation/Contacts/GetChannelProfile/ChannelProfileConversationChannel.cs new file mode 100644 index 00000000..f56bb0dc --- /dev/null +++ b/src/Sinch/Conversation/Contacts/GetChannelProfile/ChannelProfileConversationChannel.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using Sinch.Core; + +namespace Sinch.Conversation.Contacts.GetChannelProfile +{ + /// + /// The list of the channel which support channel profiles + /// + /// + [JsonConverter(typeof(EnumRecordJsonConverter))] + public record ChannelProfileConversationChannel(string Value) : EnumRecord(Value) + { + public static readonly ChannelProfileConversationChannel Messenger = new(ConversationChannel.Messenger.Value); + public static readonly ChannelProfileConversationChannel Instagram = new(ConversationChannel.Instagram.Value); + public static readonly ChannelProfileConversationChannel Viber = new(ConversationChannel.Viber.Value); + public static readonly ChannelProfileConversationChannel Line = new(ConversationChannel.Line.Value); + } +} diff --git a/src/Sinch/Conversation/Contacts/GetChannelProfile/GetChannelProfileRequest.cs b/src/Sinch/Conversation/Contacts/GetChannelProfile/GetChannelProfileRequest.cs new file mode 100644 index 00000000..7528acb0 --- /dev/null +++ b/src/Sinch/Conversation/Contacts/GetChannelProfile/GetChannelProfileRequest.cs @@ -0,0 +1,35 @@ +using Sinch.Conversation.Messages; + +namespace Sinch.Conversation.Contacts.GetChannelProfile +{ + public class GetChannelProfileRequest + { + /// + /// The recipient to check profile information. Requires either contact_id or identified_by. + /// + /// +#if NET7_0_OR_GREATER + public required IRecipient Recipient { get; set; } +#else + public IRecipient Recipient { get; set; } +#endif + + /// + /// The ID of the app. + /// +#if NET7_0_OR_GREATER + public required string AppId { get; set; } +#else + public string AppId { get; set; } +#endif + + /// + /// The channel. Must be one of the supported channels for this operation. + /// +#if NET7_0_OR_GREATER + public required ChannelProfileConversationChannel Channel { get; set; } +#else + public ChannelProfileConversationChannel Channel { get; set; } +#endif + } +} diff --git a/src/Sinch/Conversation/Contacts/List/ListContactsResponse.cs b/src/Sinch/Conversation/Contacts/List/ListContactsResponse.cs new file mode 100644 index 00000000..28d3f630 --- /dev/null +++ b/src/Sinch/Conversation/Contacts/List/ListContactsResponse.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Sinch.Conversation.Contacts.List +{ + public class ListContactsResponse + { + /// + /// Token that should be included in the next list contacts request to fetch the next page. + /// + public string NextPageToken { get; set; } + + /// + /// List of contacts belonging to the specified project. + /// + public List Contacts { get; set; } + } +} diff --git a/src/Sinch/Conversation/Contacts/List/ListContractsRequest.cs b/src/Sinch/Conversation/Contacts/List/ListContractsRequest.cs new file mode 100644 index 00000000..f701a15d --- /dev/null +++ b/src/Sinch/Conversation/Contacts/List/ListContractsRequest.cs @@ -0,0 +1,34 @@ +namespace Sinch.Conversation.Contacts.List +{ + public class ListContactsRequest + { + /// + /// Optional. The maximum number of contacts to fetch. The default is 10 and the maximum is 20. + /// + public int? PageSize { get; set; } + + /// + /// Optional. Next page token previously returned if any. + /// + public string PageToken { get; set; } + + /// + /// Optional. Contact identifier in an external system. If used, channel and identity query parameters can't be used. + /// + public string ExternalId { get; set; } + + /// + /// Optional. Specifies a channel, and must be set to one of the enum values. If set, the identity parameter must be + /// set and external_id can't be used. Used in conjunction with identity to uniquely identify the specified channel + /// identity. + /// + public ConversationChannel Channel { get; set; } + + /// + /// Optional. If set, the channel parameter must be set and external_id can't be used. Used in conjunction with channel + /// to uniquely identify the specified channel identity. This will differ from channel to channel. For example, a phone + /// number for SMS, WhatsApp, and Viber Business. + /// + public string Identity { get; set; } + } +} diff --git a/src/Sinch/Conversation/Conversation.cs b/src/Sinch/Conversation/Conversation.cs index eee11766..0e6f25d6 100644 --- a/src/Sinch/Conversation/Conversation.cs +++ b/src/Sinch/Conversation/Conversation.cs @@ -1,5 +1,6 @@ using System; using Sinch.Conversation.Apps; +using Sinch.Conversation.Contacts; using Sinch.Conversation.Messages; using Sinch.Core; using Sinch.Logger; @@ -18,8 +19,11 @@ public interface ISinchConversation /// ISinchConversationMessages Messages { get; } - /// - ISinchConversationApp App { get; } + /// + ISinchConversationApps Apps { get; } + + /// + ISinchConversationContacts Contacts { get; } } /// @@ -29,13 +33,17 @@ internal Conversation(string projectId, Uri baseAddress, LoggerFactory loggerFac { Messages = new Messages.Messages(projectId, baseAddress, loggerFactory?.Create(), http); - App = new Apps.Apps(projectId, baseAddress, loggerFactory?.Create(), http); + Apps = new Apps.Apps(projectId, baseAddress, loggerFactory?.Create(), http); + Contacts = new Contacts.Contacts(projectId, baseAddress, loggerFactory?.Create(), http); } /// public ISinchConversationMessages Messages { get; } /// - public ISinchConversationApp App { get; } + public ISinchConversationApps Apps { get; } + + /// + public ISinchConversationContacts Contacts { get; } } } diff --git a/src/Sinch/Conversation/Messages/ConversationChannel.cs b/src/Sinch/Conversation/ConversationChannel.cs similarity index 98% rename from src/Sinch/Conversation/Messages/ConversationChannel.cs rename to src/Sinch/Conversation/ConversationChannel.cs index 825b67b5..de376761 100644 --- a/src/Sinch/Conversation/Messages/ConversationChannel.cs +++ b/src/Sinch/Conversation/ConversationChannel.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using Sinch.Core; -namespace Sinch.Conversation.Messages +namespace Sinch.Conversation { /// /// Represents the identifier of the channel you want to include. diff --git a/src/Sinch/Conversation/ConversationLanguage.cs b/src/Sinch/Conversation/ConversationLanguage.cs new file mode 100644 index 00000000..550da1fa --- /dev/null +++ b/src/Sinch/Conversation/ConversationLanguage.cs @@ -0,0 +1,80 @@ +using System.Text.Json.Serialization; +using Sinch.Core; + +namespace Sinch.Conversation +{ + [JsonConverter(typeof(EnumRecordJsonConverter))] + public record ConversationLanguage(string Value) : EnumRecord(Value) + { + public static readonly ConversationLanguage Afrikaans = new("AF"); + public static readonly ConversationLanguage Albanian = new("SQ"); + public static readonly ConversationLanguage Arabic = new("AR"); + public static readonly ConversationLanguage Azerbaijani = new("AZ"); + public static readonly ConversationLanguage Bengali = new("BN"); + public static readonly ConversationLanguage Bulgarian = new("BG"); + public static readonly ConversationLanguage Catalan = new("CA"); + public static readonly ConversationLanguage Chinese = new("ZH"); + public static readonly ConversationLanguage ChineseCHN = new("ZH_CN"); + public static readonly ConversationLanguage ChineseHKG = new("ZH_HK"); + public static readonly ConversationLanguage ChineseTAI = new("ZH_TW"); + public static readonly ConversationLanguage Croatian = new("HR"); + public static readonly ConversationLanguage Czech = new("CS"); + public static readonly ConversationLanguage Danish = new("DA"); + public static readonly ConversationLanguage Dutch = new("NL"); + public static readonly ConversationLanguage English = new("EN"); + public static readonly ConversationLanguage EnglishUK = new("EN_GB"); + public static readonly ConversationLanguage EnglishUS = new("EN_US"); + public static readonly ConversationLanguage Estonian = new("ET"); + public static readonly ConversationLanguage Filipino = new("FIL"); + public static readonly ConversationLanguage Finnish = new("FI"); + public static readonly ConversationLanguage French = new("FR"); + public static readonly ConversationLanguage German = new("DE"); + public static readonly ConversationLanguage Greek = new("EL"); + public static readonly ConversationLanguage Gujarati = new("GU"); + public static readonly ConversationLanguage Hausa = new("HA"); + public static readonly ConversationLanguage Hebrew = new("HE"); + public static readonly ConversationLanguage Hindi = new("HI"); + public static readonly ConversationLanguage Hungarian = new("HU"); + public static readonly ConversationLanguage Indonesian = new("ID"); + public static readonly ConversationLanguage Irish = new("GA"); + public static readonly ConversationLanguage Italian = new("IT"); + public static readonly ConversationLanguage Japanese = new("JA"); + public static readonly ConversationLanguage Kannada = new("KN"); + public static readonly ConversationLanguage Kazakh = new("KK"); + public static readonly ConversationLanguage Korean = new("KO"); + public static readonly ConversationLanguage Lao = new("LO"); + public static readonly ConversationLanguage Latvian = new("LV"); + public static readonly ConversationLanguage Lithuanian = new("LT"); + public static readonly ConversationLanguage Macedonian = new("MK"); + public static readonly ConversationLanguage Malay = new("MS"); + public static readonly ConversationLanguage Malayalam = new("ML"); + public static readonly ConversationLanguage Marathi = new("MR"); + public static readonly ConversationLanguage Norwegian = new("NB"); + public static readonly ConversationLanguage Persian = new("FA"); + public static readonly ConversationLanguage Polish = new("PL"); + public static readonly ConversationLanguage Portuguese = new("PT"); + public static readonly ConversationLanguage PortugueseBr = new("PT_BR"); + public static readonly ConversationLanguage PortuguesePt = new("PT_PT"); + public static readonly ConversationLanguage Punjabi = new("PA"); + public static readonly ConversationLanguage Romanian = new("RO"); + public static readonly ConversationLanguage Russian = new("RU"); + public static readonly ConversationLanguage Serbian = new("SR"); + public static readonly ConversationLanguage Slovak = new("SK"); + public static readonly ConversationLanguage Slovenian = new("SL"); + public static readonly ConversationLanguage Spanish = new("ES"); + public static readonly ConversationLanguage SpanishArg = new("ES_AR"); + public static readonly ConversationLanguage SpanishSpa = new("ES_ES"); + public static readonly ConversationLanguage SpanishMex = new("ES_MX"); + public static readonly ConversationLanguage Swahili = new("SW"); + public static readonly ConversationLanguage Swedish = new("SV"); + public static readonly ConversationLanguage Tamil = new("TA"); + public static readonly ConversationLanguage Telugu = new("TE"); + public static readonly ConversationLanguage Thai = new("TH"); + public static readonly ConversationLanguage Turkish = new("TR"); + public static readonly ConversationLanguage Ukrainian = new("UK"); + public static readonly ConversationLanguage Urdu = new("UR"); + public static readonly ConversationLanguage Uzbek = new("UZ"); + public static readonly ConversationLanguage Vietnamese = new("VI"); + public static readonly ConversationLanguage Zulu = new("ZU"); + } +} diff --git a/src/Sinch/Conversation/Messages/ContactId.cs b/src/Sinch/Conversation/Messages/ContactId.cs index ec90b604..fd54c7cf 100644 --- a/src/Sinch/Conversation/Messages/ContactId.cs +++ b/src/Sinch/Conversation/Messages/ContactId.cs @@ -1,6 +1,6 @@ namespace Sinch.Conversation.Messages { - public sealed class Contact : IRecipient + public sealed class ContactRecipient : IRecipient { /// /// The ID of the contact. diff --git a/src/Sinch/Conversation/Messages/IRecipient.cs b/src/Sinch/Conversation/Messages/IRecipient.cs index 3e033803..751eaa07 100644 --- a/src/Sinch/Conversation/Messages/IRecipient.cs +++ b/src/Sinch/Conversation/Messages/IRecipient.cs @@ -1,8 +1,9 @@ -using Sinch.Core; +using System.Text.Json.Serialization; namespace Sinch.Conversation.Messages { - [JsonInterfaceConverter(typeof(InterfaceConverter))] + [JsonDerivedType(typeof(ContactRecipient))] + [JsonDerivedType(typeof(Identified))] public interface IRecipient { } diff --git a/src/Sinch/Conversation/Messages/Identified.cs b/src/Sinch/Conversation/Messages/Identified.cs index 3b3c218a..50898b4c 100644 --- a/src/Sinch/Conversation/Messages/Identified.cs +++ b/src/Sinch/Conversation/Messages/Identified.cs @@ -14,10 +14,24 @@ public class IdentifiedBy public class ChannelIdentity { + /// + /// Required if using a channel that uses app-scoped channel identities. Currently, FB Messenger, Viber Bot, Instagram, + /// Apple Messages for Business, LINE, and WeChat use app-scoped channel identities, which means contacts will have + /// different channel identities on different Conversation API apps. These can be thought of as virtual identities that + /// are app-specific and, therefore, the app_id must be included in the API call. + /// public string AppId { get; set; } + /// + /// The channel identity. This will differ from channel to channel. For example, a phone number for SMS, WhatsApp, and + /// Viber Business. + /// public string Identity { get; set; } + /// + /// The identifier of the channel you want to include. Must be one of the enum values. See + /// fields. + /// public ConversationChannel Channel { get; set; } } } diff --git a/src/Sinch/SinchClient.cs b/src/Sinch/SinchClient.cs index d7ca5e69..237c7d2d 100644 --- a/src/Sinch/SinchClient.cs +++ b/src/Sinch/SinchClient.cs @@ -219,7 +219,7 @@ private static Uri GetSmsBaseAddress(SmsHostingRegion smsHostingRegion, string s /// public ISinchVerificationClient Verification(string appKey, string appSecret, - AuthStrategy authStrategy) + AuthStrategy authStrategy = AuthStrategy.ApplicationSign) { if (string.IsNullOrEmpty(appKey)) { diff --git a/tests/Sinch.Tests/Conversation/AppsTests.cs b/tests/Sinch.Tests/Conversation/AppsTests.cs index bf1e59a3..8d821b20 100644 --- a/tests/Sinch.Tests/Conversation/AppsTests.cs +++ b/tests/Sinch.Tests/Conversation/AppsTests.cs @@ -7,10 +7,11 @@ using FluentAssertions; using Newtonsoft.Json; using RichardSzalay.MockHttp; +using Sinch.Conversation; using Sinch.Conversation.Apps; using Sinch.Conversation.Apps.Create; using Sinch.Conversation.Apps.Credentials; -using Sinch.Conversation.Messages; +using Sinch.Conversation.Apps.Update; using Xunit; namespace Sinch.Tests.Conversation @@ -243,7 +244,7 @@ public async Task Create() .Respond(HttpStatusCode.OK, JsonContent.Create(_app)); - var response = await Conversation.App.Create(_createRequest); + var response = await Conversation.Apps.Create(_createRequest); response.DisplayName.Should().Be("Sinch Conversation API Demo App 001"); response.QueueStats.Should().BeEquivalentTo(new QueueStats() @@ -332,7 +333,7 @@ public async Task List() } })); - var response = await Conversation.App.List(); + var response = await Conversation.Apps.List(); response.Should().HaveCount(2); } @@ -345,7 +346,7 @@ public async Task Get() $"https://us.conversation.api.sinch.com/v1/projects/{ProjectId}/apps/123") .Respond(HttpStatusCode.OK, JsonContent.Create(_app)); - var response = await Conversation.App.Get("123"); + var response = await Conversation.Apps.Get("123"); response.DisplayName.Should().Be("Sinch Conversation API Demo App 001"); } @@ -358,7 +359,7 @@ public async Task Delete() $"https://us.conversation.api.sinch.com/v1/projects/{ProjectId}/apps/123") .Respond(HttpStatusCode.OK); - Func response = () => Conversation.App.Delete("123"); + Func response = () => Conversation.Apps.Delete("123"); await response.Should().NotThrowAsync(); } @@ -377,7 +378,7 @@ public async Task Update() .WithQueryString("update_mask.paths", "b") .Respond(HttpStatusCode.OK, JsonContent.Create(_app)); - var request = new Sinch.Conversation.Apps.Update.UpdateAppRequest() + var request = new UpdateAppRequest() { DisplayName = "abc", UpdateMaskPaths = new List() @@ -386,7 +387,7 @@ public async Task Update() }, }; - var response = await Conversation.App.Update("123", request); + var response = await Conversation.Apps.Update("123", request); response.Should().NotBeNull(); } diff --git a/tests/Sinch.Tests/Conversation/ContactsTests.cs b/tests/Sinch.Tests/Conversation/ContactsTests.cs new file mode 100644 index 00000000..98e5996e --- /dev/null +++ b/tests/Sinch.Tests/Conversation/ContactsTests.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using FluentAssertions; +using Sinch.Conversation; +using Sinch.Conversation.Contacts; +using Sinch.Conversation.Messages; +using Xunit; + +namespace Sinch.Tests.Conversation +{ + public class ContactsTests : ConversationTestBase + { + [Fact] + public void ContactMaskTwoFields() + { + var contact = new Contact() + { + DisplayName = "hola", + Metadata = null + }; + contact.GetPropertiesMask().Should().BeEquivalentTo("display_name,metadata"); + } + + [Fact] + public void ContactMaskAllFields() + { + var contact = new Contact() + { + DisplayName = "hola", + Metadata = "aaaa", + ExternalId = "id", + ChannelPriority = new List(), + Email = "mail", + ChannelIdentities = new List(), + Language = ConversationLanguage.Arabic, + Id = "id", + }; + contact.GetPropertiesMask().Should() + .BeEquivalentTo( + "display_name,metadata,external_id,channel_priority,email,channel_identities,language,id"); + } + } +} diff --git a/tests/Sinch.Tests/Conversation/SendMessageTests.cs b/tests/Sinch.Tests/Conversation/SendMessageTests.cs index 72a91dfa..a6d007b9 100644 --- a/tests/Sinch.Tests/Conversation/SendMessageTests.cs +++ b/tests/Sinch.Tests/Conversation/SendMessageTests.cs @@ -29,7 +29,7 @@ public class SendMessageTests : ConversationTestBase ExplicitChannelMessage = null, AdditionalProperties = null }, - Recipient = new Contact() + Recipient = new ContactRecipient() { ContactId = "ContactEasy" } diff --git a/tests/Sinch.Tests/e2e/Conversation/ContactsTests.cs b/tests/Sinch.Tests/e2e/Conversation/ContactsTests.cs new file mode 100644 index 00000000..2f16c441 --- /dev/null +++ b/tests/Sinch.Tests/e2e/Conversation/ContactsTests.cs @@ -0,0 +1,215 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Sinch.Conversation; +using Sinch.Conversation.Contacts.Create; +using Sinch.Conversation.Contacts.GetChannelProfile; +using Sinch.Conversation.Contacts.List; +using Sinch.Conversation.Messages; +using Xunit; +using Contact = Sinch.Conversation.Contacts.Contact; + +namespace Sinch.Tests.e2e.Conversation +{ + public class ContactsTests : TestBase + { + [Fact] + public async Task GetContact() + { + var response = await SinchClientMockServer.Conversation.Contacts.Get("123ABC"); + + response.Should().BeEquivalentTo(new Contact + { + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Telegram, + Identity = "@hola", + AppId = string.Empty, + } + }, + ChannelPriority = new List() + { + ConversationChannel.WhatsApp + }, + DisplayName = "New Contact", + Email = "new.contact@email.com", + ExternalId = "yes", + Id = "01HKWT3XRVH6RP17S8KSBC4PYR", + Language = ConversationLanguage.EnglishUS, + Metadata = "no" + }); + } + + [Fact] + public async Task CreateContact() + { + var response = await SinchClientMockServer.Conversation.Contacts.Create(new CreateContactRequest() + { + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Sms, + Identity = "+49123123123", + }, + new ChannelIdentity() + { + Channel = ConversationChannel.Viber, + Identity = "/ret", + AppId = "15" + } + }, + Language = "ZH_CN", + ChannelPriority = new List() + { + ConversationChannel.Viber, + ConversationChannel.Sms + }, + Email = "oi@mail.org", + DisplayName = "one", + Metadata = "rogue", + ExternalId = "plan" + }); + + response.Should().BeEquivalentTo(new Contact + { + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Telegram, + Identity = "@hola", + AppId = string.Empty, + } + }, + ChannelPriority = new List() + { + ConversationChannel.WhatsApp + }, + DisplayName = "New Contact", + Email = "new.contact@email.com", + ExternalId = "yes", + Id = "01HKWT3XRVH6RP17S8KSBC4PYR", + Language = ConversationLanguage.EnglishUS, + Metadata = "no" + }); + } + + [Fact] + public async Task List() + { + var response = await SinchClientMockServer.Conversation.Contacts.List(new ListContactsRequest() + { + Channel = ConversationChannel.Instagram, + ExternalId = "@nice", + Identity = "nice", + PageSize = 10, + PageToken = "tin", + }); + + response.Contacts.Should().HaveCount(2); + response.NextPageToken.Should().BeEquivalentTo("next"); + } + + [Fact] + public async Task ListAuto() + { + var response = SinchClientMockServer.Conversation.Contacts.ListAuto(new ListContactsRequest() + { + Channel = ConversationChannel.Instagram, + ExternalId = "@nice", + Identity = "nice", + PageSize = 10, + PageToken = "tin", + }); + var counter = 0; + await foreach (var contact in response) + { + contact.Should().NotBeNull(); + counter++; + } + + counter.Should().Be(4); + } + + [Fact] + public async Task Delete() + { + var op = () => SinchClientMockServer.Conversation.Contacts.Delete("123ABC"); + await op.Should().NotThrowAsync(); + } + + [Fact] + public async Task GetChannelProfile() + { + var response = await SinchClientMockServer.Conversation.Contacts.GetChannelProfile( + new GetChannelProfileRequest() + { + AppId = "123", + Channel = ChannelProfileConversationChannel.Line, + Recipient = new Identified() + { + IdentifiedBy = new IdentifiedBy() + { + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Identity = "a", + Channel = ConversationChannel.Rcs, + }, + new ChannelIdentity() + { + Identity = "b", + Channel = ConversationChannel.Messenger, + } + } + } + } + }); + response.Should().BeEquivalentTo(new ChannelProfile() + { + ProfileName = "beast" + }); + } + + [Fact] + public async Task Update() + { + var contact = new Contact() + { + Id = "123ABC", + DisplayName = "Unknown", + ExternalId = "", + Email = "new.contact@email.com", + Metadata = "", + Language = ConversationLanguage.EnglishUS, + ChannelPriority = new List() + { + ConversationChannel.Telegram + }, + ChannelIdentities = new List() + { + new ChannelIdentity() + { + Channel = ConversationChannel.Telegram, + Identity = "@ora", + AppId = "", + } + } + }; + var response = await SinchClientMockServer.Conversation.Contacts.Update(contact); + response.Should().BeEquivalentTo(contact); + } + + + [Fact] + public async Task Merge() + { + var response = await SinchClientMockServer.Conversation.Contacts.Merge("123ABC", "456EDF"); + response.Should().NotBeNull(); + } + } +} diff --git a/tests/Sinch.Tests/e2e/TestBase.cs b/tests/Sinch.Tests/e2e/TestBase.cs index 13b2a531..3e9dfd6f 100644 --- a/tests/Sinch.Tests/e2e/TestBase.cs +++ b/tests/Sinch.Tests/e2e/TestBase.cs @@ -5,6 +5,9 @@ namespace Sinch.Tests.e2e { public class TestBase { + /// + /// It's the same value as in doppleganger common.defaultProjectId, so it's shared and common. + /// private const string ProjectId = "e15b2651-daac-4ccb-92e8-e3066d1d033b"; protected readonly ISinchClient SinchClientMockStudio;