From 8bef4fe09070ec5209cd7c207fe114b3881f5220 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 27 Nov 2023 10:24:28 -0800 Subject: [PATCH 01/10] Register SMS providers using Keyed services and introduce ISmsService --- .../Dependencies.AspNetCore.props | 1 + .../OrchardCore.Sms/Activities/SmsTask.cs | 12 +-- .../Drivers/SmsSettingsDisplayDriver.cs | 35 ++------ .../Controllers/SmsAuthenticatorController.cs | 11 ++- .../ISmsProvider.cs | 2 +- .../ISmsService.cs | 13 +++ .../SmsProviderOptions.cs | 84 ------------------- .../OrchardCore.Sms.Core.csproj | 1 + .../ServiceCollectionExtensions.cs | 44 ++-------- .../Services/DefaultSmsProvider.cs | 21 ----- .../Services/SmsNotificationProvider.cs | 9 +- .../Services/SmsService.cs | 49 +++++++++++ .../Services/TwilioSmsProvider.cs | 13 ++- .../OrchardCore.Users.Core.csproj | 9 +- src/docs/reference/modules/Sms/README.md | 31 +++++++ src/docs/releases/1.8.0.md | 4 + 16 files changed, 141 insertions(+), 198 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs delete mode 100644 src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs delete mode 100644 src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProvider.cs create mode 100644 src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs diff --git a/src/OrchardCore.Build/Dependencies.AspNetCore.props b/src/OrchardCore.Build/Dependencies.AspNetCore.props index d5679ce059e..7572e47408f 100644 --- a/src/OrchardCore.Build/Dependencies.AspNetCore.props +++ b/src/OrchardCore.Build/Dependencies.AspNetCore.props @@ -43,6 +43,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Activities/SmsTask.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Activities/SmsTask.cs index 9c227074be8..f1dee6876be 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Activities/SmsTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Activities/SmsTask.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.Extensions.Localization; using OrchardCore.Workflows.Abstractions.Models; @@ -11,21 +10,18 @@ namespace OrchardCore.Sms.Activities; public class SmsTask : TaskActivity { - private readonly ISmsProvider _smsProvider; + private readonly ISmsService _smsService; private readonly IWorkflowExpressionEvaluator _expressionEvaluator; - private readonly HtmlEncoder _htmlEncoder; protected readonly IStringLocalizer S; public SmsTask( - ISmsProvider smsProvider, + ISmsService smsService, IWorkflowExpressionEvaluator expressionEvaluator, - HtmlEncoder htmlEncoder, IStringLocalizer stringLocalizer ) { - _smsProvider = smsProvider; + _smsService = smsService; _expressionEvaluator = expressionEvaluator; - _htmlEncoder = htmlEncoder; S = stringLocalizer; } @@ -58,7 +54,7 @@ public override async Task ExecuteAsync(WorkflowExecuti Body = await _expressionEvaluator.EvaluateAsync(Body, workflowContext, null), }; - var result = await _smsProvider.SendAsync(message); + var result = await _smsService.SendAsync(message); workflowContext.LastResult = result; diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs index a5f12d4c652..a66c023162e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs @@ -5,13 +5,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; -using OrchardCore.Environment.Shell.Builders; using OrchardCore.Settings; using OrchardCore.Sms.ViewModels; @@ -22,27 +19,21 @@ public class SmsSettingsDisplayDriver : SectionDisplayDriver private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; private readonly IShellHost _shellHost; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; private readonly ShellSettings _shellSettings; - private readonly SmsProviderOptions _smsProviderOptions; + private readonly IDictionary _smsProviders; public SmsSettingsDisplayDriver( IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService, - IOptions smsProviderOptions, IShellHost shellHost, - ILogger logger, - IServiceProvider serviceProvider, + IDictionary smsProviders, ShellSettings shellSettings) { _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; _shellHost = shellHost; - _logger = logger; - _serviceProvider = serviceProvider; + _smsProviders = smsProviders; _shellSettings = shellSettings; - _smsProviderOptions = smsProviderOptions.Value; } public override async Task EditAsync(SmsSettings settings, BuildEditorContext context) @@ -95,23 +86,15 @@ private SelectListItem[] GetProviders() { var items = new List(); - foreach (var providerPair in _smsProviderOptions.Providers) + foreach (var provider in _smsProviders) { - try - { - var provider = _serviceProvider.CreateInstance(providerPair.Value); - - items.Add(new SelectListItem(provider.Name, providerPair.Key)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to resolve an SMS Provider with the technical name '{technicalName}'.", providerPair.Key); - } + items.Add(new SelectListItem(provider.Value.Name, provider.Key)); } - _providers = items - .OrderBy(item => item.Text) - .ToArray(); + _providers = + [ + .. items.OrderBy(item => item.Text), + ]; } return _providers; diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs index 02b8b78e78a..4986d47d4f7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/SmsAuthenticatorController.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Options; using OrchardCore.Admin; using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Entities; using OrchardCore.Liquid; using OrchardCore.Modules; using OrchardCore.Settings; @@ -30,7 +29,7 @@ namespace OrchardCore.Users.Controllers; public class SmsAuthenticatorController : TwoFactorAuthenticationBaseController { private readonly IUserService _userService; - private readonly ISmsProvider _smsProvider; + private readonly ISmsService _smsService; private readonly ILiquidTemplateManager _liquidTemplateManager; private readonly IPhoneFormatValidator _phoneFormatValidator; private readonly HtmlEncoder _htmlEncoder; @@ -45,7 +44,7 @@ public SmsAuthenticatorController( INotifier notifier, IDistributedCache distributedCache, IUserService userService, - ISmsProvider smsProvider, + ISmsService smsService, ILiquidTemplateManager liquidTemplateManager, IPhoneFormatValidator phoneFormatValidator, HtmlEncoder htmlEncoder, @@ -62,7 +61,7 @@ public SmsAuthenticatorController( twoFactorOptions) { _userService = userService; - _smsProvider = smsProvider; + _smsService = smsService; _liquidTemplateManager = liquidTemplateManager; _phoneFormatValidator = phoneFormatValidator; _htmlEncoder = htmlEncoder; @@ -129,7 +128,7 @@ public async Task IndexPost(RequestCodeSmsAuthenticatorViewModel Body = await GetBodyAsync(smsSettings, user, code), }; - var result = await _smsProvider.SendAsync(message); + var result = await _smsService.SendAsync(message); if (!result.Succeeded) { @@ -228,7 +227,7 @@ public async Task SendCode() Body = await GetBodyAsync(settings, user, code), }; - var result = await _smsProvider.SendAsync(message); + var result = await _smsService.SendAsync(message); return Ok(new { diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProvider.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProvider.cs index 1cd512bc134..862ba8d0d1e 100644 --- a/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProvider.cs +++ b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProvider.cs @@ -14,6 +14,6 @@ public interface ISmsProvider /// Send the given message. /// /// The message to send. - /// SmsResult object + /// SmsResult object. Task SendAsync(SmsMessage message); } diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs new file mode 100644 index 00000000000..1ff582a8091 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace OrchardCore.Sms; + +public interface ISmsService +{ + /// + /// Send the given message. + /// + /// The message to send. + /// SmsResult object. + Task SendAsync(SmsMessage message); +} diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs deleted file mode 100644 index 578fd93d1ae..00000000000 --- a/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; - -namespace OrchardCore.Sms; - -public class SmsProviderOptions -{ - private Dictionary _providers { get; } = new(); - - private IReadOnlyDictionary _readonlyProviders; - - /// - /// This read-only collections contains all registered SMS providers. - /// The 'Key' is the technical name of the provider. - /// The 'Value' is the type of the SMS provider. The type will awalys be an implementation of interface. - /// - public IReadOnlyDictionary Providers => _readonlyProviders ??= _providers.ToImmutableDictionary(x => x.Key, x => x.Value); - - /// - /// Adds a provider if one does not exist. - /// - /// The technical name of the provider. - /// The type of the provider. - /// - /// - public SmsProviderOptions TryAddProvider(string name, Type type) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException($"'{nameof(name)}' cannot be null or empty."); - } - - if (_providers.ContainsKey(name)) - { - return this; - } - - if (!typeof(ISmsProvider).IsAssignableFrom(type)) - { - throw new ArgumentException($"The type must implement the '{nameof(ISmsProvider)}' interface."); - } - - _providers.Add(name, type); - _readonlyProviders = null; - - return this; - } - - /// - /// Removes a provider if one exist. - /// - /// The technical name of the provider. - /// - /// - public SmsProviderOptions RemoveProvider(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException($"'{nameof(name)}' cannot be null or empty."); - } - - if (_providers.Remove(name)) - { - _readonlyProviders = null; - } - - return this; - } - - /// - /// Replaces existing or adds a new provider. - /// - /// The technical name of the provider. - /// The type of the provider. - /// - /// - public SmsProviderOptions ReplaceProvider(string name, Type type) - { - _providers.Remove(name); - - return TryAddProvider(name, type); - } -} diff --git a/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj b/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj index d3cb498e683..1eb64b5990c 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj +++ b/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj @@ -18,6 +18,7 @@ + diff --git a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs index 5b697509775..50433f80353 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using OrchardCore.Environment.Shell.Builders; using OrchardCore.Sms.Services; namespace OrchardCore.Sms; @@ -12,53 +11,26 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddSmsServices(this IServiceCollection services) { services.AddTransient, SmsSettingsConfiguration>(); - services.AddHttpClient(TwilioSmsProvider.TechnicalName, client => - { - client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); - }); - services.AddSingleton(serviceProvider => + services.AddHttpClient(client => { - var settings = serviceProvider.GetRequiredService>().Value; - - if (!string.IsNullOrEmpty(settings.DefaultProviderName)) - { - var smsProviderOptions = serviceProvider.GetRequiredService>().Value; - - if (smsProviderOptions.Providers.TryGetValue(settings.DefaultProviderName, out var type)) - { - return serviceProvider.CreateInstance(type); - } - } + client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); + }).AddStandardResilienceHandler(); - return serviceProvider.CreateInstance(); - }); + services.AddScoped(); return services; } public static void AddPhoneFormatValidator(this IServiceCollection services) - { - services.TryAddScoped(); - } + => services.TryAddScoped(); public static IServiceCollection AddSmsProvider(this IServiceCollection services, string name) where T : class, ISmsProvider - { - services.Configure(options => - { - options.TryAddProvider(name, typeof(T)); - }); - - return services; - } + => services.AddKeyedScoped(name); public static IServiceCollection AddTwilioSmsProvider(this IServiceCollection services) - { - return services.AddSmsProvider(TwilioSmsProvider.TechnicalName); - } + => services.AddKeyedScoped(TwilioSmsProvider.TechnicalName); public static IServiceCollection AddLogSmsProvider(this IServiceCollection services) - { - return services.AddSmsProvider(LogSmsProvider.TechnicalName); - } + => services.AddKeyedScoped(LogSmsProvider.TechnicalName); } diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProvider.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProvider.cs deleted file mode 100644 index ee7e742dfb9..00000000000 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Localization; - -namespace OrchardCore.Sms.Services; - -public class DefaultSmsProvider : ISmsProvider -{ - protected readonly IStringLocalizer S; - - public DefaultSmsProvider(IStringLocalizer stringLocalizer) - { - S = stringLocalizer; - } - - public LocalizedString Name => S["Default"]; - - public Task SendAsync(SmsMessage message) - { - return Task.FromResult(SmsResult.Failed(S["SMS settings must be configured before an SMS message can be sent."])); - } -} diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsNotificationProvider.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsNotificationProvider.cs index 6f200b95644..c27e38a093e 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsNotificationProvider.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsNotificationProvider.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Microsoft.Extensions.Localization; using OrchardCore.Notifications; @@ -8,14 +7,14 @@ namespace OrchardCore.Sms.Services; public class SmsNotificationProvider : INotificationMethodProvider { - private readonly ISmsProvider _smsProvider; + private readonly ISmsService _smsService; protected readonly IStringLocalizer S; public SmsNotificationProvider( - ISmsProvider smsProvider, + ISmsService smsService, IStringLocalizer stringLocalizer) { - _smsProvider = smsProvider; + _smsService = smsService; S = stringLocalizer; } @@ -38,7 +37,7 @@ public async Task TrySendAsync(object notify, INotificationMessage message Body = message.TextBody, }; - var result = await _smsProvider.SendAsync(mailMessage); + var result = await _smsService.SendAsync(mailMessage); return result.Succeeded; } diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs new file mode 100644 index 00000000000..bfd305bd82a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Sms.Services; + +public class SmsService : ISmsService +{ + private readonly SmsSettings _smsOptions; + private readonly IServiceProvider _serviceProvider; + + protected readonly IStringLocalizer S; + + public SmsService( + IServiceProvider serviceProvider, + IOptions smsOptions, + IStringLocalizer stringLocalizer) + { + _smsOptions = smsOptions.Value; + _serviceProvider = serviceProvider; + S = stringLocalizer; + } + + public Task SendAsync(SmsMessage message) + { + var provider = GetProvider(); + + if (provider == null) + { + return Task.FromResult(SmsResult.Failed(S["SMS settings must be configured before an SMS message can be sent."])); + } + + return provider.SendAsync(message); + } + + private ISmsProvider GetProvider() + { + if (_provider == null && !string.IsNullOrEmpty(_smsOptions.DefaultProviderName)) + { + _provider = _serviceProvider.GetRequiredKeyedService(_smsOptions.DefaultProviderName); + } + + return _provider; + } + + private ISmsProvider _provider; +} diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs index 12643d82184..d8729330f76 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; -using OrchardCore.Entities; using OrchardCore.Settings; using OrchardCore.Sms.Models; @@ -31,20 +30,20 @@ public class TwilioSmsProvider : ISmsProvider private readonly ISiteService _siteService; private readonly IDataProtectionProvider _dataProtectionProvider; private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; + private readonly HttpClient _httpClient; protected readonly IStringLocalizer S; public TwilioSmsProvider( ISiteService siteService, IDataProtectionProvider dataProtectionProvider, ILogger logger, - IHttpClientFactory httpClientFactory, + HttpClient httpClient, IStringLocalizer stringLocalizer) { _siteService = siteService; _dataProtectionProvider = dataProtectionProvider; _logger = logger; - _httpClientFactory = httpClientFactory; + _httpClient = httpClient; S = stringLocalizer; } @@ -103,13 +102,11 @@ public async Task SendAsync(SmsMessage message) private HttpClient GetHttpClient(TwilioSettings settings) { - var client = _httpClientFactory.CreateClient(TechnicalName); - var token = $"{settings.AccountSID}:{settings.AuthToken}"; var base64Token = Convert.ToBase64String(Encoding.ASCII.GetBytes(token)); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Token); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Token); - return client; + return _httpClient; } private TwilioSettings _settings; diff --git a/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj b/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj index d54112d774f..b751c863a48 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj +++ b/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj @@ -4,9 +4,11 @@ OrchardCore.Users OrchardCore Users Core - $(OCCMSDescription) + + $(OCCMSDescription) - Core implementation for Users module. + Core implementation for Users module. + $(PackageTags) OrchardCoreCMS @@ -22,7 +24,8 @@ - + + diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index d1e394ceb93..23c2fec810e 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -21,6 +21,37 @@ The `OrchardCore.Sms` module provides you with the capability to integrate addit ```csharp services.AddSmsProvider("A technical name for your implementation") ``` +## Sending SMS Message + +An SMS message can be send by injecting `ISmsService` and invoke the `SendAsync` method. For instance + +```c# +public class Test +{ + private ISmsService _smsService; + + public Test(ISmsService smsService) + { + _smsService = smsService; + } + + public async Task SendSmsMessage() + { + var message = new SmsMessage + { + To = "17023451234", + Message = "It's easy to send an SMS message using Orcahred!", + }; + + var result = await _smsService.SendAsync(message); + + if (result.Succeeded) + { + // message was sent! + } + } +} +``` ## Workflows diff --git a/src/docs/releases/1.8.0.md b/src/docs/releases/1.8.0.md index 0640ba0e324..f74007ea484 100644 --- a/src/docs/releases/1.8.0.md +++ b/src/docs/releases/1.8.0.md @@ -23,6 +23,10 @@ The `TheAdmin` theme was upgraded to Bootstrap 5.3.2. Here is a list of the brea - The property named `DisplayDarkMode` in `AdminSettings` was replaced with `DisplayThemeToggler`. - Bootstrap is no longer compiled in `TheAdmin.css`. Bootstrap are loaded independently for performance and maintainability reasons. +### SMS Module + +In the past, we utilized the injection of `ISmsProvider`for sending SMS messages. However, in this release, it is now necessary to inject `ISmsService` instead. + ### Workflow Module A new option for restarting a specific Workflow instance has been incorporated, involving adjustments to both the `IActivity` and `IWorkflowManager` interfaces.The following method was added to `IWorkflowManager` interface. From d007f2387395edc48dcbca974b485e499ff4915c Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 27 Nov 2023 16:05:18 -0800 Subject: [PATCH 02/10] cleanup --- .../OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj | 2 +- .../OrchardCore.Users.Core/OrchardCore.Users.Core.csproj | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj b/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj index 1eb64b5990c..ea178b1502f 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj +++ b/src/OrchardCore/OrchardCore.Sms.Core/OrchardCore.Sms.Core.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj b/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj index b751c863a48..6bb859c4079 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj +++ b/src/OrchardCore/OrchardCore.Users.Core/OrchardCore.Users.Core.csproj @@ -24,7 +24,6 @@ - From 4c0b0da1d09b0366bf264ef120219f2bd54ca6f2 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 1 Dec 2023 15:05:35 -0800 Subject: [PATCH 03/10] Update settings --- .../Drivers/SmsSettingsDisplayDriver.cs | 2 +- .../Drivers/TwilioSettingsDisplayDriver.cs | 12 +++------- .../Views/SmsSettings.Edit.cshtml | 22 ------------------- .../Views/TwilioSettings.Edit.cshtml | 4 ++-- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs index a66c023162e..4c5054d1137 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs @@ -49,7 +49,7 @@ public override async Task EditAsync(SmsSettings settings, Build { model.DefaultProvider = settings.DefaultProviderName; model.Providers = GetProviders(); - }).Location("Content:1") + }).Location("Content:1#Providers") .OnGroup(SmsSettings.GroupId); } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs index 4fc0dff2d15..dcd11f10473 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs @@ -38,21 +38,15 @@ public TwilioSettingsDisplayDriver( S = stringLocalizer; } - public override async Task EditAsync(TwilioSettings settings, BuildEditorContext context) + public override IDisplayResult Edit(TwilioSettings settings) { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, SmsPermissions.ManageSmsSettings)) - { - return null; - } - return Initialize("TwilioSettings_Edit", model => { model.PhoneNumber = settings.PhoneNumber; model.AccountSID = settings.AccountSID; model.HasAuthToken = !string.IsNullOrEmpty(settings.AuthToken); - }).Location("Content:5") + }).Location("Content:5#Twilio") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, SmsPermissions.ManageSmsSettings)) .OnGroup(SmsSettings.GroupId); } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml index 25371e1e5a6..b6f5727f8a7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml @@ -9,25 +9,3 @@ - - diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml index 0ace616ff4e..5ba28783e11 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml @@ -4,7 +4,7 @@ @model TwilioSettingsViewModel -
+

@T["Twilio Account Info"]

@@ -25,7 +25,7 @@ @if (Model.HasAuthToken) { - @T["Auth token was securly saved. Enter a new value if you wish to replace the existing secret."] + @T["Auth token was securely saved. Enter a new value if you wish to replace the existing secret."] }
From 07ab3b0c9f496710ea06647ba092b0a1d43c8bae Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 1 Dec 2023 17:00:06 -0800 Subject: [PATCH 04/10] Split the settings into tabs --- .../OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs | 2 +- .../OrchardCore.Sms/Views/SmsSettings.Edit.cshtml | 2 +- .../OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs index dcd11f10473..bf1842479ed 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs @@ -62,7 +62,7 @@ public override async Task UpdateAsync(TwilioSettings settings, var model = new TwilioSettingsViewModel(); - if (await context.Updater.TryUpdateModelAsync(model, Prefix) && model.DefaultProvider == TwilioSmsProvider.TechnicalName) + if (await context.Updater.TryUpdateModelAsync(model, Prefix)) { if (string.IsNullOrWhiteSpace(model.PhoneNumber)) { diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml index b6f5727f8a7..8591efef847 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Views/SmsSettings.Edit.cshtml @@ -3,7 +3,7 @@ @model SmsSettingsViewModel
- + diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml index 5ba28783e11..c37c04b4d1b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml @@ -7,20 +7,20 @@

@T["Twilio Account Info"]

- + @T["Phone number must include a country code. For example, +1 for United States."]
- +
- + @if (Model.HasAuthToken) From 91e0ecc944317fd4d2a404416b343e94ae456544 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 12 Jan 2024 16:30:14 -0800 Subject: [PATCH 05/10] Don't use Keyed services --- .../Drivers/SmsSettingsDisplayDriver.cs | 36 ++++---- .../Drivers/SmsTaskDisplayDriver.cs | 9 +- .../Drivers/TwilioSettingsDisplayDriver.cs | 12 +++ .../ISmsProviderResolver.cs | 14 +++ .../SmsProviderOptions.cs | 85 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 31 ++++--- .../Services/SmsProviderResolver.cs | 58 +++++++++++++ .../Services/SmsService.cs | 34 ++------ src/docs/reference/modules/Sms/README.md | 12 ++- src/docs/releases/1.8.0.md | 4 - 10 files changed, 229 insertions(+), 66 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProviderResolver.cs create mode 100644 src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs index 4c5054d1137..bf477dfae7c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs @@ -1,10 +1,10 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; @@ -20,19 +20,19 @@ public class SmsSettingsDisplayDriver : SectionDisplayDriver private readonly IAuthorizationService _authorizationService; private readonly IShellHost _shellHost; private readonly ShellSettings _shellSettings; - private readonly IDictionary _smsProviders; + private readonly SmsProviderOptions _smsProviderOptions; public SmsSettingsDisplayDriver( IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService, IShellHost shellHost, - IDictionary smsProviders, + IOptions smsProviders, ShellSettings shellSettings) { _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; _shellHost = shellHost; - _smsProviders = smsProviders; + _smsProviderOptions = smsProviders.Value; _shellSettings = shellSettings; } @@ -50,6 +50,7 @@ public override async Task EditAsync(SmsSettings settings, Build model.DefaultProvider = settings.DefaultProviderName; model.Providers = GetProviders(); }).Location("Content:1#Providers") + .Prefix(Prefix) .OnGroup(SmsSettings.GroupId); } @@ -78,24 +79,23 @@ public override async Task UpdateAsync(SmsSettings settings, Bui return await EditAsync(settings, context); } - private SelectListItem[] _providers; - - private SelectListItem[] GetProviders() + protected override void BuildPrefix(ISite model, string htmlFieldPrefix) { - if (_providers == null) + Prefix = typeof(SmsSettings).Name; + + if (!string.IsNullOrEmpty(htmlFieldPrefix)) { - var items = new List(); + Prefix = htmlFieldPrefix + "." + Prefix; + } + } - foreach (var provider in _smsProviders) - { - items.Add(new SelectListItem(provider.Value.Name, provider.Key)); - } + private SelectListItem[] _providers; - _providers = - [ - .. items.OrderBy(item => item.Text), - ]; - } + private SelectListItem[] GetProviders() + { + _providers ??= _smsProviderOptions.Providers.Keys.Select(key => new SelectListItem(key, key)) + .OrderBy(item => item.Text) + .ToArray(); return _providers; } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsTaskDisplayDriver.cs index d54d9b9992d..8afef0de028 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsTaskDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsTaskDisplayDriver.cs @@ -1,4 +1,3 @@ -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Localization; @@ -17,16 +16,18 @@ public class SmsTaskDisplayDriver : ActivityDisplayDriver stringLocalizer, - ILiquidTemplateManager liquidTemplateManager) + ILiquidTemplateManager liquidTemplateManager, + IStringLocalizer stringLocalizer + ) { _phoneFormatValidator = phoneFormatValidator; - S = stringLocalizer; _liquidTemplateManager = liquidTemplateManager; + S = stringLocalizer; } protected override void EditActivity(SmsTask activity, SmsTaskViewModel model) diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs index bf1842479ed..61d1c5a42d7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs @@ -22,6 +22,7 @@ public class TwilioSettingsDisplayDriver : SectionDisplayDriver _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, SmsPermissions.ManageSmsSettings)) + .Prefix(Prefix) .OnGroup(SmsSettings.GroupId); } @@ -96,4 +98,14 @@ public override async Task UpdateAsync(TwilioSettings settings, return await EditAsync(settings, context); } + + protected override void BuildPrefix(ISite model, string htmlFieldPrefix) + { + Prefix = typeof(TwilioSettings).Name; + + if (!string.IsNullOrEmpty(htmlFieldPrefix)) + { + Prefix = htmlFieldPrefix + "." + Prefix; + } + } } diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProviderResolver.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProviderResolver.cs new file mode 100644 index 00000000000..c599866d3df --- /dev/null +++ b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsProviderResolver.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace OrchardCore.Sms; + +public interface ISmsProviderResolver +{ + /// + /// Gets the SMS provider for the given name. + /// When null is null or empty, it gets the default SMS provider. + /// + /// The key of the SMS provider + /// Instance ISmsProvider or null when no service found. + Task GetAsync(string name = null); +} diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs new file mode 100644 index 00000000000..7a069613405 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; + +namespace OrchardCore.Sms; + +public class SmsProviderOptions +{ + private Dictionary _providers { get; } = []; + + private IReadOnlyDictionary _readonlyProviders; + + /// + /// This read-only collections contains all registered SMS providers. + /// The 'Key' is the technical name of the provider. + /// The 'Value' is the type of the SMS provider. The type will always be an implementation of interface. + /// + public IReadOnlyDictionary Providers + => _readonlyProviders ??= _providers.ToFrozenDictionary(x => x.Key, x => x.Value); + + /// + /// Adds a provider if one does not exist. + /// + /// The technical name of the provider. + /// The type of the provider. + /// + /// + public SmsProviderOptions TryAddProvider(string name, Type type) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or empty."); + } + + if (_providers.ContainsKey(name)) + { + return this; + } + + if (!typeof(ISmsProvider).IsAssignableFrom(type)) + { + throw new ArgumentException($"The type must implement the '{nameof(ISmsProvider)}' interface."); + } + + _providers.Add(name, type); + _readonlyProviders = null; + + return this; + } + + /// + /// Removes a provider if one exist. + /// + /// The technical name of the provider. + /// + /// + public SmsProviderOptions RemoveProvider(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or empty."); + } + + if (_providers.Remove(name)) + { + _readonlyProviders = null; + } + + return this; + } + + /// + /// Replaces existing or adds a new provider. + /// + /// The technical name of the provider. + /// The type of the provider. + /// + /// + public SmsProviderOptions ReplaceProvider(string name, Type type) + { + _providers.Remove(name); + + return TryAddProvider(name, type); + } +} diff --git a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs index 50433f80353..87aaf914452 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs @@ -10,14 +10,9 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddSmsServices(this IServiceCollection services) { - services.AddTransient, SmsSettingsConfiguration>(); - - services.AddHttpClient(client => - { - client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); - }).AddStandardResilienceHandler(); - services.AddScoped(); + services.AddScoped(); + services.AddTransient, SmsSettingsConfiguration>(); return services; } @@ -26,11 +21,27 @@ public static void AddPhoneFormatValidator(this IServiceCollection services) => services.TryAddScoped(); public static IServiceCollection AddSmsProvider(this IServiceCollection services, string name) where T : class, ISmsProvider - => services.AddKeyedScoped(name); + { + services.Configure(options => + { + options.TryAddProvider(name, typeof(T)); + }); + + return services; + } public static IServiceCollection AddTwilioSmsProvider(this IServiceCollection services) - => services.AddKeyedScoped(TwilioSmsProvider.TechnicalName); + { + services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); + }).AddStandardResilienceHandler(); + + services.AddSmsProvider(TwilioSmsProvider.TechnicalName); + + return services; + } public static IServiceCollection AddLogSmsProvider(this IServiceCollection services) - => services.AddKeyedScoped(LogSmsProvider.TechnicalName); + => services.AddSmsProvider(LogSmsProvider.TechnicalName); } diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs new file mode 100644 index 00000000000..037d7934351 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Settings; + +namespace OrchardCore.Sms.Services; + +public class SmsProviderResolver : ISmsProviderResolver +{ + private readonly ISiteService _siteService; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly SmsProviderOptions _smsProviderOptions; + + public SmsProviderResolver( + ISiteService siteService, + ILogger logger, + IOptions smsProviderOptions, + IServiceProvider serviceProvider) + { + _siteService = siteService; + _logger = logger; + _serviceProvider = serviceProvider; + _smsProviderOptions = smsProviderOptions.Value; + } + + public async Task GetAsync(string name = null) + { + if (string.IsNullOrEmpty(name)) + { + var site = await _siteService.GetSiteSettingsAsync(); + + var settings = site.As(); + + name = settings.DefaultProviderName; + } + + if (name != null && _smsProviderOptions.Providers.TryGetValue(name, out var providerType)) + { + return (ISmsProvider)_serviceProvider.GetRequiredService(providerType); + } + + if (string.IsNullOrEmpty(name) && _smsProviderOptions.Providers.Count > 0) + { + return (ISmsProvider)_serviceProvider.GetRequiredService(_smsProviderOptions.Providers.Values.Last()); + } + + if (_logger.IsEnabled(LogLevel.Error)) + { + _logger.LogError("No SMS provider registered to match the given name {name}.", name); + } + + return null; + } +} diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs index bfd305bd82a..648c92a9109 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs @@ -1,49 +1,31 @@ -using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Options; namespace OrchardCore.Sms.Services; public class SmsService : ISmsService { - private readonly SmsSettings _smsOptions; - private readonly IServiceProvider _serviceProvider; + private readonly ISmsProviderResolver _smsProviderResolver; - protected readonly IStringLocalizer S; + protected readonly IStringLocalizer S; public SmsService( - IServiceProvider serviceProvider, - IOptions smsOptions, + ISmsProviderResolver smsProviderResolver, IStringLocalizer stringLocalizer) { - _smsOptions = smsOptions.Value; - _serviceProvider = serviceProvider; + _smsProviderResolver = smsProviderResolver; S = stringLocalizer; } - public Task SendAsync(SmsMessage message) + public async Task SendAsync(SmsMessage message) { - var provider = GetProvider(); + var provider = await _smsProviderResolver.GetAsync(); if (provider == null) { - return Task.FromResult(SmsResult.Failed(S["SMS settings must be configured before an SMS message can be sent."])); + return SmsResult.Failed(S["SMS settings must be configured before an SMS message can be sent."]); } - return provider.SendAsync(message); + return await provider.SendAsync(message); } - - private ISmsProvider GetProvider() - { - if (_provider == null && !string.IsNullOrEmpty(_smsOptions.DefaultProviderName)) - { - _provider = _serviceProvider.GetRequiredKeyedService(_smsOptions.DefaultProviderName); - } - - return _provider; - } - - private ISmsProvider _provider; } diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index 23c2fec810e..a3974089505 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -26,11 +26,11 @@ The `OrchardCore.Sms` module provides you with the capability to integrate addit An SMS message can be send by injecting `ISmsService` and invoke the `SendAsync` method. For instance ```c# -public class Test +public class TestController { - private ISmsService _smsService; + private readonly ISmsService _smsService; - public Test(ISmsService smsService) + public TestController(ISmsService smsService) { _smsService = smsService; } @@ -40,7 +40,7 @@ public class Test var message = new SmsMessage { To = "17023451234", - Message = "It's easy to send an SMS message using Orcahred!", + Message = "It's easy to send an SMS message using Orchard!", }; var result = await _smsService.SendAsync(message); @@ -48,7 +48,11 @@ public class Test if (result.Succeeded) { // message was sent! + + return Ok(result); } + + return BadRequest(result); } } ``` diff --git a/src/docs/releases/1.8.0.md b/src/docs/releases/1.8.0.md index f74007ea484..0640ba0e324 100644 --- a/src/docs/releases/1.8.0.md +++ b/src/docs/releases/1.8.0.md @@ -23,10 +23,6 @@ The `TheAdmin` theme was upgraded to Bootstrap 5.3.2. Here is a list of the brea - The property named `DisplayDarkMode` in `AdminSettings` was replaced with `DisplayThemeToggler`. - Bootstrap is no longer compiled in `TheAdmin.css`. Bootstrap are loaded independently for performance and maintainability reasons. -### SMS Module - -In the past, we utilized the injection of `ISmsProvider`for sending SMS messages. However, in this release, it is now necessary to inject `ISmsService` instead. - ### Workflow Module A new option for restarting a specific Workflow instance has been incorporated, involving adjustments to both the `IActivity` and `IWorkflowManager` interfaces.The following method was added to `IWorkflowManager` interface. From 988bcaaabf4913f7fbd99f79a18d33dbe66d2e23 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 12 Jan 2024 17:03:20 -0800 Subject: [PATCH 06/10] Update docs --- .../Dependencies.AspNetCore.props | 1 - src/OrchardCore.Build/Dependencies.props | 1 + .../OrchardCore.Sms/Manifest.cs | 17 +++++++++++++--- .../OrchardCore.Sms/Startup.cs | 20 ++++++++++++------- src/docs/reference/README.md | 1 + .../reference/modules/Sms.Twilio/README.md | 7 +++++++ src/docs/reference/modules/Sms/README.md | 15 ++++---------- src/docs/releases/1.9.0.md | 6 ++++++ 8 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 src/docs/reference/modules/Sms.Twilio/README.md diff --git a/src/OrchardCore.Build/Dependencies.AspNetCore.props b/src/OrchardCore.Build/Dependencies.AspNetCore.props index ba680073f48..50b50d56bd2 100644 --- a/src/OrchardCore.Build/Dependencies.AspNetCore.props +++ b/src/OrchardCore.Build/Dependencies.AspNetCore.props @@ -43,7 +43,6 @@ - diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 07300f6f7a1..aeb2b9f932d 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -38,6 +38,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs index 9a9ecc86a9a..b2c508b7399 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs @@ -13,14 +13,25 @@ Category = "SMS" )] +[assembly: Feature( + Name = "Twilio SMS Provider", + Id = "OrchardCore.Sms.Twilio", + Description = "Provides Twilio SMS services for sending messages to users.", + Category = "SMS", + Dependencies = + [ + "OrchardCore.Sms", + ] +)] + [assembly: Feature( Name = "SMS Notifications", Id = "OrchardCore.Notifications.Sms", Description = "Provides a way to send SMS notifications to users.", Category = "Notifications", - Dependencies = new[] - { + Dependencies = + [ "OrchardCore.Notifications", "OrchardCore.Sms", - } + ] )] diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs index 16c5d00b09f..d7a9285535f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs @@ -27,10 +27,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddSmsServices(); services.AddPhoneFormatValidator(); - // Add Twilio provider. - services.AddTwilioSmsProvider() - .AddScoped, TwilioSettingsDisplayDriver>(); - if (_hostEnvironment.IsDevelopment()) { // Add Log provider. @@ -43,12 +39,13 @@ public override void ConfigureServices(IServiceCollection services) } } -[RequireFeatures("OrchardCore.Workflows")] -public class WorkflowsStartup : StartupBase +[Feature("OrchardCore.Sms.Twilio")] +public class TwilioStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.AddActivity(); + services.AddTwilioSmsProvider() + .AddScoped, TwilioSettingsDisplayDriver>(); } } @@ -60,3 +57,12 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); } } + +[RequireFeatures("OrchardCore.Workflows")] +public class WorkflowsStartup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddActivity(); + } +} diff --git a/src/docs/reference/README.md b/src/docs/reference/README.md index 7dc3372e4b3..659e405a56c 100644 --- a/src/docs/reference/README.md +++ b/src/docs/reference/README.md @@ -120,6 +120,7 @@ Here's a categorized overview of all built-in Orchard Core features at a glance. - [Diagnostics](modules/Diagnostics/README.md) - [Remote Deployment](modules/Deployment.Remote/README.md) - [Sms](modules/Sms/README.md) + - [Twilio](modules/Sms.Twilio/README.md) ### Localization diff --git a/src/docs/reference/modules/Sms.Twilio/README.md b/src/docs/reference/modules/Sms.Twilio/README.md new file mode 100644 index 00000000000..6d18abef19b --- /dev/null +++ b/src/docs/reference/modules/Sms.Twilio/README.md @@ -0,0 +1,7 @@ +# SMS (`OrchardCore.Sms.Twilio`) + +This feature provides the necessary services to send messages using the `Twilio` service. + +## Twilio Settings + +After enabling the Twilio provider, navigate to `Configurations` >> `Settings` >> `SMS`. Click on the `Twilio` tab, and provider your Twilio account info. Then in the `Providers` tab, select Twilio as your default provider. diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index 2082c1557b4..59ecaea1287 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -1,26 +1,19 @@ # SMS (`OrchardCore.Sms`) -This module provides the infrastructure necessary to send messages using an `SMS` service. +This module provides SMS settings configuration. ## SMS Settings -Enabling the `SMS` feature will add a new settings page under `Configurations` >> `Settings` >> `SMS`. You can utilize these settings to set up the default SMS provider configuration. The following are the providers that are readily accessible. +Enabling the `SMS` feature will add a new settings page under `Configurations` >> `Settings` >> `SMS`. You can utilize these settings to set up the default SMS provider configuration. When running in a development mode, we enable `Log` provider to allow you to log the SMS message to the log file for testing. However, for the `SMS` to be production ready, you must enable an SMS provider feature like `OrchardCore.Sms.Twilio`. -| Provider | Description | -| --- | --- | -| `Log` | This particular provider is exclusively meant for debugging purposes and should never be used in a production environment. It permits the message to be written to the logs. | -| `Twilio` | Opting for this provider enables the utilization of Twilio service for sending SMS messages. By choosing this provider, you will need to input your Twilio account settings. | - -!!! note - After enabling the SMS feature, you must configure the default provider in order to send SMS messages. - -## Other Providers +## Adding Custom Providers The `OrchardCore.Sms` module provides you with the capability to integrate additional providers for dispatching SMS messages. To achieve this, you can easily create an implementation of the `ISmsProvider` interface and then proceed to register it using the following approach. ```csharp services.AddSmsProvider("A technical name for your implementation") ``` + ## Sending SMS Message An SMS message can be send by injecting `ISmsService` and invoke the `SendAsync` method. For instance diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 0a3dcf025b5..977011b3956 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -12,6 +12,12 @@ Additionally, if you needed to enable indexing for text file with `.txt`, `.md` If you needed to enable indexing for other extensions like (`.docx`, or `.pptx`), you'll needed `OrchardCore.Media.Indexing.OpenXML` feature. +### SMS Module + +In the past, we utilized the injection of `ISmsProvider`for sending SMS messages. However, in this release, it is now necessary to inject `ISmsService` instead. + +Additionally, `Twilio` provider is no longer enabled by default. If you want to use Twilio SMS provider, you must enable `OrchardCore.Sms.Twilio` feature. + ## Change Logs ### Azure AI Search Module From b69127d62652cd36591feb8801149aeffa2ef3b5 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 13 Jan 2024 11:35:33 -0800 Subject: [PATCH 07/10] Adding a way to test out SMS Providers --- .../OrchardCore.Sms/AdminMenu.cs | 9 ++ .../Controllers/AdminController.cs | 113 ++++++++++++++++++ .../OrchardCore.Sms/Startup.cs | 28 ++++- .../ViewModels/SmsTestViewModel.cs | 18 +++ .../OrchardCore.Sms/Views/Admin/Test.cshtml | 40 +++++++ .../ISmsService.cs | 6 +- .../ServiceCollectionExtensions.cs | 14 +-- ...olver.cs => DefaultSmsProviderResolver.cs} | 12 +- .../Services/SmsService.cs | 4 +- .../Services/TwilioSmsProvider.cs | 21 ++-- 10 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/SmsTestViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Sms/Views/Admin/Test.cshtml rename src/OrchardCore/OrchardCore.Sms.Core/Services/{SmsProviderResolver.cs => DefaultSmsProviderResolver.cs} (78%) diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Sms/AdminMenu.cs index de7455879a6..8e11efe61e3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/AdminMenu.cs @@ -1,7 +1,9 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Localization; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Navigation; +using OrchardCore.Sms.Controllers; namespace OrchardCore.Sms; @@ -31,6 +33,13 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Permission(SmsPermissions.ManageSmsSettings) .LocalNav() ) + .Add(S["SMS Test"], S["SMS Test"].PrefixPosition(), sms => sms + .AddClass("smstest") + .Id("smstest") + .Action(nameof(AdminController.Test), typeof(AdminController).ControllerName(), new { area = "OrchardCore.Sms" }) + .Permission(SmsPermissions.ManageSmsSettings) + .LocalNav() + ) ) ); diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs new file mode 100644 index 00000000000..1ca79c029b5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs @@ -0,0 +1,113 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Sms.ViewModels; + +namespace OrchardCore.Sms.Controllers; + +public class AdminController : Controller +{ + private readonly SmsProviderOptions _smsProviderOptions; + private readonly IPhoneFormatValidator _phoneFormatValidator; + private readonly INotifier _notifier; + private readonly IAuthorizationService _authorizationService; + protected readonly IHtmlLocalizer H; + protected readonly IStringLocalizer S; + private readonly ISmsProviderResolver _smsProviderResolver; + private readonly ISmsService _smsService; + + public AdminController( + IOptions smsProviderOptions, + IPhoneFormatValidator phoneFormatValidator, + ISmsProviderResolver smsProviderResolver, + ISmsService smsService, + INotifier notifier, + IAuthorizationService authorizationService, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer) + { + _smsProviderOptions = smsProviderOptions.Value; + _phoneFormatValidator = phoneFormatValidator; + _smsProviderResolver = smsProviderResolver; + _smsService = smsService; + _notifier = notifier; + _authorizationService = authorizationService; + H = htmlLocalizer; + S = stringLocalizer; + } + + public async Task Test() + { + if (!await _authorizationService.AuthorizeAsync(User, SmsPermissions.ManageSmsSettings)) + { + return Forbid(); + } + + var model = new SmsTestViewModel(); + + PopulateModel(model); + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Test(SmsTestViewModel model) + { + if (!await _authorizationService.AuthorizeAsync(User, SmsPermissions.ManageSmsSettings)) + { + return Forbid(); + } + + if (ModelState.IsValid) + { + var provider = await _smsProviderResolver.GetAsync(model.Provider); + + if (provider is null) + { + ModelState.AddModelError(nameof(model.PhoneNumber), S["Please select a valid provider."]); + } + else if (!_phoneFormatValidator.IsValid(model.PhoneNumber)) + { + ModelState.AddModelError(nameof(model.PhoneNumber), S["Please provide a valid phone number."]); + } + else + { + var result = await _smsService.SendAsync(new SmsMessage() + { + To = model.PhoneNumber, + Body = S["This is a test SMS message."] + }, provider); + + if (result.Succeeded) + { + await _notifier.SuccessAsync(H["The test SMS message has been successfully sent."]); + + return RedirectToAction(nameof(Test)); + } + else + { + await _notifier.ErrorAsync(H["The test SMS message failed to send."]); + } + } + } + + PopulateModel(model); + + return View(model); + } + + private void PopulateModel(SmsTestViewModel model) + { + model.Providers = _smsProviderOptions.Providers.Keys + .Select(key => new SelectListItem(key, key)) + .OrderBy(item => item.Text) + .ToArray(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs index d7a9285535f..9b7f948db2f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs @@ -1,12 +1,19 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using OrchardCore.Admin; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Navigation; using OrchardCore.Notifications; using OrchardCore.Security.Permissions; using OrchardCore.Settings; using OrchardCore.Sms.Activities; +using OrchardCore.Sms.Controllers; using OrchardCore.Sms.Drivers; using OrchardCore.Sms.Services; using OrchardCore.Workflows.Helpers; @@ -16,10 +23,14 @@ namespace OrchardCore.Sms; public class Startup : StartupBase { private readonly IHostEnvironment _hostEnvironment; + private readonly AdminOptions _adminOptions; - public Startup(IHostEnvironment hostEnvironment) + public Startup( + IHostEnvironment hostEnvironment, + IOptions adminOptions) { _hostEnvironment = hostEnvironment; + _adminOptions = adminOptions.Value; } public override void ConfigureServices(IServiceCollection services) @@ -37,6 +48,16 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped, SmsSettingsDisplayDriver>(); } + + public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + routes.MapAreaControllerRoute( + name: "SMSTest", + areaName: "OrchardCore.Sms", + pattern: _adminOptions.AdminUrlPrefix + "/SMS/test", + defaults: new { controller = typeof(AdminController).ControllerName(), action = nameof(AdminController.Test) } + ); + } } [Feature("OrchardCore.Sms.Twilio")] @@ -44,6 +65,11 @@ public class TwilioStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { + services.AddHttpClient(TwilioSmsProvider.TechnicalName, client => + { + client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); + }).AddStandardResilienceHandler(); + services.AddTwilioSmsProvider() .AddScoped, TwilioSettingsDisplayDriver>(); } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/SmsTestViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/SmsTestViewModel.cs new file mode 100644 index 00000000000..7847154efb3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/SmsTestViewModel.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Sms.ViewModels; + +public class SmsTestViewModel +{ + [Required] + public string Provider { get; set; } + + [Required] + public string PhoneNumber { get; set; } + + [BindNever] + public IList Providers { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Views/Admin/Test.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms/Views/Admin/Test.cshtml new file mode 100644 index 00000000000..803ac540110 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Views/Admin/Test.cshtml @@ -0,0 +1,40 @@ +@using OrchardCore +@using OrchardCore.Sms.ViewModels + +@model SmsTestViewModel + +@if (Model.Providers == null || Model.Providers.Count == 0) +{ + + + return; +} + +
+ +
+ +
+ +
+
+ +
+ +
+ + @T["Phone number must include a country code. For example, +1 for United States."] +
+
+ +
+
+ +
+
+ +
diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs index 1ff582a8091..e6664eeb830 100644 --- a/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs +++ b/src/OrchardCore/OrchardCore.Sms.Abstractions/ISmsService.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace OrchardCore.Sms; @@ -8,6 +8,8 @@ public interface ISmsService /// Send the given message. /// /// The message to send. + /// An SMS Provider to use. When null, we sent using the default provider. /// SmsResult object. - Task SendAsync(SmsMessage message); + Task SendAsync(SmsMessage message, ISmsProvider provider = null); + } diff --git a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs index 87aaf914452..e58f7cb4a71 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -11,7 +10,7 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddSmsServices(this IServiceCollection services) { services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddTransient, SmsSettingsConfiguration>(); return services; @@ -31,16 +30,7 @@ public static IServiceCollection AddSmsProvider(this IServiceCollection servi } public static IServiceCollection AddTwilioSmsProvider(this IServiceCollection services) - { - services.AddHttpClient(client => - { - client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); - }).AddStandardResilienceHandler(); - - services.AddSmsProvider(TwilioSmsProvider.TechnicalName); - - return services; - } + => services.AddSmsProvider(TwilioSmsProvider.TechnicalName); public static IServiceCollection AddLogSmsProvider(this IServiceCollection services) => services.AddSmsProvider(LogSmsProvider.TechnicalName); diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs similarity index 78% rename from src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs rename to src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs index 037d7934351..9055e0fad2e 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsProviderResolver.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs @@ -1,23 +1,23 @@ using System; using System.Linq; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OrchardCore.Environment.Shell.Builders; using OrchardCore.Settings; namespace OrchardCore.Sms.Services; -public class SmsProviderResolver : ISmsProviderResolver +public class DefaultSmsProviderResolver : ISmsProviderResolver { private readonly ISiteService _siteService; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly SmsProviderOptions _smsProviderOptions; - public SmsProviderResolver( + public DefaultSmsProviderResolver( ISiteService siteService, - ILogger logger, + ILogger logger, IOptions smsProviderOptions, IServiceProvider serviceProvider) { @@ -40,12 +40,12 @@ public async Task GetAsync(string name = null) if (name != null && _smsProviderOptions.Providers.TryGetValue(name, out var providerType)) { - return (ISmsProvider)_serviceProvider.GetRequiredService(providerType); + return _serviceProvider.CreateInstance(providerType); } if (string.IsNullOrEmpty(name) && _smsProviderOptions.Providers.Count > 0) { - return (ISmsProvider)_serviceProvider.GetRequiredService(_smsProviderOptions.Providers.Values.Last()); + return _serviceProvider.CreateInstance(_smsProviderOptions.Providers.Values.Last()); } if (_logger.IsEnabled(LogLevel.Error)) diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs index 648c92a9109..38b49f4c3b3 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/SmsService.cs @@ -17,9 +17,9 @@ public SmsService( S = stringLocalizer; } - public async Task SendAsync(SmsMessage message) + public async Task SendAsync(SmsMessage message, ISmsProvider provider = null) { - var provider = await _smsProviderResolver.GetAsync(); + provider ??= await _smsProviderResolver.GetAsync(); if (provider == null) { diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs index d8729330f76..c81848ddb2e 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioSmsProvider.cs @@ -30,29 +30,27 @@ public class TwilioSmsProvider : ISmsProvider private readonly ISiteService _siteService; private readonly IDataProtectionProvider _dataProtectionProvider; private readonly ILogger _logger; - private readonly HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; + protected readonly IStringLocalizer S; public TwilioSmsProvider( ISiteService siteService, IDataProtectionProvider dataProtectionProvider, ILogger logger, - HttpClient httpClient, + IHttpClientFactory httpClientFactory, IStringLocalizer stringLocalizer) { _siteService = siteService; _dataProtectionProvider = dataProtectionProvider; _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; S = stringLocalizer; } public async Task SendAsync(SmsMessage message) { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } + ArgumentNullException.ThrowIfNull(message); if (string.IsNullOrEmpty(message.To)) { @@ -104,9 +102,12 @@ private HttpClient GetHttpClient(TwilioSettings settings) { var token = $"{settings.AccountSID}:{settings.AuthToken}"; var base64Token = Convert.ToBase64String(Encoding.ASCII.GetBytes(token)); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Token); - return _httpClient; + var client = _httpClientFactory.CreateClient(TechnicalName); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Token); + + return client; } private TwilioSettings _settings; @@ -124,7 +125,7 @@ private async Task GetSettingsAsync() { PhoneNumber = settings.PhoneNumber, AccountSID = settings.AccountSID, - AuthToken = protector.Unprotect(settings.AuthToken), + AuthToken = settings.AuthToken == null ? null : protector.Unprotect(settings.AuthToken), }; } From 66b3d4be40e7bae0a59272d1b1ac21ae3e6dcdea Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 13 Jan 2024 11:45:27 -0800 Subject: [PATCH 08/10] update docs --- src/docs/reference/modules/Sms.Twilio/README.md | 2 +- src/docs/reference/modules/Sms/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/reference/modules/Sms.Twilio/README.md b/src/docs/reference/modules/Sms.Twilio/README.md index 6d18abef19b..a1340226315 100644 --- a/src/docs/reference/modules/Sms.Twilio/README.md +++ b/src/docs/reference/modules/Sms.Twilio/README.md @@ -1,6 +1,6 @@ # SMS (`OrchardCore.Sms.Twilio`) -This feature provides the necessary services to send messages using the `Twilio` service. +This feature provides the necessary services to send messages using the [Twilio](https://www.twilio.com) service. ## Twilio Settings diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index 59ecaea1287..b37a3b3a19f 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -11,7 +11,7 @@ Enabling the `SMS` feature will add a new settings page under `Configurations` > The `OrchardCore.Sms` module provides you with the capability to integrate additional providers for dispatching SMS messages. To achieve this, you can easily create an implementation of the `ISmsProvider` interface and then proceed to register it using the following approach. ```csharp - services.AddSmsProvider("A technical name for your implementation") +services.AddSmsProvider("A technical name for your implementation") ``` ## Sending SMS Message From de170d436e9a620c8de79b3b274330079ec613d3 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 13 Jan 2024 11:47:06 -0800 Subject: [PATCH 09/10] cleanup controller --- .../OrchardCore.Sms/Controllers/AdminController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs index 1ca79c029b5..110e373f6da 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs @@ -17,11 +17,12 @@ public class AdminController : Controller private readonly IPhoneFormatValidator _phoneFormatValidator; private readonly INotifier _notifier; private readonly IAuthorizationService _authorizationService; - protected readonly IHtmlLocalizer H; - protected readonly IStringLocalizer S; private readonly ISmsProviderResolver _smsProviderResolver; private readonly ISmsService _smsService; + protected readonly IHtmlLocalizer H; + protected readonly IStringLocalizer S; + public AdminController( IOptions smsProviderOptions, IPhoneFormatValidator phoneFormatValidator, From 4f41b9f6f67bcb43d22ec6caa943dac6779ca31e Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sun, 14 Jan 2024 11:19:30 -0800 Subject: [PATCH 10/10] make twilio optional and merge into one feature --- .../Controllers/AdminController.cs | 5 +- .../Drivers/SmsSettingsDisplayDriver.cs | 57 +++++------ .../Drivers/TwilioSettingsDisplayDriver.cs | 96 ++++++++++++++----- .../OrchardCore.Sms/Manifest.cs | 11 --- .../OrchardCore.Sms/Startup.cs | 23 +---- .../ViewModels/TwilioSettingsViewModel.cs | 2 + .../Views/TwilioSettings.Edit.cshtml | 10 +- .../SmsProviderOptions.cs | 40 +++++--- .../Models/TwilioSettings.cs | 2 + .../ServiceCollectionExtensions.cs | 26 ++++- .../Services/DefaultSmsProviderResolver.cs | 4 +- .../TwilioProviderOptionsConfigurations.cs | 27 ++++++ src/docs/reference/README.md | 1 - .../reference/modules/Sms.Twilio/README.md | 7 -- src/docs/reference/modules/Sms/README.md | 49 +++++++++- 15 files changed, 249 insertions(+), 111 deletions(-) create mode 100644 src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioProviderOptionsConfigurations.cs delete mode 100644 src/docs/reference/modules/Sms.Twilio/README.md diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs index 110e373f6da..aa425bc3135 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Controllers/AdminController.cs @@ -106,8 +106,9 @@ public async Task Test(SmsTestViewModel model) private void PopulateModel(SmsTestViewModel model) { - model.Providers = _smsProviderOptions.Providers.Keys - .Select(key => new SelectListItem(key, key)) + model.Providers = _smsProviderOptions.Providers + .Where(entry => entry.Value.IsEnabled) + .Select(entry => new SelectListItem(entry.Key, entry.Key)) .OrderBy(item => item.Text) .ToArray(); } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs index bf477dfae7c..5ccfe17fd05 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/SmsSettingsDisplayDriver.cs @@ -3,12 +3,15 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; +using OrchardCore.Mvc.ModelBinding; using OrchardCore.Settings; using OrchardCore.Sms.ViewModels; @@ -20,6 +23,9 @@ public class SmsSettingsDisplayDriver : SectionDisplayDriver private readonly IAuthorizationService _authorizationService; private readonly IShellHost _shellHost; private readonly ShellSettings _shellSettings; + + protected IStringLocalizer S; + private readonly SmsProviderOptions _smsProviderOptions; public SmsSettingsDisplayDriver( @@ -27,32 +33,31 @@ public SmsSettingsDisplayDriver( IAuthorizationService authorizationService, IShellHost shellHost, IOptions smsProviders, - ShellSettings shellSettings) + ShellSettings shellSettings, + IStringLocalizer stringLocalizer) { _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; _shellHost = shellHost; _smsProviderOptions = smsProviders.Value; _shellSettings = shellSettings; + S = stringLocalizer; } - public override async Task EditAsync(SmsSettings settings, BuildEditorContext context) - { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, SmsPermissions.ManageSmsSettings)) - { - return null; - } - - return Initialize("SmsSettings_Edit", model => + public override IDisplayResult Edit(SmsSettings settings) + => Initialize("SmsSettings_Edit", model => { model.DefaultProvider = settings.DefaultProviderName; - model.Providers = GetProviders(); + model.Providers = _smsProviderOptions.Providers + .Where(entry => entry.Value.IsEnabled) + .Select(entry => new SelectListItem(entry.Key, entry.Key)) + .OrderBy(item => item.Text) + .ToArray(); + }).Location("Content:1#Providers") + .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, SmsPermissions.ManageSmsSettings)) .Prefix(Prefix) .OnGroup(SmsSettings.GroupId); - } public override async Task UpdateAsync(SmsSettings settings, BuildEditorContext context) { @@ -68,15 +73,22 @@ public override async Task UpdateAsync(SmsSettings settings, Bui if (await context.Updater.TryUpdateModelAsync(model, Prefix)) { - if (settings.DefaultProviderName != model.DefaultProvider) + if (string.IsNullOrEmpty(model.DefaultProvider)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultProvider), S["You must select a default provider."]); + } + else { - settings.DefaultProviderName = model.DefaultProvider; + if (settings.DefaultProviderName != model.DefaultProvider) + { + settings.DefaultProviderName = model.DefaultProvider; - await _shellHost.ReleaseShellContextAsync(_shellSettings); + await _shellHost.ReleaseShellContextAsync(_shellSettings); + } } } - return await EditAsync(settings, context); + return Edit(settings); } protected override void BuildPrefix(ISite model, string htmlFieldPrefix) @@ -88,15 +100,4 @@ protected override void BuildPrefix(ISite model, string htmlFieldPrefix) Prefix = htmlFieldPrefix + "." + Prefix; } } - - private SelectListItem[] _providers; - - private SelectListItem[] GetProviders() - { - _providers ??= _smsProviderOptions.Providers.Keys.Select(key => new SelectListItem(key, key)) - .OrderBy(item => item.Text) - .ToArray(); - - return _providers; - } } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs index 61d1c5a42d7..bf710b79d49 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Drivers/TwilioSettingsDisplayDriver.cs @@ -3,11 +3,16 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Localization; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Notify; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Entities; +using OrchardCore.Environment.Shell; using OrchardCore.Mvc.ModelBinding; using OrchardCore.Settings; using OrchardCore.Sms.Models; @@ -22,7 +27,11 @@ public class TwilioSettingsDisplayDriver : SectionDisplayDriver htmlLocalizer, IStringLocalizer stringLocalizer) { _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; _phoneFormatValidator = phoneFormatValidator; _dataProtectionProvider = dataProtectionProvider; + _shellHost = shellHost; + _shellSettings = shellSettings; + _notifier = notifier; + H = htmlLocalizer; S = stringLocalizer; } @@ -43,6 +60,7 @@ public override IDisplayResult Edit(TwilioSettings settings) { return Initialize("TwilioSettings_Edit", model => { + model.IsEnabled = settings.IsEnabled; model.PhoneNumber = settings.PhoneNumber; model.AccountSID = settings.AccountSID; model.HasAuthToken = !string.IsNullOrEmpty(settings.AuthToken); @@ -52,7 +70,7 @@ public override IDisplayResult Edit(TwilioSettings settings) .OnGroup(SmsSettings.GroupId); } - public override async Task UpdateAsync(TwilioSettings settings, BuildEditorContext context) + public override async Task UpdateAsync(ISite site, TwilioSettings settings, IUpdateModel updater, BuildEditorContext context) { var user = _httpContextAccessor.HttpContext?.User; @@ -66,37 +84,71 @@ public override async Task UpdateAsync(TwilioSettings settings, if (await context.Updater.TryUpdateModelAsync(model, Prefix)) { - if (string.IsNullOrWhiteSpace(model.PhoneNumber)) - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Phone number requires a value."]); - } - else if (!_phoneFormatValidator.IsValid(model.PhoneNumber)) - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Please provide a valid phone number."]); - } + var hasChanges = settings.IsEnabled != model.IsEnabled; - if (string.IsNullOrWhiteSpace(model.AccountSID)) + if (!model.IsEnabled) { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.AccountSID), S["Account SID requires a value."]); - } + var smsSettings = site.As(); - if (settings.AuthToken == null && string.IsNullOrWhiteSpace(model.AuthToken)) - { - context.Updater.ModelState.AddModelError(Prefix, nameof(model.AuthToken), S["Auth Token required a value."]); - } + if (hasChanges && smsSettings.DefaultProviderName == TwilioSmsProvider.TechnicalName) + { + await _notifier.WarningAsync(H["You have successfully disabled the default SMS provider. The SMS service is now disable and will remain disabled until you designate a new default provider."]); + + smsSettings.DefaultProviderName = null; - settings.PhoneNumber = model.PhoneNumber; - settings.AccountSID = model.AccountSID; + site.Put(smsSettings); + } - if (!string.IsNullOrWhiteSpace(model.AuthToken)) + settings.IsEnabled = false; + } + else { - var protector = _dataProtectionProvider.CreateProtector(TwilioSmsProvider.ProtectorName); + settings.IsEnabled = true; + + if (string.IsNullOrWhiteSpace(model.PhoneNumber)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Phone number requires a value."]); + } + else if (!_phoneFormatValidator.IsValid(model.PhoneNumber)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.PhoneNumber), S["Please provide a valid phone number."]); + } + + if (string.IsNullOrWhiteSpace(model.AccountSID)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.AccountSID), S["Account SID requires a value."]); + } + + if (settings.AuthToken == null && string.IsNullOrWhiteSpace(model.AuthToken)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.AuthToken), S["Auth Token required a value."]); + } + + // Has change should be evaluated before updating the value. + hasChanges |= settings.PhoneNumber != model.PhoneNumber; + hasChanges |= settings.AccountSID != model.AccountSID; + + settings.PhoneNumber = model.PhoneNumber; + settings.AccountSID = model.AccountSID; + + if (!string.IsNullOrWhiteSpace(model.AuthToken)) + { + var protector = _dataProtectionProvider.CreateProtector(TwilioSmsProvider.ProtectorName); + + var protectedToken = protector.Protect(model.AuthToken); + hasChanges |= settings.AuthToken != protectedToken; + + settings.AuthToken = protectedToken; + } + } - settings.AuthToken = protector.Protect(model.AuthToken); + if (context.Updater.ModelState.IsValid && hasChanges) + { + await _shellHost.ReleaseShellContextAsync(_shellSettings); } } - return await EditAsync(settings, context); + return Edit(settings); } protected override void BuildPrefix(ISite model, string htmlFieldPrefix) diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs index b2c508b7399..79948c20652 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Manifest.cs @@ -13,17 +13,6 @@ Category = "SMS" )] -[assembly: Feature( - Name = "Twilio SMS Provider", - Id = "OrchardCore.Sms.Twilio", - Description = "Provides Twilio SMS services for sending messages to users.", - Category = "SMS", - Dependencies = - [ - "OrchardCore.Sms", - ] -)] - [assembly: Feature( Name = "SMS Notifications", Id = "OrchardCore.Notifications.Sms", diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs index 9b7f948db2f..52f2a1f21c2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Startup.cs @@ -40,10 +40,12 @@ public override void ConfigureServices(IServiceCollection services) if (_hostEnvironment.IsDevelopment()) { - // Add Log provider. services.AddLogSmsProvider(); } + services.AddTwilioSmsProvider() + .AddScoped, TwilioSettingsDisplayDriver>(); + services.AddScoped(); services.AddScoped(); services.AddScoped, SmsSettingsDisplayDriver>(); @@ -52,29 +54,14 @@ public override void ConfigureServices(IServiceCollection services) public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { routes.MapAreaControllerRoute( - name: "SMSTest", + name: "SmsProviderTest", areaName: "OrchardCore.Sms", - pattern: _adminOptions.AdminUrlPrefix + "/SMS/test", + pattern: _adminOptions.AdminUrlPrefix + "/sms/test", defaults: new { controller = typeof(AdminController).ControllerName(), action = nameof(AdminController.Test) } ); } } -[Feature("OrchardCore.Sms.Twilio")] -public class TwilioStartup : StartupBase -{ - public override void ConfigureServices(IServiceCollection services) - { - services.AddHttpClient(TwilioSmsProvider.TechnicalName, client => - { - client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); - }).AddStandardResilienceHandler(); - - services.AddTwilioSmsProvider() - .AddScoped, TwilioSettingsDisplayDriver>(); - } -} - [Feature("OrchardCore.Notifications.Sms")] public class NotificationsStartup : StartupBase { diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/TwilioSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/TwilioSettingsViewModel.cs index 5d538d5d801..f1800c61e97 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/TwilioSettingsViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sms/ViewModels/TwilioSettingsViewModel.cs @@ -4,6 +4,8 @@ namespace OrchardCore.Sms.ViewModels; public class TwilioSettingsViewModel : SmsSettingsBaseViewModel { + public bool IsEnabled { get; set; } + public string PhoneNumber { get; set; } public string AccountSID { get; set; } diff --git a/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml index c37c04b4d1b..7e7aa22dc29 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Sms/Views/TwilioSettings.Edit.cshtml @@ -4,7 +4,15 @@ @model TwilioSettingsViewModel -
+
+
+ + +
+
+ +
+

@T["Twilio Account Info"]

diff --git a/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs b/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs index 877382b40fc..3822def7dae 100644 --- a/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs +++ b/src/OrchardCore/OrchardCore.Sms.Abstractions/SmsProviderOptions.cs @@ -6,26 +6,26 @@ namespace OrchardCore.Sms; public class SmsProviderOptions { - private Dictionary _providers { get; } = []; + private Dictionary _providers { get; } = []; - private FrozenDictionary _readonlyProviders; + private FrozenDictionary _readonlyProviders; /// /// This read-only collections contains all registered SMS providers. /// The 'Key' is the technical name of the provider. /// The 'Value' is the type of the SMS provider. The type will always be an implementation of interface. /// - public IReadOnlyDictionary Providers + public IReadOnlyDictionary Providers => _readonlyProviders ??= _providers.ToFrozenDictionary(x => x.Key, x => x.Value); /// /// Adds a provider if one does not exist. /// /// The technical name of the provider. - /// The type of the provider. + /// The type options of the provider. /// /// - public SmsProviderOptions TryAddProvider(string name, Type type) + public SmsProviderOptions TryAddProvider(string name, SmsProviderTypeOptions options) { if (string.IsNullOrEmpty(name)) { @@ -37,12 +37,7 @@ public SmsProviderOptions TryAddProvider(string name, Type type) return this; } - if (!typeof(ISmsProvider).IsAssignableFrom(type)) - { - throw new ArgumentException($"The type must implement the '{nameof(ISmsProvider)}' interface."); - } - - _providers.Add(name, type); + _providers.Add(name, options); _readonlyProviders = null; return this; @@ -73,13 +68,30 @@ public SmsProviderOptions RemoveProvider(string name) /// Replaces existing or adds a new provider. /// /// The technical name of the provider. - /// The type of the provider. + /// The type-options of the provider. /// /// - public SmsProviderOptions ReplaceProvider(string name, Type type) + public SmsProviderOptions ReplaceProvider(string name, SmsProviderTypeOptions options) { _providers.Remove(name); - return TryAddProvider(name, type); + return TryAddProvider(name, options); } } + +public class SmsProviderTypeOptions +{ + public Type Type { get; } + + public SmsProviderTypeOptions(Type type) + { + if (!typeof(ISmsProvider).IsAssignableFrom(type)) + { + throw new ArgumentException($"The type must implement the '{nameof(ISmsProvider)}' interface."); + } + + Type = type; + } + + public bool IsEnabled { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Models/TwilioSettings.cs b/src/OrchardCore/OrchardCore.Sms.Core/Models/TwilioSettings.cs index 762264620f3..876db74af2c 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Models/TwilioSettings.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Models/TwilioSettings.cs @@ -2,6 +2,8 @@ namespace OrchardCore.Sms.Models; public class TwilioSettings { + public bool IsEnabled { get; set; } + public string PhoneNumber { get; set; } public string AccountSID { get; set; } diff --git a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs index e58f7cb4a71..21a1dc924f0 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -19,18 +20,37 @@ public static IServiceCollection AddSmsServices(this IServiceCollection services public static void AddPhoneFormatValidator(this IServiceCollection services) => services.TryAddScoped(); - public static IServiceCollection AddSmsProvider(this IServiceCollection services, string name) where T : class, ISmsProvider + public static IServiceCollection AddSmsProvider(this IServiceCollection services, string name) + where T : class, ISmsProvider { services.Configure(options => { - options.TryAddProvider(name, typeof(T)); + options.TryAddProvider(name, new SmsProviderTypeOptions(typeof(T)) + { + IsEnabled = true, + }); }); return services; } + public static IServiceCollection AddSmsProviderOptionsConfiguration(this IServiceCollection services) + where TConfiguration : class, IConfigureOptions + { + services.AddTransient, TConfiguration>(); + + return services; + } + public static IServiceCollection AddTwilioSmsProvider(this IServiceCollection services) - => services.AddSmsProvider(TwilioSmsProvider.TechnicalName); + { + services.AddHttpClient(TwilioSmsProvider.TechnicalName, client => + { + client.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/Accounts/"); + }).AddStandardResilienceHandler(); + + return services.AddSmsProviderOptionsConfiguration(); + } public static IServiceCollection AddLogSmsProvider(this IServiceCollection services) => services.AddSmsProvider(LogSmsProvider.TechnicalName); diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs index 9055e0fad2e..aab4b71fcef 100644 --- a/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/DefaultSmsProviderResolver.cs @@ -40,12 +40,12 @@ public async Task GetAsync(string name = null) if (name != null && _smsProviderOptions.Providers.TryGetValue(name, out var providerType)) { - return _serviceProvider.CreateInstance(providerType); + return _serviceProvider.CreateInstance(providerType.Type); } if (string.IsNullOrEmpty(name) && _smsProviderOptions.Providers.Count > 0) { - return _serviceProvider.CreateInstance(_smsProviderOptions.Providers.Values.Last()); + return _serviceProvider.CreateInstance(_smsProviderOptions.Providers.Values.Last().Type); } if (_logger.IsEnabled(LogLevel.Error)) diff --git a/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioProviderOptionsConfigurations.cs b/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioProviderOptionsConfigurations.cs new file mode 100644 index 00000000000..cf027b2ee9d --- /dev/null +++ b/src/OrchardCore/OrchardCore.Sms.Core/Services/TwilioProviderOptionsConfigurations.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Settings; +using OrchardCore.Sms.Models; + +namespace OrchardCore.Sms.Services; + +public class TwilioProviderOptionsConfigurations : IConfigureOptions +{ + private readonly ISiteService _siteService; + + public TwilioProviderOptionsConfigurations(ISiteService siteService) + { + _siteService = siteService; + } + + public void Configure(SmsProviderOptions options) + { + var typeOptions = new SmsProviderTypeOptions(typeof(TwilioSmsProvider)); + + var site = _siteService.GetSiteSettingsAsync().GetAwaiter().GetResult(); + var settings = site.As(); + + typeOptions.IsEnabled = settings.IsEnabled; + + options.TryAddProvider(TwilioSmsProvider.TechnicalName, typeOptions); + } +} diff --git a/src/docs/reference/README.md b/src/docs/reference/README.md index 659e405a56c..7dc3372e4b3 100644 --- a/src/docs/reference/README.md +++ b/src/docs/reference/README.md @@ -120,7 +120,6 @@ Here's a categorized overview of all built-in Orchard Core features at a glance. - [Diagnostics](modules/Diagnostics/README.md) - [Remote Deployment](modules/Deployment.Remote/README.md) - [Sms](modules/Sms/README.md) - - [Twilio](modules/Sms.Twilio/README.md) ### Localization diff --git a/src/docs/reference/modules/Sms.Twilio/README.md b/src/docs/reference/modules/Sms.Twilio/README.md deleted file mode 100644 index a1340226315..00000000000 --- a/src/docs/reference/modules/Sms.Twilio/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# SMS (`OrchardCore.Sms.Twilio`) - -This feature provides the necessary services to send messages using the [Twilio](https://www.twilio.com) service. - -## Twilio Settings - -After enabling the Twilio provider, navigate to `Configurations` >> `Settings` >> `SMS`. Click on the `Twilio` tab, and provider your Twilio account info. Then in the `Providers` tab, select Twilio as your default provider. diff --git a/src/docs/reference/modules/Sms/README.md b/src/docs/reference/modules/Sms/README.md index b37a3b3a19f..482f38a81cc 100644 --- a/src/docs/reference/modules/Sms/README.md +++ b/src/docs/reference/modules/Sms/README.md @@ -4,16 +4,61 @@ This module provides SMS settings configuration. ## SMS Settings -Enabling the `SMS` feature will add a new settings page under `Configurations` >> `Settings` >> `SMS`. You can utilize these settings to set up the default SMS provider configuration. When running in a development mode, we enable `Log` provider to allow you to log the SMS message to the log file for testing. However, for the `SMS` to be production ready, you must enable an SMS provider feature like `OrchardCore.Sms.Twilio`. +Enabling the `SMS` feature will add a new settings page under `Configurations` >> `Settings` >> `SMS`. You can utilize these settings to set up the default SMS provider configuration. The following are the providers that are readily accessible. + +| Provider | Description | +| --- | --- | +| `Log` | This particular provider is exclusively meant for debugging purposes and should never be used in a production environment. It permits the message to be written to the logs. | +| `Twilio` | Opting for this provider enables the utilization of Twilio service for sending SMS messages. Edit the SMS settings to enable this provider. | + +!!! note + After enabling the SMS feature, you must configure the default provider in order to send SMS messages. + +# Configuring Twilio Provider + +To enable the [Twilio](https://www.twilio.com) provider, navigate to `Configurations` >> `Settings` >> `SMS`. Click on the `Twilio` tab, click the Enable checkbox and provider your Twilio account info. Then in the `Providers` tab, select Twilio as your default provider. ## Adding Custom Providers -The `OrchardCore.Sms` module provides you with the capability to integrate additional providers for dispatching SMS messages. To achieve this, you can easily create an implementation of the `ISmsProvider` interface and then proceed to register it using the following approach. +The `OrchardCore.Sms` module provides you with the capability to integrate additional providers for dispatching SMS messages. To achieve this, you can easily create an implementation of the `ISmsProvider` interface and then proceed to register it using one of the following approaches: +If your provider does not require any settings like the `LogProvider`, you may register it like this. ```csharp services.AddSmsProvider("A technical name for your implementation") ``` +However, if you have a complex provider like the Twilio provider, you may implement `IConfigureOptions` and register it using the following extensions + +```csharp +services.AddSmsProviderOptionsConfiguration() +``` + +Here is and example of how we register Twilio complex provider: + +```csharp +public class TwilioProviderOptionsConfigurations : IConfigureOptions +{ + private readonly ISiteService _siteService; + + public TwilioProviderOptionsConfigurations(ISiteService siteService) + { + _siteService = siteService; + } + + public void Configure(SmsProviderOptions options) + { + var typeOptions = new SmsProviderTypeOptions(typeof(TwilioSmsProvider)); + + var site = _siteService.GetSiteSettingsAsync().GetAwaiter().GetResult(); + var settings = site.As(); + + typeOptions.IsEnabled = settings.IsEnabled; + + options.TryAddProvider(TwilioSmsProvider.TechnicalName, typeOptions); + } +} +``` + ## Sending SMS Message An SMS message can be send by injecting `ISmsService` and invoke the `SendAsync` method. For instance