Send Text Message
This commit is contained in:
31
src/LinkMobility/Auth/AccessTokenAuthentication.cs
Normal file
31
src/LinkMobility/Auth/AccessTokenAuthentication.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the <see cref="IAuthentication"/> interface for BEARER authentication.
|
||||
/// </summary>
|
||||
public class AccessTokenAuthentication : IAuthentication
|
||||
{
|
||||
private readonly string _token;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AccessTokenAuthentication"/> class.
|
||||
/// </summary>
|
||||
/// <param name="token">The bearer token.</param>
|
||||
public AccessTokenAuthentication(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw new ArgumentNullException(nameof(token), "The token cannot be null or whitespace.");
|
||||
|
||||
_token = token;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddHeader(HttpClient httpClient)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/LinkMobility/Auth/BasicAuthentication.cs
Normal file
42
src/LinkMobility/Auth/BasicAuthentication.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the <see cref="IAuthentication"/> interface for BASIC authentication.
|
||||
/// </summary>
|
||||
public class BasicAuthentication : IAuthentication
|
||||
{
|
||||
private readonly string _username;
|
||||
private readonly string _password;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BasicAuthentication"/> class.
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
public BasicAuthentication(string username, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
throw new ArgumentNullException(nameof(username), "The username cannot be null or whitespace.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentNullException(nameof(password), "The password cannot be null or whitespace.");
|
||||
|
||||
_username = username;
|
||||
_password = password;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddHeader(HttpClient httpClient)
|
||||
{
|
||||
string plainText = $"{_username}:{_password}";
|
||||
byte[] plainBytes = Encoding.ASCII.GetBytes(plainText);
|
||||
string base64 = Convert.ToBase64String(plainBytes);
|
||||
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/LinkMobility/Auth/IAuthentication.cs
Normal file
16
src/LinkMobility/Auth/IAuthentication.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Net.Http;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the interface to add authentication information.
|
||||
/// </summary>
|
||||
public interface IAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the required authentication header to the provided <see cref="HttpClient"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="httpClient"></param>
|
||||
void AddHeader(HttpClient httpClient);
|
||||
}
|
||||
}
|
||||
45
src/LinkMobility/ClientOptions.cs
Normal file
45
src/LinkMobility/ClientOptions.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Net;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the LinkMobility API.
|
||||
/// </summary>
|
||||
public class ClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default base url for the API.
|
||||
/// </summary>
|
||||
public virtual string BaseUrl { get; set; } = "https://api.linkmobility.eu/rest/";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default timeout for the API.
|
||||
/// </summary>
|
||||
public virtual TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional default headers to every request.
|
||||
/// </summary>
|
||||
public virtual IDictionary<string, string> DefaultHeaders { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional default query parameters to every request.
|
||||
/// </summary>
|
||||
public virtual IDictionary<string, string> DefaultQueryParams { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to allow redirects.
|
||||
/// </summary>
|
||||
public virtual bool AllowRedirects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use a proxy.
|
||||
/// </summary>
|
||||
public virtual bool UseProxy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the proxy information.
|
||||
/// </summary>
|
||||
public virtual IWebProxy Proxy { get; set; }
|
||||
}
|
||||
}
|
||||
24
src/LinkMobility/Enums/ContentCategory.cs
Normal file
24
src/LinkMobility/Enums/ContentCategory.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Content categories as defined by <see href="https://developer.linkmobility.eu/sms-api/rest-api#operation/sendUsingPOST">Link Mobility</see>.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum ContentCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents content that is classified as informational.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "informational")]
|
||||
Informational = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Represents content that is classified as an advertisement.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "advertisement")]
|
||||
Advertisement = 2,
|
||||
}
|
||||
}
|
||||
18
src/LinkMobility/Enums/MessageType.cs
Normal file
18
src/LinkMobility/Enums/MessageType.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the message type.
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
/// <summary>
|
||||
/// The message is sent as defined in the account settings.
|
||||
/// </summary>
|
||||
Default = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The message is sent as voice call.
|
||||
/// </summary>
|
||||
Voice = 2,
|
||||
}
|
||||
}
|
||||
36
src/LinkMobility/Enums/SenderAddressType.cs
Normal file
36
src/LinkMobility/Enums/SenderAddressType.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the type of sender address.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum SenderAddressType
|
||||
{
|
||||
/// <summary>
|
||||
/// National number.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "national")]
|
||||
National = 1,
|
||||
|
||||
/// <summary>
|
||||
/// International number.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "international")]
|
||||
International = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Alphanumeric sender ID.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "alphanumeric")]
|
||||
Alphanumeric = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Shortcode.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "shortcode")]
|
||||
Shortcode = 4,
|
||||
}
|
||||
}
|
||||
149
src/LinkMobility/Enums/StatusCodes.cs
Normal file
149
src/LinkMobility/Enums/StatusCodes.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom status codes as defined by <see href="https://developer.linkmobility.eu/sms-api/rest-api#section/Status-codes">Link Mobility</see>.
|
||||
/// </summary>
|
||||
public enum StatusCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// Request accepted, Message(s) sent.
|
||||
/// </summary>
|
||||
Ok = 2000,
|
||||
|
||||
/// <summary>
|
||||
/// Request accepted, Message(s) queued.
|
||||
/// </summary>
|
||||
OkQueued = 2001,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid Credentials. Inactive account or customer.
|
||||
/// </summary>
|
||||
InvalidCredentials = 4001,
|
||||
|
||||
/// <summary>
|
||||
/// One or more recipients are not in the correct format or are containing invalid MSISDNs.
|
||||
/// </summary>
|
||||
InvalidRecipient = 4002,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid Sender. Sender address or type is invalid.
|
||||
/// </summary>
|
||||
InvalidSender = 4003,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid messageType.
|
||||
/// </summary>
|
||||
InvalidMessageType = 4004,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid clientMessageId.
|
||||
/// </summary>
|
||||
InvalidMessageId = 4008,
|
||||
|
||||
/// <summary>
|
||||
/// Message text (messageContent) is invalid.
|
||||
/// </summary>
|
||||
InvalidText = 4009,
|
||||
|
||||
/// <summary>
|
||||
/// Message limit is reached.
|
||||
/// </summary>
|
||||
MessageLimitExceeded = 4013,
|
||||
|
||||
/// <summary>
|
||||
/// Sender IP address is not authorized.
|
||||
/// </summary>
|
||||
UnauthorizedIp = 4014,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid Message Priority.
|
||||
/// </summary>
|
||||
InvalidMessagePriority = 4015,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid notification callback url.
|
||||
/// </summary>
|
||||
InvalidCallbackAddress = 4016,
|
||||
|
||||
/// <summary>
|
||||
/// A required parameter was not given. The parameter name is shown in the status message.
|
||||
/// </summary>
|
||||
ParameterMissing = 4019,
|
||||
|
||||
/// <summary>
|
||||
/// Account is invalid.
|
||||
/// </summary>
|
||||
InvalidAccount = 4021,
|
||||
|
||||
/// <summary>
|
||||
/// Access to the API was denied.
|
||||
/// </summary>
|
||||
AccessDenied = 4022,
|
||||
|
||||
/// <summary>
|
||||
/// Request limit exceeded for this IP address.
|
||||
/// </summary>
|
||||
ThrottlingSpammingIp = 4023,
|
||||
|
||||
/// <summary>
|
||||
/// Transfer rate for immediate transmissions exceeded.
|
||||
/// Too many recipients in this request (1000).
|
||||
/// </summary>
|
||||
ThrottlingTooManyRecipients = 4025,
|
||||
|
||||
/// <summary>
|
||||
/// The message content results in too many (automatically generated) sms segments.
|
||||
/// </summary>
|
||||
MaxSmsPerMessageExceeded = 4026,
|
||||
|
||||
/// <summary>
|
||||
/// A message content segment is invalid
|
||||
/// </summary>
|
||||
InvalidMessageSegment = 4027,
|
||||
|
||||
/// <summary>
|
||||
/// Recipients not allowed.
|
||||
/// </summary>
|
||||
RecipientsNotAllowed = 4029,
|
||||
|
||||
/// <summary>
|
||||
/// All recipients blacklisted.
|
||||
/// </summary>
|
||||
RecipientBlacklisted = 4030,
|
||||
|
||||
/// <summary>
|
||||
/// Not allowed to send sms messages.
|
||||
/// </summary>
|
||||
SmsDisabled = 4035,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid content category.
|
||||
/// </summary>
|
||||
InvalidContentCategory = 4040,
|
||||
|
||||
/// <summary>
|
||||
/// Invalid validity periode.
|
||||
/// </summary>
|
||||
InvalidValidityPeriode = 4041,
|
||||
|
||||
/// <summary>
|
||||
/// All of the recipients are blocked by quality rating.
|
||||
/// </summary>
|
||||
RecipientsBlockedByQualityRating = 4042,
|
||||
|
||||
/// <summary>
|
||||
/// All of the recipients are blocked by spamcheck.
|
||||
/// </summary>
|
||||
RecipientsBlockedBySpamcheck = 4043,
|
||||
|
||||
/// <summary>
|
||||
/// Internal error.
|
||||
/// </summary>
|
||||
InternalError = 5000,
|
||||
|
||||
/// <summary>
|
||||
/// Service unavailable.
|
||||
/// </summary>
|
||||
ServiceUnavailable = 5003
|
||||
}
|
||||
}
|
||||
19
src/LinkMobility/ILinkMobilityClient.cs
Normal file
19
src/LinkMobility/ILinkMobilityClient.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Net.Api.LinkMobility.Requests;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the interface for a Link Mobility API client.
|
||||
/// </summary>
|
||||
public interface ILinkMobilityClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a text message to a list of recipients.
|
||||
/// </summary>
|
||||
/// <param name="request">The request data.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||
Task<SendTextMessageResponse> SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
33
src/LinkMobility/LinkMobility.csproj
Normal file
33
src/LinkMobility/LinkMobility.csproj
Normal file
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<PackageId>AMWD.Net.Api.LinkMobility</PackageId>
|
||||
<PackageTags>link mobility api</PackageTags>
|
||||
|
||||
<AssemblyName>amwd-linkmobility</AssemblyName>
|
||||
<RootNamespace>AMWD.Net.Api.LinkMobility</RootNamespace>
|
||||
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>false</EmbedUntrackedSources>
|
||||
|
||||
<PackageIcon>package-icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
|
||||
<Product>LinkMobility API</Product>
|
||||
<Description>Implementation of the Link Mobility REST API</Description>
|
||||
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../../package-icon.png" Pack="true" PackagePath="/" />
|
||||
<None Include="../../LICENSE.txt" Pack="true" PackagePath="/" />
|
||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
43
src/LinkMobility/LinkMobilityClient.Messaging.cs
Normal file
43
src/LinkMobility/LinkMobilityClient.Messaging.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Net.Api.LinkMobility.Requests;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
public partial class LinkMobilityClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a text message to a list of recipients.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||
public Task<SendTextMessageResponse> SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.MessageContent))
|
||||
throw new ArgumentException("A message must be provided.", nameof(request.MessageContent));
|
||||
|
||||
if (request.RecipientAddressList == null || request.RecipientAddressList.Count == 0)
|
||||
throw new ArgumentException("At least one recipient must be provided.", nameof(request.RecipientAddressList));
|
||||
|
||||
foreach (string recipient in request.RecipientAddressList)
|
||||
{
|
||||
if (!IsValidMSISDN(recipient))
|
||||
throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(request.RecipientAddressList));
|
||||
}
|
||||
|
||||
return PostAsync<SendTextMessageResponse, SendTextMessageRequest>("/smsmessaging/text", request, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static bool IsValidMSISDN(string msisdn)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(msisdn))
|
||||
return false;
|
||||
|
||||
return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/LinkMobility/LinkMobilityClient.cs
Normal file
223
src/LinkMobility/LinkMobilityClient.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a client for interacting with the Link Mobility API.
|
||||
/// </summary>
|
||||
public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerSettings _jsonSerializerSettings = new()
|
||||
{
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
Formatting = Formatting.None,
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
private readonly ClientOptions _clientOptions;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinkMobilityClient" /> class using basic authentication.
|
||||
/// </summary>
|
||||
/// <param name="username">The username used for basic authentication.</param>
|
||||
/// <param name="password">The password used for basic authentication.</param>
|
||||
/// <param name="clientOptions">Optional configuration settings for the client.</param>
|
||||
public LinkMobilityClient(string username, string password, ClientOptions? clientOptions = null)
|
||||
: this(new BasicAuthentication(username, password), clientOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinkMobilityClient"/> class using access token authentication.
|
||||
/// </summary>
|
||||
/// <param name="token">The bearer token used for authentication.</param>
|
||||
/// <param name="clientOptions">Optional configuration settings for the client.</param>
|
||||
public LinkMobilityClient(string token, ClientOptions? clientOptions = null)
|
||||
: this(new AccessTokenAuthentication(token), clientOptions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinkMobilityClient"/> class using authentication and optional client
|
||||
/// configuration.
|
||||
/// </summary>
|
||||
/// <param name="authentication">The authentication mechanism used to authorize requests.</param>
|
||||
/// <param name="clientOptions">Optional client configuration settings.</param>
|
||||
public LinkMobilityClient(IAuthentication authentication, ClientOptions? clientOptions = null)
|
||||
{
|
||||
if (authentication == null)
|
||||
throw new ArgumentNullException(nameof(authentication));
|
||||
|
||||
_clientOptions = clientOptions ?? new ClientOptions();
|
||||
ValidateClientOptions();
|
||||
|
||||
_httpClient = CreateHttpClient();
|
||||
authentication.AddHeader(_httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of the resources used by the <see cref="LinkMobilityClient"/> object.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_httpClient.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void ValidateClientOptions()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
|
||||
throw new ArgumentNullException(nameof(_clientOptions.BaseUrl), "BaseUrl cannot be null or whitespace.");
|
||||
|
||||
if (_clientOptions.Timeout <= TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(_clientOptions.Timeout), "Timeout must be greater than zero.");
|
||||
|
||||
if (_clientOptions.UseProxy && _clientOptions.Proxy == null)
|
||||
throw new ArgumentNullException(nameof(_clientOptions.Proxy), "Proxy cannot be null when UseProxy is true.");
|
||||
}
|
||||
|
||||
private static void ValidateRequestPath(string requestPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requestPath))
|
||||
throw new ArgumentNullException(nameof(requestPath), "The request path is required");
|
||||
|
||||
if (requestPath.Contains('?'))
|
||||
throw new ArgumentException("Query parameters are not allowed in the request path", nameof(requestPath));
|
||||
}
|
||||
|
||||
private HttpClient CreateHttpClient()
|
||||
{
|
||||
string version = GetType().Assembly
|
||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion ?? "unknown";
|
||||
|
||||
HttpMessageHandler handler;
|
||||
try
|
||||
{
|
||||
handler = new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = _clientOptions.AllowRedirects,
|
||||
UseProxy = _clientOptions.UseProxy,
|
||||
Proxy = _clientOptions.Proxy
|
||||
};
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
handler = new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = _clientOptions.AllowRedirects
|
||||
};
|
||||
}
|
||||
|
||||
string baseUrl = _clientOptions.BaseUrl.Trim().TrimEnd('/');
|
||||
|
||||
var client = new HttpClient(handler, disposeHandler: true)
|
||||
{
|
||||
BaseAddress = new Uri($"{baseUrl}/"),
|
||||
Timeout = _clientOptions.Timeout
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(nameof(LinkMobilityClient), version));
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
if (_clientOptions.DefaultHeaders.Count > 0)
|
||||
{
|
||||
foreach (var headerKvp in _clientOptions.DefaultHeaders)
|
||||
client.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
private async Task<TResponse> PostAsync<TResponse, TRequest>(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
ValidateRequestPath(requestPath);
|
||||
|
||||
string requestUrl = BuildRequestUrl(requestPath, queryParams);
|
||||
var httpContent = ConvertRequest(request);
|
||||
|
||||
var response = await _httpClient.PostAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false);
|
||||
return await GetResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null)
|
||||
{
|
||||
string path = requestPath.Trim().TrimStart('/');
|
||||
var param = new Dictionary<string, string>();
|
||||
|
||||
if (_clientOptions.DefaultQueryParams.Count > 0)
|
||||
{
|
||||
foreach (var kvp in _clientOptions.DefaultQueryParams)
|
||||
param[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
var customQueryParams = queryParams?.GetQueryParameters();
|
||||
if (customQueryParams?.Count > 0)
|
||||
{
|
||||
foreach (var kvp in customQueryParams)
|
||||
param[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
if (param.Count == 0)
|
||||
return path;
|
||||
|
||||
string queryString = string.Join("&", param.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
|
||||
return $"{path}?{queryString}";
|
||||
}
|
||||
|
||||
private static HttpContent? ConvertRequest<T>(T request)
|
||||
{
|
||||
if (request == null)
|
||||
return null;
|
||||
|
||||
if (request is HttpContent httpContent)
|
||||
return httpContent;
|
||||
|
||||
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
|
||||
return new StringContent(json, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
private static async Task<TResponse> GetResponse<TResponse>(HttpResponseMessage httpResponse, CancellationToken cancellationToken)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
string content = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
string content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
return httpResponse.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => throw new AuthenticationException($"HTTP auth missing: {httpResponse.StatusCode}"),
|
||||
HttpStatusCode.Forbidden => throw new AuthenticationException($"HTTP auth missing: {httpResponse.StatusCode}"),
|
||||
HttpStatusCode.OK =>
|
||||
JsonConvert.DeserializeObject<TResponse>(content, _jsonSerializerSettings)
|
||||
?? throw new ApplicationException("Response could not be deserialized"),
|
||||
_ => throw new ApplicationException($"Unknown HTTP response: {httpResponse.StatusCode}"),
|
||||
};
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_isDisposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/LinkMobility/LinkMobilityResponse.cs
Normal file
20
src/LinkMobility/LinkMobilityResponse.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
public class LinkMobilityResponse
|
||||
{
|
||||
[JsonProperty("clientMessageId")]
|
||||
public string ClientMessageId { get; set; }
|
||||
|
||||
[JsonProperty("smsCount")]
|
||||
public int SmsCount { get; set; }
|
||||
|
||||
[JsonProperty("statusCode")]
|
||||
public StatusCodes StatusCode { get; set; }
|
||||
|
||||
[JsonProperty("statusMessage")]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[JsonProperty("transferId")]
|
||||
public string TransferId { get; set; }
|
||||
}
|
||||
}
|
||||
12
src/LinkMobility/QueryParameters/IQueryParameter.cs
Normal file
12
src/LinkMobility/QueryParameters/IQueryParameter.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents options defined via query parameters.
|
||||
/// </summary>
|
||||
public interface IQueryParameter
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the query parameters.
|
||||
IReadOnlyDictionary<string, string> GetQueryParameters();
|
||||
}
|
||||
}
|
||||
18
src/LinkMobility/README.md
Normal file
18
src/LinkMobility/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# LinkMobility API
|
||||
|
||||
This project aims to implement the [LinkMobility REST API].
|
||||
|
||||
## Overview
|
||||
|
||||
Link Mobility is a provider for communication with customers via SMS, RCS or WhatsApp.
|
||||
|
||||
With this project the REST API of Link Mobility will be implemented.
|
||||
|
||||
- [SMS API](https://developer.linkmobility.eu/sms-api/rest-api)
|
||||
|
||||
---
|
||||
|
||||
Published under MIT License (see [**tl;dr**Legal])
|
||||
|
||||
[LinkMobility REST API]: https://developer.linkmobility.eu/
|
||||
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||
139
src/LinkMobility/Requests/SendTextMessageRequest.cs
Normal file
139
src/LinkMobility/Requests/SendTextMessageRequest.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
namespace AMWD.Net.Api.LinkMobility.Requests
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to send a text message to a list of recipients.
|
||||
/// </summary>
|
||||
public class SendTextMessageRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SendTextMessageRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="messageContent">The message.</param>
|
||||
/// <param name="recipientAddressList">The recipient list.</param>
|
||||
public SendTextMessageRequest(string messageContent, IReadOnlyCollection<string> recipientAddressList)
|
||||
{
|
||||
MessageContent = messageContent;
|
||||
RecipientAddressList = recipientAddressList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// May contain a freely definable message id.
|
||||
/// </summary>
|
||||
[JsonProperty("clientMessageId")]
|
||||
public string? ClientMessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// The content category that is used to categorize the message (used for blacklisting).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The following content categories are supported: <see cref="ContentCategory.Informational"/> or <see cref="ContentCategory.Advertisement"/>.
|
||||
/// If no content category is provided, the default setting is used (may be changed inside the web interface).
|
||||
/// </remarks>
|
||||
[JsonProperty("contentCategory")]
|
||||
public ContentCategory? ContentCategory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// Specifies the maximum number of SMS to be generated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the system generates more than this number of SMS, the status code <see cref="StatusCodes.MaxSmsPerMessageExceeded"/> is returned.
|
||||
/// The default value of this parameter is <c>0</c>.
|
||||
/// If set to <c>0</c>, no limitation is applied.
|
||||
/// </remarks>
|
||||
[JsonProperty("maxSmsPerMessage")]
|
||||
public int? MaxSmsPerMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>UTF-8</em> encoded message content.
|
||||
/// </summary>
|
||||
[JsonProperty("messageContent")]
|
||||
public string MessageContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// Specifies the message type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Allowed values are <see cref="MessageType.Default"/> and <see cref="MessageType.Voice"/>.
|
||||
/// When using the message type <see cref="MessageType.Default"/>, the outgoing message type is determined based on account settings.
|
||||
/// Using the message type <see cref="MessageType.Voice"/> triggers a voice call.
|
||||
/// </remarks>
|
||||
[JsonProperty("messageType")]
|
||||
public MessageType? MessageType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// When setting a <c>NotificationCallbackUrl</c> all delivery reports are forwarded to this URL.
|
||||
/// </summary>
|
||||
[JsonProperty("notificationCallbackUrl")]
|
||||
public string? NotificationCallbackUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// Priority of the message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Must not exceed the value configured for the account used to send the message.
|
||||
/// For more information please contact our customer service.
|
||||
/// </remarks>
|
||||
[JsonProperty("priority")]
|
||||
public int? Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of recipients (E.164 formatted <see href="https://en.wikipedia.org/wiki/MSISDN">MSISDN</see>s)
|
||||
/// to whom the message should be sent.
|
||||
/// <br/>
|
||||
/// The list of recipients may contain a maximum of <em>1000</em> entries.
|
||||
/// </summary>
|
||||
[JsonProperty("recipientAddressList")]
|
||||
public IReadOnlyCollection<string> RecipientAddressList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// <br/>
|
||||
/// <see langword="true"/>: The message is sent as flash SMS (displayed directly on the screen of the mobile phone).
|
||||
/// <br/>
|
||||
/// <see langword="false"/>: The message is sent as standard text SMS (default).
|
||||
/// </summary>
|
||||
[JsonProperty("sendAsFlashSms")]
|
||||
public bool? SendAsFlashSms { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// Address of the sender (assigned to the account) from which the message is sent.
|
||||
/// </summary>
|
||||
[JsonProperty("senderAddress")]
|
||||
public string? SenderAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// The sender address type.
|
||||
/// </summary>
|
||||
[JsonProperty("senderAddressType")]
|
||||
public SenderAddressType? SenderAddressType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// <br/>
|
||||
/// <see langword="true"/>: The transmission is only simulated, no SMS is sent.
|
||||
/// Depending on the number of recipients the status code <see cref="StatusCodes.Ok"/> or <see cref="StatusCodes.OkQueued"/> is returned.
|
||||
/// <br/>
|
||||
/// <see langword="false"/>: No simulation is done. The SMS is sent via the SMS Gateway. (default)
|
||||
/// </summary>
|
||||
[JsonProperty("test")]
|
||||
public bool? Test { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <em>Optional</em>.
|
||||
/// Specifies the validity periode (in seconds) in which the message is tried to be delivered to the recipient.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A minimum of 1 minute (<c>60</c> seconds) and a maximum of 3 days (<c>259200</c> seconds) are allowed.
|
||||
/// </remarks>
|
||||
[JsonProperty("validityPeriode")]
|
||||
public int? ValidityPeriode { get; set; }
|
||||
}
|
||||
}
|
||||
38
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
38
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace AMWD.Net.Api.LinkMobility
|
||||
{
|
||||
/// <summary>
|
||||
/// Response of a text message sent to a list of recipients.
|
||||
/// </summary>
|
||||
public class SendTextMessageResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the message id defined in the request.
|
||||
/// </summary>
|
||||
[JsonProperty("clientMessageId")]
|
||||
public string? ClientMessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual number of generated SMS.
|
||||
/// </summary>
|
||||
[JsonProperty("smsCount")]
|
||||
public int? SmsCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Status code.
|
||||
/// </summary>
|
||||
[JsonProperty("statusCode")]
|
||||
public StatusCodes? StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Description of the response status code.
|
||||
/// </summary>
|
||||
[JsonProperty("statusMessage")]
|
||||
public string? StatusMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier that is set after successful processing of the request.
|
||||
/// </summary>
|
||||
[JsonProperty("transferId")]
|
||||
public string? TransferId { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user