Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion plugins/OnlineSales.Plugin.Sms/Configuration/PluginSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class PluginConfig

public GatewaysConfig SmsGateways { get; set; } = new GatewaysConfig();

public OtpGatewaysConfig OtpGateways { get; set; } = new OtpGatewaysConfig();

public List<CountryGatewayConfig> SmsCountryGateways { get; set; } = new List<CountryGatewayConfig>();
}

Expand All @@ -26,6 +28,8 @@ public class GatewaysConfig
public NotifyLkConfig NotifyLk { get; set; } = new NotifyLkConfig();

public TwilioConfig Twilio { get; set; } = new TwilioConfig();

public WhatsAppConfig WhatsApp { get; set; } = new WhatsAppConfig();
}

public class CountryGatewayConfig
Expand Down Expand Up @@ -99,4 +103,52 @@ public class TwilioConfig
public string AuthToken { get; set; } = string.Empty;

public string SenderId { get; set; } = string.Empty;
}
}

public class WhatsAppConfig
{
public Uri ApiUrl { get; set; } = new Uri("https://graph.facebook.com/");

public string ApiVersion { get; set; } = string.Empty;

public string AuthToken { get; set; } = string.Empty;

public string BusinessPhoneId { get; set; } = string.Empty;

public bool EnableLinkPreviewByDefault { get; set; } = true;
}

public class OtpGatewaysConfig
{
public TelegramConfig Telegram { get; set; } = new TelegramConfig();

public WhatsAppOtpConfig WhatsApp { get; set; } = new WhatsAppOtpConfig();
}

public class TelegramConfig
{
public Uri ApiUrl { get; set; } = new Uri("https://gatewayapi.telegram.org/");

public string AuthToken { get; set; } = string.Empty;
}

public class WhatsAppOtpConfig
{
public WhatsAppTemplateMessageConfig Default { get; set; } = new WhatsAppTemplateMessageConfig();

public Dictionary<string, WhatsAppTemplateMessageConfig> Language { get; set; } = new Dictionary<string, WhatsAppTemplateMessageConfig>();

public WhatsAppTemplateMessageConfig GetByLanguage(string? language)
{
return language != null && Language.TryGetValue(language, out var config)
? config
: Default;
}
}

public class WhatsAppTemplateMessageConfig
{
public string TemplateName { get; set; } = string.Empty;

public string LanguageCode { get; set; } = string.Empty;
}
68 changes: 12 additions & 56 deletions plugins/OnlineSales.Plugin.Sms/Controllers/MessagesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,89 +5,45 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OnlineSales.Exceptions;
using OnlineSales.Plugin.Sms.Data;
using OnlineSales.Plugin.Sms.DTOs;
using OnlineSales.Plugin.Sms.Entities;
using PhoneNumbers;
using Serilog;

namespace OnlineSales.Plugin.Sms.Controllers;

[Route("api/messages")]
public class MessagesController : Controller
public class MessagesController : MessagesControllerBase
{
private readonly ISmsService smsService;
private readonly PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.GetInstance();
private readonly SmsDbContext dbContext;

public MessagesController(SmsDbContext dbContext, ISmsService smsService)
: base(dbContext)
{
this.dbContext = dbContext;
this.smsService = smsService;
}

[HttpPost]
[Route("sms")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> SendSms(
[FromBody] SmsDetailsDto smsDetails,
[FromHeader(Name = "Authentication")] string accessToken)
{
try
{
if (accessToken == null || accessToken.Replace("Bearer ", string.Empty) != SmsPlugin.Configuration.SmsAccessKey)
{
return new UnauthorizedResult();
}

var recipient = string.Empty;

try
{
var phoneNumber = phoneNumberUtil.Parse(smsDetails.Recipient, string.Empty);

recipient = phoneNumberUtil.Format(phoneNumber, PhoneNumberFormat.E164);
}
catch (NumberParseException npex)
{
ModelState.AddModelError(npex.ErrorType.ToString(), npex.Message);
}

if (!ModelState.IsValid)
{
throw new InvalidModelStateException(ModelState);
}

var smsLog = new SmsLog
{
Sender = smsService.GetSender(recipient),
Recipient = smsDetails.Recipient,
Message = smsDetails.Message,
Status = SmsLog.SmsStatus.NotSent,
};

dbContext.SmsLogs!.Add(smsLog);
await dbContext.SaveChangesAsync();

await smsService.SendAsync(recipient, smsDetails.Message);

smsLog.Status = SmsLog.SmsStatus.Sent;
return await SendMessage(accessToken, smsDetails.Recipient, smsDetails.Message);
}

return Ok();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to send SMS message to {0}: {1}", smsDetails.Recipient, smsDetails.Message);
protected override string GetSender(string recipient)
{
return smsService.GetSender(recipient);
}

throw;
}
finally
{
await dbContext.SaveChangesAsync();
}
protected override async Task SendMessage(string recipient, string message)
{
await smsService.SendAsync(recipient, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// <copyright file="MessagesControllerBase.cs" company="WavePoint Co. Ltd.">
// Licensed under the MIT license. See LICENSE file in the samples root for full license information.
// </copyright>

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using OnlineSales.Entities;
using OnlineSales.Exceptions;
using OnlineSales.Plugin.Sms.Data;
using OnlineSales.Plugin.Sms.Entities;
using PhoneNumbers;
using Serilog;

namespace OnlineSales.Plugin.Sms.Controllers;

public abstract class MessagesControllerBase : Controller
{
private readonly PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.GetInstance();
private readonly SmsDbContext dbContext;

protected MessagesControllerBase(SmsDbContext dbContext)
{
this.dbContext = dbContext;
}

protected async Task<ActionResult> SendMessage(string accessToken, string phone, string message)
{
try
{
if (accessToken == null || accessToken.Replace("Bearer ", string.Empty) != SmsPlugin.Configuration.SmsAccessKey)
{
return new UnauthorizedResult();
}

var recipient = GetRecipient(phone);
var sender = GetSender(recipient);
var smsLog = await AddLog(recipient, sender, message);

await SendMessage(recipient, message);
smsLog.Status = SmsLog.SmsStatus.Sent;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to send message to {0}: {1}", phone, message);
throw;
}
finally
{
await dbContext.SaveChangesAsync();
}

return Ok();
}

protected abstract string GetSender(string recipient);

protected abstract Task SendMessage(string recipient, string message);

private string GetRecipient(string phone)
{
var recipient = string.Empty;
try
{
var phoneNumber = phoneNumberUtil.Parse(phone, string.Empty);

recipient = phoneNumberUtil.Format(phoneNumber, PhoneNumberFormat.E164);
}
catch (NumberParseException npex)
{
ModelState.AddModelError(npex.ErrorType.ToString(), npex.Message);
}

if (!ModelState.IsValid)
{
throw new InvalidModelStateException(ModelState);
}

return recipient;
}

private async Task<SmsLog> AddLog(string recipient, string sender, string message)
{
var smsLog = new SmsLog
{
Sender = sender,
Recipient = recipient,
Message = message,
Status = SmsLog.SmsStatus.NotSent,
};

dbContext.SmsLogs!.Add(smsLog);
await dbContext.SaveChangesAsync();

return smsLog;
}
}
51 changes: 51 additions & 0 deletions plugins/OnlineSales.Plugin.Sms/Controllers/OTPController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// <copyright file="OTPController.cs" company="WavePoint Co. Ltd.">
// Licensed under the MIT license. See LICENSE file in the samples root for full license information.
// </copyright>

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using OnlineSales.Plugin.Sms.Data;
using OnlineSales.Plugin.Sms.DTOs;
using Serilog;

namespace OnlineSales.Plugin.Sms.Controllers;

[Route("api/otp")]
public class OtpController : MessagesControllerBase
{
private readonly IOtpService otpService;

private string language = string.Empty;

public OtpController(SmsDbContext dbContext, IOtpService otpService)
: base(dbContext)
{
this.otpService = otpService;
}

[HttpPost]
[Route("otp")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
public async Task<ActionResult> SendOtp(
[FromBody] OtpDetailsDto otpDetails,
[FromHeader(Name = "Authentication")] string accessToken)
{
language = otpDetails.Language;
return await SendMessage(accessToken, otpDetails.Recipient, otpDetails.OtpCode);
}

protected override string GetSender(string recipient)
{
return otpService.GetSender();
}

protected override async Task SendMessage(string recipient, string message)
{
await otpService.SendOtpAsync(recipient, language, message);
}
}
22 changes: 22 additions & 0 deletions plugins/OnlineSales.Plugin.Sms/DTOs/OtpDetailsDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// <copyright file="OtpDetailsDto.cs" company="WavePoint Co. Ltd.">
// Licensed under the MIT license. See LICENSE file in the samples root for full license information.
// </copyright>

using System.ComponentModel.DataAnnotations;

namespace OnlineSales.Plugin.Sms.DTOs;

public class OtpDetailsDto
{
[Required]
required public string Recipient { get; set; }

[Required]
required public string Language { get; set; }

[Required]
[MinLength(4)]
[MaxLength(8)]
[RegularExpression(@"^\d+$", ErrorMessage = "OTP code can contain digits only")]
required public string OtpCode { get; set; }
}
43 changes: 43 additions & 0 deletions plugins/OnlineSales.Plugin.Sms/DTOs/TelegramServiceDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// <copyright file="TelegramServiceDto.cs" company="WavePoint Co. Ltd.">
// Licensed under the MIT license. See LICENSE file in the samples root for full license information.
// </copyright>

namespace OnlineSales.Plugin.Sms.DTOs;

[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "This is the Telegram name convinion")]
public class TelegramCheckSendAbilityDto
{
required public string phone_number { get; set; }
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "This is the Telegram name convinion")]
public class TelegramSendVerificationMessageDto
{
required public string phone_number { get; set; }

required public string request_id { get; set; }

required public string code { get; set; }
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "This is the Telegram name convinion")]
public class TelegramResponseDto
{
public bool ok { get; set; }

public string? error { get; set; }

public TelegramRequestStatusDto? result { get; set; }
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "This is the Telegram name convinion")]
public class TelegramRequestStatusDto
{
public string request_id { get; set; } = string.Empty; // Unique identifier of the verification request.

public string phone_number { get; set; } = string.Empty; // The phone number to which the verification code was sent, in the E.164 format.

public float request_cost { get; set; } // Total request cost incurred by either checkSendAbility or sendVerificationMessage.

public float? remaining_balance { get; set; } // Optional. Remaining balance in credits. Returned only in response to a request that incurs a charge.
}
Loading