Skip to content

Commit 45fe40f

Browse files
authored
Merge pull request #21 from teesofttech/feature/7-messaging-api
Implement Messaging API send support
2 parents ee5a006 + 6e216e2 commit 45fe40f

16 files changed

Lines changed: 478 additions & 4 deletions

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ builder.Services.AddTermii(options =>
3535
});
3636
```
3737

38+
Send a message:
39+
40+
```csharp
41+
var response = await client.Messaging.SendAsync(new SendMessageRequest
42+
{
43+
To = "2348012345678",
44+
From = "Termii",
45+
Sms = "Hello from .NET",
46+
Channel = TermiiMessageChannel.Generic,
47+
Type = TermiiMessageType.Plain
48+
});
49+
```
50+
3851
## Examples
3952

4053
Run the examples project after setting your Termii API key:

docs/API_COVERAGE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The Termii docs describe a REST/JSON API and state that each account has its own
3030
| Client configuration, `HttpClient` pipeline, API-key injection, DI registration | Implemented | #2, PR #17 |
3131
| API coverage matrix | Implemented | #14, PR #18 |
3232
| Error handling and request validation | Implemented | #6, PR #19 |
33-
| Reusable fake HTTP test helpers and opt-in live test conventions | In progress | #10 |
33+
| Reusable fake HTTP test helpers and opt-in live test conventions | Implemented | #10, PR #20 |
3434
| README endpoint examples | Planned | #12 |
3535
| CI build/test/package validation | Planned | #11 |
3636
| NuGet package metadata and publishing workflow | Planned | #8 |
@@ -42,9 +42,9 @@ The Termii docs describe a REST/JSON API and state that each account has its own
4242
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
4343
| Messaging | Fetch sender IDs | GET | `/api/sender-id` | Query | `TermiiClient.SenderIds` | Planned | #3 | Planned unit + optional integration |
4444
| Messaging | Request sender ID | POST | `/api/sender-id/request` | JSON body | `TermiiClient.SenderIds` | Planned | #3 | Planned unit + optional integration |
45-
| Messaging | Send SMS/channel message | POST | `/api/sms/send` | JSON body | `TermiiClient.Messaging` | Planned | #7 | Planned unit + optional integration |
46-
| Messaging | Send WhatsApp conversational message | POST | `/api/sms/send` | JSON body | `TermiiClient.Messaging` | Planned | #7 | Planned unit + optional integration |
47-
| Messaging | Send bulk message | POST | `/api/sms/send/bulk` | JSON body | `TermiiClient.Messaging` | Planned | #7 | Planned unit + optional integration |
45+
| Messaging | Send SMS/channel message | POST | `/api/sms/send` | JSON body | `TermiiClient.Messaging` | In progress | #7 | Unit tests added |
46+
| Messaging | Send WhatsApp conversational message | POST | `/api/sms/send` | JSON body | `TermiiClient.Messaging` | In progress | #7 | Unit tests added |
47+
| Messaging | Send bulk message | POST | `/api/sms/send/bulk` | JSON body | `TermiiClient.Messaging` | In progress | #7 | Unit tests added |
4848
| Messaging | Send via Number API | POST | `/api/sms/number/send` | Query or JSON body, docs/examples vary | `TermiiClient.Messaging` or `TermiiClient.Numbers` | Planned | #5 | Planned unit + optional integration |
4949
| Token | Send OTP token | POST | `/api/sms/otp/send` | JSON body | `TermiiClient.Token` | Planned | #4 | Planned unit + optional integration |
5050
| Token | Verify OTP token | POST | `/api/sms/otp/verify` | JSON body | `TermiiClient.Token` | Planned | #4 | Planned unit + optional integration |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Termii;
2+
3+
public interface ITermiiMessagingClient
4+
{
5+
Task<TermiiMessageResponse> SendAsync(
6+
SendMessageRequest request,
7+
CancellationToken cancellationToken = default);
8+
9+
Task<TermiiMessageResponse> SendBulkAsync(
10+
SendBulkMessageRequest request,
11+
CancellationToken cancellationToken = default);
12+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class SendBulkMessageRequest
6+
{
7+
[JsonPropertyName("to")]
8+
public IReadOnlyCollection<string> To { get; set; } = Array.Empty<string>();
9+
10+
[JsonPropertyName("from")]
11+
public string From { get; set; } = string.Empty;
12+
13+
[JsonPropertyName("sms")]
14+
public string Sms { get; set; } = string.Empty;
15+
16+
[JsonIgnore]
17+
public TermiiMessageType Type { get; set; } = TermiiMessageType.Plain;
18+
19+
[JsonIgnore]
20+
public TermiiMessageChannel Channel { get; set; } = TermiiMessageChannel.Generic;
21+
22+
internal void Validate()
23+
{
24+
if (To is null || To.Count == 0)
25+
{
26+
throw new ArgumentException("At least one recipient phone number is required.", nameof(To));
27+
}
28+
29+
foreach (var recipient in To)
30+
{
31+
TermiiRequestValidation.Required(recipient, nameof(To));
32+
}
33+
34+
TermiiRequestValidation.Required(From, nameof(From));
35+
TermiiRequestValidation.Required(Sms, nameof(Sms));
36+
}
37+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class SendMessageRequest
6+
{
7+
[JsonPropertyName("to")]
8+
public string To { get; set; } = string.Empty;
9+
10+
[JsonPropertyName("from")]
11+
public string From { get; set; } = string.Empty;
12+
13+
[JsonPropertyName("sms")]
14+
public string Sms { get; set; } = string.Empty;
15+
16+
[JsonIgnore]
17+
public TermiiMessageType Type { get; set; } = TermiiMessageType.Plain;
18+
19+
[JsonIgnore]
20+
public TermiiMessageChannel Channel { get; set; } = TermiiMessageChannel.Generic;
21+
22+
[JsonPropertyName("media")]
23+
public TermiiMessageMedia? Media { get; set; }
24+
25+
internal void Validate()
26+
{
27+
TermiiRequestValidation.Required(To, nameof(To));
28+
TermiiRequestValidation.Required(From, nameof(From));
29+
TermiiRequestValidation.Required(Sms, nameof(Sms));
30+
}
31+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Termii;
2+
3+
public enum TermiiMessageChannel
4+
{
5+
Generic,
6+
Dnd,
7+
WhatsApp,
8+
Voice,
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class TermiiMessageMedia
6+
{
7+
[JsonPropertyName("url")]
8+
public string Url { get; set; } = string.Empty;
9+
10+
[JsonPropertyName("caption")]
11+
public string? Caption { get; set; }
12+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
public sealed class TermiiMessageResponse
6+
{
7+
[JsonPropertyName("code")]
8+
public string? Code { get; set; }
9+
10+
[JsonPropertyName("balance")]
11+
public decimal? Balance { get; set; }
12+
13+
[JsonPropertyName("message_id")]
14+
public string? MessageId { get; set; }
15+
16+
[JsonPropertyName("message")]
17+
public string? Message { get; set; }
18+
19+
[JsonPropertyName("user")]
20+
public string? User { get; set; }
21+
22+
[JsonPropertyName("message_id_str")]
23+
public string? MessageIdString { get; set; }
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Termii;
2+
3+
public enum TermiiMessageType
4+
{
5+
Plain,
6+
Unicode,
7+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Termii;
4+
5+
internal sealed class TermiiMessagingClient : ITermiiMessagingClient
6+
{
7+
private readonly TermiiJsonHttpPipeline _pipeline;
8+
9+
public TermiiMessagingClient(TermiiJsonHttpPipeline pipeline)
10+
{
11+
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
12+
}
13+
14+
public Task<TermiiMessageResponse> SendAsync(
15+
SendMessageRequest request,
16+
CancellationToken cancellationToken = default)
17+
{
18+
if (request is null)
19+
{
20+
throw new ArgumentNullException(nameof(request));
21+
}
22+
23+
request.Validate();
24+
25+
return _pipeline.SendJsonAsync<TermiiMessageResponse>(
26+
HttpMethod.Post,
27+
"/api/sms/send",
28+
new SendMessagePayload(request),
29+
TermiiAuthenticationLocation.Body,
30+
cancellationToken);
31+
}
32+
33+
public Task<TermiiMessageResponse> SendBulkAsync(
34+
SendBulkMessageRequest request,
35+
CancellationToken cancellationToken = default)
36+
{
37+
if (request is null)
38+
{
39+
throw new ArgumentNullException(nameof(request));
40+
}
41+
42+
request.Validate();
43+
44+
return _pipeline.SendJsonAsync<TermiiMessageResponse>(
45+
HttpMethod.Post,
46+
"/api/sms/send/bulk",
47+
new SendBulkMessagePayload(request),
48+
TermiiAuthenticationLocation.Body,
49+
cancellationToken);
50+
}
51+
52+
private sealed class SendMessagePayload
53+
{
54+
public SendMessagePayload(SendMessageRequest request)
55+
{
56+
To = request.To;
57+
From = request.From;
58+
Sms = request.Sms;
59+
Type = request.Type.ToWireValue();
60+
Channel = request.Channel.ToWireValue();
61+
Media = request.Media;
62+
}
63+
64+
[JsonPropertyName("to")]
65+
public string To { get; }
66+
67+
[JsonPropertyName("from")]
68+
public string From { get; }
69+
70+
[JsonPropertyName("sms")]
71+
public string Sms { get; }
72+
73+
[JsonPropertyName("type")]
74+
public string Type { get; }
75+
76+
[JsonPropertyName("channel")]
77+
public string Channel { get; }
78+
79+
[JsonPropertyName("media")]
80+
public TermiiMessageMedia? Media { get; }
81+
}
82+
83+
private sealed class SendBulkMessagePayload
84+
{
85+
public SendBulkMessagePayload(SendBulkMessageRequest request)
86+
{
87+
To = request.To;
88+
From = request.From;
89+
Sms = request.Sms;
90+
Type = request.Type.ToWireValue();
91+
Channel = request.Channel.ToWireValue();
92+
}
93+
94+
[JsonPropertyName("to")]
95+
public IReadOnlyCollection<string> To { get; }
96+
97+
[JsonPropertyName("from")]
98+
public string From { get; }
99+
100+
[JsonPropertyName("sms")]
101+
public string Sms { get; }
102+
103+
[JsonPropertyName("type")]
104+
public string Type { get; }
105+
106+
[JsonPropertyName("channel")]
107+
public string Channel { get; }
108+
}
109+
}

0 commit comments

Comments
 (0)