Skip to content

Commit 58098c8

Browse files
committed
add Sms segments
1 parent a600dc4 commit 58098c8

File tree

4 files changed

+106
-9
lines changed

4 files changed

+106
-9
lines changed

src/Arbiter.Communication.Azure/AzureSmsDeliveryService.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Globalization;
2+
13
using Arbiter.Communication.Extensions;
24
using Arbiter.Communication.Sms;
35

@@ -41,7 +43,7 @@ public AzureSmsDeliveryService(ILogger<AzureSmsDeliveryService> logger, SmsClien
4143
/// </returns>
4244
public async Task<SmsResult> Send(SmsMessage message, CancellationToken cancellationToken = default)
4345
{
44-
var truncatedMessage = message.Message.Truncate(20);
46+
var truncatedMessage = message.Message.Truncate(50);
4547
var senderNumber = message.Sender.HasValue() ? message.Sender : _options.Value.SenderNumber;
4648

4749
LogSendingSms(_logger, message.Recipient, senderNumber, truncatedMessage);
@@ -59,8 +61,9 @@ public async Task<SmsResult> Send(SmsMessage message, CancellationToken cancella
5961

6062
if (results.Value.Successful)
6163
{
62-
LogSmsSent(_logger, message.Recipient, truncatedMessage);
63-
return SmsResult.Success("SMS sent successfully.");
64+
var segments = SmsSegment.Calculate(message.Message);
65+
LogSmsSent(_logger, message.Recipient, truncatedMessage, segments);
66+
return SmsResult.Success("SMS sent successfully.", segments);
6467
}
6568

6669
LogSmsSendError(_logger, message.Recipient, truncatedMessage);
@@ -77,8 +80,8 @@ public async Task<SmsResult> Send(SmsMessage message, CancellationToken cancella
7780
[LoggerMessage(1, LogLevel.Debug, "Sending SMS to '{Recipient}' from '{Sender} with message '{Message}' using Azure Communication")]
7881
static partial void LogSendingSms(ILogger logger, string recipient, string? sender, string message);
7982

80-
[LoggerMessage(2, LogLevel.Information, "Sent SMS to '{Recipient}' with message '{Message}'")]
81-
static partial void LogSmsSent(ILogger logger, string recipient, string message);
83+
[LoggerMessage(2, LogLevel.Information, "Sent SMS to '{Recipient}' with message '{Message}' using segments {Segments}")]
84+
static partial void LogSmsSent(ILogger logger, string recipient, string message, int? segments);
8285

8386
[LoggerMessage(3, LogLevel.Error, "Error sending SMS to '{Recipient}' with message '{Message}'")]
8487
static partial void LogSmsSendError(ILogger logger, string recipient, string message);

src/Arbiter.Communication.Twilio/TwilioSmsDeliveryService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Globalization;
2+
13
using Arbiter.Communication.Extensions;
24
using Arbiter.Communication.Sms;
35

@@ -44,7 +46,7 @@ public TwilioSmsDeliveryService(ILogger<TwilioSmsDeliveryService> logger, IOptio
4446
/// </returns>
4547
public async Task<SmsResult> Send(SmsMessage message, CancellationToken cancellationToken = default)
4648
{
47-
var truncatedMessage = message.Message.Truncate(20);
49+
var truncatedMessage = message.Message.Truncate(50);
4850
var senderNumber = message.Sender.HasValue() ? message.Sender : _options.Value.SenderNumber;
4951

5052
_logger.LogDebug("Sending SMS to '{Recipient}' from '{Sender} with message '{Message}' using Twilio", message.Recipient, senderNumber, truncatedMessage);
@@ -67,7 +69,8 @@ public async Task<SmsResult> Send(SmsMessage message, CancellationToken cancella
6769
if (messageResponse.ErrorCode.HasValue)
6870
return SmsResult.Fail($"SMS send failed with status {messageResponse.Status}");
6971

70-
return SmsResult.Success("SMS sent successfully.");
72+
var segments = int.TryParse(messageResponse.NumSegments, CultureInfo.InvariantCulture, out var s) ? s : SmsSegment.Calculate(message.Message);
73+
return SmsResult.Success("SMS sent successfully.", segments);
7174
}
7275
catch (Exception ex)
7376
{

src/Arbiter.Communication/Sms/SmsResult.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ public readonly record struct SmsResult
1515
/// </summary>
1616
public string? Message { get; init; }
1717

18+
/// <summary>
19+
/// Gets the number of segments the SMS message was split into, if applicable.
20+
/// </summary>
21+
public int? Segments { get; init; }
22+
1823
/// <summary>
1924
/// Gets the exception that occurred during the SMS operation, if any.
2025
/// </summary>
@@ -24,9 +29,10 @@ public readonly record struct SmsResult
2429
/// Creates a successful <see cref="SmsResult"/> with an optional message.
2530
/// </summary>
2631
/// <param name="message">An optional message describing the success.</param>
32+
/// <param name="segments">The number of segments the SMS message was split into, if applicable.</param>
2733
/// <returns>An <see cref="SmsResult"/> indicating success.</returns>
28-
public static SmsResult Success(string? message = null)
29-
=> new() { Successful = true, Message = message };
34+
public static SmsResult Success(string? message = null, int? segments = null)
35+
=> new() { Successful = true, Message = message, Segments = segments };
3036

3137
/// <summary>
3238
/// Creates a failed <see cref="SmsResult"/> with an optional message and exception.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Ignore Spelling: Sms
2+
3+
using System.Buffers;
4+
5+
namespace Arbiter.Communication.Sms;
6+
7+
/// <summary>
8+
/// Provides utilities for calculating SMS message segments based on character encoding.
9+
/// </summary>
10+
public static class SmsSegment
11+
{
12+
// GSM-7 basic character set for O(1) lookup using SearchValues
13+
private static readonly SearchValues<char> Gsm7Characters = SearchValues.Create("@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà");
14+
15+
// GSM-7 extended character set (these require escape character, counting as 2 characters)
16+
private static readonly SearchValues<char> Gsm7Extended = SearchValues.Create("^{}\\[~]|€");
17+
18+
/// <summary>
19+
/// Calculates the number of SMS segments required to send the specified message.
20+
/// </summary>
21+
/// <param name="message">The message to calculate segments for.</param>
22+
/// <returns>
23+
/// The number of SMS segments required. Returns 0 if the message is null or empty.
24+
/// For GSM-7 encoding: 1 segment for up to 160 characters, then 153 characters per additional segment.
25+
/// For UCS-2/Unicode encoding: 1 segment for up to 70 characters, then 67 characters per additional segment.
26+
/// </returns>
27+
/// <remarks>
28+
/// Extended GSM-7 characters (^{}\\[~]|€) count as 2 characters each due to the required escape sequence.
29+
/// If any character outside the GSM-7 character set is found, UCS-2/Unicode encoding is used for the entire message.
30+
/// </remarks>
31+
public static int Calculate(ReadOnlySpan<char> message)
32+
{
33+
if (message.IsEmpty)
34+
return 0;
35+
36+
int effectiveLength = CalculateLength(message, out bool isGsm7);
37+
38+
// GSM-7 encoding; Single SMS: 160 chars, Concatenated SMS: 153 chars per segment
39+
// UCS-2/Unicode encoding; Single SMS: 70 chars, Concatenated SMS: 67 chars per segment
40+
(int single, double concatenated) = isGsm7 ? (160, 153) : (70, 67);
41+
42+
if (effectiveLength <= single)
43+
return 1;
44+
45+
return (int)Math.Ceiling(effectiveLength / concatenated);
46+
}
47+
48+
/// <summary>
49+
/// Calculates the effective length of the message and determines the encoding type.
50+
/// </summary>
51+
/// <param name="text">The text to calculate the length for.</param>
52+
/// <param name="isGsm7">
53+
/// When this method returns, contains <c>true</c> if the message can be encoded using GSM-7;
54+
/// otherwise, <c>false</c> if UCS-2/Unicode encoding is required.
55+
/// </param>
56+
/// <returns>
57+
/// The effective character length of the message. For GSM-7 encoding, extended characters count as 2.
58+
/// For UCS-2/Unicode encoding, returns the actual string length.
59+
/// </returns>
60+
private static int CalculateLength(ReadOnlySpan<char> text, out bool isGsm7)
61+
{
62+
isGsm7 = true;
63+
int length = 0;
64+
65+
foreach (char c in text)
66+
{
67+
if (Gsm7Characters.Contains(c))
68+
{
69+
length++;
70+
}
71+
else if (Gsm7Extended.Contains(c))
72+
{
73+
length += 2; // Extended characters require escape sequence
74+
}
75+
else
76+
{
77+
// Non-GSM7 character found, must use UCS-2 encoding
78+
isGsm7 = false;
79+
return text.Length;
80+
}
81+
}
82+
83+
return length;
84+
}
85+
}

0 commit comments

Comments
 (0)